@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.
- package/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +2 -0
- package/client/dependency-paths.js +24 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +112 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +14 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +310 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +23 -0
- package/common/dependency-reviews.js +81 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +254 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +35 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-4F6JSF57.js +3 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-AQ75QITE.js +1 -0
- package/out/chunk-B2RQY6OP.js +2 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-IISEF2BZ.js +1 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-MYDDTA7W.js +225 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-RSHMVYNS.js +1 -0
- package/out/chunk-TGANXVTA.js +1 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-UN5ZSYLE.js +1 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UXNUAIIC.js +33 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-VQJCXGIY.js +7 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-WZDTZE64.js +1 -0
- package/out/chunk-XYDGU6BM.js +51 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZBBOBQ43.js +1 -0
- package/out/chunk-ZMQQCGF6.js +2 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +37 -0
- package/out/client-sync.js +10 -13
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +6 -2
- package/out/stasis.svg +20 -0
- package/out/terminal.js +365 -39
- package/out/view.css +1 -1
- package/out/view.js +83 -117
- package/package.json +297 -59
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +358 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +142 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +219 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +113 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +486 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +56 -35
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +116 -18
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +35 -18
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +27 -26
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +102 -68
- package/{server → server-e2e}/sse-session.ts +28 -8
- package/{server → server-e2e}/static.ts +31 -21
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +245 -0
- package/server-managed/avatar-store.ts +12 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +111 -0
- package/server-managed/bundle-build.ts +198 -0
- package/server-managed/bundle-cache.ts +199 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +120 -0
- package/server-managed/cache-storage.ts +19 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +158 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1725 -0
- package/server-managed/db-neon.ts +367 -0
- package/server-managed/db-schema.ts +227 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +132 -0
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +391 -0
- package/server-managed/github-errors.ts +68 -0
- package/server-managed/github-issue-links.ts +127 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +235 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/http-response.ts +50 -0
- package/server-managed/http.ts +3472 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +187 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +158 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +75 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +74 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +220 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +47 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +153 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-risk.ts +418 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/session.ts +92 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +31 -0
- package/server-managed/sql.ts +76 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +279 -0
- package/server-managed/storage-maintenance.ts +256 -0
- package/server-managed/storage-payload.ts +103 -0
- package/server-managed/storage-stores.ts +85 -0
- package/server-managed/storage.ts +78 -0
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +173 -0
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/team-reports.ts +228 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +86 -0
- package/server-managed/upstream-cache.ts +215 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +166 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -477
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -320
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /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)
|