@preventive/triage 1.0.0-alpha.44 → 1.0.0-alpha.46
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/client/linked-findings.js +3 -0
- package/common/bundle-metadata.d.ts +1 -0
- package/common/bundle-metadata.js +1 -1
- package/common/bundle-sources.js +25 -2
- package/common/default-scan-models.ts +1 -1
- package/common/dependency-reviews.d.ts +0 -5
- package/common/dependency-reviews.js +0 -17
- package/common/line-diff.js +5 -4
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/github-agents.ts +92 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/roles.ts +7 -0
- package/common/managed/routes.js +88 -7
- package/common/managed/team-app.d.ts +5 -2
- package/common/managed/team-app.js +2 -2
- package/common/server-info.ts +3 -0
- package/common/workspace-app.js +6 -3
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2XUEZHIP.js +18 -18
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-3PS4KWD2.js +10 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-4UFAMJAX.js +1 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7XRR4OFU.js +1 -0
- package/out/chunk-BC2UCLFJ.js +1 -0
- package/out/chunk-BOIXMUU5.js +3 -0
- package/out/chunk-C4Z6ZBXV.js +6 -6
- package/out/chunk-CC4OM6TZ.js +3 -0
- package/out/chunk-G24A42V6.js +2 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-I2MDGSQX.js +3 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -1
- package/out/chunk-JKGQ3O7O.js +6 -0
- package/out/chunk-LE5ZWLG4.js +235 -0
- package/out/chunk-MSION3G4.js +36 -0
- package/out/chunk-NRPC74AM.js +1 -1
- package/out/chunk-OKRLLE5I.js +17 -0
- package/out/chunk-OOJM4CTU.js +1 -1
- package/out/chunk-PNHBZGDD.js +1 -1
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +1 -1
- package/out/chunk-S6GIXWQW.js +52 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TUII34K2.js +1 -1
- package/out/chunk-V3KWNNMY.js +1 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WQ74ZBZF.js +7 -0
- package/out/client-managed-import.js +3 -3
- package/out/client-managed.js +31 -35
- package/out/client-sync.js +11 -11
- package/out/compare.js +35 -0
- package/out/dialogs.js +51 -0
- package/out/github-issue-authorized.html +1 -1
- package/out/github-issue-authorized.js +1 -1
- package/out/graph.js +17 -17
- package/out/index.html +30 -34
- package/out/prism.js +14 -14
- package/out/scan.js +2 -0
- package/out/terminal.js +294 -293
- package/out/view.css +1 -1
- package/out/view.js +16 -107
- package/package.json +53 -6
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/hosts.js +6 -6
- package/risk/files/readability.js +23 -17
- package/risk/files/runs-code.js +18 -6
- package/risk/files/script.js +7 -3
- package/risk/files/stamps.js +8 -7
- package/risk/index.d.ts +71 -1
- package/risk/index.js +6 -2
- package/risk/npm/assess.js +1 -1
- package/risk/npm/compare.js +34 -10
- package/risk/npm/files.js +413 -2
- package/risk/npm/tarball.js +56 -73
- package/server-e2e/app.ts +5 -4
- package/server-e2e/http.ts +5 -2
- package/server-managed/agent-merges.ts +198 -0
- package/server-managed/app-agents.ts +30 -0
- package/server-managed/avatar-store.ts +1 -0
- package/server-managed/bundle-cache.ts +7 -1
- package/server-managed/combined.ts +3 -2
- package/server-managed/commit-coauthors.ts +83 -0
- package/server-managed/commit-lockfiles.ts +72 -0
- package/server-managed/config.ts +6 -0
- package/server-managed/db-methods.ts +66 -19
- package/server-managed/db-neon.ts +89 -21
- package/server-managed/db-schema.ts +13 -2
- package/server-managed/db.ts +9 -1
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +97 -0
- package/server-managed/dependency-reviews.ts +78 -18
- package/server-managed/github-accounts.ts +33 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +33 -13
- package/server-managed/github-errors.ts +2 -0
- package/server-managed/github-issue-links.ts +11 -5
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-oauth.ts +5 -2
- package/server-managed/github-pulls.ts +2 -2
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +10 -2
- package/server-managed/http.ts +1251 -97
- package/server-managed/index.ts +13 -8
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/locked-packages.ts +37 -16
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +178 -0
- package/server-managed/npm-insights.ts +20 -11
- package/server-managed/npm-kept.ts +83 -6
- package/server-managed/npm-loads.ts +114 -37
- package/server-managed/npm-packages.ts +110 -64
- package/server-managed/npm-risk.ts +4 -1
- package/server-managed/pretty-print.ts +67 -23
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +300 -0
- package/server-managed/pull-dependency-reviews.ts +318 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +16 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff.ts +22 -13
- package/server-managed/report-sources.ts +1 -1
- package/server-managed/report-store.ts +42 -9
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +220 -0
- package/server-managed/repository-browser.ts +166 -37
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +52 -0
- package/server-managed/repository-dependency-reviews.ts +157 -0
- package/server-managed/repository-history.ts +461 -0
- package/server-managed/repository-issues.ts +181 -0
- package/server-managed/repository-pulse.ts +439 -0
- package/server-managed/repository-risk-audit.ts +13 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +133 -118
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/session.ts +27 -9
- package/server-managed/sql.ts +6 -3
- package/server-managed/storage-stores.ts +5 -0
- package/server-managed/storage.ts +4 -2
- package/server-managed/team-app.ts +19 -3
- package/server-managed/team-reports.ts +24 -12
- package/server-managed/upstream-cache.ts +12 -0
- package/server-managed/workspace-shares.ts +4 -1
- package/server.ts +25 -5
- package/out/chunk-2ABLKCOY.js +0 -46
- package/out/chunk-2KBF5TZ7.js +0 -2
- package/out/chunk-35VE6CDH.js +0 -2
- package/out/chunk-3BZ7JERB.js +0 -1
- package/out/chunk-4XCDFGDP.js +0 -10
- package/out/chunk-5LSXOLUS.js +0 -1
- package/out/chunk-5RCM2J63.js +0 -1
- package/out/chunk-7B6HB4D7.js +0 -1
- package/out/chunk-AJ4GTATZ.js +0 -3
- package/out/chunk-CH4UN6WK.js +0 -225
- package/out/chunk-CMQA4DM6.js +0 -1
- package/out/chunk-CVVJAXVY.js +0 -2
- package/out/chunk-DIJJNTRH.js +0 -10
- package/out/chunk-EDKLBQQN.js +0 -17
- package/out/chunk-F47DXCEV.js +0 -33
- package/out/chunk-FK3DZEKL.js +0 -1
- package/out/chunk-G6AJWENO.js +0 -1
- package/out/chunk-GJ4IUCNS.js +0 -1
- package/out/chunk-GNXOKIGP.js +0 -80
- package/out/chunk-GZ4MS3TE.js +0 -13
- package/out/chunk-GZWM7UQM.js +0 -2
- package/out/chunk-IU3CCINR.js +0 -3
- package/out/chunk-K3X5EJBQ.js +0 -2
- package/out/chunk-KEX2ATEW.js +0 -2
- package/out/chunk-KYCNJVB7.js +0 -1
- package/out/chunk-LPCTJYNA.js +0 -7
- package/out/chunk-O4RJY5TP.js +0 -2
- package/out/chunk-OD4LPN3H.js +0 -1
- package/out/chunk-QWDWTBM7.js +0 -1
- package/out/chunk-QYWKC557.js +0 -1
- package/out/chunk-R75XOKUP.js +0 -1
- package/out/chunk-SWTLADW5.js +0 -1
- package/out/chunk-T7OKH4VW.js +0 -1
- package/out/chunk-TZ5KB2Y5.js +0 -10
- package/out/chunk-UNTRGE26.js +0 -2
- package/out/chunk-UTR2LVME.js +0 -2
- package/out/chunk-UYAXVRVL.js +0 -1
- package/out/chunk-V3ME6FI5.js +0 -2
- package/out/chunk-V5O7M2YT.js +0 -1
- package/out/chunk-VIHGO77Y.js +0 -1
- package/out/chunk-W4J3MGLB.js +0 -1
- package/out/chunk-WGG7KQUH.js +0 -1
- package/out/chunk-XYFXXB2G.js +0 -10
- package/out/chunk-ZESLQCTF.js +0 -6
|
@@ -4,16 +4,22 @@
|
|
|
4
4
|
// instance: public versions' documents, of the fields read of them
|
|
5
5
|
// (npm-manifest.ts npmVersionFields), for a day (readNpmVersion asks npm again
|
|
6
6
|
// then, as a version can be deprecated, its package made private, or itself
|
|
7
|
-
// removed)
|
|
7
|
+
// removed), and when each was published, for a month (npmPublishTimeStore);
|
|
8
|
+
// and, for scans, each package's risk line, for as long as a scan
|
|
8
9
|
// keeps it, and what a scan read of GitHub, for a day, and of npm's
|
|
9
|
-
// advisories, for an hour (repository-risk.ts)
|
|
10
|
+
// advisories, for an hour (repository-risk.ts); and what the code viewer read
|
|
11
|
+
// of a repository's GitHub, its issues, pull requests and agents' work, for a
|
|
12
|
+
// day (http.ts keptAnswer), and its history, a page from a commit at a time,
|
|
13
|
+
// for a month (repository-history.ts historyFrom), as are the files each of
|
|
14
|
+
// its commits changes and holds (repository-changes.ts), and the packages a
|
|
15
|
+
// pull request's lockfiles add (pull-dependencies.ts). Each store removes its own rows kept longer
|
|
10
16
|
// than that as it writes, at most once an hour a database (pruneKept), so
|
|
11
17
|
// that rows no read will take again don't pile up.
|
|
12
18
|
// The database is an optimization: a failure reads as a miss, and writes
|
|
13
19
|
// nothing.
|
|
14
20
|
import { createHash } from 'node:crypto'
|
|
15
21
|
import { plainObject } from './npm-manifest.ts'
|
|
16
|
-
import { type NpmVersionStore, VERSION_KEPT_MS } from './npm-packages.ts'
|
|
22
|
+
import { type NpmPublishTimeStore, type NpmVersionStore, VERSION_KEPT_MS } from './npm-packages.ts'
|
|
17
23
|
import type { UpstreamCacheStore } from './upstream-cache.ts'
|
|
18
24
|
|
|
19
25
|
// A value kept by key, as JSON, `at` when it was.
|
|
@@ -21,7 +27,12 @@ export interface KeptStore<T> {
|
|
|
21
27
|
read(key: string): Promise<{ at: number; value: T } | null>
|
|
22
28
|
// Those of `keys` kept, by key, in one read of the database.
|
|
23
29
|
readMany(keys: string[]): Promise<Map<string, { at: number; value: T }>>
|
|
30
|
+
// Those of `first` kept; where none is, those of `then`, in one read of the
|
|
31
|
+
// database (`first` whether they are `first`'s).
|
|
32
|
+
readFirst(first: string[], then: string[]): Promise<{ first: boolean; found: Map<string, { at: number; value: T }> }>
|
|
24
33
|
write(key: string, value: T, at: number): Promise<void>
|
|
34
|
+
// Removes what is kept by `key`.
|
|
35
|
+
remove(key: string): Promise<void>
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
// When each database last had the rows under each prefix removed.
|
|
@@ -59,10 +70,20 @@ function keptStore<T>(db: UpstreamCacheStore, type: string, keptMs: number, debu
|
|
|
59
70
|
return new Map(keys.flatMap(key => { const kept = valueOf(rows.get(keyOf(key))); return kept ? [[key, kept] as const] : [] }))
|
|
60
71
|
} catch (error) { warn('read', error); return new Map() }
|
|
61
72
|
},
|
|
73
|
+
async readFirst(first, then) {
|
|
74
|
+
try {
|
|
75
|
+
const { first: answered, entries } = await db.getFirstUpstreamCacheEntries(first.map(keyOf), then.map(keyOf))
|
|
76
|
+
const asked = answered ? first : then
|
|
77
|
+
return { first: answered, found: new Map(asked.flatMap(key => { const kept = valueOf(entries.get(keyOf(key))); return kept ? [[key, kept] as const] : [] })) }
|
|
78
|
+
} catch (error) { warn('read', error); return { first: false, found: new Map() } }
|
|
79
|
+
},
|
|
62
80
|
async write(key, value, at) {
|
|
63
81
|
try { await db.setUpstreamCacheEntry(keyOf(key), JSON.stringify({ at, value }), at) } catch (error) { warn('write', error) }
|
|
64
82
|
await prune(at)
|
|
65
83
|
},
|
|
84
|
+
async remove(key) {
|
|
85
|
+
try { await db.deleteUpstreamCacheEntries(keyOf(key), Number.MAX_SAFE_INTEGER) } catch (error) { warn('remove', error) }
|
|
86
|
+
},
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
|
|
@@ -73,17 +94,73 @@ export function npmVersionStore(db: UpstreamCacheStore, debug = false): NpmVersi
|
|
|
73
94
|
const hit = await kept.read(`${name}@${version}`)
|
|
74
95
|
return hit && plainObject(hit.value) ? { at: hit.at, json: hit.value } : null
|
|
75
96
|
},
|
|
97
|
+
async readMany(versions) {
|
|
98
|
+
const hits = await kept.readMany(versions.map(({ name, version }) => `${name}@${version}`))
|
|
99
|
+
return new Map([...hits].flatMap(([key, hit]) => plainObject(hit.value) ? [[key, { at: hit.at, json: hit.value }] as const] : []))
|
|
100
|
+
},
|
|
76
101
|
write: (name, version, json) => kept.write(`${name}@${version}`, json, Date.now()),
|
|
77
102
|
}
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
// When public versions were published (npm-packages.ts npmPublishTime), which
|
|
106
|
+
// never changes once they are: kept a month, and kept again by the next view
|
|
107
|
+
// of the version past it.
|
|
108
|
+
const PUBLISH_TIME_KEPT_MS = 30 * 24 * 60 * 60_000
|
|
109
|
+
export function npmPublishTimeStore(db: UpstreamCacheStore, debug = false): NpmPublishTimeStore {
|
|
110
|
+
const kept = keptStore<string>(db, 'npm/publish-times', PUBLISH_TIME_KEPT_MS, debug)
|
|
111
|
+
return {
|
|
112
|
+
async read(name, version) {
|
|
113
|
+
const hit = await kept.read(`${name}@${version}`)
|
|
114
|
+
return typeof hit?.value === 'string' ? hit.value : null
|
|
115
|
+
},
|
|
116
|
+
write: (name, version, at) => kept.write(`${name}@${version}`, at, Date.now()),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
80
120
|
// Risk lines, each kept `keptMs`, for good where Infinity, a scan telling one
|
|
81
121
|
// past RISK_KEPT_MS as stale (repository-risk.ts).
|
|
82
122
|
export const riskLineStore = <T>(db: UpstreamCacheStore, keptMs: number, debug = false): KeptStore<T> => keptStore<T>(db, 'repository-risk/lines', keptMs, debug)
|
|
83
123
|
// What a scan read, each read for keptMs (repository-risk.ts): of GitHub, a
|
|
84
124
|
// repository's lockfiles as listed at a commit (`listings`) and a lockfile's
|
|
85
125
|
// packages by its blob (`lockfiles`), for SCAN_READ_KEPT_MS; of npm, a
|
|
86
|
-
// lockfile's advisories by its blob (`advisories`), for RISK_KEPT_MS;
|
|
87
|
-
// repositories a scan in parts told whole (`progress`)
|
|
88
|
-
|
|
126
|
+
// lockfile's advisories by its blob (`advisories`), for RISK_KEPT_MS; the
|
|
127
|
+
// repositories a scan in parts told whole (`progress`); a repository's
|
|
128
|
+
// Actions' files by the commit read (`action-files`), and the actions and
|
|
129
|
+
// reusable workflows of others they name, each resolved (`actions`), of
|
|
130
|
+
// public repositories alone (repository-actions.ts).
|
|
131
|
+
export const scanReadStore = <T>(db: UpstreamCacheStore, type: 'listings' | 'lockfiles' | 'advisories' | 'progress' | 'action-files' | 'actions', keptMs: number, debug = false): KeptStore<T> =>
|
|
89
132
|
keptStore<T>(db, `repository-risk/${type}`, keptMs, debug)
|
|
133
|
+
// What the code viewer read of a repository (http.ts readWholeRepository), by
|
|
134
|
+
// its id and what was asked, each kept REPOSITORY_READ_KEPT_MS.
|
|
135
|
+
export const REPOSITORY_READ_KEPT_MS = 24 * 60 * 60_000
|
|
136
|
+
export const repositoryReadStore = (db: UpstreamCacheStore, debug = false): KeptStore<object> =>
|
|
137
|
+
keptStore<object>(db, 'repository/reads', REPOSITORY_READ_KEPT_MS, debug)
|
|
138
|
+
// Pages of a repository's history (repository-history.ts historyFrom), by its
|
|
139
|
+
// id and the commit each starts at, which never change: kept a month after
|
|
140
|
+
// they were last read from GitHub.
|
|
141
|
+
export const HISTORY_PAGE_KEPT_MS = 30 * 24 * 60 * 60_000
|
|
142
|
+
export const historyPageStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/history', HISTORY_PAGE_KEPT_MS, debug)
|
|
143
|
+
// The pull requests squashed into a repository's commits, by the commit
|
|
144
|
+
// (repository-history.ts squashedPulls): kept as its history's pages are.
|
|
145
|
+
export const historySquashStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/squashes', HISTORY_PAGE_KEPT_MS, debug)
|
|
146
|
+
// The files each of a repository's commits changes (repository-changes.ts
|
|
147
|
+
// repositoryChanges), by the commit, which never change: kept as its
|
|
148
|
+
// history's pages are.
|
|
149
|
+
export const historyChangeStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/changes', HISTORY_PAGE_KEPT_MS, debug)
|
|
150
|
+
// The files a repository's commit holds (repository-changes.ts
|
|
151
|
+
// repositoryTree), by the commit, which never change: kept as its history's
|
|
152
|
+
// pages are.
|
|
153
|
+
export const historyTreeStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/trees', HISTORY_PAGE_KEPT_MS, debug)
|
|
154
|
+
// The packages a repository's pull request's lockfiles add
|
|
155
|
+
// (pull-dependencies.ts), by the commits of its base and head, which never
|
|
156
|
+
// change: kept as its history's pages are.
|
|
157
|
+
export const pullDependencyStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/pull-dependencies', HISTORY_PAGE_KEPT_MS, debug)
|
|
158
|
+
// The versions a repository's lockfiles locked at the commit its dependencies
|
|
159
|
+
// are reviewed since (pull-dependency-reviews.ts reviewBaseline), by the
|
|
160
|
+
// commit, which never change: kept as its history's pages are, never stored
|
|
161
|
+
// as reviews.
|
|
162
|
+
// The lines a version changes against another, or none (npm-delta.ts), by
|
|
163
|
+
// both sides' integrities, each kept HISTORY_PAGE_KEPT_MS.
|
|
164
|
+
export const npmDeltaStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'npm/deltas', HISTORY_PAGE_KEPT_MS, debug)
|
|
165
|
+
export const reviewBaselineStore = (db: UpstreamCacheStore, debug = false): KeptStore<{ versions: string[]; unread: string[] }> =>
|
|
166
|
+
keptStore<{ versions: string[]; unread: string[] }>(db, 'repository/review-baselines', HISTORY_PAGE_KEPT_MS, debug)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Loading a published version for the npm package viewer (npm-packages.ts):
|
|
2
2
|
// its tarball, kept on disk as bundle builds keep theirs, else read from the
|
|
3
|
-
// registry, and the viewer's response built from
|
|
4
|
-
// readers asking for it meanwhile and held to a few
|
|
3
|
+
// registry, its files unpacked from it, and the viewer's response built from
|
|
4
|
+
// them, each shared by the readers asking for it meanwhile and held to a few
|
|
5
|
+
// versions at once per process; its files kept a little while after.
|
|
5
6
|
import { Buffer } from 'node:buffer'
|
|
6
7
|
import { homedir } from 'node:os'
|
|
7
8
|
import { join, resolve } from 'node:path'
|
|
8
|
-
import {
|
|
9
|
-
import { gunzip } from 'node:zlib'
|
|
9
|
+
import { CompressionError, decompress } from '@preventive/archive/compression.js'
|
|
10
10
|
import { encodeBrotli } from './brotli.ts'
|
|
11
|
+
import { keptBriefly } from './kept-briefly.ts'
|
|
11
12
|
import { MAX_NPM_PACKAGE_BYTES, MAX_NPM_PACKAGE_FILES, NPM_REGISTRY, NpmPackageError, type NpmPackageFile, type NpmVersionDocument, askUpstream,
|
|
12
13
|
npmFileRows, npmPublishTime, npmToken, readNpmTar } from './npm-packages.ts'
|
|
13
14
|
import { keepFile, readKeptFile, upstreamCachePath } from './upstream-cache.ts'
|
|
@@ -17,22 +18,30 @@ import { keepFile, readKeptFile, upstreamCachePath } from './upstream-cache.ts'
|
|
|
17
18
|
// arrives.
|
|
18
19
|
export const MAX_TAR_BYTES = 96 * 1024 * 1024
|
|
19
20
|
const TARBALL_TIMEOUT_MS = 120_000
|
|
20
|
-
// Loads in flight per process
|
|
21
|
-
// and
|
|
21
|
+
// Loads in flight per process, those of one version (its tarball, its files,
|
|
22
|
+
// its response) as one, as they read its tarball and unpack it once between
|
|
23
|
+
// them; each can hold a tarball, its tar, its files and their encoded JSON.
|
|
22
24
|
const MAX_ACTIVE_LOADS = 4
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
// How long a public version's files are kept once unpacked, and what they
|
|
26
|
+
// may take between them, the least lately read going first past it:
|
|
27
|
+
// Compare's requests for a pair of versions, and the Code tab's, come one
|
|
28
|
+
// after another, each loading the same files. A private version's are never
|
|
29
|
+
// kept, as nothing else of it is past its readers.
|
|
30
|
+
const UNPACKED_KEPT_MS = 60_000
|
|
31
|
+
const MAX_UNPACKED_BYTES = 128 * 1024 * 1024
|
|
25
32
|
|
|
26
33
|
// A load's result, shared by its readers, and a reader's release of it.
|
|
27
34
|
export interface NpmLoad<T> { result: Promise<T>; release: () => void }
|
|
28
35
|
|
|
29
|
-
interface Load { job: Promise<unknown>; readers: number; settled: boolean }
|
|
36
|
+
interface Load { job: Promise<unknown>; readers: number; settled: boolean; slot: string }
|
|
30
37
|
|
|
31
38
|
// Loads by what they read; readers asking for the same share one. A load
|
|
32
39
|
// keeps its place among the active ones until its work is done and each
|
|
33
40
|
// reader has released it, as a reader does once its response is written or
|
|
34
|
-
// abandoned, so what responses still hold counts too.
|
|
41
|
+
// abandoned, so what responses still hold counts too. Places by the slot
|
|
42
|
+
// their loads share, with how many hold each.
|
|
35
43
|
const loads = new Map<string, Load>()
|
|
44
|
+
const slots = new Map<string, number>()
|
|
36
45
|
|
|
37
46
|
const refused = <T>(status: number, code: string): NpmLoad<T> => {
|
|
38
47
|
const result = Promise.reject(new NpmPackageError(status, code))
|
|
@@ -40,19 +49,30 @@ const refused = <T>(status: number, code: string): NpmLoad<T> => {
|
|
|
40
49
|
return { result, release() {} }
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
// What names a version's tarball, and so its files: its integrity and URL,
|
|
53
|
+
// and whether it is read with the token.
|
|
54
|
+
const tarballKey = (doc: NpmVersionDocument) => `${doc.dist.integrity} ${doc.dist.tarball} ${doc.private}`
|
|
55
|
+
|
|
56
|
+
function shared<T>(what: string, doc: NpmVersionDocument, work: () => Promise<T>): NpmLoad<T> {
|
|
44
57
|
const { unpackedSize, fileCount } = doc.dist
|
|
45
58
|
if ((unpackedSize ?? 0) > MAX_NPM_PACKAGE_BYTES || (fileCount ?? 0) > MAX_NPM_PACKAGE_FILES) return refused(413, 'package-too-large')
|
|
59
|
+
const slot = tarballKey(doc)
|
|
60
|
+
return sharedLoad(`${what} ${slot}`, work, 'npm-busy', slot)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A load among the active ones, by `key`, refused as `busy` past them: a
|
|
64
|
+
// repository directory's (repository-code.ts) as a package's, each holding
|
|
65
|
+
// alike a tarball, its tar, its files and their encoded JSON. Loads of one
|
|
66
|
+
// `slot` take one place between them.
|
|
67
|
+
export function sharedLoad<T>(key: string, work: () => Promise<T>, busy: string, slot = key): NpmLoad<T> {
|
|
46
68
|
let load = loads.get(key)
|
|
47
69
|
if (!load) {
|
|
48
|
-
if (
|
|
49
|
-
const created: Load = { job: work(), readers: 0, settled: false }
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (created.readers === 0 && loads.get(key) === created) loads.delete(key)
|
|
53
|
-
}
|
|
54
|
-
created.job.then(settle, settle)
|
|
70
|
+
if (!slots.has(slot) && slots.size >= MAX_ACTIVE_LOADS) return refused(429, busy)
|
|
71
|
+
const created: Load = { job: work(), readers: 0, settled: false, slot }
|
|
72
|
+
const finish = () => { settle(key, created) }
|
|
73
|
+
created.job.then(finish, finish)
|
|
55
74
|
loads.set(key, created)
|
|
75
|
+
slots.set(slot, (slots.get(slot) ?? 0) + 1)
|
|
56
76
|
load = created
|
|
57
77
|
}
|
|
58
78
|
const held = load
|
|
@@ -63,11 +83,27 @@ function shared<T>(key: string, doc: NpmVersionDocument, work: () => Promise<T>)
|
|
|
63
83
|
release() {
|
|
64
84
|
if (released) return
|
|
65
85
|
released = true
|
|
66
|
-
|
|
86
|
+
held.readers--
|
|
87
|
+
done(key, held)
|
|
67
88
|
},
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
|
|
92
|
+
function settle(key: string, load: Load) {
|
|
93
|
+
load.settled = true
|
|
94
|
+
done(key, load)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A load gone from the active ones, and its place with it, once its work is
|
|
98
|
+
// done and its readers have released it.
|
|
99
|
+
function done(key: string, load: Load) {
|
|
100
|
+
if (load.readers !== 0 || !load.settled || loads.get(key) !== load) return
|
|
101
|
+
loads.delete(key)
|
|
102
|
+
const holding = slots.get(load.slot)! - 1
|
|
103
|
+
if (holding === 0) slots.delete(load.slot)
|
|
104
|
+
else slots.set(load.slot, holding)
|
|
105
|
+
}
|
|
106
|
+
|
|
71
107
|
// The sha512 an integrity names, in base64, or null where it names none.
|
|
72
108
|
const sha512Of = (integrity: string) => /(?:^|\s)sha512-([\d+/A-Za-z]{86}==)(?=\s|$)/u.exec(integrity)?.[1] ?? null
|
|
73
109
|
// Hashed off the event loop: a tarball runs to tens of MiB.
|
|
@@ -125,43 +161,84 @@ async function readTarball(doc: NpmVersionDocument): Promise<Uint8Array> {
|
|
|
125
161
|
return bytes
|
|
126
162
|
}
|
|
127
163
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
164
|
+
// Tarballs being read, and versions' files being unpacked, by what names
|
|
165
|
+
// them (tarballKey): readers asking for one meanwhile share it, its bytes
|
|
166
|
+
// those each one's own document names.
|
|
167
|
+
const reading = new Map<string, Promise<Uint8Array>>()
|
|
168
|
+
const unpacking = new Map<string, Promise<Unpacked>>()
|
|
169
|
+
|
|
170
|
+
function sharedRead<T>(pending: Map<string, Promise<T>>, key: string, read: () => Promise<T>): Promise<T> {
|
|
171
|
+
let job = pending.get(key)
|
|
172
|
+
if (!job) {
|
|
173
|
+
const started = read().finally(() => { if (pending.get(key) === started) pending.delete(key) })
|
|
174
|
+
pending.set(key, job = started)
|
|
175
|
+
}
|
|
176
|
+
return job
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sharedTarball = (doc: NpmVersionDocument) => sharedRead(reading, tarballKey(doc), () => readTarball(doc))
|
|
180
|
+
|
|
181
|
+
// A version's files, and the size of the tarball they came in.
|
|
182
|
+
interface Unpacked { files: NpmPackageFile[]; tarballSize: number }
|
|
183
|
+
|
|
184
|
+
// Public versions' files lately unpacked, by tarballKey.
|
|
185
|
+
const unpacked = keptBriefly<Unpacked>({ keptMs: UNPACKED_KEPT_MS, maxBytes: MAX_UNPACKED_BYTES })
|
|
186
|
+
|
|
187
|
+
// The version's files, kept from a load of them a little while since, else
|
|
188
|
+
// unpacked from its tarball (and kept, a public version's): bytes its
|
|
189
|
+
// reader's own document names, as its tarball's are.
|
|
190
|
+
function unpackedOf(doc: NpmVersionDocument): Promise<Unpacked> {
|
|
191
|
+
const key = tarballKey(doc)
|
|
192
|
+
const kept = unpacked.get(key)
|
|
193
|
+
if (kept) return Promise.resolve(kept)
|
|
194
|
+
return sharedRead(unpacking, key, async () => {
|
|
195
|
+
const tarball = await sharedTarball(doc)
|
|
196
|
+
const value = { files: await readTarballFiles(tarball), tarballSize: tarball.byteLength }
|
|
197
|
+
if (!doc.private) unpacked.keep(key, value, value.files.reduce((sum, file) => sum + file.path.length + file.bytes.byteLength, 0))
|
|
198
|
+
return value
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A gzipped tarball's files, its top directory left out, as npm extracts
|
|
203
|
+
// a package's: a repository tree's as GitHub archives it, too, its symlinks
|
|
204
|
+
// let climb as readNpmTar's `symlinksAbove` says.
|
|
205
|
+
export async function readTarballFiles(tarball: Uint8Array, options: { symlinksAbove?: number } = {}): Promise<NpmPackageFile[]> {
|
|
206
|
+
let tar: Uint8Array
|
|
207
|
+
try { tar = await decompress(tarball, 'gzip', { limit: MAX_TAR_BYTES }) }
|
|
131
208
|
catch (err) {
|
|
132
|
-
|
|
133
|
-
|
|
209
|
+
const limited = err instanceof CompressionError && err.limited
|
|
210
|
+
throw new NpmPackageError(limited ? 413 : 422, limited ? 'package-too-large' : 'bad-tarball')
|
|
134
211
|
}
|
|
135
|
-
return readNpmTar(tar)
|
|
212
|
+
return readNpmTar(tar, options)
|
|
136
213
|
}
|
|
137
214
|
|
|
138
215
|
// The version's tarball, for download.
|
|
139
216
|
export function loadNpmTarball(doc: NpmVersionDocument): NpmLoad<Uint8Array> {
|
|
140
|
-
return shared(
|
|
217
|
+
return shared('tarball', doc, () => sharedTarball(doc))
|
|
141
218
|
}
|
|
142
219
|
|
|
143
220
|
// The version's files, for one of them to be pretty-printed (pretty-print.ts).
|
|
144
221
|
export function loadNpmFiles(doc: NpmVersionDocument): NpmLoad<NpmPackageFile[]> {
|
|
145
|
-
return shared(
|
|
222
|
+
return shared('files', doc, async () => (await unpackedOf(doc)).files)
|
|
146
223
|
}
|
|
147
224
|
|
|
148
225
|
// The version as the viewer reads it, as brotli-encoded JSON: `{ name,
|
|
149
226
|
// version, private, integrity, tarballSize, published, manifest, files,
|
|
150
227
|
// known }`, its files as npmFileRows gives them, and with them, as its files
|
|
151
|
-
// are encoded, when it was published where that is kept (
|
|
152
|
-
// else null) and what `known` gives: what is kept about its package
|
|
228
|
+
// are encoded, when it was published where that is kept (`published`, asked
|
|
229
|
+
// then, else null) and what `known` gives: what is kept about its package
|
|
153
230
|
// (npm-insights.ts npmKnown), which the viewer need not then ask for. Encoding
|
|
154
231
|
// is where a load holds the most, so it takes its place among the active
|
|
155
232
|
// loads until its body is done, and readers asking meanwhile share that body,
|
|
156
|
-
// the first one's manifest and `known` in it;
|
|
157
|
-
// are done.
|
|
158
|
-
export function loadNpmPackageBody(doc: NpmVersionDocument, known: () => object = () => ({})
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const files = await
|
|
233
|
+
// the first one's manifest, `published` and `known` in it; the body is not
|
|
234
|
+
// kept once its readers are done, its files a little while (unpackedOf).
|
|
235
|
+
export function loadNpmPackageBody(doc: NpmVersionDocument, known: () => object = () => ({}),
|
|
236
|
+
published: () => Promise<string | null> = npmPublishTime(doc).read): NpmLoad<Buffer> {
|
|
237
|
+
return shared('package', doc, async () => {
|
|
238
|
+
const { files, tarballSize } = await unpackedOf(doc)
|
|
162
239
|
return encodeBrotli(Buffer.from(JSON.stringify({
|
|
163
|
-
name: doc.name, version: doc.version, private: doc.private, integrity: doc.dist.integrity, tarballSize
|
|
164
|
-
published:
|
|
240
|
+
name: doc.name, version: doc.version, private: doc.private, integrity: doc.dist.integrity, tarballSize,
|
|
241
|
+
published: await published(), manifest: doc.manifest, files: npmFileRows(files), known: known(),
|
|
165
242
|
})))
|
|
166
243
|
})
|
|
167
244
|
}
|
|
@@ -14,6 +14,7 @@ import { Buffer } from 'node:buffer'
|
|
|
14
14
|
import { createHash } from 'node:crypto'
|
|
15
15
|
import { rm } from 'node:fs/promises'
|
|
16
16
|
import { resolve } from 'node:path'
|
|
17
|
+
import { ArchiveError, type Entry, unpack } from '@preventive/archive/tar.js'
|
|
17
18
|
import { isExactVersion } from '@preventive/upstream/semver.js'
|
|
18
19
|
import { isNpmPackageName, isNpmPackageSpec, npmPackageScope } from '../common/managed/npm-packages.js'
|
|
19
20
|
import type { Role } from '../common/managed/roles.ts'
|
|
@@ -33,6 +34,9 @@ const MAX_DOCUMENT_BYTES = 4 * PACKUMENT_BYTES
|
|
|
33
34
|
// stream holding them is bounded in npm-loads.ts.
|
|
34
35
|
export const MAX_NPM_PACKAGE_BYTES = 64 * 1024 * 1024
|
|
35
36
|
export const MAX_NPM_PACKAGE_FILES = 20_000
|
|
37
|
+
// The entries its tar stream may hold, read before any is counted as a file:
|
|
38
|
+
// a directory's too, as a GitHub tree's tarball has one for each, npm's none.
|
|
39
|
+
export const MAX_NPM_TAR_ENTRIES = 2 * MAX_NPM_PACKAGE_FILES
|
|
36
40
|
// What the files may take as the viewer's JSON. Escaping grows a text up to
|
|
37
41
|
// six times (a control character becomes `\u0001`), so it is counted before
|
|
38
42
|
// anything is serialized.
|
|
@@ -149,14 +153,60 @@ async function publicWholeDocument(name: string, url: string, { limit, signal }:
|
|
|
149
153
|
return json
|
|
150
154
|
}
|
|
151
155
|
|
|
156
|
+
// Whole documents being asked of npm as anyone asks, by URL and limit:
|
|
157
|
+
// readers asking for one meanwhile share it, as opening a version asks for
|
|
158
|
+
// its package's for its versions, its advisories and its risk at once; each
|
|
159
|
+
// gives up on it alone once its own signal aborts, and it is given up on once
|
|
160
|
+
// they all have. None is asked with the token. Read alike by every reader,
|
|
161
|
+
// none changing it.
|
|
162
|
+
const wholeAsks = new Map<string, { asked: Promise<Record<string, unknown> | null>; controller: AbortController; readers: number }>()
|
|
163
|
+
|
|
164
|
+
function sharedWholeDocument(name: string, url: string, { limit, signal }: { limit: number; signal: AbortSignal }): Promise<Record<string, unknown> | null> {
|
|
165
|
+
if (signal.aborted) return Promise.reject(signal.reason)
|
|
166
|
+
const key = `${url} ${limit}`
|
|
167
|
+
let ask = wholeAsks.get(key)
|
|
168
|
+
if (!ask) {
|
|
169
|
+
const controller = new AbortController()
|
|
170
|
+
const created = { asked: publicWholeDocument(name, url, { limit, signal: controller.signal }), controller, readers: 0 }
|
|
171
|
+
const settle = () => { if (wholeAsks.get(key) === created) wholeAsks.delete(key) }
|
|
172
|
+
created.asked.then(settle, settle)
|
|
173
|
+
wholeAsks.set(key, ask = created)
|
|
174
|
+
}
|
|
175
|
+
const held = ask
|
|
176
|
+
held.readers++
|
|
177
|
+
return new Promise((answer, fail) => {
|
|
178
|
+
let left = false
|
|
179
|
+
const leave = () => {
|
|
180
|
+
if (left) return false
|
|
181
|
+
left = true
|
|
182
|
+
held.readers--
|
|
183
|
+
signal.removeEventListener('abort', onAbort)
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
const onAbort = () => {
|
|
187
|
+
if (!leave()) return
|
|
188
|
+
if (held.readers === 0) {
|
|
189
|
+
if (wholeAsks.get(key) === held) wholeAsks.delete(key)
|
|
190
|
+
held.controller.abort(signal.reason)
|
|
191
|
+
}
|
|
192
|
+
fail(signal.reason)
|
|
193
|
+
}
|
|
194
|
+
const answered = (json: Record<string, unknown> | null) => { if (leave()) answer(json) }
|
|
195
|
+
const failed = (err: unknown) => { if (leave()) fail(err) }
|
|
196
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
197
|
+
held.asked.then(answered, failed)
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
152
201
|
// Asked anonymously first, unless its caller knows npm has none for anyone
|
|
153
202
|
// (`anonymous` false): a reader without private access stops there. One
|
|
154
203
|
// with it asks again with the token where npmTokenReads says so, and the
|
|
155
204
|
// answer is private. A whole document (`whole`) anyone may read is kept
|
|
156
|
-
// (publicWholeDocument)
|
|
205
|
+
// (publicWholeDocument), and asked once for those asking at once
|
|
206
|
+
// (sharedWholeDocument).
|
|
157
207
|
async function readDocument(name: string, url: string, privileged: boolean,
|
|
158
208
|
{ whole = false, anonymous = true, ...options }: { accept: string; limit: number; signal: AbortSignal; whole?: boolean; anonymous?: boolean }) {
|
|
159
|
-
const open = anonymous ? whole ? await
|
|
209
|
+
const open = anonymous ? whole ? await sharedWholeDocument(name, url, options) : await registryDocument(url, { ...options, token: null }) : null
|
|
160
210
|
if (open) return { json: open, private: false }
|
|
161
211
|
if (!npmTokenReads(name, privileged)) return null
|
|
162
212
|
const json = await registryDocument(url, { ...options, token: npmToken() })
|
|
@@ -199,6 +249,8 @@ function versionDocument(name: string, json: Record<string, unknown>, isPrivate:
|
|
|
199
249
|
// (npm-manifest.ts npmVersionFields), as the database keeps them (npm-kept.ts).
|
|
200
250
|
export interface NpmVersionStore {
|
|
201
251
|
read(name: string, version: string): Promise<{ at: number; json: Record<string, unknown> } | null>
|
|
252
|
+
// Those of `versions` kept, by `name@version`, in one read.
|
|
253
|
+
readMany?(versions: { name: string; version: string }[]): Promise<Map<string, { at: number; json: Record<string, unknown> }>>
|
|
202
254
|
write(name: string, version: string, json: Record<string, unknown>): Promise<void>
|
|
203
255
|
}
|
|
204
256
|
|
|
@@ -224,6 +276,20 @@ export function readNpmVersion(name: string, spec: string, privileged: boolean,
|
|
|
224
276
|
})
|
|
225
277
|
}
|
|
226
278
|
|
|
279
|
+
// `store`, where it reads many at once, with the exact ones of `versions`
|
|
280
|
+
// read from it in one read, for readNpmVersion to read each of those from as
|
|
281
|
+
// it asks: a version and the one it is compared with, read together.
|
|
282
|
+
export function readTogether(store: NpmVersionStore, versions: { name: string; version: string }[]): NpmVersionStore {
|
|
283
|
+
const exact = versions.filter(({ version }) => isExactVersion(version))
|
|
284
|
+
if (!store.readMany || exact.length < 2) return store
|
|
285
|
+
const keys = new Set(exact.map(({ name, version }) => `${name}@${version}`))
|
|
286
|
+
const kept = store.readMany(exact)
|
|
287
|
+
return { ...store, async read(name, version) {
|
|
288
|
+
const key = `${name}@${version}`
|
|
289
|
+
return keys.has(key) ? (await kept).get(key) ?? null : store.read(name, version)
|
|
290
|
+
} }
|
|
291
|
+
}
|
|
292
|
+
|
|
227
293
|
// The package's dist-tags and versions, newest published first, from its
|
|
228
294
|
// abbreviated document; with `times`, from its whole one, which alone has
|
|
229
295
|
// when each was published, `times` by version (ISO dates), and `created`,
|
|
@@ -273,77 +339,57 @@ const isPublishTime = (value: string | undefined) => typeof value === 'string' &
|
|
|
273
339
|
const PUBLISH_TIMES = 100_000
|
|
274
340
|
const publishTimes = new Map<string, string>()
|
|
275
341
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
export interface
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const field = bytes.subarray(start, start + length)
|
|
282
|
-
const end = field.indexOf(0)
|
|
283
|
-
return Buffer.from(end === -1 ? field : field.subarray(0, end)).toString('utf8')
|
|
342
|
+
// Where public versions' publish times are kept beyond the instance, as the
|
|
343
|
+
// database keeps them (npm-kept.ts npmPublishTimeStore).
|
|
344
|
+
export interface NpmPublishTimeStore {
|
|
345
|
+
read(name: string, version: string): Promise<string | null>
|
|
346
|
+
write(name: string, version: string, at: string): Promise<void>
|
|
284
347
|
}
|
|
285
348
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
349
|
+
// When `doc` was published: as noted here, else as `store` keeps a public
|
|
350
|
+
// version's, so that another instance, or this one restarted, has it before
|
|
351
|
+
// it reads the whole document again. The store is read once, at once, beside
|
|
352
|
+
// whatever its caller reads meanwhile, a failure read as none; `read` looks
|
|
353
|
+
// here again as it is asked, as the whole document read meanwhile may have
|
|
354
|
+
// noted it by then, and `keep` keeps there the time noted here where it read
|
|
355
|
+
// none.
|
|
356
|
+
export function npmPublishTime(doc: Pick<NpmVersionDocument, 'name' | 'version' | 'private'>, store?: NpmPublishTimeStore) {
|
|
357
|
+
const noted = () => publishTimes.get(`${doc.name}@${doc.version}`) ?? null
|
|
358
|
+
const kept = store && !doc.private ? store.read(doc.name, doc.version).catch(() => null) : Promise.resolve(null)
|
|
359
|
+
return {
|
|
360
|
+
read: async (): Promise<string | null> => noted() ?? await kept ?? noted(),
|
|
361
|
+
async keep() {
|
|
362
|
+
const at = noted()
|
|
363
|
+
if (at !== null && store && !doc.private && await kept === null) await store.write(doc.name, doc.version, at)
|
|
364
|
+
},
|
|
292
365
|
}
|
|
293
|
-
const text = textOf(bytes, offset, 12).trim()
|
|
294
|
-
return /^[0-7]+$/u.test(text) ? Number.parseInt(text, 8) : Number.NaN
|
|
295
366
|
}
|
|
296
367
|
|
|
297
|
-
|
|
298
|
-
// (`package/`, or whatever the archive used), and a path that would escape the
|
|
299
|
-
// package, or names nothing, is not extracted.
|
|
300
|
-
function packagePath(raw: string): string | null {
|
|
301
|
-
const parts = raw.split('/').slice(1).filter(part => part !== '' && part !== '.')
|
|
302
|
-
return parts.length === 0 || parts.includes('..') ? null : parts.join('/')
|
|
303
|
-
}
|
|
368
|
+
export interface NpmPackageFile { path: string; bytes: Uint8Array }
|
|
304
369
|
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
370
|
+
// The regular files of a tar stream, by package path: npm drops the first
|
|
371
|
+
// component (`package/`, or whatever the archive used), and a file at the top
|
|
372
|
+
// is not extracted. The stream is read by @preventive/archive, which refuses
|
|
373
|
+
// one malformed, a name or a symlink that would climb out of it -- a symlink
|
|
374
|
+
// past `symlinksAbove` levels above it, none unless given -- and a name
|
|
375
|
+
// repeated as another entry, and gives up past MAX_NPM_TAR_ENTRIES before
|
|
376
|
+
// reading on; a file under another top directory at the same path replaces
|
|
377
|
+
// an earlier one, as extraction would. Directories, links and devices hold no
|
|
378
|
+
// file content and are skipped.
|
|
379
|
+
export function readNpmTar(tar: Uint8Array, { symlinksAbove = 0 }: { symlinksAbove?: number } = {}): NpmPackageFile[] {
|
|
380
|
+
let entries: Entry[]
|
|
381
|
+
try { entries = unpack(tar, { maxEntries: MAX_NPM_TAR_ENTRIES, symlinksAbove }) }
|
|
382
|
+
catch (err) {
|
|
383
|
+
if (err instanceof ArchiveError) throw err.limited ? new NpmPackageError(413, 'package-too-large') : new NpmPackageError(422, 'bad-tarball')
|
|
384
|
+
throw err
|
|
317
385
|
}
|
|
318
|
-
return path
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// The regular files of a tar stream, by package path, a later entry
|
|
322
|
-
// replacing an earlier one as extraction would. Directories, links and
|
|
323
|
-
// devices hold no file content and are skipped.
|
|
324
|
-
export function readNpmTar(tar: Uint8Array): NpmPackageFile[] {
|
|
325
386
|
const files = new Map<string, Uint8Array>()
|
|
326
387
|
let total = 0
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
const size = sizeOf(header, 124)
|
|
333
|
-
const type = String.fromCodePoint(header[156]!)
|
|
334
|
-
const start = offset + 512
|
|
335
|
-
if (!Number.isSafeInteger(size) || size < 0 || start + size > tar.length) throw new NpmPackageError(422, 'bad-tarball')
|
|
336
|
-
const data = tar.subarray(start, start + size)
|
|
337
|
-
offset = start + Math.ceil(size / 512) * 512
|
|
338
|
-
if (type === 'x' || type === 'L') { longName = type === 'x' ? paxPath(data) : textOf(data, 0, data.length); continue }
|
|
339
|
-
if (type === 'g' || type === 'K') continue
|
|
340
|
-
const prefix = textOf(header, 257, 6) === 'ustar' ? textOf(header, 345, 155) : ''
|
|
341
|
-
const name = longName ?? (prefix ? `${prefix}/${textOf(header, 0, 100)}` : textOf(header, 0, 100))
|
|
342
|
-
longName = null
|
|
343
|
-
if (!['0', '\0', '7'].includes(type)) continue
|
|
344
|
-
const path = packagePath(name)
|
|
345
|
-
if (path === null) continue
|
|
346
|
-
total += size - (files.get(path)?.length ?? 0)
|
|
388
|
+
for (const { name, type, data } of entries) {
|
|
389
|
+
const slash = name.indexOf('/')
|
|
390
|
+
if ((type !== 'file' && type !== 'contiguous-file') || slash === -1) continue
|
|
391
|
+
const path = name.slice(slash + 1)
|
|
392
|
+
total += data.length - (files.get(path)?.length ?? 0)
|
|
347
393
|
files.set(path, data)
|
|
348
394
|
if (files.size > MAX_NPM_PACKAGE_FILES || total > MAX_NPM_PACKAGE_BYTES) throw new NpmPackageError(413, 'package-too-large')
|
|
349
395
|
}
|
|
@@ -35,6 +35,7 @@ import { buffer } from 'node:stream/consumers'
|
|
|
35
35
|
import type { CacheStore } from '@preventive/upstream/npm.js'
|
|
36
36
|
import { DEFAULT_MINIMUM_RELEASE_AGE, type NpmAssessment, assessNpm, npmReadyDownload, npmReadyDownloads } from '../risk/index.js'
|
|
37
37
|
import { CacheMissError, type CacheStorage } from './cache-storage.ts'
|
|
38
|
+
import { npmVersionStore } from './npm-kept.ts'
|
|
38
39
|
import { loadNpmTarball } from './npm-loads.ts'
|
|
39
40
|
import { NpmPackageError, type NpmVersionDocument, npmTokenReads, readNpmPackument, readNpmVersion } from './npm-packages.ts'
|
|
40
41
|
import { npmPackageScope } from '../common/managed/npm-packages.js'
|
|
@@ -176,7 +177,9 @@ export function npmRisk(doc: NpmVersionDocument, old: NpmVersionDocument | 'pick
|
|
|
176
177
|
return scopes.get(scope)!
|
|
177
178
|
}
|
|
178
179
|
const given = old === 'pick' ? null : old
|
|
179
|
-
|
|
180
|
+
// The one picked, read as the viewer reads a version, kept a day where public.
|
|
181
|
+
const documentOf = async (version: string) => [doc, given].find(read => read?.version === version)
|
|
182
|
+
?? await readNpmVersion(doc.name, version, privileged, signal, npmVersionStore(stores.db, stores.debug)) ?? undefined
|
|
180
183
|
return bounded(spare, () => assessNpm(doc.name, doc.version, { oldVersion: old === 'pick' ? true : old?.version ?? null, fromEmpty,
|
|
181
184
|
minimumReleaseAge: teamScoped ? 0 : DEFAULT_MINIMUM_RELEASE_AGE,
|
|
182
185
|
popularity: !teamScoped, cache: npmRiskCache(stores, !doc.private && !given?.private), signal, dependencies: !spare, advisories: !spare, osv: !doc.private, owned,
|