@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50

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 (366) 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 +3 -0
  6. package/client/dependency-paths.js +36 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +115 -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 +357 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-compare-versions.js +172 -0
  17. package/common/bundle-comparison.d.ts +8 -0
  18. package/common/bundle-comparison.js +62 -0
  19. package/common/bundle-metadata.d.ts +15 -0
  20. package/common/bundle-metadata.js +226 -0
  21. package/common/bundle-pkg-of.d.ts +1 -0
  22. package/common/bundle-pkg-of.js +90 -0
  23. package/common/bundle-reasons.d.ts +2 -0
  24. package/common/bundle-reasons.js +21 -0
  25. package/common/bundle-source-links.d.ts +6 -0
  26. package/common/bundle-source-links.js +216 -0
  27. package/common/bundle-sourcemap.d.ts +10 -0
  28. package/common/bundle-sourcemap.js +100 -0
  29. package/common/bundle-sources.d.ts +7 -0
  30. package/common/bundle-sources.js +333 -0
  31. package/common/bundle-stats.js +41 -0
  32. package/common/bundle-tabs.js +1 -0
  33. package/common/code-language.js +65 -0
  34. package/common/default-scan-models.ts +31 -0
  35. package/common/dependency-reviews.d.ts +18 -0
  36. package/common/dependency-reviews.js +64 -0
  37. package/common/finding-duplicates.js +58 -0
  38. package/common/finding-filters.js +274 -0
  39. package/common/finding-id.js +47 -0
  40. package/common/github-issue-labels.d.ts +1 -0
  41. package/common/github-issue-labels.js +11 -0
  42. package/common/github-pr.ts +56 -0
  43. package/common/gzip.js +59 -0
  44. package/common/line-diff.d.ts +8 -0
  45. package/common/line-diff.js +57 -0
  46. package/common/linked-workspace-groups.js +63 -0
  47. package/common/managed/agent-tasks.ts +292 -0
  48. package/common/managed/comments.ts +34 -0
  49. package/common/managed/finding-detail.d.ts +2 -0
  50. package/common/managed/finding-detail.js +32 -0
  51. package/common/managed/github-agents.ts +114 -0
  52. package/common/managed/github-logins.ts +49 -0
  53. package/common/managed/npm-packages.d.ts +5 -0
  54. package/common/managed/npm-packages.js +34 -0
  55. package/common/managed/permissions.ts +31 -0
  56. package/common/managed/report-content.ts +72 -0
  57. package/common/managed/report-filter.ts +109 -0
  58. package/common/managed/repository-alias.ts +65 -0
  59. package/common/managed/roles.ts +35 -0
  60. package/common/managed/routes.d.ts +2 -0
  61. package/common/managed/routes.js +347 -0
  62. package/common/managed/scan-models.ts +6 -0
  63. package/common/managed/team-app.d.ts +5 -0
  64. package/common/managed/team-app.js +31 -0
  65. package/common/managed/triage.ts +83 -0
  66. package/common/managed/upload-seal.ts +126 -0
  67. package/common/pretty-print.d.ts +5 -0
  68. package/common/pretty-print.js +24 -0
  69. package/common/revalidation-conflicts.js +48 -0
  70. package/common/revalidation-input-groups.js +56 -0
  71. package/common/save-error-reason.ts +20 -7
  72. package/common/scan-server.ts +13 -0
  73. package/common/security-links.d.ts +5 -0
  74. package/common/security-links.js +33 -0
  75. package/common/server-info.ts +46 -0
  76. package/common/utf8.d.ts +3 -0
  77. package/common/utf8.js +45 -0
  78. package/common/workspace-app.js +39 -0
  79. package/common/workspace-groups.js +176 -0
  80. package/out/activity.js +4 -0
  81. package/out/brotli-fallback.js +3 -3
  82. package/out/chunk-2WDZPXVL.js +3 -0
  83. package/out/chunk-2XUEZHIP.js +28 -0
  84. package/out/chunk-2ZDZWLFY.js +1 -0
  85. package/out/chunk-4HI2MSAV.js +17 -0
  86. package/out/chunk-4XMP7YVH.js +36 -0
  87. package/out/chunk-6LNQTKAV.js +52 -0
  88. package/out/chunk-6S2U2UYG.js +232 -0
  89. package/out/chunk-75YLHBCM.js +3 -0
  90. package/out/chunk-7BKKYVWR.js +7 -0
  91. package/out/chunk-7C7DWL7I.js +1 -0
  92. package/out/chunk-7VAWINQF.js +6 -0
  93. package/out/chunk-C4Z6ZBXV.js +10 -0
  94. package/out/chunk-FC65TPUB.js +1 -0
  95. package/out/chunk-FKBXNUW4.js +1 -0
  96. package/out/chunk-GDPCLSOS.js +2 -0
  97. package/out/chunk-GO7FT4OD.js +1 -0
  98. package/out/chunk-HTBYHSPG.js +1 -0
  99. package/out/chunk-IYPY47MC.js +1 -0
  100. package/out/chunk-IZ7POWI3.js +18 -0
  101. package/out/chunk-JCIIBLKR.js +1 -0
  102. package/out/chunk-NRPC74AM.js +1 -0
  103. package/out/chunk-OOJM4CTU.js +1 -0
  104. package/out/chunk-PBZPYIG7.js +1 -0
  105. package/out/chunk-PNHBZGDD.js +1 -0
  106. package/out/chunk-PWFUOLZK.js +17 -0
  107. package/out/chunk-PXK7EU4V.js +1 -0
  108. package/out/chunk-QB4CBOX2.js +1 -0
  109. package/out/chunk-R3NZFXVR.js +1 -0
  110. package/out/chunk-R5O6FAW4.js +2 -0
  111. package/out/chunk-SXV2T5C4.js +80 -0
  112. package/out/chunk-TQJDYHVE.js +2 -0
  113. package/out/chunk-TUII34K2.js +1 -0
  114. package/out/chunk-UK7RGJT6.js +3 -0
  115. package/out/chunk-VFWMQ3LG.js +2 -0
  116. package/out/chunk-VH4XMZIW.js +2 -0
  117. package/out/chunk-WCPJDAYA.js +10 -0
  118. package/out/chunk-WFYLMF6Z.js +10 -0
  119. package/out/chunk-WYLQWYLA.js +1 -0
  120. package/out/client-managed-import.js +4 -0
  121. package/out/client-managed.js +33 -0
  122. package/out/client-sync.js +11 -17
  123. package/out/compare.js +35 -0
  124. package/out/dialogs.js +55 -0
  125. package/out/github-issue-authorized.html +3 -0
  126. package/out/github-issue-authorized.js +1 -0
  127. package/out/graph.js +17 -4
  128. package/out/index.html +90 -8
  129. package/out/prism.js +14 -10
  130. package/out/scan.js +2 -0
  131. package/out/stasis.svg +20 -0
  132. package/out/terminal.js +366 -39
  133. package/out/view.css +1 -1
  134. package/out/view.js +19 -144
  135. package/package.json +362 -59
  136. package/risk/actions/findings.js +161 -0
  137. package/risk/actions/resolve.js +219 -0
  138. package/risk/actions/scripts/commands.js +182 -0
  139. package/risk/actions/scripts/grade.js +113 -0
  140. package/risk/actions/scripts/packages.js +123 -0
  141. package/risk/actions/scripts/shell.js +169 -0
  142. package/risk/actions/uses.js +76 -0
  143. package/risk/actions/workflow.js +110 -0
  144. package/risk/files/characters.js +196 -0
  145. package/risk/files/hosts.js +205 -0
  146. package/risk/files/readability.js +177 -0
  147. package/risk/files/runs-code.js +615 -0
  148. package/risk/files/script.js +118 -0
  149. package/risk/files/stamps.js +47 -0
  150. package/risk/index.d.ts +254 -0
  151. package/risk/index.js +11 -0
  152. package/risk/npm/assess.js +285 -0
  153. package/risk/npm/combined.js +68 -0
  154. package/risk/npm/compare.js +257 -0
  155. package/risk/npm/dependencies.js +172 -0
  156. package/risk/npm/diff.js +79 -0
  157. package/risk/npm/downloads.js +84 -0
  158. package/risk/npm/files.js +445 -0
  159. package/risk/npm/held.js +63 -0
  160. package/risk/npm/install-scripts.js +34 -0
  161. package/risk/npm/malware.js +36 -0
  162. package/risk/npm/manifest.js +65 -0
  163. package/risk/npm/new-dependency.js +60 -0
  164. package/risk/npm/old-version.js +108 -0
  165. package/risk/npm/registry.js +388 -0
  166. package/risk/npm/release.js +85 -0
  167. package/risk/npm/score.js +59 -0
  168. package/risk/npm/specs.js +124 -0
  169. package/risk/npm/tarball.js +96 -0
  170. package/server-common/database-config.ts +16 -0
  171. package/server-common/initialize.ts +18 -0
  172. package/server-common/npm-advisories.ts +101 -0
  173. package/{server → server-common}/origin.ts +5 -5
  174. package/server-common/reap.ts +48 -0
  175. package/server-common/scan-config.ts +19 -0
  176. package/server-common/standalone.ts +29 -0
  177. package/server-common/storage-crypto.ts +182 -0
  178. package/server-common/storage-log.ts +38 -0
  179. package/server-common/vercel-blob.ts +103 -0
  180. package/server-e2e/app.ts +487 -0
  181. package/{server → server-e2e}/bus-receiver.ts +2 -1
  182. package/{server → server-e2e}/cli.js +9 -4
  183. package/{server → server-e2e}/config.ts +50 -42
  184. package/{server → server-e2e}/db-neon.ts +2 -2
  185. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  186. package/{server → server-e2e}/db-stmt.ts +2 -2
  187. package/{server → server-e2e}/db.ts +7 -7
  188. package/{server → server-e2e}/http.ts +44 -13
  189. package/{server → server-e2e}/hub.ts +40 -9
  190. package/server-e2e/index.ts +17 -0
  191. package/server-e2e/lifecycle.ts +95 -0
  192. package/{server → server-e2e}/neon-driver.ts +2 -2
  193. package/{server → server-e2e}/npm-proxy.ts +12 -145
  194. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  195. package/{server → server-e2e}/objstore/blob.ts +1 -1
  196. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  197. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  198. package/{server → server-e2e}/objstore/init.ts +19 -27
  199. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  200. package/server-e2e/objstore/rest-deny.ts +28 -0
  201. package/server-e2e/objstore/rest-mint.ts +224 -0
  202. package/{server → server-e2e}/objstore/rest.ts +3 -3
  203. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  204. package/{server → server-e2e}/objstore/store.ts +7 -7
  205. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  206. package/{server → server-e2e}/pubsub.ts +12 -9
  207. package/{server → server-e2e}/sign.ts +4 -4
  208. package/{server → server-e2e}/sse-server.ts +60 -33
  209. package/{server → server-e2e}/static.ts +31 -21
  210. package/{server → server-e2e}/sync-handlers.ts +64 -30
  211. package/{server → server-e2e}/ws-server.ts +20 -9
  212. package/server-managed/activity.ts +258 -0
  213. package/server-managed/agent-merges.ts +109 -0
  214. package/server-managed/app-agents.ts +21 -0
  215. package/server-managed/avatar-store.ts +13 -0
  216. package/server-managed/blob-store.ts +17 -0
  217. package/server-managed/brotli.ts +10 -0
  218. package/server-managed/bundle-advisories.ts +120 -0
  219. package/server-managed/bundle-advisory-inventory.ts +77 -0
  220. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  221. package/server-managed/bundle-build-diagnostics.js +23 -0
  222. package/server-managed/bundle-build-leases.ts +41 -0
  223. package/server-managed/bundle-build-worker.js +115 -0
  224. package/server-managed/bundle-build.ts +200 -0
  225. package/server-managed/bundle-cache.ts +211 -0
  226. package/server-managed/bundle-catalog.ts +29 -0
  227. package/server-managed/bundle-commits.ts +140 -0
  228. package/server-managed/bundle-store.ts +24 -0
  229. package/server-managed/bundle-summary-cache.ts +130 -0
  230. package/server-managed/bundle.ts +119 -0
  231. package/server-managed/cache-storage.ts +20 -0
  232. package/server-managed/cli.js +21 -0
  233. package/server-managed/combined.ts +47 -0
  234. package/server-managed/comments.ts +151 -0
  235. package/server-managed/commit-coauthors.ts +106 -0
  236. package/server-managed/commit-lockfiles.ts +73 -0
  237. package/server-managed/comparison.ts +41 -0
  238. package/server-managed/config.ts +164 -0
  239. package/server-managed/content-access.ts +15 -0
  240. package/server-managed/crypto.ts +25 -0
  241. package/server-managed/db-methods.ts +1923 -0
  242. package/server-managed/db-neon.ts +458 -0
  243. package/server-managed/db-schema.ts +238 -0
  244. package/server-managed/db-table-names.ts +22 -0
  245. package/server-managed/db.ts +142 -0
  246. package/server-managed/dependabot-access.ts +60 -0
  247. package/server-managed/dependabot-alerts.ts +185 -0
  248. package/server-managed/dependency-review-comments.ts +100 -0
  249. package/server-managed/dependency-reviews.ts +346 -0
  250. package/server-managed/github-accounts.ts +60 -0
  251. package/server-managed/github-actions.ts +71 -0
  252. package/server-managed/github-app.ts +415 -0
  253. package/server-managed/github-errors.ts +70 -0
  254. package/server-managed/github-graphql.ts +39 -0
  255. package/server-managed/github-issue-links.ts +140 -0
  256. package/server-managed/github-issue-oauth.ts +47 -0
  257. package/server-managed/github-issues.ts +152 -0
  258. package/server-managed/github-link-oauth.ts +69 -0
  259. package/server-managed/github-links.ts +204 -0
  260. package/server-managed/github-metadata.ts +213 -0
  261. package/server-managed/github-oauth.ts +238 -0
  262. package/server-managed/github-pulls.ts +227 -0
  263. package/server-managed/github-requests.ts +39 -0
  264. package/server-managed/github-users.ts +30 -0
  265. package/server-managed/http-response.ts +76 -0
  266. package/server-managed/http.ts +5329 -0
  267. package/server-managed/import-triage.ts +51 -0
  268. package/server-managed/index.ts +192 -0
  269. package/server-managed/kept-briefly.ts +37 -0
  270. package/server-managed/kept.ts +28 -0
  271. package/server-managed/link-reports.ts +162 -0
  272. package/server-managed/locked-packages.ts +129 -0
  273. package/server-managed/lockfile-scope.ts +49 -0
  274. package/server-managed/lru.ts +30 -0
  275. package/server-managed/managed-issues.ts +78 -0
  276. package/server-managed/management-catalog.ts +45 -0
  277. package/server-managed/management.ts +99 -0
  278. package/server-managed/npm-delta-worker.js +23 -0
  279. package/server-managed/npm-delta.ts +194 -0
  280. package/server-managed/npm-insights.ts +449 -0
  281. package/server-managed/npm-kept.ts +215 -0
  282. package/server-managed/npm-loads.ts +264 -0
  283. package/server-managed/npm-manifest.ts +115 -0
  284. package/server-managed/npm-packages.ts +454 -0
  285. package/server-managed/npm-provenance.ts +123 -0
  286. package/server-managed/npm-risk.ts +285 -0
  287. package/server-managed/object-storage-disk.ts +148 -0
  288. package/server-managed/object-storage-vercel.ts +67 -0
  289. package/server-managed/object-storage.ts +75 -0
  290. package/server-managed/package-entry-points.ts +51 -0
  291. package/server-managed/pretty-print.ts +434 -0
  292. package/server-managed/public-workspace.ts +238 -0
  293. package/server-managed/pull-comment-reviews.ts +161 -0
  294. package/server-managed/pull-dependencies.ts +348 -0
  295. package/server-managed/pull-dependency-reviews.ts +328 -0
  296. package/server-managed/pull-reviews.ts +205 -0
  297. package/server-managed/queue.ts +39 -0
  298. package/server-managed/release-commits.ts +84 -0
  299. package/server-managed/rename-diff-worker.js +26 -0
  300. package/server-managed/rename-diff.ts +279 -0
  301. package/server-managed/repo-path.ts +21 -0
  302. package/server-managed/report-migration.ts +35 -0
  303. package/server-managed/report-query.ts +10 -0
  304. package/server-managed/report-response.ts +16 -0
  305. package/server-managed/report-sources.ts +178 -0
  306. package/server-managed/report-store.ts +85 -0
  307. package/server-managed/repository-actions.ts +218 -0
  308. package/server-managed/repository-agents.ts +355 -0
  309. package/server-managed/repository-aliases.ts +91 -0
  310. package/server-managed/repository-browser.ts +294 -0
  311. package/server-managed/repository-changes.ts +284 -0
  312. package/server-managed/repository-code.ts +215 -0
  313. package/server-managed/repository-dependency-reviews.ts +165 -0
  314. package/server-managed/repository-discovery.ts +82 -0
  315. package/server-managed/repository-history.ts +432 -0
  316. package/server-managed/repository-issues.ts +195 -0
  317. package/server-managed/repository-policy.ts +27 -0
  318. package/server-managed/repository-pulls.ts +156 -0
  319. package/server-managed/repository-pulse.ts +460 -0
  320. package/server-managed/repository-risk-assess.ts +105 -0
  321. package/server-managed/repository-risk-audit.ts +79 -0
  322. package/server-managed/repository-risk-read.ts +170 -0
  323. package/server-managed/repository-risk.ts +491 -0
  324. package/server-managed/repository-squashes.ts +168 -0
  325. package/server-managed/request-timing.ts +63 -0
  326. package/server-managed/reviewed-dependencies.ts +150 -0
  327. package/server-managed/revisions.ts +35 -0
  328. package/server-managed/scan-downloads.ts +87 -0
  329. package/server-managed/session.ts +132 -0
  330. package/server-managed/slugs.ts +38 -0
  331. package/server-managed/solidity-entry-points.ts +71 -0
  332. package/server-managed/sql-postgres.ts +34 -0
  333. package/server-managed/sql.ts +88 -0
  334. package/server-managed/static.ts +28 -0
  335. package/server-managed/storage-command.ts +16 -0
  336. package/server-managed/storage-db.ts +183 -0
  337. package/server-managed/storage-encryption.ts +290 -0
  338. package/server-managed/storage-maintenance.ts +259 -0
  339. package/server-managed/storage-payload.ts +121 -0
  340. package/server-managed/storage-stores.ts +91 -0
  341. package/server-managed/storage.ts +82 -0
  342. package/server-managed/team-app.ts +134 -0
  343. package/server-managed/team-catalog.ts +7 -0
  344. package/server-managed/team-feed.ts +180 -0
  345. package/server-managed/team-npm-scopes.ts +106 -0
  346. package/server-managed/team-reports.ts +307 -0
  347. package/server-managed/triage-response.ts +16 -0
  348. package/server-managed/type-only-worker.js +19 -0
  349. package/server-managed/type-only.ts +105 -0
  350. package/server-managed/uploads.ts +106 -0
  351. package/server-managed/upstream-cache.ts +243 -0
  352. package/server-managed/view-as.ts +58 -0
  353. package/server-managed/workspace-shares.ts +169 -0
  354. package/server.ts +70 -0
  355. package/server/index.ts +0 -480
  356. package/server/lifecycle.ts +0 -208
  357. /package/{LICENSE → LICENSE-MIT} +0 -0
  358. /package/{server → server-e2e}/auth.ts +0 -0
  359. /package/{server → server-e2e}/config.example.json +0 -0
  360. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  361. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  362. /package/{server → server-e2e}/objstore/sign.ts +0 -0
  363. /package/{server → server-e2e}/peer.ts +0 -0
  364. /package/{server → server-e2e}/sse-session.ts +0 -0
  365. /package/{server → server-e2e}/util.ts +0 -0
  366. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,215 @@
1
+ // What the npm viewer and repository risk scans keep in the database
2
+ // (managed_upstream_cache, upstream-cache.ts), beyond the instance, so that
3
+ // neither asks for it again on every read, nor after a restart or on another
4
+ // instance: public versions' documents, of the fields read of them
5
+ // (npm-manifest.ts npmVersionFields), for a day (readNpmVersion asks npm again
6
+ // then, as a version can be deprecated, its package made private, or itself
7
+ // removed), and when each was published, for a month (npmPublishTimeStore);
8
+ // and, for scans, each package's risk line, for as long as a scan
9
+ // keeps it, and what a scan read of GitHub, for a day, and of npm's
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), its trees as the
15
+ // code viewer reads them (codeTreeStore), the packages a pull request's
16
+ // lockfiles add (pull-dependencies.ts), and the lockfiles a commit lists and
17
+ // each one's packages, as its dependency review reads them
18
+ // (commit-lockfiles.ts); and the commit the code viewer keeps its
19
+ // directories at, for as long as it does (codeHeadStore). Each store removes
20
+ // its own rows kept longer than that as it writes, at most once an hour a
21
+ // database (pruneKept), so that rows no read will take again don't pile up.
22
+ // The database is an optimization: a failure reads as a miss, and writes
23
+ // nothing.
24
+ import { createHash } from 'node:crypto'
25
+ import { plainObject } from './npm-manifest.ts'
26
+ import { type NpmPublishTimeStore, type NpmVersionStore, VERSION_KEPT_MS } from './npm-packages.ts'
27
+ import type { UpstreamCacheStore } from './upstream-cache.ts'
28
+
29
+ // A value kept by key, as JSON, `at` when it was.
30
+ export interface KeptStore<T> {
31
+ read(key: string): Promise<{ at: number; value: T } | null>
32
+ // Those of `keys` kept, by key, in one read of the database.
33
+ readMany(keys: string[]): Promise<Map<string, { at: number; value: T }>>
34
+ // Those of `first` kept; where none is, those of `then`, in one read of the
35
+ // database (`first` whether they are `first`'s).
36
+ readFirst(first: string[], then: string[]): Promise<{ first: boolean; found: Map<string, { at: number; value: T }> }>
37
+ write(key: string, value: T, at: number): Promise<void>
38
+ // Removes what is kept by `key`.
39
+ remove(key: string): Promise<void>
40
+ }
41
+
42
+ // When each database last had the rows under each prefix removed.
43
+ const PRUNE_EVERY_MS = 60 * 60_000
44
+ const pruned = new WeakMap<UpstreamCacheStore, Map<string, number>>()
45
+ // Who sees each database's prunes through (index.ts: its app, which waits
46
+ // for them before closing it, and on serverless keeps its invocation alive
47
+ // till they settle), as a write no longer waits for one.
48
+ const pruneOwners = new WeakMap<UpstreamCacheStore, (prune: Promise<unknown>) => void>()
49
+ export function ownKeptPrunes(db: UpstreamCacheStore, own: (prune: Promise<unknown>) => void) {
50
+ pruneOwners.set(db, own)
51
+ }
52
+
53
+ // Removes the rows under `prefix` kept `keptMs` before `at`, at most once an
54
+ // hour a database, as rows under it are written: here, and risk's downloads
55
+ // (npm-risk.ts), each one run handed to the database's owner. A failure is
56
+ // `warn`ed of, and removes nothing.
57
+ export function pruneKept(db: UpstreamCacheStore, prefix: string, keptMs: number, at: number, warn: (error: unknown) => void): Promise<void> {
58
+ const times = pruned.get(db) ?? pruned.set(db, new Map()).get(db)!
59
+ if (at - (times.get(prefix) ?? -Infinity) < PRUNE_EVERY_MS) return Promise.resolve()
60
+ times.set(prefix, at)
61
+ const prune = (async () => { try { await db.deleteUpstreamCacheEntries(prefix, at - keptMs) } catch (error) { warn(error) } })()
62
+ pruneOwners.get(db)?.(prune)
63
+ return prune
64
+ }
65
+
66
+ // Values of `type`, each read for `keptMs` after it was kept.
67
+ function keptStore<T>(db: UpstreamCacheStore, type: string, keptMs: number, debug: boolean): KeptStore<T> {
68
+ const warn = (what: string, error: unknown) => { if (debug) console.warn(`managed: kept ${type} ${what} failed:`, error) }
69
+ // Kept for good (Infinity): none removed.
70
+ const prune = (at: number) => keptMs === Infinity ? Promise.resolve() : pruneKept(db, `${type}/`, keptMs, at, error => { warn('prune', error) })
71
+ // Keys of any length or character, as the database's: hashed.
72
+ const keyOf = (key: string) => `${type}/${createHash('sha256').update(key).digest('hex')}`
73
+ const valueOf = (row: string | null | undefined) => {
74
+ const kept: unknown = JSON.parse(row ?? 'null')
75
+ return plainObject(kept) && typeof kept['at'] === 'number' && 'value' in kept ? { at: kept['at'], value: kept['value'] as T } : null
76
+ }
77
+ return {
78
+ async read(key) {
79
+ try { return valueOf(await db.getUpstreamCacheEntry(keyOf(key))) } catch (error) { warn('read', error); return null }
80
+ },
81
+ async readMany(keys) {
82
+ try {
83
+ const rows = await db.getUpstreamCacheEntries(keys.map(keyOf))
84
+ return new Map(keys.flatMap(key => { const kept = valueOf(rows.get(keyOf(key))); return kept ? [[key, kept] as const] : [] }))
85
+ } catch (error) { warn('read', error); return new Map() }
86
+ },
87
+ async readFirst(first, then) {
88
+ try {
89
+ const { first: answered, entries } = await db.getFirstUpstreamCacheEntries(first.map(keyOf), then.map(keyOf))
90
+ const asked = answered ? first : then
91
+ return { first: answered, found: new Map(asked.flatMap(key => { const kept = valueOf(entries.get(keyOf(key))); return kept ? [[key, kept] as const] : [] })) }
92
+ } catch (error) { warn('read', error); return { first: false, found: new Map() } }
93
+ },
94
+ async write(key, value, at) {
95
+ try { await db.setUpstreamCacheEntry(keyOf(key), JSON.stringify({ at, value }), at) } catch (error) { warn('write', error) }
96
+ // Behind the write, not before it is done: the prune reads the whole
97
+ // table (no index serves its prefix), which no writer need wait on.
98
+ // Its hour is marked before it runs, so writes meanwhile start none,
99
+ // and its database's owner sees it through (ownKeptPrunes).
100
+ void prune(at)
101
+ },
102
+ async remove(key) {
103
+ try { await db.deleteUpstreamCacheEntries(keyOf(key), Number.MAX_SAFE_INTEGER) } catch (error) { warn('remove', error) }
104
+ },
105
+ }
106
+ }
107
+
108
+ export function npmVersionStore(db: UpstreamCacheStore, debug = false): NpmVersionStore {
109
+ const kept = keptStore<Record<string, unknown>>(db, 'npm/version-documents', VERSION_KEPT_MS, debug)
110
+ return {
111
+ async read(name, version) {
112
+ const hit = await kept.read(`${name}@${version}`)
113
+ return hit && plainObject(hit.value) ? { at: hit.at, json: hit.value } : null
114
+ },
115
+ async readMany(versions) {
116
+ const hits = await kept.readMany(versions.map(({ name, version }) => `${name}@${version}`))
117
+ return new Map([...hits].flatMap(([key, hit]) => plainObject(hit.value) ? [[key, { at: hit.at, json: hit.value }] as const] : []))
118
+ },
119
+ write: (name, version, json) => kept.write(`${name}@${version}`, json, Date.now()),
120
+ }
121
+ }
122
+
123
+ // When public versions were published (npm-packages.ts npmPublishTime), which
124
+ // never changes once they are: kept a month, and kept again by the next view
125
+ // of the version past it.
126
+ const PUBLISH_TIME_KEPT_MS = 30 * 24 * 60 * 60_000
127
+ export function npmPublishTimeStore(db: UpstreamCacheStore, debug = false): NpmPublishTimeStore {
128
+ const kept = keptStore<string>(db, 'npm/publish-times', PUBLISH_TIME_KEPT_MS, debug)
129
+ return {
130
+ async read(name, version) {
131
+ const hit = await kept.read(`${name}@${version}`)
132
+ return typeof hit?.value === 'string' ? hit.value : null
133
+ },
134
+ write: (name, version, at) => kept.write(`${name}@${version}`, at, Date.now()),
135
+ }
136
+ }
137
+
138
+ // Risk lines, each kept `keptMs`, for good where Infinity, a scan telling one
139
+ // past RISK_KEPT_MS as stale (repository-risk.ts).
140
+ export const riskLineStore = <T>(db: UpstreamCacheStore, keptMs: number, debug = false): KeptStore<T> => keptStore<T>(db, 'repository-risk/lines', keptMs, debug)
141
+ // What a scan read, each read for keptMs (repository-risk.ts): of GitHub, a
142
+ // repository's lockfiles as listed at a commit (`listings`) and a lockfile's
143
+ // packages by its blob (`lockfiles`), for SCAN_READ_KEPT_MS; of npm, a
144
+ // lockfile's advisories by its blob (`advisories`), for RISK_KEPT_MS; the
145
+ // repositories a scan in parts told whole (`progress`); a repository's
146
+ // Actions' files by the commit read (`action-files`), and the actions and
147
+ // reusable workflows of others they name, each resolved (`actions`), of
148
+ // public repositories alone (repository-actions.ts).
149
+ export const scanReadStore = <T>(db: UpstreamCacheStore, type: 'listings' | 'lockfiles' | 'advisories' | 'progress' | 'action-files' | 'actions', keptMs: number, debug = false): KeptStore<T> =>
150
+ keptStore<T>(db, `repository-risk/${type}`, keptMs, debug)
151
+ // What the code viewer read of a repository (http.ts readWholeRepository), by
152
+ // its id and what was asked, each kept REPOSITORY_READ_KEPT_MS.
153
+ export const REPOSITORY_READ_KEPT_MS = 24 * 60 * 60_000
154
+ export const repositoryReadStore = (db: UpstreamCacheStore, debug = false): KeptStore<object> =>
155
+ keptStore<object>(db, 'repository/reads', REPOSITORY_READ_KEPT_MS, debug)
156
+ // Pages of a repository's history (repository-history.ts historyFrom), by its
157
+ // id and the commit each starts at, which never change: kept a month after
158
+ // they were last read from GitHub.
159
+ export const HISTORY_PAGE_KEPT_MS = 30 * 24 * 60 * 60_000
160
+ export const historyPageStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/history', HISTORY_PAGE_KEPT_MS, debug)
161
+ // The pull requests squashed into a repository's commits, by the commit
162
+ // (repository-squashes.ts squashedPulls): kept as its history's pages are.
163
+ export const historySquashStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/squashes', HISTORY_PAGE_KEPT_MS, debug)
164
+ // Whether a pull request brought a commit, by its repository and commit, as
165
+ // the Agents tab looks it up of those an agent marked (repository-agents.ts
166
+ // pulledCommits), kept as the history is.
167
+ export const commitPullStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/commit-pulls', HISTORY_PAGE_KEPT_MS, debug)
168
+ // A repository's pull requests, the most recently updated first, as the
169
+ // Pulse and the Agents tab read them (repository-pulls.ts readPulls), by its
170
+ // id: kept as its history's pages are, those updated since read on.
171
+ export const pullListStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/pulls', HISTORY_PAGE_KEPT_MS, debug)
172
+ // The files each of a repository's commits changes (repository-changes.ts
173
+ // repositoryChanges), by the commit, which never change: kept as its
174
+ // history's pages are.
175
+ export const historyChangeStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/changes', HISTORY_PAGE_KEPT_MS, debug)
176
+ // The files a repository's commit holds (repository-changes.ts
177
+ // repositoryTree), by the commit, which never change: kept as its history's
178
+ // pages are.
179
+ export const historyTreeStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/trees', HISTORY_PAGE_KEPT_MS, debug)
180
+ // What the code viewer read of a repository's trees (http.ts
181
+ // repositoryTreesOf), which never changes once a commit or tree names it: the
182
+ // tree at a directory of a commit, by the repository's id, the commit and the
183
+ // directory (`trees`), and how many files a tree holds and their bytes, by
184
+ // the repository's id and the tree's (`tree-files`): kept as its history's
185
+ // pages are.
186
+ export const codeTreeStore = <T>(db: UpstreamCacheStore, type: 'trees' | 'tree-files', debug = false): KeptStore<T> =>
187
+ keptStore<T>(db, `repository/code-${type}`, HISTORY_PAGE_KEPT_MS, debug)
188
+ // The commit the code viewer keeps a repository's directories at in byte
189
+ // storage, its default branch's head as last read, and the repository's
190
+ // name they are kept under (repository-code.ts repositoryCodeKept), by the
191
+ // repository's id: kept until the head moves, the repository is renamed or
192
+ // it is removed, one for each repository.
193
+ export const codeHeadStore = (db: UpstreamCacheStore, debug = false): KeptStore<{ commit: string; repo: string }> =>
194
+ keptStore<{ commit: string; repo: string }>(db, 'repository/code-heads', Infinity, debug)
195
+ // The packages a repository's pull request's lockfiles add
196
+ // (pull-dependencies.ts), by the commits of its base and head, which never
197
+ // change: kept as its history's pages are.
198
+ export const pullDependencyStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'repository/pull-dependencies', HISTORY_PAGE_KEPT_MS, debug)
199
+ // The versions a repository's lockfiles locked at the commit its dependencies
200
+ // are reviewed since (pull-dependency-reviews.ts reviewBaseline), by the
201
+ // commit, which never change: kept as its history's pages are, never stored
202
+ // as reviews.
203
+ // The lines a version changes against another, or none (npm-delta.ts), by
204
+ // both sides' integrities, each kept HISTORY_PAGE_KEPT_MS.
205
+ export const npmDeltaStore = <T>(db: UpstreamCacheStore, debug = false): KeptStore<T> => keptStore<T>(db, 'npm/deltas', HISTORY_PAGE_KEPT_MS, debug)
206
+ export const reviewBaselineStore = (db: UpstreamCacheStore, debug = false): KeptStore<{ versions: string[]; unread: string[] }> =>
207
+ keptStore<{ versions: string[]; unread: string[] }>(db, 'repository/review-baselines', HISTORY_PAGE_KEPT_MS, debug)
208
+ // What a repository's dependency review read of GitHub (pull-dependencies.ts
209
+ // LockfileStore): the lockfiles a commit lists (`listings`), by the commit,
210
+ // and a lockfile's packages with what their reviews are held to
211
+ // (`lockfiles`), by its blob, which never change: kept as its history's
212
+ // pages are, apart from a scan's (scanReadStore), which reads them without
213
+ // what their reviews are held to.
214
+ export const lockfileReadStore = <T>(db: UpstreamCacheStore, type: 'listings' | 'lockfiles', debug = false): KeptStore<T> =>
215
+ keptStore<T>(db, `repository/review-${type}`, HISTORY_PAGE_KEPT_MS, debug)
@@ -0,0 +1,264 @@
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, 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.
6
+ import { Buffer } from 'node:buffer'
7
+ import { homedir } from 'node:os'
8
+ import { join, resolve } from 'node:path'
9
+ import { CompressionError, decompress } from '@preventive/archive/compression.js'
10
+ import { encodeBrotli } from './brotli.ts'
11
+ import { keptBriefly } from './kept-briefly.ts'
12
+ import { MAX_NPM_PACKAGE_BYTES, MAX_NPM_PACKAGE_FILES, NPM_REGISTRY, NpmPackageError, type NpmPackageFile, type NpmVersionDocument, type TarReading,
13
+ askUpstream, npmFileRows, npmPublishTime, npmToken, readNpmTar } from './npm-packages.ts'
14
+ import { keepFile, readKeptFile, upstreamCachePath } from './upstream-cache.ts'
15
+
16
+ // The tar stream a package's files may come in, headers and padding included.
17
+ // A tarball larger than that cannot unpack within it, so it is refused as it
18
+ // arrives.
19
+ export const MAX_TAR_BYTES = 96 * 1024 * 1024
20
+ const TARBALL_TIMEOUT_MS = 120_000
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.
24
+ const MAX_ACTIVE_LOADS = 4
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
32
+
33
+ // A load's result, shared by its readers, and a reader's release of it.
34
+ export interface NpmLoad<T> { result: Promise<T>; release: () => void }
35
+
36
+ interface Load { job: Promise<unknown>; readers: number; settled: boolean; slot: string }
37
+
38
+ // Loads by what they read; readers asking for the same share one. A load
39
+ // keeps its place among the active ones until its work is done and each
40
+ // reader has released it, as a reader does once its response is written or
41
+ // abandoned, so what responses still hold counts too. Places by the slot
42
+ // their loads share, with how many hold each and how many places it takes.
43
+ const loads = new Map<string, Load>()
44
+ const slots = new Map<string, { holding: number; places: number }>()
45
+ const placesTaken = () => [...slots.values()].reduce((sum, slot) => sum + slot.places, 0)
46
+
47
+ const refused = <T>(status: number, code: string): NpmLoad<T> => {
48
+ const result = Promise.reject(new NpmPackageError(status, code))
49
+ result.catch(() => {})
50
+ return { result, release() {} }
51
+ }
52
+
53
+ // What names a version's tarball, and so its files: its integrity, its
54
+ // shasum and URL, and whether it is read with the token.
55
+ const tarballKey = (doc: NpmVersionDocument) => `${doc.dist.integrity} ${doc.dist.shasum ?? ''} ${doc.dist.tarball} ${doc.private}`
56
+
57
+ function shared<T>(what: string, doc: NpmVersionDocument, work: () => Promise<T>): NpmLoad<T> {
58
+ const { unpackedSize, fileCount } = doc.dist
59
+ if ((unpackedSize ?? 0) > MAX_NPM_PACKAGE_BYTES || (fileCount ?? 0) > MAX_NPM_PACKAGE_FILES) return refused(413, 'package-too-large')
60
+ const slot = tarballKey(doc)
61
+ return sharedLoad(`${what} ${slot}`, work, 'npm-busy', { slot })
62
+ }
63
+
64
+ // The places among the active ones a load takes whose files hold `files` and
65
+ // `bytes`: as many as packages they would fill, up to every one, each place
66
+ // holding what a package may.
67
+ export function loadPlaces({ files, bytes }: { files: number; bytes: number }): number {
68
+ return Math.min(MAX_ACTIVE_LOADS, Math.max(1, Math.ceil(files / MAX_NPM_PACKAGE_FILES), Math.ceil(bytes / MAX_NPM_PACKAGE_BYTES)))
69
+ }
70
+
71
+ // A load among the active ones, by `key`, refused as `busy` past them: a
72
+ // repository directory's (repository-code.ts) as a package's, each holding
73
+ // alike a tarball, its tar, its files and their encoded JSON. Loads of one
74
+ // `slot` take one place between them, or `places` (loadPlaces) where they
75
+ // hold more.
76
+ export function sharedLoad<T>(key: string, work: () => Promise<T>, busy: string,
77
+ { slot = key, places = 1 }: { slot?: string; places?: number } = {}): NpmLoad<T> {
78
+ let load = loads.get(key)
79
+ if (!load) {
80
+ if (!slots.has(slot) && placesTaken() + places > MAX_ACTIVE_LOADS) return refused(429, busy)
81
+ const created: Load = { job: work(), readers: 0, settled: false, slot }
82
+ const finish = () => { settle(key, created) }
83
+ created.job.then(finish, finish)
84
+ loads.set(key, created)
85
+ const taken = slots.get(slot)
86
+ slots.set(slot, { holding: (taken?.holding ?? 0) + 1, places: taken?.places ?? places })
87
+ load = created
88
+ }
89
+ const held = load
90
+ held.readers++
91
+ let released = false
92
+ return {
93
+ result: held.job as Promise<T>,
94
+ release() {
95
+ if (released) return
96
+ released = true
97
+ held.readers--
98
+ done(key, held)
99
+ },
100
+ }
101
+ }
102
+
103
+ function settle(key: string, load: Load) {
104
+ load.settled = true
105
+ done(key, load)
106
+ }
107
+
108
+ // A load gone from the active ones, and its place with it, once its work is
109
+ // done and its readers have released it.
110
+ function done(key: string, load: Load) {
111
+ if (load.readers !== 0 || !load.settled || loads.get(key) !== load) return
112
+ loads.delete(key)
113
+ const taken = slots.get(load.slot)!
114
+ if (taken.holding === 1) slots.delete(load.slot)
115
+ else taken.holding--
116
+ }
117
+
118
+ // The sha512 an integrity names, in base64, or null where it names none.
119
+ const sha512Of = (integrity: string) => /(?:^|\s)sha512-([\d+/A-Za-z]{86}==)(?=\s|$)/u.exec(integrity)?.[1] ?? null
120
+ // Hashed off the event loop: a tarball runs to tens of MiB.
121
+ const digest = async (algorithm: string, bytes: Uint8Array) => Buffer.from(await crypto.subtle.digest(algorithm, bytes as Uint8Array<ArrayBuffer>))
122
+ // Whether `bytes` are those of the sha512 an integrity names and, where there
123
+ // is one, of the shasum (a sha1, in hex).
124
+ async function hashedAs(bytes: Uint8Array, sha512: string, shasum: string | null): Promise<boolean> {
125
+ const [own512, own1] = await Promise.all([digest('SHA-512', bytes), shasum === null ? null : digest('SHA-1', bytes)])
126
+ return own512.toString('base64') === sha512 && (own1 === null || own1.toString('hex') === shasum)
127
+ }
128
+
129
+ // Upstream's disk cache (setCacheDir), where tarballs are kept between
130
+ // loads, or null to keep none and read none.
131
+ let tarballCache: string | null = null
132
+
133
+ export function setNpmTarballCache(dir: string | null) {
134
+ tarballCache = dir === null ? null : resolve(dir)
135
+ }
136
+
137
+ // Where npm's own cache files content by its sha512, short of an .npmrc
138
+ // moving the cache.
139
+ function npmCachePath(hash: string): string | null {
140
+ const configured = process.env['npm_config_cache'] || process.env['NPM_CONFIG_CACHE']
141
+ let root
142
+ try {
143
+ root = configured ? resolve(configured.replace(/^~(?=[/\\])/u, homedir()))
144
+ : process.platform === 'win32' ? join(process.env['LOCALAPPDATA'] || homedir(), 'npm-cache') : join(homedir(), '.npm')
145
+ } catch { return null }
146
+ const hex = Buffer.from(hash, 'base64').toString('hex')
147
+ return join(root, '_cacache', 'content-v2', 'sha512', hex.slice(0, 2), hex.slice(2, 4), hex.slice(4))
148
+ }
149
+
150
+ // The version's tarball from the registry, at the package's own path: with
151
+ // the token only for a private version, and its size bounded as it arrives.
152
+ async function downloadTarball(url: string, isPrivate: boolean): Promise<Buffer> {
153
+ const token = isPrivate ? npmToken() : null
154
+ const bytes = await askUpstream(url, { headers: token ? { authorization: `Bearer ${token}` } : {}, limit: MAX_TAR_BYTES, ms: TARBALL_TIMEOUT_MS, absent: [401, 403, 404] })
155
+ .catch((err: unknown) => { throw err instanceof NpmPackageError && err.message === 'upstream-too-large' ? new NpmPackageError(413, 'package-too-large') : err })
156
+ if (bytes === null) throw new NpmPackageError(404, 'package-not-found')
157
+ return bytes
158
+ }
159
+
160
+ // The version's tarball, its bytes held to the document's sha512, and to its
161
+ // shasum where it tells one, wherever they come from: kept in the cache,
162
+ // where bundle builds keep what they fetch with the token, or in npm's; else
163
+ // downloaded, and then kept. Bytes matching the hashes the reader's own
164
+ // document names are the tarball that reader may download, wherever they
165
+ // were kept and whoever fetched them.
166
+ async function readTarball(doc: NpmVersionDocument): Promise<Uint8Array> {
167
+ const expected = sha512Of(doc.dist.integrity), shasum = doc.dist.shasum ?? null, url = doc.dist.tarball
168
+ if (!url.startsWith(`${NPM_REGISTRY}/${doc.name}/-/`) || URL.parse(url)?.href !== url || expected === null
169
+ || (shasum !== null && !/^[\da-f]{40}$/u.test(shasum))) throw new NpmPackageError(502, 'upstream-invalid')
170
+ const kept = tarballCache === null ? null : upstreamCachePath(tarballCache, 'npm/tarballs', `${doc.name}@${doc.version}.tgz`)
171
+ if (kept !== null) {
172
+ for (const path of [kept, npmCachePath(expected)]) {
173
+ const bytes = path === null ? null : await readKeptFile(path, MAX_TAR_BYTES)
174
+ if (bytes && await hashedAs(bytes, expected, shasum)) return bytes
175
+ }
176
+ }
177
+ const bytes = await downloadTarball(url, doc.private)
178
+ if (!await hashedAs(bytes, expected, shasum)) throw new NpmPackageError(502, 'upstream-invalid')
179
+ if (kept !== null) await keepFile(kept, bytes)
180
+ return bytes
181
+ }
182
+
183
+ // Tarballs being read, and versions' files being unpacked, by what names
184
+ // them (tarballKey): readers asking for one meanwhile share it, its bytes
185
+ // those each one's own document names.
186
+ const reading = new Map<string, Promise<Uint8Array>>()
187
+ const unpacking = new Map<string, Promise<Unpacked>>()
188
+
189
+ function sharedRead<T>(pending: Map<string, Promise<T>>, key: string, read: () => Promise<T>): Promise<T> {
190
+ let job = pending.get(key)
191
+ if (!job) {
192
+ const started = read().finally(() => { if (pending.get(key) === started) pending.delete(key) })
193
+ pending.set(key, job = started)
194
+ }
195
+ return job
196
+ }
197
+
198
+ const sharedTarball = (doc: NpmVersionDocument) => sharedRead(reading, tarballKey(doc), () => readTarball(doc))
199
+
200
+ // A version's files, and the size of the tarball they came in.
201
+ interface Unpacked { files: NpmPackageFile[]; tarballSize: number }
202
+
203
+ // Public versions' files lately unpacked, by tarballKey.
204
+ const unpacked = keptBriefly<Unpacked>({ keptMs: UNPACKED_KEPT_MS, maxBytes: MAX_UNPACKED_BYTES })
205
+
206
+ // The version's files, kept from a load of them a little while since, else
207
+ // unpacked from its tarball (and kept, a public version's): bytes its
208
+ // reader's own document names, as its tarball's are.
209
+ function unpackedOf(doc: NpmVersionDocument): Promise<Unpacked> {
210
+ const key = tarballKey(doc)
211
+ const kept = unpacked.get(key)
212
+ if (kept) return Promise.resolve(kept)
213
+ return sharedRead(unpacking, key, async () => {
214
+ const tarball = await sharedTarball(doc)
215
+ const value = { files: await readTarballFiles(tarball), tarballSize: tarball.byteLength }
216
+ if (!doc.private) unpacked.keep(key, value, value.files.reduce((sum, file) => sum + file.path.length + file.bytes.byteLength, 0))
217
+ return value
218
+ })
219
+ }
220
+
221
+ // A gzipped tarball's files, its top directory left out, as npm extracts
222
+ // a package's: a repository tree's as GitHub archives it, too, its symlinks
223
+ // let climb as readNpmTar's `symlinksAbove` says, and its tar stream and
224
+ // files bounded as `options` give, a package's bounds unless given.
225
+ export async function readTarballFiles(tarball: Uint8Array, { maxTarBytes = MAX_TAR_BYTES, ...options }: TarReading & { maxTarBytes?: number } = {}): Promise<NpmPackageFile[]> {
226
+ let tar: Uint8Array
227
+ try { tar = await decompress(tarball, 'gzip', { limit: maxTarBytes }) }
228
+ catch (err) {
229
+ const limited = err instanceof CompressionError && err.limited
230
+ throw new NpmPackageError(limited ? 413 : 422, limited ? 'package-too-large' : 'bad-tarball')
231
+ }
232
+ return readNpmTar(tar, options)
233
+ }
234
+
235
+ // The version's tarball, for download.
236
+ export function loadNpmTarball(doc: NpmVersionDocument): NpmLoad<Uint8Array> {
237
+ return shared('tarball', doc, () => sharedTarball(doc))
238
+ }
239
+
240
+ // The version's files, for one of them to be pretty-printed (pretty-print.ts).
241
+ export function loadNpmFiles(doc: NpmVersionDocument): NpmLoad<NpmPackageFile[]> {
242
+ return shared('files', doc, async () => (await unpackedOf(doc)).files)
243
+ }
244
+
245
+ // The version as the viewer reads it, as brotli-encoded JSON: `{ name,
246
+ // version, private, integrity, tarballSize, published, manifest, files,
247
+ // known }`, its files as npmFileRows gives them, and with them, as its files
248
+ // are encoded, when it was published where that is kept (`published`, asked
249
+ // then, else null) and what `known` gives: what is kept about its package
250
+ // (npm-insights.ts npmKnown), which the viewer need not then ask for. Encoding
251
+ // is where a load holds the most, so it takes its place among the active
252
+ // loads until its body is done, and readers asking meanwhile share that body,
253
+ // the first one's manifest, `published` and `known` in it; the body is not
254
+ // kept once its readers are done, its files a little while (unpackedOf).
255
+ export function loadNpmPackageBody(doc: NpmVersionDocument, known: () => object = () => ({}),
256
+ published: () => Promise<string | null> = npmPublishTime(doc).read): NpmLoad<Buffer> {
257
+ return shared('package', doc, async () => {
258
+ const { files, tarballSize } = await unpackedOf(doc)
259
+ return encodeBrotli(Buffer.from(JSON.stringify({
260
+ name: doc.name, version: doc.version, private: doc.private, integrity: doc.dist.integrity, tarballSize,
261
+ published: await published(), manifest: doc.manifest, files: npmFileRows(files), known: known(),
262
+ })))
263
+ })
264
+ }
@@ -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', 'shasum', '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
+ }