@preventive/triage 1.0.0-alpha.37 → 1.0.0-alpha.38
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/README.md +16 -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 +6 -2
- package/common/bundle-metadata.js +63 -20
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-source-links.js +7 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +3 -1
- package/common/bundle-sources.js +47 -19
- package/common/code-language.js +16 -2
- package/common/default-scan-models.ts +2 -1
- 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/github-pr.ts +1 -1
- 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/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +1 -5
- package/common/managed/routes.js +123 -12
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +1 -1
- 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/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-4XCDFGDP.js +10 -0
- package/out/chunk-5FKRIJGF.js +1 -0
- package/out/chunk-6Z5XLSPJ.js +1 -0
- package/out/chunk-ABW5FVSN.js +1 -0
- package/out/chunk-BJKRVROW.js +1 -0
- package/out/chunk-BYUEDJOM.js +7 -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-DCQX2J3C.js +223 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-F4OMDNEY.js +1 -0
- package/out/chunk-FU4MTW7F.js +1 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GMFXFEUA.js +2 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-HIV7SVI7.js +1 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-JAKWIZWL.js +2 -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-NRPC74AM.js +1 -0
- package/out/chunk-OJ5DGP5E.js +33 -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-SD2WJAXN.js +48 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-UNTRGE26.js +2 -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-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZPD5R5RP.js +3 -0
- package/out/client-managed-import.js +3 -87
- package/out/client-managed.js +26 -338
- package/out/client-sync.js +8 -14
- package/out/graph.js +17 -30
- package/out/index.html +34 -0
- package/out/prism.js +6 -2
- package/out/terminal.js +241 -257
- package/out/view.css +1 -1
- package/out/view.js +76 -520
- package/package.json +88 -7
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +164 -0
- package/risk/files/runs-code.js +329 -0
- package/risk/files/script.js +112 -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 +222 -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 +33 -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-e2e/http.ts +7 -7
- package/server-e2e/npm-proxy.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +2 -2
- package/server-e2e/objstore/handlers.ts +1 -1
- package/server-e2e/objstore/tokens.ts +1 -1
- package/server-e2e/sign.ts +1 -1
- package/server-managed/bundle-advisories.ts +68 -11
- package/server-managed/bundle-cache.ts +37 -4
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-summary-cache.ts +24 -10
- package/server-managed/config.ts +4 -0
- package/server-managed/db-methods.ts +98 -11
- package/server-managed/db-neon.ts +39 -4
- package/server-managed/db-schema.ts +14 -2
- package/server-managed/db-table-names.ts +1 -1
- package/server-managed/db.ts +7 -3
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-issue-oauth.ts +5 -6
- package/server-managed/github-metadata.ts +115 -0
- package/server-managed/github-oauth.ts +38 -2
- package/server-managed/http-response.ts +26 -0
- package/server-managed/http.ts +943 -52
- package/server-managed/index.ts +11 -5
- package/server-managed/kept.ts +28 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -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/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +78 -7
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/report-sources.ts +13 -2
- package/server-managed/repository-browser.ts +19 -5
- package/server-managed/repository-risk.ts +413 -0
- package/server-managed/session.ts +16 -2
- package/server-managed/sql.ts +5 -2
- package/server-managed/storage-encryption.ts +5 -3
- package/server-managed/storage-payload.ts +4 -0
- package/server-managed/storage-stores.ts +2 -0
- package/server-managed/storage.ts +6 -3
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-feed.ts +1 -1
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +25 -4
- package/server-managed/upstream-cache.ts +147 -5
- package/server-managed/view-as.ts +58 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Where npm installs a dependency from, read off its spec as npm-package-arg
|
|
2
|
+
// reads one: the registry (a version, a range, a tag, an `npm:` alias), a
|
|
3
|
+
// path, a repository on a host npm knows, a git URL or a tarball's.
|
|
4
|
+
|
|
5
|
+
// The hosts npm takes a repository on (hosted-git-info, which npm-package-arg
|
|
6
|
+
// reads a spec with): by name (`github:owner/repo`), or by its domain over
|
|
7
|
+
// the protocols each takes, where its path names a repository, `[user,
|
|
8
|
+
// project, committish]`, and not a tarball or a file.
|
|
9
|
+
const trim = project => project?.replace(/\.git$/u, '')
|
|
10
|
+
const GIT_HOSTS = new Map([
|
|
11
|
+
['github', { label: 'GitHub', domain: 'github.com', protocols: ['git:', 'http:', 'git+ssh:', 'git+https:', 'ssh:', 'https:'], repo({ pathname, hash }) {
|
|
12
|
+
const [, user, project, type, committish] = pathname.split('/', 5)
|
|
13
|
+
return (!type || type === 'tree') && user && trim(project) ? [user, trim(project), type ? committish : hash.slice(1)] : null
|
|
14
|
+
} }],
|
|
15
|
+
['bitbucket', { label: 'Bitbucket', domain: 'bitbucket.org', protocols: ['git+ssh:', 'git+https:', 'ssh:', 'https:'], repo({ pathname, hash }) {
|
|
16
|
+
const [, user, project, aux] = pathname.split('/', 4)
|
|
17
|
+
return aux !== 'get' && user && trim(project) ? [user, trim(project), hash.slice(1)] : null
|
|
18
|
+
} }],
|
|
19
|
+
['gitlab', { label: 'GitLab', domain: 'gitlab.com', protocols: ['git+ssh:', 'git+https:', 'ssh:', 'https:'], repo({ pathname, hash }) {
|
|
20
|
+
const path = pathname.slice(1)
|
|
21
|
+
const at = path.lastIndexOf('/')
|
|
22
|
+
if (path.includes('/-/') || path.includes('/archive.tar.gz')) return null
|
|
23
|
+
return path.slice(0, Math.max(at, 0)) && trim(path.slice(at + 1)) ? [path.slice(0, at), trim(path.slice(at + 1)), hash.slice(1)] : null
|
|
24
|
+
} }],
|
|
25
|
+
['gist', { label: 'Gist', domain: 'gist.github.com', protocols: ['git:', 'git+ssh:', 'git+https:', 'ssh:', 'https:'], repo({ pathname, hash }) {
|
|
26
|
+
const [, user, project, aux] = pathname.split('/', 4)
|
|
27
|
+
return aux !== 'raw' && (user || project) ? [project ? user : null, trim(project || user), hash.slice(1)] : null
|
|
28
|
+
} }],
|
|
29
|
+
['sourcehut', { label: 'SourceHut', domain: 'git.sr.ht', protocols: ['git+ssh:', 'https:'], repo({ pathname, hash }) {
|
|
30
|
+
const [, user, project, aux] = pathname.split('/', 4)
|
|
31
|
+
return aux !== 'archive' && user && trim(project) ? [user, trim(project), hash.slice(1)] : null
|
|
32
|
+
} }],
|
|
33
|
+
])
|
|
34
|
+
const BY_DOMAIN = new Map([...GIT_HOSTS.values()].map(host => [host.domain, host]))
|
|
35
|
+
// The protocols hosted-git-info reads a URL by as it is, the hosts' names too.
|
|
36
|
+
const PROTOCOLS = new Set(['git+ssh:', 'ssh:', 'git+https:', 'git:', 'http:', 'https:', 'git+http:', ...[...GIT_HOSTS.keys()].map(name => `${name}:`)])
|
|
37
|
+
|
|
38
|
+
// GitHub's `owner/repo` shorthand, before a commit, branch or tag
|
|
39
|
+
// (`#v1.0.0`) or not, as hosted-git-info tells it: a slash, not first, nor
|
|
40
|
+
// last before any `#`; no `.` first; and no space, `@`, `:` or second slash
|
|
41
|
+
// before any `#`.
|
|
42
|
+
function githubShorthand(spec) {
|
|
43
|
+
const hash = spec.indexOf('#'), slash = spec.indexOf('/')
|
|
44
|
+
const beforeHash = at => at > -1 && !(hash > -1 && at > hash)
|
|
45
|
+
return slash > 0 && !spec.startsWith('.') && (hash > -1 ? spec[hash - 1] !== '/' : !spec.endsWith('/'))
|
|
46
|
+
&& ![/\s/u.exec(spec)?.index ?? -1, spec.indexOf('@'), spec.indexOf(':'), spec.indexOf('/', slash + 1)].some(beforeHash)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A URL as hosted-git-info parses one: a protocol it doesn't know put
|
|
50
|
+
// before a `git@host:path` (as `git+ssh://`) or given its `//`, and else
|
|
51
|
+
// `host:path` read as `host/path`.
|
|
52
|
+
const safeUrl = text => { try { return new URL(text) } catch { return null } }
|
|
53
|
+
function hostedUrl(spec) {
|
|
54
|
+
const at = spec.indexOf('@'), colon = spec.indexOf(':')
|
|
55
|
+
const corrected = PROTOCOLS.has(spec.slice(0, colon + 1)) || at > -1 && at < colon ? spec : at > -1 ? `git+ssh://${spec}`
|
|
56
|
+
: spec.indexOf('//') === colon + 1 ? spec : `${spec.slice(0, colon + 1)}//${spec.slice(colon + 1)}`
|
|
57
|
+
if (safeUrl(corrected)) return safeUrl(corrected)
|
|
58
|
+
const before = char => { const hash = corrected.indexOf('#'); return corrected.lastIndexOf(char, hash > -1 ? hash : Infinity) }
|
|
59
|
+
let scp = before(':') > before('@') ? `${corrected.slice(0, before(':'))}/${corrected.slice(before(':') + 1)}` : corrected
|
|
60
|
+
if (scp.lastIndexOf(':', scp.indexOf('#') > -1 ? scp.indexOf('#') : Infinity) === -1 && !scp.includes('//')) scp = `git+ssh://${scp}`
|
|
61
|
+
return safeUrl(scp)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The host npm takes a spec's repository on (hosted-git-info's fromUrl), or
|
|
65
|
+
// null: one by its name, whatever its path, or by its domain over a protocol
|
|
66
|
+
// it takes, where its path names a repository; none whose parts don't
|
|
67
|
+
// decode.
|
|
68
|
+
function gitHost(spec) {
|
|
69
|
+
if (!spec) return null
|
|
70
|
+
const url = hostedUrl(githubShorthand(spec) ? `github:${spec}` : spec)
|
|
71
|
+
if (!url) return null
|
|
72
|
+
const named = GIT_HOSTS.get(url.protocol.slice(0, -1))
|
|
73
|
+
const host = named ?? BY_DOMAIN.get(url.hostname.replace(/^www\./u, ''))
|
|
74
|
+
const parts = named ? [url.pathname.replace(/^\/?[^@]*@|^\//u, ''), url.hash.slice(1)] : host?.protocols.includes(url.protocol) && host.repo(url)
|
|
75
|
+
try { return parts ? (parts.forEach(part => part && decodeURIComponent(part)), host) : null } catch { return null }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Where npm installs a dependency's spec from, read once in
|
|
79
|
+
// npm-package-arg's resolve order: `path` for a path (`./x`, `~/x`, `/x`,
|
|
80
|
+
// `c:/x`, `file:`); `registry` for an `npm:` alias of a registry package,
|
|
81
|
+
// or a spec with no `/` or `:` that names no tarball, which no later step
|
|
82
|
+
// takes; a repository on a host npm knows (gitHost), named for it
|
|
83
|
+
// (`GitHub`); for a URL, `git` for a repository's (`git:`, `git+https:`,
|
|
84
|
+
// `git+ssh:`, `git+http:`, `git+rsync:`, `git+ftp:`, `git+file:`, and
|
|
85
|
+
// `git+ssh://` to `user@host:path`), `URL` for a tarball's (`https:`,
|
|
86
|
+
// `http:`), and null for one npm doesn't install (`workspace:`, `ssh:` on
|
|
87
|
+
// another host); then `path` again for one with a `/` or a tarball's name
|
|
88
|
+
// (`a/b/c`, `x.tgz`, `git@example.com:x/y`), and else `registry`: a
|
|
89
|
+
// version, a range or a tag.
|
|
90
|
+
function specFrom(spec) {
|
|
91
|
+
const value = String(spec ?? '').trim()
|
|
92
|
+
if (/^(?:[.]|~\/|\/|[a-z]:|file:)/iu.test(value)) return 'path'
|
|
93
|
+
if (/^npm:/iu.test(value) || !/[/:]|[.](?:tgz|tar\.gz|tar)$/iu.test(value)) return 'registry'
|
|
94
|
+
const host = gitHost(value)
|
|
95
|
+
if (host) return host.label
|
|
96
|
+
if (/^(?:git\+)?[a-z]+:/iu.test(value)) {
|
|
97
|
+
const scp = /^git\+ssh:\/\/([^:#]+:[^#]+(?:\.git)?)(?:#.*)?$/iu.exec(value)
|
|
98
|
+
if (value.startsWith('git+ssh:') && scp && !/:\d+(?:\/|$)/u.test(scp[1])) return 'git'
|
|
99
|
+
const protocol = safeUrl(value)?.protocol
|
|
100
|
+
return /^(?:git|git\+(?:https?|rsync|ftp|file|ssh)):$/u.test(protocol) ? 'git' : /^https?:$/u.test(protocol) ? 'URL' : null
|
|
101
|
+
}
|
|
102
|
+
return /\/|[.](?:tgz|tar\.gz|tar)$/iu.test(value) ? 'path' : 'registry'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Where a dependency's spec has npm fetch it from, where that isn't its
|
|
106
|
+
// registry (specFrom): a host's name (`GitHub`), `git` or `URL`. Null for
|
|
107
|
+
// the registry's ranges and tags, a path, and what npm doesn't install.
|
|
108
|
+
export function npmDependencySource(spec) {
|
|
109
|
+
const from = specFrom(spec)
|
|
110
|
+
return from === 'registry' || from === 'path' ? null : from
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The package and spec an `npm:` alias names (`npm:pkg@^1.2.3`), as
|
|
114
|
+
// specFrom reads one; null for a spec that isn't one.
|
|
115
|
+
export function npmAlias(spec) {
|
|
116
|
+
const [, name, range = ''] = /^npm:((?:@[^/@]+\/)?[^/@]+)(?:@(.*))?$/iu.exec(String(spec ?? '').trim()) ?? []
|
|
117
|
+
return name ? { name, spec: range } : null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Whether npm installs a dependency's spec from its registry (specFrom):
|
|
121
|
+
// not from a path, a repository or a URL, nor a scheme npm doesn't install
|
|
122
|
+
// (`workspace:`), the registry's package of that name being what npm
|
|
123
|
+
// installs for none of those.
|
|
124
|
+
export const npmRegistrySpec = spec => specFrom(spec) === 'registry'
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// A package's files from its tarball, as npm installs them: gunzipped, read as
|
|
2
|
+
// a tar archive (ustar, pax and GNU long names), each file by the path npm
|
|
3
|
+
// installs it at, its first component dropped, none escaping the package; a
|
|
4
|
+
// text where it is UTF-8 with no NUL, else null. Held to the npm viewer's
|
|
5
|
+
// bounds (server-managed/npm-packages.ts readNpmTar).
|
|
6
|
+
const MAX_PACKAGE_FILES = 20_000
|
|
7
|
+
export const MAX_PACKAGE_BYTES = 64 * 1024 * 1024
|
|
8
|
+
// What its tar stream may unpack to, its headers and padding beside its
|
|
9
|
+
// files, as npm-loads.ts bounds it (MAX_TAR_BYTES).
|
|
10
|
+
export const MAX_TAR_BYTES = 96 * 1024 * 1024
|
|
11
|
+
|
|
12
|
+
const latin1 = new TextDecoder('latin1')
|
|
13
|
+
const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true })
|
|
14
|
+
const utf8Lenient = bytes => new TextDecoder().decode(bytes)
|
|
15
|
+
// A header's text field, to its first NUL.
|
|
16
|
+
const textOf = (bytes, start, length) => {
|
|
17
|
+
const field = bytes.subarray(start, start + length)
|
|
18
|
+
const end = field.indexOf(0)
|
|
19
|
+
return utf8Lenient(end === -1 ? field : field.subarray(0, end))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A header's size field: octal, or GNU base-256 past its 11 digits.
|
|
23
|
+
function sizeOf(bytes, offset) {
|
|
24
|
+
if (bytes[offset] & 0x80) {
|
|
25
|
+
let size = bytes[offset] & 0x7f
|
|
26
|
+
for (let i = 1; i < 12; i++) size = size * 256 + bytes[offset + i]
|
|
27
|
+
return size
|
|
28
|
+
}
|
|
29
|
+
const text = textOf(bytes, offset, 12).trim()
|
|
30
|
+
return /^[0-7]+$/u.test(text) ? Number.parseInt(text, 8) : Number.NaN
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A pax header's path: records `<length> <key>=<value>\n`, their lengths in
|
|
34
|
+
// bytes.
|
|
35
|
+
function paxPath(data) {
|
|
36
|
+
let at = 0, path = null
|
|
37
|
+
while (at < data.length) {
|
|
38
|
+
const space = data.indexOf(0x20, at)
|
|
39
|
+
const length = Number.parseInt(latin1.decode(data.subarray(at, space)), 10)
|
|
40
|
+
if (space === -1 || !Number.isSafeInteger(length) || length <= 0) break
|
|
41
|
+
const record = utf8Lenient(data.subarray(space + 1, at + length - 1))
|
|
42
|
+
if (record.startsWith('path=')) path = record.slice(5)
|
|
43
|
+
at += length
|
|
44
|
+
}
|
|
45
|
+
return path
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const packagePath = raw => {
|
|
49
|
+
const parts = raw.split('/').slice(1).filter(part => part !== '' && part !== '.')
|
|
50
|
+
return parts.length === 0 || parts.includes('..') ? null : parts.join('/')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The files of a tar archive, by path: their bytes.
|
|
54
|
+
function npmTarFiles(tar) {
|
|
55
|
+
const files = new Map()
|
|
56
|
+
let longName = null, offset = 0, total = 0
|
|
57
|
+
while (offset + 512 <= tar.length) {
|
|
58
|
+
const header = tar.subarray(offset, offset + 512)
|
|
59
|
+
if (header.every(byte => byte === 0)) break
|
|
60
|
+
const size = sizeOf(header, 124), start = offset + 512, type = String.fromCodePoint(header[156])
|
|
61
|
+
if (!Number.isSafeInteger(size) || size < 0 || start + size > tar.length) throw new Error('bad-tarball')
|
|
62
|
+
const data = tar.subarray(start, start + size)
|
|
63
|
+
offset = start + Math.ceil(size / 512) * 512
|
|
64
|
+
if (type === 'x' || type === 'L') { longName = type === 'x' ? paxPath(data) : textOf(data, 0, data.length); continue }
|
|
65
|
+
if (type === 'g' || type === 'K') continue
|
|
66
|
+
const prefix = textOf(header, 257, 6) === 'ustar' ? textOf(header, 345, 155) : ''
|
|
67
|
+
const name = longName ?? (prefix ? `${prefix}/${textOf(header, 0, 100)}` : textOf(header, 0, 100))
|
|
68
|
+
longName = null
|
|
69
|
+
const path = ['0', '\0', '7'].includes(type) ? packagePath(name) : null
|
|
70
|
+
if (path === null) continue
|
|
71
|
+
total += size - (files.get(path)?.length ?? 0)
|
|
72
|
+
files.set(path, data)
|
|
73
|
+
if (files.size > MAX_PACKAGE_FILES || total > MAX_PACKAGE_BYTES) throw new Error('package-too-large')
|
|
74
|
+
}
|
|
75
|
+
return files
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A file's text: UTF-8 with no NUL, else null.
|
|
79
|
+
export function npmFileText(bytes) {
|
|
80
|
+
if (bytes.includes(0)) return null
|
|
81
|
+
try { return utf8.decode(bytes) } catch { return null }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A gzipped tarball's tar stream, given up on as `package-too-large` once it
|
|
85
|
+
// unpacks past MAX_TAR_BYTES, before any more of it is held.
|
|
86
|
+
async function gunzip(gzipped) {
|
|
87
|
+
const reader = new Blob([gzipped]).stream().pipeThrough(new DecompressionStream('gzip')).getReader()
|
|
88
|
+
const chunks = []
|
|
89
|
+
let size = 0
|
|
90
|
+
for (let read = await reader.read(); !read.done; read = await reader.read()) {
|
|
91
|
+
size += read.value.byteLength
|
|
92
|
+
if (size > MAX_TAR_BYTES) { await reader.cancel(); throw new Error('package-too-large') }
|
|
93
|
+
chunks.push(read.value)
|
|
94
|
+
}
|
|
95
|
+
return new Uint8Array(await new Blob(chunks).arrayBuffer())
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A file's bytes' digest, as the managed server gives one of a file that
|
|
99
|
+
// isn't text (server-managed/npm-packages.ts).
|
|
100
|
+
const sha256 = async bytes => `sha256-${btoa(String.fromCodePoint(...new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))))}`
|
|
101
|
+
|
|
102
|
+
// A gzipped tarball's files as a version holds them for risk, as the npm
|
|
103
|
+
// viewer holds a version (ui/view/npm-package.js npmPackageDetails), a source
|
|
104
|
+
// map listing them (`kind: 'sourcemap'`): `json.sources` their paths, sorted,
|
|
105
|
+
// `json.sourcesContent` their texts, `fileSizes` by path, and `npmBinaries`,
|
|
106
|
+
// those that aren't text, `{ size, digest }` by path, which compare.js and
|
|
107
|
+
// diff.js tell them by.
|
|
108
|
+
export async function npmTarballVersion(gzipped) {
|
|
109
|
+
const files = [...npmTarFiles(await gunzip(gzipped))].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
110
|
+
const texts = files.map(([, bytes]) => npmFileText(bytes))
|
|
111
|
+
const binaries = await Promise.all(files.filter((_, at) => texts[at] === null).map(async ([path, bytes]) => [path, { size: bytes.length, digest: await sha256(bytes) }]))
|
|
112
|
+
return { kind: 'sourcemap', json: { version: 3, sources: files.map(([path]) => path), sourcesContent: texts }, fileSizes: new Map(files.map(([path, bytes]) => [path, bytes.length])), npmBinaries: new Map(binaries) }
|
|
113
|
+
}
|
package/server-e2e/http.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const WS_UPGRADE_PATH = '/api/sync'
|
|
|
26
26
|
// here commits + broadcasts WITHOUT taking over the client's SSE event-stream
|
|
27
27
|
// (each in-band POST forces a stream takeover; see sse-server.ts). Sibling of
|
|
28
28
|
// the WS upgrade path so one nginx `/api/*` block routes both.
|
|
29
|
-
|
|
29
|
+
const SAVE_REST_PATH = '/api/sync/save'
|
|
30
30
|
function isUpgradePath(url: string | undefined): boolean {
|
|
31
31
|
if (typeof url !== 'string') return false
|
|
32
32
|
// Strip `?…` so clients can carry build / debug tags. Exact match
|
|
@@ -174,12 +174,12 @@ export function createHttpServer(deps: HttpServerDeps): Server {
|
|
|
174
174
|
// REST handlers go through a separate path and must mirror it.
|
|
175
175
|
// Without this, a REST PUT arriving on an existing keep-alive
|
|
176
176
|
// socket AFTER SIGTERM but BEFORE `httpServer.close()` finishes
|
|
177
|
-
// draining could
|
|
178
|
-
//
|
|
179
|
-
// `
|
|
180
|
-
//
|
|
181
|
-
// the client to retry against a different replica.
|
|
182
|
-
// audit + multi-replica shutdown ordering review.
|
|
177
|
+
// draining could start a fresh upload + commit for the drain to
|
|
178
|
+
// outwait — or, landing after the drain's in-flight snapshot
|
|
179
|
+
// (lifecycle.ts `drain`), run on against the closed DB, the race
|
|
180
|
+
// ws-server.ts gates on the WS plane. The 503 + `shutting-down`
|
|
181
|
+
// reason tells the client to retry against a different replica.
|
|
182
|
+
// Transport audit + multi-replica shutdown ordering review.
|
|
183
183
|
if (isShuttingDown()) {
|
|
184
184
|
res.writeHead(503, { 'content-type': 'application/json', 'connection': 'close' })
|
|
185
185
|
res.end(JSON.stringify({ error: 'shutting-down' }))
|
package/server-e2e/npm-proxy.ts
CHANGED
|
@@ -141,7 +141,7 @@ async function readRequestBody(req: IncomingMessage): Promise<Buffer | null> {
|
|
|
141
141
|
return Buffer.concat(chunks)
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
async function handleNpmAdvisories(
|
|
145
145
|
deps: NpmProxyDeps,
|
|
146
146
|
req: IncomingMessage,
|
|
147
147
|
res: ServerResponse,
|
|
@@ -35,8 +35,8 @@ export type FetchMintGuard = {
|
|
|
35
35
|
size: () => number
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const DEFAULT_FETCH_MINT_WINDOW_MS = 60_000
|
|
39
|
+
const DEFAULT_FETCH_MINT_MAX_ENTRIES = 50_000
|
|
40
40
|
|
|
41
41
|
export function createFetchMintGuard(
|
|
42
42
|
{ windowMs = DEFAULT_FETCH_MINT_WINDOW_MS, maxEntries = DEFAULT_FETCH_MINT_MAX_ENTRIES }:
|
|
@@ -229,7 +229,7 @@ async function handleFetch(deps: ObjstoreDeps, socket: WebSocket, msg: ObjstoreF
|
|
|
229
229
|
// the reaper's TTL pass within `STAGING_TTL_MS_DEFAULT` — no per-
|
|
230
230
|
// socket bookkeeping is needed on disconnect today. If that ever
|
|
231
231
|
// changes, wire a `cleanupSocket(socket)` into both this bundle and
|
|
232
|
-
// server-e2e/
|
|
232
|
+
// server-e2e/ws-server.ts's per-socket close handler.
|
|
233
233
|
|
|
234
234
|
export type ObjstoreHandlers = {
|
|
235
235
|
handlePutBegin: (s: WebSocket, m: ObjstorePutBeginMsg) => Promise<void>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
25
25
|
import { Buffer } from 'node:buffer'
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000
|
|
28
28
|
|
|
29
29
|
export type PutTokenPayload = {
|
|
30
30
|
op: 'put'
|
package/server-e2e/sign.ts
CHANGED
|
@@ -157,7 +157,7 @@ export async function verifyEd25519(
|
|
|
157
157
|
// `{ ok: false, canonical: null }` on bad shape / bad sig;
|
|
158
158
|
// `{ ok: true, canonical: <bytes> }` on success.
|
|
159
159
|
//
|
|
160
|
-
// NOT used by `server-e2e/
|
|
160
|
+
// NOT used by `server-e2e/sync-handlers.ts handleSave`. Production composes
|
|
161
161
|
// the smaller helpers (`canonicalSave`, `computeRevisionIdFromCanonical`,
|
|
162
162
|
// `verifyEd25519`) separately so the dup-precheck via
|
|
163
163
|
// `revisionExists` can fire BEFORE Ed25519-verify and skip the
|
|
@@ -1,23 +1,70 @@
|
|
|
1
1
|
import { type Advisory, type CacheStore, type Package, advisories } from '@preventive/upstream/advisories.js'
|
|
2
2
|
import { type Client, HttpError, createClient } from '@preventive/upstream/github.js'
|
|
3
|
+
import { isNpmPackageName, npmPackageScope } from '../common/managed/npm-packages.js'
|
|
4
|
+
import { NPM_REGISTRY, askUpstream } from './npm-packages.ts'
|
|
3
5
|
|
|
4
6
|
export const ADVISORIES_TIMEOUT_MS = 30_000
|
|
7
|
+
const PUBLIC_AT_ONCE = 16
|
|
8
|
+
const order = (a: string, b: string) => Number(a > b) - Number(a < b)
|
|
9
|
+
|
|
10
|
+
// Whether anyone may read a package, so that its name may go to a service
|
|
11
|
+
// other than npm (OSV): an unscoped one always, npm's private packages
|
|
12
|
+
// always being scoped; a scoped one where npm answers it anonymously, asked
|
|
13
|
+
// by a HEAD, which reads nothing of it. One npm can't be asked of is taken
|
|
14
|
+
// as private, and not kept; the rest are, for PUBLIC_MS, PUBLIC_NAMES at
|
|
15
|
+
// most, as a package may be made private, or public. A name npm can't have
|
|
16
|
+
// is asked of no one, upstream refusing it.
|
|
17
|
+
const PUBLIC_MS = 60 * 60_000
|
|
18
|
+
const PUBLIC_NAMES = 10_000
|
|
19
|
+
const publicNames = new Map<string, { at: number; open: boolean }>()
|
|
20
|
+
async function isPublicNpmPackage(name: string, signal: AbortSignal): Promise<boolean> {
|
|
21
|
+
if (!isNpmPackageName(name)) return false
|
|
22
|
+
if (npmPackageScope(name) === null) return true
|
|
23
|
+
const kept = publicNames.get(name)
|
|
24
|
+
if (kept && Date.now() - kept.at < PUBLIC_MS) return kept.open
|
|
25
|
+
let open
|
|
26
|
+
try { open = await askUpstream(`${NPM_REGISTRY}/${name}`, { method: 'HEAD', limit: Number.MAX_SAFE_INTEGER, signal, absent: [401, 403, 404] }) !== null } catch { return false }
|
|
27
|
+
publicNames.delete(name)
|
|
28
|
+
publicNames.set(name, { at: Date.now(), open })
|
|
29
|
+
while (publicNames.size > PUBLIC_NAMES) publicNames.delete(publicNames.keys().next().value!)
|
|
30
|
+
return open
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The npm packages anyone may read (isPublicNpmPackage), a few asked at a
|
|
34
|
+
// time, within PUBLIC_BUDGET_MS, a third of the audit's: a scoped one npm
|
|
35
|
+
// hasn't told of by then is taken as private, asked of npm alone, for the
|
|
36
|
+
// audit to keep the rest of its time.
|
|
37
|
+
const PUBLIC_BUDGET_MS = 10_000
|
|
38
|
+
async function publicNpmNames(packages: Package[], signal: AbortSignal): Promise<Set<string>> {
|
|
39
|
+
const names = [...new Set(packages.filter(pkg => pkg.ecosystem === 'npm').map(pkg => pkg.name))]
|
|
40
|
+
const open = new Set<string>()
|
|
41
|
+
const budget = new AbortController()
|
|
42
|
+
const timer = setTimeout(() => { budget.abort(new Error('public-check-budget')) }, PUBLIC_BUDGET_MS)
|
|
43
|
+
const asking = AbortSignal.any([signal, budget.signal])
|
|
44
|
+
try {
|
|
45
|
+
for (let at = 0; at < names.length; at += PUBLIC_AT_ONCE) {
|
|
46
|
+
const chunk = names.slice(at, at + PUBLIC_AT_ONCE)
|
|
47
|
+
const checked = await Promise.all(chunk.map(name => isPublicNpmPackage(name, asking)))
|
|
48
|
+
for (const [i, name] of chunk.entries()) if (checked[i]) open.add(name)
|
|
49
|
+
}
|
|
50
|
+
} finally { clearTimeout(timer) }
|
|
51
|
+
return open
|
|
52
|
+
}
|
|
5
53
|
|
|
6
54
|
function advisoryGithubClient(token: string | null, signal: AbortSignal): Client {
|
|
7
55
|
const authenticated = createClient({ token, userAgent: 'deepview-triage' })
|
|
8
|
-
if (token === null) return authenticated
|
|
9
56
|
let client = authenticated
|
|
10
57
|
return {
|
|
11
58
|
...authenticated,
|
|
12
59
|
async listRepoAdvisories(options) {
|
|
13
60
|
const current = client
|
|
14
61
|
try { return await current.listRepoAdvisories(options) } catch (error) {
|
|
15
|
-
if (current !== authenticated || !(error instanceof HttpError) || error.status !== 401) throw error
|
|
62
|
+
if (token === null || current !== authenticated || !(error instanceof HttpError) || error.status !== 401) throw error
|
|
16
63
|
signal.throwIfAborted()
|
|
17
64
|
// Published advisories remain readable after a token is revoked. Reuse
|
|
18
65
|
// anonymous access for subsequent repositories in this audit as well.
|
|
19
66
|
if (client === authenticated) client = createClient({ token: null, userAgent: 'deepview-triage' })
|
|
20
|
-
return client.listRepoAdvisories(options)
|
|
67
|
+
return await client.listRepoAdvisories(options)
|
|
21
68
|
}
|
|
22
69
|
},
|
|
23
70
|
}
|
|
@@ -25,11 +72,15 @@ function advisoryGithubClient(token: string | null, signal: AbortSignal): Client
|
|
|
25
72
|
|
|
26
73
|
// The managed caller supplies its user's token when available; public shares
|
|
27
74
|
// remain anonymous. Upstream sends this credential only to GitHub and returns
|
|
28
|
-
// published advisories, matching versions across npm, OSV and repository rows
|
|
29
|
-
//
|
|
75
|
+
// published advisories, matching versions across npm, OSV and repository rows:
|
|
76
|
+
// OSV's for npm packages anyone may read too, the malicious packages' records
|
|
77
|
+
// among them, which npm's registry hasn't.
|
|
78
|
+
// `cache` keeps each repository's listing (upstream-cache.ts) across audits;
|
|
79
|
+
// `github`, a client of the caller's, asks GitHub in place of one with
|
|
80
|
+
// `githubToken`.
|
|
30
81
|
export async function fetchBundleAdvisories(packages: Package[], signal: AbortSignal,
|
|
31
|
-
{ debug = false, repoAdvisories = false, details = false, githubToken = null, cache }: {
|
|
32
|
-
debug?: boolean; repoAdvisories?: boolean; details?: boolean; githubToken?: string | null; cache?: CacheStore | undefined
|
|
82
|
+
{ debug = false, repoAdvisories = false, details = false, githubToken = null, github, cache }: {
|
|
83
|
+
debug?: boolean; repoAdvisories?: boolean; details?: boolean; githubToken?: string | null; github?: Client; cache?: CacheStore | undefined
|
|
33
84
|
} = {}): Promise<
|
|
34
85
|
{ status: 200; body: Advisory[] } | { status: 502; body: { error: string } }
|
|
35
86
|
> {
|
|
@@ -43,12 +94,18 @@ export async function fetchBundleAdvisories(packages: Package[], signal: AbortSi
|
|
|
43
94
|
onAbort = () => reject(signal.reason)
|
|
44
95
|
signal.addEventListener('abort', onAbort, { once: true })
|
|
45
96
|
})
|
|
97
|
+
// npm packages anyone may read are asked of OSV too (upstream's `osv`),
|
|
98
|
+
// for the malicious packages' records npm's registry hasn't; a private
|
|
99
|
+
// one's name never leaves for OSV, as it never does for Socket. A package
|
|
100
|
+
// is wholly in one ask or the other, so upstream's order holds.
|
|
101
|
+
const open = await Promise.race([publicNpmNames(packages, signal), deadline])
|
|
102
|
+
const toOsv = (pkg: Package) => pkg.ecosystem === 'npm' && open.has(pkg.name)
|
|
46
103
|
// Kept in the caller's store, or without one in upstream's disk cache,
|
|
47
104
|
// where the server set one (auditCache).
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
])
|
|
51
|
-
return { status: 200, body: result }
|
|
105
|
+
const options = { github: github ?? advisoryGithubClient(githubToken, signal), repoAdvisories, details, ...(cache && { cache }) }
|
|
106
|
+
const asked = [{ part: packages.filter(toOsv), osv: true }, { part: packages.filter(pkg => !toOsv(pkg)), osv: false }]
|
|
107
|
+
const result = await Promise.race([Promise.all(asked.map(({ part, osv }) => part.length === 0 ? Promise.resolve([]) : advisories(part, { ...options, osv }))), deadline])
|
|
108
|
+
return { status: 200, body: result.flat().toSorted((a, b) => order(a.ecosystem, b.ecosystem) || order(a.name, b.name)) }
|
|
52
109
|
} catch (error) {
|
|
53
110
|
if (debug) console.warn('managed: advisories failed:', error)
|
|
54
111
|
return { status: 502, body: { error: 'upstream-unavailable' } }
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import { Buffer } from 'node:buffer'
|
|
4
4
|
import { brotliDecompress } from 'node:zlib'
|
|
5
5
|
import { promisify } from 'node:util'
|
|
6
|
-
import {
|
|
6
|
+
import { bundleEdges } from '@preventive/sourcemap/edges.js'
|
|
7
|
+
import { BUNDLE_METADATA_VERSION, type BundleDetails, createBundleMetadata, createBundleSummary, parseBundleContents, parseBundleMetadata } from '../common/bundle-metadata.js'
|
|
7
8
|
import { bundleReasons } from '../common/bundle-reasons.js'
|
|
9
|
+
import { sourcemapEdges } from '../common/bundle-sourcemap.js'
|
|
8
10
|
import { type BundleAdvisoryInventory, bundleAdvisoryInventory } from './bundle-advisory-inventory.ts'
|
|
9
11
|
import { decodeUtf8 } from '../common/utf8.js'
|
|
10
12
|
import type { OpenedBlob } from './blob-store.ts'
|
|
@@ -19,7 +21,7 @@ const MAX_DECODED_BYTES = 512 * 1024 * 1024
|
|
|
19
21
|
export const MAX_PACKAGE_INVENTORY_BYTES = 1024 * 1024
|
|
20
22
|
export type BundleCachePart = 'metadata' | 'contents'
|
|
21
23
|
export type BundleCacheRecord = Pick<ManagedBundle, 'id' | 'integrity' | 'filename' | 'kind' | 'byteSize'>
|
|
22
|
-
export interface BundleSummary { files: number; codeFiles: number; lines: number; commit?: string }
|
|
24
|
+
export interface BundleSummary { files: number; codeFiles: number; lines: number; commit?: string; stasisVersion?: number; versionedPackages?: number }
|
|
23
25
|
|
|
24
26
|
export async function readBundleDetails(record: BundleCacheRecord, store: BundleStore) {
|
|
25
27
|
const bytes = await store.get(record.id, record.kind)
|
|
@@ -76,15 +78,36 @@ function encodePackageInventory(details: BundleDetails): Buffer {
|
|
|
76
78
|
return Buffer.from(parts.join(''))
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
// What a catalog sends of a bundle before it opens. A Stasis bundle's
|
|
82
|
+
// `versionedPackages` counts the name@version pairs its unscoped advisory
|
|
83
|
+
// audit covers, audited or skipped: with none, Advisories has nothing to show.
|
|
84
|
+
export function bundleSummary(details: BundleDetails, metadata?: Parameters<typeof createBundleSummary>[1]): BundleSummary {
|
|
85
|
+
const summary = createBundleSummary(details, metadata)
|
|
86
|
+
if (details.kind !== 'stasis') return summary
|
|
87
|
+
const { packages, skipped } = bundleAdvisoryInventory(details)
|
|
88
|
+
return { ...summary, versionedPackages: packages.reduce((count, pkg) => count + pkg.versions.length, 0) + skipped.length }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A sourcemap's edges, read with the parser (edges.js) that the client goes
|
|
92
|
+
// without: every map's, where the client reads Metro's alone. Without it,
|
|
93
|
+
// the metadata keeps what the client would read. A map with no
|
|
94
|
+
// sourcesContent has no source to read them from.
|
|
95
|
+
function readSourcemapEdges(details: BundleDetails) {
|
|
96
|
+
if (details.kind !== 'sourcemap' || !details.map?.files.some(file => file.content !== null)) return
|
|
97
|
+
try { details.edges = sourcemapEdges(details.map, bundleEdges) }
|
|
98
|
+
catch (err) { console.warn('managed: sourcemap edges failed:', err) }
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
async function build(record: BundleCacheRecord, storage: BundleCacheStorage, db: ManagedDb, store: BundleStore) {
|
|
80
102
|
const details = await readBundleDetails(record, store)
|
|
81
103
|
if (!details) throw new Error('Bundle bytes unavailable')
|
|
104
|
+
readSourcemapEdges(details)
|
|
82
105
|
const metadata = { ...await createBundleMetadata(details), id: record.id, filename: record.filename }
|
|
83
106
|
const body = await encodeBrotli(Buffer.from(JSON.stringify(metadata)))
|
|
84
107
|
if (!(await db.getBundle(record.id))) throw new Error('Bundle deleted')
|
|
85
108
|
await storage.put(record.id, filename, body)
|
|
86
109
|
if (record.kind === 'stasis') await storage.put(record.id, packagesFilename, encodePackageInventory(details))
|
|
87
|
-
const summary =
|
|
110
|
+
const summary = bundleSummary(details, metadata)
|
|
88
111
|
await storage.put(record.id, SUMMARY_FILENAME, Buffer.from(JSON.stringify(summary)))
|
|
89
112
|
// A different instance may have deleted the row while these writes ran.
|
|
90
113
|
// Reconcile after publishing so its cleanup cannot be undone by us.
|
|
@@ -102,7 +125,7 @@ export function createBundleCache(storage: BundleCacheStorage, db: ManagedDb, st
|
|
|
102
125
|
const summaries = createBundleSummaryCache(storage, async record => {
|
|
103
126
|
const details = await readBundleDetails(record, store)
|
|
104
127
|
if (!details) throw new Error('Bundle bytes unavailable')
|
|
105
|
-
return
|
|
128
|
+
return bundleSummary(details)
|
|
106
129
|
}, id => db.getBundle(id))
|
|
107
130
|
let queue = Promise.resolve()
|
|
108
131
|
async function ensure(record: BundleCacheRecord): Promise<void> {
|
|
@@ -137,6 +160,16 @@ export function createBundleCache(storage: BundleCacheStorage, db: ManagedDb, st
|
|
|
137
160
|
}
|
|
138
161
|
return openCached(record, filename)
|
|
139
162
|
},
|
|
163
|
+
// A sourcemap's edges as its metadata keeps them, read with the parser
|
|
164
|
+
// once, for a derivative that reads the bundle itself (report previews).
|
|
165
|
+
async sourcemapEdges(record: ManagedBundle): Promise<BundleDetails['edges']> {
|
|
166
|
+
const cached = await openCached(record, filename)
|
|
167
|
+
const chunks: Buffer[] = []
|
|
168
|
+
try { for await (const chunk of cached.stream) chunks.push(Buffer.from(chunk)) }
|
|
169
|
+
finally { cached.stream.destroy() }
|
|
170
|
+
const decoded = await decompress(Buffer.concat(chunks), { maxOutputLength: MAX_DECODED_BYTES })
|
|
171
|
+
return parseBundleMetadata(JSON.parse(decodeUtf8(decoded)), record.integrity).edges
|
|
172
|
+
},
|
|
140
173
|
async advisoryInventory(record: ManagedBundle, reason = ''): Promise<BundleAdvisoryInventory | null | undefined> {
|
|
141
174
|
if (record.kind !== 'stasis') return { packages: [], skipped: [] }
|
|
142
175
|
const cached = await openCached(record, packagesFilename)
|