@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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type LinkReportStore, linkReportMethods, linkRevision } from './link-reports.ts'
|
|
2
2
|
import { type RepositoryAliasStore, repositoryAliasMethods } from './repository-aliases.ts'
|
|
3
3
|
import { type DependencyReviewStore, dependencyReviewMethods } from './dependency-reviews.ts'
|
|
4
|
+
import { type RepositoryDependencyReviewStore, repositoryDependencyReviewMethods } from './repository-dependency-reviews.ts'
|
|
4
5
|
import { type TeamNpmScopeStore, teamNpmScopeMethods } from './team-npm-scopes.ts'
|
|
6
|
+
import { type GithubLinkStore, githubLinkMethods } from './github-links.ts'
|
|
5
7
|
import { createHash, randomUUID } from 'node:crypto'
|
|
6
8
|
import { teamCatalogRevision } from './team-catalog.ts'
|
|
7
9
|
import { type Role, roleAtLeast } from '../common/managed/roles.ts'
|
|
@@ -358,21 +360,26 @@ export interface UserTeam {
|
|
|
358
360
|
dependencyReview?: boolean
|
|
359
361
|
permissions: TeamUserPermissions
|
|
360
362
|
cacheKey?: string
|
|
363
|
+
// The team's repository links, by name: its Repositories, whether or not
|
|
364
|
+
// anything is attached to them yet.
|
|
365
|
+
repos: TeamRepoLink[]
|
|
361
366
|
reports: UserTeamReport[]
|
|
362
367
|
bundles: UserTeamBundle[]
|
|
363
368
|
}
|
|
364
369
|
|
|
365
370
|
// Backend-agnostic store surface (SQLite + PostgreSQL implementations).
|
|
366
|
-
export interface ManagedDb extends DependencyReviewStore, ActivityStore, CommentStore, GithubMetadataStore, GithubCommitStore, ManagedIssueStore, BundleBuildLeaseStore, UpstreamCacheStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb, LinkReportStore, RepositoryAliasStore, TeamNpmScopeStore {
|
|
371
|
+
export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyReviewStore, ActivityStore, CommentStore, GithubMetadataStore, GithubCommitStore, ManagedIssueStore, BundleBuildLeaseStore, UpstreamCacheStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb, LinkReportStore, RepositoryAliasStore, TeamNpmScopeStore, GithubLinkStore {
|
|
367
372
|
claimMaintenanceLease(owner: string, now: number, until: number, migration?: boolean): Promise<boolean>
|
|
368
373
|
finishMaintenanceLease(owner: string, until: number): Promise<void>
|
|
369
374
|
getFeedState(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; catalog: number; annotations: number } | null>
|
|
370
375
|
// Reuse connections within a finite request or one feed polling iteration.
|
|
371
376
|
withRequest?<T>(work: () => Promise<T>): Promise<T>
|
|
372
|
-
// Upsert the identity; returns the user's opaque id (stable across logins)
|
|
377
|
+
// Upsert the identity; returns the user's opaque id (stable across logins),
|
|
378
|
+
// or null for an account linked to a user (github-links.ts), which neither
|
|
379
|
+
// signs in nor registers. A name chosen from a linked account is kept.
|
|
373
380
|
// Initial-admin approval comes only from trusted login configuration. It
|
|
374
381
|
// promotes a matching No access identity only while it is the sole user.
|
|
375
|
-
upsertUser(user: ManagedUser, now: number, initialAdminGithubId?: number | null): Promise<string>
|
|
382
|
+
upsertUser(user: ManagedUser, now: number, initialAdminGithubId?: number | null): Promise<string | null>
|
|
376
383
|
createSession(session: ManagedSession, now: number): Promise<void>
|
|
377
384
|
// A sign-in session; never a view session, so a view token in the session
|
|
378
385
|
// cookie authenticates nothing.
|
|
@@ -385,6 +392,10 @@ export interface ManagedDb extends DependencyReviewStore, ActivityStore, Comment
|
|
|
385
392
|
// A view session opened by `viewerSessionId`, while that session is current
|
|
386
393
|
// and its user is still an admin. Presence is the admin's, not the viewed user's.
|
|
387
394
|
viewSessionWithUser(id: string, viewerSessionId: string, now: number): Promise<{ session: ManagedSession; user: StoredUser } | null>
|
|
395
|
+
// The two above as they read, recording no presence: for a read again in a
|
|
396
|
+
// request whose first read recorded it.
|
|
397
|
+
getSessionWithUser(id: string, now: number): Promise<{ session: ManagedSession; user: StoredUser } | null>
|
|
398
|
+
getViewSessionWithUser(id: string, viewerSessionId: string, now: number): Promise<{ session: ManagedSession; user: StoredUser } | null>
|
|
388
399
|
// Store `candidate` unless the session already has an upload key; returns
|
|
389
400
|
// the stored key, or null when the session no longer exists.
|
|
390
401
|
ensureSessionUploadKey(id: string, candidate: string): Promise<string | null>
|
|
@@ -406,6 +417,8 @@ export interface ManagedDb extends DependencyReviewStore, ActivityStore, Comment
|
|
|
406
417
|
deselectRepo(repoId: number): Promise<boolean>
|
|
407
418
|
listSelectedRepos(): Promise<SelectedRepo[]>
|
|
408
419
|
listAllRepos(): Promise<ManagedRepo[]>
|
|
420
|
+
// A selected repository's full name, active or not; null for none.
|
|
421
|
+
getRepoFullName(repoId: number): Promise<string | null>
|
|
409
422
|
deactivateRepo(repoId: number): Promise<boolean>
|
|
410
423
|
reactivateRepo(repoId: number): Promise<boolean>
|
|
411
424
|
deleteRepo(repoId: number): Promise<boolean>
|
|
@@ -452,6 +465,8 @@ export interface ManagedDb extends DependencyReviewStore, ActivityStore, Comment
|
|
|
452
465
|
// stamp nor the trail. Every change also appends to managed_finding_triage_event;
|
|
453
466
|
// listTriageHistory walks one finding's trail, newest first.
|
|
454
467
|
listTriage(findingIds: readonly string[]): Promise<TriageRow[]>
|
|
468
|
+
// The distinct Fix links of those, without the rest of their entries.
|
|
469
|
+
listTriageFixes(findingIds: readonly string[]): Promise<string[]>
|
|
455
470
|
getAnnotations(findingIds: readonly string[]): Promise<{ triage: TriageRow[]; comments: Awaited<ReturnType<CommentStore['listComments']>>; issues: Awaited<ReturnType<ManagedIssueStore['listManagedIssues']>> }>
|
|
456
471
|
applyManagedIssueFixes(sessionId: string, snapshot: TeamReportAccessSnapshot, updates: IssueFixUpdate[]): Promise<boolean>
|
|
457
472
|
// A content-free fingerprint of current annotations, read in one snapshot.
|
|
@@ -536,6 +551,15 @@ type SessionRow = {
|
|
|
536
551
|
uid: string; login: string; name: string | null; avatar: string | null; role: Role
|
|
537
552
|
}
|
|
538
553
|
type ViewSessionRow = SessionRow & { viewerId: string; viewerLogin: string; viewerName: string | null }
|
|
554
|
+
const ownSession = (row: SessionRow) => ({
|
|
555
|
+
session: { id: row.id, userId: row.uid, csrfToken: row.csrf, expiresAt: row.exp, uploadKey: row.uploadKey },
|
|
556
|
+
user: { id: row.uid, login: row.login, name: row.name, avatarUrl: row.avatar, role: row.role },
|
|
557
|
+
})
|
|
558
|
+
const viewSession = (row: ViewSessionRow) => ({
|
|
559
|
+
session: { id: row.id, userId: row.uid, csrfToken: row.csrf, expiresAt: row.exp, uploadKey: row.uploadKey,
|
|
560
|
+
viewer: { id: row.viewerId, login: row.viewerLogin, name: row.viewerName } },
|
|
561
|
+
user: { id: row.uid, login: row.login, name: row.name, avatarUrl: row.avatar, role: row.role },
|
|
562
|
+
})
|
|
539
563
|
|
|
540
564
|
type UserRow = { id: string; login: string; name: string | null; role: Role; created: number; lastSeen: number | null; lastActivity: number | null }
|
|
541
565
|
|
|
@@ -563,12 +587,14 @@ function prepareStatements(db: ManagedSql) {
|
|
|
563
587
|
// Identity refreshes preserve roles. Login can separately bootstrap the
|
|
564
588
|
// configured sole No access user within this same writer transaction.
|
|
565
589
|
upsertUserStmt: db.prepare(
|
|
566
|
-
`INSERT INTO managed_user (id, github_user_id, login, name, avatar_url, role, created_at, updated_at)
|
|
567
|
-
VALUES (?, ?, ?, ?, ?, 'none', ?, ?)
|
|
590
|
+
`INSERT INTO managed_user (id, github_user_id, login, name, github_name, avatar_url, role, created_at, updated_at)
|
|
591
|
+
VALUES (?, ?, ?, ?, ?, ?, 'none', ?, ?)
|
|
568
592
|
ON CONFLICT(github_user_id) DO UPDATE SET
|
|
569
|
-
login = excluded.login,
|
|
593
|
+
login = excluded.login, github_name = excluded.github_name,
|
|
594
|
+
name = CASE WHEN managed_user.name_link IS NULL THEN excluded.name ELSE managed_user.name END,
|
|
570
595
|
avatar_url = excluded.avatar_url, updated_at = excluded.updated_at`,
|
|
571
596
|
),
|
|
597
|
+
selectLinkedStmt: db.prepare(`SELECT 1 AS linked FROM managed_github_link WHERE github_user_id = ?`),
|
|
572
598
|
selectUserIdStmt: db.prepare(`SELECT id, role FROM managed_user WHERE github_user_id = ?`),
|
|
573
599
|
promoteInitialAdminStmt: db.prepare(
|
|
574
600
|
`UPDATE managed_user SET role = 'admin' WHERE id = ? AND role = 'none'
|
|
@@ -681,6 +707,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
681
707
|
added_by AS addedBy, added_at AS addedAt, active AS active
|
|
682
708
|
FROM managed_selected_repo WHERE active = 1 ORDER BY full_name ASC`,
|
|
683
709
|
),
|
|
710
|
+
selectRepoFullNameStmt: db.prepare(`SELECT full_name AS fullName FROM managed_selected_repo WHERE repo_id = ?`),
|
|
684
711
|
selectAllReposStmt: db.prepare(
|
|
685
712
|
`SELECT repo_id AS repoId, full_name AS fullName, is_private AS priv,
|
|
686
713
|
installation_id AS installId, default_branch AS branch, cached_default_branch AS cachedDefaultBranch, html_url AS htmlUrl,
|
|
@@ -811,6 +838,8 @@ function prepareStatements(db: ManagedSql) {
|
|
|
811
838
|
WHERE t.finding_id IN (SELECT value FROM json_each(?))
|
|
812
839
|
ORDER BY t.finding_id ASC`,
|
|
813
840
|
),
|
|
841
|
+
selectTriageFixesStmt: db.prepare(`SELECT DISTINCT fix FROM managed_finding_triage
|
|
842
|
+
WHERE finding_id IN (SELECT value FROM json_each(?)) AND fix IS NOT NULL ORDER BY fix`),
|
|
814
843
|
annotationTriageRevisionStmt: db.prepare(`
|
|
815
844
|
SELECT t.finding_id AS id,
|
|
816
845
|
(SELECT e.batch_id FROM managed_finding_triage_event e
|
|
@@ -910,10 +939,15 @@ function prepareStatements(db: ManagedSql) {
|
|
|
910
939
|
renameTeamStmt: db.prepare(`UPDATE managed_team SET name = ?, updated_at = ? WHERE id = ?`),
|
|
911
940
|
setTeamHiddenStmt: db.prepare(`UPDATE managed_team SET hidden = ?, updated_at = ? WHERE id = ?`),
|
|
912
941
|
selectTeamsStmt: db.prepare(`SELECT id, slug, name, hidden FROM managed_team ORDER BY name ASC`),
|
|
942
|
+
// `dependencyReview` where the user reviews dependencies in the team
|
|
943
|
+
// (dependency-reviews.ts reviewsIn): one that reviews them, and a member
|
|
944
|
+
// who manages, or triages and may see its dependencies; an admin in any.
|
|
913
945
|
selectTeamsForUserStmt: db.prepare(
|
|
914
|
-
`SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
946
|
+
`SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
947
|
+
CASE WHEN t.dependency_review = 1 AND (u.role IN ('admin', 'manage') OR (u.role = 'triage' AND tu.view_dependencies = 1))
|
|
948
|
+
THEN 1 ELSE 0 END AS dependencyReview,
|
|
915
949
|
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
916
|
-
FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id
|
|
950
|
+
FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id JOIN managed_user u ON u.id = tu.user_id
|
|
917
951
|
WHERE tu.user_id = ? AND t.hidden = 0 ORDER BY t.name ASC, t.id ASC`,
|
|
918
952
|
),
|
|
919
953
|
selectUserTeamScopesStmt: db.prepare(
|
|
@@ -1051,6 +1085,9 @@ function selectedRepoMethods(stmts: ReturnType<typeof prepareStatements>) {
|
|
|
1051
1085
|
const rows = (await selectAllReposStmt.all()) as RepoRow[]
|
|
1052
1086
|
return rows.map(readManagedRepo)
|
|
1053
1087
|
},
|
|
1088
|
+
async getRepoFullName(repoId: number): Promise<string | null> {
|
|
1089
|
+
return ((await stmts.selectRepoFullNameStmt.get(repoId)) as { fullName: string } | undefined)?.fullName ?? null
|
|
1090
|
+
},
|
|
1054
1091
|
async deactivateRepo(repoId: number): Promise<boolean> {
|
|
1055
1092
|
return Number((await deactivateRepoStmt.run(repoId)).changes) > 0
|
|
1056
1093
|
},
|
|
@@ -1275,6 +1312,10 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1275
1312
|
actorId: r.actorId, actorLogin: r.actorLogin, actorName: r.actorName, at: r.at,
|
|
1276
1313
|
}))
|
|
1277
1314
|
},
|
|
1315
|
+
async listTriageFixes(findingIds: readonly string[]): Promise<string[]> {
|
|
1316
|
+
if (findingIds.length === 0) return []
|
|
1317
|
+
return ((await stmts.selectTriageFixesStmt.all(JSON.stringify(findingIds))) as { fix: string }[]).map(row => row.fix)
|
|
1318
|
+
},
|
|
1278
1319
|
async listTriage(findingIds: readonly string[]): Promise<TriageRow[]> {
|
|
1279
1320
|
if (findingIds.length === 0) return []
|
|
1280
1321
|
const rows = (await selectTriageStmt.all(JSON.stringify(findingIds))) as TriageDbRow[]
|
|
@@ -1513,6 +1554,8 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1513
1554
|
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
1514
1555
|
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
1515
1556
|
])).digest('base64url'),
|
|
1557
|
+
repos: scopes.filter(scope => scope.teamId === t.id).map(scope => ({ repoId: scope.repoId, fullName: scope.fullName, path: scope.path || null }))
|
|
1558
|
+
.toSorted((a, b) => a.fullName.localeCompare(b.fullName) || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
1516
1559
|
reports: reportsByTeam.get(t.id) ?? [],
|
|
1517
1560
|
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
1518
1561
|
}))
|
|
@@ -1597,7 +1640,7 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1597
1640
|
const reports = reportMethods(stmts, db, key)
|
|
1598
1641
|
const triage = triageMethods(stmts, options.triageHistoryLimit ?? 0, db)
|
|
1599
1642
|
const {
|
|
1600
|
-
upsertUserStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectOwnSessionStmt, selectUsersStmt,
|
|
1643
|
+
upsertUserStmt, selectLinkedStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectOwnSessionStmt, selectUsersStmt,
|
|
1601
1644
|
touchUserSeenStmt, updateRoleStmt, updateTokensStmt, selectTokensStmt, ensureUploadKeyStmt, deleteSessionStmt, deleteExpiredStmt,
|
|
1602
1645
|
selectViewSessionStmt, deleteOtherViewsStmt, insertViewSessionStmt, deleteDemotedViewsStmt,
|
|
1603
1646
|
} = stmts
|
|
@@ -1607,7 +1650,9 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1607
1650
|
...linkReportMethods(db, key),
|
|
1608
1651
|
...repositoryAliasMethods(db),
|
|
1609
1652
|
...teamNpmScopeMethods(db),
|
|
1653
|
+
...githubLinkMethods(db),
|
|
1610
1654
|
...dependencyReviewMethods(db),
|
|
1655
|
+
...repositoryDependencyReviewMethods(db),
|
|
1611
1656
|
...bundleBuildLeaseMethods(db),
|
|
1612
1657
|
...upstreamCacheMethods(db),
|
|
1613
1658
|
async claimMaintenanceLease(owner, now, until, migration = false) {
|
|
@@ -1619,9 +1664,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1619
1664
|
await db.prepare('UPDATE managed_maintenance_lease SET expires_at = ? WHERE owner = ?').run(until, owner)
|
|
1620
1665
|
},
|
|
1621
1666
|
async upsertUser(user, now, initialAdminGithubId = null) {
|
|
1667
|
+
if (await selectLinkedStmt.get(user.githubUserId)) return null
|
|
1622
1668
|
// New row → a fresh id; ON CONFLICT(github_user_id) keeps an existing
|
|
1623
1669
|
// user's id (DO UPDATE leaves it untouched), so re-read to return it.
|
|
1624
|
-
await upsertUserStmt.run(randomUUID(), user.githubUserId, user.login, user.name, user.avatarUrl, now, now)
|
|
1670
|
+
await upsertUserStmt.run(randomUUID(), user.githubUserId, user.login, user.name, user.name, user.avatarUrl, now, now)
|
|
1625
1671
|
const row = (await selectUserIdStmt.get(user.githubUserId)) as { id: string; role: Role }
|
|
1626
1672
|
// Avoid the population check unless this login matches the configured
|
|
1627
1673
|
// identity and still needs approval. NOT EXISTS stops at any other user.
|
|
@@ -1639,10 +1685,11 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1639
1685
|
const row = (await selectOwnSessionStmt.get(id, now)) as SessionRow | undefined
|
|
1640
1686
|
if (row == null) return null
|
|
1641
1687
|
await touchUserSeenStmt.run(now, row.uid, now)
|
|
1642
|
-
return
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1688
|
+
return ownSession(row)
|
|
1689
|
+
},
|
|
1690
|
+
async getSessionWithUser(id, now) {
|
|
1691
|
+
const row = (await selectOwnSessionStmt.get(id, now)) as SessionRow | undefined
|
|
1692
|
+
return row == null ? null : ownSession(row)
|
|
1646
1693
|
},
|
|
1647
1694
|
async createViewSession(view, now) {
|
|
1648
1695
|
if (Number((await insertViewSessionStmt.run(view.id, view.csrfToken, now, view.userId, view.viewerSessionId, now)).changes) === 0) return false
|
|
@@ -1653,11 +1700,11 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1653
1700
|
const row = (await selectViewSessionStmt.get(id, viewerSessionId, now, now)) as ViewSessionRow | undefined
|
|
1654
1701
|
if (row == null) return null
|
|
1655
1702
|
await touchUserSeenStmt.run(now, row.viewerId, now)
|
|
1656
|
-
return
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1703
|
+
return viewSession(row)
|
|
1704
|
+
},
|
|
1705
|
+
async getViewSessionWithUser(id, viewerSessionId, now) {
|
|
1706
|
+
const row = (await selectViewSessionStmt.get(id, viewerSessionId, now, now)) as ViewSessionRow | undefined
|
|
1707
|
+
return row == null ? null : viewSession(row)
|
|
1661
1708
|
},
|
|
1662
1709
|
async ensureSessionUploadKey(id, candidate) {
|
|
1663
1710
|
const row = (await ensureUploadKeyStmt.get(candidate, id)) as { uploadKey: string } | undefined
|
|
@@ -2,11 +2,13 @@ import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
|
|
|
2
2
|
import { LINK_REPORT_SCHEMA } from './link-reports.ts'
|
|
3
3
|
import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
|
|
4
4
|
import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
|
|
5
|
+
import { GITHUB_LINK_SCHEMA, GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
|
|
6
|
+
import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
|
|
5
7
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
6
8
|
import { WebSocket } from 'ws'
|
|
7
9
|
import { type ManagedDb, type ManagedDbOptions, createManagedMethods } from './db-methods.ts'
|
|
8
10
|
import { revisionSchema } from './revisions.ts'
|
|
9
|
-
import { MANAGED_SCHEMA, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
11
|
+
import { MANAGED_SCHEMA, REPO_UPLOAD_INDEXES, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
10
12
|
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
11
13
|
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
12
14
|
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
@@ -25,7 +27,8 @@ export interface PgConnection {
|
|
|
25
27
|
rows: Record<string, unknown>[]; rowCount?: number | null
|
|
26
28
|
fields?: { name: string; dataTypeID: number }[]
|
|
27
29
|
}>
|
|
28
|
-
|
|
30
|
+
// After a failure the connection's state is unknown: discard it, never reuse it.
|
|
31
|
+
release(discard?: boolean): Promise<void>
|
|
29
32
|
}
|
|
30
33
|
export type PgConnect = () => Promise<PgConnection>
|
|
31
34
|
// Content/access mutations cooperate on this transaction-scoped lock. Reads,
|
|
@@ -36,7 +39,7 @@ async function currentSchema(db: PgConnection): Promise<boolean> {
|
|
|
36
39
|
const exists = (await db.query("SELECT to_regclass('managed_schema_version') AS name")).rows[0]?.['name']
|
|
37
40
|
if (!exists) return false
|
|
38
41
|
const versions = new Set((await db.query('SELECT version FROM managed_schema_version')).rows.map(row => Number(row['version'])))
|
|
39
|
-
return Array.from({ length:
|
|
42
|
+
return Array.from({ length: 31 }, (_, i) => i + 1).every(version => versions.has(version))
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
async function migrateRepositoryDefaultCache(db: PgConnection): Promise<void> {
|
|
@@ -142,6 +145,37 @@ async function migrateDependencyReviews(db: PgConnection): Promise<void> {
|
|
|
142
145
|
await db.query('INSERT INTO managed_schema_version VALUES (27)')
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
// Existing names are the sign-in accounts' own: none was chosen yet.
|
|
149
|
+
async function migrateGithubLinks(db: PgConnection): Promise<void> {
|
|
150
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 28')).rows.length > 0) return
|
|
151
|
+
await db.query(postgresSchema(GITHUB_LINK_SCHEMA))
|
|
152
|
+
await db.query(`ALTER TABLE managed_user ${GITHUB_LINK_USER_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
153
|
+
await db.query('UPDATE managed_user SET github_name = name')
|
|
154
|
+
await db.query('INSERT INTO managed_schema_version VALUES (28)')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function migrateRepositoryDependencyReviews(db: PgConnection): Promise<void> {
|
|
158
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 29')).rows.length > 0) return
|
|
159
|
+
await db.query(`ALTER TABLE managed_selected_repo ${REPOSITORY_DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
160
|
+
await db.query('INSERT INTO managed_schema_version VALUES (29)')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The lockfiles a repository's dependency review holds to, its columns
|
|
164
|
+
// added since 29 added there too; and a team's published reviews, listed by
|
|
165
|
+
// it, indexed so.
|
|
166
|
+
async function migrateRepositoryDependencyReviewLockfiles(db: PgConnection): Promise<void> {
|
|
167
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 30')).rows.length > 0) return
|
|
168
|
+
await db.query(`ALTER TABLE managed_selected_repo ${REPOSITORY_DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
169
|
+
await db.query('CREATE INDEX IF NOT EXISTS managed_dependency_review_team_idx ON managed_dependency_review_team(team_id, review_id)')
|
|
170
|
+
await db.query('INSERT INTO managed_schema_version VALUES (30)')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function migrateRepoUploadIndexes(db: PgConnection): Promise<void> {
|
|
174
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 31')).rows.length > 0) return
|
|
175
|
+
await db.query(REPO_UPLOAD_INDEXES)
|
|
176
|
+
await db.query('INSERT INTO managed_schema_version VALUES (31)')
|
|
177
|
+
}
|
|
178
|
+
|
|
145
179
|
async function initialize(db: PgConnection): Promise<void> {
|
|
146
180
|
if (await currentSchema(db)) return
|
|
147
181
|
await db.query('BEGIN')
|
|
@@ -215,7 +249,8 @@ async function initialize(db: PgConnection): Promise<void> {
|
|
|
215
249
|
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 11')).rows.length === 0) {
|
|
216
250
|
await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (11)')
|
|
217
251
|
}
|
|
218
|
-
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews
|
|
252
|
+
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews, migrateGithubLinks, migrateRepositoryDependencyReviews,
|
|
253
|
+
migrateRepositoryDependencyReviewLockfiles, migrateRepoUploadIndexes]) await migrate(db)
|
|
219
254
|
await db.query('COMMIT')
|
|
220
255
|
} catch (err) {
|
|
221
256
|
await db.query('ROLLBACK')
|
|
@@ -248,7 +283,7 @@ function requestConnections(connect: PgConnect) {
|
|
|
248
283
|
const connection = await connect()
|
|
249
284
|
let result: T
|
|
250
285
|
try { result = await work(connection) }
|
|
251
|
-
catch (error) { await connection.release().catch(() => {}); throw error }
|
|
286
|
+
catch (error) { await connection.release(true).catch(() => {}); throw error }
|
|
252
287
|
await connection.release()
|
|
253
288
|
return result
|
|
254
289
|
}
|
|
@@ -259,7 +294,7 @@ function requestConnections(connect: PgConnect) {
|
|
|
259
294
|
try { return await work(connection) }
|
|
260
295
|
catch (error) {
|
|
261
296
|
delete request.connection
|
|
262
|
-
await connection.release().catch(() => {})
|
|
297
|
+
await connection.release(true).catch(() => {})
|
|
263
298
|
throw error
|
|
264
299
|
}
|
|
265
300
|
})
|
|
@@ -275,7 +310,7 @@ function requestConnections(connect: PgConnect) {
|
|
|
275
310
|
finally {
|
|
276
311
|
request.ended = true
|
|
277
312
|
await request.queue
|
|
278
|
-
if (failed) await request.connection?.release().catch(() => {})
|
|
313
|
+
if (failed) await request.connection?.release(true).catch(() => {})
|
|
279
314
|
else await request.connection?.release()
|
|
280
315
|
}
|
|
281
316
|
}
|
|
@@ -285,7 +320,8 @@ function requestConnections(connect: PgConnect) {
|
|
|
285
320
|
// Exposed for parity tests against real PostgreSQL semantics via PGlite.
|
|
286
321
|
export async function openPostgresManagedDb(connect: PgConnect, options: ManagedDbOptions = {}, reuseConnections = false): Promise<ManagedDb> {
|
|
287
322
|
const initial = await connect()
|
|
288
|
-
try { await initialize(initial) }
|
|
323
|
+
try { await initialize(initial) } catch (err) { await initial.release(true).catch(() => {}); throw err }
|
|
324
|
+
await initial.release()
|
|
289
325
|
const { withConnection, withRequest } = requestConnections(connect)
|
|
290
326
|
const context = new AsyncLocalStorage<PgConnection>()
|
|
291
327
|
let closed = false
|
|
@@ -348,20 +384,52 @@ export async function openPostgresManagedDb(connect: PgConnect, options: Managed
|
|
|
348
384
|
return methods
|
|
349
385
|
}
|
|
350
386
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
387
|
+
// Idle connections stay open this long between leases, so a warm instance's
|
|
388
|
+
// requests and feed polls (three seconds apart) skip the TLS, WebSocket and
|
|
389
|
+
// Postgres handshake a new connection repeats.
|
|
390
|
+
const NEON_POOL_IDLE_MS = 5_000
|
|
391
|
+
|
|
392
|
+
type NeonPoolClient = {
|
|
393
|
+
query: PgConnection['query']
|
|
394
|
+
release(discard?: boolean): void
|
|
395
|
+
on(event: string, listener: (...args: unknown[]) => void): void
|
|
396
|
+
}
|
|
397
|
+
export type NeonPool = {
|
|
398
|
+
connect(): Promise<NeonPoolClient>
|
|
399
|
+
end(): Promise<void>
|
|
400
|
+
on(event: string, listener: (...args: unknown[]) => void): void
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// `attachPool` is Vercel's attachDatabasePool on serverless instances: each
|
|
404
|
+
// release keeps the invocation alive until idle connections have closed, so
|
|
405
|
+
// no socket survives a frozen instance.
|
|
406
|
+
export async function openNeonManagedDb(url: string, options: ManagedDbOptions = {}, attachPool?: (pool: NeonPool) => void): Promise<ManagedDb> {
|
|
407
|
+
// Reuse the e2e optional-driver boundary.
|
|
408
|
+
const { Pool, neonConfig } = await import('../server-e2e/neon-driver.ts') as unknown as {
|
|
409
|
+
Pool: new (config: { connectionString: string; max: number; idleTimeoutMillis: number }) => NeonPool
|
|
356
410
|
neonConfig: { webSocketConstructor: typeof WebSocket }
|
|
357
411
|
}
|
|
358
412
|
neonConfig.webSocketConstructor = WebSocket
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
413
|
+
// Leases stay unbounded, one per request or operation as before: a cap would
|
|
414
|
+
// stall a request that holds one while it awaits work needing another.
|
|
415
|
+
const pool = new Pool({ connectionString: url, max: Infinity, idleTimeoutMillis: NEON_POOL_IDLE_MS })
|
|
416
|
+
// The pool evicts an idle connection the server closed. Neither that nor a
|
|
417
|
+
// remote disconnect while leased may become an unhandled EventEmitter error;
|
|
418
|
+
// a leased one must instead reject its next query, and is then discarded.
|
|
419
|
+
pool.on('error', () => {})
|
|
420
|
+
attachPool?.(pool)
|
|
421
|
+
const failed = new WeakSet<NeonPoolClient>(), watched = new WeakSet<NeonPoolClient>()
|
|
422
|
+
let ending: Promise<void> | undefined
|
|
423
|
+
const end = () => ending ??= pool.end()
|
|
424
|
+
let db: ManagedDb
|
|
425
|
+
try {
|
|
426
|
+
db = await openPostgresManagedDb(async () => {
|
|
427
|
+
const client = await pool.connect()
|
|
428
|
+
if (!watched.has(client)) { watched.add(client); client.on('error', () => { failed.add(client) }) }
|
|
429
|
+
return { query: (sql, params) => client.query(sql, params),
|
|
430
|
+
release: discard => Promise.try(() => client.release(discard === true || failed.has(client))) }
|
|
431
|
+
}, options, true)
|
|
432
|
+
} catch (err) { await end().catch(() => {}); throw err }
|
|
433
|
+
const close = db.close
|
|
434
|
+
return { ...db, async close() { try { await close() } finally { await end() } } }
|
|
367
435
|
}
|
|
@@ -3,9 +3,14 @@ import { LINK_REPORT_SCHEMA } from './link-reports.ts'
|
|
|
3
3
|
import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
|
|
4
4
|
import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
|
|
5
5
|
import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
|
|
6
|
+
import { GITHUB_LINK_SCHEMA } from './github-links.ts'
|
|
6
7
|
|
|
7
8
|
// Created after the column migration, which predates it on older databases.
|
|
8
9
|
export const VIEW_SESSION_INDEX = 'CREATE INDEX IF NOT EXISTS managed_session_viewer_idx ON managed_session(viewer_session);'
|
|
10
|
+
// What a team's scopes join reports and bundles on: their repository, and the
|
|
11
|
+
// directory in it. Created after the column migrations, as above.
|
|
12
|
+
export const REPO_UPLOAD_INDEXES = `CREATE INDEX IF NOT EXISTS managed_report_repo_idx ON managed_report(repo_id, repo_directory);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS managed_bundle_repo_idx ON managed_bundle(repo_id, repo_directory);`
|
|
9
14
|
|
|
10
15
|
export const MANAGED_SCHEMA = `
|
|
11
16
|
CREATE TABLE IF NOT EXISTS managed_user (
|
|
@@ -64,7 +69,13 @@ CREATE TABLE IF NOT EXISTS managed_selected_repo (
|
|
|
64
69
|
added_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
65
70
|
added_at INTEGER NOT NULL,
|
|
66
71
|
updated_at INTEGER NOT NULL,
|
|
67
|
-
active INTEGER NOT NULL DEFAULT 1
|
|
72
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
73
|
+
-- Its pull requests' dependencies reviewed (repository-dependency-reviews.ts):
|
|
74
|
+
-- as many published reviews required of each version, and the commit at
|
|
75
|
+
-- whose lockfiles' versions none is (null for none).
|
|
76
|
+
dependency_review INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
dependency_reviews_required INTEGER NOT NULL DEFAULT 1,
|
|
78
|
+
dependency_review_since TEXT
|
|
68
79
|
) STRICT;
|
|
69
80
|
|
|
70
81
|
CREATE INDEX IF NOT EXISTS selected_repo_full_name_idx ON managed_selected_repo(full_name);
|
|
@@ -224,4 +235,4 @@ CREATE TABLE IF NOT EXISTS managed_team_user (
|
|
|
224
235
|
) STRICT;
|
|
225
236
|
|
|
226
237
|
CREATE INDEX IF NOT EXISTS team_user_user_idx ON managed_team_user(user_id);
|
|
227
|
-
` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA + TEAM_NPM_SCOPE_SCHEMA + DEPENDENCY_REVIEW_SCHEMA
|
|
238
|
+
` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA + TEAM_NPM_SCOPE_SCHEMA + DEPENDENCY_REVIEW_SCHEMA + GITHUB_LINK_SCHEMA
|
package/server-managed/db.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { migrateReportLocations } from './report-migration.ts'
|
|
|
7
7
|
import { initCommentMethods } from './comments.ts'
|
|
8
8
|
import { initActivityMethods } from './activity.ts'
|
|
9
9
|
import { revisionSchema } from './revisions.ts'
|
|
10
|
-
import { MANAGED_SCHEMA, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
10
|
+
import { MANAGED_SCHEMA, REPO_UPLOAD_INDEXES, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
11
|
+
import { GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
|
|
12
|
+
import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
|
|
11
13
|
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
12
14
|
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
13
15
|
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
@@ -79,6 +81,10 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
79
81
|
ensureColumn(db, table, 'storage_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
80
82
|
}
|
|
81
83
|
ensureColumn(db, 'managed_user', 'gh_tokens_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
84
|
+
// Existing names are the sign-in accounts' own: none was chosen yet.
|
|
85
|
+
for (const [column, type] of GITHUB_LINK_USER_COLUMNS) {
|
|
86
|
+
if (ensureColumn(db, 'managed_user', column, type) && column === 'github_name') db.exec('UPDATE managed_user SET github_name = name')
|
|
87
|
+
}
|
|
82
88
|
ensureColumn(db, 'managed_github_metadata', 'state_reason', GITHUB_STATE_REASON_COLUMN)
|
|
83
89
|
ensureColumn(db, 'managed_github_metadata', 'attempted_at', 'INTEGER')
|
|
84
90
|
// Existing public links must opt in too; never preserve an implicit grant.
|
|
@@ -107,6 +113,7 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
107
113
|
if (addedVisible) db.prepare('UPDATE managed_report SET visible = 1 WHERE visible = 0').run()
|
|
108
114
|
ensureColumn(db, 'managed_bundle', 'uploaded_by_login', 'TEXT')
|
|
109
115
|
ensureColumn(db, 'managed_bundle', 'repo_directory', "TEXT NOT NULL DEFAULT ''")
|
|
116
|
+
db.exec(REPO_UPLOAD_INDEXES)
|
|
110
117
|
ensureColumn(db, 'managed_bundle', 'visible', 'INTEGER NOT NULL DEFAULT 1')
|
|
111
118
|
// Existing rows stay NULL (unknown): some may have been built on the server.
|
|
112
119
|
ensureColumn(db, 'managed_bundle', 'provenance', 'TEXT')
|
|
@@ -116,6 +123,7 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
116
123
|
ensureColumn(db, 'managed_finding_issue', 'auto_fix_url', 'TEXT')
|
|
117
124
|
ensureColumn(db, 'managed_finding_issue', 'auto_fix_checked_at', 'INTEGER')
|
|
118
125
|
ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
|
|
126
|
+
for (const [column, type] of REPOSITORY_DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_selected_repo', column, type)
|
|
119
127
|
ensureColumn(db, 'managed_finding_triage_event', 'report_id', 'TEXT')
|
|
120
128
|
ensureColumn(db, 'managed_finding_triage_event', 'report', 'TEXT')
|
|
121
129
|
ensureColumn(db, 'managed_finding_triage_event', 'repo', 'TEXT')
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// A repository's Dependabot alerts for a repository risk scan, read with the
|
|
2
|
+
// App's installation token where the admin's own access lets them see them on
|
|
3
|
+
// GitHub, as the README's repository risk section tells.
|
|
4
|
+
import type { ManagedConfig } from './config.ts'
|
|
5
|
+
import type { SelectedRepo } from './db.ts'
|
|
6
|
+
import { type DependabotAlert, dependabotAlerts } from './dependabot-alerts.ts'
|
|
7
|
+
import { GithubApiError, type GithubIdentity, githubRepoPermission, repoAccessToken, repositoryInstallation } from './github-app.ts'
|
|
8
|
+
|
|
9
|
+
// The levels GitHub lets see a repository's Dependabot alerts by default
|
|
10
|
+
// (maintain's reads `write`).
|
|
11
|
+
const SEES_ALERTS: ReadonlySet<string> = new Set(['admin', 'write'])
|
|
12
|
+
// Failures of verifying the sign-in that are the sign-in's own: no token, or
|
|
13
|
+
// one of another account (repository-browser.ts getIdentity), or one GitHub no
|
|
14
|
+
// longer takes. Any other, as GitHub failing to answer, is told as it is.
|
|
15
|
+
const UNVERIFIED: ReadonlySet<string> = new Set(['no-repository', 'github-unauthorized'])
|
|
16
|
+
|
|
17
|
+
// What of a repository's reader (repository-browser.ts) the alerts are asked
|
|
18
|
+
// with: the user's verified identity, and their permission level where its
|
|
19
|
+
// access gate asked GitHub of it already.
|
|
20
|
+
export interface AlertsReader {
|
|
21
|
+
identity(): Promise<GithubIdentity>
|
|
22
|
+
permission(): string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Alerts read, and their `recheck`: the user's access to them checked again,
|
|
26
|
+
// asked of GitHub afresh, just before they are told (repository-risk.ts), as
|
|
27
|
+
// access to the repository is, as it may be withdrawn meanwhile.
|
|
28
|
+
export interface AlertsRead { alerts: DependabotAlert[]; recheck(): Promise<void> }
|
|
29
|
+
|
|
30
|
+
// `repo`'s Dependabot alerts (dependabot-alerts.ts), read with the token of
|
|
31
|
+
// the App's installation GitHub tells covers it now, its token checked to read
|
|
32
|
+
// it (github-app.ts repositoryInstallation), never the one its connection
|
|
33
|
+
// keeps, which may no longer cover it, once the user's own permission on it
|
|
34
|
+
// lets them see them; else refused, the alerts not asked:
|
|
35
|
+
// `dependabot-not-installed`, `dependabot-unverified`, or
|
|
36
|
+
// `dependabot-forbidden`, naming their permission. The token is asked each
|
|
37
|
+
// time of repoAccessToken, which renews it before it expires, as the recheck
|
|
38
|
+
// may come long after the alerts are read.
|
|
39
|
+
export async function appDependabotAlerts(config: ManagedConfig, repo: Pick<SelectedRepo, 'repoId' | 'fullName'>, reader: AlertsReader,
|
|
40
|
+
fetchImpl: typeof fetch = globalThis.fetch): Promise<AlertsRead> {
|
|
41
|
+
const id = await repositoryInstallation(config, repo.repoId, repo.fullName, fetchImpl)
|
|
42
|
+
const tokenNow = async () => {
|
|
43
|
+
const token = await repoAccessToken(config, id, fetchImpl)
|
|
44
|
+
if (token == null) throw new GithubApiError(403, 'dependabot-not-installed')
|
|
45
|
+
return token
|
|
46
|
+
}
|
|
47
|
+
const token = await tokenNow()
|
|
48
|
+
let who
|
|
49
|
+
try { who = await reader.identity() } catch (error) {
|
|
50
|
+
throw error instanceof GithubApiError && UNVERIFIED.has(error.message) ? new GithubApiError(403, 'dependabot-unverified') : error
|
|
51
|
+
}
|
|
52
|
+
const seen = (permission: string) => {
|
|
53
|
+
if (!SEES_ALERTS.has(permission)) {
|
|
54
|
+
throw new GithubApiError(403, 'dependabot-forbidden', null, `@${who.login}'s access to this repository (${permission}) doesn't let them see its Dependabot alerts on GitHub`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
seen(reader.permission() ?? await githubRepoPermission(repo.fullName, token, who, fetchImpl))
|
|
58
|
+
const alerts = await dependabotAlerts(repo.fullName, token, fetchImpl)
|
|
59
|
+
return { alerts, recheck: async () => { seen(await githubRepoPermission(repo.fullName, await tokenNow(), who, fetchImpl)) } }
|
|
60
|
+
}
|