@preventive/triage 1.0.0-alpha.45 → 1.0.0-alpha.46
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/client/linked-findings.js +3 -0
- package/common/bundle-metadata.d.ts +1 -0
- package/common/bundle-metadata.js +1 -1
- package/common/bundle-sources.js +5 -1
- package/common/line-diff.js +5 -4
- package/out/{chunk-NPW52BM2.js → chunk-4HI2MSAV.js} +13 -13
- package/out/{chunk-AFLZH7WG.js → chunk-4UFAMJAX.js} +1 -1
- package/out/{chunk-HMAJDVXN.js → chunk-7XRR4OFU.js} +1 -1
- package/out/{chunk-Q6MVYGYV.js → chunk-BOIXMUU5.js} +1 -1
- package/out/{chunk-XVBEOGOS.js → chunk-CC4OM6TZ.js} +2 -2
- package/out/chunk-G24A42V6.js +2 -0
- package/out/{chunk-WI7Z5A2T.js → chunk-JKGQ3O7O.js} +1 -1
- package/out/chunk-LE5ZWLG4.js +235 -0
- package/out/chunk-MSION3G4.js +36 -0
- package/out/{chunk-6HNXNIQY.js → chunk-S6GIXWQW.js} +1 -1
- package/out/chunk-V3KWNNMY.js +1 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +25 -25
- package/out/compare.js +13 -13
- package/out/dialogs.js +40 -40
- package/out/graph.js +1 -1
- package/out/index.html +6 -6
- package/out/scan.js +2 -2
- package/out/terminal.js +1 -1
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +3 -1
- package/risk/actions/findings.js +14 -9
- package/risk/actions/scripts/shell.js +5 -1
- package/risk/index.d.ts +5 -2
- package/server-e2e/app.ts +5 -4
- package/server-e2e/http.ts +5 -2
- package/server-managed/bundle-cache.ts +7 -1
- package/server-managed/combined.ts +3 -2
- package/server-managed/commit-lockfiles.ts +21 -11
- package/server-managed/config.ts +6 -0
- package/server-managed/db-methods.ts +37 -9
- package/server-managed/db-neon.ts +61 -21
- package/server-managed/db-schema.ts +4 -0
- package/server-managed/db.ts +2 -1
- package/server-managed/dependency-reviews.ts +13 -3
- package/server-managed/github-app.ts +12 -6
- package/server-managed/github-issue-links.ts +11 -5
- package/server-managed/github-pulls.ts +2 -2
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/http-response.ts +10 -2
- package/server-managed/http.ts +195 -111
- package/server-managed/index.ts +8 -7
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/npm-delta.ts +11 -6
- package/server-managed/npm-kept.ts +32 -2
- package/server-managed/npm-loads.ts +100 -30
- package/server-managed/npm-packages.ts +89 -3
- package/server-managed/npm-risk.ts +4 -1
- package/server-managed/pretty-print.ts +67 -23
- package/server-managed/pull-dependencies.ts +29 -9
- package/server-managed/pull-dependency-reviews.ts +8 -8
- package/server-managed/queue.ts +16 -0
- package/server-managed/rename-diff.ts +22 -13
- package/server-managed/report-sources.ts +1 -1
- package/server-managed/report-store.ts +42 -9
- package/server-managed/repository-actions.ts +15 -6
- package/server-managed/repository-browser.ts +119 -31
- package/server-managed/repository-dependency-reviews.ts +41 -19
- package/server-managed/repository-history.ts +24 -8
- package/server-managed/repository-pulse.ts +8 -1
- package/server-managed/repository-risk-audit.ts +13 -0
- package/server-managed/repository-risk-read.ts +11 -6
- package/server-managed/repository-risk.ts +27 -20
- package/server-managed/session.ts +18 -4
- package/server-managed/sql.ts +5 -2
- package/server-managed/storage.ts +4 -2
- package/server-managed/team-reports.ts +24 -12
- package/server-managed/upstream-cache.ts +12 -0
- package/server.ts +14 -5
- package/out/chunk-5DHL5NG3.js +0 -2
- package/out/chunk-NV7ES4EW.js +0 -1
- package/out/chunk-PTGDYLNS.js +0 -36
- package/out/chunk-QRD6GIG5.js +0 -229
package/server-managed/http.ts
CHANGED
|
@@ -51,9 +51,10 @@
|
|
|
51
51
|
// description's Markdown | 400/401/403/404/409/429/502
|
|
52
52
|
// GET /api/admin/repositories/pull-dependencies?repoId=&number= → the package versions a pull request's npm, pnpm and
|
|
53
53
|
// Yarn 1 lockfiles lock that they didn't at its merge base, an update's with those it was, by directory; with review=1, each's
|
|
54
|
-
// review, where its repository reviews dependencies
|
|
54
|
+
// review, where its repository reviews dependencies; with numbers=1,2,3 (twenty at most) in place of number, those of each,
|
|
55
|
+
// { pulls }, one GitHub fails told { number, error } | 400/401/403/404/409/429/502
|
|
55
56
|
// GET /api/admin/repositories/branch-dependencies?repoId= → the versions the default branch's lockfiles lock that want review, where
|
|
56
|
-
// its review is in effect for the reader | 400/401/403/404/409/429/502
|
|
57
|
+
// its review is in effect for the reader, GitHub not asked where it reviews none | 400/401/403/404/409/429/502
|
|
57
58
|
// GET /api/admin/repositories/lockfiles?repoId= → the lockfiles of the default branch its dependency review reads, by their
|
|
58
59
|
// paths, for its settings to name | 400/401/403/404/409/429/502
|
|
59
60
|
// GET /api/admin/repositories/pulse?repoId=&weeks=1|4|13|52 → the default branch's commits and lines a week and a day over
|
|
@@ -134,7 +135,7 @@ import { mergeLinkGroups } from './link-reports.ts'
|
|
|
134
135
|
import { backfillFindingIds, reportEntries } from '@preventive/report'
|
|
135
136
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
136
137
|
import { pipeline } from 'node:stream/promises'
|
|
137
|
-
import { REPOSITORY_READ_KEPT_MS, historyChangeStore, historyPageStore, historySquashStore, historyTreeStore, npmDeltaStore, npmVersionStore, pullDependencyStore,
|
|
138
|
+
import { REPOSITORY_READ_KEPT_MS, historyChangeStore, historyPageStore, historySquashStore, historyTreeStore, npmDeltaStore, npmPublishTimeStore, npmVersionStore, pullDependencyStore,
|
|
138
139
|
repositoryReadStore, reviewBaselineStore, riskLineStore, scanReadStore } from './npm-kept.ts'
|
|
139
140
|
import { type NpmDelta, npmDelta } from './npm-delta.ts'
|
|
140
141
|
import { ADVISORIES_TIMEOUT_MS, fetchBundleAdvisories } from './bundle-advisories.ts'
|
|
@@ -181,7 +182,7 @@ import { WORKFLOWS_DIR } from './repository-actions.ts'
|
|
|
181
182
|
import { githubActionsSource } from './github-actions.ts'
|
|
182
183
|
import { type RepositoryEntry, createRepositoryBrowser, repositoryTreeFiles, scopedDirectory } from './repository-browser.ts'
|
|
183
184
|
import { CALLBACK_PATH, LOGIN_PATH, OAuthError, buildLoginRedirect, cacheAvatar, ensureUserAccessToken, handleCallback } from './github-oauth.ts'
|
|
184
|
-
import { clearCookie, endSession, readSession } from './session.ts'
|
|
185
|
+
import { clearCookie, endSession, readSession, runRequestPresence } from './session.ts'
|
|
185
186
|
import { VIEW_AS_PATH, VIEW_ONLY_ERROR, clearViewCookie, endViewSession, runViewing, startViewSession, viewToken } from './view-as.ts'
|
|
186
187
|
import type { ActivityContext, ActivityInput } from './activity.ts'
|
|
187
188
|
import { acceptsReportMetadata } from './report-response.ts'
|
|
@@ -196,7 +197,7 @@ import { LINK_LOGIN_PATH, isLinkOAuthCallback, linkLoginRedirect, linkOAuthCallb
|
|
|
196
197
|
import type { ProfileChoice } from './github-links.ts'
|
|
197
198
|
import { withAppUsers } from './github-users.ts'
|
|
198
199
|
import { isBlobId } from './object-storage.ts'
|
|
199
|
-
import { attachmentDisposition, sendJson, sendLines, writeResponse } from './http-response.ts'
|
|
200
|
+
import { attachmentDisposition, sendJson, sendLines, writeResponse, writeResponseParts } from './http-response.ts'
|
|
200
201
|
import { triageWireEntry } from './triage-response.ts'
|
|
201
202
|
import { handlePublicWorkspace } from './public-workspace.ts'
|
|
202
203
|
import { serveUserTeamFeed } from './team-feed.ts'
|
|
@@ -205,7 +206,8 @@ import { hashToken, randomToken, safeEqual } from './crypto.ts'
|
|
|
205
206
|
import { canDeleteComment, parseCommentBody } from '../common/managed/comments.ts'
|
|
206
207
|
import { ManagedMutationError, reportReferenceSnapshot } from './management.ts'
|
|
207
208
|
import { BundleBuildError, buildRepositoryBundle, parseBundleBuild, withBundleBuildLease } from './bundle-build.ts'
|
|
208
|
-
import { NpmPackageError, type NpmReader, type NpmVersionDocument, canReadPrivateNpm, npmFileRows, npmScopesCount, npmTarballFilename, readNpmVersion, readNpmVersions
|
|
209
|
+
import { NpmPackageError, type NpmReader, type NpmVersionDocument, type NpmVersionStore, canReadPrivateNpm, npmFileRows, npmPublishTime, npmScopesCount, npmTarballFilename, readNpmVersion, readNpmVersions,
|
|
210
|
+
readTogether } from './npm-packages.ts'
|
|
209
211
|
import { loadNpmFiles, loadNpmPackageBody, loadNpmTarball } from './npm-loads.ts'
|
|
210
212
|
import { loadRepositoryCodeBody, repositoryCodeFits } from './repository-code.ts'
|
|
211
213
|
import { type ChangesStore, type CommitChanges, type CommitTree, TREES_READ, changesAsked, changesWithin, directoryFiles, repositoryChanges } from './repository-changes.ts'
|
|
@@ -214,8 +216,8 @@ import { type AllHeads, type HistoryCommit, type HistoryPages, MAX_HISTORY_PAGE,
|
|
|
214
216
|
repositoryAllHeads, repositoryAllHistory, repositoryHistory, repositoryRefTips, squashedPulls } from './repository-history.ts'
|
|
215
217
|
import { AGENTS_READ_VERSION, APP_AGENTS, COMMITS_CURSOR, repositoryAgents } from './repository-agents.ts'
|
|
216
218
|
import { ISSUES_CURSOR, ISSUES_READ_VERSION, repositoryIssues, repositoryItem } from './repository-issues.ts'
|
|
217
|
-
import { MAX_LOCKED, PULL_DEPENDENCIES_READ, type PullDependencies, type PullDependencyStore, eachOf, lockfileFormat,
|
|
218
|
-
import { lockedAt, lockedIn, lockfilesAt } from './commit-lockfiles.ts'
|
|
219
|
+
import { MAX_LOCKED, PULL_DEPENDENCIES_READ, type PullDependencies, type PullDependencyStore, eachOf, lockfileFormat, pullDependenciesAt } from './pull-dependencies.ts'
|
|
220
|
+
import { type GithubAsked, lockedAt, lockedIn, lockfilesAt } from './commit-lockfiles.ts'
|
|
219
221
|
import { MAX_REQUIRED_REVIEWS, SINCE_COMMIT } from './repository-dependency-reviews.ts'
|
|
220
222
|
import { EVERY_LOCKFILE, heldTo, scopeOf } from './lockfile-scope.ts'
|
|
221
223
|
import { type ReviewBaselineStore, baselineFor, heldDependencies, reviewedDependencies, reviewedPackages } from './pull-dependency-reviews.ts'
|
|
@@ -335,7 +337,7 @@ async function handleWorkspaceFixes(res: ServerResponse, deps: ManagedHttpDeps,
|
|
|
335
337
|
if (!s) return
|
|
336
338
|
const snapshot = await teamSnapshot(deps.db, s.session.id, teamId, reportId)
|
|
337
339
|
const ids = [...await teamWorkspaceFindingIds(deps.db, deps.reportStore, snapshot)]
|
|
338
|
-
const manualUrls = storedFixUrls(await deps.db.
|
|
340
|
+
const manualUrls = storedFixUrls(await deps.db.listTriageFixes(ids))
|
|
339
341
|
const storedIssues = await deps.db.listManagedIssues(ids)
|
|
340
342
|
const issues = visibleManagedIssues(storedIssues, snapshot)
|
|
341
343
|
const visibleIds = new Set(issues.map(issue => issue.findingId))
|
|
@@ -346,7 +348,7 @@ async function handleWorkspaceFixes(res: ServerResponse, deps: ManagedHttpDeps,
|
|
|
346
348
|
: { fixes: await lookupFixes(deps.config, deps.db, snapshot, urls), updates: [] }
|
|
347
349
|
// Cold report reads, token refresh and upstream batches can outlive changes
|
|
348
350
|
// to security/links, team grants, publication, sessions or the Fix links.
|
|
349
|
-
const currentUrls = storedFixUrls(await deps.db.
|
|
351
|
+
const currentUrls = storedFixUrls(await deps.db.listTriageFixes(ids))
|
|
350
352
|
if (await readWorkspaceSession(res, deps, cookie) == null) return
|
|
351
353
|
await recheckTeam(deps.db, s.session.id, snapshot)
|
|
352
354
|
if (JSON.stringify(currentUrls) !== JSON.stringify(manualUrls)) {
|
|
@@ -385,9 +387,8 @@ function activity(deps: ManagedHttpDeps, user: StoredUser, kind: ActivityInput['
|
|
|
385
387
|
return deps.db.recordActivity({ kind, actor: user.login, actorId: user.id, action, ...context }, Date.now())
|
|
386
388
|
}
|
|
387
389
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return (await deps.db.listAllRepos()).find(repo => repo.repoId === repoId)?.fullName ?? null
|
|
390
|
+
function repositoryName(deps: ManagedHttpDeps, repoId: number | null | undefined): Promise<string | null> {
|
|
391
|
+
return repoId == null ? Promise.resolve(null) : deps.db.getRepoFullName(repoId)
|
|
391
392
|
}
|
|
392
393
|
|
|
393
394
|
// Workspace history is authorized BEFORE filtering, counting, or paging.
|
|
@@ -411,8 +412,14 @@ async function handleHistory(res: ServerResponse, deps: ManagedHttpDeps, cookie:
|
|
|
411
412
|
if (s.user.role !== 'admin') {
|
|
412
413
|
const findings = new Map<string, ActivityContext>()
|
|
413
414
|
const reports = kind === 'all' || kind === 'triage' ? await deps.db.listActivityReports(s.user.id) : []
|
|
415
|
+
// Every report's record in one read, as visibleFindingIds reads each one's.
|
|
416
|
+
const snapshot = reports.length > 0 ? await deps.db.getReportAccessSnapshot(s.session.id, Date.now(), reports.map(report => report.reportId)) : { reports: [] }
|
|
417
|
+
if (snapshot == null) { sendJson(res, 401, { error: 'unauthenticated' }); return }
|
|
418
|
+
const records = new Map(snapshot.reports.map(record => [record.id, record]))
|
|
414
419
|
for (const report of reports) {
|
|
415
|
-
|
|
420
|
+
const record = records.get(report.reportId)
|
|
421
|
+
if (record == null) continue
|
|
422
|
+
for (const finding of await reportFindingIds(deps, record, record.repo.github)) {
|
|
416
423
|
if (!findings.has(finding)) findings.set(finding, { finding, ...report })
|
|
417
424
|
}
|
|
418
425
|
}
|
|
@@ -436,6 +443,9 @@ export interface ManagedHttpDeps {
|
|
|
436
443
|
originGate: OriginGate
|
|
437
444
|
isShuttingDown: () => boolean
|
|
438
445
|
track: (p: Promise<unknown>) => void
|
|
446
|
+
// Serverless (Vercel's): keeps the invocation alive for work that goes on
|
|
447
|
+
// past its answer, as a kept read read again behind it does.
|
|
448
|
+
waitUntil?: (p: Promise<unknown>) => void
|
|
439
449
|
// Combined boot delegates unmatched paths to e2e and overrides discovery.
|
|
440
450
|
serveStatic?: (req: IncomingMessage, res: ServerResponse) => boolean
|
|
441
451
|
next?: Handler
|
|
@@ -1679,12 +1689,22 @@ async function handleRepositoryHistory(res: ServerResponse, deps: ManagedHttpDep
|
|
|
1679
1689
|
}
|
|
1680
1690
|
const access = await authorize()
|
|
1681
1691
|
if (!access) return
|
|
1692
|
+
let readingPulls: Promise<{ number: number; sha: string }[] | null> = Promise.resolve(null)
|
|
1682
1693
|
try {
|
|
1683
1694
|
const reader = await createRepositoryBrowser(deps.config, await repositoryBrowserUser(deps, access.user.id)).reader(access.repo)
|
|
1684
1695
|
const repo = access.repo.fullName
|
|
1685
1696
|
const token = reader.readToken()
|
|
1686
1697
|
// With a token, as GraphQL reads them.
|
|
1687
1698
|
const pages = historyPages(deps, repoId, token !== null)
|
|
1699
|
+
const quiet = <T>(fallback: T) => (err: unknown) => {
|
|
1700
|
+
if (deps.config.debug) console.warn('managed: history pull requests read failed:', err)
|
|
1701
|
+
return fallback
|
|
1702
|
+
}
|
|
1703
|
+
// Open pull heads do not depend on the commit history. Start them with
|
|
1704
|
+
// it, handling failures immediately even if the history fails first.
|
|
1705
|
+
if (token && path === '' && page === 1) {
|
|
1706
|
+
readingPulls = keptAnswer(deps, `${repoId}\0openPulls`, () => openPullHeads(repo, token), undefined, fresh).then(answer => answer.value.pulls, quiet(null))
|
|
1707
|
+
}
|
|
1688
1708
|
let body: object
|
|
1689
1709
|
if (all) {
|
|
1690
1710
|
if (!token) { sendJson(res, 403, { error: 'github-token-required' }); return }
|
|
@@ -1719,13 +1739,9 @@ async function handleRepositoryHistory(res: ServerResponse, deps: ManagedHttpDep
|
|
|
1719
1739
|
// naming commits: with a token, as far as GitHub answers them, the
|
|
1720
1740
|
// history sent all the same where it doesn't.
|
|
1721
1741
|
const listed = (body as { commits: HistoryCommit[] }).commits
|
|
1722
|
-
const quiet = <T>(fallback: T) => (err: unknown) => {
|
|
1723
|
-
if (deps.config.debug) console.warn('managed: history pull requests read failed:', err)
|
|
1724
|
-
return fallback
|
|
1725
|
-
}
|
|
1726
1742
|
const [pulls, openPulls] = token && path === '' ? await Promise.all([
|
|
1727
1743
|
squashedPulls(repo, token, listed, historySquashes(deps, repoId)).catch(quiet<SquashedPull[]>([])),
|
|
1728
|
-
|
|
1744
|
+
readingPulls,
|
|
1729
1745
|
]) : [[], null]
|
|
1730
1746
|
await reader.recheckAccess()
|
|
1731
1747
|
const current = await authorize()
|
|
@@ -1740,6 +1756,10 @@ async function handleRepositoryHistory(res: ServerResponse, deps: ManagedHttpDep
|
|
|
1740
1756
|
} catch (err) {
|
|
1741
1757
|
if (err instanceof GithubApiError) { sendGithubError(res, err); return }
|
|
1742
1758
|
throw err
|
|
1759
|
+
} finally {
|
|
1760
|
+
// A failed history must not release the request's database lease while
|
|
1761
|
+
// its independent pull read is still writing to the cache.
|
|
1762
|
+
await readingPulls
|
|
1743
1763
|
}
|
|
1744
1764
|
}
|
|
1745
1765
|
|
|
@@ -1840,9 +1860,9 @@ const refusedRead = (err: unknown) => err instanceof GithubApiError
|
|
|
1840
1860
|
// What `read` answers of a repository, kept by `key` (its id first) for every
|
|
1841
1861
|
// reader (repositoryReadStore), each one's access checked as they ask before
|
|
1842
1862
|
// this: answered as kept for REPOSITORY_READ_FRESH_MS, then, while it is
|
|
1843
|
-
// kept, answered at once and read again behind it for the next to ask,
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1863
|
+
// kept, answered at once and read again behind it for the next to ask, the
|
|
1864
|
+
// read tracked (serverless, its invocation kept alive till it settles,
|
|
1865
|
+
// deps.waitUntil); `kept` where it was, or where reading it again
|
|
1846
1866
|
// failed as `keepThrough` lets it. One asked for afresh (`fresh`) is read
|
|
1847
1867
|
// now, unless it was read REPOSITORY_READ_FLOOR_MS since, what was kept
|
|
1848
1868
|
// answered where that read fails. One GitHub refuses to read again is no
|
|
@@ -1877,10 +1897,12 @@ async function keptAnswer<T extends object>(deps: ManagedHttpDeps, key: string,
|
|
|
1877
1897
|
}
|
|
1878
1898
|
const age = held ? Date.now() - held.at : Infinity
|
|
1879
1899
|
const now = fresh && age >= REPOSITORY_READ_FLOOR_MS
|
|
1880
|
-
if (held && !now
|
|
1900
|
+
if (held && !now) {
|
|
1881
1901
|
if (age >= REPOSITORY_READ_FRESH_MS && !repositoryRereads.has(key)) {
|
|
1882
1902
|
// Its failure leaves the kept answer sent until it is a day old, so it is said.
|
|
1883
|
-
|
|
1903
|
+
const behind = reread().catch((err: unknown) => { console.warn('managed: repository read failed:', err) })
|
|
1904
|
+
deps.track(behind)
|
|
1905
|
+
deps.waitUntil?.(behind)
|
|
1884
1906
|
}
|
|
1885
1907
|
return { value: held.value as T, kept: true, at: held.at }
|
|
1886
1908
|
}
|
|
@@ -1902,14 +1924,16 @@ async function keptAnswer<T extends object>(deps: ManagedHttpDeps, key: string,
|
|
|
1902
1924
|
function historyPages(deps: ManagedHttpDeps, repoId: number, graphql = false): HistoryPages {
|
|
1903
1925
|
const store = historyPageStore<HistoryCommit[]>(deps.db, deps.config.debug)
|
|
1904
1926
|
const keyOf = (top: string) => `${repoId}\0${COAUTHORS_READ}\0${top}`
|
|
1927
|
+
const told = (tops: string[], kept: Map<string, { value: HistoryCommit[] }>) => new Map(tops.flatMap(top => {
|
|
1928
|
+
const page = kept.get(keyOf(top))?.value
|
|
1929
|
+
return Array.isArray(page) && page.every(commit => Array.isArray(commit.coauthors)) && (!graphql || page.every(commit => commit.additions != null))
|
|
1930
|
+
? [[top, page] as const] : []
|
|
1931
|
+
}))
|
|
1905
1932
|
return {
|
|
1906
|
-
async
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
return Array.isArray(page) && page.every(commit => Array.isArray(commit.coauthors)) && (!graphql || page.every(commit => commit.additions != null))
|
|
1911
|
-
? [[top, page] as const] : []
|
|
1912
|
-
}))
|
|
1933
|
+
read: async tops => told(tops, await store.readMany(tops.map(keyOf))),
|
|
1934
|
+
async readFirst(first, then) {
|
|
1935
|
+
const kept = await store.readFirst(first.map(keyOf), then.map(keyOf))
|
|
1936
|
+
return { first: kept.first, pages: told(kept.first ? first : then, kept.found) }
|
|
1913
1937
|
},
|
|
1914
1938
|
write: (top, commits) => store.write(keyOf(top), commits, Date.now()),
|
|
1915
1939
|
}
|
|
@@ -1955,9 +1979,11 @@ async function readWholeRepository(res: ServerResponse, deps: ManagedHttpDeps, c
|
|
|
1955
1979
|
// What `answer` gives of the whole of repository `repoId` (with its name and
|
|
1956
1980
|
// a token), as readWholeRepository reads it: sent at once where it was kept,
|
|
1957
1981
|
// else once access is checked again, with the app's users it names
|
|
1958
|
-
// (github-users.ts), never kept with it
|
|
1982
|
+
// (github-users.ts), never kept with it; or what `unread` tells of it, the
|
|
1983
|
+
// app's alone, once the app's access is checked, GitHub then never asked.
|
|
1959
1984
|
async function answerWholeRepository(res: ServerResponse, deps: ManagedHttpDeps, cookie: string | undefined, repoId: number,
|
|
1960
|
-
answer: (fullName: string, token: string, user: StoredUser) => Promise<{ value: object; kept: boolean }
|
|
1985
|
+
answer: (fullName: string, token: string, user: StoredUser) => Promise<{ value: object; kept: boolean }>,
|
|
1986
|
+
unread: () => Promise<object | null> = () => Promise.resolve(null)): Promise<void> {
|
|
1961
1987
|
const authorize = async () => {
|
|
1962
1988
|
const access = await authorizeRepository(res, deps, cookie, repoId, '')
|
|
1963
1989
|
if (access && access.virtualEntries !== null) { sendJson(res, 404, { error: 'no-directory' }); return null }
|
|
@@ -1965,6 +1991,8 @@ async function answerWholeRepository(res: ServerResponse, deps: ManagedHttpDeps,
|
|
|
1965
1991
|
}
|
|
1966
1992
|
const access = await authorize()
|
|
1967
1993
|
if (!access) return
|
|
1994
|
+
const told = await unread()
|
|
1995
|
+
if (told) { sendJson(res, 200, await withAppUsers(deps.db, told)); return }
|
|
1968
1996
|
try {
|
|
1969
1997
|
const reader = await createRepositoryBrowser(deps.config, await repositoryBrowserUser(deps, access.user.id)).reader(access.repo)
|
|
1970
1998
|
const token = reader.readToken()
|
|
@@ -2033,72 +2061,108 @@ function itemNumber(query: URLSearchParams): number | null {
|
|
|
2033
2061
|
// GET /api/admin/repositories/pull-dependencies: the package versions pull
|
|
2034
2062
|
// request `number`'s lockfiles add (pull-dependencies.ts), what its base's and head's
|
|
2035
2063
|
// commits tell kept by them (pullDependencyKept), its `number` with them.
|
|
2036
|
-
// They are the whole repository's (answerWholeRepository),
|
|
2037
|
-
// as its popup's item is, its head moving with each
|
|
2064
|
+
// They are the whole repository's (answerWholeRepository), the commits they
|
|
2065
|
+
// are of kept by its number as its popup's item is, its head moving with each
|
|
2066
|
+
// push, what they tell kept by them alone, not again. With `review=1`,
|
|
2038
2067
|
// as an open one's are asked, where the repository reviews dependencies,
|
|
2039
2068
|
// each version's review (pull-dependency-reviews.ts), read afresh as each
|
|
2040
2069
|
// asks, reviews being published meanwhile; its baseline kept, where the host
|
|
2041
2070
|
// registered no reviewed dependencies, which are asked in its place.
|
|
2071
|
+
// Up to MAX_ASKED_PULLS pull requests asked together (`numbers=1,2,3`) are
|
|
2072
|
+
// answered together, as many read at once as PULLS_AT_ONCE, `{ pulls }`,
|
|
2073
|
+
// each's answer, else `{ number, error }` where GitHub fails it, with one
|
|
2074
|
+
// check of its access before and one after, but where each was kept.
|
|
2075
|
+
const MAX_ASKED_PULLS = 20
|
|
2076
|
+
const PULLS_AT_ONCE = 3
|
|
2042
2077
|
async function handleRepositoryPullDependencies(res: ServerResponse, deps: ManagedHttpDeps, cookie: string | undefined, query: URLSearchParams): Promise<void> {
|
|
2043
2078
|
const repoId = Number(query.get('repoId'))
|
|
2044
|
-
const number = itemNumber(query)
|
|
2045
2079
|
const review = query.get('review') === '1'
|
|
2046
|
-
|
|
2080
|
+
const listed = query.get('numbers')
|
|
2081
|
+
const numbers = listed === null ? [itemNumber(query)] : listed.split(',').map(text => itemNumber(new URLSearchParams({ number: text })))
|
|
2082
|
+
if (!Number.isSafeInteger(repoId) || repoId <= 0 || numbers.length > MAX_ASKED_PULLS || numbers.some(number => number === null) || new Set(numbers).size < numbers.length
|
|
2083
|
+
|| !['1', null].includes(query.get('review'))) { sendJson(res, 400, { error: 'bad-request' }); return }
|
|
2047
2084
|
await answerWholeRepository(res, deps, cookie, repoId, async (fullName, token, user) => {
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
// Its review by the lockfiles they were told by, though they have changed
|
|
2056
|
-
// since; turned off meanwhile, all of them, as where it is off.
|
|
2057
|
-
const state = now && setting?.enabled ? { ...now, lockfiles: setting.lockfiles } : now
|
|
2058
|
-
const told = review && !now && setting?.enabled && !(await deps.db.listRepoDependencyReviews()).get(repoId)?.enabled ? answered.value : held
|
|
2059
|
-
const reviewed = deps.reviewedDependencies ?? null
|
|
2060
|
-
const { baseline, unread, error } = state && !reviewed ? await baselineFor(fullName, token, repoId, state, baselineKept(deps)) : { baseline: null, unread: [], error: null }
|
|
2061
|
-
// Its comments read for reviews, where the host reads them, while it is
|
|
2062
|
-
// open (pull-comment-reviews.ts reads whether it is with them).
|
|
2063
|
-
const parse = reviewed?.parseComments
|
|
2064
|
-
const comments = state && parse ? commentReviews({ fullName, number, token, parse, reviewers: accounts => repoReviewers(deps, repoId, accounts) }) : null
|
|
2065
|
-
// The comment the host formats of the reader's reviews of its versions, for its copy button.
|
|
2066
|
-
const formatted = await formattedPull(reviewed, deps.db, user.id, told, state, { repository: fullName, number })
|
|
2067
|
-
return { value: { number: told.number, ...await reviewedDependencies(told, state, { baseline, baselineUnread: unread, baselineError: error, reviewed, commentReviews: comments }),
|
|
2068
|
-
...formatted ? { formatted } : {} }, kept: answered.kept }
|
|
2085
|
+
if (listed === null) return pullDependencyAnswer(deps, fullName, token, user, repoId, numbers[0]!, review)
|
|
2086
|
+
const answers = await eachOf(numbers as number[], PULLS_AT_ONCE, number => pullDependencyAnswer(deps, fullName, token, user, repoId, number, review).catch((err: unknown) => {
|
|
2087
|
+
// GitHub asked, though it failed: its access asked again after.
|
|
2088
|
+
if (err instanceof GithubApiError) return { value: { number, error: err.message }, kept: false }
|
|
2089
|
+
throw err
|
|
2090
|
+
}))
|
|
2091
|
+
return { value: { pulls: answers.map(({ value }) => value) }, kept: answers.every(({ kept }) => kept) }
|
|
2069
2092
|
})
|
|
2070
2093
|
}
|
|
2071
2094
|
|
|
2095
|
+
// Pull request `number`'s answer, as the endpoint gives it, and whether all
|
|
2096
|
+
// of it was kept, GitHub asked for none of it.
|
|
2097
|
+
async function pullDependencyAnswer(deps: ManagedHttpDeps, fullName: string, token: string, user: StoredUser, repoId: number, number: number, review: boolean) {
|
|
2098
|
+
const kept = pullDependencyKept(deps, repoId)
|
|
2099
|
+
const reading: { told?: PullDependencies } = {}
|
|
2100
|
+
const answered = await keptAnswer(deps, `${repoId}\0pull-commits\0${PULL_DEPENDENCIES_READ}\0${number}`, async () => {
|
|
2101
|
+
const { commits, told } = await pullDependenciesAt(fullName, token, number, kept)
|
|
2102
|
+
reading.told = told
|
|
2103
|
+
return { commits }
|
|
2104
|
+
})
|
|
2105
|
+
// What its commits tell, as read, else as kept by them, else, no longer
|
|
2106
|
+
// kept, read again.
|
|
2107
|
+
const held = reading.told ?? (typeof answered.value.commits === 'string' ? await kept.read(answered.value.commits) : null)
|
|
2108
|
+
const pulled = { number, ...held ?? (await pullDependenciesAt(fullName, token, number, kept)).told }
|
|
2109
|
+
// Where its review is on, those of lockfiles it leaves out told to none,
|
|
2110
|
+
// whoever asks.
|
|
2111
|
+
const setting = await deps.db.repoDependencyReview(repoId)
|
|
2112
|
+
const scoped = setting?.enabled ? heldDependencies(pulled, setting.lockfiles) : pulled
|
|
2113
|
+
const now = review ? await deps.db.repoDependencyReviewState(repoId, user.id, reviewedPackages(scoped)) : null
|
|
2114
|
+
// Its review by the lockfiles they were told by, though they have changed
|
|
2115
|
+
// since; turned off meanwhile, all of them, as where it is off.
|
|
2116
|
+
const state = now && setting?.enabled ? { ...now, lockfiles: setting.lockfiles } : now
|
|
2117
|
+
const told = review && !now && setting?.enabled && !(await deps.db.repoDependencyReview(repoId))?.enabled ? pulled : scoped
|
|
2118
|
+
const reviewed = deps.reviewedDependencies ?? null
|
|
2119
|
+
const { baseline, unread, error } = state && !reviewed ? await baselineFor(fullName, token, repoId, state, baselineKept(deps)) : { baseline: null, unread: [], error: null }
|
|
2120
|
+
// Its comments read for reviews, where the host reads them, while it is
|
|
2121
|
+
// open (pull-comment-reviews.ts reads whether it is with them).
|
|
2122
|
+
const parse = reviewed?.parseComments
|
|
2123
|
+
const comments = state && parse ? commentReviews({ fullName, number, token, parse, reviewers: accounts => repoReviewers(deps, repoId, accounts) }) : null
|
|
2124
|
+
// The comment the host formats of the reader's reviews of its versions, for its copy button.
|
|
2125
|
+
const formatted = await formattedPull(reviewed, deps.db, user.id, told, state, { repository: fullName, number })
|
|
2126
|
+
return { value: { number: told.number, ...await reviewedDependencies(told, state, { baseline, baselineUnread: unread, baselineError: error, reviewed, commentReviews: comments }),
|
|
2127
|
+
...formatted ? { formatted } : {} }, kept: answered.kept && held !== null }
|
|
2128
|
+
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2072
2131
|
// GET /api/admin/repositories/branch-dependencies: what the lockfiles of the
|
|
2073
2132
|
// repository's default branch lock that wants review, where its review is in
|
|
2074
|
-
// effect for the reader (`review`, else none
|
|
2075
|
-
//
|
|
2076
|
-
// there its review holds to
|
|
2077
|
-
//
|
|
2078
|
-
// (pull-dependency-reviews.ts), each left unread
|
|
2079
|
-
// failing the review, as a pull request's are.
|
|
2133
|
+
// effect for the reader (`review`, else none, told without asking GitHub
|
|
2134
|
+
// where it reviews none), as a team's Dependencies tab shows it: its head, as
|
|
2135
|
+
// its pulse reads it, kept a while; every lockfile there its review holds to
|
|
2136
|
+
// (lockfile-scope.ts, commit-lockfiles.ts) as one directory, its versions as
|
|
2137
|
+
// a pull request's others are (pull-dependency-reviews.ts), each left unread
|
|
2138
|
+
// a directory of its own failing the review, as a pull request's are. Kept,
|
|
2139
|
+
// its access not asked again, where its head was kept and its lockfiles and
|
|
2140
|
+
// baseline held, GitHub asked for none of them.
|
|
2080
2141
|
async function handleRepositoryBranchDependencies(res: ServerResponse, deps: ManagedHttpDeps, cookie: string | undefined, query: URLSearchParams): Promise<void> {
|
|
2081
2142
|
const repoId = Number(query.get('repoId'))
|
|
2082
2143
|
if (!Number.isSafeInteger(repoId) || repoId <= 0) { sendJson(res, 400, { error: 'bad-request' }); return }
|
|
2083
2144
|
await answerWholeRepository(res, deps, cookie, repoId, async (fullName, token, user) => {
|
|
2084
|
-
const
|
|
2145
|
+
const headRead = await keptAnswer(deps, `${repoId}\0pulse-head`, async () => ({ sha: await defaultHead(fullName, token) }))
|
|
2146
|
+
const head = headRead.value.sha
|
|
2085
2147
|
if (!head) return { value: { head: null, review: null }, kept: false }
|
|
2148
|
+
const asked: GithubAsked = { github: !headRead.kept }
|
|
2086
2149
|
// Those its review holds to alone read, as many as are read of them.
|
|
2087
|
-
const setting =
|
|
2150
|
+
const setting = await deps.db.repoDependencyReview(repoId)
|
|
2088
2151
|
if (!setting?.enabled) return { value: { head, review: null }, kept: false }
|
|
2089
|
-
const read = await lockedAt(fullName, token, head, path => heldTo(setting.lockfiles, path))
|
|
2152
|
+
const read = await lockedAt(fullName, token, head, path => heldTo(setting.lockfiles, path), asked)
|
|
2090
2153
|
const { packages, paths } = lockedIn(read)
|
|
2091
2154
|
const told = { base: head, head, cut: false, lockfiles: [{ paths, added: [], more: 0, locked: packages.slice(0, MAX_LOCKED), moreLocked: Math.max(0, packages.length - MAX_LOCKED) },
|
|
2092
2155
|
...read.unread.map(path => ({ paths: [path], error: lockfileFormat(path) === 'bun' ? 'lockfile-unsupported' : 'lockfile-unreadable' }))] }
|
|
2093
2156
|
const now = await deps.db.repoDependencyReviewState(repoId, user.id, reviewedPackages(told))
|
|
2094
|
-
if (!now) return { value: { head, review: null }, kept:
|
|
2157
|
+
if (!now) return { value: { head, review: null }, kept: !asked.github }
|
|
2095
2158
|
// Told by the lockfiles it held to as they were read, though they have
|
|
2096
2159
|
// changed since.
|
|
2097
2160
|
const state = { ...now, lockfiles: setting.lockfiles }
|
|
2098
2161
|
const reviewed = deps.reviewedDependencies ?? null
|
|
2099
|
-
const { baseline, unread: baselineUnread, error } = reviewed ? { baseline: null, unread: [], error: null }
|
|
2100
|
-
|
|
2101
|
-
|
|
2162
|
+
const { baseline, unread: baselineUnread, error } = reviewed ? { baseline: null, unread: [], error: null }
|
|
2163
|
+
: await baselineFor(fullName, token, repoId, state, baselineKept(deps), asked)
|
|
2164
|
+
return { value: await reviewedDependencies(told, state, { baseline, baselineUnread, baselineError: error, reviewed }), kept: !asked.github }
|
|
2165
|
+
}, async () => (await deps.db.repoDependencyReview(repoId))?.enabled ? null : { review: null })
|
|
2102
2166
|
}
|
|
2103
2167
|
|
|
2104
2168
|
// GET /api/admin/repositories/lockfiles: the lockfiles of the repository's
|
|
@@ -3205,7 +3269,7 @@ async function handleQueryReports(req: IncomingMessage, res: ServerResponse, dep
|
|
|
3205
3269
|
if (outputBytes > MAX_REPORT_QUERY_BYTES) { sendJson(res, 413, { error: 'batch-too-large' }); return }
|
|
3206
3270
|
parts.push(suffix)
|
|
3207
3271
|
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' })
|
|
3208
|
-
|
|
3272
|
+
writeResponseParts(res, parts, outputBytes)
|
|
3209
3273
|
}
|
|
3210
3274
|
|
|
3211
3275
|
// The server selects the complete published workspace for classification.
|
|
@@ -3235,24 +3299,23 @@ async function visibleFindingIds(deps: ManagedHttpDeps, user: StoredUser, report
|
|
|
3235
3299
|
}
|
|
3236
3300
|
const rec = await deps.db.getReport(reportId)
|
|
3237
3301
|
if (rec == null) return new Set()
|
|
3238
|
-
|
|
3239
|
-
|
|
3302
|
+
return reportFindingIds(deps, rec, await repositoryName(deps, rec.repoId))
|
|
3303
|
+
}
|
|
3304
|
+
// A whole report's finding ids, kept by its content, name and repository,
|
|
3305
|
+
// the one read least recently dropped first.
|
|
3306
|
+
async function reportFindingIds(deps: ManagedHttpDeps, rec: { id: string; sha256: string; filename: string }, github: string | null): Promise<Set<string>> {
|
|
3307
|
+
const key = JSON.stringify([rec.id, rec.sha256, rec.filename, { github }])
|
|
3240
3308
|
let cache = visibleIdsCaches.get(deps)
|
|
3241
3309
|
if (cache == null) { cache = new Map(); visibleIdsCaches.set(deps, cache) }
|
|
3242
3310
|
const hit = cache.get(key)
|
|
3243
|
-
if (hit != null) return hit
|
|
3244
|
-
const
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
if (report == null) return ids
|
|
3252
|
-
for (const f of report.findings) {
|
|
3253
|
-
const id = (f as { id?: unknown }).id
|
|
3254
|
-
if (typeof id === 'string' && id !== '') ids.add(id)
|
|
3255
|
-
}
|
|
3311
|
+
if (hit != null) { cache.delete(key); cache.set(key, hit); return hit }
|
|
3312
|
+
const bytes = await deps.reportStore.get(rec.id)
|
|
3313
|
+
if (bytes == null) return new Set()
|
|
3314
|
+
const { data } = readManagedReport(bytes.toString('utf8'), rec.filename)
|
|
3315
|
+
if (data == null) return new Set()
|
|
3316
|
+
// The ids the team workspace derives (teamFindingIds), from the one parse.
|
|
3317
|
+
const ids = await reportIds(data)
|
|
3318
|
+
ids.delete('')
|
|
3256
3319
|
while (cache.size >= VISIBLE_IDS_CACHE_MAX) {
|
|
3257
3320
|
const oldest = cache.keys().next().value
|
|
3258
3321
|
if (oldest === undefined) break
|
|
@@ -3663,8 +3726,12 @@ async function handleSetTeamNpmScopes(req: IncomingMessage, res: ServerResponse,
|
|
|
3663
3726
|
// Null for none.
|
|
3664
3727
|
async function npmReader(deps: ManagedHttpDeps, cookie: string | undefined, names: readonly string[]): Promise<NpmReader | null> {
|
|
3665
3728
|
const s = await readSession(deps.config, deps.db, cookie, Date.now())
|
|
3666
|
-
|
|
3667
|
-
|
|
3729
|
+
return s == null ? null : npmReaderOf(deps, s.user, names)
|
|
3730
|
+
}
|
|
3731
|
+
// The same of `user`, as a session the request read just now tells them.
|
|
3732
|
+
async function npmReaderOf(deps: ManagedHttpDeps, user: { id: string; role: NpmReader['role'] }, names: readonly string[]): Promise<NpmReader | null> {
|
|
3733
|
+
if (!roleAtLeast(user.role, 'view')) return null
|
|
3734
|
+
return { role: user.role, scopes: new Set(npmScopesCount(user.role, names) ? await deps.db.listUserNpmScopes(user.id) : []), userId: user.id }
|
|
3668
3735
|
}
|
|
3669
3736
|
|
|
3670
3737
|
// What a registry read found, or a 404 for the package (handleNpm's catch
|
|
@@ -3680,20 +3747,25 @@ function found<T>(value: T | null | undefined): T {
|
|
|
3680
3747
|
const REVIEW_READS_AT_ONCE = 8
|
|
3681
3748
|
|
|
3682
3749
|
// Lists must enforce the same target and base access as the review itself,
|
|
3683
|
-
// including permission changes while the registry requests are in flight
|
|
3684
|
-
|
|
3750
|
+
// including permission changes while the registry requests are in flight;
|
|
3751
|
+
// first as `session`, the one the request read just now, where given.
|
|
3752
|
+
async function readableNpmReviews<T extends ReviewContext>(res: ServerResponse, deps: ManagedHttpDeps, cookie: string | undefined, user: string, reviews: T[],
|
|
3753
|
+
session?: { id: string; role: NpmReader['role'] }): Promise<T[] | null> {
|
|
3685
3754
|
const controller = new AbortController()
|
|
3686
3755
|
const onClose = () => { if (!res.writableEnded) controller.abort() }
|
|
3687
3756
|
res.on('close', onClose)
|
|
3688
3757
|
try {
|
|
3689
|
-
const
|
|
3758
|
+
const names = reviews.flatMap(review => [review.package, ...review.basePackage ? [review.basePackage] : []])
|
|
3759
|
+
const reader = session ? await npmReaderOf(deps, session, names) : await npmReader(deps, cookie, names)
|
|
3690
3760
|
if (!reader || reader.userId !== user) { sendJson(res, 401, { error: 'unauthenticated' }); return null }
|
|
3691
3761
|
// Public versions' documents as the viewer keeps them (npm-kept.ts), a
|
|
3692
|
-
// day: a list asks npm for none it read
|
|
3693
|
-
// many at once as REVIEW_READS_AT_ONCE.
|
|
3694
|
-
const kept = npmVersionStore(deps.db, deps.config.debug)
|
|
3762
|
+
// day, all read of the database at once: a list asks npm for none it read
|
|
3763
|
+
// within it. Each version once, as many at once as REVIEW_READS_AT_ONCE.
|
|
3695
3764
|
const versionsOf = (review: T) => [[review.package, review.version], ...review.basePackage ? [[review.basePackage, review.baseVersion!]] : []] as [string, string][]
|
|
3696
3765
|
const asked = new Map(reviews.flatMap(versionsOf).map(([name, version]) => [`${name}\0${version}`, [name, version] as const]))
|
|
3766
|
+
const store = npmVersionStore(deps.db, deps.config.debug)
|
|
3767
|
+
const held = await store.readMany?.([...asked.values()].map(([name, version]) => ({ name, version }))) ?? new Map()
|
|
3768
|
+
const kept: NpmVersionStore = { read: (name, version) => Promise.resolve(held.get(`${name}@${version}`) ?? null), write: store.write }
|
|
3697
3769
|
const read = await eachOf([...asked], REVIEW_READS_AT_ONCE, ([, [name, version]]) => readNpmVersion(name, version, canReadPrivateNpm(reader, name), controller.signal, kept))
|
|
3698
3770
|
const documents = new Map([...asked.keys()].map((key, at) => [key, read[at] ?? null]))
|
|
3699
3771
|
const packages = reviews.map(review => versionsOf(review).map(([name, version]) => documents.get(`${name}\0${version}`) ?? null))
|
|
@@ -3809,7 +3881,7 @@ async function handleNpmReviews(req: IncomingMessage, res: ServerResponse, deps:
|
|
|
3809
3881
|
try {
|
|
3810
3882
|
const reader = await npmReader(deps, cookie, [context.package, ...context.basePackage ? [context.basePackage] : []])
|
|
3811
3883
|
if (!reader || reader.userId !== session.user.id) { sendJson(res, 401, { error: 'unauthenticated' }); return }
|
|
3812
|
-
const kept = npmVersionStore(deps.db, deps.config.debug)
|
|
3884
|
+
const kept = readTogether(npmVersionStore(deps.db, deps.config.debug), [{ name, version }, ...context.basePackage ? [{ name: context.basePackage, version: context.baseVersion! }] : []])
|
|
3813
3885
|
const read = (pkg: string, spec: string) => readNpmVersion(pkg, spec, canReadPrivateNpm(reader, pkg), controller.signal, kept)
|
|
3814
3886
|
const [doc, base] = await Promise.all([read(name, version), context.basePackage ? read(context.basePackage, context.baseVersion!) : null])
|
|
3815
3887
|
if (!doc || (context.basePackage && !base)) { sendJson(res, 404, { error: 'package-not-found' }); return }
|
|
@@ -3844,7 +3916,7 @@ async function handleNpmReviews(req: IncomingMessage, res: ServerResponse, deps:
|
|
|
3844
3916
|
if (!change) {
|
|
3845
3917
|
const allBases = params.get('allBases') === 'true'
|
|
3846
3918
|
const rows = await deps.db.listDependencyReviews(context, current.userId, allBases)
|
|
3847
|
-
const reviews = allBases ? await readableNpmReviews(res, deps, cookie, current.userId, rows) : rows
|
|
3919
|
+
const reviews = allBases ? await readableNpmReviews(res, deps, cookie, current.userId, rows, { id: current.userId, role: current.role }) : rows
|
|
3848
3920
|
if (reviews) sendJson(res, 200, { reviews: await withFormatted(deps.reviewedDependencies ?? null, current.userId, reviews), teams: await deps.db.listDependencyReviewTeams(current.userId) })
|
|
3849
3921
|
return
|
|
3850
3922
|
}
|
|
@@ -3949,7 +4021,17 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
3949
4021
|
await reply(doc.private, { name, downloads, github })
|
|
3950
4022
|
return
|
|
3951
4023
|
}
|
|
3952
|
-
|
|
4024
|
+
// The version it is compared with, where its route names one, read
|
|
4025
|
+
// beside it, the two read of those kept at once.
|
|
4026
|
+
const against: [string, string, boolean] | null = (path === NPM_RISK_PATH && otherSpec !== '' && otherSpec !== '(empty)') || (path === NPM_DELTA_PATH && otherSpec !== '(empty)')
|
|
4027
|
+
|| ((path === NPM_TYPE_ONLY_PATH || path === NPM_RENAME_DIFF_PATH || (path === NPM_PRETTY_PATH && !params.has('path'))) && otherSpec !== '')
|
|
4028
|
+
? [name, otherSpec, privileged]
|
|
4029
|
+
: path === NPM_COMPARISON_PATH && otherSpec !== '(empty)' ? [otherName, otherSpec, canReadPrivateNpm(reader, otherName)] : null
|
|
4030
|
+
const keptTogether = against ? readTogether(keptVersions, [{ name, version: spec }, { name: against[0], version: against[1] }]) : keptVersions
|
|
4031
|
+
const otherAsked = against && readNpmVersion(against[0], against[1], against[2], controller.signal, keptTogether)
|
|
4032
|
+
otherAsked?.catch(() => {})
|
|
4033
|
+
const otherDoc = async () => found(await otherAsked!)
|
|
4034
|
+
const doc = found(await readNpmVersion(name, spec, privileged, controller.signal, keptTogether))
|
|
3953
4035
|
if (spec === 'latest') latest = Promise.resolve(doc)
|
|
3954
4036
|
const repo = githubRepoOf(doc), sha = bundleCommitHash(doc.manifest['gitHead'])
|
|
3955
4037
|
// What the viewer asks about a version beside its files, by its route,
|
|
@@ -3972,7 +4054,7 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
3972
4054
|
// new and however few download it where a team lists its scope, its own
|
|
3973
4055
|
// packages, as a new dependency on a package of such a scope is.
|
|
3974
4056
|
if (path === NPM_RISK_PATH) {
|
|
3975
|
-
const old = otherSpec === '' || otherSpec === '(empty)' ? null :
|
|
4057
|
+
const old = otherSpec === '' || otherSpec === '(empty)' ? null : await otherDoc()
|
|
3976
4058
|
const scope = npmPackageScope(name)
|
|
3977
4059
|
const teamScoped = scope !== null && await deps.db.hasTeamNpmScope(scope)
|
|
3978
4060
|
const assessment = await npmRisk(doc, otherSpec === '' ? 'pick' : old, { db: deps.db, npmCache: deps.npmCacheStorage, debug: deps.config.debug, teamScoped, fromEmpty: otherSpec === '(empty)',
|
|
@@ -3987,7 +4069,7 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
3987
4069
|
// package's, and those but for declaration files changed in types alone
|
|
3988
4070
|
// (npm-delta.ts), as a list of versions wanting reviews tells them.
|
|
3989
4071
|
if (path === NPM_DELTA_PATH) {
|
|
3990
|
-
const old = otherSpec === '(empty)' ? null :
|
|
4072
|
+
const old = otherSpec === '(empty)' ? null : await otherDoc()
|
|
3991
4073
|
res.setTimeout(NPM_RESPONSE_IDLE_MS, () => res.destroy())
|
|
3992
4074
|
const side = (version: NpmVersionDocument) => ({ integrity: version.dist.integrity, files: async () => {
|
|
3993
4075
|
const load = loadNpmFiles(version)
|
|
@@ -4003,7 +4085,7 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
4003
4085
|
// the one compared with, or `(empty)`, none, as Review reads a first
|
|
4004
4086
|
// version. Read of both, each package's access checked again once worked out.
|
|
4005
4087
|
if (path === NPM_COMPARISON_PATH) {
|
|
4006
|
-
const old = otherSpec === '(empty)' ? null :
|
|
4088
|
+
const old = otherSpec === '(empty)' ? null : await otherDoc()
|
|
4007
4089
|
res.setTimeout(NPM_RESPONSE_IDLE_MS, () => res.destroy())
|
|
4008
4090
|
const loads = [doc, ...old ? [old] : []].map(loadNpmFiles)
|
|
4009
4091
|
let body
|
|
@@ -4041,7 +4123,7 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
4041
4123
|
// and those that changed in types alone (type-only.ts).
|
|
4042
4124
|
if (path === NPM_PRETTY_PATH || path === NPM_TYPE_ONLY_PATH || (path === NPM_RENAME_DIFF_PATH && !diffed)) {
|
|
4043
4125
|
if (!deps.prettyCache) { sendJson(res, 503, { error: 'unavailable' }); return }
|
|
4044
|
-
const other = otherSpec === '' ? null :
|
|
4126
|
+
const other = otherSpec === '' ? null : await otherDoc()
|
|
4045
4127
|
const sides = other && await deps.prettyCache.npmSides(doc, other)
|
|
4046
4128
|
const lines = path === NPM_RENAME_DIFF_PATH ? renamedPairs(sides!, !doc.private && !other!.private)
|
|
4047
4129
|
: path === NPM_TYPE_ONLY_PATH ? typeOnlyFiles(sides!) : sides ? compared(sides) : await deps.prettyCache.npmAll(doc)
|
|
@@ -4059,7 +4141,7 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
4059
4141
|
if (diffed) {
|
|
4060
4142
|
const { prettyCache } = deps
|
|
4061
4143
|
if (!prettyCache) { sendJson(res, 503, { error: 'unavailable' }); return }
|
|
4062
|
-
const other =
|
|
4144
|
+
const other = await otherDoc()
|
|
4063
4145
|
const diff = await renameDiffOf(diffed, async () => [await prettyText(await prettyCache.npm(doc, diffed.base)),
|
|
4064
4146
|
await prettyText(await prettyCache.npm(other, diffed.other))], !doc.private && !other.private)
|
|
4065
4147
|
if (!(await recheck(doc.private || other.private))) { diff.stream.destroy(); return }
|
|
@@ -4079,18 +4161,20 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
4079
4161
|
// viewer's own request to meet. Its whole document, read for its
|
|
4080
4162
|
// advisories' version list and the time each version was published
|
|
4081
4163
|
// (npmPublishTime), is read only where registry reads leave room for
|
|
4082
|
-
// readers'
|
|
4083
|
-
|
|
4164
|
+
// readers'; a public version's time is then kept in the database
|
|
4165
|
+
// (npm-kept.ts), to come with the version on any instance.
|
|
4166
|
+
const published = download ? undefined : npmPublishTime(doc, npmPublishTimeStore(deps.db, deps.config.debug))
|
|
4167
|
+
if (published) {
|
|
4084
4168
|
const quiet = (asked: Promise<unknown>) => { asked.catch(() => {}) }
|
|
4085
4169
|
quiet(npmDownloads(name))
|
|
4086
4170
|
quiet(latestDoc().then(newest => newest && githubFigures(newest)))
|
|
4087
4171
|
quiet(readNpmVersions(name, privileged, controller.signal, { times: true, spare: true })
|
|
4088
|
-
.then(versions => versions && npmAdvisories(name, versions.versions, advisoryOptions)))
|
|
4172
|
+
.then(versions => versions && Promise.all([published.keep(), npmAdvisories(name, versions.versions, advisoryOptions)])))
|
|
4089
4173
|
quiet(tagsOf())
|
|
4090
4174
|
quiet(socketOf())
|
|
4091
4175
|
quiet(provenanceOf())
|
|
4092
4176
|
}
|
|
4093
|
-
const load =
|
|
4177
|
+
const load = published ? loadNpmPackageBody(doc, () => npmKnown(name, doc.version, doc.private, repo, sha), published.read) : loadNpmTarball(doc)
|
|
4094
4178
|
res.once('close', load.release)
|
|
4095
4179
|
const bytes = await load.result
|
|
4096
4180
|
if (!(await recheck(doc.private))) return
|
|
@@ -4562,7 +4646,7 @@ export function createManagedRequestHandler(deps: ManagedHttpDeps): Handler {
|
|
|
4562
4646
|
sendJson(res, 400, { error: 'bad-review-context' }); return
|
|
4563
4647
|
}
|
|
4564
4648
|
const review = await deps.db.getLatestOwnDependencyReview(s.user.id, { ecosystem: 'npm', package: name, version, integrity })
|
|
4565
|
-
const visible = await readableNpmReviews(res, deps, cookie, s.user.id, review ? [review] : [])
|
|
4649
|
+
const visible = await readableNpmReviews(res, deps, cookie, s.user.id, review ? [review] : [], s.user)
|
|
4566
4650
|
if (visible) sendJson(res, 200, { review: (await withFormatted(deps.reviewedDependencies ?? null, s.user.id, visible))[0] ?? null })
|
|
4567
4651
|
return
|
|
4568
4652
|
}
|
|
@@ -4592,7 +4676,7 @@ export function createManagedRequestHandler(deps: ManagedHttpDeps): Handler {
|
|
|
4592
4676
|
sendJson(res, 400, { error: 'bad-review-cursor' }); return
|
|
4593
4677
|
}
|
|
4594
4678
|
const page = await deps.db.listOwnDependencyReviews(s.user.id, time !== null && id !== null ? { time: Number(time), id } : null)
|
|
4595
|
-
const reviews = await readableNpmReviews(res, deps, cookie, s.user.id, page.reviews)
|
|
4679
|
+
const reviews = await readableNpmReviews(res, deps, cookie, s.user.id, page.reviews, s.user)
|
|
4596
4680
|
if (reviews) sendJson(res, 200, { ...page, reviews: await withFormatted(deps.reviewedDependencies ?? null, s.user.id, reviews) })
|
|
4597
4681
|
return
|
|
4598
4682
|
}
|
|
@@ -4609,7 +4693,7 @@ export function createManagedRequestHandler(deps: ManagedHttpDeps): Handler {
|
|
|
4609
4693
|
}
|
|
4610
4694
|
const page = await deps.db.listTeamDependencyReviews(teamId, s.user.id, time !== null && id !== null ? { time: Number(time), id } : null)
|
|
4611
4695
|
if (!page) { sendJson(res, 404, { error: 'no-team' }); return }
|
|
4612
|
-
const reviews = await readableNpmReviews(res, deps, cookie, s.user.id, page.reviews)
|
|
4696
|
+
const reviews = await readableNpmReviews(res, deps, cookie, s.user.id, page.reviews, s.user)
|
|
4613
4697
|
if (!reviews) return
|
|
4614
4698
|
// Its access asked again, as it may have been taken while npm was read.
|
|
4615
4699
|
if (!await deps.db.readsTeamDependencyReviews(teamId, s.user.id)) { sendJson(res, 404, { error: 'no-team' }); return }
|
|
@@ -4634,7 +4718,7 @@ export function createManagedRequestHandler(deps: ManagedHttpDeps): Handler {
|
|
|
4634
4718
|
// Feeds lease a connection per poll, never across streaming sleeps. This
|
|
4635
4719
|
// also covers capability-authenticated feeds handled by public-workspace.
|
|
4636
4720
|
const feed = /^\/api\/teams(?:\/[^/?]+)?\/feed(?:\?|$)/u.test(req.url ?? '')
|
|
4637
|
-
const serve = () => db.withRequest && !feed ? db.withRequest(() => route(req, res)) : route(req, res)
|
|
4721
|
+
const serve = () => runRequestPresence(() => db.withRequest && !feed ? db.withRequest(() => route(req, res)) : route(req, res))
|
|
4638
4722
|
const work = runViewing(viewToken(config, req.headers.cookie) != null, serve).catch((err) => {
|
|
4639
4723
|
if ((err instanceof TeamReportsError || err instanceof IssueError || err instanceof OAuthError || err instanceof ManagedMutationError) && !res.headersSent) { sendJson(res, err.status, { error: err.message }); return }
|
|
4640
4724
|
console.warn('managed: request handler error:', err)
|