@preventive/triage 1.0.0-alpha.44 → 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 +25 -2
- package/common/default-scan-models.ts +1 -1
- package/common/dependency-reviews.d.ts +0 -5
- package/common/dependency-reviews.js +0 -17
- package/common/line-diff.js +5 -4
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/github-agents.ts +92 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/roles.ts +7 -0
- package/common/managed/routes.js +88 -7
- package/common/managed/team-app.d.ts +5 -2
- package/common/managed/team-app.js +2 -2
- package/common/server-info.ts +3 -0
- package/common/workspace-app.js +6 -3
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2XUEZHIP.js +18 -18
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-3PS4KWD2.js +10 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-4UFAMJAX.js +1 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7XRR4OFU.js +1 -0
- package/out/chunk-BC2UCLFJ.js +1 -0
- package/out/chunk-BOIXMUU5.js +3 -0
- package/out/chunk-C4Z6ZBXV.js +6 -6
- package/out/chunk-CC4OM6TZ.js +3 -0
- package/out/chunk-G24A42V6.js +2 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-I2MDGSQX.js +3 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -1
- package/out/chunk-JKGQ3O7O.js +6 -0
- package/out/chunk-LE5ZWLG4.js +235 -0
- package/out/chunk-MSION3G4.js +36 -0
- package/out/chunk-NRPC74AM.js +1 -1
- package/out/chunk-OKRLLE5I.js +17 -0
- package/out/chunk-OOJM4CTU.js +1 -1
- package/out/chunk-PNHBZGDD.js +1 -1
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +1 -1
- package/out/chunk-S6GIXWQW.js +52 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TUII34K2.js +1 -1
- package/out/chunk-V3KWNNMY.js +1 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WQ74ZBZF.js +7 -0
- package/out/client-managed-import.js +3 -3
- package/out/client-managed.js +31 -35
- package/out/client-sync.js +11 -11
- package/out/compare.js +35 -0
- package/out/dialogs.js +51 -0
- package/out/github-issue-authorized.html +1 -1
- package/out/github-issue-authorized.js +1 -1
- package/out/graph.js +17 -17
- package/out/index.html +30 -34
- package/out/prism.js +14 -14
- package/out/scan.js +2 -0
- package/out/terminal.js +294 -293
- package/out/view.css +1 -1
- package/out/view.js +16 -107
- package/package.json +53 -6
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/hosts.js +6 -6
- package/risk/files/readability.js +23 -17
- package/risk/files/runs-code.js +18 -6
- package/risk/files/script.js +7 -3
- package/risk/files/stamps.js +8 -7
- package/risk/index.d.ts +71 -1
- package/risk/index.js +6 -2
- package/risk/npm/assess.js +1 -1
- package/risk/npm/compare.js +34 -10
- package/risk/npm/files.js +413 -2
- package/risk/npm/tarball.js +56 -73
- package/server-e2e/app.ts +5 -4
- package/server-e2e/http.ts +5 -2
- package/server-managed/agent-merges.ts +198 -0
- package/server-managed/app-agents.ts +30 -0
- package/server-managed/avatar-store.ts +1 -0
- package/server-managed/bundle-cache.ts +7 -1
- package/server-managed/combined.ts +3 -2
- package/server-managed/commit-coauthors.ts +83 -0
- package/server-managed/commit-lockfiles.ts +72 -0
- package/server-managed/config.ts +6 -0
- package/server-managed/db-methods.ts +66 -19
- package/server-managed/db-neon.ts +89 -21
- package/server-managed/db-schema.ts +13 -2
- package/server-managed/db.ts +9 -1
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +97 -0
- package/server-managed/dependency-reviews.ts +78 -18
- package/server-managed/github-accounts.ts +33 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +33 -13
- package/server-managed/github-errors.ts +2 -0
- package/server-managed/github-issue-links.ts +11 -5
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-oauth.ts +5 -2
- package/server-managed/github-pulls.ts +2 -2
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +10 -2
- package/server-managed/http.ts +1251 -97
- package/server-managed/index.ts +13 -8
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/locked-packages.ts +37 -16
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +178 -0
- package/server-managed/npm-insights.ts +20 -11
- package/server-managed/npm-kept.ts +83 -6
- package/server-managed/npm-loads.ts +114 -37
- package/server-managed/npm-packages.ts +110 -64
- package/server-managed/npm-risk.ts +4 -1
- package/server-managed/pretty-print.ts +67 -23
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +300 -0
- package/server-managed/pull-dependency-reviews.ts +318 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +16 -0
- package/server-managed/release-commits.ts +84 -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 +218 -0
- package/server-managed/repository-agents.ts +220 -0
- package/server-managed/repository-browser.ts +166 -37
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +52 -0
- package/server-managed/repository-dependency-reviews.ts +157 -0
- package/server-managed/repository-history.ts +461 -0
- package/server-managed/repository-issues.ts +181 -0
- package/server-managed/repository-pulse.ts +439 -0
- package/server-managed/repository-risk-audit.ts +13 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +133 -118
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/session.ts +27 -9
- package/server-managed/sql.ts +6 -3
- package/server-managed/storage-stores.ts +5 -0
- package/server-managed/storage.ts +4 -2
- package/server-managed/team-app.ts +19 -3
- package/server-managed/team-reports.ts +24 -12
- package/server-managed/upstream-cache.ts +12 -0
- package/server-managed/workspace-shares.ts +4 -1
- package/server.ts +25 -5
- package/out/chunk-2ABLKCOY.js +0 -46
- package/out/chunk-2KBF5TZ7.js +0 -2
- package/out/chunk-35VE6CDH.js +0 -2
- package/out/chunk-3BZ7JERB.js +0 -1
- package/out/chunk-4XCDFGDP.js +0 -10
- package/out/chunk-5LSXOLUS.js +0 -1
- package/out/chunk-5RCM2J63.js +0 -1
- package/out/chunk-7B6HB4D7.js +0 -1
- package/out/chunk-AJ4GTATZ.js +0 -3
- package/out/chunk-CH4UN6WK.js +0 -225
- package/out/chunk-CMQA4DM6.js +0 -1
- package/out/chunk-CVVJAXVY.js +0 -2
- package/out/chunk-DIJJNTRH.js +0 -10
- package/out/chunk-EDKLBQQN.js +0 -17
- package/out/chunk-F47DXCEV.js +0 -33
- package/out/chunk-FK3DZEKL.js +0 -1
- package/out/chunk-G6AJWENO.js +0 -1
- package/out/chunk-GJ4IUCNS.js +0 -1
- package/out/chunk-GNXOKIGP.js +0 -80
- package/out/chunk-GZ4MS3TE.js +0 -13
- package/out/chunk-GZWM7UQM.js +0 -2
- package/out/chunk-IU3CCINR.js +0 -3
- package/out/chunk-K3X5EJBQ.js +0 -2
- package/out/chunk-KEX2ATEW.js +0 -2
- package/out/chunk-KYCNJVB7.js +0 -1
- package/out/chunk-LPCTJYNA.js +0 -7
- package/out/chunk-O4RJY5TP.js +0 -2
- package/out/chunk-OD4LPN3H.js +0 -1
- package/out/chunk-QWDWTBM7.js +0 -1
- package/out/chunk-QYWKC557.js +0 -1
- package/out/chunk-R75XOKUP.js +0 -1
- package/out/chunk-SWTLADW5.js +0 -1
- package/out/chunk-T7OKH4VW.js +0 -1
- package/out/chunk-TZ5KB2Y5.js +0 -10
- package/out/chunk-UNTRGE26.js +0 -2
- package/out/chunk-UTR2LVME.js +0 -2
- package/out/chunk-UYAXVRVL.js +0 -1
- package/out/chunk-V3ME6FI5.js +0 -2
- package/out/chunk-V5O7M2YT.js +0 -1
- package/out/chunk-VIHGO77Y.js +0 -1
- package/out/chunk-W4J3MGLB.js +0 -1
- package/out/chunk-WGG7KQUH.js +0 -1
- package/out/chunk-XYFXXB2G.js +0 -10
- package/out/chunk-ZESLQCTF.js +0 -6
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The reviewed dependencies a host keeps beyond the app, as it registers
|
|
2
|
+
// them before the server is assembled (server.ts registerReviewedDependencies):
|
|
3
|
+
// an async method telling whether a dependency, as a lockfile locks it, is
|
|
4
|
+
// reviewed there (`lookup`), and, optionally, one reading the reviews of
|
|
5
|
+
// dependencies an open pull request's comments make (`parseComments`,
|
|
6
|
+
// pull-comment-reviews.ts), and one formatting one's own dependency reviews
|
|
7
|
+
// as a comment, for a copy button to give (`formatDependencyReviews`,
|
|
8
|
+
// dependency-review-comments.ts). Registered, they replace the commit a repository
|
|
9
|
+
// is reviewed since (pull-dependency-reviews.ts): a version passes by the
|
|
10
|
+
// reviews its teams publish, and those its pull request's comments make, as
|
|
11
|
+
// many as the repository requires, or by `lookup`'s saying so, and is
|
|
12
|
+
// otherwise unreviewed. A patched version (pnpm's patchedDependencies)
|
|
13
|
+
// passes by `lookup`, or reviews its comments make of its patch, alone, the
|
|
14
|
+
// app reviewing no patch yet. Those comments' reviews count for the pull
|
|
15
|
+
// request alone, as it is open: the host's to keep, as it is merged, among
|
|
16
|
+
// those `lookup` tells of.
|
|
17
|
+
//
|
|
18
|
+
// Each version is asked of `lookup` once a request, as many at once as
|
|
19
|
+
// ASK_AT_ONCE, its answer kept in this instance KEPT_MS; one it fails to
|
|
20
|
+
// answer is unreviewed, and counted so (`unasked`).
|
|
21
|
+
import { type Lru, lru } from './lru.ts'
|
|
22
|
+
|
|
23
|
+
export type ReviewedLockfile = 'npm' | 'pnpm' | 'yarn1'
|
|
24
|
+
// A dependency as a lockfile locks it: npm's always with its integrity (one
|
|
25
|
+
// with none is never reviewed, nor asked of), its shasum where the lockfile
|
|
26
|
+
// has one (Yarn 1's, after a tarball's `#`), and the hash of its patch where
|
|
27
|
+
// it is patched (pnpm's).
|
|
28
|
+
export interface ReviewedDependency {
|
|
29
|
+
ecosystem: 'npm'
|
|
30
|
+
lockfile: ReviewedLockfile
|
|
31
|
+
name: string
|
|
32
|
+
version: string
|
|
33
|
+
integrity?: string
|
|
34
|
+
shasum?: string
|
|
35
|
+
patchHash?: string
|
|
36
|
+
}
|
|
37
|
+
export type ReviewVerdict = 'accept' | 'accept-with-nits' | 'request-changes' | 'reject'
|
|
38
|
+
// A comment of an open pull request's, as `parseComments` is given it: one
|
|
39
|
+
// on the pull request (`comment`), a review's own (`review`, whatever its
|
|
40
|
+
// state, a comment's alone too), or one on a line of its diff
|
|
41
|
+
// (`review-comment`); by its author's GitHub login; when it was written, and
|
|
42
|
+
// last edited (`editedAt`) where it was.
|
|
43
|
+
export interface ReviewComment {
|
|
44
|
+
kind: 'comment' | 'review' | 'review-comment'
|
|
45
|
+
id: string
|
|
46
|
+
author: string | null
|
|
47
|
+
body: string
|
|
48
|
+
state?: string
|
|
49
|
+
createdAt: string
|
|
50
|
+
editedAt?: string
|
|
51
|
+
url: string
|
|
52
|
+
}
|
|
53
|
+
// What `parseComments` reads: the pull request, its comments, and the
|
|
54
|
+
// dependencies: every one the lockfiles its review holds to add that can be
|
|
55
|
+
// reviewed and `lookup` doesn't pass, and the others still wanting reviews.
|
|
56
|
+
export interface ParseCommentsInput { repository: string; number: number; comments: ReviewComment[]; dependencies: ReviewedDependency[] }
|
|
57
|
+
// A review its comments make, by `user` (a GitHub login, one of their
|
|
58
|
+
// authors'), of each of `dependencies`, with its verdict, as of `time` (when
|
|
59
|
+
// it was made, an ISO date or milliseconds; that user's latest comment's
|
|
60
|
+
// where none is told).
|
|
61
|
+
export interface CommentReview {
|
|
62
|
+
user: string
|
|
63
|
+
time?: string | number
|
|
64
|
+
dependencies: { ecosystem?: 'npm'; name: string; version: string; integrity: string; patchHash?: string; status: ReviewVerdict }[]
|
|
65
|
+
}
|
|
66
|
+
// A version a dependency review is of, or compared it with: its package's
|
|
67
|
+
// name, its version and integrity, and its tarball's shasum and its patch's
|
|
68
|
+
// hash where they are known (a lockfile's, pnpm's patched dependencies').
|
|
69
|
+
export interface ReviewedVersion { name: string; version: string; integrity: string; shasum?: string; patchHash?: string }
|
|
70
|
+
// One of one's own published dependency reviews, as `formatDependencyReviews`
|
|
71
|
+
// is given it: the version it is of (`new`) and the one it compared it with
|
|
72
|
+
// (`old`, none where it read its package whole); its verdict (`status`);
|
|
73
|
+
// whether it read every file (`coverage`, `full`, else `partial`); its final
|
|
74
|
+
// comment; each file's verdict and comment (`files`, of those with either);
|
|
75
|
+
// by whom (`reviewer`, their GitHub login), and when it was published.
|
|
76
|
+
export interface DependencyReviewEntry {
|
|
77
|
+
ecosystem: 'npm'
|
|
78
|
+
old: ReviewedVersion | null
|
|
79
|
+
new: ReviewedVersion
|
|
80
|
+
status: ReviewVerdict
|
|
81
|
+
coverage: 'full' | 'partial'
|
|
82
|
+
comment: string
|
|
83
|
+
files: { path: string; status: ReviewVerdict | null; comment: string }[]
|
|
84
|
+
reviewer: string
|
|
85
|
+
publishedAt: number
|
|
86
|
+
}
|
|
87
|
+
// What `formatDependencyReviews` formats: one's own reviews, never another's,
|
|
88
|
+
// and the pull request whose versions they are of, where its copy asks.
|
|
89
|
+
export interface FormatDependencyReviewsInput { reviews: DependencyReviewEntry[]; pullRequest?: { repository: string; number: number } }
|
|
90
|
+
export interface ReviewedDependencies {
|
|
91
|
+
lookup(dependency: ReviewedDependency): Promise<boolean>
|
|
92
|
+
parseComments?(input: ParseCommentsInput): Promise<CommentReview[]>
|
|
93
|
+
// The comment to copy, none where it is empty, null or undefined.
|
|
94
|
+
formatDependencyReviews?(input: FormatDependencyReviewsInput): Promise<string | null | undefined>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const ASK_AT_ONCE = 16
|
|
98
|
+
const KEPT_MS = 60_000
|
|
99
|
+
|
|
100
|
+
let registered: ReviewedDependencies | null = null
|
|
101
|
+
let assembled = false
|
|
102
|
+
// Each lookup's answers, apart.
|
|
103
|
+
const answers = new WeakMap<ReviewedDependencies['lookup'], Lru<{ at: number; reviewed: boolean }>>()
|
|
104
|
+
|
|
105
|
+
// Registers the host's reviewed dependencies, once, before the server is
|
|
106
|
+
// assembled (init()): a server already assembled keeps what it was given.
|
|
107
|
+
export function registerReviewedDependencies(reviewed: ReviewedDependencies): void {
|
|
108
|
+
const optional = (method: unknown) => method === undefined || typeof method === 'function'
|
|
109
|
+
if (typeof reviewed?.lookup !== 'function' || !optional(reviewed.parseComments) || !optional(reviewed.formatDependencyReviews)) {
|
|
110
|
+
throw new TypeError('registerReviewedDependencies: expected { lookup, parseComments?, formatDependencyReviews? }, async methods')
|
|
111
|
+
}
|
|
112
|
+
if (assembled) throw new Error('registerReviewedDependencies: call it before init(), as the server takes them as it is assembled')
|
|
113
|
+
if (registered) throw new Error('registerReviewedDependencies: reviewed dependencies are registered already')
|
|
114
|
+
registered = { lookup: reviewed.lookup.bind(reviewed), ...reviewed.parseComments ? { parseComments: reviewed.parseComments.bind(reviewed) } : {},
|
|
115
|
+
...reviewed.formatDependencyReviews ? { formatDependencyReviews: reviewed.formatDependencyReviews.bind(reviewed) } : {} }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The reviewed dependencies registered, as a server takes them once it is
|
|
119
|
+
// assembled; none registers after.
|
|
120
|
+
export function assembledReviewedDependencies(): ReviewedDependencies | null {
|
|
121
|
+
assembled = true
|
|
122
|
+
return registered
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Which of `dependencies`, by their keys, `lookup` passes, and how many it
|
|
126
|
+
// failed to answer.
|
|
127
|
+
export async function askReviewed(lookup: ReviewedDependencies['lookup'], dependencies: Map<string, ReviewedDependency>): Promise<{ passed: Set<string>; unasked: number }> {
|
|
128
|
+
const passed = new Set<string>()
|
|
129
|
+
let unasked = 0
|
|
130
|
+
const pending = [...dependencies]
|
|
131
|
+
const kept = answers.get(lookup) ?? answers.set(lookup, lru(100_000)).get(lookup)!
|
|
132
|
+
await Promise.all(Array.from({ length: Math.min(ASK_AT_ONCE, pending.length) }, async () => {
|
|
133
|
+
for (let next = pending.pop(); next; next = pending.pop()) {
|
|
134
|
+
const [key, dependency] = next
|
|
135
|
+
const known = kept.get(key)
|
|
136
|
+
let answer = known && Date.now() - known.at < KEPT_MS ? known.reviewed : null
|
|
137
|
+
if (answer === null) {
|
|
138
|
+
try { answer = await lookup({ ...dependency }) === true } catch (error) {
|
|
139
|
+
unasked++
|
|
140
|
+
console.warn('reviewed dependencies: failed to answer', `${dependency.name}@${dependency.version}`, error instanceof Error ? error.message : error)
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
kept.set(key, { at: Date.now(), reviewed: answer })
|
|
144
|
+
}
|
|
145
|
+
if (answer) passed.add(key)
|
|
146
|
+
}
|
|
147
|
+
}))
|
|
148
|
+
return { passed, unasked }
|
|
149
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Async throughout — the store (db.ts) is async so a future non-SQLite
|
|
7
7
|
// backend slots in without touching these helpers.
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
8
9
|
import type { ManagedConfig } from './config.ts'
|
|
9
10
|
import type { ManagedDb, ManagedSession, ManagedUser, StoredUser } from './db.ts'
|
|
10
11
|
import { hashToken, randomToken } from './crypto.ts'
|
|
@@ -13,13 +14,15 @@ import { hashToken, randomToken } from './crypto.ts'
|
|
|
13
14
|
// `cookieName` when cookies are Secure.
|
|
14
15
|
export const STATE_COOKIE = 'dvstate'
|
|
15
16
|
|
|
16
|
-
// Base names for GitHub issue authorization state (github-issue-oauth.ts)
|
|
17
|
-
//
|
|
17
|
+
// Base names for GitHub issue authorization state (github-issue-oauth.ts),
|
|
18
|
+
// linking another GitHub account (github-link-oauth.ts) and an
|
|
19
|
+
// administrator's read-only view as another user (view-as.ts).
|
|
18
20
|
export const ISSUE_STATE_COOKIE = 'dvissuestate'
|
|
21
|
+
export const LINK_STATE_COOKIE = 'dvlinkstate'
|
|
19
22
|
export const VIEW_COOKIE = 'dvview'
|
|
20
23
|
// The session cookie cannot share a name with these, or one would overwrite
|
|
21
24
|
// or be read as the other.
|
|
22
|
-
export const RESERVED_COOKIES: readonly string[] = [STATE_COOKIE, ISSUE_STATE_COOKIE, VIEW_COOKIE]
|
|
25
|
+
export const RESERVED_COOKIES: readonly string[] = [STATE_COOKIE, ISSUE_STATE_COOKIE, LINK_STATE_COOKIE, VIEW_COOKIE]
|
|
23
26
|
|
|
24
27
|
// `__Host-`-prefix the base name when cookies are Secure (the prefix mandates
|
|
25
28
|
// Secure + Path=/ + no Domain, which `buildCookie` already emits).
|
|
@@ -55,13 +58,15 @@ export function parseCookies(header: string | undefined): Map<string, string> {
|
|
|
55
58
|
|
|
56
59
|
// Mint a session for a freshly-authenticated user. Upserts the identity, then
|
|
57
60
|
// stores the session keyed by the cookie token's hash. Returns the cookie to
|
|
58
|
-
// set and the CSRF token (also persisted on the row)
|
|
61
|
+
// set and the CSRF token (also persisted on the row), or null for an account
|
|
62
|
+
// a user linked to theirs, which can neither sign in nor register.
|
|
59
63
|
export async function createSession(
|
|
60
64
|
config: ManagedConfig, db: ManagedDb, user: ManagedUser, now: number,
|
|
61
|
-
): Promise<{ setCookie: string; csrfToken: string; userId: string }> {
|
|
65
|
+
): Promise<{ setCookie: string; csrfToken: string; userId: string } | null> {
|
|
62
66
|
const token = randomToken()
|
|
63
67
|
const csrfToken = randomToken()
|
|
64
68
|
const userId = await db.upsertUser(user, now, config.initialAdminGithubId)
|
|
69
|
+
if (userId == null) return null
|
|
65
70
|
await db.createSession({ id: hashToken(token), userId, csrfToken, expiresAt: now + config.sessionTtlMs }, now)
|
|
66
71
|
const setCookie = buildCookie(config.sessionCookieName, token, {
|
|
67
72
|
maxAgeS: Math.floor(config.sessionTtlMs / 1000), secure: config.cookieSecure, sameSite: 'Lax',
|
|
@@ -69,19 +74,32 @@ export async function createSession(
|
|
|
69
74
|
return { setCookie, csrfToken, userId }
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
// A request's sign-in, whose presence its first read to find it records.
|
|
78
|
+
// Every read after it in that request (the rechecks after its reads of
|
|
79
|
+
// storage, GitHub or npm among them) only reads: last_seen_at was written a
|
|
80
|
+
// moment before, by the read that let the request in.
|
|
81
|
+
const requests = new AsyncLocalStorage<{ seen: boolean }>()
|
|
82
|
+
export function runRequestPresence<T>(work: () => T): T {
|
|
83
|
+
return requests.run({ seen: false }, work)
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
// Resolve the current session (+ user) from the request's cookies, or null.
|
|
73
87
|
// A view cookie resolves only to that admin's view as another user: a view
|
|
74
88
|
// that has ended never falls back to the admin's own session. `own` skips the
|
|
75
89
|
// view and reads the signed-in session itself.
|
|
76
|
-
export function readSession(
|
|
90
|
+
export async function readSession(
|
|
77
91
|
config: ManagedConfig, db: ManagedDb, cookieHeader: string | undefined, now: number, { own = false } = {},
|
|
78
92
|
): Promise<{ session: ManagedSession; user: StoredUser } | null> {
|
|
79
93
|
const cookies = parseCookies(cookieHeader)
|
|
80
94
|
const token = cookies.get(config.sessionCookieName)
|
|
81
|
-
if (token == null || token === '') return
|
|
95
|
+
if (token == null || token === '') return null
|
|
82
96
|
const view = own ? undefined : cookies.get(cookieName(config, VIEW_COOKIE))
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
const request = requests.getStore(), seen = request?.seen === true
|
|
98
|
+
const s = view != null && view !== ''
|
|
99
|
+
? await (seen ? db.getViewSessionWithUser : db.viewSessionWithUser)(hashToken(view), hashToken(token), now)
|
|
100
|
+
: await (seen ? db.getSessionWithUser : db.sessionWithUser)(hashToken(token), now)
|
|
101
|
+
if (s && request) request.seen = true
|
|
102
|
+
return s
|
|
85
103
|
}
|
|
86
104
|
|
|
87
105
|
// Drop the session named by the request's cookie (logout). The caller also
|
package/server-managed/sql.ts
CHANGED
|
@@ -19,11 +19,14 @@ export interface ManagedSqlDriver extends ManagedSql {
|
|
|
19
19
|
// These operations execute exactly one SQL statement. Postgres already gives
|
|
20
20
|
// that statement an atomic snapshot; an explicit BEGIN/COMMIT adds only trips.
|
|
21
21
|
// Lease and cache writes coordinate through their conditional UPSERT/UPDATE alone.
|
|
22
|
-
const SINGLE_STATEMENTS = new Set(['getReport', 'getBundle', 'getStorageRow', 'getStorageEncryption',
|
|
22
|
+
const SINGLE_STATEMENTS = new Set(['getReport', 'getBundle', 'getStorageRow', 'getStorageEncryption', 'getTeam', 'getComment',
|
|
23
|
+
'userCanReadReport', 'userCanReadRepo', 'userCanReadRepoPath', 'userCanReadBundle', 'reportPermissionsFor', 'listComments', 'listManagedIssues',
|
|
24
|
+
'listTriageHistory', 'listUsers', 'listUserOptions', 'listActivityReports', 'getRepoFullName', 'getSessionWithUser', 'getViewSessionWithUser',
|
|
25
|
+
'listTriageFixes',
|
|
23
26
|
'listReports', 'listBundles', 'listReadableBundleIds', 'listAllRepos', 'listSelectedRepos', 'listRepoScopesForUser', 'listTriage',
|
|
24
27
|
'getFeedState', 'getWorkspaceShareFeedState', 'claimMaintenanceLease', 'finishMaintenanceLease', 'cacheRepoDefaultBranch', 'releaseBundleBuildLease',
|
|
25
|
-
'getUpstreamCacheEntry', 'getUpstreamCacheEntries', 'setUpstreamCacheEntry', 'setUpstreamCacheEntries', 'ensureSessionUploadKey',
|
|
26
|
-
'listGithubCommits', 'listGithubCommitTags', 'setGithubCommits'])
|
|
28
|
+
'getUpstreamCacheEntry', 'getUpstreamCacheEntries', 'getFirstUpstreamCacheEntries', 'setUpstreamCacheEntry', 'setUpstreamCacheEntries', 'ensureSessionUploadKey',
|
|
29
|
+
'listGithubCommits', 'listGithubCommitTags', 'setGithubCommits', 'getAvatarKey', 'listUsersByAccount'])
|
|
27
30
|
// Session presence and upload keys change only their own rows.
|
|
28
31
|
const UNLOCKED_WRITES = new Set(['sessionWithUser', 'viewSessionWithUser', 'ensureSessionUploadKey'])
|
|
29
32
|
|
|
@@ -61,6 +61,11 @@ function avatarStore(objects: ObjectStorage, sidecar: boolean): AvatarStore {
|
|
|
61
61
|
if (split < 0 || split > 100) throw new Error('Invalid cached avatar')
|
|
62
62
|
return { contentType: bytes.subarray(0, split).toString(), bytes: bytes.subarray(split + 1) }
|
|
63
63
|
},
|
|
64
|
+
async delete(value) {
|
|
65
|
+
const path = `avatars/${id(value)}`
|
|
66
|
+
await objects.delete(path)
|
|
67
|
+
if (sidecar) await objects.delete(`${path}.type`)
|
|
68
|
+
},
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -15,8 +15,10 @@ import { migrateStorage, reapStorageUploads } from './storage-maintenance.ts'
|
|
|
15
15
|
// Database-only callers (notably status) must not activate byte encryption.
|
|
16
16
|
export async function openManagedStorageDb(config: ManagedConfig) {
|
|
17
17
|
const options = { triageHistoryLimit: config.triageHistoryLimit, storageEncryptionKey: parseStorageKey(config.storageEncryptionKey) }
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (!config.neonUrl) return (await import('./db.ts')).openSqliteManagedDb(config.dbPath, options)
|
|
19
|
+
// Serverless instances hold pooled connections only while an invocation lasts.
|
|
20
|
+
const attachPool = config.serverless ? (await import('@vercel/functions')).attachDatabasePool : undefined
|
|
21
|
+
return openNeonManagedDb(config.neonUrl, options, attachPool)
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export async function openManagedStorage(config: ManagedConfig) {
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
// The same read names the reports a restricted reader would find nothing in:
|
|
9
9
|
// under -deps or -security the catalog leaves those out of the team's list.
|
|
10
10
|
// Readers with both permissions keep every report, empty or not.
|
|
11
|
+
//
|
|
12
|
+
// A team that is not App also says how many of its reports open in App mode
|
|
13
|
+
// on their own, and its findings for the badge it collapses to. None is as
|
|
14
|
+
// uniform as an App team, and collapses like one. A team mixing App and non-App
|
|
15
|
+
// reports stays expanded, and so does one whose reports each open as App but
|
|
16
|
+
// whose combined workspace does not (contradicting verdicts, say): it has App
|
|
17
|
+
// findings, and only expanded does it show that they fail to combine.
|
|
11
18
|
import { reportEntries } from '@preventive/report'
|
|
12
19
|
import { type TeamAppMetadata, managedTeamAppMetadata } from '../common/managed/team-app.js'
|
|
13
20
|
import type { BlobStore } from './blob-store.ts'
|
|
@@ -48,10 +55,14 @@ function remember(db: ManagedDb, key: string, summary: TeamWorkspaceSummary): Te
|
|
|
48
55
|
return summary
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
function isLinks(data: unknown): boolean {
|
|
59
|
+
const record = data as { source?: unknown; links?: unknown } | null
|
|
60
|
+
return record?.source === 'links' && Array.isArray(record.links)
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
// A links document counts by its links, every other report by its findings.
|
|
52
64
|
function hasContent(data: unknown): boolean {
|
|
53
|
-
|
|
54
|
-
if (record?.source === 'links' && Array.isArray(record.links)) return record.links.length > 0
|
|
65
|
+
if (isLinks(data)) return (data as { links: unknown[] }).links.length > 0
|
|
55
66
|
return (reportEntries(data) ?? []).some(entry => !Array.isArray(entry) || entry.length > 0)
|
|
56
67
|
}
|
|
57
68
|
|
|
@@ -79,7 +90,12 @@ async function classify(db: ManagedDb, store: BlobStore, key: string, team: User
|
|
|
79
90
|
}
|
|
80
91
|
const restricted = access.reports.some(report => !report.permissions.dependencies || !report.permissions.security)
|
|
81
92
|
const empty = restricted ? body.reports.filter(report => !hasContent(report.data)).map(report => report.id) : []
|
|
82
|
-
|
|
93
|
+
const links = body.links ?? []
|
|
94
|
+
const app = managedTeamAppMetadata(Object.assign(body.reports, { links }), { countFindings: true })
|
|
95
|
+
if (app.appMode) return remember(db, key, { app, empty })
|
|
96
|
+
const appReports = body.reports.filter(report => !isLinks(report.data)
|
|
97
|
+
&& managedTeamAppMetadata(Object.assign([report], { links })).appMode).length
|
|
98
|
+
return remember(db, key, { app: { ...app, appReports }, empty })
|
|
83
99
|
})().catch((error: unknown) => {
|
|
84
100
|
if (error instanceof TeamReportsError && [413, 422].includes(error.status)) return remember(db, key, NO_APP)
|
|
85
101
|
if (!(error instanceof TeamReportsError)) console.warn('managed: team App classification failed:', error)
|
|
@@ -59,6 +59,13 @@ export class TeamReportsError extends Error {
|
|
|
59
59
|
export function teamSnapshotKey(snapshot: TeamReportAccessSnapshot): string {
|
|
60
60
|
return JSON.stringify([snapshot.user.id, snapshot.user.role, snapshot.teamId, snapshot.hiddenReportId ?? null, snapshot.reports, snapshot.repositories, snapshot.linkRevision])
|
|
61
61
|
}
|
|
62
|
+
// What a built workspace is kept by: everything its build reads, and nothing
|
|
63
|
+
// of who asked. A user's role and grants decide which reports (and which
|
|
64
|
+
// hidden preview) their snapshot holds and each one's permissions, all of it
|
|
65
|
+
// in the key, so members whose snapshots agree share one build.
|
|
66
|
+
function workspaceKey(snapshot: TeamReportAccessSnapshot): string {
|
|
67
|
+
return JSON.stringify([snapshot.teamId, snapshot.hiddenReportId ?? null, snapshot.reports, snapshot.repositories, snapshot.linkRevision])
|
|
68
|
+
}
|
|
62
69
|
export async function teamSnapshot(db: ManagedDb, sessionId: string, teamId: string, reportId: string | null = null): Promise<TeamReportAccessSnapshot> {
|
|
63
70
|
const snapshot = await db.getTeamReportAccessSnapshot(sessionId, Date.now(), teamId, reportId)
|
|
64
71
|
if (!snapshot) throw new TeamReportsError(401, 'unauthenticated')
|
|
@@ -145,15 +152,15 @@ async function buildTeamWorkspace(db: ManagedDb, store: BlobStore, snapshot: Tea
|
|
|
145
152
|
if (outputBytes + suffix.length > MAX_REPORT_QUERY_BYTES) throw new TeamReportsError(413, 'batch-too-large')
|
|
146
153
|
parts.push(suffix)
|
|
147
154
|
const body = Buffer.concat(parts)
|
|
148
|
-
retainVisibility(db,
|
|
149
|
-
retainReports(db,
|
|
155
|
+
retainVisibility(db, workspaceKey(snapshot), visible)
|
|
156
|
+
retainReports(db, workspaceKey(snapshot), body)
|
|
150
157
|
return { body, visible }
|
|
151
158
|
}
|
|
152
159
|
const pendingWorkspaces = new WeakMap<ManagedDb, Map<string, ReturnType<typeof buildTeamWorkspace>>>()
|
|
153
160
|
async function loadTeamWorkspace(db: ManagedDb, store: BlobStore, snapshot: TeamReportAccessSnapshot) {
|
|
154
161
|
let pending = pendingWorkspaces.get(db)
|
|
155
162
|
if (!pending) { pending = new Map(); pendingWorkspaces.set(db, pending) }
|
|
156
|
-
const key =
|
|
163
|
+
const key = workspaceKey(snapshot)
|
|
157
164
|
const existing = pending.get(key)
|
|
158
165
|
if (existing) return existing
|
|
159
166
|
const job = buildTeamWorkspace(db, store, snapshot)
|
|
@@ -163,7 +170,7 @@ async function loadTeamWorkspace(db: ManagedDb, store: BlobStore, snapshot: Team
|
|
|
163
170
|
// Treat the encoded body as immutable and recheck access before sending it.
|
|
164
171
|
// Warm HTTP reads need neither a parsed copy nor another serialization pass.
|
|
165
172
|
export async function loadTeamReportsResponse(db: ManagedDb, store: BlobStore, snapshot: TeamReportAccessSnapshot): Promise<Buffer> {
|
|
166
|
-
const cached = reportCaches.get(db)?.get(
|
|
173
|
+
const cached = reportCaches.get(db)?.get(workspaceKey(snapshot))
|
|
167
174
|
return cached ?? (await loadTeamWorkspace(db, store, snapshot)).body
|
|
168
175
|
}
|
|
169
176
|
export async function loadTeamReports(db: ManagedDb, store: BlobStore, snapshot: TeamReportAccessSnapshot): Promise<TeamReport[]> {
|
|
@@ -172,8 +179,7 @@ export async function loadTeamReports(db: ManagedDb, store: BlobStore, snapshot:
|
|
|
172
179
|
return (JSON.parse(body.toString('utf8')) as { reports: TeamReport[] }).reports
|
|
173
180
|
}
|
|
174
181
|
async function teamVisibility(db: ManagedDb, store: BlobStore, snapshot: TeamReportAccessSnapshot): Promise<Visibility> {
|
|
175
|
-
|
|
176
|
-
let visible = caches.get(db)?.get(key)
|
|
182
|
+
let visible = caches.get(db)?.get(workspaceKey(snapshot))
|
|
177
183
|
if (!visible) {
|
|
178
184
|
// A large snapshot may deliberately remain uncached, or be evicted by
|
|
179
185
|
// another request. Authorization uses this load's result in either case.
|
|
@@ -191,17 +197,23 @@ export async function teamReportVisibility(db: ManagedDb, store: BlobStore, snap
|
|
|
191
197
|
const visible = await teamVisibility(db, store, snapshot)
|
|
192
198
|
return visible.get(reportId)!
|
|
193
199
|
}
|
|
194
|
-
|
|
200
|
+
// A report's visibility for the session's current snapshot. A workspace built
|
|
201
|
+
// (or waited for) here outlives that snapshot's read, so the team's access is
|
|
202
|
+
// read again after it; one kept is answered with no read in between, which
|
|
203
|
+
// leaves nothing for another snapshot read to guard.
|
|
204
|
+
async function sessionReportVisibility(db: ManagedDb, store: BlobStore, sessionId: string, teamId: string, reportId: string): Promise<ReportVisibility> {
|
|
195
205
|
const snapshot = await teamSnapshot(db, sessionId, teamId, reportId)
|
|
206
|
+
const kept = caches.get(db)?.get(workspaceKey(snapshot))?.get(reportId)
|
|
207
|
+
if (kept) return kept
|
|
196
208
|
const visible = await teamReportVisibility(db, store, snapshot, reportId)
|
|
197
209
|
await recheckTeam(db, sessionId, snapshot)
|
|
198
|
-
return visible
|
|
210
|
+
return visible
|
|
211
|
+
}
|
|
212
|
+
export async function teamFindingIds(db: ManagedDb, store: BlobStore, sessionId: string, teamId: string, reportId: string): Promise<Set<string>> {
|
|
213
|
+
return (await sessionReportVisibility(db, store, sessionId, teamId, reportId)).ids
|
|
199
214
|
}
|
|
200
215
|
export async function teamSourcePaths(db: ManagedDb, store: BlobStore, sessionId: string, teamId: string, reportId: string): Promise<Set<string>> {
|
|
201
|
-
|
|
202
|
-
const visible = await teamReportVisibility(db, store, snapshot, reportId)
|
|
203
|
-
await recheckTeam(db, sessionId, snapshot)
|
|
204
|
-
return visible.sourcePaths
|
|
216
|
+
return (await sessionReportVisibility(db, store, sessionId, teamId, reportId)).sourcePaths
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
// Serialize shared annotation bodies once. Reports carry only the annotated
|
|
@@ -26,6 +26,9 @@ export interface UpstreamCacheStore {
|
|
|
26
26
|
getUpstreamCacheEntry(key: string): Promise<string | null>
|
|
27
27
|
// Key → value for those of `keys` that are kept.
|
|
28
28
|
getUpstreamCacheEntries(keys: string[]): Promise<Map<string, string>>
|
|
29
|
+
// Those of `first` that are kept; where none is, those of `then`, in the
|
|
30
|
+
// same statement (`first` whether they are `first`'s).
|
|
31
|
+
getFirstUpstreamCacheEntries(first: string[], then: string[]): Promise<{ first: boolean; entries: Map<string, string> }>
|
|
29
32
|
setUpstreamCacheEntry(key: string, value: string, cachedAt: number): Promise<void>
|
|
30
33
|
// Each of `entries` (key, value), as setUpstreamCacheEntry keeps one, in one
|
|
31
34
|
// statement: the last of a key given twice.
|
|
@@ -38,6 +41,10 @@ export interface UpstreamCacheStore {
|
|
|
38
41
|
export function upstreamCacheMethods(db: ManagedSql): UpstreamCacheStore {
|
|
39
42
|
const get = db.prepare('SELECT value FROM managed_upstream_cache WHERE cache_key = ?')
|
|
40
43
|
const getMany = db.prepare('SELECT cache_key, value FROM managed_upstream_cache WHERE cache_key IN (SELECT value FROM json_each(?))')
|
|
44
|
+
const getFirst = db.prepare(`WITH found AS (SELECT cache_key, value FROM managed_upstream_cache WHERE cache_key IN (SELECT value FROM json_each(?)))
|
|
45
|
+
SELECT cache_key, value, 1 AS in_first FROM found
|
|
46
|
+
UNION ALL SELECT cache_key, value, 0 AS in_first FROM managed_upstream_cache
|
|
47
|
+
WHERE cache_key IN (SELECT value FROM json_each(?)) AND NOT EXISTS (SELECT 1 FROM found)`)
|
|
41
48
|
// A slower concurrent audit cannot replace a newer listing with an older one.
|
|
42
49
|
const set = db.prepare(`INSERT INTO managed_upstream_cache (cache_key, value, cached_at) VALUES (?, ?, ?)
|
|
43
50
|
ON CONFLICT (cache_key) DO UPDATE SET value = excluded.value, cached_at = excluded.cached_at
|
|
@@ -55,6 +62,11 @@ export function upstreamCacheMethods(db: ManagedSql): UpstreamCacheStore {
|
|
|
55
62
|
const rows = await getMany.all(JSON.stringify(keys)) as { cache_key: string; value: string }[]
|
|
56
63
|
return new Map(rows.map(row => [row.cache_key, row.value]))
|
|
57
64
|
},
|
|
65
|
+
async getFirstUpstreamCacheEntries(first, then) {
|
|
66
|
+
if (first.length === 0 && then.length === 0) return { first: false, entries: new Map() }
|
|
67
|
+
const rows = await getFirst.all(JSON.stringify(first), JSON.stringify(then)) as { cache_key: string; value: string; in_first: number }[]
|
|
68
|
+
return { first: rows.some(row => Number(row.in_first) === 1), entries: new Map(rows.map(row => [row.cache_key, row.value])) }
|
|
69
|
+
},
|
|
58
70
|
async setUpstreamCacheEntry(key, value, cachedAt) {
|
|
59
71
|
await set.run(key, value, cachedAt)
|
|
60
72
|
},
|
|
@@ -145,15 +145,18 @@ export function workspaceShareMethods(db: ManagedSql): WorkspaceShareStore {
|
|
|
145
145
|
const links = await linkRevision(db)
|
|
146
146
|
const bundleRows = (await q.bundles.all(team.id) as (Omit<ManagedBundle, 'buildConditions'> & { buildConditions: string | null; repoFullName: string })[])
|
|
147
147
|
.map(row => ({ ...row, buildConditions: row.buildConditions == null ? null : JSON.parse(row.buildConditions) as BundleBuildConditions }))
|
|
148
|
+
const repositories = await q.repositories.all(team.id) as WorkspaceShareSnapshot['repositories']
|
|
148
149
|
return {
|
|
149
150
|
user: { id: `share:${tokenHash}`, login: 'public', name: 'Public workspace', avatarUrl: null, role: 'view' },
|
|
150
151
|
teamId: team.id,
|
|
151
152
|
linkRevision: links,
|
|
152
153
|
permissions,
|
|
153
|
-
repositories
|
|
154
|
+
repositories,
|
|
154
155
|
reports: rows.map(row => ({ id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
155
156
|
repo: { github: row.github, directory: row.directory }, permissions })),
|
|
156
157
|
team: { ...team, permissions, cacheKey: links,
|
|
158
|
+
repos: repositories.map(repo => ({ repoId: repo.repoId, fullName: repo.github, path: repo.path || null }))
|
|
159
|
+
.toSorted((a, b) => a.fullName.localeCompare(b.fullName) || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
157
160
|
reports: rows.map(row => ({ id: row.id, slug: row.slug, filename: row.filename, visible: true, analyzer: row.analyzer, repoFullName: row.github, repoDirectory: row.directory,
|
|
158
161
|
cacheKey: JSON.stringify([row.sha256, row.github, row.directory, row.filename, permissions, links]) })),
|
|
159
162
|
bundles: bundleRows.map(row => ({ id: row.id, slug: row.slug, integrity: row.integrity,
|
package/server.ts
CHANGED
|
@@ -2,6 +2,17 @@ import { parseArgs } from 'node:util'
|
|
|
2
2
|
import type { Server } from 'node:http'
|
|
3
3
|
import { startServer } from './server-common/standalone.ts'
|
|
4
4
|
|
|
5
|
+
// The reviewed dependencies a host keeps beyond the app, registered before
|
|
6
|
+
// init() (server-managed/reviewed-dependencies.ts): an async method telling
|
|
7
|
+
// whether a dependency, as a lockfile locks it, is reviewed there
|
|
8
|
+
// (`lookup`), and optionally one reading the reviews an open pull request's
|
|
9
|
+
// comments make (`parseComments`), and one formatting one's own dependency
|
|
10
|
+
// reviews as a comment to copy (`formatDependencyReviews`). They replace a
|
|
11
|
+
// repository's commit its dependencies are reviewed since.
|
|
12
|
+
export { registerReviewedDependencies } from './server-managed/reviewed-dependencies.ts'
|
|
13
|
+
export type { CommentReview, DependencyReviewEntry, FormatDependencyReviewsInput, ParseCommentsInput, ReviewComment, ReviewedDependencies, ReviewedDependency,
|
|
14
|
+
ReviewedLockfile, ReviewedVersion, ReviewVerdict } from './server-managed/reviewed-dependencies.ts'
|
|
15
|
+
|
|
5
16
|
const modes = ['e2e', 'managed', 'managed-e2e', 'e2e-managed'] as const
|
|
6
17
|
export type ServerMode = typeof modes[number]
|
|
7
18
|
|
|
@@ -10,20 +21,29 @@ function parseMode(mode: string): ServerMode {
|
|
|
10
21
|
throw new Error(`Unknown mode: ${mode}. Expected ${modes.join(', ')}.`)
|
|
11
22
|
}
|
|
12
23
|
|
|
24
|
+
export type InitOptions = {
|
|
25
|
+
mode?: ServerMode
|
|
26
|
+
// False serves only the API: `out/` is never read, nor its UI served, for
|
|
27
|
+
// hosts that serve the UI themselves.
|
|
28
|
+
ui?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
// Assemble the selected server without binding a port. Hosts can reuse its
|
|
14
|
-
// request/upgrade listeners or call listen() themselves.
|
|
15
|
-
|
|
32
|
+
// request/upgrade listeners or call listen() themselves. `init(mode)` is
|
|
33
|
+
// `init({ mode })`.
|
|
34
|
+
export async function init(options: ServerMode | InitOptions = {}): Promise<Server> {
|
|
35
|
+
const { mode = 'e2e', ui = true }: InitOptions = typeof options === 'string' ? { mode: options } : options
|
|
16
36
|
const selected = parseMode(mode)
|
|
17
37
|
if (selected === 'e2e') {
|
|
18
38
|
const { createE2eApp } = await import('./server-e2e/app.ts')
|
|
19
|
-
return (await createE2eApp()).httpServer
|
|
39
|
+
return (await createE2eApp(undefined, { ui })).httpServer
|
|
20
40
|
}
|
|
21
41
|
if (selected === 'managed') {
|
|
22
42
|
const managed = await import('./server-managed/index.ts')
|
|
23
|
-
return managed.init()
|
|
43
|
+
return managed.init(undefined, { ui })
|
|
24
44
|
}
|
|
25
45
|
const combined = await import('./server-managed/combined.ts')
|
|
26
|
-
return combined.init(selected === 'managed-e2e' ? 'managed+e2e' : 'e2e+managed')
|
|
46
|
+
return combined.init(selected === 'managed-e2e' ? 'managed+e2e' : 'e2e+managed', { ui })
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
export async function start(): Promise<void> {
|