@preventive/triage 1.0.0-alpha.44 → 1.0.0-alpha.45

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 (183) hide show
  1. package/common/bundle-sources.js +20 -1
  2. package/common/default-scan-models.ts +1 -1
  3. package/common/dependency-reviews.d.ts +0 -5
  4. package/common/dependency-reviews.js +0 -17
  5. package/common/managed/agent-tasks.ts +292 -0
  6. package/common/managed/github-agents.ts +92 -0
  7. package/common/managed/github-logins.ts +49 -0
  8. package/common/managed/roles.ts +7 -0
  9. package/common/managed/routes.js +88 -7
  10. package/common/managed/team-app.d.ts +5 -2
  11. package/common/managed/team-app.js +2 -2
  12. package/common/server-info.ts +3 -0
  13. package/common/workspace-app.js +6 -3
  14. package/out/brotli-fallback.js +3 -3
  15. package/out/chunk-2XUEZHIP.js +18 -18
  16. package/out/chunk-2ZDZWLFY.js +1 -0
  17. package/out/chunk-3PS4KWD2.js +10 -0
  18. package/out/chunk-5DHL5NG3.js +2 -0
  19. package/out/chunk-6HNXNIQY.js +52 -0
  20. package/out/chunk-7C7DWL7I.js +1 -0
  21. package/out/chunk-AFLZH7WG.js +1 -0
  22. package/out/chunk-BC2UCLFJ.js +1 -0
  23. package/out/chunk-C4Z6ZBXV.js +6 -6
  24. package/out/chunk-GDPCLSOS.js +2 -0
  25. package/out/chunk-GO7FT4OD.js +1 -0
  26. package/out/chunk-HMAJDVXN.js +1 -0
  27. package/out/chunk-I2MDGSQX.js +3 -0
  28. package/out/chunk-IYPY47MC.js +1 -0
  29. package/out/chunk-IZ7POWI3.js +18 -0
  30. package/out/chunk-JCIIBLKR.js +1 -1
  31. package/out/chunk-NPW52BM2.js +17 -0
  32. package/out/chunk-NRPC74AM.js +1 -1
  33. package/out/chunk-NV7ES4EW.js +1 -0
  34. package/out/chunk-OKRLLE5I.js +17 -0
  35. package/out/chunk-OOJM4CTU.js +1 -1
  36. package/out/chunk-PNHBZGDD.js +1 -1
  37. package/out/chunk-PTGDYLNS.js +36 -0
  38. package/out/chunk-PXK7EU4V.js +1 -0
  39. package/out/chunk-Q6MVYGYV.js +3 -0
  40. package/out/chunk-QB4CBOX2.js +1 -0
  41. package/out/chunk-QRD6GIG5.js +229 -0
  42. package/out/chunk-R3NZFXVR.js +1 -0
  43. package/out/chunk-R5O6FAW4.js +1 -1
  44. package/out/chunk-SXV2T5C4.js +80 -0
  45. package/out/chunk-TUII34K2.js +1 -1
  46. package/out/chunk-VFWMQ3LG.js +2 -0
  47. package/out/chunk-VH4XMZIW.js +2 -0
  48. package/out/chunk-WFYLMF6Z.js +10 -0
  49. package/out/chunk-WI7Z5A2T.js +6 -0
  50. package/out/chunk-WQ74ZBZF.js +7 -0
  51. package/out/chunk-XVBEOGOS.js +3 -0
  52. package/out/client-managed-import.js +3 -3
  53. package/out/client-managed.js +31 -35
  54. package/out/client-sync.js +11 -11
  55. package/out/compare.js +35 -0
  56. package/out/dialogs.js +51 -0
  57. package/out/github-issue-authorized.html +1 -1
  58. package/out/github-issue-authorized.js +1 -1
  59. package/out/graph.js +17 -17
  60. package/out/index.html +30 -34
  61. package/out/prism.js +14 -14
  62. package/out/scan.js +2 -0
  63. package/out/terminal.js +294 -293
  64. package/out/view.css +1 -1
  65. package/out/view.js +16 -107
  66. package/package.json +51 -6
  67. package/risk/actions/findings.js +156 -0
  68. package/risk/actions/resolve.js +219 -0
  69. package/risk/actions/scripts/commands.js +182 -0
  70. package/risk/actions/scripts/grade.js +113 -0
  71. package/risk/actions/scripts/packages.js +123 -0
  72. package/risk/actions/scripts/shell.js +165 -0
  73. package/risk/actions/uses.js +76 -0
  74. package/risk/actions/workflow.js +110 -0
  75. package/risk/files/hosts.js +6 -6
  76. package/risk/files/readability.js +23 -17
  77. package/risk/files/runs-code.js +18 -6
  78. package/risk/files/script.js +7 -3
  79. package/risk/files/stamps.js +8 -7
  80. package/risk/index.d.ts +68 -1
  81. package/risk/index.js +6 -2
  82. package/risk/npm/assess.js +1 -1
  83. package/risk/npm/compare.js +34 -10
  84. package/risk/npm/files.js +413 -2
  85. package/risk/npm/tarball.js +56 -73
  86. package/server-managed/agent-merges.ts +198 -0
  87. package/server-managed/app-agents.ts +30 -0
  88. package/server-managed/avatar-store.ts +1 -0
  89. package/server-managed/commit-coauthors.ts +83 -0
  90. package/server-managed/commit-lockfiles.ts +62 -0
  91. package/server-managed/db-methods.ts +29 -10
  92. package/server-managed/db-neon.ts +30 -2
  93. package/server-managed/db-schema.ts +9 -2
  94. package/server-managed/db.ts +7 -0
  95. package/server-managed/dependabot-access.ts +60 -0
  96. package/server-managed/dependabot-alerts.ts +185 -0
  97. package/server-managed/dependency-review-comments.ts +97 -0
  98. package/server-managed/dependency-reviews.ts +65 -15
  99. package/server-managed/github-accounts.ts +33 -0
  100. package/server-managed/github-actions.ts +71 -0
  101. package/server-managed/github-app.ts +23 -9
  102. package/server-managed/github-errors.ts +2 -0
  103. package/server-managed/github-link-oauth.ts +69 -0
  104. package/server-managed/github-links.ts +204 -0
  105. package/server-managed/github-oauth.ts +5 -2
  106. package/server-managed/github-users.ts +30 -0
  107. package/server-managed/http.ts +1122 -52
  108. package/server-managed/index.ts +6 -2
  109. package/server-managed/locked-packages.ts +37 -16
  110. package/server-managed/lockfile-scope.ts +49 -0
  111. package/server-managed/npm-delta-worker.js +23 -0
  112. package/server-managed/npm-delta.ts +173 -0
  113. package/server-managed/npm-insights.ts +20 -11
  114. package/server-managed/npm-kept.ts +51 -4
  115. package/server-managed/npm-loads.ts +20 -13
  116. package/server-managed/npm-packages.ts +24 -64
  117. package/server-managed/pull-comment-reviews.ts +161 -0
  118. package/server-managed/pull-dependencies.ts +280 -0
  119. package/server-managed/pull-dependency-reviews.ts +318 -0
  120. package/server-managed/pull-reviews.ts +205 -0
  121. package/server-managed/release-commits.ts +84 -0
  122. package/server-managed/repository-actions.ts +209 -0
  123. package/server-managed/repository-agents.ts +220 -0
  124. package/server-managed/repository-browser.ts +54 -13
  125. package/server-managed/repository-changes.ts +284 -0
  126. package/server-managed/repository-code.ts +52 -0
  127. package/server-managed/repository-dependency-reviews.ts +135 -0
  128. package/server-managed/repository-history.ts +445 -0
  129. package/server-managed/repository-issues.ts +181 -0
  130. package/server-managed/repository-pulse.ts +432 -0
  131. package/server-managed/repository-risk-read.ts +165 -0
  132. package/server-managed/repository-risk.ts +113 -105
  133. package/server-managed/reviewed-dependencies.ts +149 -0
  134. package/server-managed/session.ts +9 -5
  135. package/server-managed/sql.ts +1 -1
  136. package/server-managed/storage-stores.ts +5 -0
  137. package/server-managed/team-app.ts +19 -3
  138. package/server-managed/workspace-shares.ts +4 -1
  139. package/server.ts +11 -0
  140. package/out/chunk-2ABLKCOY.js +0 -46
  141. package/out/chunk-2KBF5TZ7.js +0 -2
  142. package/out/chunk-35VE6CDH.js +0 -2
  143. package/out/chunk-3BZ7JERB.js +0 -1
  144. package/out/chunk-4XCDFGDP.js +0 -10
  145. package/out/chunk-5LSXOLUS.js +0 -1
  146. package/out/chunk-5RCM2J63.js +0 -1
  147. package/out/chunk-7B6HB4D7.js +0 -1
  148. package/out/chunk-AJ4GTATZ.js +0 -3
  149. package/out/chunk-CH4UN6WK.js +0 -225
  150. package/out/chunk-CMQA4DM6.js +0 -1
  151. package/out/chunk-CVVJAXVY.js +0 -2
  152. package/out/chunk-DIJJNTRH.js +0 -10
  153. package/out/chunk-EDKLBQQN.js +0 -17
  154. package/out/chunk-F47DXCEV.js +0 -33
  155. package/out/chunk-FK3DZEKL.js +0 -1
  156. package/out/chunk-G6AJWENO.js +0 -1
  157. package/out/chunk-GJ4IUCNS.js +0 -1
  158. package/out/chunk-GNXOKIGP.js +0 -80
  159. package/out/chunk-GZ4MS3TE.js +0 -13
  160. package/out/chunk-GZWM7UQM.js +0 -2
  161. package/out/chunk-IU3CCINR.js +0 -3
  162. package/out/chunk-K3X5EJBQ.js +0 -2
  163. package/out/chunk-KEX2ATEW.js +0 -2
  164. package/out/chunk-KYCNJVB7.js +0 -1
  165. package/out/chunk-LPCTJYNA.js +0 -7
  166. package/out/chunk-O4RJY5TP.js +0 -2
  167. package/out/chunk-OD4LPN3H.js +0 -1
  168. package/out/chunk-QWDWTBM7.js +0 -1
  169. package/out/chunk-QYWKC557.js +0 -1
  170. package/out/chunk-R75XOKUP.js +0 -1
  171. package/out/chunk-SWTLADW5.js +0 -1
  172. package/out/chunk-T7OKH4VW.js +0 -1
  173. package/out/chunk-TZ5KB2Y5.js +0 -10
  174. package/out/chunk-UNTRGE26.js +0 -2
  175. package/out/chunk-UTR2LVME.js +0 -2
  176. package/out/chunk-UYAXVRVL.js +0 -1
  177. package/out/chunk-V3ME6FI5.js +0 -2
  178. package/out/chunk-V5O7M2YT.js +0 -1
  179. package/out/chunk-VIHGO77Y.js +0 -1
  180. package/out/chunk-W4J3MGLB.js +0 -1
  181. package/out/chunk-WGG7KQUH.js +0 -1
  182. package/out/chunk-XYFXXB2G.js +0 -10
  183. package/out/chunk-ZESLQCTF.js +0 -6
@@ -11,6 +11,7 @@ import { initializeApp } from '../server-common/initialize.ts'
11
11
  import { type ManagedConfig, loadManagedConfig } from './config.ts'
12
12
  import { type ManagedHttpDeps, createManagedRequestHandler } from './http.ts'
13
13
  import { setNpmTarballCache } from './npm-loads.ts'
14
+ import { assembledReviewedDependencies } from './reviewed-dependencies.ts'
14
15
  import { setNpmDocumentCache } from './npm-packages.ts'
15
16
  import { loadManagedStatic } from './static.ts'
16
17
  import { openManagedStorage } from './storage.ts'
@@ -21,7 +22,7 @@ import { setCacheDir } from '@preventive/upstream/npm.js'
21
22
  const REAP_INTERVAL_MS = 3_600_000
22
23
  const REAP_RETRY_MS = 60_000
23
24
 
24
- type ManagedAppOptions = Partial<Pick<ManagedHttpDeps, 'next' | 'serverInfo' | 'isShuttingDown'>>
25
+ type ManagedAppOptions = Partial<Pick<ManagedHttpDeps, 'next' | 'serverInfo' | 'isShuttingDown' | 'reviewedDependencies'>>
25
26
 
26
27
  export async function createManagedApp(config: ManagedConfig, options: ManagedAppOptions = {}) {
27
28
  // Audits keep upstream's records on disk where the config has a cache
@@ -34,7 +35,10 @@ export async function createManagedApp(config: ManagedConfig, options: ManagedAp
34
35
  // Load the optional peer only for serverless managed deployments. Embedding
35
36
  // hosts can discard listener promises, so retain maintenance inside the app.
36
37
  const waitUntil = config.serverless ? (await import('@vercel/functions')).waitUntil : undefined
37
- return await initializeApp(rollback => assembleManagedApp(config, options, rollback, waitUntil))
38
+ // The host's reviewed dependencies, registered before; none after.
39
+ const reviewedDependencies = assembledReviewedDependencies()
40
+ const assembled = { ...options, ...reviewedDependencies ? { reviewedDependencies } : {} }
41
+ return await initializeApp(rollback => assembleManagedApp(config, assembled, rollback, waitUntil))
38
42
  }
39
43
 
40
44
  async function assembleManagedApp(config: ManagedConfig, options: ManagedAppOptions, rollback: AsyncDisposableStack, waitUntil?: (promise: Promise<unknown>) => void) {
@@ -7,6 +7,7 @@ import { LockfileError, parseNpmLockfile } from '@preventive/lockfile/npm.js'
7
7
  import { parsePnpmLockfile } from '@preventive/lockfile/pnpm.js'
8
8
  import { parseYarn1Lockfile } from '@preventive/lockfile/yarn1.js'
9
9
  import type { LockfileFormat } from '@preventive/upstream/github.js'
10
+ import type { ReviewedLockfile } from './reviewed-dependencies.ts'
10
11
 
11
12
  export interface LockedPackage {
12
13
  name: string
@@ -19,9 +20,16 @@ export interface LockedPackage {
19
20
  // Where one from elsewhere than npm's registry comes from, without any
20
21
  // credentials its URL has.
21
22
  from?: string
22
- // A mirror's tarball's, as the lockfile has it: npm's package of its name
23
- // and version only where npm's integrity is among it.
23
+ // As the lockfile has it: a mirror's tarball's, npm's package of its name
24
+ // and version only where npm's integrity is among it; and, where its
25
+ // reviews ask (`review`), npm's registry's too.
24
26
  integrity?: string
27
+ // Where its reviews ask: the sha1 Yarn 1 writes after a tarball's `#`, in
28
+ // hex (`shasum`); the hash of the patch pnpm applies to it
29
+ // (`patchHash`, its patchedDependencies'); and the lockfile's format.
30
+ shasum?: string
31
+ patchHash?: string
32
+ lockfile?: ReviewedLockfile
25
33
  // Where only the importers' devDependencies reach it, as npm's `dev` flag
26
34
  // says, and an install of no devDependencies leaves it out: in npm's and
27
35
  // pnpm's lockfiles. Yarn 1's says nothing of it, which only the
@@ -32,10 +40,10 @@ export interface LockedPackage {
32
40
  // A package as each reader resolves one, its name the one it is published
33
41
  // under, and a registry tarball's URL held to its name and version.
34
42
  type Resolution =
35
- | { type: 'tarball'; tarball: string | undefined; integrity: string | undefined; gitHosted?: boolean }
43
+ | { type: 'tarball'; tarball: string | undefined; integrity: string | undefined; sha1?: string | undefined; gitHosted?: boolean }
36
44
  | { type: 'git'; repo: string }
37
45
  | { type: 'directory' }
38
- interface ReadPackage { name: string; version: string | undefined; resolution: Resolution | undefined; dev?: boolean | undefined }
46
+ interface ReadPackage { name: string; version: string | undefined; resolution: Resolution | undefined; dev?: boolean | undefined; patchHash?: string | undefined }
39
47
 
40
48
  // The packages of a lockfile of `format`, as its reader resolves them.
41
49
  // Versions go unchecked against ranges, as only what is locked is asked for.
@@ -54,41 +62,54 @@ function packagesOf(format: LockfileFormat, text: string): ReadPackage[] {
54
62
  }
55
63
 
56
64
  // npm's registry, by either of its names, and yarn's mirror of it, as
57
- // @preventive/lockfile holds their tarballs to a package's name and version.
65
+ // @preventive/lockfile holds their tarballs to a package's name and version:
66
+ // its link to one, as it names it, a hash after it alone (Yarn 1's sha1).
58
67
  const REGISTRY_HOSTS = new Set(['registry.npmjs.org', 'registry.npmjs.com', 'registry.yarnpkg.com'])
59
- const isRegistry = (url: URL) => url.protocol === 'https:' && !url.port && !url.username && !url.password
68
+ const isRegistry = (url: URL) => url.protocol === 'https:' && !url.port && !url.username && !url.password && !url.search
60
69
  && REGISTRY_HOSTS.has(url.hostname.replace(/\.+$/u, ''))
61
70
  // Where a URL is, as it may be shown: its credentials, query and fragment
62
71
  // left out.
63
72
  const shown = (url: URL | null) => url ? { from: `${url.protocol}//${url.host}${url.pathname}` } : {}
64
73
 
65
- // Where a package read comes from (LockedPackage); null for one a package
66
- // bundles, with no resolution, or one of the project's own directories.
67
- function locked({ name, version, resolution }: ReadPackage): LockedPackage | null {
74
+ // Where a package read comes from (LockedPackage), with npm's registry's
75
+ // integrity where `review`; null for one a package bundles, with no
76
+ // resolution, or one of the project's own directories.
77
+ function located({ name, version, resolution }: ReadPackage, review: boolean): LockedPackage | null {
68
78
  const at = { name, version: version ?? null }
69
79
  if (resolution === undefined || resolution.type === 'directory') return null
70
80
  if (resolution.type === 'git') return { ...at, source: 'git', ...shown(URL.parse(resolution.repo)) }
71
81
  const { tarball, integrity } = resolution
72
82
  if (resolution.gitHosted) return { ...at, source: 'git', ...shown(URL.parse(tarball ?? '')) }
73
- if (tarball === undefined) return { ...at, source: 'registry' }
83
+ const registry = { ...at, source: 'registry' as const, ...review && integrity ? { integrity } : {} }
84
+ if (tarball === undefined) return registry
74
85
  if (tarball.startsWith('file:')) return { ...at, source: 'file' }
75
86
  const url = URL.parse(tarball)
76
- if (url && isRegistry(url)) return { ...at, source: 'registry' }
87
+ if (url && isRegistry(url)) return registry
77
88
  return integrity ? { ...at, ...shown(url), source: 'mirror', integrity } : { ...at, ...shown(url), source: 'tarball' }
78
89
  }
90
+ // As above, and with what its reviews ask (LockedPackage `shasum`,
91
+ // `patchHash`, `lockfile`) where `review`.
92
+ function locked(pkg: ReadPackage, format: LockfileFormat, review: boolean): LockedPackage | null {
93
+ const one = located(pkg, review)
94
+ if (!one || !review) return one
95
+ const sha1 = pkg.resolution?.type === 'tarball' ? pkg.resolution.sha1 : undefined
96
+ return { ...one, ...sha1 ? { shasum: sha1 } : {}, ...pkg.patchHash ? { patchHash: pkg.patchHash } : {}, lockfile: format === 'yarn' ? 'yarn1' : format as ReviewedLockfile }
97
+ }
79
98
 
80
99
  // The packages a lockfile of `format` (npm, pnpm or yarn, as
81
100
  // findRepoLockfiles names them) locks, each once, in the lockfile's order:
82
101
  // `dev` only where it is wherever it is installed, at each path of npm's
83
- // tree, and with each of pnpm's peers. Throws the reader's LockfileError or
84
- // YamlError for one it refuses.
85
- export function lockedPackages(format: LockfileFormat, text: string): LockedPackage[] {
102
+ // tree, and with each of pnpm's peers; with `review`, each with what its
103
+ // reviews hold it to (LockedPackage `integrity`, `shasum`, `patchHash`),
104
+ // each patch of it apart. Throws the reader's LockfileError or YamlError for
105
+ // one it refuses.
106
+ export function lockedPackages(format: LockfileFormat, text: string, { review = false } = {}): LockedPackage[] {
86
107
  const found = new Map<string, LockedPackage>()
87
108
  for (const pkg of packagesOf(format, text)) {
88
- const one = locked(pkg)
109
+ const one = locked(pkg, format, review)
89
110
  if (!one) continue
90
111
  if (pkg.dev === true) one.dev = true
91
- const key = `${one.source}\0${one.name}@${one.version ?? ''}\0${one.from ?? ''}\0${one.integrity ?? ''}`
112
+ const key = `${one.source}\0${one.name}@${one.version ?? ''}\0${one.from ?? ''}\0${one.integrity ?? ''}\0${one.shasum ?? ''}\0${one.patchHash ?? ''}`
92
113
  const had = found.get(key)
93
114
  if (!had) found.set(key, one)
94
115
  else if (had.dev && !one.dev) delete had.dev
@@ -0,0 +1,49 @@
1
+ // The lockfiles a repository's dependency review holds to, as an admin sets
2
+ // it (repository-dependency-reviews.ts): every one but those it names
3
+ // (`exclude`, naming none for all of them), or those it names alone
4
+ // (`include`). Each is named by its path in the repository, or, ending in
5
+ // `/`, by a directory, every lockfile under it. A directory of a pull
6
+ // request's lockfiles (pull-dependencies.ts) is reviewed where one of its
7
+ // lockfiles is held to; one none of whose are, not at all (`excluded`), nor
8
+ // failing the review where it couldn't be read.
9
+ export interface LockfileScope { mode: 'exclude' | 'include'; paths: string[] }
10
+ export const EVERY_LOCKFILE: LockfileScope = { mode: 'exclude', paths: [] }
11
+ export const MAX_SCOPED_LOCKFILES = 100
12
+ const MAX_PATH = 300
13
+
14
+ // A path as one is named: relative, of `/`-separated names none of which is
15
+ // empty, `.` or `..`, nor holding a backslash or a control character; a
16
+ // directory's ending in `/`.
17
+ function namedPath(path: unknown): path is string {
18
+ if (typeof path !== 'string' || path.length === 0 || path.length > MAX_PATH || path.startsWith('/')) return false
19
+ if (/[\\\p{Cc}]/u.test(path)) return false
20
+ const names = path.endsWith('/') ? path.slice(0, -1).split('/') : path.split('/')
21
+ return names.every(name => name !== '' && name !== '.' && name !== '..')
22
+ }
23
+
24
+ // A scope as a request sends it, each path once, in order; null for one
25
+ // that isn't one.
26
+ export function scopeOf(value: unknown): LockfileScope | null {
27
+ const { mode, paths } = (value ?? {}) as { mode?: unknown; paths?: unknown }
28
+ if ((mode !== 'exclude' && mode !== 'include') || !Array.isArray(paths) || paths.length > MAX_SCOPED_LOCKFILES || !paths.every(namedPath)) return null
29
+ return { mode, paths: [...new Set(paths as string[])].toSorted() }
30
+ }
31
+
32
+ // A scope as stored (JSON, none for every lockfile), every lockfile where
33
+ // it can't be read.
34
+ export function storedScope(text: string | null): LockfileScope {
35
+ if (!text) return EVERY_LOCKFILE
36
+ try { return scopeOf(JSON.parse(text)) ?? EVERY_LOCKFILE } catch { return EVERY_LOCKFILE }
37
+ }
38
+ export const scopeText = (scope: LockfileScope) => isEvery(scope) ? null : JSON.stringify({ mode: scope.mode, paths: scope.paths })
39
+ export const isEvery = (scope: LockfileScope) => scope.mode === 'exclude' && scope.paths.length === 0
40
+
41
+ const names = (path: string, named: string) => named.endsWith('/') ? path.startsWith(named) : path === named
42
+ // Whether the lockfile at `path` is held to.
43
+ export function heldTo(scope: LockfileScope, path: string) {
44
+ const named = scope.paths.some(one => names(path, one))
45
+ return scope.mode === 'include' ? named : !named
46
+ }
47
+ // Whether a directory of lockfiles at `paths` is reviewed: where one is
48
+ // held to, or it names none.
49
+ export const reviewedIn = (scope: LockfileScope, paths: string[]) => paths.length === 0 || paths.some(path => heldTo(scope, path))
@@ -0,0 +1,23 @@
1
+ // The lines a package's files change from one side to another (npm-delta.ts),
2
+ // as @preventive/compare's compareTrees counts them (git's --numstat), off
3
+ // the server's event loop: of its text files, renames found as git finds
4
+ // them, a side not there empty; and, apart, those of its changes it stamps
5
+ // `typeOnly`, declaration files changed in types alone.
6
+ import { parentPort, workerData } from 'node:worker_threads'
7
+ import { compareTrees } from '@preventive/compare'
8
+
9
+ const tree = files => files.map(([name, data]) => ({ name, data }))
10
+ const changes = compareTrees(tree(workerData.base), tree(workerData.other), { paths: 'keys', lines: true, renames: true })
11
+ const answer = { additions: 0, deletions: 0, typeAdditions: 0, typeDeletions: 0, typeFiles: 0 }
12
+ for (const change of changes) {
13
+ if (change.binary) continue
14
+ answer.additions += change.insertions ?? 0
15
+ answer.deletions += change.deletions ?? 0
16
+ if (change.typeOnly !== true) continue
17
+ answer.typeAdditions += change.insertions ?? 0
18
+ answer.typeDeletions += change.deletions ?? 0
19
+ answer.typeFiles++
20
+ }
21
+ // Node worker messages have no browser target origin.
22
+ // oxlint-disable-next-line unicorn/require-post-message-target-origin
23
+ parentPort.postMessage(answer)
@@ -0,0 +1,173 @@
1
+ // The lines a version of a package changes against another, or against none,
2
+ // its whole package's (GET /api/npm/delta), as a list of versions wanting
3
+ // reviews tells them beside each: added and removed (`additions`,
4
+ // `deletions`), as git's --numstat counts them, of their text files, a
5
+ // renamed one diffed with what it was; and, where some of the files changed
6
+ // are declaration files @preventive/compare's compareTrees proves changed in
7
+ // types alone (`typeOnly`, proveTypeOnly: types and comments alone erased,
8
+ // the runtime they leave as it was, an added one's empty or a single bare
9
+ // `export {}`), those of the rest (`typeless`), what a reviewer may pass by
10
+ // leaving those out; and whether a binary file, which they count none of,
11
+ // was added, removed or changed (`binary`), one moved as it was not. Worked
12
+ // out in a worker, off the event loop, as many at once per process as
13
+ // MAX_ACTIVE, their files read only once their turn has come, those waiting
14
+ // past MAX_WAITING refused, each waiting a minute at most and counting in a
15
+ // minute at most; one asked again meanwhile sharing the first's; and kept by
16
+ // both sides' integrities, in this instance and beyond it (`kept`), what a
17
+ // version holds never changing. Access is the caller's to check.
18
+ import { Worker } from 'node:worker_threads'
19
+ import { createHash } from 'node:crypto'
20
+ import type { KeptStore } from './npm-kept.ts'
21
+ import { type Lru, lru } from './lru.ts'
22
+ import { NpmPackageError, type NpmPackageFile, npmFileText } from './npm-packages.ts'
23
+
24
+ export interface NpmDelta { additions: number; deletions: number; typeless: { additions: number; deletions: number } | null; binary: boolean }
25
+
26
+ const MAX_ACTIVE = 2
27
+ const MAX_WAITING = 64
28
+ const DELTA_TIMEOUT_MS = 60_000
29
+ // The text the two sides may hold together, past which none is counted.
30
+ const MAX_TEXT_LENGTH = 64 * 1024 * 1024
31
+
32
+ const deltas: Lru<NpmDelta> = lru(20_000)
33
+ const working = new Map<string, Promise<NpmDelta>>()
34
+ let active = 0
35
+ const waiting: Array<() => void> = []
36
+
37
+ async function turn(deadline: number): Promise<void> {
38
+ if (active < MAX_ACTIVE) { active++; return }
39
+ if (waiting.length >= MAX_WAITING) throw new NpmPackageError(429, 'delta-busy')
40
+ await new Promise<void>((resolve, reject) => {
41
+ const next = () => { clearTimeout(timer); resolve() }
42
+ const timer = setTimeout(() => {
43
+ waiting.splice(waiting.indexOf(next), 1)
44
+ reject(new NpmPackageError(504, 'delta-timeout'))
45
+ }, deadline - Date.now())
46
+ waiting.push(next)
47
+ })
48
+ }
49
+ function done() {
50
+ const next = waiting.shift()
51
+ if (next) next()
52
+ else active--
53
+ }
54
+
55
+ const lines = (told: unknown): told is { additions: number; deletions: number } => typeof told === 'object' && told !== null
56
+ && Number.isSafeInteger((told as { additions?: unknown }).additions) && Number.isSafeInteger((told as { deletions?: unknown }).deletions)
57
+ // A delta as kept, none where it isn't one, or was kept before it told of
58
+ // binary files.
59
+ const deltaOf = (told: unknown): NpmDelta | null => lines(told) && ((told as NpmDelta).typeless === null || lines((told as NpmDelta).typeless))
60
+ && typeof (told as NpmDelta).binary === 'boolean' ? { additions: told.additions, deletions: told.deletions, typeless: (told as NpmDelta).typeless,
61
+ binary: (told as NpmDelta).binary } : null
62
+
63
+ type Counted = { additions: number; deletions: number; typeAdditions: number; typeDeletions: number; typeFiles: number }
64
+ const isCounted = (told: unknown): told is Counted => typeof told === 'object' && told !== null
65
+ && ['additions', 'deletions', 'typeAdditions', 'typeDeletions', 'typeFiles'].every(key => Number.isSafeInteger((told as Record<string, unknown>)[key]))
66
+
67
+ // What the worker counts of `base` against `other`, each its text files' paths and texts.
68
+ function counted(base: [string, string][], other: [string, string][], deadline: number): Promise<Counted> {
69
+ let worker: Worker
70
+ try {
71
+ worker = new Worker(new URL('./npm-delta-worker.js', import.meta.url), {
72
+ workerData: { base, other }, execArgv: ['--disable-warning=ExperimentalWarning'], resourceLimits: { maxOldGenerationSizeMb: 1536 },
73
+ })
74
+ } catch (err) {
75
+ console.warn('managed: a package delta could not start:', err)
76
+ throw new NpmPackageError(503, 'unavailable')
77
+ }
78
+ return new Promise((resolve, reject) => {
79
+ let answer: Counted | null = null, failure = new NpmPackageError(422, 'delta-failed')
80
+ const timer = setTimeout(() => { failure = new NpmPackageError(504, 'delta-timeout'); void worker.terminate() }, deadline - Date.now())
81
+ worker.once('message', (told: unknown) => {
82
+ if (isCounted(told)) answer = told
83
+ void worker.terminate()
84
+ })
85
+ worker.once('error', err => { console.warn('managed: a package delta failed:', err) })
86
+ worker.once('exit', () => {
87
+ clearTimeout(timer)
88
+ if (answer) resolve(answer)
89
+ else reject(failure)
90
+ })
91
+ })
92
+ }
93
+
94
+ // A side's text files, by path, how long their texts are, and its other,
95
+ // binary, files.
96
+ function texts(files: NpmPackageFile[] | null) {
97
+ let length = 0
98
+ const binaries: NpmPackageFile[] = []
99
+ const read = (files ?? []).flatMap(file => {
100
+ const text = npmFileText(file.bytes)
101
+ if (text === null) { binaries.push(file); return [] }
102
+ length += text.length
103
+ return [[file.path, text] as [string, string]]
104
+ })
105
+ return { read, length, binaries }
106
+ }
107
+
108
+ const same = (a: Uint8Array, b: Uint8Array) => Buffer.from(a.buffer, a.byteOffset, a.byteLength).equals(b)
109
+ const digest = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('base64')
110
+
111
+ // Whether `news`, a side's binary files, differ from `olds`, the other's:
112
+ // one added, removed or changed, one moved where it is the same as one that
113
+ // was elsewhere not, matched by their digests, each file looked at once.
114
+ function binaryChanged(olds: NpmPackageFile[], news: NpmPackageFile[]): boolean {
115
+ if (olds.length !== news.length) return true
116
+ const left = new Map(olds.map(({ path, bytes }) => [path, bytes]))
117
+ const moved: Uint8Array[] = []
118
+ for (const { path, bytes } of news) {
119
+ const was = left.get(path)
120
+ if (was && same(was, bytes)) left.delete(path)
121
+ else moved.push(bytes)
122
+ }
123
+ // Those not where they were, by their digests, as many of each as there are.
124
+ const unmoved = new Map<string, number>()
125
+ for (const bytes of left.values()) {
126
+ const key = digest(bytes)
127
+ unmoved.set(key, (unmoved.get(key) ?? 0) + 1)
128
+ }
129
+ return moved.some(bytes => {
130
+ const key = digest(bytes)
131
+ const count = unmoved.get(key) ?? 0
132
+ if (count === 0) return true
133
+ unmoved.set(key, count - 1)
134
+ return false
135
+ })
136
+ }
137
+
138
+ type Side = { integrity: string; files: () => Promise<NpmPackageFile[]> }
139
+
140
+ // The delta of `other` (its integrity, and how to read its files) against
141
+ // `base`, or none, as held, else shared with one asked before it, else
142
+ // looked for where it is kept, else worked out and kept: shared from before
143
+ // it is looked for, lest those asked together each look, find none, and work
144
+ // it out.
145
+ export function npmDelta(base: Side | null, other: Side, kept: KeptStore<NpmDelta> | null = null): Promise<NpmDelta> {
146
+ const key = `${base?.integrity ?? '(empty)'}\0${other.integrity}`
147
+ const known = deltas.get(key)
148
+ if (known) return Promise.resolve(known)
149
+ const shared = working.get(key)
150
+ if (shared) return shared
151
+ const work = worked(key, base, other, kept).finally(() => working.delete(key))
152
+ working.set(key, work)
153
+ return work
154
+ }
155
+
156
+ // As kept, else its turn awaited, then both sides' files read, counted and kept.
157
+ async function worked(key: string, base: Side | null, other: Side, kept: KeptStore<NpmDelta> | null): Promise<NpmDelta> {
158
+ const known = deltaOf((await kept?.read(key))?.value)
159
+ if (known) { deltas.set(key, known); return known }
160
+ await turn(Date.now() + DELTA_TIMEOUT_MS)
161
+ let binary, told
162
+ try {
163
+ const [olds, news] = [texts(base ? await base.files() : null), texts(await other.files())]
164
+ if (olds.length + news.length > MAX_TEXT_LENGTH) throw new NpmPackageError(413, 'package-too-large')
165
+ binary = binaryChanged(olds.binaries, news.binaries)
166
+ told = await counted(olds.read, news.read, Date.now() + DELTA_TIMEOUT_MS)
167
+ } finally { done() }
168
+ const delta = { additions: told.additions, deletions: told.deletions,
169
+ typeless: told.typeFiles > 0 ? { additions: told.additions - told.typeAdditions, deletions: told.deletions - told.typeDeletions } : null, binary }
170
+ deltas.set(key, delta)
171
+ await kept?.write(key, delta, Date.now())
172
+ return delta
173
+ }
@@ -362,25 +362,34 @@ async function askAdvisories(name: string, versions: string[], { githubToken, re
362
362
  }
363
363
  if (result.status !== 200) throw new NpmPackageError(502, 'upstream-unavailable')
364
364
  const index = new Map(versions.map((version, i) => [version, i]))
365
- // npm's registry answers an advisory once a range it covers: here it is one
366
- // row, its ranges joined as semver joins them, the versions and CWEs they
367
- // cover together, and the highest score.
365
+ // npm's registry answers an advisory once a range it covers, and upstream
366
+ // answers the versions OSV's record of it, or its repository, covers beyond
367
+ // npm's under the same id, in a row of their own (as hermes-engine's
368
+ // GHSA-f5x2-xv93-4p23: npm's `<=0.4.3`, OSV's fixed in 0.5.2): here it is
369
+ // one row, the first's (npm's, then OSV's, then the repository's, as
370
+ // upstream orders them) with what it lacks taken from the rest, its ranges
371
+ // joined as semver joins them, the versions and CWEs they cover together,
372
+ // and the worst severity and highest score of them: a version only the
373
+ // repository reports keeps the severity it gives.
368
374
  const merged = new Map<string, NpmAdvisory>()
369
375
  for (const row of result.body) {
370
376
  const affected = row.versions.map(version => index.get(version)).filter(at => at !== undefined)
371
- const key = `${row.source}\0${row.id}`
372
- const known = merged.get(key)
377
+ const own: NpmAdvisory = {
378
+ id: row.id, source: row.source, ...row.ghsa && { ghsa: row.ghsa }, ...row.url && { url: row.url }, ...row.title && { title: row.title },
379
+ ...row.severity && { severity: row.severity }, ...row.cvss !== undefined && { cvss: row.cvss }, cwe: row.cwe, ...row.range && { range: row.range },
380
+ ...row.details && { details: row.details }, affected,
381
+ }
382
+ const known = merged.get(row.id)
373
383
  if (!known) {
374
- merged.set(key, {
375
- id: row.id, source: row.source, ...row.ghsa && { ghsa: row.ghsa }, ...row.url && { url: row.url }, ...row.title && { title: row.title },
376
- ...row.severity && { severity: row.severity }, ...row.cvss !== undefined && { cvss: row.cvss }, cwe: row.cwe, ...row.range && { range: row.range },
377
- ...row.details && { details: row.details }, affected,
378
- })
384
+ merged.set(row.id, own)
379
385
  continue
380
386
  }
381
387
  const range = [known.range, row.range].filter(Boolean).join(' || ')
382
388
  const cvss = Math.max(known.cvss ?? -1, row.cvss ?? -1)
383
- merged.set(key, { ...known, ...range && { range }, ...cvss >= 0 && { cvss }, cwe: [...new Set([...known.cwe, ...row.cwe])], affected: [...known.affected, ...affected] })
389
+ const rank = (severity: string | undefined) => SEVERITY_RANK[severity ?? ''] ?? 4
390
+ const severity = rank(row.severity) < rank(known.severity) ? row.severity : known.severity
391
+ merged.set(row.id, { ...own, ...known, ...range && { range }, ...severity && { severity }, ...cvss >= 0 && { cvss }, cwe: [...new Set([...known.cwe, ...row.cwe])],
392
+ affected: [...known.affected, ...affected] })
384
393
  }
385
394
  return {
386
395
  versions, repository, ...keptAt && { keptAt },
@@ -6,7 +6,12 @@
6
6
  // then, as a version can be deprecated, its package made private, or itself
7
7
  // removed); and, for scans, each package's risk line, for as long as a scan
8
8
  // keeps it, and what a scan read of GitHub, for a day, and of npm's
9
- // advisories, for an hour (repository-risk.ts). Each store removes its own rows kept longer
9
+ // advisories, for an hour (repository-risk.ts); and what the code viewer read
10
+ // of a repository's GitHub, its issues, pull requests and agents' work, for a
11
+ // day (http.ts keptAnswer), and its history, a page from a commit at a time,
12
+ // for a month (repository-history.ts historyFrom), as are the files each of
13
+ // its commits changes and holds (repository-changes.ts), and the packages a
14
+ // pull request's lockfiles add (pull-dependencies.ts). Each store removes its own rows kept longer
10
15
  // than that as it writes, at most once an hour a database (pruneKept), so
11
16
  // that rows no read will take again don't pile up.
12
17
  // The database is an optimization: a failure reads as a miss, and writes
@@ -22,6 +27,8 @@ export interface KeptStore<T> {
22
27
  // Those of `keys` kept, by key, in one read of the database.
23
28
  readMany(keys: string[]): Promise<Map<string, { at: number; value: T }>>
24
29
  write(key: string, value: T, at: number): Promise<void>
30
+ // Removes what is kept by `key`.
31
+ remove(key: string): Promise<void>
25
32
  }
26
33
 
27
34
  // When each database last had the rows under each prefix removed.
@@ -63,6 +70,9 @@ function keptStore<T>(db: UpstreamCacheStore, type: string, keptMs: number, debu
63
70
  try { await db.setUpstreamCacheEntry(keyOf(key), JSON.stringify({ at, value }), at) } catch (error) { warn('write', error) }
64
71
  await prune(at)
65
72
  },
73
+ async remove(key) {
74
+ try { await db.deleteUpstreamCacheEntries(keyOf(key), Number.MAX_SAFE_INTEGER) } catch (error) { warn('remove', error) }
75
+ },
66
76
  }
67
77
  }
68
78
 
@@ -83,7 +93,44 @@ export const riskLineStore = <T>(db: UpstreamCacheStore, keptMs: number, debug =
83
93
  // What a scan read, each read for keptMs (repository-risk.ts): of GitHub, a
84
94
  // repository's lockfiles as listed at a commit (`listings`) and a lockfile's
85
95
  // 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; and the
87
- // repositories a scan in parts told whole (`progress`).
88
- export const scanReadStore = <T>(db: UpstreamCacheStore, type: 'listings' | 'lockfiles' | 'advisories' | 'progress', keptMs: number, debug = false): KeptStore<T> =>
96
+ // lockfile's advisories by its blob (`advisories`), for RISK_KEPT_MS; the
97
+ // repositories a scan in parts told whole (`progress`); a repository's
98
+ // Actions' files by the commit read (`action-files`), and the actions and
99
+ // reusable workflows of others they name, each resolved (`actions`), of
100
+ // public repositories alone (repository-actions.ts).
101
+ export const scanReadStore = <T>(db: UpstreamCacheStore, type: 'listings' | 'lockfiles' | 'advisories' | 'progress' | 'action-files' | 'actions', keptMs: number, debug = false): KeptStore<T> =>
89
102
  keptStore<T>(db, `repository-risk/${type}`, keptMs, debug)
103
+ // What the code viewer read of a repository (http.ts readWholeRepository), by
104
+ // its id and what was asked, each kept REPOSITORY_READ_KEPT_MS.
105
+ export const REPOSITORY_READ_KEPT_MS = 24 * 60 * 60_000
106
+ export const repositoryReadStore = (db: UpstreamCacheStore, debug = false): KeptStore<object> =>
107
+ keptStore<object>(db, 'repository/reads', REPOSITORY_READ_KEPT_MS, debug)
108
+ // Pages of a repository's history (repository-history.ts historyFrom), by its
109
+ // id and the commit each starts at, which never change: kept a month after
110
+ // they were last read from GitHub.
111
+ export const HISTORY_PAGE_KEPT_MS = 30 * 24 * 60 * 60_000
112
+ export const historyPageStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/history', HISTORY_PAGE_KEPT_MS, debug)
113
+ // The pull requests squashed into a repository's commits, by the commit
114
+ // (repository-history.ts squashedPulls): kept as its history's pages are.
115
+ export const historySquashStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/squashes', HISTORY_PAGE_KEPT_MS, debug)
116
+ // The files each of a repository's commits changes (repository-changes.ts
117
+ // repositoryChanges), by the commit, which never change: kept as its
118
+ // history's pages are.
119
+ export const historyChangeStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/changes', HISTORY_PAGE_KEPT_MS, debug)
120
+ // The files a repository's commit holds (repository-changes.ts
121
+ // repositoryTree), by the commit, which never change: kept as its history's
122
+ // pages are.
123
+ export const historyTreeStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/trees', HISTORY_PAGE_KEPT_MS, debug)
124
+ // The packages a repository's pull request's lockfiles add
125
+ // (pull-dependencies.ts), by the commits of its base and head, which never
126
+ // change: kept as its history's pages are.
127
+ export const pullDependencyStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/pull-dependencies', HISTORY_PAGE_KEPT_MS, debug)
128
+ // The versions a repository's lockfiles locked at the commit its dependencies
129
+ // are reviewed since (pull-dependency-reviews.ts reviewBaseline), by the
130
+ // commit, which never change: kept as its history's pages are, never stored
131
+ // as reviews.
132
+ // The lines a version changes against another, or none (npm-delta.ts), by
133
+ // both sides' integrities, each kept HISTORY_PAGE_KEPT_MS.
134
+ export const npmDeltaStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'npm/deltas', HISTORY_PAGE_KEPT_MS, debug)
135
+ export const reviewBaselineStore = (db: UpstreamCacheStore, debug = false): KeptStore<{ versions: string[]; unread: string[] }> =>
136
+ keptStore<{ versions: string[]; unread: string[] }>(db, 'repository/review-baselines', HISTORY_PAGE_KEPT_MS, debug)
@@ -5,8 +5,7 @@
5
5
  import { Buffer } from 'node:buffer'
6
6
  import { homedir } from 'node:os'
7
7
  import { join, resolve } from 'node:path'
8
- import { promisify } from 'node:util'
9
- import { gunzip } from 'node:zlib'
8
+ import { CompressionError, decompress } from '@preventive/archive/compression.js'
10
9
  import { encodeBrotli } from './brotli.ts'
11
10
  import { MAX_NPM_PACKAGE_BYTES, MAX_NPM_PACKAGE_FILES, NPM_REGISTRY, NpmPackageError, type NpmPackageFile, type NpmVersionDocument, askUpstream,
12
11
  npmFileRows, npmPublishTime, npmToken, readNpmTar } from './npm-packages.ts'
@@ -21,8 +20,6 @@ const TARBALL_TIMEOUT_MS = 120_000
21
20
  // and their encoded JSON.
22
21
  const MAX_ACTIVE_LOADS = 4
23
22
 
24
- const gunzipAsync = promisify(gunzip)
25
-
26
23
  // A load's result, shared by its readers, and a reader's release of it.
27
24
  export interface NpmLoad<T> { result: Promise<T>; release: () => void }
28
25
 
@@ -43,9 +40,16 @@ const refused = <T>(status: number, code: string): NpmLoad<T> => {
43
40
  function shared<T>(key: string, doc: NpmVersionDocument, work: () => Promise<T>): NpmLoad<T> {
44
41
  const { unpackedSize, fileCount } = doc.dist
45
42
  if ((unpackedSize ?? 0) > MAX_NPM_PACKAGE_BYTES || (fileCount ?? 0) > MAX_NPM_PACKAGE_FILES) return refused(413, 'package-too-large')
43
+ return sharedLoad(key, work, 'npm-busy')
44
+ }
45
+
46
+ // A load among the active ones, by `key`, refused as `busy` past them: a
47
+ // repository directory's (repository-code.ts) as a package's, each holding
48
+ // alike a tarball, its tar, its files and their encoded JSON.
49
+ export function sharedLoad<T>(key: string, work: () => Promise<T>, busy: string): NpmLoad<T> {
46
50
  let load = loads.get(key)
47
51
  if (!load) {
48
- if (loads.size >= MAX_ACTIVE_LOADS) return refused(429, 'npm-busy')
52
+ if (loads.size >= MAX_ACTIVE_LOADS) return refused(429, busy)
49
53
  const created: Load = { job: work(), readers: 0, settled: false }
50
54
  const settle = () => {
51
55
  created.settled = true
@@ -125,14 +129,17 @@ async function readTarball(doc: NpmVersionDocument): Promise<Uint8Array> {
125
129
  return bytes
126
130
  }
127
131
 
128
- async function readFiles(tarball: Uint8Array): Promise<NpmPackageFile[]> {
129
- let tar: Buffer
130
- try { tar = await gunzipAsync(tarball, { maxOutputLength: MAX_TAR_BYTES }) }
132
+ // A gzipped tarball's files, its top directory left out, as npm extracts
133
+ // a package's: a repository tree's as GitHub archives it, too, its symlinks
134
+ // let climb as readNpmTar's `symlinksAbove` says.
135
+ export async function readTarballFiles(tarball: Uint8Array, options: { symlinksAbove?: number } = {}): Promise<NpmPackageFile[]> {
136
+ let tar: Uint8Array
137
+ try { tar = await decompress(tarball, 'gzip', { limit: MAX_TAR_BYTES }) }
131
138
  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')
139
+ const limited = err instanceof CompressionError && err.limited
140
+ throw new NpmPackageError(limited ? 413 : 422, limited ? 'package-too-large' : 'bad-tarball')
134
141
  }
135
- return readNpmTar(tar)
142
+ return readNpmTar(tar, options)
136
143
  }
137
144
 
138
145
  // The version's tarball, for download.
@@ -142,7 +149,7 @@ export function loadNpmTarball(doc: NpmVersionDocument): NpmLoad<Uint8Array> {
142
149
 
143
150
  // The version's files, for one of them to be pretty-printed (pretty-print.ts).
144
151
  export function loadNpmFiles(doc: NpmVersionDocument): NpmLoad<NpmPackageFile[]> {
145
- return shared(`files ${doc.dist.integrity} ${doc.dist.tarball}`, doc, async () => readFiles(await readTarball(doc)))
152
+ return shared(`files ${doc.dist.integrity} ${doc.dist.tarball}`, doc, async () => readTarballFiles(await readTarball(doc)))
146
153
  }
147
154
 
148
155
  // The version as the viewer reads it, as brotli-encoded JSON: `{ name,
@@ -158,7 +165,7 @@ export function loadNpmFiles(doc: NpmVersionDocument): NpmLoad<NpmPackageFile[]>
158
165
  export function loadNpmPackageBody(doc: NpmVersionDocument, known: () => object = () => ({})): NpmLoad<Buffer> {
159
166
  return shared(`package ${doc.dist.integrity} ${doc.dist.tarball} ${doc.private}`, doc, async () => {
160
167
  const tarball = await readTarball(doc)
161
- const files = await readFiles(tarball)
168
+ const files = await readTarballFiles(tarball)
162
169
  return encodeBrotli(Buffer.from(JSON.stringify({
163
170
  name: doc.name, version: doc.version, private: doc.private, integrity: doc.dist.integrity, tarballSize: tarball.byteLength,
164
171
  published: npmPublishTime(doc.name, doc.version), manifest: doc.manifest, files: npmFileRows(files), known: known(),