@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,434 @@
1
+ // Minified files pretty-printed for the Code tab (GET /api/bundles/:id/pretty
2
+ // one file at a time, GET /api/npm/pretty all a version's at once), and the
3
+ // files that changed between two bundles or versions for Compare (the same
4
+ // with `other`, or `otherVersion`), named by path and content hash, formatted
5
+ // with oxfmt once and kept Brotli-encoded by that hash: a bundle's in its
6
+ // cache directory (so only made from, and read by readers of, that bundle), a
7
+ // public npm version's under `cache/npm/`; a private version's are never
8
+ // kept. Access is the caller's to check.
9
+ import { Buffer } from 'node:buffer'
10
+ import { Readable } from 'node:stream'
11
+ import { promisify } from 'node:util'
12
+ import { brotliDecompress } from 'node:zlib'
13
+ import { format } from '@preventive/compare/pretty.js'
14
+ import { computeFileHash } from '@preventive/report'
15
+ import { FILE_HASH, computeBundleFileHashes } from '../common/bundle-metadata.js'
16
+ import { bundleCompareFiles } from '../common/bundle-compare-inputs.js'
17
+ import { bundlePackageDirs, bundleSourcesAsMap } from '../common/bundle-sources.js'
18
+ import { MAX_PRETTY_BYTES, needsPrettyPrint, prettyExtension } from '../common/pretty-print.js'
19
+ import type { OpenedBlob } from './blob-store.ts'
20
+ import { type BundleCacheStorage, readSizedBundleDetails } from './bundle-cache.ts'
21
+ import type { BundleStore } from './bundle-store.ts'
22
+ import { encodeBrotli } from './brotli.ts'
23
+ import { CacheMissError, type CacheStorage } from './cache-storage.ts'
24
+ import type { ManagedBundle, ManagedDb } from './db.ts'
25
+ import { keptBriefly } from './kept-briefly.ts'
26
+ import { loadNpmFiles } from './npm-loads.ts'
27
+ import { type NpmVersionDocument, npmFileText } from './npm-packages.ts'
28
+
29
+ // Part of every kept copy's name; bumped when what the formatter writes for
30
+ // the same file changes, or what its check lets through, so copies written
31
+ // before are no longer read.
32
+ const PRETTY_VERSION = 4
33
+ // Formats running at once per process, each holding its file's syntax tree,
34
+ // and those waiting their turn past them (a version's files are formatted one
35
+ // after another, beside other readers'), any more refused.
36
+ const MAX_ACTIVE_FORMATS = 2
37
+ const MAX_WAITING_FORMATS = 16
38
+ // How long a bundle's details are kept once read, and what the two last read
39
+ // may have decoded from between them, the least lately read going first past
40
+ // it: Compare's requests for a pair of bundles come one after another, each
41
+ // reading both, and the Code tab's for one, each reading it.
42
+ const DETAILS_KEPT_MS = 60_000
43
+ const MAX_KEPT_DETAILS = 2
44
+ const MAX_KEPT_DETAILS_BYTES = 256 * 1024 * 1024
45
+ // Files hashed at once: one at a time, each waits its own round trip to the
46
+ // thread pool; all at once, every one's job is held together.
47
+ const HASHES_AT_ONCE = 32
48
+ // Kept copies a stream reads ahead of the line it is sending (readAhead):
49
+ // one at a time, each waits its own storage round trip; each read whole, as
50
+ // its line will send it.
51
+ const KEPT_AHEAD = 8
52
+
53
+ export class PrettyError extends Error {
54
+ status: number
55
+ constructor(status: number, code: string) { super(code); this.status = status }
56
+ }
57
+
58
+ export interface PrettyRequest { path: string; hash: string; extension: string }
59
+
60
+ // One of a version's files pretty-printed (npmAll), named by `source`, the
61
+ // hash of the file it was formatted from: its copy's text, or why it has
62
+ // none, as a PrettyError tells it.
63
+ export type PrettyLine = { path: string; source: string; text: string } | { path: string; source: string; error: string; status: number }
64
+
65
+ // Which of a comparison's two its files are of: the bundle or version asked
66
+ // about, or the `other`.
67
+ type Side = 'base' | 'other'
68
+
69
+ // One of a comparison's changed files pretty-printed (compared), as a
70
+ // version's is, with the side it is of.
71
+ export type ComparedLine = PrettyLine & { side: Side }
72
+
73
+ // A side of a comparison: its files that can be formatted, with their texts,
74
+ // and every one of its files' hashes, by path; its packages' directories by
75
+ // path, as a bundle records them (bundlePackageDirs), or null where it
76
+ // records none; the copy kept for one, or null; and one made, kept where the
77
+ // side keeps copies.
78
+ export interface ComparedSide {
79
+ side: Side
80
+ files: Array<PrettyRequest & { text: string }>
81
+ hashes: Map<string, string>
82
+ packageDirs: Map<string, string> | null
83
+ kept(request: PrettyRequest): Promise<OpenedBlob | null>
84
+ made(request: PrettyRequest, text: string): Promise<OpenedBlob>
85
+ }
86
+
87
+ // The file a request's `path` and `hash` name, or null for one that can't be
88
+ // formatted.
89
+ export function prettyRequest(params: URLSearchParams): PrettyRequest | null {
90
+ const path = params.get('path') ?? ''
91
+ const extension = prettyExtension(path), hash = params.get('hash') ?? ''
92
+ return extension !== null && FILE_HASH.test(hash) ? { path, hash, extension } : null
93
+ }
94
+
95
+ // A copy's name: its file's hash in hex, with the extension it was parsed by.
96
+ const prettyFile = ({ hash, extension }: PrettyRequest) =>
97
+ `pretty-v${PRETTY_VERSION}/${Buffer.from(hash.slice('sha512-'.length), 'base64').toString('hex')}.${extension}.br`
98
+
99
+ let activeFormats = 0
100
+ const waitingFormats: Array<() => void> = []
101
+
102
+ // The text formatted and Brotli-encoded, where @preventive/compare's checks
103
+ // find formatting changed only its layout: refused as `unsupported` where it
104
+ // has no check for the kind of file, `unformattable` or `pretty-mismatch`.
105
+ export async function prettyBody(text: string, extension: string): Promise<Buffer> {
106
+ if (Buffer.byteLength(text) > MAX_PRETTY_BYTES) throw new PrettyError(413, 'file-too-large')
107
+ if (activeFormats < MAX_ACTIVE_FORMATS) activeFormats++
108
+ else if (waitingFormats.length >= MAX_WAITING_FORMATS) throw new PrettyError(429, 'pretty-busy')
109
+ else await new Promise<void>(resolve => { waitingFormats.push(resolve) })
110
+ let result
111
+ try { result = await format(text, extension) }
112
+ finally {
113
+ const next = waitingFormats.shift()
114
+ if (next) next()
115
+ else activeFormats--
116
+ }
117
+ if ('error' in result) {
118
+ if (result.error === 'pretty-mismatch') console.warn(`managed: pretty-printing a .${extension} file changed more than its layout; not served`)
119
+ throw new PrettyError(422, result.error)
120
+ }
121
+ return encodeBrotli(Buffer.from(result.code))
122
+ }
123
+
124
+ const opened = (body: Buffer): OpenedBlob => ({ size: body.byteLength, stream: Readable.from([body]) })
125
+
126
+ // A version's file's hash, by its bytes: those of a version's files loaded
127
+ // a little while since are the same bytes (npm-loads.ts), hashed once.
128
+ const fileHashes = new WeakMap<Uint8Array, Promise<string>>()
129
+ function npmFileHash(bytes: Uint8Array): Promise<string> {
130
+ let hash = fileHashes.get(bytes)
131
+ if (!hash) {
132
+ fileHashes.set(bytes, hash = computeFileHash(bytes))
133
+ hash.catch(() => { fileHashes.delete(bytes) })
134
+ }
135
+ return hash
136
+ }
137
+
138
+ const decompress = promisify(brotliDecompress)
139
+
140
+ // A pretty-printed copy's text, from its Brotli-encoded bytes.
141
+ export async function prettyText(copy: OpenedBlob): Promise<string> {
142
+ const chunks: Buffer[] = []
143
+ for await (const chunk of copy.stream) chunks.push(chunk as Buffer)
144
+ return (await decompress(Buffer.concat(chunks))).toString()
145
+ }
146
+
147
+ // Every file of a version that needs pretty-printing (needsPrettyPrint), read
148
+ // in one load of its files, in path order.
149
+ async function npmPrintable(doc: NpmVersionDocument): Promise<Array<PrettyRequest & { text: string }>> {
150
+ const load = loadNpmFiles(doc)
151
+ const files: Array<PrettyRequest & { text: string }> = []
152
+ try {
153
+ for (const { path, bytes } of await load.result) {
154
+ const extension = prettyExtension(path)
155
+ if (extension === null || bytes.byteLength > MAX_PRETTY_BYTES) continue
156
+ const text = npmFileText(bytes)
157
+ if (text !== null && needsPrettyPrint(path, text)) files.push({ path, hash: await npmFileHash(bytes), extension, text })
158
+ }
159
+ } finally { load.release() }
160
+ return files.toSorted((a, b) => (a.path < b.path ? -1 : 1))
161
+ }
162
+
163
+ // A file's line (PrettyLine): the text of its copy, as `copy` has it, or why
164
+ // it has none.
165
+ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<OpenedBlob>): Promise<PrettyLine> {
166
+ try { return { path, source: hash, text: await prettyText(await copy()) } }
167
+ catch (err) {
168
+ if (!(err instanceof PrettyError)) throw err
169
+ return { path, source: hash, error: err.message, status: err.status }
170
+ }
171
+ }
172
+
173
+ // A file's line from its kept copy, `open` opening it, or null where none is
174
+ // kept.
175
+ async function keptLine(request: PrettyRequest, open: () => Promise<OpenedBlob | null>): Promise<PrettyLine | null> {
176
+ const copy = await open()
177
+ return copy && prettyLine(request, () => Promise.resolve(copy))
178
+ }
179
+
180
+ // Each of `items` with what `read` reads of it, in their order, up to
181
+ // KEPT_AHEAD of them read at once ahead of the one iterated, each answered
182
+ // (or failing) in its turn. One iterated no further starts no more; those
183
+ // under way settle unread.
184
+ async function* readAhead<T, R>(items: readonly T[], read: (item: T) => Promise<R>): AsyncGenerator<[T, R]> {
185
+ const reads: Promise<R>[] = []
186
+ for (const [i, item] of items.entries()) {
187
+ while (reads.length < Math.min(items.length, i + KEPT_AHEAD)) {
188
+ const reading = read(items[reads.length]!)
189
+ reading.catch(() => {})
190
+ reads.push(reading)
191
+ }
192
+ yield [item, await reads[i]!]
193
+ }
194
+ }
195
+
196
+ // The files that changed between two sides, pretty-printed: each side's
197
+ // formattable files the other has another text at the path of, or, where it
198
+ // has none there, none of their text at a path of its own (a file renamed, or
199
+ // moved with its tree, unchanged, has it there; a copy at a path both have is
200
+ // not one), by the paths each side has them at. Those are both of the pairs
201
+ // the viewer diffs but where it pairs files otherwise (among copies of one
202
+ // text, across packages), whose copies it asks for alone. As they are
203
+ // iterated, in path order, the base's first where both have one: those kept,
204
+ // at once, a few read ahead (readAhead); then the rest, formatted one after
205
+ // another, the minified before the others (needsPrettyPrint), as the viewer
206
+ // prints those unasked.
207
+ export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGenerator<ComparedLine> {
208
+ // Each side's texts at the paths the other hasn't: where a file moved has its own.
209
+ const moved = sides.map((own, i) => new Set([...own.hashes].filter(([path]) => !sides[1 - i]!.hashes.has(path)).map(([, hash]) => hash)))
210
+ const changed = sides.flatMap((own, i) => {
211
+ const other = sides[1 - i]!.hashes
212
+ return own.files.filter(({ path, hash }) => (other.has(path) ? other.get(path) !== hash : !moved[1 - i]!.has(hash))).map(file => ({ own, file }))
213
+ }).toSorted((a, b) => (a.file.path < b.file.path ? -1 : a.file.path > b.file.path ? 1 : 0))
214
+ return (async function* () {
215
+ const unkept: typeof changed = []
216
+ for await (const [{ own, file }, line] of readAhead(changed, item => keptLine(item.file, () => item.own.kept(item.file)))) {
217
+ if (line) yield { side: own.side, ...line }
218
+ else unkept.push({ own, file })
219
+ }
220
+ const minified = new Map(unkept.map(({ file }) => [file, needsPrettyPrint(file.path, file.text)]))
221
+ for (const { own, file } of unkept.toSorted((a, b) => Number(minified.get(b.file)) - Number(minified.get(a.file)))) {
222
+ yield { side: own.side, ...await prettyLine(file, () => own.made(file, file.text)) }
223
+ }
224
+ })()
225
+ }
226
+
227
+ // Jobs by key, readers asking for one while it runs sharing it.
228
+ function sharing<T>(): (key: string, make: () => Promise<T>) => Promise<T> {
229
+ const pending = new Map<string, Promise<T>>()
230
+ return (key, make) => {
231
+ let job = pending.get(key)
232
+ if (!job) pending.set(key, job = make().finally(() => pending.delete(key)))
233
+ return job
234
+ }
235
+ }
236
+
237
+ // Work done one job at a time, in the order asked.
238
+ function serial(): <T>(work: () => Promise<T>) => Promise<T> {
239
+ let queue = Promise.resolve()
240
+ return work => {
241
+ const job = queue.then(work)
242
+ queue = job.then(() => undefined, () => undefined)
243
+ return job
244
+ }
245
+ }
246
+
247
+ async function keep(put: () => Promise<void>) {
248
+ try { await put() } catch (err) { console.warn('managed: pretty-print cache write failed:', err) }
249
+ }
250
+
251
+ // A copy kept, or null where none is.
252
+ async function kept(open: () => Promise<OpenedBlob>): Promise<OpenedBlob | null> {
253
+ try { return await open() }
254
+ catch (err) { if (err instanceof CacheMissError) return null; throw err }
255
+ }
256
+
257
+ // A side's files that can be formatted, with their texts, and every one of
258
+ // its files' hashes by path, from their paths and contents: texts, or bytes,
259
+ // read as text where they are; hashed by `hashOf`, a few at once.
260
+ async function sideFiles<T extends string | Uint8Array>(entries: Array<[string, T]>, hashOf: (file: string, content: T) => Promise<string>): Promise<Pick<ComparedSide, 'files' | 'hashes'>> {
261
+ const files: ComparedSide['files'] = [], hashes = new Map<string, string>()
262
+ for (let i = 0; i < entries.length; i += HASHES_AT_ONCE) {
263
+ const chunk = entries.slice(i, i + HASHES_AT_ONCE)
264
+ const hashed = await Promise.all(chunk.map(async ([path, content]) => [path, await hashOf(path, content)] as const))
265
+ for (const [path, hash] of hashed) hashes.set(path, hash)
266
+ }
267
+ for (const [path, content] of entries) {
268
+ const extension = prettyExtension(path)
269
+ if (extension === null || (typeof content === 'string' ? Buffer.byteLength(content) : content.byteLength) > MAX_PRETTY_BYTES) continue
270
+ const text = typeof content === 'string' ? content : npmFileText(content)
271
+ if (text !== null) files.push({ path, hash: hashes.get(path)!, extension, text })
272
+ }
273
+ return { files, hashes }
274
+ }
275
+
276
+ // A bundle's side, from its details: its files, as sideFiles reads them,
277
+ // within a reason `scope` as Compare reads them there (bundleCompareFiles),
278
+ // and its packages' directories; hashed once for its details, as long as
279
+ // they are kept (computeBundleFileHashes).
280
+ async function bundleSide(details: Parameters<typeof bundlePackageDirs>[0], scope: string): Promise<Pick<ComparedSide, 'files' | 'hashes' | 'packageDirs'>> {
281
+ const scoped = scope ? new Set(bundleCompareFiles(details, scope).keys()) : null, sources = bundleSourcesAsMap(details)
282
+ const hashed: Map<string, string> = await computeBundleFileHashes(details)
283
+ return { ...await sideFiles(scoped ? [...sources].filter(([path]) => scoped.has(path)) : [...sources], (path, content) => {
284
+ const hash = hashed.get(path)
285
+ return hash === undefined ? computeFileHash(content) : Promise.resolve(hash)
286
+ }),
287
+ packageDirs: bundlePackageDirs(details) }
288
+ }
289
+
290
+ // A version's files by path, from one load of them.
291
+ async function npmFiles(doc: NpmVersionDocument): Promise<Array<[string, Uint8Array]>> {
292
+ const load = loadNpmFiles(doc)
293
+ try { return (await load.result).map(({ path, bytes }) => [path, bytes]) }
294
+ finally { load.release() }
295
+ }
296
+
297
+ // Removes the public npm copies of every version before PRETTY_VERSION, which
298
+ // nothing reads any more, for maintenance: once a process, again where
299
+ // removing them failed. A bundle's earlier copies go with the bundle.
300
+ export function stalePrettyRemover(npm: CacheStorage): (signal?: AbortSignal) => Promise<void> {
301
+ let removed = false
302
+ return async signal => {
303
+ if (removed) return
304
+ for (let version = 1; version < PRETTY_VERSION; version++) {
305
+ if (signal?.aborted) return
306
+ await npm.delete(`pretty-v${version}`)
307
+ }
308
+ removed = true
309
+ }
310
+ }
311
+
312
+ // A bundle's details, from its bytes in `store`: bundles are read one at a
313
+ // time, as reading one decodes all of it, readers asking for one waiting its
314
+ // turn or being read sharing it; and kept a little while once read
315
+ // (DETAILS_KEPT_MS), each reader's access to the bundle the caller's to check
316
+ // as ever.
317
+ function detailsReader(store: BundleStore) {
318
+ type Details = NonNullable<Awaited<ReturnType<typeof readSizedBundleDetails>>>['details']
319
+ const enqueue = serial(), share = sharing<Details>()
320
+ const held = keptBriefly<Details>({ keptMs: DETAILS_KEPT_MS, maxBytes: MAX_KEPT_DETAILS_BYTES, maxEntries: MAX_KEPT_DETAILS })
321
+ return (record: ManagedBundle): Promise<Details> => {
322
+ const key = `${record.id} ${record.integrity}`
323
+ const hit = held.get(key)
324
+ if (hit) return Promise.resolve(hit)
325
+ return share(key, () => enqueue(async () => {
326
+ const read = await readSizedBundleDetails(record, store)
327
+ if (!read) throw new PrettyError(422, 'bundle-unavailable')
328
+ held.keep(key, read.details, read.decodedBytes)
329
+ return read.details
330
+ }))
331
+ }
332
+ }
333
+
334
+ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage, db: Pick<ManagedDb, 'getBundle'>, store: BundleStore) {
335
+ // Copies being made; readers asking for one meanwhile share it.
336
+ const shared = sharing<Buffer>()
337
+ const bundleDetails = detailsReader(store)
338
+ // A bundle's file pretty-printed from the text `read` reads, and kept in
339
+ // its cache.
340
+ function bundleCopy(record: ManagedBundle, request: PrettyRequest, read: () => Promise<string>): Promise<Buffer> {
341
+ const file = prettyFile(request)
342
+ return shared(`bundle ${record.id} ${file}`, async () => {
343
+ const body = await prettyBody(await read(), request.extension)
344
+ // As for its metadata (bundle-cache.ts): a bundle deleted meanwhile
345
+ // keeps no copy, nor does one deleted while it was written.
346
+ if (!await db.getBundle(record.id)) throw new PrettyError(404, 'no-bundle')
347
+ await keep(() => bundles.put(record.id, file, body))
348
+ if (!await db.getBundle(record.id)) {
349
+ await bundles.delete(record.id)
350
+ throw new PrettyError(404, 'no-bundle')
351
+ }
352
+ return body
353
+ })
354
+ }
355
+ // The copy kept for a public version's file, or null where none is (and
356
+ // for a private version's, which none is kept for).
357
+ const npmKept = (doc: NpmVersionDocument, request: PrettyRequest) => (doc.private ? Promise.resolve(null) : kept(() => npm.open(prettyFile(request))))
358
+ // A version's file pretty-printed, `read` reading its text (and checking
359
+ // it is the file named): from the copy kept for a public version, else
360
+ // made, and kept for a public one.
361
+ async function npmCopy(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
362
+ return await npmKept(doc, request) ?? npmMade(doc, request, read)
363
+ }
364
+ // The file pretty-printed from the text `read` reads, kept for a public version.
365
+ async function npmMade(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
366
+ const file = prettyFile(request)
367
+ return opened(await shared(doc.private ? `npm ${doc.dist.integrity} ${file}` : `npm ${file}`, async () => {
368
+ const body = await prettyBody(await read(), request.extension)
369
+ if (!doc.private) await keep(() => npm.put(file, body))
370
+ return body
371
+ }))
372
+ }
373
+ return {
374
+ async bundle(record: ManagedBundle, request: PrettyRequest): Promise<OpenedBlob> {
375
+ const copy = await kept(() => bundles.open(record.id, prettyFile(request)))
376
+ if (copy) return copy
377
+ return opened(await bundleCopy(record, request, async () => {
378
+ const text = bundleSourcesAsMap(await bundleDetails(record)).get(request.path)
379
+ if (text === undefined) throw new PrettyError(404, 'no-file')
380
+ if (await computeFileHash(text) !== request.hash) throw new PrettyError(409, 'hash-mismatch')
381
+ return text
382
+ }))
383
+ },
384
+ // The version's file, from its files loaded as the viewer's are (and
385
+ // held to the same few loads at once).
386
+ npm(doc: NpmVersionDocument, request: PrettyRequest): Promise<OpenedBlob> {
387
+ return npmCopy(doc, request, async () => {
388
+ const load = loadNpmFiles(doc)
389
+ let text: string | null
390
+ try {
391
+ const bytes = (await load.result).find(entry => entry.path === request.path)?.bytes
392
+ if (!bytes) throw new PrettyError(404, 'no-file')
393
+ if (await npmFileHash(bytes) !== request.hash) throw new PrettyError(409, 'hash-mismatch')
394
+ text = npmFileText(bytes)
395
+ } finally { load.release() }
396
+ if (text === null) throw new PrettyError(422, 'unformattable')
397
+ return text
398
+ })
399
+ },
400
+ // Every file of the version that needs pretty-printing (npmPrintable),
401
+ // read before this resolves; then, as they are iterated, each
402
+ // pretty-printed as `npm` has it, one after another, those kept a few
403
+ // read ahead (readAhead).
404
+ async npmAll(doc: NpmVersionDocument): Promise<AsyncGenerator<PrettyLine>> {
405
+ const files = await npmPrintable(doc)
406
+ return (async function* () {
407
+ for await (const [file, line] of readAhead(files, request => keptLine(request, () => npmKept(doc, request)))) {
408
+ yield line ?? await prettyLine(file, () => npmMade(doc, file, () => Promise.resolve(file.text)))
409
+ }
410
+ })()
411
+ },
412
+ // Two bundles' sides, or two versions', as a comparison reads them, read
413
+ // before this resolves: for the files that changed between them
414
+ // pretty-printed (compared), and their diffs with renamed names hidden
415
+ // (rename-diff.ts renamedPairs); a bundle's within a reason `scope`
416
+ // where one is asked for, as Compare reads it then.
417
+ bundleSides(base: ManagedBundle, other: ManagedBundle, scope = ''): Promise<[ComparedSide, ComparedSide]> {
418
+ const sideOf = async (record: ManagedBundle, side: Side): Promise<ComparedSide> => ({ side, ...await bundleSide(await bundleDetails(record), scope),
419
+ kept: request => kept(() => bundles.open(record.id, prettyFile(request))),
420
+ made: async (request, text) => opened(await bundleCopy(record, request, () => Promise.resolve(text))) })
421
+ return Promise.all([sideOf(base, 'base'), sideOf(other, 'other')])
422
+ },
423
+ // A bundle's details, read in its turn, as Compare pairs them (comparison.ts).
424
+ bundleDetails,
425
+ npmSides(base: NpmVersionDocument, other: NpmVersionDocument): Promise<[ComparedSide, ComparedSide]> {
426
+ const sideOf = async (doc: NpmVersionDocument, side: Side): Promise<ComparedSide> => ({ side, ...await sideFiles(await npmFiles(doc), (_, bytes) => npmFileHash(bytes)),
427
+ packageDirs: null,
428
+ kept: request => npmKept(doc, request),
429
+ made: (request, text) => npmCopy(doc, request, () => Promise.resolve(text)) })
430
+ return Promise.all([sideOf(base, 'base'), sideOf(other, 'other')])
431
+ },
432
+ }
433
+ }
434
+ export type PrettyCache = ReturnType<typeof createPrettyCache>