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

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 (311) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +2 -0
  6. package/client/dependency-paths.js +24 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +112 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +522 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-metadata.d.ts +14 -0
  17. package/common/bundle-metadata.js +226 -0
  18. package/common/bundle-pkg-of.d.ts +1 -0
  19. package/common/bundle-pkg-of.js +90 -0
  20. package/common/bundle-reasons.d.ts +2 -0
  21. package/common/bundle-reasons.js +21 -0
  22. package/common/bundle-source-links.d.ts +6 -0
  23. package/common/bundle-source-links.js +216 -0
  24. package/common/bundle-sourcemap.d.ts +10 -0
  25. package/common/bundle-sourcemap.js +100 -0
  26. package/common/bundle-sources.d.ts +7 -0
  27. package/common/bundle-sources.js +310 -0
  28. package/common/bundle-stats.js +41 -0
  29. package/common/bundle-tabs.js +1 -0
  30. package/common/code-language.js +65 -0
  31. package/common/default-scan-models.ts +31 -0
  32. package/common/dependency-reviews.d.ts +23 -0
  33. package/common/dependency-reviews.js +81 -0
  34. package/common/finding-duplicates.js +58 -0
  35. package/common/finding-filters.js +274 -0
  36. package/common/finding-id.js +47 -0
  37. package/common/github-issue-labels.d.ts +1 -0
  38. package/common/github-issue-labels.js +11 -0
  39. package/common/github-pr.ts +56 -0
  40. package/common/gzip.js +59 -0
  41. package/common/line-diff.d.ts +8 -0
  42. package/common/line-diff.js +56 -0
  43. package/common/linked-workspace-groups.js +63 -0
  44. package/common/managed/comments.ts +34 -0
  45. package/common/managed/npm-packages.d.ts +5 -0
  46. package/common/managed/npm-packages.js +34 -0
  47. package/common/managed/permissions.ts +31 -0
  48. package/common/managed/report-content.ts +72 -0
  49. package/common/managed/report-filter.ts +108 -0
  50. package/common/managed/repository-alias.ts +65 -0
  51. package/common/managed/roles.ts +28 -0
  52. package/common/managed/routes.d.ts +2 -0
  53. package/common/managed/routes.js +254 -0
  54. package/common/managed/scan-models.ts +6 -0
  55. package/common/managed/team-app.d.ts +2 -0
  56. package/common/managed/team-app.js +31 -0
  57. package/common/managed/triage.ts +83 -0
  58. package/common/managed/upload-seal.ts +126 -0
  59. package/common/pretty-print.d.ts +5 -0
  60. package/common/pretty-print.js +24 -0
  61. package/common/revalidation-conflicts.js +48 -0
  62. package/common/revalidation-input-groups.js +56 -0
  63. package/common/save-error-reason.ts +20 -7
  64. package/common/scan-server.ts +13 -0
  65. package/common/server-info.ts +35 -0
  66. package/common/utf8.d.ts +3 -0
  67. package/common/utf8.js +45 -0
  68. package/common/workspace-app.js +23 -0
  69. package/common/workspace-groups.js +176 -0
  70. package/out/brotli-fallback.js +3 -3
  71. package/out/chunk-2KBF5TZ7.js +2 -0
  72. package/out/chunk-2XUEZHIP.js +28 -0
  73. package/out/chunk-35VE6CDH.js +2 -0
  74. package/out/chunk-3BZ7JERB.js +1 -0
  75. package/out/chunk-4F6JSF57.js +3 -0
  76. package/out/chunk-4XCDFGDP.js +10 -0
  77. package/out/chunk-AQ75QITE.js +1 -0
  78. package/out/chunk-B2RQY6OP.js +2 -0
  79. package/out/chunk-C4Z6ZBXV.js +10 -0
  80. package/out/chunk-CMQA4DM6.js +1 -0
  81. package/out/chunk-CVVJAXVY.js +2 -0
  82. package/out/chunk-DIJJNTRH.js +10 -0
  83. package/out/chunk-GJ4IUCNS.js +1 -0
  84. package/out/chunk-GNXOKIGP.js +80 -0
  85. package/out/chunk-GZ4MS3TE.js +13 -0
  86. package/out/chunk-IISEF2BZ.js +1 -0
  87. package/out/chunk-IU3CCINR.js +3 -0
  88. package/out/chunk-K3X5EJBQ.js +2 -0
  89. package/out/chunk-KEX2ATEW.js +2 -0
  90. package/out/chunk-KYCNJVB7.js +1 -0
  91. package/out/chunk-MYDDTA7W.js +225 -0
  92. package/out/chunk-NRPC74AM.js +1 -0
  93. package/out/chunk-OOJM4CTU.js +1 -0
  94. package/out/chunk-PNHBZGDD.js +1 -0
  95. package/out/chunk-R5O6FAW4.js +2 -0
  96. package/out/chunk-RFZQQXSG.js +17 -0
  97. package/out/chunk-RSHMVYNS.js +1 -0
  98. package/out/chunk-TGANXVTA.js +1 -0
  99. package/out/chunk-TUII34K2.js +1 -0
  100. package/out/chunk-TZ5KB2Y5.js +10 -0
  101. package/out/chunk-UN5ZSYLE.js +1 -0
  102. package/out/chunk-UNTRGE26.js +2 -0
  103. package/out/chunk-UXNUAIIC.js +33 -0
  104. package/out/chunk-UYAXVRVL.js +1 -0
  105. package/out/chunk-V3ME6FI5.js +2 -0
  106. package/out/chunk-V5O7M2YT.js +1 -0
  107. package/out/chunk-VIHGO77Y.js +1 -0
  108. package/out/chunk-VQJCXGIY.js +7 -0
  109. package/out/chunk-W4J3MGLB.js +1 -0
  110. package/out/chunk-WGG7KQUH.js +1 -0
  111. package/out/chunk-WZDTZE64.js +1 -0
  112. package/out/chunk-XYDGU6BM.js +51 -0
  113. package/out/chunk-XYFXXB2G.js +10 -0
  114. package/out/chunk-YVZH5SHP.js +1 -0
  115. package/out/chunk-ZBBOBQ43.js +1 -0
  116. package/out/chunk-ZMQQCGF6.js +2 -0
  117. package/out/client-managed-import.js +4 -0
  118. package/out/client-managed.js +37 -0
  119. package/out/client-sync.js +10 -13
  120. package/out/github-issue-authorized.html +3 -0
  121. package/out/github-issue-authorized.js +1 -0
  122. package/out/graph.js +17 -4
  123. package/out/index.html +90 -8
  124. package/out/prism.js +6 -2
  125. package/out/stasis.svg +20 -0
  126. package/out/terminal.js +365 -39
  127. package/out/view.css +1 -1
  128. package/out/view.js +83 -117
  129. package/package.json +297 -59
  130. package/risk/files/characters.js +196 -0
  131. package/risk/files/hosts.js +205 -0
  132. package/risk/files/readability.js +171 -0
  133. package/risk/files/runs-code.js +358 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +142 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +219 -0
  139. package/risk/npm/combined.js +65 -0
  140. package/risk/npm/compare.js +228 -0
  141. package/risk/npm/dependencies.js +153 -0
  142. package/risk/npm/diff.js +43 -0
  143. package/risk/npm/downloads.js +84 -0
  144. package/risk/npm/files.js +34 -0
  145. package/risk/npm/held.js +60 -0
  146. package/risk/npm/install-scripts.js +34 -0
  147. package/risk/npm/malware.js +36 -0
  148. package/risk/npm/manifest.js +65 -0
  149. package/risk/npm/new-dependency.js +30 -0
  150. package/risk/npm/old-version.js +108 -0
  151. package/risk/npm/registry.js +282 -0
  152. package/risk/npm/release.js +85 -0
  153. package/risk/npm/specs.js +124 -0
  154. package/risk/npm/tarball.js +113 -0
  155. package/server-common/database-config.ts +16 -0
  156. package/server-common/initialize.ts +18 -0
  157. package/server-common/npm-advisories.ts +101 -0
  158. package/{server → server-common}/origin.ts +5 -5
  159. package/server-common/reap.ts +48 -0
  160. package/server-common/scan-config.ts +19 -0
  161. package/server-common/standalone.ts +29 -0
  162. package/server-common/storage-crypto.ts +182 -0
  163. package/server-common/storage-log.ts +38 -0
  164. package/server-common/vercel-blob.ts +103 -0
  165. package/server-e2e/app.ts +486 -0
  166. package/{server → server-e2e}/auth.ts +5 -1
  167. package/{server → server-e2e}/bus-receiver.ts +2 -1
  168. package/{server → server-e2e}/cli.js +9 -4
  169. package/{server → server-e2e}/config.ts +56 -35
  170. package/{server → server-e2e}/db-neon.ts +2 -2
  171. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  172. package/{server → server-e2e}/db-stmt.ts +2 -2
  173. package/{server → server-e2e}/db.ts +7 -7
  174. package/{server → server-e2e}/http.ts +116 -18
  175. package/{server → server-e2e}/hub.ts +40 -9
  176. package/server-e2e/index.ts +17 -0
  177. package/server-e2e/lifecycle.ts +95 -0
  178. package/{server → server-e2e}/neon-driver.ts +2 -2
  179. package/{server → server-e2e}/npm-proxy.ts +12 -145
  180. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  181. package/{server → server-e2e}/objstore/blob.ts +1 -1
  182. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  183. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  184. package/{server → server-e2e}/objstore/init.ts +35 -18
  185. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  186. package/server-e2e/objstore/rest-deny.ts +28 -0
  187. package/server-e2e/objstore/rest-mint.ts +224 -0
  188. package/{server → server-e2e}/objstore/rest.ts +27 -26
  189. package/{server → server-e2e}/objstore/sign.ts +105 -0
  190. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  191. package/{server → server-e2e}/objstore/store.ts +7 -7
  192. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  193. package/{server → server-e2e}/pubsub.ts +12 -9
  194. package/{server → server-e2e}/sign.ts +4 -4
  195. package/{server → server-e2e}/sse-server.ts +102 -68
  196. package/{server → server-e2e}/sse-session.ts +28 -8
  197. package/{server → server-e2e}/static.ts +31 -21
  198. package/server-e2e/sync-handlers.ts +408 -0
  199. package/{server → server-e2e}/ws-server.ts +20 -9
  200. package/server-managed/activity.ts +245 -0
  201. package/server-managed/avatar-store.ts +12 -0
  202. package/server-managed/blob-store.ts +17 -0
  203. package/server-managed/brotli.ts +10 -0
  204. package/server-managed/bundle-advisories.ts +120 -0
  205. package/server-managed/bundle-advisory-inventory.ts +77 -0
  206. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  207. package/server-managed/bundle-build-diagnostics.js +23 -0
  208. package/server-managed/bundle-build-leases.ts +41 -0
  209. package/server-managed/bundle-build-worker.js +111 -0
  210. package/server-managed/bundle-build.ts +198 -0
  211. package/server-managed/bundle-cache.ts +199 -0
  212. package/server-managed/bundle-catalog.ts +29 -0
  213. package/server-managed/bundle-commits.ts +140 -0
  214. package/server-managed/bundle-store.ts +24 -0
  215. package/server-managed/bundle-summary-cache.ts +130 -0
  216. package/server-managed/bundle.ts +120 -0
  217. package/server-managed/cache-storage.ts +19 -0
  218. package/server-managed/cli.js +21 -0
  219. package/server-managed/combined.ts +46 -0
  220. package/server-managed/comments.ts +151 -0
  221. package/server-managed/config.ts +158 -0
  222. package/server-managed/content-access.ts +15 -0
  223. package/server-managed/crypto.ts +25 -0
  224. package/server-managed/db-methods.ts +1725 -0
  225. package/server-managed/db-neon.ts +367 -0
  226. package/server-managed/db-schema.ts +227 -0
  227. package/server-managed/db-table-names.ts +22 -0
  228. package/server-managed/db.ts +132 -0
  229. package/server-managed/dependency-reviews.ts +248 -0
  230. package/server-managed/github-app.ts +391 -0
  231. package/server-managed/github-errors.ts +68 -0
  232. package/server-managed/github-issue-links.ts +127 -0
  233. package/server-managed/github-issue-oauth.ts +47 -0
  234. package/server-managed/github-issues.ts +152 -0
  235. package/server-managed/github-metadata.ts +213 -0
  236. package/server-managed/github-oauth.ts +235 -0
  237. package/server-managed/github-pulls.ts +227 -0
  238. package/server-managed/http-response.ts +50 -0
  239. package/server-managed/http.ts +3472 -0
  240. package/server-managed/import-triage.ts +51 -0
  241. package/server-managed/index.ts +187 -0
  242. package/server-managed/kept.ts +28 -0
  243. package/server-managed/link-reports.ts +158 -0
  244. package/server-managed/locked-packages.ts +85 -0
  245. package/server-managed/lru.ts +30 -0
  246. package/server-managed/managed-issues.ts +75 -0
  247. package/server-managed/management-catalog.ts +45 -0
  248. package/server-managed/management.ts +99 -0
  249. package/server-managed/npm-insights.ts +440 -0
  250. package/server-managed/npm-kept.ts +65 -0
  251. package/server-managed/npm-loads.ts +167 -0
  252. package/server-managed/npm-manifest.ts +115 -0
  253. package/server-managed/npm-packages.ts +378 -0
  254. package/server-managed/npm-provenance.ts +123 -0
  255. package/server-managed/npm-risk.ts +177 -0
  256. package/server-managed/object-storage-disk.ts +148 -0
  257. package/server-managed/object-storage-vercel.ts +67 -0
  258. package/server-managed/object-storage.ts +74 -0
  259. package/server-managed/package-entry-points.ts +51 -0
  260. package/server-managed/pretty-print.ts +352 -0
  261. package/server-managed/public-workspace.ts +220 -0
  262. package/server-managed/rename-diff-worker.js +26 -0
  263. package/server-managed/rename-diff.ts +270 -0
  264. package/server-managed/repo-path.ts +21 -0
  265. package/server-managed/report-migration.ts +35 -0
  266. package/server-managed/report-query.ts +10 -0
  267. package/server-managed/report-response.ts +16 -0
  268. package/server-managed/report-sources.ts +178 -0
  269. package/server-managed/report-store.ts +47 -0
  270. package/server-managed/repository-aliases.ts +91 -0
  271. package/server-managed/repository-browser.ts +153 -0
  272. package/server-managed/repository-discovery.ts +82 -0
  273. package/server-managed/repository-policy.ts +27 -0
  274. package/server-managed/repository-risk.ts +418 -0
  275. package/server-managed/revisions.ts +35 -0
  276. package/server-managed/session.ts +92 -0
  277. package/server-managed/slugs.ts +38 -0
  278. package/server-managed/solidity-entry-points.ts +69 -0
  279. package/server-managed/sql-postgres.ts +31 -0
  280. package/server-managed/sql.ts +76 -0
  281. package/server-managed/static.ts +28 -0
  282. package/server-managed/storage-command.ts +16 -0
  283. package/server-managed/storage-db.ts +175 -0
  284. package/server-managed/storage-encryption.ts +279 -0
  285. package/server-managed/storage-maintenance.ts +256 -0
  286. package/server-managed/storage-payload.ts +103 -0
  287. package/server-managed/storage-stores.ts +85 -0
  288. package/server-managed/storage.ts +78 -0
  289. package/server-managed/team-app.ts +94 -0
  290. package/server-managed/team-catalog.ts +7 -0
  291. package/server-managed/team-feed.ts +173 -0
  292. package/server-managed/team-npm-scopes.ts +77 -0
  293. package/server-managed/team-reports.ts +228 -0
  294. package/server-managed/triage-response.ts +16 -0
  295. package/server-managed/type-only-worker.js +19 -0
  296. package/server-managed/type-only.ts +103 -0
  297. package/server-managed/uploads.ts +86 -0
  298. package/server-managed/upstream-cache.ts +215 -0
  299. package/server-managed/view-as.ts +58 -0
  300. package/server-managed/workspace-shares.ts +166 -0
  301. package/server.ts +50 -0
  302. package/server/index.ts +0 -477
  303. package/server/lifecycle.ts +0 -204
  304. package/server/sync-handlers.ts +0 -320
  305. /package/{LICENSE → LICENSE-MIT} +0 -0
  306. /package/{server → server-e2e}/config.example.json +0 -0
  307. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  308. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  309. /package/{server → server-e2e}/peer.ts +0 -0
  310. /package/{server → server-e2e}/util.ts +0 -0
  311. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,352 @@
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 } 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, readBundleDetails } 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 { loadNpmFiles } from './npm-loads.ts'
26
+ import { type NpmVersionDocument, npmFileText } from './npm-packages.ts'
27
+
28
+ // Part of every kept copy's name; bumped when what the formatter writes for
29
+ // the same file changes, or what its check lets through, so copies written
30
+ // before are no longer read.
31
+ const PRETTY_VERSION = 3
32
+ // Formats running at once per process, each holding its file's syntax tree,
33
+ // and those waiting their turn past them (a version's files are formatted one
34
+ // after another, beside other readers'), any more refused.
35
+ const MAX_ACTIVE_FORMATS = 2
36
+ const MAX_WAITING_FORMATS = 16
37
+
38
+ export class PrettyError extends Error {
39
+ status: number
40
+ constructor(status: number, code: string) { super(code); this.status = status }
41
+ }
42
+
43
+ export interface PrettyRequest { path: string; hash: string; extension: string }
44
+
45
+ // One of a version's files pretty-printed (npmAll), named by `source`, the
46
+ // hash of the file it was formatted from: its copy's text, or why it has
47
+ // none, as a PrettyError tells it.
48
+ export type PrettyLine = { path: string; source: string; text: string } | { path: string; source: string; error: string; status: number }
49
+
50
+ // Which of a comparison's two its files are of: the bundle or version asked
51
+ // about, or the `other`.
52
+ type Side = 'base' | 'other'
53
+
54
+ // One of a comparison's changed files pretty-printed (compared), as a
55
+ // version's is, with the side it is of.
56
+ export type ComparedLine = PrettyLine & { side: Side }
57
+
58
+ // A side of a comparison: its files that can be formatted, with their texts,
59
+ // and every one of its files' hashes, by path; its packages' directories by
60
+ // path, as a bundle records them (bundlePackageDirs), or null where it
61
+ // records none; the copy kept for one, or null; and one made, kept where the
62
+ // side keeps copies.
63
+ export interface ComparedSide {
64
+ side: Side
65
+ files: Array<PrettyRequest & { text: string }>
66
+ hashes: Map<string, string>
67
+ packageDirs: Map<string, string> | null
68
+ kept(request: PrettyRequest): Promise<OpenedBlob | null>
69
+ made(request: PrettyRequest, text: string): Promise<OpenedBlob>
70
+ }
71
+
72
+ // The file a request's `path` and `hash` name, or null for one that can't be
73
+ // formatted.
74
+ export function prettyRequest(params: URLSearchParams): PrettyRequest | null {
75
+ const path = params.get('path') ?? ''
76
+ const extension = prettyExtension(path), hash = params.get('hash') ?? ''
77
+ return extension !== null && FILE_HASH.test(hash) ? { path, hash, extension } : null
78
+ }
79
+
80
+ // A copy's name: its file's hash in hex, with the extension it was parsed by.
81
+ const prettyFile = ({ hash, extension }: PrettyRequest) =>
82
+ `pretty-v${PRETTY_VERSION}/${Buffer.from(hash.slice('sha512-'.length), 'base64').toString('hex')}.${extension}.br`
83
+
84
+ let activeFormats = 0
85
+ const waitingFormats: Array<() => void> = []
86
+
87
+ // The text formatted and Brotli-encoded, where @preventive/compare's checks
88
+ // find formatting changed only its layout: refused as `unsupported` where it
89
+ // has no check for the kind of file, `unformattable` or `pretty-mismatch`.
90
+ export async function prettyBody(text: string, extension: string): Promise<Buffer> {
91
+ if (Buffer.byteLength(text) > MAX_PRETTY_BYTES) throw new PrettyError(413, 'file-too-large')
92
+ if (activeFormats < MAX_ACTIVE_FORMATS) activeFormats++
93
+ else if (waitingFormats.length >= MAX_WAITING_FORMATS) throw new PrettyError(429, 'pretty-busy')
94
+ else await new Promise<void>(resolve => { waitingFormats.push(resolve) })
95
+ let result
96
+ try { result = await format(text, extension) }
97
+ finally {
98
+ const next = waitingFormats.shift()
99
+ if (next) next()
100
+ else activeFormats--
101
+ }
102
+ if ('error' in result) {
103
+ if (result.error === 'pretty-mismatch') console.warn(`managed: pretty-printing a .${extension} file changed more than its layout; not served`)
104
+ throw new PrettyError(422, result.error)
105
+ }
106
+ return encodeBrotli(Buffer.from(result.code))
107
+ }
108
+
109
+ const opened = (body: Buffer): OpenedBlob => ({ size: body.byteLength, stream: Readable.from([body]) })
110
+
111
+ const decompress = promisify(brotliDecompress)
112
+
113
+ // A pretty-printed copy's text, from its Brotli-encoded bytes.
114
+ export async function prettyText(copy: OpenedBlob): Promise<string> {
115
+ const chunks: Buffer[] = []
116
+ for await (const chunk of copy.stream) chunks.push(chunk as Buffer)
117
+ return (await decompress(Buffer.concat(chunks))).toString()
118
+ }
119
+
120
+ // Every file of a version that needs pretty-printing (needsPrettyPrint), read
121
+ // in one load of its files, in path order.
122
+ async function npmPrintable(doc: NpmVersionDocument): Promise<Array<PrettyRequest & { text: string }>> {
123
+ const load = loadNpmFiles(doc)
124
+ const files: Array<PrettyRequest & { text: string }> = []
125
+ try {
126
+ for (const { path, bytes } of await load.result) {
127
+ const extension = prettyExtension(path)
128
+ if (extension === null || bytes.byteLength > MAX_PRETTY_BYTES) continue
129
+ const text = npmFileText(bytes)
130
+ if (text !== null && needsPrettyPrint(path, text)) files.push({ path, hash: await computeFileHash(bytes), extension, text })
131
+ }
132
+ } finally { load.release() }
133
+ return files.toSorted((a, b) => (a.path < b.path ? -1 : 1))
134
+ }
135
+
136
+ // A file's line (PrettyLine): the text of its copy, as `copy` has it, or why
137
+ // it has none.
138
+ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<OpenedBlob>): Promise<PrettyLine> {
139
+ try { return { path, source: hash, text: await prettyText(await copy()) } }
140
+ catch (err) {
141
+ if (!(err instanceof PrettyError)) throw err
142
+ return { path, source: hash, error: err.message, status: err.status }
143
+ }
144
+ }
145
+
146
+ // The files that changed between two sides, pretty-printed: each side's
147
+ // formattable files the other has another text at the path of, or, where it
148
+ // has none there, none of their text at a path of its own (a file renamed, or
149
+ // moved with its tree, unchanged, has it there; a copy at a path both have is
150
+ // not one), by the paths each side has them at. Those are both of the pairs
151
+ // the viewer diffs but where it pairs files otherwise (among copies of one
152
+ // text, across packages), whose copies it asks for alone. As they are
153
+ // iterated, in path order, the base's first where both have one: those kept,
154
+ // at once; then the rest, formatted one after another, the minified before
155
+ // the others (needsPrettyPrint), as the viewer prints those unasked.
156
+ export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGenerator<ComparedLine> {
157
+ // Each side's texts at the paths the other hasn't: where a file moved has its own.
158
+ const moved = sides.map((own, i) => new Set([...own.hashes].filter(([path]) => !sides[1 - i]!.hashes.has(path)).map(([, hash]) => hash)))
159
+ const changed = sides.flatMap((own, i) => {
160
+ const other = sides[1 - i]!.hashes
161
+ return own.files.filter(({ path, hash }) => (other.has(path) ? other.get(path) !== hash : !moved[1 - i]!.has(hash))).map(file => ({ own, file }))
162
+ }).toSorted((a, b) => (a.file.path < b.file.path ? -1 : a.file.path > b.file.path ? 1 : 0))
163
+ return (async function* () {
164
+ const unkept: typeof changed = []
165
+ for (const { own, file } of changed) {
166
+ const copy = await own.kept(file)
167
+ if (copy) yield { side: own.side, ...await prettyLine(file, () => Promise.resolve(copy)) }
168
+ else unkept.push({ own, file })
169
+ }
170
+ const minified = new Map(unkept.map(({ file }) => [file, needsPrettyPrint(file.path, file.text)]))
171
+ for (const { own, file } of unkept.toSorted((a, b) => Number(minified.get(b.file)) - Number(minified.get(a.file)))) {
172
+ yield { side: own.side, ...await prettyLine(file, () => own.made(file, file.text)) }
173
+ }
174
+ })()
175
+ }
176
+
177
+ // Jobs by key, readers asking for one while it runs sharing it.
178
+ function sharing<T>(): (key: string, make: () => Promise<T>) => Promise<T> {
179
+ const pending = new Map<string, Promise<T>>()
180
+ return (key, make) => {
181
+ let job = pending.get(key)
182
+ if (!job) pending.set(key, job = make().finally(() => pending.delete(key)))
183
+ return job
184
+ }
185
+ }
186
+
187
+ // Work done one job at a time, in the order asked.
188
+ function serial(): <T>(work: () => Promise<T>) => Promise<T> {
189
+ let queue = Promise.resolve()
190
+ return work => {
191
+ const job = queue.then(work)
192
+ queue = job.then(() => undefined, () => undefined)
193
+ return job
194
+ }
195
+ }
196
+
197
+ async function keep(put: () => Promise<void>) {
198
+ try { await put() } catch (err) { console.warn('managed: pretty-print cache write failed:', err) }
199
+ }
200
+
201
+ // A copy kept, or null where none is.
202
+ async function kept(open: () => Promise<OpenedBlob>): Promise<OpenedBlob | null> {
203
+ try { return await open() }
204
+ catch (err) { if (err instanceof CacheMissError) return null; throw err }
205
+ }
206
+
207
+ // A side's files that can be formatted, with their texts, and every one of
208
+ // its files' hashes by path, from their paths and contents: texts, or bytes,
209
+ // read as text where they are.
210
+ async function sideFiles(entries: Iterable<[string, string | Uint8Array]>): Promise<Pick<ComparedSide, 'files' | 'hashes'>> {
211
+ const files: ComparedSide['files'] = [], hashes = new Map<string, string>()
212
+ for (const [path, content] of entries) {
213
+ const hash = await computeFileHash(content)
214
+ hashes.set(path, hash)
215
+ const extension = prettyExtension(path)
216
+ if (extension === null || (typeof content === 'string' ? Buffer.byteLength(content) : content.byteLength) > MAX_PRETTY_BYTES) continue
217
+ const text = typeof content === 'string' ? content : npmFileText(content)
218
+ if (text !== null) files.push({ path, hash, extension, text })
219
+ }
220
+ return { files, hashes }
221
+ }
222
+
223
+ // A bundle's side, from its details: its files, as sideFiles reads them,
224
+ // within a reason `scope` as Compare reads them there (bundleCompareFiles),
225
+ // and its packages' directories.
226
+ async function bundleSide(details: Parameters<typeof bundlePackageDirs>[0], scope: string): Promise<Pick<ComparedSide, 'files' | 'hashes' | 'packageDirs'>> {
227
+ const scoped = scope ? new Set(bundleCompareFiles(details, scope).keys()) : null, sources = bundleSourcesAsMap(details)
228
+ return { ...await sideFiles(scoped ? [...sources].filter(([path]) => scoped.has(path)) : sources), packageDirs: bundlePackageDirs(details) }
229
+ }
230
+
231
+ // A version's files by path, from one load of them.
232
+ async function npmFiles(doc: NpmVersionDocument): Promise<Array<[string, Uint8Array]>> {
233
+ const load = loadNpmFiles(doc)
234
+ try { return (await load.result).map(({ path, bytes }) => [path, bytes]) }
235
+ finally { load.release() }
236
+ }
237
+
238
+ // Removes the public npm copies of every version before PRETTY_VERSION, which
239
+ // nothing reads any more, for maintenance: once a process, again where
240
+ // removing them failed. A bundle's earlier copies go with the bundle.
241
+ export function stalePrettyRemover(npm: CacheStorage): (signal?: AbortSignal) => Promise<void> {
242
+ let removed = false
243
+ return async signal => {
244
+ if (removed) return
245
+ for (let version = 1; version < PRETTY_VERSION; version++) {
246
+ if (signal?.aborted) return
247
+ await npm.delete(`pretty-v${version}`)
248
+ }
249
+ removed = true
250
+ }
251
+ }
252
+
253
+ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage, db: Pick<ManagedDb, 'getBundle'>, store: BundleStore) {
254
+ // Copies being made; readers asking for one meanwhile share it.
255
+ const shared = sharing<Buffer>()
256
+ // Bundles are read one at a time: reading one decodes all of it.
257
+ const enqueue = serial()
258
+ function bundleDetails(record: ManagedBundle) {
259
+ return enqueue(async () => {
260
+ const details = await readBundleDetails(record, store)
261
+ if (!details) throw new PrettyError(422, 'bundle-unavailable')
262
+ return details
263
+ })
264
+ }
265
+ // A bundle's file pretty-printed from the text `read` reads, and kept in
266
+ // its cache.
267
+ function bundleCopy(record: ManagedBundle, request: PrettyRequest, read: () => Promise<string>): Promise<Buffer> {
268
+ const file = prettyFile(request)
269
+ return shared(`bundle ${record.id} ${file}`, async () => {
270
+ const body = await prettyBody(await read(), request.extension)
271
+ // As for its metadata (bundle-cache.ts): a bundle deleted meanwhile
272
+ // keeps no copy, nor does one deleted while it was written.
273
+ if (!await db.getBundle(record.id)) throw new PrettyError(404, 'no-bundle')
274
+ await keep(() => bundles.put(record.id, file, body))
275
+ if (!await db.getBundle(record.id)) {
276
+ await bundles.delete(record.id)
277
+ throw new PrettyError(404, 'no-bundle')
278
+ }
279
+ return body
280
+ })
281
+ }
282
+ // A version's file pretty-printed, `read` reading its text (and checking
283
+ // it is the file named): from the copy kept for a public version, else
284
+ // made, and kept for a public one.
285
+ async function npmCopy(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
286
+ const file = prettyFile(request)
287
+ if (!doc.private) {
288
+ try { return await npm.open(file) }
289
+ catch (err) { if (!(err instanceof CacheMissError)) throw err }
290
+ }
291
+ return opened(await shared(doc.private ? `npm ${doc.dist.integrity} ${file}` : `npm ${file}`, async () => {
292
+ const body = await prettyBody(await read(), request.extension)
293
+ if (!doc.private) await keep(() => npm.put(file, body))
294
+ return body
295
+ }))
296
+ }
297
+ return {
298
+ async bundle(record: ManagedBundle, request: PrettyRequest): Promise<OpenedBlob> {
299
+ const copy = await kept(() => bundles.open(record.id, prettyFile(request)))
300
+ if (copy) return copy
301
+ return opened(await bundleCopy(record, request, async () => {
302
+ const text = bundleSourcesAsMap(await bundleDetails(record)).get(request.path)
303
+ if (text === undefined) throw new PrettyError(404, 'no-file')
304
+ if (await computeFileHash(text) !== request.hash) throw new PrettyError(409, 'hash-mismatch')
305
+ return text
306
+ }))
307
+ },
308
+ // The version's file, from its files loaded as the viewer's are (and
309
+ // held to the same few loads at once).
310
+ npm(doc: NpmVersionDocument, request: PrettyRequest): Promise<OpenedBlob> {
311
+ return npmCopy(doc, request, async () => {
312
+ const load = loadNpmFiles(doc)
313
+ let text: string | null
314
+ try {
315
+ const bytes = (await load.result).find(entry => entry.path === request.path)?.bytes
316
+ if (!bytes) throw new PrettyError(404, 'no-file')
317
+ if (await computeFileHash(bytes) !== request.hash) throw new PrettyError(409, 'hash-mismatch')
318
+ text = npmFileText(bytes)
319
+ } finally { load.release() }
320
+ if (text === null) throw new PrettyError(422, 'unformattable')
321
+ return text
322
+ })
323
+ },
324
+ // Every file of the version that needs pretty-printing (npmPrintable),
325
+ // read before this resolves; then, as they are iterated, each
326
+ // pretty-printed as `npm` has it, one after another.
327
+ async npmAll(doc: NpmVersionDocument): Promise<AsyncGenerator<PrettyLine>> {
328
+ const files = await npmPrintable(doc)
329
+ return (async function* () {
330
+ for (const { text, ...request } of files) yield await prettyLine(request, () => npmCopy(doc, request, () => Promise.resolve(text)))
331
+ })()
332
+ },
333
+ // Two bundles' sides, or two versions', as a comparison reads them, read
334
+ // before this resolves: for the files that changed between them
335
+ // pretty-printed (compared), and their diffs with renamed names hidden
336
+ // (rename-diff.ts renamedPairs); a bundle's within a reason `scope`
337
+ // where one is asked for, as Compare reads it then.
338
+ async bundleSides(base: ManagedBundle, other: ManagedBundle, scope = ''): Promise<[ComparedSide, ComparedSide]> {
339
+ const sideOf = async (record: ManagedBundle, side: Side): Promise<ComparedSide> => ({ side, ...await bundleSide(await bundleDetails(record), scope),
340
+ kept: request => kept(() => bundles.open(record.id, prettyFile(request))),
341
+ made: async (request, text) => opened(await bundleCopy(record, request, () => Promise.resolve(text))) })
342
+ return [await sideOf(base, 'base'), await sideOf(other, 'other')]
343
+ },
344
+ async npmSides(base: NpmVersionDocument, other: NpmVersionDocument): Promise<[ComparedSide, ComparedSide]> {
345
+ const sideOf = async (doc: NpmVersionDocument, side: Side): Promise<ComparedSide> => ({ side, ...await sideFiles(await npmFiles(doc)), packageDirs: null,
346
+ kept: request => (doc.private ? Promise.resolve(null) : kept(() => npm.open(prettyFile(request)))),
347
+ made: (request, text) => npmCopy(doc, request, () => Promise.resolve(text)) })
348
+ return [await sideOf(base, 'base'), await sideOf(other, 'other')]
349
+ },
350
+ }
351
+ }
352
+ export type PrettyCache = ReturnType<typeof createPrettyCache>
@@ -0,0 +1,220 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import { pipeline } from 'node:stream/promises'
3
+ import { Buffer } from 'node:buffer'
4
+ import type { ManagedHttpDeps } from './http.ts'
5
+ import type { ManagedBundle } from './db.ts'
6
+ import type { OpenedBlob } from './blob-store.ts'
7
+ import type { WorkspaceShareSnapshot } from './workspace-shares.ts'
8
+ import { sendJson, sendLines, writeResponse } from './http-response.ts'
9
+ import { hashToken } from './crypto.ts'
10
+ import { TeamReportsError, loadTeamAnnotations, loadTeamReportsResponse, teamReportVisibility } from './team-reports.ts'
11
+ import { triageWireEntry } from './triage-response.ts'
12
+ import { MAX_PACKAGE_INVENTORY_BYTES } from './bundle-cache.ts'
13
+ import { backfillBundleSummaries, bundleSummaries } from './bundle-catalog.ts'
14
+ import { ADVISORIES_TIMEOUT_MS, fetchBundleAdvisories } from './bundle-advisories.ts'
15
+ import { auditCache, auditedRepos } from './upstream-cache.ts'
16
+ import { serveTeamFeed } from './team-feed.ts'
17
+ import { type ComparedSide, PrettyError, compared, prettyRequest, prettyText } from './pretty-print.ts'
18
+ import { renameDiffOf, renameDiffRequest, renamedPairs } from './rename-diff.ts'
19
+ import { typeOnlyFiles } from './type-only.ts'
20
+ import { sharedTeamApp } from './team-app.ts'
21
+
22
+ function json(res: ServerResponse, status: number, body: unknown): void {
23
+ sendJson(res, status, body, { 'cache-control': 'private, no-store', 'referrer-policy': 'no-referrer' })
24
+ }
25
+
26
+ // Called before ALL other routing, including OAuth, discovery, static and the
27
+ // combined server fallback. A supplied capability never inherits cookie auth.
28
+ export async function handlePublicWorkspace(req: IncomingMessage, res: ServerResponse, deps: ManagedHttpDeps, url: URL): Promise<void> {
29
+ const token = req.headers['x-deepview-share']
30
+ if (!deps.config.allowShare || typeof token !== 'string' || !/^[A-Za-z0-9_-]{43}$/u.test(token)) {
31
+ json(res, 401, { error: 'invalid-share' }); return
32
+ }
33
+ const method = req.method ?? 'GET'
34
+ if (method !== 'GET' && method !== 'HEAD') { json(res, 403, { error: 'share-read-only' }); return }
35
+ const shareRoute = /^\/api\/shares\/([A-Za-z0-9_-]+)\/workspace$/u.exec(url.pathname)
36
+ const teamRoute = /^\/api\/teams\/([^/]+)\/(shared|reports|feed|annotations)$/u.exec(url.pathname)
37
+ const reportRoute = /^\/api\/reports\/([^/]+)\/(triage|comments|sources)$/u.exec(url.pathname)
38
+ const bundleRoute = /^\/api\/bundles\/([^/]+)\/(metadata|contents|download|advisories|pretty|rename-diff|type-only)$/u.exec(url.pathname)
39
+ if (!shareRoute && !teamRoute && !reportRoute && !bundleRoute) { json(res, 403, { error: 'share-scope-required' }); return }
40
+ const tokenHash = hashToken(token)
41
+ const snapshot = await deps.db.getWorkspaceShare(tokenHash)
42
+ if (!snapshot) { json(res, 401, { error: 'invalid-share' }); return }
43
+ const recheck = async () => {
44
+ const current = deps.config.allowShare && await deps.db.getWorkspaceShare(tokenHash)
45
+ if (!current || JSON.stringify(current) !== JSON.stringify(snapshot)) throw new TeamReportsError(404, 'workspace-changed')
46
+ }
47
+ const send = async (body: unknown) => { await recheck(); json(res, 200, body) }
48
+ const sendTeam = async () => {
49
+ const [summaries, app] = await Promise.all([bundleSummaries(snapshot.team.bundles, deps.bundleCache), sharedTeamApp(deps.db, deps.reportStore, snapshot)])
50
+ await send({ user: snapshot.user, team: { ...snapshot.team, app,
51
+ bundles: snapshot.team.bundles.map(bundle => ({ ...bundle, ...(summaries.get(bundle.integrity) ?? { summary: null, summaryRetryAt: null }) })),
52
+ } })
53
+ await backfillBundleSummaries(snapshot.team.bundles, deps.bundleCache)
54
+ }
55
+ const stream = async (stored: OpenedBlob, encoding: string | null, contentType = 'application/json') => {
56
+ try { await recheck() } catch (error) { stored.stream.destroy(); throw error }
57
+ res.writeHead(200, { 'content-type': contentType, 'cache-control': 'private, no-store',
58
+ 'x-content-type-options': 'nosniff', ...(encoding ? { 'content-encoding': encoding } : {}),
59
+ ...(stored.size == null ? {} : { 'content-length': String(stored.size) }),
60
+ })
61
+ if (method === 'HEAD') { stored.stream.destroy(); res.end(); return }
62
+ try { await pipeline(stored.stream, res) } catch { res.destroy() }
63
+ }
64
+ if (url.searchParams.has('team') && (url.searchParams.getAll('team').length !== 1 || url.searchParams.get('team') !== snapshot.teamId)) {
65
+ json(res, 404, { error: 'no-team' }); return
66
+ }
67
+ if (shareRoute) {
68
+ // The short hash is a recognizable link ID, never an access grant. Resolve
69
+ // with the full token so colliding IDs or slugs cannot broaden its scope.
70
+ // Existing links used the team ID as their fragment prefix.
71
+ if (shareRoute[1] !== tokenHash.slice(0, 8) && shareRoute[1] !== snapshot.teamId) {
72
+ json(res, 404, { error: 'no-share' }); return
73
+ }
74
+ await sendTeam(); return
75
+ }
76
+ if (teamRoute) {
77
+ if (teamRoute[1] !== snapshot.teamId) { json(res, 404, { error: 'no-team' }); return }
78
+ if (teamRoute[2] === 'feed') {
79
+ if (method !== 'GET') { json(res, 405, { error: 'method-not-allowed' }); return }
80
+ await serveTeamFeed(res, deps, snapshot, recheck, { readState: () => deps.db.getWorkspaceShareFeedState(tokenHash) }); return
81
+ }
82
+ if (teamRoute[2] === 'annotations') {
83
+ await send(await loadTeamAnnotations(deps.db, deps.reportStore, snapshot, url.searchParams.get('reportId'))); return
84
+ }
85
+ if (teamRoute[2] === 'shared') { await sendTeam(); return }
86
+ const body = await loadTeamReportsResponse(deps.db, deps.reportStore, snapshot)
87
+ await recheck()
88
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'private, no-store', 'referrer-policy': 'no-referrer' })
89
+ writeResponse(res, body); return
90
+ }
91
+ if (reportRoute) {
92
+ const id = reportRoute[1]!, part = reportRoute[2]!
93
+ if (!snapshot.reports.some(report => report.id === id)) { json(res, 404, { error: 'no-report' }); return }
94
+ const visible = await teamReportVisibility(deps.db, deps.reportStore, snapshot, id)
95
+ await recheck()
96
+ if (part === 'sources') { await sources(res, deps, snapshot, id, visible.sourcePaths, recheck, stream); return }
97
+ if (part === 'comments') { await send({ comments: await deps.db.listComments([...visible.ids]) }); return }
98
+ const entries = Object.fromEntries((await deps.db.listTriage([...visible.ids])).map(row => [row.findingId, triageWireEntry(row)]))
99
+ await send({ entries }); return
100
+ }
101
+ const bundle = snapshot.bundles.find(item => item.id === bundleRoute![1])
102
+ if (!bundle) { json(res, 404, { error: 'no-bundle' }); return }
103
+ if (bundleRoute![2] === 'advisories' && !snapshot.permissions.security) { json(res, 403, { error: 'security-access-required' }); return }
104
+ if (bundleRoute![2] === 'rename-diff' || bundleRoute![2] === 'type-only' || (bundleRoute![2] === 'pretty' && url.searchParams.has('other'))) {
105
+ if (method !== 'GET') { json(res, 405, { error: 'method-not-allowed' }); return }
106
+ await serveCompared(res, deps, bundle, snapshot.bundles.find(item => item.id === url.searchParams.get('other')), bundleRoute![2], url, recheck, stream); return
107
+ }
108
+ await serveBundle(res, deps, bundle, bundleRoute![2]!, url, recheck, stream)
109
+ }
110
+
111
+ // What a bundle is compared with `other`, another of the link's bundles, by:
112
+ // one of their JavaScript files diffed with renamed names hidden, or the
113
+ // files that changed between them, or the pairs of those, or those that
114
+ // changed in types alone, a line each.
115
+ function serveCompared(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, other: ManagedBundle | undefined, part: string, url: URL,
116
+ recheck: () => Promise<void>, stream: (stored: OpenedBlob, encoding: string | null, contentType?: string) => Promise<void>) {
117
+ if (part === 'rename-diff' && url.searchParams.has('path')) return serveRenameDiff(res, deps, bundle, other, url, stream)
118
+ return serveChanges(res, deps, bundle, other, recheck, part === 'pretty' ? compared : part === 'type-only' ? typeOnlyFiles : renamedPairs,
119
+ part === 'pretty' ? '' : url.searchParams.get('scope') ?? '')
120
+ }
121
+
122
+ // One of a bundle's JavaScript files diffed with one of `other`, another of
123
+ // the link's bundles (rename-diff.ts).
124
+ async function serveRenameDiff(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, other: ManagedBundle | undefined, url: URL,
125
+ stream: (stored: OpenedBlob, encoding: string | null, contentType?: string) => Promise<void>) {
126
+ const request = renameDiffRequest(url.searchParams)
127
+ if (!request) { json(res, 400, { error: 'bad-file' }); return }
128
+ if (!other) { json(res, 404, { error: 'no-bundle' }); return }
129
+ const { prettyCache } = deps
130
+ if (!prettyCache) { json(res, 503, { error: 'unavailable' }); return }
131
+ let diff
132
+ try {
133
+ diff = await renameDiffOf(request, async () => [await prettyText(await prettyCache.bundle(bundle, request.base)),
134
+ await prettyText(await prettyCache.bundle(other, request.other))])
135
+ } catch (err) {
136
+ if (err instanceof PrettyError) { json(res, err.status, { error: err.message }); return }
137
+ throw err
138
+ }
139
+ await stream(diff, 'br')
140
+ }
141
+
142
+ // The files that changed between a bundle and `other`, another of the link's
143
+ // bundles, both sides' pretty-printed (pretty-print.ts compared), or the pairs
144
+ // of them its viewer diffs with renamed names hidden (rename-diff.ts
145
+ // renamedPairs), those within a reason `scope` where given, the link checked
146
+ // again before each line.
147
+ async function serveChanges(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, other: ManagedBundle | undefined, recheck: () => Promise<void>,
148
+ linesOf: (sides: [ComparedSide, ComparedSide]) => AsyncIterable<unknown>, scope: string) {
149
+ if (!other) { json(res, 404, { error: 'no-bundle' }); return }
150
+ if (!deps.prettyCache) { json(res, 503, { error: 'unavailable' }); return }
151
+ let lines
152
+ try { lines = linesOf(await deps.prettyCache.bundleSides(bundle, other, scope)) }
153
+ catch (err) {
154
+ if (err instanceof PrettyError) { json(res, err.status, { error: err.message }); return }
155
+ throw err
156
+ }
157
+ await recheck()
158
+ await sendLines(res, lines, () => recheck().then(() => true, () => false))
159
+ }
160
+
161
+ async function serveBundle(res: ServerResponse, deps: ManagedHttpDeps, bundle: ManagedBundle, part: string, url: URL,
162
+ recheck: () => Promise<void>, stream: (stored: OpenedBlob, encoding: string | null, contentType?: string) => Promise<void>) {
163
+ if (part === 'download') {
164
+ const stored = await deps.bundleStore.open(bundle.id, bundle.kind)
165
+ if (!stored) { json(res, 503, { error: 'unavailable' }); return }
166
+ await stream(stored, bundle.kind === 'sourcemap' ? 'br' : null, 'application/octet-stream'); return
167
+ }
168
+ if (part === 'pretty') {
169
+ const file = prettyRequest(url.searchParams)
170
+ if (!file) { json(res, 400, { error: 'bad-file' }); return }
171
+ if (!deps.prettyCache) { json(res, 503, { error: 'unavailable' }); return }
172
+ let pretty
173
+ try { pretty = await deps.prettyCache.bundle(bundle, file) }
174
+ catch (err) {
175
+ if (err instanceof PrettyError) { json(res, err.status, { error: err.message }); return }
176
+ throw err
177
+ }
178
+ await stream(pretty, 'br', 'text/plain; charset=utf-8'); return
179
+ }
180
+ if (!deps.bundleCache) { json(res, 503, { error: 'unavailable' }); return }
181
+ if (part === 'advisories') {
182
+ if (bundle.kind !== 'stasis') { json(res, 422, { error: 'unsupported-bundle' }); return }
183
+ const inventory = await deps.bundleCache.advisoryInventory(bundle, url.searchParams.get('reason') ?? '')
184
+ await recheck()
185
+ if (inventory === null) { json(res, 413, { error: 'payload-too-large' }); return }
186
+ if (inventory === undefined) { json(res, 400, { error: 'unknown-reason' }); return }
187
+ if (Buffer.byteLength(JSON.stringify(inventory)) > MAX_PACKAGE_INVENTORY_BYTES) { json(res, 413, { error: 'payload-too-large' }); return }
188
+ const signal = AbortSignal.timeout(ADVISORIES_TIMEOUT_MS)
189
+ const repoAdvisories = url.searchParams.get('repoAdvisories') === 'true'
190
+ const result = await fetchBundleAdvisories(inventory.packages, signal, {
191
+ debug: deps.config.debug, repoAdvisories, details: url.searchParams.get('details') === 'true',
192
+ cache: auditCache(deps.config.upstreamCacheDir, deps.db, signal, deps.config.debug, auditedRepos(inventory.packages, repoAdvisories)),
193
+ })
194
+ await recheck()
195
+ json(res, result.status, result.status === 200 ? { ...inventory, advisories: result.body } : result.body); return
196
+ }
197
+ let cached
198
+ try { cached = await deps.bundleCache.open(bundle, part as 'metadata' | 'contents') }
199
+ catch { json(res, 422, { error: 'bundle-unavailable' }); return }
200
+ await stream(cached, 'br')
201
+ }
202
+
203
+ async function sources(res: ServerResponse, deps: ManagedHttpDeps, snapshot: WorkspaceShareSnapshot,
204
+ id: string, paths: Set<string>, recheck: () => Promise<void>, stream: (stored: OpenedBlob, encoding: string | null) => Promise<void>) {
205
+ const report = await deps.db.getReport(id)
206
+ const bundle = report?.bundleId ? await deps.db.getBundle(report.bundleId) : null
207
+ const empty = async () => { await recheck(); res.writeHead(204, { 'cache-control': 'private, no-store' }); res.end() }
208
+ if (!report || !bundle || !['stasis', 'sourcemap'].includes(bundle.kind ?? '') || bundle.repoId !== report.repoId) { await empty(); return }
209
+ if (!deps.reportSourcesCache) { json(res, 503, { error: 'unavailable' }); return }
210
+ let cached
211
+ try { cached = await deps.reportSourcesCache.open(report, bundle, snapshot.permissions, paths) }
212
+ catch { await empty(); return }
213
+ if (!cached) { await empty(); return }
214
+ const current = await deps.db.getReport(id), currentBundle = await deps.db.getBundle(bundle.id)
215
+ if (JSON.stringify(current) !== JSON.stringify(report) || JSON.stringify(currentBundle) !== JSON.stringify(bundle)
216
+ || snapshot.reports.find(item => item.id === id)?.repo.github !== cached.repo.github) {
217
+ cached.stream.destroy(); throw new TeamReportsError(404, 'workspace-changed')
218
+ }
219
+ await stream(cached, 'gzip')
220
+ }
@@ -0,0 +1,26 @@
1
+ // Compare's diff with renamed names hidden (rename-diff.ts), worked out off the
2
+ // server's event loop by @preventive/compare's renamedDiffs: the old side given
3
+ // the new side's names, formatted again where that changes its layout alone,
4
+ // diffed with whitespace changes shown and hidden, and the blocks checked to
5
+ // take it to the new side. `{ before, blocks, reason, reads, marks,
6
+ // ignoringWhitespace }`: the old side as diffed, the change blocks, why the
7
+ // old side is as it was where its names could not be given safely, the
8
+ // properties through which either side reads names (a view marks renamed
9
+ // lines rather than hiding them where any are read, as a rename may then
10
+ // change what the code does), and the blocks of the diff of the files as they
11
+ // are that differ in renamed names alone; `ignoringWhitespace` the blocks and
12
+ // those marked with whitespace changes hidden, as diff -b hides them. Blocks
13
+ // that would not take the old side to the new are thrown, and so answer
14
+ // nothing.
15
+ import { parentPort, workerData } from 'node:worker_threads'
16
+ import { renamedDiffs } from '@preventive/compare/names.js'
17
+
18
+ const { before, after, extension, otherExtension } = workerData
19
+ const diff = await renamedDiffs(before, after, { extension, otherExtension, whitespace: ['none', 'change'] })
20
+ // A check that should never fail did, or a step gave way: told, the file shown with its names.
21
+ for (const warning of diff.warnings) console.warn(`managed: rename diff: ${warning}`)
22
+ const [shown, hidden] = diff.diffs
23
+ const answer = { before: diff.before, blocks: shown.blocks, reason: diff.reason, reads: diff.reads, marks: shown.marks, ignoringWhitespace: { blocks: hidden.blocks, marks: hidden.marks } }
24
+ // Node worker messages have no browser target origin.
25
+ // oxlint-disable-next-line unicorn/require-post-message-target-origin
26
+ parentPort.postMessage(answer)