@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.41

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.
Files changed (312) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +2 -0
  6. package/client/dependency-paths.js +24 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +112 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +522 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-metadata.d.ts +14 -0
  17. package/common/bundle-metadata.js +226 -0
  18. package/common/bundle-pkg-of.d.ts +1 -0
  19. package/common/bundle-pkg-of.js +90 -0
  20. package/common/bundle-reasons.d.ts +2 -0
  21. package/common/bundle-reasons.js +21 -0
  22. package/common/bundle-source-links.d.ts +6 -0
  23. package/common/bundle-source-links.js +216 -0
  24. package/common/bundle-sourcemap.d.ts +10 -0
  25. package/common/bundle-sourcemap.js +100 -0
  26. package/common/bundle-sources.d.ts +7 -0
  27. package/common/bundle-sources.js +310 -0
  28. package/common/bundle-stats.js +41 -0
  29. package/common/bundle-tabs.js +1 -0
  30. package/common/code-language.js +65 -0
  31. package/common/default-scan-models.ts +31 -0
  32. package/common/dependency-reviews.d.ts +23 -0
  33. package/common/dependency-reviews.js +81 -0
  34. package/common/finding-duplicates.js +58 -0
  35. package/common/finding-filters.js +274 -0
  36. package/common/finding-id.js +47 -0
  37. package/common/github-issue-labels.d.ts +1 -0
  38. package/common/github-issue-labels.js +11 -0
  39. package/common/github-pr.ts +56 -0
  40. package/common/gzip.js +59 -0
  41. package/common/line-diff.d.ts +8 -0
  42. package/common/line-diff.js +56 -0
  43. package/common/linked-workspace-groups.js +63 -0
  44. package/common/managed/comments.ts +34 -0
  45. package/common/managed/npm-packages.d.ts +5 -0
  46. package/common/managed/npm-packages.js +34 -0
  47. package/common/managed/permissions.ts +31 -0
  48. package/common/managed/report-content.ts +72 -0
  49. package/common/managed/report-filter.ts +108 -0
  50. package/common/managed/repository-alias.ts +65 -0
  51. package/common/managed/roles.ts +28 -0
  52. package/common/managed/routes.d.ts +2 -0
  53. package/common/managed/routes.js +254 -0
  54. package/common/managed/scan-models.ts +6 -0
  55. package/common/managed/team-app.d.ts +2 -0
  56. package/common/managed/team-app.js +31 -0
  57. package/common/managed/triage.ts +83 -0
  58. package/common/managed/upload-seal.ts +126 -0
  59. package/common/pretty-print.d.ts +5 -0
  60. package/common/pretty-print.js +24 -0
  61. package/common/revalidation-conflicts.js +48 -0
  62. package/common/revalidation-input-groups.js +56 -0
  63. package/common/save-error-reason.ts +20 -7
  64. package/common/scan-server.ts +13 -0
  65. package/common/server-info.ts +35 -0
  66. package/common/utf8.d.ts +3 -0
  67. package/common/utf8.js +45 -0
  68. package/common/workspace-app.js +23 -0
  69. package/common/workspace-groups.js +176 -0
  70. package/out/brotli-fallback.js +3 -3
  71. package/out/chunk-2AFQKRAM.js +51 -0
  72. package/out/chunk-2KBF5TZ7.js +2 -0
  73. package/out/chunk-2XUEZHIP.js +28 -0
  74. package/out/chunk-35VE6CDH.js +2 -0
  75. package/out/chunk-3BZ7JERB.js +1 -0
  76. package/out/chunk-4F6JSF57.js +3 -0
  77. package/out/chunk-4XCDFGDP.js +10 -0
  78. package/out/chunk-C4Z6ZBXV.js +10 -0
  79. package/out/chunk-CMQA4DM6.js +1 -0
  80. package/out/chunk-CVVJAXVY.js +2 -0
  81. package/out/chunk-DIJJNTRH.js +10 -0
  82. package/out/chunk-GJ4IUCNS.js +1 -0
  83. package/out/chunk-GNXOKIGP.js +80 -0
  84. package/out/chunk-GZ4MS3TE.js +13 -0
  85. package/out/chunk-IU3CCINR.js +3 -0
  86. package/out/chunk-K3X5EJBQ.js +2 -0
  87. package/out/chunk-KEX2ATEW.js +2 -0
  88. package/out/chunk-KVEUXIEU.js +1 -0
  89. package/out/chunk-KYCNJVB7.js +1 -0
  90. package/out/chunk-NRPC74AM.js +1 -0
  91. package/out/chunk-OOJM4CTU.js +1 -0
  92. package/out/chunk-PNHBZGDD.js +1 -0
  93. package/out/chunk-QZ3A376W.js +1 -0
  94. package/out/chunk-R5O6FAW4.js +2 -0
  95. package/out/chunk-RFZQQXSG.js +17 -0
  96. package/out/chunk-RSHMVYNS.js +1 -0
  97. package/out/chunk-TUII34K2.js +1 -0
  98. package/out/chunk-TY4RZD3Z.js +226 -0
  99. package/out/chunk-TZ5KB2Y5.js +10 -0
  100. package/out/chunk-U6OPEXIA.js +1 -0
  101. package/out/chunk-UNTRGE26.js +2 -0
  102. package/out/chunk-UXNUAIIC.js +33 -0
  103. package/out/chunk-UYAXVRVL.js +1 -0
  104. package/out/chunk-UZAZM2SX.js +2 -0
  105. package/out/chunk-V3ME6FI5.js +2 -0
  106. package/out/chunk-V5O7M2YT.js +1 -0
  107. package/out/chunk-VIHGO77Y.js +1 -0
  108. package/out/chunk-VQJCXGIY.js +7 -0
  109. package/out/chunk-W4J3MGLB.js +1 -0
  110. package/out/chunk-WGG7KQUH.js +1 -0
  111. package/out/chunk-WZDTZE64.js +1 -0
  112. package/out/chunk-XL35ZV6R.js +1 -0
  113. package/out/chunk-XYFXXB2G.js +10 -0
  114. package/out/chunk-YVZH5SHP.js +1 -0
  115. package/out/chunk-ZBBOBQ43.js +1 -0
  116. package/out/chunk-ZMQQCGF6.js +2 -0
  117. package/out/client-managed-import.js +4 -0
  118. package/out/client-managed.js +37 -0
  119. package/out/client-sync.js +10 -13
  120. package/out/github-issue-authorized.html +3 -0
  121. package/out/github-issue-authorized.js +1 -0
  122. package/out/graph.js +17 -4
  123. package/out/index.html +90 -8
  124. package/out/prism.js +6 -2
  125. package/out/stasis.svg +20 -0
  126. package/out/terminal.js +365 -39
  127. package/out/view.css +1 -1
  128. package/out/view.js +83 -117
  129. package/package.json +298 -59
  130. package/risk/files/characters.js +196 -0
  131. package/risk/files/hosts.js +205 -0
  132. package/risk/files/readability.js +171 -0
  133. package/risk/files/runs-code.js +595 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +156 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +223 -0
  139. package/risk/npm/combined.js +65 -0
  140. package/risk/npm/compare.js +228 -0
  141. package/risk/npm/dependencies.js +153 -0
  142. package/risk/npm/diff.js +43 -0
  143. package/risk/npm/downloads.js +84 -0
  144. package/risk/npm/files.js +34 -0
  145. package/risk/npm/held.js +60 -0
  146. package/risk/npm/install-scripts.js +34 -0
  147. package/risk/npm/malware.js +36 -0
  148. package/risk/npm/manifest.js +65 -0
  149. package/risk/npm/new-dependency.js +30 -0
  150. package/risk/npm/old-version.js +108 -0
  151. package/risk/npm/registry.js +282 -0
  152. package/risk/npm/release.js +85 -0
  153. package/risk/npm/score.js +59 -0
  154. package/risk/npm/specs.js +124 -0
  155. package/risk/npm/tarball.js +113 -0
  156. package/server-common/database-config.ts +16 -0
  157. package/server-common/initialize.ts +18 -0
  158. package/server-common/npm-advisories.ts +101 -0
  159. package/{server → server-common}/origin.ts +5 -5
  160. package/server-common/reap.ts +48 -0
  161. package/server-common/scan-config.ts +19 -0
  162. package/server-common/standalone.ts +29 -0
  163. package/server-common/storage-crypto.ts +182 -0
  164. package/server-common/storage-log.ts +38 -0
  165. package/server-common/vercel-blob.ts +103 -0
  166. package/server-e2e/app.ts +486 -0
  167. package/{server → server-e2e}/auth.ts +5 -1
  168. package/{server → server-e2e}/bus-receiver.ts +2 -1
  169. package/{server → server-e2e}/cli.js +9 -4
  170. package/{server → server-e2e}/config.ts +56 -35
  171. package/{server → server-e2e}/db-neon.ts +2 -2
  172. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  173. package/{server → server-e2e}/db-stmt.ts +2 -2
  174. package/{server → server-e2e}/db.ts +7 -7
  175. package/{server → server-e2e}/http.ts +116 -18
  176. package/{server → server-e2e}/hub.ts +40 -9
  177. package/server-e2e/index.ts +17 -0
  178. package/server-e2e/lifecycle.ts +95 -0
  179. package/{server → server-e2e}/neon-driver.ts +2 -2
  180. package/{server → server-e2e}/npm-proxy.ts +12 -145
  181. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  182. package/{server → server-e2e}/objstore/blob.ts +1 -1
  183. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  184. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  185. package/{server → server-e2e}/objstore/init.ts +35 -18
  186. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  187. package/server-e2e/objstore/rest-deny.ts +28 -0
  188. package/server-e2e/objstore/rest-mint.ts +224 -0
  189. package/{server → server-e2e}/objstore/rest.ts +27 -26
  190. package/{server → server-e2e}/objstore/sign.ts +105 -0
  191. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  192. package/{server → server-e2e}/objstore/store.ts +7 -7
  193. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  194. package/{server → server-e2e}/pubsub.ts +12 -9
  195. package/{server → server-e2e}/sign.ts +4 -4
  196. package/{server → server-e2e}/sse-server.ts +102 -68
  197. package/{server → server-e2e}/sse-session.ts +28 -8
  198. package/{server → server-e2e}/static.ts +31 -21
  199. package/server-e2e/sync-handlers.ts +408 -0
  200. package/{server → server-e2e}/ws-server.ts +20 -9
  201. package/server-managed/activity.ts +245 -0
  202. package/server-managed/avatar-store.ts +12 -0
  203. package/server-managed/blob-store.ts +17 -0
  204. package/server-managed/brotli.ts +10 -0
  205. package/server-managed/bundle-advisories.ts +120 -0
  206. package/server-managed/bundle-advisory-inventory.ts +77 -0
  207. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  208. package/server-managed/bundle-build-diagnostics.js +23 -0
  209. package/server-managed/bundle-build-leases.ts +41 -0
  210. package/server-managed/bundle-build-worker.js +111 -0
  211. package/server-managed/bundle-build.ts +198 -0
  212. package/server-managed/bundle-cache.ts +199 -0
  213. package/server-managed/bundle-catalog.ts +29 -0
  214. package/server-managed/bundle-commits.ts +140 -0
  215. package/server-managed/bundle-store.ts +24 -0
  216. package/server-managed/bundle-summary-cache.ts +130 -0
  217. package/server-managed/bundle.ts +120 -0
  218. package/server-managed/cache-storage.ts +19 -0
  219. package/server-managed/cli.js +21 -0
  220. package/server-managed/combined.ts +46 -0
  221. package/server-managed/comments.ts +151 -0
  222. package/server-managed/config.ts +158 -0
  223. package/server-managed/content-access.ts +15 -0
  224. package/server-managed/crypto.ts +25 -0
  225. package/server-managed/db-methods.ts +1725 -0
  226. package/server-managed/db-neon.ts +367 -0
  227. package/server-managed/db-schema.ts +227 -0
  228. package/server-managed/db-table-names.ts +22 -0
  229. package/server-managed/db.ts +132 -0
  230. package/server-managed/dependency-reviews.ts +248 -0
  231. package/server-managed/github-app.ts +391 -0
  232. package/server-managed/github-errors.ts +68 -0
  233. package/server-managed/github-issue-links.ts +127 -0
  234. package/server-managed/github-issue-oauth.ts +47 -0
  235. package/server-managed/github-issues.ts +152 -0
  236. package/server-managed/github-metadata.ts +213 -0
  237. package/server-managed/github-oauth.ts +235 -0
  238. package/server-managed/github-pulls.ts +227 -0
  239. package/server-managed/http-response.ts +50 -0
  240. package/server-managed/http.ts +3472 -0
  241. package/server-managed/import-triage.ts +51 -0
  242. package/server-managed/index.ts +187 -0
  243. package/server-managed/kept.ts +28 -0
  244. package/server-managed/link-reports.ts +158 -0
  245. package/server-managed/locked-packages.ts +85 -0
  246. package/server-managed/lru.ts +30 -0
  247. package/server-managed/managed-issues.ts +75 -0
  248. package/server-managed/management-catalog.ts +45 -0
  249. package/server-managed/management.ts +99 -0
  250. package/server-managed/npm-insights.ts +440 -0
  251. package/server-managed/npm-kept.ts +65 -0
  252. package/server-managed/npm-loads.ts +167 -0
  253. package/server-managed/npm-manifest.ts +115 -0
  254. package/server-managed/npm-packages.ts +378 -0
  255. package/server-managed/npm-provenance.ts +123 -0
  256. package/server-managed/npm-risk.ts +177 -0
  257. package/server-managed/object-storage-disk.ts +148 -0
  258. package/server-managed/object-storage-vercel.ts +67 -0
  259. package/server-managed/object-storage.ts +74 -0
  260. package/server-managed/package-entry-points.ts +51 -0
  261. package/server-managed/pretty-print.ts +352 -0
  262. package/server-managed/public-workspace.ts +220 -0
  263. package/server-managed/rename-diff-worker.js +26 -0
  264. package/server-managed/rename-diff.ts +270 -0
  265. package/server-managed/repo-path.ts +21 -0
  266. package/server-managed/report-migration.ts +35 -0
  267. package/server-managed/report-query.ts +10 -0
  268. package/server-managed/report-response.ts +16 -0
  269. package/server-managed/report-sources.ts +178 -0
  270. package/server-managed/report-store.ts +47 -0
  271. package/server-managed/repository-aliases.ts +91 -0
  272. package/server-managed/repository-browser.ts +153 -0
  273. package/server-managed/repository-discovery.ts +82 -0
  274. package/server-managed/repository-policy.ts +27 -0
  275. package/server-managed/repository-risk.ts +420 -0
  276. package/server-managed/revisions.ts +35 -0
  277. package/server-managed/session.ts +92 -0
  278. package/server-managed/slugs.ts +38 -0
  279. package/server-managed/solidity-entry-points.ts +69 -0
  280. package/server-managed/sql-postgres.ts +31 -0
  281. package/server-managed/sql.ts +76 -0
  282. package/server-managed/static.ts +28 -0
  283. package/server-managed/storage-command.ts +16 -0
  284. package/server-managed/storage-db.ts +175 -0
  285. package/server-managed/storage-encryption.ts +279 -0
  286. package/server-managed/storage-maintenance.ts +256 -0
  287. package/server-managed/storage-payload.ts +103 -0
  288. package/server-managed/storage-stores.ts +85 -0
  289. package/server-managed/storage.ts +78 -0
  290. package/server-managed/team-app.ts +94 -0
  291. package/server-managed/team-catalog.ts +7 -0
  292. package/server-managed/team-feed.ts +173 -0
  293. package/server-managed/team-npm-scopes.ts +77 -0
  294. package/server-managed/team-reports.ts +228 -0
  295. package/server-managed/triage-response.ts +16 -0
  296. package/server-managed/type-only-worker.js +19 -0
  297. package/server-managed/type-only.ts +103 -0
  298. package/server-managed/uploads.ts +86 -0
  299. package/server-managed/upstream-cache.ts +215 -0
  300. package/server-managed/view-as.ts +58 -0
  301. package/server-managed/workspace-shares.ts +166 -0
  302. package/server.ts +50 -0
  303. package/server/index.ts +0 -477
  304. package/server/lifecycle.ts +0 -204
  305. package/server/sync-handlers.ts +0 -320
  306. /package/{LICENSE → LICENSE-MIT} +0 -0
  307. /package/{server → server-e2e}/config.example.json +0 -0
  308. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  309. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  310. /package/{server → server-e2e}/peer.ts +0 -0
  311. /package/{server → server-e2e}/util.ts +0 -0
  312. /package/{server → server-e2e}/validation.ts +0 -0
@@ -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
+ }