@preventive/triage 1.0.0-alpha.50 → 1.0.0-alpha.51
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/common/managed/finding-detail.d.ts +2 -1
- package/common/managed/finding-detail.js +43 -9
- package/common/managed/npm-packages.d.ts +2 -0
- package/common/managed/npm-packages.js +18 -1
- package/common/managed/routes.js +19 -7
- package/common/managed/team-app.d.ts +1 -0
- package/common/managed/team-app.js +17 -1
- package/out/activity.js +4 -4
- package/out/{chunk-75YLHBCM.js → chunk-2K6UN3PC.js} +1 -1
- package/out/{chunk-2WDZPXVL.js → chunk-7JBSSRD7.js} +1 -1
- package/out/{chunk-4XMP7YVH.js → chunk-FMXWXDR4.js} +1 -1
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/chunk-IPAH4LKF.js +232 -0
- package/out/{chunk-FKBXNUW4.js → chunk-KOGNQDSJ.js} +1 -1
- package/out/{chunk-UK7RGJT6.js → chunk-VDRXHIHN.js} +2 -2
- package/out/{chunk-HTBYHSPG.js → chunk-WRAJVD35.js} +1 -1
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +25 -25
- package/out/compare.js +14 -14
- package/out/dialogs.js +41 -41
- package/out/graph.js +1 -1
- package/out/index.html +6 -6
- package/out/scan.js +2 -2
- package/out/view.css +1 -1
- package/out/view.js +7 -7
- package/package.json +3 -1
- package/server-managed/agent-merges.ts +1 -1
- package/server-managed/app-agents.ts +4 -2
- package/server-managed/commit-lockfiles.ts +7 -4
- package/server-managed/comparison.ts +13 -2
- package/server-managed/db-neon.ts +13 -3
- package/server-managed/db.ts +2 -1
- package/server-managed/dependency-review-comments.ts +16 -18
- package/server-managed/dependency-reviews.ts +45 -27
- package/server-managed/github-app.ts +3 -42
- package/server-managed/github-requests.ts +97 -0
- package/server-managed/http.ts +249 -75
- package/server-managed/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +28 -5
- package/server-managed/npm-kept.ts +14 -4
- package/server-managed/npm-patch.ts +325 -0
- package/server-managed/pull-dependencies.ts +9 -6
- package/server-managed/pull-dependency-reviews.ts +19 -13
- package/server-managed/repository-agents.ts +162 -69
- package/server-managed/repository-dependency-reviews.ts +6 -5
- package/server-managed/repository-pulls.ts +114 -58
- package/server-managed/repository-pulse.ts +1 -1
- package/server-managed/reviewed-dependencies.ts +4 -5
- package/server-managed/team-reports.ts +19 -7
- package/out/chunk-6S2U2UYG.js +0 -232
- package/out/chunk-FC65TPUB.js +0 -1
package/server-managed/http.ts
CHANGED
|
@@ -101,12 +101,15 @@
|
|
|
101
101
|
// POST /api/admin/teams/{set,remove}-member → admin links/unlinks a user (+perms) | 401/403/404
|
|
102
102
|
// POST /api/admin/teams/set-dependency-review → admin toggles dependency review publication for a team | 400/401/403/404
|
|
103
103
|
// POST /api/admin/teams/set-npm-scopes → admin replaces a team's npm scopes | 400/401/403/404
|
|
104
|
-
// GET/POST /api/npm/reviews → read private/team reviews; save, publish, revoke or continue a set
|
|
104
|
+
// GET/POST /api/npm/reviews → read private/team reviews; save, publish, revoke or continue a set; a patched version's
|
|
105
|
+
// (patchHash) changed with where its patch is (patchRepoId, patchCommit, patchPath) | 400/401/403/404/409/422
|
|
105
106
|
// POST /api/npm/reviews/discard → author discards an unpublished review and its file rows | 400/401/403/409
|
|
106
107
|
// GET /api/npm/reviews/latest → latest own review for exact target bytes, across all bases | 400/401/403
|
|
107
108
|
// GET /api/npm/reviews/team?teamId=&beforeTime=&beforeId= → the reviews published to a team, the latest first, for one who
|
|
108
109
|
// reads them there | 400/401/404
|
|
109
110
|
// GET /api/npm/package?name=&version= → a published version's manifest and files | 400/401/404/413/429/502
|
|
111
|
+
// GET /api/npm/package?name=&version=&patchHash=&patchRepoId=&patchCommit=&patchPath= → its files as a pnpm patch of a
|
|
112
|
+
// repository the reader reads all of changes them | 400/401/403/404/409/413/422/429/502
|
|
110
113
|
// GET /api/npm/versions?name= → its dist-tags, versions, when each was published and its first was, and whether a team lists its scope | 400/401/404/429/502
|
|
111
114
|
// GET /api/npm/download?name=&version= → its tarball | 400/401/404/413/429/502
|
|
112
115
|
// GET /api/npm/stats?name= → its downloads over the last year and its GitHub repository's figures | 400/401/404/429/502
|
|
@@ -135,8 +138,8 @@ import { mergeLinkGroups } from './link-reports.ts'
|
|
|
135
138
|
import { backfillFindingIds, reportEntries } from '@preventive/report'
|
|
136
139
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
137
140
|
import { pipeline } from 'node:stream/promises'
|
|
138
|
-
import { type KeptStore, REPOSITORY_READ_KEPT_MS, codeHeadStore, codeTreeStore, commitPullStore, historyChangeStore, historyPageStore, historySquashStore, historyTreeStore,
|
|
139
|
-
lockfileReadStore,
|
|
141
|
+
import { type KeptStore, REPOSITORY_READ_KEPT_MS, authoredCommitStore, branchTipStore, codeHeadStore, codeTreeStore, commitPullStore, historyChangeStore, historyPageStore, historySquashStore, historyTreeStore,
|
|
142
|
+
issueListStore, lockfileReadStore,
|
|
140
143
|
npmDeltaStore, npmPublishTimeStore, npmVersionStore, ownKeptPrunes, pullDependencyStore, pullListStore, repositoryReadStore, reviewBaselineStore, riskLineStore, scanReadStore } from './npm-kept.ts'
|
|
141
144
|
import { plainObject } from './npm-manifest.ts'
|
|
142
145
|
import { type NpmDelta, keptNpmDeltas, npmDelta } from './npm-delta.ts'
|
|
@@ -213,13 +216,14 @@ import { ManagedMutationError, reportReferenceSnapshot } from './management.ts'
|
|
|
213
216
|
import { BundleBuildError, buildRepositoryBundle, parseBundleBuild, withBundleBuildLease } from './bundle-build.ts'
|
|
214
217
|
import { NpmPackageError, type NpmReader, type NpmVersionDocument, type NpmVersionStore, VERSION_KEPT_MS, canReadPrivateNpm, npmFileRows, npmPublishTime, npmScopesCount, npmTarballFilename, readNpmVersion, readNpmVersions, readTogether } from './npm-packages.ts'
|
|
215
218
|
import { loadNpmFiles, loadNpmPackageBody, loadNpmTarball } from './npm-loads.ts'
|
|
219
|
+
import { type NpmPatch, type PatchedFile, patchedFiles, readPatch, requestedPatch } from './npm-patch.ts'
|
|
216
220
|
import { type RepositoryCodeKept, loadRepositoryCodeBody, repositoryCodeFits, repositoryCodeKept } from './repository-code.ts'
|
|
217
221
|
import { type ChangesStore, type CommitChanges, type CommitTree, TREES_READ, changesAsked, changesWithin, directoryFiles, repositoryChanges } from './repository-changes.ts'
|
|
218
222
|
import { COAUTHORS_READ, coauthorsOf } from './commit-coauthors.ts'
|
|
219
223
|
import { type AllHeads, type HistoryCommit, type HistoryPages, MAX_HISTORY_PAGE, historyFrom, historyStarts, openPullHeads,
|
|
220
224
|
repositoryAllHeads, repositoryAllHistory, repositoryHistory, repositoryRefTips } from './repository-history.ts'
|
|
221
225
|
import { type KnownMerges, type SquashedPull, type Squashes, squashedPulls } from './repository-squashes.ts'
|
|
222
|
-
import { AGENTS_READ_VERSION, COMMITS_CURSOR, type CommitPulls, repositoryAgents } from './repository-agents.ts'
|
|
226
|
+
import { AGENTS_READ_VERSION, type AuthoredCommit, type BranchTip, type BranchTips, COMMITS_CURSOR, type CommitPulls, repositoryAgents } from './repository-agents.ts'
|
|
223
227
|
import { ISSUES_CURSOR, ISSUES_READ_VERSION, repositoryIssues, repositoryItem } from './repository-issues.ts'
|
|
224
228
|
import { type GithubAsked, type ListedLockfile, type LockfileRead, type LockfileStore, MAX_LOCKED, PULL_DEPENDENCIES_READ, type PullCommits, type PullDependencies,
|
|
225
229
|
type PullDependencyStore, eachOf, lockfileFormat, pullDependenciesAt } from './pull-dependencies.ts'
|
|
@@ -230,10 +234,12 @@ import { type ReviewBaselineStore, baselineFor, heldDependencies, reviewedDepend
|
|
|
230
234
|
import { formattedPull, withFormatted } from './dependency-review-comments.ts'
|
|
231
235
|
import type { ReviewedDependencies } from './reviewed-dependencies.ts'
|
|
232
236
|
import { commentReviews } from './pull-comment-reviews.ts'
|
|
233
|
-
import { PULSE_WEEKS, PULSE_YEAR_VERSION, type PulseStats, StatsPending, commitStats, defaultHead, periodStats, pulseSince,
|
|
237
|
+
import { PULSE_WEEKS, PULSE_YEAR_VERSION, type PulseNarrowing, type PulseStats, StatsPending, commitStats, defaultHead, periodStats, pulseSince,
|
|
238
|
+
repositoryActivity, repositoryLines,
|
|
234
239
|
repositoryStats, withLines, yearCommits } from './repository-pulse.ts'
|
|
235
240
|
import { type AgentMerges, agentCommits, agentMerges, directCommits, sameMerges } from './agent-merges.ts'
|
|
236
|
-
import {
|
|
241
|
+
import { listedActivity } from './listed-activity.ts'
|
|
242
|
+
import { ISSUES_LIST_READ_VERSION, type IssueList, type Listed, PULLS_READ_VERSION, type PullList, pullsReadTo, readIssues, readPulls } from './repository-pulls.ts'
|
|
237
243
|
import { AGENT_MARKS_READ } from './app-agents.ts'
|
|
238
244
|
import { releaseCommits } from './release-commits.ts'
|
|
239
245
|
import { computeBundleDiff } from '../common/bundle-compare-diff.js'
|
|
@@ -246,8 +252,8 @@ import { NpmRiskError } from '../risk/index.js'
|
|
|
246
252
|
import { type PrettyCache, PrettyError, type PrettyRequest, compared, prettyRequest, prettyText } from './pretty-print.ts'
|
|
247
253
|
import { renameDiffOf, renameDiffRequest, renamedPairs } from './rename-diff.ts'
|
|
248
254
|
import { typeOnlyFiles } from './type-only.ts'
|
|
249
|
-
import { comparisonBody, emptySide, npmComparedSide } from './comparison.ts'
|
|
250
|
-
import { isNpmPackageName, isNpmPackageSpec, npmPackageScope } from '../common/managed/npm-packages.js'
|
|
255
|
+
import { comparisonBody, emptySide, npmComparedSide, patchedVersionBody } from './comparison.ts'
|
|
256
|
+
import { isNpmPackageName, isNpmPackageSpec, isPatchHash, npmPackageScope } from '../common/managed/npm-packages.js'
|
|
251
257
|
|
|
252
258
|
const SESSION_PATH = '/api/auth/session'
|
|
253
259
|
const PROFILE_PATH = '/api/auth/profile'
|
|
@@ -1525,18 +1531,24 @@ type RepositoryReader = Awaited<ReturnType<ReturnType<typeof createRepositoryBro
|
|
|
1525
1531
|
type RepositoryContents = Awaited<ReturnType<typeof readRepositoryContents>>
|
|
1526
1532
|
type RepositoryRefs = Awaited<ReturnType<RepositoryReader['refs']>> & { defaultContents?: RepositoryContents | null }
|
|
1527
1533
|
|
|
1528
|
-
|
|
1534
|
+
// A directory's listing at a commit (`reader.directory`), which never changes,
|
|
1535
|
+
// as kept by them (repositoryTreesOf `listings`), else read.
|
|
1536
|
+
type DirectoryListing = Awaited<ReturnType<RepositoryReader['directory']>>
|
|
1537
|
+
type ListedAt = (commit: string, path: string, read: () => Promise<DirectoryListing>) => Promise<DirectoryListing>
|
|
1538
|
+
|
|
1539
|
+
async function readRepositoryContents(reader: RepositoryReader, ref: string, path: string, virtualEntries: RepositoryEntry[] | null, listed: ListedAt) {
|
|
1529
1540
|
const commit = await reader.commit(ref)
|
|
1530
|
-
const contents = virtualEntries ? { entries: virtualEntries, limited: false } : await reader.directory(path, commit)
|
|
1541
|
+
const contents = virtualEntries ? { entries: virtualEntries, limited: false } : await listed(commit, path, () => reader.directory(path, commit))
|
|
1531
1542
|
return { ...contents, path, commit }
|
|
1532
1543
|
}
|
|
1533
1544
|
|
|
1534
|
-
async function readRepositoryRefs(db: ManagedDb, reader: RepositoryReader, repo: SelectedRepo, withDefault: boolean, virtualEntries: RepositoryEntry[] | null
|
|
1545
|
+
async function readRepositoryRefs(db: ManagedDb, reader: RepositoryReader, repo: SelectedRepo, withDefault: boolean, virtualEntries: RepositoryEntry[] | null,
|
|
1546
|
+
listed: ListedAt): Promise<RepositoryRefs> {
|
|
1535
1547
|
// Wait for both, including failures: a deleted cached branch must not prevent
|
|
1536
1548
|
// a successful live default lookup from recovering the initial directory.
|
|
1537
1549
|
const [refs, contents] = await Promise.allSettled([
|
|
1538
1550
|
reader.refs(),
|
|
1539
|
-
withDefault && repo.cachedDefaultBranch ? readRepositoryContents(reader, `heads/${repo.cachedDefaultBranch}`, '', virtualEntries) : Promise.resolve(null),
|
|
1551
|
+
withDefault && repo.cachedDefaultBranch ? readRepositoryContents(reader, `heads/${repo.cachedDefaultBranch}`, '', virtualEntries, listed) : Promise.resolve(null),
|
|
1540
1552
|
])
|
|
1541
1553
|
if (refs.status === 'rejected') throw refs.reason
|
|
1542
1554
|
const defaultBranch = refs.value.defaultBranch || null
|
|
@@ -1544,7 +1556,7 @@ async function readRepositoryRefs(db: ManagedDb, reader: RepositoryReader, repo:
|
|
|
1544
1556
|
if (changed) await db.cacheRepoDefaultBranch(repo, defaultBranch)
|
|
1545
1557
|
if (!withDefault) return refs.value
|
|
1546
1558
|
if (!defaultBranch) return { ...refs.value, defaultContents: null }
|
|
1547
|
-
if (changed) return { ...refs.value, defaultContents: await readRepositoryContents(reader, `heads/${defaultBranch}`, '', virtualEntries) }
|
|
1559
|
+
if (changed) return { ...refs.value, defaultContents: await readRepositoryContents(reader, `heads/${defaultBranch}`, '', virtualEntries, listed) }
|
|
1548
1560
|
if (contents.status === 'rejected') throw contents.reason
|
|
1549
1561
|
return { ...refs.value, defaultContents: contents.value }
|
|
1550
1562
|
}
|
|
@@ -1613,6 +1625,9 @@ async function handleRepositoryBrowser(res: ServerResponse, deps: ManagedHttpDep
|
|
|
1613
1625
|
if (!asked) return
|
|
1614
1626
|
const { repoId, ref } = asked
|
|
1615
1627
|
const withDefault = refs && query.get('withDefault') === 'true'
|
|
1628
|
+
// The commit alone (`listing=none`), as a view reading none of its entries
|
|
1629
|
+
// opens at.
|
|
1630
|
+
const commitOnly = !refs && query.get('listing') === 'none'
|
|
1616
1631
|
const path = refs ? '' : asked.path
|
|
1617
1632
|
const authorize = async (handed = false) => {
|
|
1618
1633
|
const access = await authorizeRepository(res, deps, cookie, repoId, path, { handed })
|
|
@@ -1626,8 +1641,10 @@ async function handleRepositoryBrowser(res: ServerResponse, deps: ManagedHttpDep
|
|
|
1626
1641
|
const reader = await browser.reader(access.repo)
|
|
1627
1642
|
// Microseconds, so overlapping browses practically never share a listing time.
|
|
1628
1643
|
const observedUs = Math.round((performance.timeOrigin + performance.now()) * 1000)
|
|
1629
|
-
const
|
|
1630
|
-
const
|
|
1644
|
+
const trees = repositoryTreesOf(deps)
|
|
1645
|
+
const listedAt: ListedAt = (commit, at, read) => trees.listing(`${repoId}\0${LISTING_READ}\0${commit}\0${at}`, read)
|
|
1646
|
+
const revisions = refs ? await readRepositoryRefs(deps.db, reader, access.repo, withDefault, access.virtualEntries, listedAt) : null
|
|
1647
|
+
const contents = refs ? null : commitOnly ? { path, commit: await reader.commit(ref) } : await readRepositoryContents(reader, ref, path, access.virtualEntries, listedAt)
|
|
1631
1648
|
const { tagCommits, tagsComplete, ...listed } = revisions ?? { tagCommits: [], tagsComplete: false }
|
|
1632
1649
|
// Every refs read (Create a bundle's, and the code viewer's) refreshes the
|
|
1633
1650
|
// tags bundle catalogs show, and a tag read keeps the one its revision
|
|
@@ -1644,7 +1661,7 @@ async function handleRepositoryBrowser(res: ServerResponse, deps: ManagedHttpDep
|
|
|
1644
1661
|
if (!sameRepository(current.repo, access.repo)) { sendJson(res, 409, { error: 'repository-changed' }); return }
|
|
1645
1662
|
sendJson(res, 200, refs ? { ...listed, tagCommits, ...(withDefault ? {
|
|
1646
1663
|
defaultContents: scopeRepositoryContents(revisions?.defaultContents ?? null, current.virtualEntries),
|
|
1647
|
-
} : {}) } : scopeRepositoryContents(contents, current.virtualEntries))
|
|
1664
|
+
} : {}) } : commitOnly ? contents : scopeRepositoryContents(contents as RepositoryContents, current.virtualEntries))
|
|
1648
1665
|
} catch (err) {
|
|
1649
1666
|
if (err instanceof GithubApiError) { sendGithubError(res, err); return }
|
|
1650
1667
|
throw err
|
|
@@ -1669,19 +1686,27 @@ async function handleRepositoryBrowser(res: ServerResponse, deps: ManagedHttpDep
|
|
|
1669
1686
|
// the reader's own access has named the commit, as GitHub would have
|
|
1670
1687
|
// answered them; held by each instance a while, by the database it has, and
|
|
1671
1688
|
// kept in that database for every instance (npm-kept.ts codeTreeStore).
|
|
1689
|
+
// And a directory's listing at a commit (readRepositoryContents), by the
|
|
1690
|
+
// repository's id, the rules it is read by (LISTING_READ), the commit and the
|
|
1691
|
+
// directory, fewer held, each the larger.
|
|
1672
1692
|
const REPOSITORY_TREES_KEPT = 4096
|
|
1693
|
+
const REPOSITORY_LISTINGS_HELD = 256
|
|
1694
|
+
const LISTING_READ = 1
|
|
1673
1695
|
type TreeFiles = Awaited<ReturnType<typeof repositoryTreeFiles>>
|
|
1674
|
-
const repositoryTrees = new WeakMap<ManagedDb, { ids: Lru<string>; files: Lru<TreeFiles> }>()
|
|
1696
|
+
const repositoryTrees = new WeakMap<ManagedDb, { ids: Lru<string>; files: Lru<TreeFiles>; listings: Lru<DirectoryListing> }>()
|
|
1675
1697
|
const treeId = (value: unknown): value is string => typeof value === 'string' && /^[a-f\d]{40}$/u.test(value)
|
|
1676
1698
|
const treeFiles = (value: unknown): value is TreeFiles => value === null || (plainObject(value)
|
|
1677
1699
|
&& Number.isSafeInteger(value['files']) && (value['files'] as number) >= 0 && Number.isSafeInteger(value['bytes']) && (value['bytes'] as number) >= 0)
|
|
1700
|
+
const directoryListing = (value: unknown): value is DirectoryListing => plainObject(value) && Array.isArray(value['entries']) && typeof value['limited'] === 'boolean'
|
|
1678
1701
|
function repositoryTreesOf(deps: ManagedHttpDeps) {
|
|
1679
1702
|
let held = repositoryTrees.get(deps.db)
|
|
1680
|
-
if (!held) repositoryTrees.set(deps.db, held = { ids: lru(REPOSITORY_TREES_KEPT), files: lru(REPOSITORY_TREES_KEPT) })
|
|
1681
|
-
const { ids, files } = held
|
|
1703
|
+
if (!held) repositoryTrees.set(deps.db, held = { ids: lru(REPOSITORY_TREES_KEPT), files: lru(REPOSITORY_TREES_KEPT), listings: lru(REPOSITORY_LISTINGS_HELD) })
|
|
1704
|
+
const { ids, files, listings } = held
|
|
1682
1705
|
return {
|
|
1683
1706
|
id: (key: string, read: () => Promise<string>) => keptTree(ids, codeTreeStore<string>(deps.db, 'trees', deps.config.debug), key, treeId, read),
|
|
1684
1707
|
files: (key: string, read: () => Promise<TreeFiles>) => keptTree(files, codeTreeStore<TreeFiles>(deps.db, 'tree-files', deps.config.debug), key, treeFiles, read),
|
|
1708
|
+
listing: (key: string, read: () => Promise<DirectoryListing>) =>
|
|
1709
|
+
keptTree(listings, codeTreeStore<DirectoryListing>(deps.db, 'listings', deps.config.debug), key, directoryListing, read),
|
|
1685
1710
|
}
|
|
1686
1711
|
}
|
|
1687
1712
|
// What `memory` holds by `key`, else what `store` keeps of it as `kept`
|
|
@@ -2006,8 +2031,9 @@ const REPOSITORY_READ_FLOOR_MS = 10_000
|
|
|
2006
2031
|
// How long a history's answer waits on GitHub looking up the pull requests
|
|
2007
2032
|
// squashed into its commits (handleRepositoryHistory).
|
|
2008
2033
|
const SQUASH_WAIT_MS = 1500
|
|
2009
|
-
// The repositories' reads being read again, behind a kept answer or
|
|
2010
|
-
// for afresh, by key: one at a time, those asking meanwhile
|
|
2034
|
+
// The repositories' reads being read, first or again, behind a kept answer or
|
|
2035
|
+
// as asked for afresh, by key: one at a time, those asking meanwhile
|
|
2036
|
+
// answered by it.
|
|
2011
2037
|
const repositoryRereads = new Map<string, Promise<unknown>>()
|
|
2012
2038
|
// A read GitHub refused, not failed: its credential or the GitHub App's
|
|
2013
2039
|
// permission for it withdrawn (Issues, Pull requests, Checks), the
|
|
@@ -2023,7 +2049,8 @@ const refusedRead = (err: unknown) => err instanceof GithubApiError
|
|
|
2023
2049
|
// deps.waitUntil); `kept` where it was, or where reading it again
|
|
2024
2050
|
// failed as `keepThrough` lets it. One asked for afresh (`fresh`) is read
|
|
2025
2051
|
// now, unless it was read REPOSITORY_READ_FLOOR_MS since, what was kept
|
|
2026
|
-
// answered where that read fails.
|
|
2052
|
+
// answered where that read fails. Those asking for one not kept while it is
|
|
2053
|
+
// read, as a tab's first readers do together, are answered by that read. One GitHub refuses to read again is no
|
|
2027
2054
|
// longer kept: the next to ask is refused as GitHub refused it. `at` is when
|
|
2028
2055
|
// what it answers was read.
|
|
2029
2056
|
async function keptAnswer<T extends object>(deps: ManagedHttpDeps, key: string, read: () => Promise<T>,
|
|
@@ -2065,7 +2092,7 @@ async function keptAnswer<T extends object>(deps: ManagedHttpDeps, key: string,
|
|
|
2065
2092
|
return { value: held.value as T, kept: true, at: held.at }
|
|
2066
2093
|
}
|
|
2067
2094
|
try {
|
|
2068
|
-
return { ...
|
|
2095
|
+
return { ...await reread(), kept: false }
|
|
2069
2096
|
} catch (err) {
|
|
2070
2097
|
if (held && ((now && !refusedRead(err)) || keepThrough(err))) return { value: held.value as T, kept: true, at: held.at }
|
|
2071
2098
|
throw err
|
|
@@ -2117,37 +2144,102 @@ function commitPulls(deps: ManagedHttpDeps, repoId: number): CommitPulls {
|
|
|
2117
2144
|
}
|
|
2118
2145
|
}
|
|
2119
2146
|
|
|
2120
|
-
//
|
|
2121
|
-
//
|
|
2122
|
-
//
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2147
|
+
// Repository `repoId`'s branches' tips as the Agents tab looks them up
|
|
2148
|
+
// (repository-agents.ts BranchTips), kept by the commit and the rules their
|
|
2149
|
+
// messages' marks were read by (app-agents.ts AGENT_MARKS_READ).
|
|
2150
|
+
function branchTips(deps: ManagedHttpDeps, repoId: number): BranchTips {
|
|
2151
|
+
const store = branchTipStore<BranchTip>(deps.db, deps.config.debug)
|
|
2152
|
+
const keyOf = (sha: string) => `${repoId}\0${AGENT_MARKS_READ}\0${sha}`
|
|
2153
|
+
return {
|
|
2154
|
+
async read(shas) {
|
|
2155
|
+
const kept = await store.readMany(shas.map(keyOf))
|
|
2156
|
+
return new Map(shas.flatMap(sha => { const tip = kept.get(keyOf(sha))?.value; return tip ? [[sha, tip] as const] : [] }))
|
|
2157
|
+
},
|
|
2158
|
+
write: (sha, tip) => store.write(keyOf(sha), tip, Date.now()),
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2127
2161
|
|
|
2128
|
-
//
|
|
2129
|
-
//
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2132
|
-
|
|
2162
|
+
// The commits repository `repoId`'s app agents' accounts authored from a
|
|
2163
|
+
// head, as the Agents tab reads them (repository-agents.ts AuthoredCommits),
|
|
2164
|
+
// kept by it and the rules their messages' marks were read by
|
|
2165
|
+
// (AGENT_MARKS_READ).
|
|
2166
|
+
function authoredCommits(deps: ManagedHttpDeps, repoId: number) {
|
|
2167
|
+
const store = authoredCommitStore<AuthoredCommit[]>(deps.db, deps.config.debug)
|
|
2168
|
+
const keyOf = (key: string) => `${repoId}\0${AGENT_MARKS_READ}\0${key}`
|
|
2169
|
+
return {
|
|
2170
|
+
read: async (key: string) => { const kept = (await store.read(keyOf(key)))?.value; return Array.isArray(kept) ? kept : null },
|
|
2171
|
+
write: (key: string, commits: AuthoredCommit[]) => store.write(keyOf(key), commits, Date.now()),
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// How long a repository's pull requests or issues kept (keptList) are taken
|
|
2176
|
+
// as they are before those updated since are read: less than their readers
|
|
2177
|
+
// keep their answers (keptAnswer), that one read of them serves them all.
|
|
2178
|
+
const LIST_FRESH_MS = 30_000
|
|
2179
|
+
// Each repository's reads of its pull requests, and of its issues, by them
|
|
2180
|
+
// and its id: one at a time, each after those asked before it.
|
|
2181
|
+
const listReads = new Map<string, Promise<unknown>>()
|
|
2182
|
+
|
|
2183
|
+
// A repository's pull requests or issues, as kept in `store` by `key`
|
|
2184
|
+
// (repository-pulls.ts readList, `read`), read on where they were read
|
|
2185
|
+
// LIST_FRESH_MS ago or longer, or not as far back as `need`: as far back as
|
|
2186
|
+
// it, for the Pulse; for the Agents tab (null), as far back as they were
|
|
2187
|
+
// read, those updated since read (a read not finished, on), else their
|
|
2188
|
+
// first page. Not to be read further back (`further` false): those kept,
|
|
2189
|
+
// none where none are, where they are not read as far as `need`, or a read
|
|
2190
|
+
// of them is not finished, those updated since read alone. One read of a
|
|
2133
2191
|
// repository's at a time, the rest after it, from what it kept.
|
|
2134
|
-
function
|
|
2135
|
-
|
|
2136
|
-
const
|
|
2137
|
-
const reading = (pullsReads.get(repoId) ?? Promise.resolve()).catch(() => {}).then(async () => {
|
|
2192
|
+
function keptList<L extends Listed<unknown>>(store: KeptStore<L>, key: string, read: (since: number, kept: L | null, need: number) => Promise<L>,
|
|
2193
|
+
need: number | null, further = true): Promise<L | null> {
|
|
2194
|
+
const reading = (listReads.get(key) ?? Promise.resolve()).catch(() => {}).then(async () => {
|
|
2138
2195
|
const kept = await store.read(key)
|
|
2139
2196
|
const list = kept?.value ?? null
|
|
2140
|
-
if (list && Date.now() - kept!.at <
|
|
2197
|
+
if (list && Date.now() - kept!.at < LIST_FRESH_MS && (need === null || pullsReadTo(list) <= need)) return list
|
|
2198
|
+
if (!further && need !== null && (!list || list.next || pullsReadTo(list) > need)) return list
|
|
2141
2199
|
const since = pulseSince(52, Date.now())
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
return read
|
|
2200
|
+
const listed = await read(since, list, need ?? (list ? list.next ? since : list.reach ?? since : Date.now()))
|
|
2201
|
+
await store.write(key, listed, Date.now())
|
|
2202
|
+
return listed
|
|
2146
2203
|
})
|
|
2147
|
-
|
|
2148
|
-
void reading.catch(() => {}).finally(() => { if (
|
|
2204
|
+
listReads.set(key, reading)
|
|
2205
|
+
void reading.catch(() => {}).finally(() => { if (listReads.get(key) === reading) listReads.delete(key) })
|
|
2149
2206
|
return reading
|
|
2150
2207
|
}
|
|
2208
|
+
// Repository `repoId`'s pull requests (keptList), kept by how agents are
|
|
2209
|
+
// told too; those kept alone, not read further back (`further` false).
|
|
2210
|
+
function keptPulls(deps: ManagedHttpDeps, repoId: number, fullName: string, token: string, need: number | null): Promise<PullList>
|
|
2211
|
+
function keptPulls(deps: ManagedHttpDeps, repoId: number, fullName: string, token: string, need: number, further: false): Promise<PullList | null>
|
|
2212
|
+
function keptPulls(deps: ManagedHttpDeps, repoId: number, fullName: string, token: string, need: number | null, further = true) {
|
|
2213
|
+
return keptList(pullListStore<PullList>(deps.db, deps.config.debug), `${repoId}\0${PULLS_READ_VERSION}\0${AGENTS_READ_VERSION}\0${PULSE_YEAR_VERSION}`,
|
|
2214
|
+
(since, kept, asked) => readPulls(fullName, token, since, kept, globalThis.fetch, asked), need, further)
|
|
2215
|
+
}
|
|
2216
|
+
// Repository `repoId`'s issues (keptList).
|
|
2217
|
+
const keptIssues = (deps: ManagedHttpDeps, repoId: number, fullName: string, token: string, need: number | null) =>
|
|
2218
|
+
keptList(issueListStore<IssueList>(deps.db, deps.config.debug), `${repoId}\0${ISSUES_LIST_READ_VERSION}`,
|
|
2219
|
+
(since, kept, asked) => readIssues(fullName, token, since, kept, globalThis.fetch, asked), need) as Promise<IssueList>
|
|
2220
|
+
|
|
2221
|
+
// What is kept by `key` (keptAnswer), read now where it was read
|
|
2222
|
+
// REPOSITORY_READ_FRESH_MS ago or longer, not behind: for an answer that is
|
|
2223
|
+
// kept as fresh itself, as told of it (the Agents tab's).
|
|
2224
|
+
async function currentAnswer<T extends object>(deps: ManagedHttpDeps, key: string, read: () => Promise<T>) {
|
|
2225
|
+
const stored = await repositoryReadStore(deps.db, deps.config.debug).read(key)
|
|
2226
|
+
return keptAnswer(deps, key, read, undefined, !stored || Date.now() - stored.at >= REPOSITORY_READ_FRESH_MS)
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// Repository `repoId`'s default branch's head, as its tabs read it: as the
|
|
2230
|
+
// Commits tab's branches last told it (`heads`), where they were read
|
|
2231
|
+
// fresh, else read alone and kept (`pulse-head`), read now where that is no
|
|
2232
|
+
// longer fresh as asked (`current`); whether it was kept (`kept`), GitHub
|
|
2233
|
+
// asked for none of it.
|
|
2234
|
+
async function defaultTip(deps: ManagedHttpDeps, repoId: number, fullName: string, token: string, current = false): Promise<{ sha: string | null; kept: boolean }> {
|
|
2235
|
+
const heads = await repositoryReadStore(deps.db, deps.config.debug).read(`${repoId}\0heads`)
|
|
2236
|
+
const all = heads && Date.now() - heads.at < REPOSITORY_READ_FRESH_MS ? heads.value as AllHeads : null
|
|
2237
|
+
const tip = all?.heads.find(head => head.name === all.defaultBranch)?.sha
|
|
2238
|
+
if (typeof tip === 'string' && /^[a-f\d]{40}$/u.test(tip)) return { sha: tip, kept: true }
|
|
2239
|
+
const head = async () => ({ sha: await defaultHead(fullName, token) })
|
|
2240
|
+
const read = await (current ? currentAnswer(deps, `${repoId}\0pulse-head`, head) : keptAnswer(deps, `${repoId}\0pulse-head`, head))
|
|
2241
|
+
return { sha: read.value.sha, kept: read.kept }
|
|
2242
|
+
}
|
|
2151
2243
|
|
|
2152
2244
|
// What the Pulse keeps of repository `repoId`'s pull requests merged, as
|
|
2153
2245
|
// it tells by them whose its year's commits are (pulseCommits, agentMerges).
|
|
@@ -2264,6 +2356,22 @@ async function readWholeRepositoryOf<T>(deps: ManagedHttpDeps, access: WholeAcce
|
|
|
2264
2356
|
}
|
|
2265
2357
|
}
|
|
2266
2358
|
|
|
2359
|
+
// The patch `patch` names (npm-patch.ts), read of the whole of its repository
|
|
2360
|
+
// as readWholeRepositoryOf reads it, with its reader's own GitHub access, and
|
|
2361
|
+
// held to its hash: as kept, else with that access checked again after; else
|
|
2362
|
+
// why it was refused. Its failing to read or to apply throws.
|
|
2363
|
+
async function npmPatchOf(deps: ManagedHttpDeps, cookie: string | undefined, patch: NpmPatch): Promise<{ files: PatchedFile[] } | Refusal> {
|
|
2364
|
+
const access = await wholeRepositoryAccess(deps, cookie, patch.repoId)
|
|
2365
|
+
if ('error' in access) return access
|
|
2366
|
+
const read = await readWholeRepositoryOf(deps, access, async (fullName, token) => {
|
|
2367
|
+
const asked: GithubAsked = { github: false }
|
|
2368
|
+
return { value: await readPatch(fullName, token, patch, asked), kept: !asked.github }
|
|
2369
|
+
}, () => wholeRepositoryAccess(deps, cookie, patch.repoId))
|
|
2370
|
+
return 'error' in read ? read : { files: read.value }
|
|
2371
|
+
}
|
|
2372
|
+
// The patch a request's fields name, as npm-patch.ts requestedPatch takes them.
|
|
2373
|
+
const patchFields = (field: (key: string) => unknown) => requestedPatch({ hash: field('patchHash'), repoId: field('patchRepoId'), commit: field('patchCommit'), path: field('patchPath') })
|
|
2374
|
+
|
|
2267
2375
|
// GET /api/admin/repositories/issues: what the code viewer's Issues and Pull
|
|
2268
2376
|
// requests tabs show (repository-issues.ts): `kind`'s open or closed
|
|
2269
2377
|
// (`state`), a page at a time, on from the cursor `after` names. They are
|
|
@@ -2298,7 +2406,10 @@ async function handleRepositoryAgents(res: ServerResponse, deps: ManagedHttpDeps
|
|
|
2298
2406
|
sendJson(res, 400, { error: 'bad-request' }); return
|
|
2299
2407
|
}
|
|
2300
2408
|
await readWholeRepository(res, deps, cookie, repoId, (fullName, token) => repositoryAgents(fullName, token, cursors, historyPages(deps, repoId, true, true),
|
|
2301
|
-
commitPulls(deps, repoId), () => keptPulls(deps, repoId, fullName, token, null)),
|
|
2409
|
+
commitPulls(deps, repoId), { pulls: () => keptPulls(deps, repoId, fullName, token, null), issues: () => keptIssues(deps, repoId, fullName, token, null),
|
|
2410
|
+
head: async () => (await defaultTip(deps, repoId, fullName, token, true)).sha,
|
|
2411
|
+
heads: async () => (await currentAnswer<AllHeads>(deps, `${repoId}\0heads`, () => repositoryAllHeads(fullName, token))).value,
|
|
2412
|
+
tips: branchTips(deps, repoId), authored: authoredCommits(deps, repoId) }),
|
|
2302
2413
|
`agents\0${AGENTS_READ_VERSION}\0${cursors.pulls ?? ''}\0${cursors.commits ?? ''}`)
|
|
2303
2414
|
}
|
|
2304
2415
|
|
|
@@ -2416,8 +2527,8 @@ async function handleRepositoryBranchDependencies(res: ServerResponse, deps: Man
|
|
|
2416
2527
|
// setting as read already (`known`), where it was.
|
|
2417
2528
|
async function branchDependencyAnswer(deps: ManagedHttpDeps, fullName: string, token: string, user: StoredUser, repoId: number,
|
|
2418
2529
|
known?: RepoDependencyReview | null): Promise<{ value: object; kept: boolean }> {
|
|
2419
|
-
const headRead = await
|
|
2420
|
-
const head = headRead.
|
|
2530
|
+
const headRead = await defaultTip(deps, repoId, fullName, token)
|
|
2531
|
+
const head = headRead.sha
|
|
2421
2532
|
if (!head) return { value: { head: null, review: null }, kept: false }
|
|
2422
2533
|
const asked: GithubAsked = { github: !headRead.kept }
|
|
2423
2534
|
// Those its review holds to alone read, as many as are read of them.
|
|
@@ -2447,7 +2558,7 @@ async function handleRepositoryLockfiles(res: ServerResponse, deps: ManagedHttpD
|
|
|
2447
2558
|
const repoId = Number(query.get('repoId'))
|
|
2448
2559
|
if (!Number.isSafeInteger(repoId) || repoId <= 0) { sendJson(res, 400, { error: 'bad-request' }); return }
|
|
2449
2560
|
await answerWholeRepository(res, deps, cookie, repoId, async (fullName, token) => {
|
|
2450
|
-
const head = (await
|
|
2561
|
+
const head = (await defaultTip(deps, repoId, fullName, token)).sha
|
|
2451
2562
|
return { value: { head, lockfiles: head ? (await lockfilesAt(fullName, token, head, lockfileKept(deps, repoId))).map(({ path, format }) => ({ path, format })) : [] }, kept: false }
|
|
2452
2563
|
})
|
|
2453
2564
|
}
|
|
@@ -2603,18 +2714,19 @@ async function teamOpenPulls(deps: ManagedHttpDeps, repoId: number, fullName: st
|
|
|
2603
2714
|
}
|
|
2604
2715
|
|
|
2605
2716
|
// The lines the versions wanting reviews of `answers` change, as their rows
|
|
2606
|
-
// show them (npm-delta.js deltaBase), where they are
|
|
2607
|
-
// public versions alone, those npm answered for in a day
|
|
2608
|
-
// their integrities those npmDelta keys them by. `[name,
|
|
2609
|
-
// delta]` each, none counted here.
|
|
2717
|
+
// show them (npm-delta.js deltaBase), a patched one's none, where they are
|
|
2718
|
+
// counted already: of public versions alone, those npm answered for in a day
|
|
2719
|
+
// (npmVersionStore), their integrities those npmDelta keys them by. `[name,
|
|
2720
|
+
// version, base, delta]` each, none counted here.
|
|
2610
2721
|
async function teamDeltas(deps: ManagedHttpDeps, answers: object[]) {
|
|
2611
2722
|
const asked = new Map<string, [string, string, string]>()
|
|
2612
2723
|
for (const answer of answers as { lockfiles?: { added?: unknown; unreviewed?: unknown }[]; review?: { required?: number } }[]) {
|
|
2613
2724
|
const required = answer.review?.required
|
|
2614
2725
|
if (!Number.isSafeInteger(required)) continue
|
|
2615
2726
|
for (const pkg of (answer.lockfiles ?? []).flatMap(dir => [...Array.isArray(dir.added) ? dir.added : [], ...Array.isArray(dir.unreviewed) ? dir.unreviewed : []]) as
|
|
2616
|
-
{ name?: unknown; version?: unknown; source?: unknown; was?: unknown; reviews?: unknown; rejected?: unknown; baseline?: unknown; reviewed?: unknown; unsupported?: unknown
|
|
2617
|
-
|
|
2727
|
+
{ name?: unknown; version?: unknown; source?: unknown; was?: unknown; reviews?: unknown; rejected?: unknown; baseline?: unknown; reviewed?: unknown; unsupported?: unknown
|
|
2728
|
+
patch?: unknown }[]) {
|
|
2729
|
+
if (pkg.source !== 'registry' || pkg.patch || typeof pkg.name !== 'string' || typeof pkg.version !== 'string' || !isExactVersion(pkg.version)) continue
|
|
2618
2730
|
if (pkg.unsupported || pkg.baseline || pkg.reviewed || typeof pkg.reviews !== 'number' || (pkg.reviews >= required! && !pkg.rejected)) continue
|
|
2619
2731
|
const was = Array.isArray(pkg.was) ? pkg.was.find(prior => typeof prior === 'string' && prior !== pkg.version) as string | undefined : undefined
|
|
2620
2732
|
asked.set(`${pkg.name}\0${pkg.version}\0${was ?? '(empty)'}`, [pkg.name, pkg.version, was ?? '(empty)'])
|
|
@@ -2719,7 +2831,7 @@ async function handleRepositoryPulse(res: ServerResponse, deps: ManagedHttpDeps,
|
|
|
2719
2831
|
if (!Number.isSafeInteger(repoId) || repoId <= 0 || !PULSE_WEEKS.includes(weeks)) { sendJson(res, 400, { error: 'bad-request' }); return }
|
|
2720
2832
|
await answerWholeRepository(res, deps, cookie, repoId, async (fullName, token) => {
|
|
2721
2833
|
const [period, year] = await Promise.all([
|
|
2722
|
-
keptAnswer(deps, `${repoId}\0pulse\0${weeks}`, () =>
|
|
2834
|
+
keptAnswer(deps, `${repoId}\0pulse\0${weeks}`, () => pulseActivity(deps, repoId, fullName, token, weeks)),
|
|
2723
2835
|
pulseCommits(deps, repoId, fullName, token, weeks),
|
|
2724
2836
|
])
|
|
2725
2837
|
return { value: { weeks, ...period.value, stats: year.stats && periodStats(year.stats, weeks), reading: year.reading, pulls: year.pulls,
|
|
@@ -2761,6 +2873,25 @@ const pulseDay = (value: string | null) => {
|
|
|
2761
2873
|
return Number.isSafeInteger(at) ? at : null
|
|
2762
2874
|
}
|
|
2763
2875
|
|
|
2876
|
+
// The pull requests and issues of a period of `weeks` weeks, narrowed as
|
|
2877
|
+
// asked (`narrowing`), as the Pulse shows them: of those kept
|
|
2878
|
+
// (listed-activity.ts listedActivity), where they are read whole as far
|
|
2879
|
+
// back as the period before it starts, within the year they are read for:
|
|
2880
|
+
// the issues read on as far first; the pull requests as the year's commits
|
|
2881
|
+
// read them (pulseCommits), never further for this; else searched for
|
|
2882
|
+
// (repositoryActivity), as while they are read.
|
|
2883
|
+
async function pulseActivity(deps: ManagedHttpDeps, repoId: number, fullName: string, token: string, weeks: number, narrowing: PulseNarrowing | null = null) {
|
|
2884
|
+
const now = Date.now()
|
|
2885
|
+
// As far back as the period before it starts, and its narrowing's first day.
|
|
2886
|
+
const need = Math.min(pulseSince(2 * weeks, now), narrowing?.from ?? Number.POSITIVE_INFINITY)
|
|
2887
|
+
if (need >= pulseSince(52, now)) {
|
|
2888
|
+
const [pulls, issues] = await Promise.all([keptPulls(deps, repoId, fullName, token, need, false), keptIssues(deps, repoId, fullName, token, need)])
|
|
2889
|
+
const whole = (list: Listed<unknown> | null): list is Listed<unknown> => list !== null && !list.next && pullsReadTo(list) <= need
|
|
2890
|
+
if (whole(pulls) && whole(issues)) return listedActivity(pulls.rows, issues.rows, weeks, now, narrowing)
|
|
2891
|
+
}
|
|
2892
|
+
return repositoryActivity(fullName, token, weeks, now, globalThis.fetch, narrowing)
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2764
2895
|
// GET /api/admin/repositories/pulse/items: the Pulse tab's pull requests and
|
|
2765
2896
|
// issues narrowed to a picked day or week (`from` to `to`, else on from
|
|
2766
2897
|
// `from`) and the GitHub accounts a picked contributor committed as
|
|
@@ -2777,7 +2908,7 @@ async function handleRepositoryPulseItems(res: ServerResponse, deps: ManagedHttp
|
|
|
2777
2908
|
await answerWholeRepository(res, deps, cookie, repoId, async (fullName, token) => {
|
|
2778
2909
|
const narrowing = { from, to, authors }
|
|
2779
2910
|
const items = await keptAnswer(deps, `${repoId}\0pulse-items\0${weeks}\0${from}\0${to ?? ''}\0${narrowing.authors.join(',')}`,
|
|
2780
|
-
() =>
|
|
2911
|
+
() => pulseActivity(deps, repoId, fullName, token, weeks, narrowing))
|
|
2781
2912
|
return { value: { weeks, ...items.value }, kept: items.kept }
|
|
2782
2913
|
})
|
|
2783
2914
|
}
|
|
@@ -2789,17 +2920,17 @@ async function pulseCommits(deps: ManagedHttpDeps, repoId: number, fullName: str
|
|
|
2789
2920
|
// weeks and as many before them, which it is shown beside; the year's,
|
|
2790
2921
|
// all of it.
|
|
2791
2922
|
const need = Math.max(first, pulseSince(2 * weeks, now))
|
|
2792
|
-
const head = await
|
|
2793
|
-
if (!head.
|
|
2923
|
+
const head = await defaultTip(deps, repoId, fullName, token)
|
|
2924
|
+
if (!head.sha) return { stats: commitStats([], first), reading: null, pulls: null, pending: false, kept: head.kept }
|
|
2794
2925
|
const store = repositoryReadStore(deps.db, deps.config.debug)
|
|
2795
|
-
const key = `${repoId}\0pulse-year\0${PULSE_YEAR_VERSION}\0${head.
|
|
2926
|
+
const key = `${repoId}\0pulse-year\0${PULSE_YEAR_VERSION}\0${head.sha}\0${first}`
|
|
2796
2927
|
const lastKey = `${repoId}\0pulse-year-last`
|
|
2797
2928
|
const mergesKey = pulseMergesKey(repoId)
|
|
2798
2929
|
const last = async () => {
|
|
2799
2930
|
const kept = (await store.read(lastKey))?.value as { first: number; stats: PulseStats } | undefined
|
|
2800
2931
|
return kept && first - kept.first <= PULSE_LAST_MS ? kept.stats : null
|
|
2801
2932
|
}
|
|
2802
|
-
const sha = head.
|
|
2933
|
+
const sha = head.sha
|
|
2803
2934
|
// What tells whose the year's commits are, kept as read: the agents' pull
|
|
2804
2935
|
// requests merged (agentMerges), as the pull requests kept tell, read as
|
|
2805
2936
|
// far back as `whole` (keptPulls), and the commits that release the
|
|
@@ -4343,8 +4474,11 @@ async function npmReviewPublicationLimit(deps: ManagedHttpDeps, reader: NpmReade
|
|
|
4343
4474
|
// Reviews are scoped to the bytes actually displayed, including the base:
|
|
4344
4475
|
// each change but a revocation loads both versions' files, of tarballs held
|
|
4345
4476
|
// to their integrity and to their shasum where npm tells one (npm-loads.ts),
|
|
4346
|
-
// and the review keeps those shasums.
|
|
4347
|
-
//
|
|
4477
|
+
// and the review keeps those shasums. A patched version's (`patchHash`) are
|
|
4478
|
+
// its files as its patch changes them, the patch read where the request says
|
|
4479
|
+
// (`patchRepoId`, `patchCommit`, `patchPath`), of the whole of a repository
|
|
4480
|
+
// its reader reads (npmPatchOf), which the review keeps. Access to both
|
|
4481
|
+
// versions is rechecked before returning or saving annotations.
|
|
4348
4482
|
async function handleNpmReviews(req: IncomingMessage, res: ServerResponse, deps: ManagedHttpDeps, cookie: string | undefined, params: URLSearchParams): Promise<void> {
|
|
4349
4483
|
const method = req.method ?? 'GET'
|
|
4350
4484
|
if (!['GET', 'POST'].includes(method)) { send405(res, 'GET, POST'); return }
|
|
@@ -4367,13 +4501,17 @@ async function handleNpmReviews(req: IncomingMessage, res: ServerResponse, deps:
|
|
|
4367
4501
|
const basePackage = field('basePackage') ?? null
|
|
4368
4502
|
const baseVersion = field('baseVersion') ?? null
|
|
4369
4503
|
const baseIntegrity = field('baseIntegrity') ?? null
|
|
4504
|
+
const patchHash = field('patchHash') ?? null
|
|
4505
|
+
// Where its patch is, where it names that: its hash alone where only listed.
|
|
4506
|
+
const patch = patchFields(field)
|
|
4370
4507
|
const validIntegrity = (value: unknown): value is string => typeof value === 'string' && value.length > 0 && value.length <= 1024
|
|
4371
4508
|
if (field('ecosystem') !== 'npm' || !isNpmPackageName(name) || !isNpmPackageSpec(version) || !validIntegrity(integrity)
|
|
4372
4509
|
|| (basePackage !== null || baseVersion !== null || baseIntegrity !== null)
|
|
4373
|
-
&& (!isNpmPackageName(basePackage) || !isNpmPackageSpec(baseVersion) || !validIntegrity(baseIntegrity))
|
|
4510
|
+
&& (!isNpmPackageName(basePackage) || !isNpmPackageSpec(baseVersion) || !validIntegrity(baseIntegrity))
|
|
4511
|
+
|| (patchHash !== null && !isPatchHash(patchHash)) || patch === 'invalid') {
|
|
4374
4512
|
sendJson(res, 400, { error: 'bad-review-context' }); return
|
|
4375
4513
|
}
|
|
4376
|
-
const context: ReviewContext = { ecosystem: 'npm', package: name, version, integrity,
|
|
4514
|
+
const context: ReviewContext = { ecosystem: 'npm', package: name, version, integrity, patchHash: patchHash as string | null,
|
|
4377
4515
|
basePackage: basePackage as string | null, baseVersion: baseVersion as string | null, baseIntegrity: baseIntegrity as string | null }
|
|
4378
4516
|
let change: ReviewChange | null = null
|
|
4379
4517
|
if (mutation) {
|
|
@@ -4409,6 +4547,8 @@ async function handleNpmReviews(req: IncomingMessage, res: ServerResponse, deps:
|
|
|
4409
4547
|
change = { action: 'save', reviewId: reviewId as string | null, revision: revision as number | null, entryId: entryId as string | null,
|
|
4410
4548
|
entry: { file, comment, status: status as ReviewInput['status'] } }
|
|
4411
4549
|
}
|
|
4550
|
+
// Each change but a revocation reads a patched version's patch.
|
|
4551
|
+
if (patchHash !== null && action !== 'revoke' && !patch) { sendJson(res, 400, { error: 'bad-review-context' }); return }
|
|
4412
4552
|
}
|
|
4413
4553
|
const controller = new AbortController()
|
|
4414
4554
|
const onClose = () => { if (!res.writableEnded) controller.abort() }
|
|
@@ -4424,15 +4564,22 @@ async function handleNpmReviews(req: IncomingMessage, res: ServerResponse, deps:
|
|
|
4424
4564
|
|| (base && (base.version !== baseVersion || base.dist.integrity !== baseIntegrity))) {
|
|
4425
4565
|
sendJson(res, 409, { error: 'review-content-changed' }); return
|
|
4426
4566
|
}
|
|
4427
|
-
// Each change but a revocation loads both versions' files
|
|
4428
|
-
// publication is held to them.
|
|
4429
|
-
const
|
|
4567
|
+
// Each change but a revocation loads both versions' files, and a patched
|
|
4568
|
+
// one's patch; a save or a publication is held to them.
|
|
4569
|
+
const reading = change && change.action !== 'revoke'
|
|
4570
|
+
const patchRead = reading && patch ? await npmPatchOf(deps, cookie, patch) : null
|
|
4571
|
+
if (patchRead && 'error' in patchRead) { sendRefusal(res, patchRead); return }
|
|
4572
|
+
const loads = reading ? [doc, ...base ? [base] : []].map(loadNpmFiles) : []
|
|
4430
4573
|
let tarballs: ReviewedTarballs | undefined
|
|
4431
4574
|
try {
|
|
4432
4575
|
const files = await Promise.all(loads.map(load => load.result))
|
|
4433
|
-
if (loads.length > 0)
|
|
4576
|
+
if (loads.length > 0) {
|
|
4577
|
+
tarballs = { shasum: doc.dist.shasum, baseShasum: base?.dist.shasum ?? null,
|
|
4578
|
+
...patchRead && patch ? { patch: { repoId: patch.repoId, commit: patch.commit, path: patch.path } } : {} }
|
|
4579
|
+
}
|
|
4434
4580
|
if (change?.action === 'publish' || change?.action === 'save') {
|
|
4435
|
-
const maps = files.map(list => new Map<string, unknown>(npmFileRows(
|
|
4581
|
+
const maps = files.map((list, at) => new Map<string, unknown>(npmFileRows(at === 0 && patchRead ? patchedFiles(list, patchRead.files) : list)
|
|
4582
|
+
.map(([path, size, text, digest]) => [path, text ?? { format: 'digest', size, digest }])))
|
|
4436
4583
|
const after = maps[0]!, before = maps[1] ?? new Map<string, unknown>()
|
|
4437
4584
|
const diff = computeBundleDiff(before, after, () => '.')
|
|
4438
4585
|
const changed: { path: string; basePath?: string }[] = [...diff.files.onlyBase, ...diff.files.onlyOther, ...diff.files.changed]
|
|
@@ -4551,6 +4698,12 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
4551
4698
|
}
|
|
4552
4699
|
const diffed = path === NPM_RENAME_DIFF_PATH && params.has('path') ? renameDiffRequest(params) : null
|
|
4553
4700
|
if (path === NPM_RENAME_DIFF_PATH && params.has('path') && !diffed) { sendJson(res, 400, { error: 'bad-file' }); return }
|
|
4701
|
+
// The patch a patched version's files and comparison are of, where named
|
|
4702
|
+
// (`patchHash`, `patchRepoId`, `patchCommit`, `patchPath`), as Review reads them.
|
|
4703
|
+
const patch = patchFields(key => params.get(key))
|
|
4704
|
+
if (patch === 'invalid' || (params.has('patchHash') && !patch) || (patch && path !== NPM_PACKAGE_PATH && path !== NPM_COMPARISON_PATH)) {
|
|
4705
|
+
sendJson(res, 400, { error: 'bad-patch' }); return
|
|
4706
|
+
}
|
|
4554
4707
|
const reader = await npmReader(deps, cookie, otherName === name ? [name] : [name, otherName], { handed: true })
|
|
4555
4708
|
if (reader == null) { sendJson(res, 401, { error: 'unauthenticated' }); return }
|
|
4556
4709
|
const controller = new AbortController()
|
|
@@ -4678,15 +4831,20 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
4678
4831
|
// same package), as Compare and Review pair them, with what their
|
|
4679
4832
|
// Overview reads beside them (comparison.ts): the version shown against
|
|
4680
4833
|
// the one compared with, or `(empty)`, none, as Review reads a first
|
|
4681
|
-
// version
|
|
4834
|
+
// version; as `patch` changes its files, where named, the patch read
|
|
4835
|
+
// first (npmPatchOf). Read of both, each package's access checked again
|
|
4836
|
+
// once worked out.
|
|
4682
4837
|
if (path === NPM_COMPARISON_PATH) {
|
|
4683
4838
|
const old = otherSpec === '(empty)' ? null : await otherDoc()
|
|
4839
|
+
const patched = patch ? await npmPatchOf(deps, cookie, patch) : null
|
|
4840
|
+
if (patched && 'error' in patched) { sendRefusal(res, patched); return }
|
|
4684
4841
|
res.setTimeout(NPM_RESPONSE_IDLE_MS, () => res.destroy())
|
|
4685
4842
|
const loads = [doc, ...old ? [old] : []].map(loadNpmFiles)
|
|
4686
4843
|
let body
|
|
4687
4844
|
try {
|
|
4688
4845
|
const [files, oldFiles] = await Promise.all(loads.map(load => load.result))
|
|
4689
|
-
body = await comparisonBody(old ? npmComparedSide(old.dist.integrity, oldFiles!) : emptySide(),
|
|
4846
|
+
body = await comparisonBody(old ? npmComparedSide(old.dist.integrity, oldFiles!) : emptySide(),
|
|
4847
|
+
npmComparedSide(doc.dist.integrity, patched ? patchedFiles(files!, patched.files) : files!))
|
|
4690
4848
|
} finally { for (const load of loads) load.release() }
|
|
4691
4849
|
const current = await npmReader(deps, cookie, otherName === name ? [name] : [name, otherName])
|
|
4692
4850
|
if (current == null) { body.stream.destroy(); sendJson(res, 401, { error: 'unauthenticated' }); return }
|
|
@@ -4744,6 +4902,20 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
|
|
|
4744
4902
|
await sendEncoded(req, res, diff, 'application/json')
|
|
4745
4903
|
return
|
|
4746
4904
|
}
|
|
4905
|
+
// A patched version's files, as its patch changes them (npm-patch.ts),
|
|
4906
|
+
// its patch read first (npmPatchOf), as Review reads them beside the
|
|
4907
|
+
// version's own; none of what is asked beside those.
|
|
4908
|
+
if (patch) {
|
|
4909
|
+
const patched = await npmPatchOf(deps, cookie, patch)
|
|
4910
|
+
if ('error' in patched) { sendRefusal(res, patched); return }
|
|
4911
|
+
const load = loadNpmFiles(doc)
|
|
4912
|
+
let body
|
|
4913
|
+
try { body = await patchedVersionBody(doc, patch, patchedFiles(await load.result, patched.files)) } finally { load.release() }
|
|
4914
|
+
if (!(await recheck(doc.private))) { body.stream.destroy(); return }
|
|
4915
|
+
res.setTimeout(NPM_RESPONSE_IDLE_MS, () => res.destroy())
|
|
4916
|
+
await sendEncoded(req, res, body, 'application/json')
|
|
4917
|
+
return
|
|
4918
|
+
}
|
|
4747
4919
|
// The load stays held until this response is written or abandoned, and
|
|
4748
4920
|
// a reader who stops reading it gives it up after a while. One already
|
|
4749
4921
|
// gone takes none, as its close has passed.
|
|
@@ -5240,10 +5412,12 @@ export function createManagedRequestHandler(deps: ManagedHttpDeps): Handler {
|
|
|
5240
5412
|
if (method !== 'GET') { send405(res, 'GET'); return }
|
|
5241
5413
|
const s = workspaceSession!
|
|
5242
5414
|
const integrity = url.searchParams.get('integrity'), name = url.searchParams.get('package'), version = url.searchParams.get('version')
|
|
5243
|
-
|
|
5415
|
+
const patchHash = url.searchParams.get('patchHash')
|
|
5416
|
+
if (!isNpmPackageName(name) || !isNpmPackageSpec(version) || !integrity || integrity.length > 1024
|
|
5417
|
+
|| (patchHash !== null && !isPatchHash(patchHash))) {
|
|
5244
5418
|
sendJson(res, 400, { error: 'bad-review-context' }); return
|
|
5245
5419
|
}
|
|
5246
|
-
const review = await deps.db.getLatestOwnDependencyReview(s.user.id, { ecosystem: 'npm', package: name, version, integrity })
|
|
5420
|
+
const review = await deps.db.getLatestOwnDependencyReview(s.user.id, { ecosystem: 'npm', package: name, version, integrity, patchHash })
|
|
5247
5421
|
const visible = await readableNpmReviews(res, deps, cookie, s.user.id, review ? [review] : [], s.user)
|
|
5248
5422
|
if (visible) sendJson(res, 200, { review: (await withFormatted(deps.reviewedDependencies ?? null, s.user.id, visible))[0] ?? null })
|
|
5249
5423
|
return
|