@preventive/triage 1.0.0-alpha.46 → 1.0.0-alpha.48
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/dependency-paths.d.ts +2 -1
- package/client/dependency-paths.js +16 -4
- package/common/managed/agent-tasks.ts +12 -12
- package/common/managed/github-agents.ts +29 -10
- package/out/activity.js +4 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C2AVODGZ.js +36 -0
- package/out/{chunk-I2MDGSQX.js → chunk-IO7JTZ6O.js} +1 -1
- package/out/chunk-MOACFRJ4.js +3 -0
- package/out/chunk-O6M574JI.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PENGGKX7.js +233 -0
- package/out/{chunk-OKRLLE5I.js → chunk-PWFUOLZK.js} +1 -1
- package/out/chunk-SZD5CW5R.js +3 -0
- package/out/{chunk-G24A42V6.js → chunk-TQJDYHVE.js} +1 -1
- package/out/{chunk-3PS4KWD2.js → chunk-WCPJDAYA.js} +1 -1
- package/out/{chunk-7XRR4OFU.js → chunk-WYLQWYLA.js} +1 -1
- package/out/chunk-X6PJAK3T.js +1 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +26 -26
- package/out/client-sync.js +2 -2
- package/out/compare.js +14 -14
- package/out/dialogs.js +40 -40
- package/out/graph.js +11 -11
- package/out/index.html +12 -12
- package/out/scan.js +2 -2
- package/out/terminal.js +215 -215
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +9 -5
- package/risk/index.d.ts +4 -2
- package/risk/npm/assess.js +15 -4
- package/risk/npm/registry.js +6 -1
- package/risk/npm/score.js +1 -1
- package/server-managed/activity.ts +24 -11
- package/server-managed/agent-merges.ts +10 -8
- package/server-managed/app-agents.ts +8 -21
- package/server-managed/bundle-build-worker.js +7 -3
- package/server-managed/bundle-build.ts +5 -3
- package/server-managed/bundle-cache.ts +9 -3
- package/server-managed/bundle.ts +3 -4
- package/server-managed/cache-storage.ts +3 -2
- package/server-managed/commit-coauthors.ts +11 -10
- package/server-managed/commit-lockfiles.ts +22 -21
- package/server-managed/db-methods.ts +338 -187
- package/server-managed/db-neon.ts +31 -8
- package/server-managed/db.ts +2 -0
- package/server-managed/dependabot-access.ts +1 -1
- package/server-managed/dependency-review-comments.ts +7 -4
- package/server-managed/dependency-reviews.ts +71 -33
- package/server-managed/github-accounts.ts +28 -1
- package/server-managed/github-app.ts +27 -23
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +8 -1
- package/server-managed/github-metadata.ts +2 -2
- package/server-managed/http-response.ts +28 -10
- package/server-managed/http.ts +736 -247
- package/server-managed/index.ts +2 -2
- package/server-managed/link-reports.ts +5 -1
- package/server-managed/managed-issues.ts +3 -0
- package/server-managed/npm-delta.ts +16 -0
- package/server-managed/npm-kept.ts +51 -10
- package/server-managed/npm-loads.ts +48 -28
- package/server-managed/npm-manifest.ts +1 -1
- package/server-managed/npm-packages.ts +31 -15
- package/server-managed/npm-risk.ts +64 -17
- package/server-managed/object-storage.ts +4 -3
- package/server-managed/pretty-print.ts +45 -12
- package/server-managed/public-workspace.ts +3 -3
- package/server-managed/pull-dependencies.ts +84 -36
- package/server-managed/pull-dependency-reviews.ts +13 -10
- package/server-managed/rename-diff-worker.js +1 -1
- package/server-managed/report-store.ts +16 -11
- package/server-managed/repository-agents.ts +9 -6
- package/server-managed/repository-browser.ts +17 -5
- package/server-managed/repository-code.ts +180 -17
- package/server-managed/repository-dependency-reviews.ts +7 -0
- package/server-managed/repository-history.ts +85 -152
- package/server-managed/repository-issues.ts +27 -13
- package/server-managed/repository-pulse.ts +9 -3
- package/server-managed/repository-squashes.ts +145 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +3 -2
- package/server-managed/session.ts +34 -12
- package/server-managed/solidity-entry-points.ts +5 -3
- package/server-managed/sql-postgres.ts +3 -0
- package/server-managed/sql.ts +17 -8
- package/server-managed/storage-db.ts +11 -3
- package/server-managed/storage-encryption.ts +23 -12
- package/server-managed/storage-maintenance.ts +5 -2
- package/server-managed/storage-payload.ts +18 -0
- package/server-managed/storage-stores.ts +3 -2
- package/server-managed/storage.ts +4 -2
- package/server-managed/team-feed.ts +19 -12
- package/server-managed/team-npm-scopes.ts +29 -0
- package/server-managed/team-reports.ts +74 -17
- package/server-managed/uploads.ts +29 -9
- package/out/chunk-4UFAMJAX.js +0 -1
- package/out/chunk-BOIXMUU5.js +0 -3
- package/out/chunk-CC4OM6TZ.js +0 -3
- package/out/chunk-JKGQ3O7O.js +0 -6
- package/out/chunk-LE5ZWLG4.js +0 -235
- package/out/chunk-MSION3G4.js +0 -36
- package/out/chunk-S6GIXWQW.js +0 -52
- package/out/chunk-V3KWNNMY.js +0 -1
- package/out/chunk-WQ74ZBZF.js +0 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LinkReportStore, linkReportMethods, linkRevision } from './link-reports.ts'
|
|
1
|
+
import { type LinkReportStore, linkReportMethods, linkRevision, linkRevisionOf } 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
4
|
import { type RepositoryDependencyReviewStore, repositoryDependencyReviewMethods } from './repository-dependency-reviews.ts'
|
|
@@ -13,7 +13,7 @@ import { preferredSlug } from './slugs.ts'
|
|
|
13
13
|
import { type CommentStore, commentMethods } from './comments.ts'
|
|
14
14
|
import { type ActivityStore, activityMethods } from './activity.ts'
|
|
15
15
|
import { type GithubCommitStore, type GithubMetadataStore, githubCommitMethods, githubMetadataMethods } from './github-metadata.ts'
|
|
16
|
-
import { type ManagedIssueStore, managedIssueMethods } from './managed-issues.ts'
|
|
16
|
+
import { type ManagedIssue, type ManagedIssueStore, managedIssueMethods } from './managed-issues.ts'
|
|
17
17
|
import type { IssueFixUpdate } from './github-issue-links.ts'
|
|
18
18
|
import { type BundleBuildLeaseStore, bundleBuildLeaseMethods } from './bundle-build-leases.ts'
|
|
19
19
|
import { type UpstreamCacheStore, upstreamCacheMethods } from './upstream-cache.ts'
|
|
@@ -23,7 +23,7 @@ import { type WorkspaceShareStore, workspaceShareMethods } from './workspace-sha
|
|
|
23
23
|
import { ManagedMutationError, type ManagementStore, managementMethods } from './management.ts'
|
|
24
24
|
import { type ManagementCatalogStore, managementCatalogMethods } from './management-catalog.ts'
|
|
25
25
|
import { type StorageKey } from '../server-common/storage-crypto.ts'
|
|
26
|
-
import { type StorageDb, type StoredTokens, decodeStorageTokens, encodeStorageTokens, storageMethods, storageState, validateStorageUpload } from './storage-db.ts'
|
|
26
|
+
import { type StorageDb, type StoredTokens, decodeStorageTokens, encodeStorageTokens, storageMethods, storageState, storageStateColumns, storageStateOf, validateStorageUpload } from './storage-db.ts'
|
|
27
27
|
|
|
28
28
|
// A managed user identity (the subset of GitHub's `GET /user` we keep). Input
|
|
29
29
|
// to the upsert; `githubUserId` is the provider lookup key, never exposed to
|
|
@@ -402,6 +402,9 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
|
|
|
402
402
|
deleteSession(id: string): Promise<void>
|
|
403
403
|
deleteExpiredSessions(now: number): Promise<number>
|
|
404
404
|
listUsers(): Promise<AdminUser[]>
|
|
405
|
+
// One user by id, as their sessions read them, or null; for an action naming
|
|
406
|
+
// one, where listUsers would work out every user's latest activity.
|
|
407
|
+
getUser(id: string): Promise<StoredUser | null>
|
|
405
408
|
// Set a user's role; resolves true iff a matching user row was updated.
|
|
406
409
|
setUserRole(id: string, role: Role): Promise<boolean>
|
|
407
410
|
// Persist / read a user's GitHub token (for on-demand repo listing).
|
|
@@ -491,7 +494,7 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
|
|
|
491
494
|
getBundleByIntegrity(integrity: string): Promise<ManagedBundle | null>
|
|
492
495
|
resolveBundleUpload(integrity: string): Promise<ManagedBundle | null>
|
|
493
496
|
getBundle(id: string): Promise<ManagedBundle | null>
|
|
494
|
-
// One
|
|
497
|
+
// One statement reads the live session, bundle and grants. Passing a team
|
|
495
498
|
// (or null for any team) also checks advisory security access; omission skips it.
|
|
496
499
|
getBundleAccessSnapshot(sessionId: string, now: number, id: string, advisoriesTeamId?: string | null): Promise<{
|
|
497
500
|
bundle: ManagedBundle | null; canReadAdvisories: boolean
|
|
@@ -535,6 +538,9 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
|
|
|
535
538
|
// Whether `userId` may read `reportId`: true iff the report's repo belongs to
|
|
536
539
|
// a non-hidden team the user is a member of. Backs the team-scoped report view endpoint.
|
|
537
540
|
userCanReadReport(userId: string, reportId: string): Promise<boolean>
|
|
541
|
+
// What viewing `reportId` turns on, in one read: who uploaded it, whether it
|
|
542
|
+
// is published, and whether userCanReadReport; null where there is none.
|
|
543
|
+
getReportView(userId: string, reportId: string): Promise<{ uploadedBy: string | null; visible: boolean; readable: boolean } | null>
|
|
538
544
|
// The viewer's effective visibility permissions for a report, OR'd across the
|
|
539
545
|
// teams (holding the report's repo) the user belongs to — drives the content
|
|
540
546
|
// filter (a viewer without a permission has those findings stripped).
|
|
@@ -563,6 +569,10 @@ const viewSession = (row: ViewSessionRow) => ({
|
|
|
563
569
|
|
|
564
570
|
type UserRow = { id: string; login: string; name: string | null; role: Role; created: number; lastSeen: number | null; lastActivity: number | null }
|
|
565
571
|
|
|
572
|
+
// Rows a statement gathered into one JSON array value (json_group_array): its
|
|
573
|
+
// text on SQLite, parsed already on Postgres, where none gather as NULL.
|
|
574
|
+
const jsonRows = <T>(value: unknown): T[] => value == null ? [] : (typeof value === 'string' ? JSON.parse(value) : value) as T[]
|
|
575
|
+
|
|
566
576
|
// Shared by listing, access checks, and permission aggregation. A team sees
|
|
567
577
|
// reports rooted at its path or below it. Literal substring comparison keeps
|
|
568
578
|
// separators, case, and SQL wildcard characters significant.
|
|
@@ -572,6 +582,46 @@ const REPORT_IN_TEAM_PATH_SQL = `(tr.path IS NULL OR tr.path = ''
|
|
|
572
582
|
|
|
573
583
|
const BUNDLE_IN_TEAM_PATH_SQL = REPORT_IN_TEAM_PATH_SQL.replaceAll('r.repo_directory', 'b.repo_directory')
|
|
574
584
|
|
|
585
|
+
// What a user's team catalog reads (listTeamsForUser), for `user`: a
|
|
586
|
+
// placeholder, or a column of a statement reading it with more. Their teams,
|
|
587
|
+
// `dependencyReview` where the user reviews dependencies in the team
|
|
588
|
+
// (dependency-reviews.ts reviewsIn): one that reviews them, and a member
|
|
589
|
+
// who manages, or triages and may see its dependencies; an admin in any.
|
|
590
|
+
const USER_TEAMS_SQL = (user: string) => `SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
591
|
+
CASE WHEN t.dependency_review = 1 AND (u.role IN ('admin', 'manage') OR (u.role = 'triage' AND tu.view_dependencies = 1))
|
|
592
|
+
THEN 1 ELSE 0 END AS dependencyReview,
|
|
593
|
+
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
594
|
+
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
|
|
595
|
+
WHERE tu.user_id = ${user} AND t.hidden = 0`
|
|
596
|
+
// Their teams' repository scopes.
|
|
597
|
+
const USER_TEAM_SCOPES_SQL = (user: string) => `SELECT tr.team_id AS teamId, tr.repo_id AS repoId, tr.path AS path, sr.full_name AS fullName
|
|
598
|
+
FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
599
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
600
|
+
JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
601
|
+
WHERE tu.user_id = ${user}`
|
|
602
|
+
// Reports attached to the repos of their teams, tagged by team (a report
|
|
603
|
+
// shows under every team whose repo it's attached to).
|
|
604
|
+
const USER_TEAM_REPORTS_SQL = (user: string) => `SELECT DISTINCT tr.team_id AS teamId, r.id AS id, r.slug AS slug, r.filename AS filename, r.analyzer, r.visible, r.uploaded_at,
|
|
605
|
+
r.sha256 AS sha256, r.bundle_id AS bundleId, r.repo_id AS repoId, r.repo_directory AS repoDirectory, sr.full_name AS repoFullName,
|
|
606
|
+
tu.view_dependencies AS dependencies, tu.view_security AS security,
|
|
607
|
+
(SELECT role FROM managed_user WHERE id = tu.user_id) AS role
|
|
608
|
+
FROM managed_team_user tu
|
|
609
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
610
|
+
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
611
|
+
JOIN managed_report r ON r.repo_id = tr.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
612
|
+
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
613
|
+
WHERE tu.user_id = ${user} AND (r.visible = 1 OR (SELECT role FROM managed_user WHERE id = tu.user_id) IN ('admin', 'manage'))`
|
|
614
|
+
// Bundles, scoped through the same team -> repository links as reports.
|
|
615
|
+
// Hidden bundles remain available to managers within their team scopes.
|
|
616
|
+
const USER_TEAM_BUNDLES_SQL = (user: string) => `SELECT DISTINCT tr.team_id AS teamId, b.id AS id, b.slug AS slug, b.integrity AS integrity,
|
|
617
|
+
b.filename AS filename, b.kind, b.visible, b.byte_size AS byteSize, b.repo_id AS repoId, b.repo_directory AS repoDirectory, sr.full_name AS repoFullName, b.uploaded_at, b.provenance
|
|
618
|
+
FROM managed_team_user tu
|
|
619
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
620
|
+
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
621
|
+
JOIN managed_bundle b ON b.repo_id = tr.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
622
|
+
JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
623
|
+
WHERE tu.user_id = ${user} AND (b.visible = 1 OR (SELECT role FROM managed_user WHERE id = tu.user_id) IN ('admin', 'manage'))`
|
|
624
|
+
|
|
575
625
|
// Whether a repeated upload/build renames the stored row. Uploads never rename
|
|
576
626
|
// a built row, or one whose provenance is unknown; builds rename either. The
|
|
577
627
|
// kind must stay readable from its blob store (sourcemaps are stored apart):
|
|
@@ -580,6 +630,49 @@ const BUNDLE_RENAMED_SQL = `(:rename = 1 AND (provenance = 'upload' OR (:provena
|
|
|
580
630
|
AND (COALESCE(kind, '') = COALESCE(:kind, '') OR (kind IS NULL AND :kind = 'stasis'))
|
|
581
631
|
AND (filename <> :filename OR kind IS DISTINCT FROM :kind))`
|
|
582
632
|
|
|
633
|
+
// The rows a user's team catalog is built of (USER_TEAMS_SQL and the rest).
|
|
634
|
+
type UserTeamRows = {
|
|
635
|
+
teams: { id: string; slug: string; name: string; dependencyReview: number; dependencies: number; security: number }[]
|
|
636
|
+
scopes: { teamId: string; repoId: number; path: string; fullName: string }[]
|
|
637
|
+
reports: {
|
|
638
|
+
teamId: string; id: string; slug: string; filename: string; analyzer: string | null; sha256: string; bundleId: string | null; repoId: number
|
|
639
|
+
repoDirectory: string; repoFullName: string | null; visible: number; dependencies: number; security: number; role: string
|
|
640
|
+
}[]
|
|
641
|
+
bundles: (Omit<UserTeamBundle, 'visible'> & { teamId: string; visible: number })[]
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// A user's team catalog from its rows, `links` the link reports' revision.
|
|
645
|
+
function userTeams(teams: UserTeamRows['teams'], scopes: UserTeamRows['scopes'], links: string, reports: UserTeamRows['reports'], bundles: UserTeamRows['bundles']): UserTeam[] {
|
|
646
|
+
const reportsByTeam = new Map<string, UserTeamReport[]>()
|
|
647
|
+
for (const r of reports) {
|
|
648
|
+
const list = reportsByTeam.get(r.teamId) ?? []
|
|
649
|
+
const cacheKey = createHash('sha256').update(JSON.stringify([
|
|
650
|
+
r.sha256, r.bundleId, r.filename, r.repoId, r.repoDirectory, r.repoFullName, r.dependencies, r.security, r.role, links,
|
|
651
|
+
])).digest('base64url')
|
|
652
|
+
list.push({ id: r.id, slug: r.slug, filename: r.filename, visible: r.visible === 1, analyzer: r.analyzer, repoFullName: r.repoFullName, repoDirectory: r.repoDirectory, cacheKey })
|
|
653
|
+
reportsByTeam.set(r.teamId, list)
|
|
654
|
+
}
|
|
655
|
+
const bundlesByTeam = new Map<string, UserTeamBundle[]>()
|
|
656
|
+
for (const b of bundles) {
|
|
657
|
+
const list = bundlesByTeam.get(b.teamId) ?? []
|
|
658
|
+
list.push({ id: b.id, slug: b.slug, integrity: b.integrity, filename: b.filename, visible: b.visible === 1,
|
|
659
|
+
kind: b.kind, byteSize: b.byteSize, repoId: b.repoId, repoDirectory: b.repoDirectory, repoFullName: b.repoFullName, provenance: b.provenance })
|
|
660
|
+
bundlesByTeam.set(b.teamId, list)
|
|
661
|
+
}
|
|
662
|
+
return teams.map((t) => ({
|
|
663
|
+
id: t.id, slug: t.slug, name: t.name,
|
|
664
|
+
...(t.dependencyReview === 1 ? { dependencyReview: true } : {}),
|
|
665
|
+
permissions: { dependencies: t.dependencies === 1, security: t.security === 1 },
|
|
666
|
+
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
667
|
+
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
668
|
+
])).digest('base64url'),
|
|
669
|
+
repos: scopes.filter(scope => scope.teamId === t.id).map(scope => ({ repoId: scope.repoId, fullName: scope.fullName, path: scope.path || null }))
|
|
670
|
+
.toSorted((a, b) => a.fullName.localeCompare(b.fullName) || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
671
|
+
reports: reportsByTeam.get(t.id) ?? [],
|
|
672
|
+
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
673
|
+
}))
|
|
674
|
+
}
|
|
675
|
+
|
|
583
676
|
// Prepare every statement the store uses, returned as a bag the factory
|
|
584
677
|
// destructures — keeps openSqliteManagedDb itself small (one place per query).
|
|
585
678
|
function prepareStatements(db: ManagedSql) {
|
|
@@ -604,13 +697,6 @@ function prepareStatements(db: ManagedSql) {
|
|
|
604
697
|
insertSessionStmt: db.prepare(
|
|
605
698
|
`INSERT INTO managed_session (id, user_id, csrf_token, created_at, expires_at) VALUES (?, ?, ?, ?, ?)`,
|
|
606
699
|
),
|
|
607
|
-
selectSessionStmt: db.prepare(
|
|
608
|
-
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
609
|
-
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
610
|
-
FROM managed_session s
|
|
611
|
-
JOIN managed_user u ON u.id = s.user_id
|
|
612
|
-
WHERE s.id = ? AND s.expires_at > ?`,
|
|
613
|
-
),
|
|
614
700
|
selectOwnSessionStmt: db.prepare(
|
|
615
701
|
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
616
702
|
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
@@ -618,6 +704,15 @@ function prepareStatements(db: ManagedSql) {
|
|
|
618
704
|
JOIN managed_user u ON u.id = s.user_id
|
|
619
705
|
WHERE s.id = ? AND s.expires_at > ? AND s.viewer_session IS NULL`,
|
|
620
706
|
),
|
|
707
|
+
// selectOwnSessionStmt with touchUserSeenStmt in one statement: the user of
|
|
708
|
+
// a live own session seen, and returned with it.
|
|
709
|
+
touchOwnSessionStmt: db.prepare(
|
|
710
|
+
`UPDATE managed_user SET last_seen_at = CASE WHEN last_seen_at IS NULL OR last_seen_at < ? THEN ? ELSE last_seen_at END
|
|
711
|
+
WHERE id = (SELECT user_id FROM managed_session WHERE id = ? AND expires_at > ? AND viewer_session IS NULL)
|
|
712
|
+
RETURNING (SELECT id FROM managed_session WHERE id = ?) AS id, (SELECT csrf_token FROM managed_session WHERE id = ?) AS csrf,
|
|
713
|
+
(SELECT expires_at FROM managed_session WHERE id = ?) AS exp, (SELECT upload_key FROM managed_session WHERE id = ?) AS uploadKey,
|
|
714
|
+
id AS uid, login AS login, name AS name, avatar_url AS avatar, role AS role`,
|
|
715
|
+
),
|
|
621
716
|
selectViewSessionStmt: db.prepare(
|
|
622
717
|
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
623
718
|
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role,
|
|
@@ -651,6 +746,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
651
746
|
) AS events) AS lastActivity
|
|
652
747
|
FROM managed_user u ORDER BY u.created_at ASC, u.login ASC`,
|
|
653
748
|
),
|
|
749
|
+
selectUserStmt: db.prepare(`SELECT id, login, name, avatar_url AS avatarUrl, role FROM managed_user WHERE id = ?`),
|
|
654
750
|
touchUserSeenStmt: db.prepare(
|
|
655
751
|
`UPDATE managed_user SET last_seen_at = ? WHERE id = ? AND (last_seen_at IS NULL OR last_seen_at < ?)`,
|
|
656
752
|
),
|
|
@@ -660,9 +756,14 @@ function prepareStatements(db: ManagedSql) {
|
|
|
660
756
|
SET gh_access_token = ?, gh_refresh_token = ?, gh_token_expires_at = ?, gh_tokens_encrypted = ?, updated_at = ?
|
|
661
757
|
WHERE id = ?`,
|
|
662
758
|
),
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
759
|
+
// A user's GitHub tokens with storage-db.ts storageState's read, in one
|
|
760
|
+
// statement.
|
|
761
|
+
selectTokensStateStmt: db.prepare(
|
|
762
|
+
`SELECT u.id AS userId, u.gh_access_token AS access, u.gh_refresh_token AS refresh, u.gh_token_expires_at AS exp, u.gh_tokens_encrypted AS encrypted,
|
|
763
|
+
e.id AS stateId, ${storageStateColumns('e')}
|
|
764
|
+
FROM (SELECT 1 AS one) AS o
|
|
765
|
+
LEFT JOIN managed_user u ON u.id = ?
|
|
766
|
+
LEFT JOIN managed_storage_encryption e ON e.id = 1`,
|
|
666
767
|
),
|
|
667
768
|
// Concurrent first requests keep whichever key was stored first.
|
|
668
769
|
ensureUploadKeyStmt: db.prepare(
|
|
@@ -752,42 +853,68 @@ function prepareStatements(db: ManagedSql) {
|
|
|
752
853
|
`SELECT id FROM managed_report WHERE sha256 = ? AND COALESCE(analyzer, '') = COALESCE(?, '')
|
|
753
854
|
ORDER BY uploaded_at, id LIMIT 1`,
|
|
754
855
|
),
|
|
755
|
-
|
|
756
|
-
|
|
856
|
+
// A report access snapshot's reads in one statement, so one snapshot of
|
|
857
|
+
// the database answers them all: the session's user; those of the
|
|
858
|
+
// requested reports (a JSON array of ids) they may read, each with the
|
|
859
|
+
// grants their teams give them (a JSON array); and the enabled link
|
|
860
|
+
// reports (linkRevision).
|
|
861
|
+
selectReportSnapshotStmt: db.prepare(
|
|
862
|
+
`WITH viewer AS (
|
|
863
|
+
SELECT u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
864
|
+
FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
865
|
+
WHERE s.id = ? AND s.expires_at > ?
|
|
866
|
+
), requested AS (SELECT value AS id FROM json_each(?)), report_grants AS (
|
|
757
867
|
SELECT r.id AS id, MAX(tu.view_dependencies) AS dependencies, MAX(tu.view_security) AS security
|
|
758
868
|
FROM managed_report r JOIN requested q ON q.id = r.id
|
|
759
869
|
JOIN managed_team_repo tr ON tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
760
|
-
JOIN managed_team_user tu ON tu.team_id = tr.team_id AND tu.user_id =
|
|
870
|
+
JOIN managed_team_user tu ON tu.team_id = tr.team_id AND tu.user_id = (SELECT uid FROM viewer)
|
|
761
871
|
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
762
872
|
GROUP BY r.id
|
|
763
873
|
)
|
|
764
|
-
SELECT
|
|
765
|
-
r.
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
selectTeamRepositoriesStmt: db.prepare(
|
|
776
|
-
`SELECT tr.repo_id AS repoId, sr.full_name AS github, tr.path AS path
|
|
777
|
-
FROM managed_team_repo tr JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
778
|
-
WHERE tr.team_id = ? ORDER BY tr.repo_id, tr.path`,
|
|
874
|
+
SELECT v.uid AS uid, v.login AS login, v.name AS name, v.avatar AS avatar, v.role AS role,
|
|
875
|
+
(SELECT json_group_array(json_object('id', r.id, 'filename', r.filename, 'byteSize', r.byte_size, 'sha256', r.sha256,
|
|
876
|
+
'repoDirectory', r.repo_directory, 'repoFullName', sr.full_name,
|
|
877
|
+
'dependencies', COALESCE(g.dependencies, 0), 'security', COALESCE(g.security, 0)) ORDER BY r.id)
|
|
878
|
+
FROM managed_report r JOIN requested q ON q.id = r.id
|
|
879
|
+
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
880
|
+
LEFT JOIN report_grants g ON g.id = r.id
|
|
881
|
+
WHERE v.role = 'admin' OR (v.role = 'manage' AND r.uploaded_by = v.uid)
|
|
882
|
+
OR (g.id IS NOT NULL AND (v.role = 'manage' OR r.visible = 1))) AS reports,
|
|
883
|
+
(SELECT json_group_array(json_object('id', id, 'sha256', sha256) ORDER BY id) FROM managed_link_report WHERE enabled = 1) AS links
|
|
884
|
+
FROM viewer v`,
|
|
779
885
|
),
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
886
|
+
// A team snapshot's reads in one statement, so one snapshot of the
|
|
887
|
+
// database answers them all: the session's user; whether they see the
|
|
888
|
+
// team; the team's visible reports in the scopes it grants them, with the
|
|
889
|
+
// grants, and the hidden one asked for where their role may preview it;
|
|
890
|
+
// the team's repositories (each a JSON array); and the enabled link
|
|
891
|
+
// reports (linkRevision).
|
|
892
|
+
selectTeamSnapshotStmt: db.prepare(
|
|
893
|
+
`WITH viewer AS (
|
|
894
|
+
SELECT u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
895
|
+
FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
896
|
+
WHERE s.id = ? AND s.expires_at > ?
|
|
897
|
+
)
|
|
898
|
+
SELECT v.uid AS uid, v.login AS login, v.name AS name, v.avatar AS avatar, v.role AS role,
|
|
899
|
+
EXISTS(SELECT 1 FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id
|
|
900
|
+
WHERE tu.user_id = v.uid AND tu.team_id = ? AND t.hidden = 0) AS member,
|
|
901
|
+
(SELECT json_group_array(json_object('id', x.id, 'filename', x.filename, 'byteSize', x.byteSize, 'sha256', x.sha256,
|
|
902
|
+
'visible', x.visible, 'repoDirectory', x.repoDirectory, 'repoFullName', x.repoFullName,
|
|
903
|
+
'dependencies', x.dependencies, 'security', x.security) ORDER BY x.id)
|
|
904
|
+
FROM (SELECT DISTINCT r.id AS id, r.filename AS filename, r.byte_size AS byteSize, r.sha256 AS sha256, r.visible AS visible,
|
|
905
|
+
r.repo_directory AS repoDirectory, sr.full_name AS repoFullName,
|
|
906
|
+
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
907
|
+
FROM managed_team_user tu
|
|
908
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
909
|
+
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
910
|
+
JOIN managed_report r ON r.repo_id = tr.repo_id AND ${REPORT_IN_TEAM_PATH_SQL}
|
|
911
|
+
LEFT JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
912
|
+
WHERE tu.user_id = v.uid AND tu.team_id = ? AND (r.visible = 1 OR (v.role IN ('admin', 'manage') AND r.id = ?))) AS x) AS reports,
|
|
913
|
+
(SELECT json_group_array(json_object('repoId', tr.repo_id, 'github', sr.full_name, 'path', tr.path) ORDER BY tr.repo_id, tr.path)
|
|
914
|
+
FROM managed_team_repo tr JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
915
|
+
WHERE tr.team_id = ?) AS repositories,
|
|
916
|
+
(SELECT json_group_array(json_object('id', id, 'sha256', sha256) ORDER BY id) FROM managed_link_report WHERE enabled = 1) AS links
|
|
917
|
+
FROM viewer v`,
|
|
791
918
|
),
|
|
792
919
|
reportFilenamesWithBundleHashStmt: db.prepare(`SELECT DISTINCT filename FROM managed_report WHERE bundle_id = ? AND sha256 = ?`),
|
|
793
920
|
deleteReportStmt: db.prepare(`DELETE FROM managed_report WHERE id = ?`),
|
|
@@ -838,17 +965,40 @@ function prepareStatements(db: ManagedSql) {
|
|
|
838
965
|
WHERE t.finding_id IN (SELECT value FROM json_each(?))
|
|
839
966
|
ORDER BY t.finding_id ASC`,
|
|
840
967
|
),
|
|
968
|
+
// A set of findings' triage (selectTriageStmt), comments (comments.ts
|
|
969
|
+
// listComments) and issues (managed-issues.ts listManagedIssues), each
|
|
970
|
+
// a JSON array of the rows those read, in their order, from one snapshot.
|
|
971
|
+
selectAnnotationsStmt: db.prepare(
|
|
972
|
+
`WITH requested AS (SELECT value AS id FROM json_each(?))
|
|
973
|
+
SELECT (SELECT json_group_array(json_object('findingId', t.finding_id, 'color', t.color, 'triage', t.triage, 'comment', t.comment,
|
|
974
|
+
'fix', t.fix, 'flagged', t.flagged, 'updatedByLogin', COALESCE(u.login, t.updated_by_login), 'updatedAt', t.updated_at)
|
|
975
|
+
ORDER BY t.finding_id ASC)
|
|
976
|
+
FROM managed_finding_triage t LEFT JOIN managed_user u ON u.id = t.updated_by
|
|
977
|
+
WHERE t.finding_id IN (SELECT id FROM requested)) AS triage,
|
|
978
|
+
(SELECT json_group_array(json_object('id', c.id, 'findingId', c.finding_id, 'body', c.body, 'authorId', c.author_id,
|
|
979
|
+
'authorLogin', COALESCE(u.login, c.author_login), 'createdAt', c.created_at, 'updatedAt', c.updated_at, 'version', c.version)
|
|
980
|
+
ORDER BY c.created_at NULLS FIRST, c.id)
|
|
981
|
+
FROM managed_finding_comment c LEFT JOIN managed_user u ON u.id = c.author_id
|
|
982
|
+
WHERE c.finding_id IN (SELECT id FROM requested)) AS comments,
|
|
983
|
+
(SELECT json_group_array(json_object('findingId', i.finding_id, 'repoId', i.repo_id, 'repository', i.repository, 'requestId', i.request_id,
|
|
984
|
+
'issueUrl', i.issue_url, 'autoFixUrl', i.auto_fix_url, 'autoFixCheckedAt', i.auto_fix_checked_at,
|
|
985
|
+
'createdBy', i.created_by, 'createdAt', i.created_at) ORDER BY i.finding_id)
|
|
986
|
+
FROM managed_finding_issue i WHERE i.finding_id IN (SELECT id FROM requested)) AS issues`,
|
|
987
|
+
),
|
|
841
988
|
selectTriageFixesStmt: db.prepare(`SELECT DISTINCT fix FROM managed_finding_triage
|
|
842
989
|
WHERE finding_id IN (SELECT value FROM json_each(?)) AND fix IS NOT NULL ORDER BY fix`),
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
990
|
+
// Each finding's latest triage batch, comment versions and linked issues,
|
|
991
|
+
// in one statement.
|
|
992
|
+
annotationRevisionStmt: db.prepare(
|
|
993
|
+
`WITH requested AS (SELECT value AS id FROM json_each(?))
|
|
994
|
+
SELECT (SELECT json_group_array(json_object('id', t.finding_id, 'revision', (SELECT e.batch_id FROM managed_finding_triage_event e
|
|
995
|
+
WHERE e.finding_id = t.finding_id ORDER BY e.seq DESC LIMIT 1)) ORDER BY t.finding_id)
|
|
996
|
+
FROM managed_finding_triage t WHERE t.finding_id IN (SELECT id FROM requested)) AS triage,
|
|
997
|
+
(SELECT json_group_array(json_object('id', c.id, 'version', c.version) ORDER BY c.id)
|
|
998
|
+
FROM managed_finding_comment c WHERE c.finding_id IN (SELECT id FROM requested)) AS comments,
|
|
999
|
+
(SELECT json_group_array(json_object('findingId', i.finding_id, 'issueUrl', i.issue_url, 'autoFixUrl', i.auto_fix_url) ORDER BY i.finding_id)
|
|
1000
|
+
FROM managed_finding_issue i WHERE i.finding_id IN (SELECT id FROM requested) AND i.issue_url IS NOT NULL) AS issues`,
|
|
1001
|
+
),
|
|
852
1002
|
insertBundleStmt: db.prepare(
|
|
853
1003
|
`WITH candidate(id, slug) AS (VALUES (?, ?))
|
|
854
1004
|
INSERT INTO managed_bundle (id, slug, integrity, filename, kind, byte_size, uploaded_by, uploaded_by_login, repo_id, repo_directory, visible, uploaded_at, data_key, storage_encrypted, provenance, build_conditions)
|
|
@@ -939,51 +1089,39 @@ function prepareStatements(db: ManagedSql) {
|
|
|
939
1089
|
renameTeamStmt: db.prepare(`UPDATE managed_team SET name = ?, updated_at = ? WHERE id = ?`),
|
|
940
1090
|
setTeamHiddenStmt: db.prepare(`UPDATE managed_team SET hidden = ?, updated_at = ? WHERE id = ?`),
|
|
941
1091
|
selectTeamsStmt: db.prepare(`SELECT id, slug, name, hidden FROM managed_team ORDER BY name ASC`),
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
// Bundles are scoped through the same team -> repository links as reports.
|
|
976
|
-
// Hidden bundles remain available to managers within their team scopes.
|
|
977
|
-
selectUserTeamBundlesStmt: db.prepare(
|
|
978
|
-
`SELECT DISTINCT tr.team_id AS teamId, b.id AS id, b.slug AS slug, b.integrity AS integrity,
|
|
979
|
-
b.filename AS filename, b.kind, b.visible, b.byte_size AS byteSize, b.repo_id AS repoId, b.repo_directory AS repoDirectory, sr.full_name AS repoFullName, b.uploaded_at, b.provenance
|
|
980
|
-
FROM managed_team_user tu
|
|
981
|
-
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
982
|
-
JOIN managed_team_repo tr ON tr.team_id = tu.team_id
|
|
983
|
-
JOIN managed_bundle b ON b.repo_id = tr.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
984
|
-
JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
985
|
-
WHERE tu.user_id = ? AND (b.visible = 1 OR (SELECT role FROM managed_user WHERE id = tu.user_id) IN ('admin', 'manage'))
|
|
986
|
-
ORDER BY b.uploaded_at DESC, b.filename ASC, b.id ASC`,
|
|
1092
|
+
selectTeamsForUserStmt: db.prepare(`${USER_TEAMS_SQL('?')} ORDER BY t.name ASC, t.id ASC`),
|
|
1093
|
+
selectUserTeamScopesStmt: db.prepare(`${USER_TEAM_SCOPES_SQL('?')} ORDER BY tr.team_id, tr.repo_id, tr.path`),
|
|
1094
|
+
selectUserTeamReportsStmt: db.prepare(`${USER_TEAM_REPORTS_SQL('?')} ORDER BY r.uploaded_at DESC, r.filename ASC, r.id ASC`),
|
|
1095
|
+
selectUserTeamBundlesStmt: db.prepare(`${USER_TEAM_BUNDLES_SQL('?')} ORDER BY b.uploaded_at DESC, b.filename ASC, b.id ASC`),
|
|
1096
|
+
// getUserTeamFeedSnapshot's reads in one statement, so one snapshot of the
|
|
1097
|
+
// database answers them all: the session's user and the change revisions
|
|
1098
|
+
// (getFeedState), and listTeamsForUser's lists, each a JSON array in the
|
|
1099
|
+
// order those statements read it.
|
|
1100
|
+
selectUserTeamFeedStmt: db.prepare(
|
|
1101
|
+
`WITH viewer AS (
|
|
1102
|
+
SELECT u.id AS id, u.role AS role, r.catalog AS catalog, r.annotations AS annotations
|
|
1103
|
+
FROM managed_session s JOIN managed_user u ON u.id = s.user_id JOIN managed_change_revision r ON r.id = 1
|
|
1104
|
+
WHERE s.id = ? AND s.expires_at > ?
|
|
1105
|
+
)
|
|
1106
|
+
SELECT v.id AS id, v.role AS role, v.catalog AS catalog, v.annotations AS annotations,
|
|
1107
|
+
(SELECT json_group_array(json_object('id', x.id, 'slug', x.slug, 'name', x.name, 'dependencyReview', x.dependencyReview,
|
|
1108
|
+
'dependencies', x.dependencies, 'security', x.security) ORDER BY x.name ASC, x.id ASC)
|
|
1109
|
+
FROM (${USER_TEAMS_SQL('v.id')}) AS x) AS teams,
|
|
1110
|
+
(SELECT json_group_array(json_object('teamId', x.teamId, 'repoId', x.repoId, 'path', x.path, 'fullName', x.fullName)
|
|
1111
|
+
ORDER BY x.teamId, x.repoId, x.path)
|
|
1112
|
+
FROM (${USER_TEAM_SCOPES_SQL('v.id')}) AS x) AS scopes,
|
|
1113
|
+
(SELECT json_group_array(json_object('id', id, 'sha256', sha256) ORDER BY id) FROM managed_link_report WHERE enabled = 1) AS links,
|
|
1114
|
+
(SELECT json_group_array(json_object('teamId', x.teamId, 'id', x.id, 'slug', x.slug, 'filename', x.filename, 'analyzer', x.analyzer,
|
|
1115
|
+
'visible', x.visible, 'sha256', x.sha256, 'bundleId', x.bundleId, 'repoId', x.repoId, 'repoDirectory', x.repoDirectory,
|
|
1116
|
+
'repoFullName', x.repoFullName, 'dependencies', x.dependencies, 'security', x.security, 'role', x.role)
|
|
1117
|
+
ORDER BY x.uploaded_at DESC, x.filename ASC, x.id ASC)
|
|
1118
|
+
FROM (${USER_TEAM_REPORTS_SQL('v.id')}) AS x) AS reports,
|
|
1119
|
+
(SELECT json_group_array(json_object('teamId', x.teamId, 'id', x.id, 'slug', x.slug, 'integrity', x.integrity, 'filename', x.filename,
|
|
1120
|
+
'kind', x.kind, 'visible', x.visible, 'byteSize', x.byteSize, 'repoId', x.repoId, 'repoDirectory', x.repoDirectory,
|
|
1121
|
+
'repoFullName', x.repoFullName, 'provenance', x.provenance)
|
|
1122
|
+
ORDER BY x.uploaded_at DESC, x.filename ASC, x.id ASC)
|
|
1123
|
+
FROM (${USER_TEAM_BUNDLES_SQL('v.id')}) AS x) AS bundles
|
|
1124
|
+
FROM viewer v`,
|
|
987
1125
|
),
|
|
988
1126
|
selectUserRepoScopesStmt: db.prepare(
|
|
989
1127
|
`SELECT DISTINCT tr.repo_id AS repoId, NULLIF(tr.path, '') AS path
|
|
@@ -999,6 +1137,14 @@ function prepareStatements(db: ManagedSql) {
|
|
|
999
1137
|
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
1000
1138
|
WHERE r.id = ? AND tu.user_id = ? LIMIT 1`,
|
|
1001
1139
|
),
|
|
1140
|
+
selectReportViewStmt: db.prepare(
|
|
1141
|
+
`SELECT r.uploaded_by AS uploadedBy, r.visible AS visible,
|
|
1142
|
+
EXISTS(SELECT 1 FROM managed_team_repo tr
|
|
1143
|
+
JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
1144
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
1145
|
+
WHERE tr.repo_id = r.repo_id AND ${REPORT_IN_TEAM_PATH_SQL} AND tu.user_id = ?) AS readable
|
|
1146
|
+
FROM managed_report r WHERE r.id = ?`,
|
|
1147
|
+
),
|
|
1002
1148
|
selectBundleSecurityStmt: db.prepare(
|
|
1003
1149
|
`SELECT 1 FROM managed_bundle b
|
|
1004
1150
|
JOIN managed_team_repo tr ON tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL}
|
|
@@ -1007,6 +1153,32 @@ function prepareStatements(db: ManagedSql) {
|
|
|
1007
1153
|
WHERE b.id = ? AND tu.user_id = ? AND tu.view_security = 1
|
|
1008
1154
|
AND (? IS NULL OR tr.team_id = ?) LIMIT 1`,
|
|
1009
1155
|
),
|
|
1156
|
+
// A bundle access snapshot's reads in one statement, so one snapshot of
|
|
1157
|
+
// the database answers them all: the session's user, view sessions
|
|
1158
|
+
// included; the bundle, NULL where there is none; whether one of the
|
|
1159
|
+
// user's team scopes contains it, the grant both selectBundleReadableStmt
|
|
1160
|
+
// (a manager's, beside their own uploads) and selectRepoPathReadableStmt
|
|
1161
|
+
// (a reader's, of its repository directory) ask for; and, where the flag
|
|
1162
|
+
// asks, whether one in the team (any, where NULL) shows them its
|
|
1163
|
+
// advisories (selectBundleSecurityStmt).
|
|
1164
|
+
selectBundleAccessStmt: db.prepare(
|
|
1165
|
+
`WITH viewer AS (
|
|
1166
|
+
SELECT u.id AS uid, u.role AS role
|
|
1167
|
+
FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
1168
|
+
WHERE s.id = ? AND s.expires_at > ?
|
|
1169
|
+
)
|
|
1170
|
+
SELECT v.uid AS uid, v.role AS role, b.id AS id, b.slug AS slug, b.integrity AS integrity, b.filename AS filename, b.kind AS kind,
|
|
1171
|
+
b.byte_size AS byteSize, b.uploaded_by AS uploadedBy, b.repo_id AS repoId, b.repo_directory AS repoDirectory, b.visible AS visible,
|
|
1172
|
+
b.uploaded_at AS uploadedAt, b.provenance AS provenance, b.build_conditions AS buildConditions,
|
|
1173
|
+
EXISTS(SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
1174
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
1175
|
+
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = v.uid) AS granted,
|
|
1176
|
+
(? = 1 AND EXISTS(SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
1177
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
1178
|
+
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = v.uid AND tu.view_security = 1
|
|
1179
|
+
AND (? IS NULL OR tr.team_id = ?))) AS security
|
|
1180
|
+
FROM viewer v LEFT JOIN managed_bundle b ON b.id = ?`,
|
|
1181
|
+
),
|
|
1010
1182
|
// The viewer's effective visibility permissions for a report: OR'd (MAX over
|
|
1011
1183
|
// 0/1) across memberships whose repository path contains the report. NULLs
|
|
1012
1184
|
// (no such membership) read as 0 = no permission.
|
|
@@ -1190,39 +1362,40 @@ function reportMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1190
1362
|
return await stmts.findingCatalogReportsStmt.all(after, limit) as Pick<ReportRecord, 'id' | 'byteSize'>[]
|
|
1191
1363
|
},
|
|
1192
1364
|
async getTeamReportAccessSnapshot(sessionId: string, now: number, teamId: string, reportId: string | null = null): Promise<TeamReportAccessSnapshot | null> {
|
|
1193
|
-
const session = await stmts.
|
|
1365
|
+
const session = await stmts.selectTeamSnapshotStmt.get(sessionId, now, teamId, teamId, reportId, teamId) as
|
|
1366
|
+
SessionRow & { member: unknown; reports: unknown; repositories: unknown; links: unknown } | undefined
|
|
1194
1367
|
if (!session) return null
|
|
1195
1368
|
const user: StoredUser = { id: session.uid, login: session.login, name: session.name, avatarUrl: session.avatar, role: session.role }
|
|
1196
|
-
if (user.role === 'none' || !
|
|
1369
|
+
if (user.role === 'none' || !session.member) return { user, teamId: null, reports: [], repositories: [] }
|
|
1197
1370
|
const whole = user.role === 'admin' || user.role === 'manage'
|
|
1198
|
-
const rows =
|
|
1371
|
+
const rows = jsonRows<{
|
|
1199
1372
|
id: string; filename: string; byteSize: number; sha256: string; repoDirectory: string
|
|
1200
1373
|
repoFullName: string | null; dependencies: number; security: number; visible: number
|
|
1201
|
-
}
|
|
1374
|
+
}>(session.reports)
|
|
1202
1375
|
// Only privileged callers can receive a hidden row from this query. Give
|
|
1203
1376
|
// that individual preview its own capacity; published report selections
|
|
1204
1377
|
// still use the complete workspace for cross-report classification.
|
|
1205
1378
|
const hidden = rows.find(row => row.id === reportId && row.visible === 0)
|
|
1206
1379
|
const selected = hidden ? [hidden] : rows
|
|
1207
|
-
const repositories =
|
|
1208
|
-
return { user, teamId, reportId, hiddenReportId: hidden?.id ?? null, repositories, linkRevision:
|
|
1380
|
+
const repositories = jsonRows<TeamReportAccessSnapshot['repositories'][number]>(session.repositories)
|
|
1381
|
+
return { user, teamId, reportId, hiddenReportId: hidden?.id ?? null, repositories, linkRevision: linkRevisionOf(jsonRows(session.links)), reports: selected.map(row => ({
|
|
1209
1382
|
id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
1210
1383
|
repo: { github: row.repoFullName, directory: row.repoDirectory },
|
|
1211
1384
|
permissions: { dependencies: whole || row.dependencies === 1, security: whole || row.security === 1 },
|
|
1212
1385
|
})) }
|
|
1213
1386
|
},
|
|
1214
1387
|
async getReportAccessSnapshot(sessionId: string, now: number, ids: readonly string[]): Promise<ReportAccessSnapshot | null> {
|
|
1215
|
-
const session = await stmts.
|
|
1388
|
+
const session = await stmts.selectReportSnapshotStmt.get(sessionId, now, JSON.stringify([...new Set(ids)])) as
|
|
1389
|
+
SessionRow & { reports: unknown; links: unknown } | undefined
|
|
1216
1390
|
if (!session) return null
|
|
1217
1391
|
const user: StoredUser = { id: session.uid, login: session.login, name: session.name, avatarUrl: session.avatar, role: session.role }
|
|
1218
1392
|
if (user.role === 'none' || ids.length === 0) return { user, reports: [] }
|
|
1219
1393
|
const admin = user.role === 'admin', manage = user.role === 'manage'
|
|
1220
|
-
const rows =
|
|
1221
|
-
admin ? 1 : 0, manage ? 1 : 0, user.id, manage ? 1 : 0) as {
|
|
1394
|
+
const rows = jsonRows<{
|
|
1222
1395
|
id: string; filename: string; byteSize: number; sha256: string; repoDirectory: string
|
|
1223
1396
|
repoFullName: string | null; dependencies: number; security: number
|
|
1224
|
-
}
|
|
1225
|
-
return { user, linkRevision:
|
|
1397
|
+
}>(session.reports)
|
|
1398
|
+
return { user, linkRevision: linkRevisionOf(jsonRows(session.links)), reports: rows.map(row => ({
|
|
1226
1399
|
id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
1227
1400
|
repo: { github: row.repoFullName, directory: row.repoDirectory },
|
|
1228
1401
|
permissions: { dependencies: admin || manage || row.dependencies === 1, security: admin || manage || row.security === 1 },
|
|
@@ -1249,6 +1422,11 @@ type TriageDbRow = {
|
|
|
1249
1422
|
comment: string | null; fix: string | null; flagged: number | null
|
|
1250
1423
|
updatedByLogin: string | null; updatedAt: number
|
|
1251
1424
|
}
|
|
1425
|
+
const triageRow = (r: TriageDbRow): TriageRow => ({
|
|
1426
|
+
findingId: r.findingId, color: r.color, triage: r.triage, comment: r.comment, fix: r.fix,
|
|
1427
|
+
flagged: r.flagged == null ? null : r.flagged === 1,
|
|
1428
|
+
updatedByLogin: r.updatedByLogin, updatedAt: r.updatedAt,
|
|
1429
|
+
})
|
|
1252
1430
|
type TriageStateDbRow = { color: string | null; triage: string | null; comment: string | null; fix: string | null; flagged: number | null }
|
|
1253
1431
|
type TriageEventDbRow = TriageStateDbRow & {
|
|
1254
1432
|
seq: number; findingId: string; batchId: string; actorId: string | null; actorLogin: string | null; actorName: string | null; at: number
|
|
@@ -1283,15 +1461,10 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1283
1461
|
return {
|
|
1284
1462
|
async getAnnotationRevision(findingIds: readonly string[]): Promise<string> {
|
|
1285
1463
|
if (findingIds.length === 0) return '[]'
|
|
1286
|
-
const ids = JSON.stringify(findingIds)
|
|
1287
1464
|
// Batch UUIDs survive history trimming and distinguish same-timestamp
|
|
1288
1465
|
// writes, even if SQLite reuses a sequence after an annotation purge.
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
await stmts.annotationCommentRevisionStmt.all(ids),
|
|
1292
|
-
await db.prepare(`SELECT finding_id, issue_url, auto_fix_url FROM managed_finding_issue
|
|
1293
|
-
WHERE finding_id IN (SELECT value FROM json_each(?)) AND issue_url IS NOT NULL ORDER BY finding_id`).all(ids),
|
|
1294
|
-
])
|
|
1466
|
+
const row = await stmts.annotationRevisionStmt.get(JSON.stringify(findingIds)) as { triage: unknown; comments: unknown; issues: unknown }
|
|
1467
|
+
return JSON.stringify([jsonRows(row.triage), jsonRows(row.comments), jsonRows(row.issues)])
|
|
1295
1468
|
},
|
|
1296
1469
|
async deleteTriage(findingIds: readonly string[]): Promise<number> {
|
|
1297
1470
|
if (findingIds.length === 0) return 0
|
|
@@ -1318,12 +1491,7 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1318
1491
|
},
|
|
1319
1492
|
async listTriage(findingIds: readonly string[]): Promise<TriageRow[]> {
|
|
1320
1493
|
if (findingIds.length === 0) return []
|
|
1321
|
-
|
|
1322
|
-
return rows.map((r) => ({
|
|
1323
|
-
findingId: r.findingId, color: r.color, triage: r.triage, comment: r.comment, fix: r.fix,
|
|
1324
|
-
flagged: r.flagged == null ? null : r.flagged === 1,
|
|
1325
|
-
updatedByLogin: r.updatedByLogin, updatedAt: r.updatedAt,
|
|
1326
|
-
}))
|
|
1494
|
+
return ((await selectTriageStmt.all(JSON.stringify(findingIds))) as TriageDbRow[]).map(triageRow)
|
|
1327
1495
|
},
|
|
1328
1496
|
async setTriage(findingId: string, entry: TriageEntryPatch | null, updatedBy: string | null, updatedByLogin: string | null, now: number): Promise<void> {
|
|
1329
1497
|
// A single write is its own batch. The row and its event go together.
|
|
@@ -1428,22 +1596,19 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1428
1596
|
return row == null ? null : mapBundle(row)
|
|
1429
1597
|
},
|
|
1430
1598
|
async getBundleAccessSnapshot(sessionId: string, now: number, id: string, advisoriesTeamId?: string | null) {
|
|
1431
|
-
|
|
1432
|
-
|
|
1599
|
+
// One statement, shared by SQLite and Postgres; the advisories grant is
|
|
1600
|
+
// read only where a team (or null, any) is passed.
|
|
1601
|
+
const team = advisoriesTeamId ?? null
|
|
1602
|
+
const row = await stmts.selectBundleAccessStmt.get(sessionId, now, advisoriesTeamId === undefined ? 0 : 1, team, team, id) as
|
|
1603
|
+
{ uid: string; role: Role; granted: unknown; security: unknown } & (BundleRow | { id: null }) | undefined
|
|
1604
|
+
if (!row) return null
|
|
1433
1605
|
const denied = { bundle: null, canReadAdvisories: false }
|
|
1434
|
-
if (!roleAtLeast(
|
|
1435
|
-
const
|
|
1436
|
-
if (!row) return denied
|
|
1437
|
-
const manager = roleAtLeast(session.role, 'manage')
|
|
1606
|
+
if (!roleAtLeast(row.role, 'view') || row.id === null) return denied
|
|
1607
|
+
const manager = roleAtLeast(row.role, 'manage')
|
|
1438
1608
|
if (!manager && row.visible !== 1) return denied
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
if (!allowed) return denied
|
|
1443
|
-
}
|
|
1444
|
-
const canReadAdvisories = manager || (advisoriesTeamId !== undefined
|
|
1445
|
-
&& await stmts.selectBundleSecurityStmt.get(id, session.uid, advisoriesTeamId, advisoriesTeamId) != null)
|
|
1446
|
-
return { bundle: mapBundle(row), canReadAdvisories }
|
|
1609
|
+
// Admins read every bundle, managers their own uploads too.
|
|
1610
|
+
if (row.role !== 'admin' && !(row.granted || (manager && row.uploadedBy === row.uid))) return denied
|
|
1611
|
+
return { bundle: mapBundle(row), canReadAdvisories: manager || Boolean(row.security) }
|
|
1447
1612
|
},
|
|
1448
1613
|
async listBundles(userId?: string): Promise<AdminBundle[]> {
|
|
1449
1614
|
const rows = (await selectBundlesStmt.all(userId ?? null, userId ?? null, userId ?? null)) as BundleListRow[]
|
|
@@ -1525,40 +1690,8 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1525
1690
|
return (await selectUserRepoScopesStmt.all(userId)) as { repoId: number; path: string | null }[]
|
|
1526
1691
|
},
|
|
1527
1692
|
async listTeamsForUser(userId: string): Promise<UserTeam[]> {
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
const links = await linkRevision(db)
|
|
1531
|
-
const reportsByTeam = new Map<string, UserTeamReport[]>()
|
|
1532
|
-
for (const r of (await selectUserTeamReportsStmt.all(userId)) as {
|
|
1533
|
-
teamId: string; id: string; slug: string; filename: string; analyzer: string | null; sha256: string; bundleId: string | null; repoId: number
|
|
1534
|
-
repoDirectory: string; repoFullName: string | null; visible: number; dependencies: number; security: number; role: string
|
|
1535
|
-
}[]) {
|
|
1536
|
-
const list = reportsByTeam.get(r.teamId) ?? []
|
|
1537
|
-
const cacheKey = createHash('sha256').update(JSON.stringify([
|
|
1538
|
-
r.sha256, r.bundleId, r.filename, r.repoId, r.repoDirectory, r.repoFullName, r.dependencies, r.security, r.role, links,
|
|
1539
|
-
])).digest('base64url')
|
|
1540
|
-
list.push({ id: r.id, slug: r.slug, filename: r.filename, visible: r.visible === 1, analyzer: r.analyzer, repoFullName: r.repoFullName, repoDirectory: r.repoDirectory, cacheKey })
|
|
1541
|
-
reportsByTeam.set(r.teamId, list)
|
|
1542
|
-
}
|
|
1543
|
-
const bundlesByTeam = new Map<string, UserTeamBundle[]>()
|
|
1544
|
-
for (const b of (await selectUserTeamBundlesStmt.all(userId)) as (Omit<UserTeamBundle, 'visible'> & { teamId: string; visible: number })[]) {
|
|
1545
|
-
const list = bundlesByTeam.get(b.teamId) ?? []
|
|
1546
|
-
list.push({ id: b.id, slug: b.slug, integrity: b.integrity, filename: b.filename, visible: b.visible === 1,
|
|
1547
|
-
kind: b.kind, byteSize: b.byteSize, repoId: b.repoId, repoDirectory: b.repoDirectory, repoFullName: b.repoFullName, provenance: b.provenance })
|
|
1548
|
-
bundlesByTeam.set(b.teamId, list)
|
|
1549
|
-
}
|
|
1550
|
-
return teams.map((t) => ({
|
|
1551
|
-
id: t.id, slug: t.slug, name: t.name,
|
|
1552
|
-
...(t.dependencyReview === 1 ? { dependencyReview: true } : {}),
|
|
1553
|
-
permissions: { dependencies: t.dependencies === 1, security: t.security === 1 },
|
|
1554
|
-
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
1555
|
-
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
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 ?? '')),
|
|
1559
|
-
reports: reportsByTeam.get(t.id) ?? [],
|
|
1560
|
-
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
1561
|
-
}))
|
|
1693
|
+
return userTeams(await selectTeamsForUserStmt.all(userId) as UserTeamRows['teams'], await stmts.selectUserTeamScopesStmt.all(userId) as UserTeamRows['scopes'],
|
|
1694
|
+
await linkRevision(db), await selectUserTeamReportsStmt.all(userId) as UserTeamRows['reports'], await selectUserTeamBundlesStmt.all(userId) as UserTeamRows['bundles'])
|
|
1562
1695
|
},
|
|
1563
1696
|
async getFeedState(sessionId: string, now: number) {
|
|
1564
1697
|
const row = await db.prepare(`SELECT u.id, u.role, r.catalog, r.annotations FROM managed_session s
|
|
@@ -1567,16 +1700,22 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1567
1700
|
return row ? { user: { id: row.id, role: row.role }, catalog: row.catalog, annotations: row.annotations } : null
|
|
1568
1701
|
},
|
|
1569
1702
|
async getUserTeamFeedSnapshot(sessionId: string, now: number) {
|
|
1570
|
-
// One
|
|
1571
|
-
//
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1703
|
+
// One statement, shared by SQLite and Postgres. Never touch last-seen
|
|
1704
|
+
// timestamps or include other users' memberships.
|
|
1705
|
+
const row = await stmts.selectUserTeamFeedStmt.get(sessionId, now) as {
|
|
1706
|
+
id: string; role: Role; catalog: number; annotations: number; teams: unknown; scopes: unknown; links: unknown; reports: unknown; bundles: unknown
|
|
1707
|
+
} | undefined
|
|
1708
|
+
if (!row) return null
|
|
1709
|
+
const teams = row.role === 'none' ? [] : userTeams(jsonRows(row.teams), jsonRows(row.scopes), linkRevisionOf(jsonRows(row.links)), jsonRows(row.reports), jsonRows(row.bundles))
|
|
1710
|
+
return { user: { id: row.id, role: row.role }, catalog: row.catalog, teams, revision: teamCatalogRevision(teams) }
|
|
1576
1711
|
},
|
|
1577
1712
|
async userCanReadReport(userId: string, reportId: string): Promise<boolean> {
|
|
1578
1713
|
return (await selectReportReadableStmt.get(reportId, userId)) != null
|
|
1579
1714
|
},
|
|
1715
|
+
async getReportView(userId: string, reportId: string) {
|
|
1716
|
+
const row = await stmts.selectReportViewStmt.get(userId, reportId) as { uploadedBy: string | null; visible: number; readable: unknown } | undefined
|
|
1717
|
+
return row ? { uploadedBy: row.uploadedBy, visible: row.visible === 1, readable: Boolean(row.readable) } : null
|
|
1718
|
+
},
|
|
1580
1719
|
async userCanReadBundleAdvisories(userId: string, bundleId: string, teamId: string | null): Promise<boolean> {
|
|
1581
1720
|
return (await stmts.selectBundleSecurityStmt.get(bundleId, userId, teamId, teamId)) != null
|
|
1582
1721
|
},
|
|
@@ -1641,10 +1780,16 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1641
1780
|
const triage = triageMethods(stmts, options.triageHistoryLimit ?? 0, db)
|
|
1642
1781
|
const {
|
|
1643
1782
|
upsertUserStmt, selectLinkedStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectOwnSessionStmt, selectUsersStmt,
|
|
1644
|
-
touchUserSeenStmt, updateRoleStmt, updateTokensStmt,
|
|
1783
|
+
selectUserStmt, touchOwnSessionStmt, touchUserSeenStmt, updateRoleStmt, updateTokensStmt, ensureUploadKeyStmt, deleteSessionStmt, deleteExpiredStmt,
|
|
1645
1784
|
selectViewSessionStmt, deleteOtherViewsStmt, insertViewSessionStmt, deleteDemotedViewsStmt,
|
|
1646
1785
|
} = stmts
|
|
1647
1786
|
|
|
1787
|
+
// Claimed at every automatic reap: prepared once.
|
|
1788
|
+
const claimLeaseStmt = db.prepare(`INSERT INTO managed_maintenance_lease (id, owner, expires_at) VALUES (?, ?, ?)
|
|
1789
|
+
ON CONFLICT(id) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at
|
|
1790
|
+
WHERE managed_maintenance_lease.expires_at <= ? RETURNING id`)
|
|
1791
|
+
const finishLeaseStmt = db.prepare('UPDATE managed_maintenance_lease SET expires_at = ? WHERE owner = ?')
|
|
1792
|
+
|
|
1648
1793
|
const methods: Omit<ManagedDb, keyof ManagementStore | keyof ManagementCatalogStore> = {
|
|
1649
1794
|
...storageMethods(db, key),
|
|
1650
1795
|
...linkReportMethods(db, key),
|
|
@@ -1656,12 +1801,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1656
1801
|
...bundleBuildLeaseMethods(db),
|
|
1657
1802
|
...upstreamCacheMethods(db),
|
|
1658
1803
|
async claimMaintenanceLease(owner, now, until, migration = false) {
|
|
1659
|
-
return !!await
|
|
1660
|
-
ON CONFLICT(id) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at
|
|
1661
|
-
WHERE managed_maintenance_lease.expires_at <= ? RETURNING id`).get(migration ? 2 : 1, owner, until, now)
|
|
1804
|
+
return !!await claimLeaseStmt.get(migration ? 2 : 1, owner, until, now)
|
|
1662
1805
|
},
|
|
1663
1806
|
async finishMaintenanceLease(owner, until) {
|
|
1664
|
-
await
|
|
1807
|
+
await finishLeaseStmt.run(until, owner)
|
|
1665
1808
|
},
|
|
1666
1809
|
async upsertUser(user, now, initialAdminGithubId = null) {
|
|
1667
1810
|
if (await selectLinkedStmt.get(user.githubUserId)) return null
|
|
@@ -1682,10 +1825,8 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1682
1825
|
await touchUserSeenStmt.run(now, session.userId, now)
|
|
1683
1826
|
},
|
|
1684
1827
|
async sessionWithUser(id, now) {
|
|
1685
|
-
const row = (await
|
|
1686
|
-
|
|
1687
|
-
await touchUserSeenStmt.run(now, row.uid, now)
|
|
1688
|
-
return ownSession(row)
|
|
1828
|
+
const row = (await touchOwnSessionStmt.get(now, now, id, now, id, id, id, id)) as SessionRow | undefined
|
|
1829
|
+
return row == null ? null : ownSession(row)
|
|
1689
1830
|
},
|
|
1690
1831
|
async getSessionWithUser(id, now) {
|
|
1691
1832
|
const row = (await selectOwnSessionStmt.get(id, now)) as SessionRow | undefined
|
|
@@ -1720,6 +1861,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1720
1861
|
const rows = (await selectUsersStmt.all()) as UserRow[]
|
|
1721
1862
|
return rows.map((r) => ({ id: r.id, login: r.login, name: r.name, role: r.role, createdAt: r.created, lastSeenAt: r.lastSeen, lastActivityAt: r.lastActivity }))
|
|
1722
1863
|
},
|
|
1864
|
+
async getUser(id) {
|
|
1865
|
+
const row = (await selectUserStmt.get(id)) as StoredUser | undefined
|
|
1866
|
+
return row == null ? null : { id: row.id, login: row.login, name: row.name, avatarUrl: row.avatarUrl, role: row.role }
|
|
1867
|
+
},
|
|
1723
1868
|
async setUserRole(id, role) {
|
|
1724
1869
|
if (role !== 'admin') await deleteDemotedViewsStmt.run(id)
|
|
1725
1870
|
return Number((await updateRoleStmt.run(role, Date.now(), id)).changes) > 0
|
|
@@ -1730,8 +1875,11 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1730
1875
|
await updateTokensStmt.run(value.access, value.refresh, tokens.expiresAt, state ? 1 : 0, Date.now(), id)
|
|
1731
1876
|
},
|
|
1732
1877
|
async getUserTokens(id) {
|
|
1733
|
-
|
|
1734
|
-
|
|
1878
|
+
// The tokens and the storage encryption state they were kept under, in
|
|
1879
|
+
// one read: a user missing reads as no tokens, no encryption row as none.
|
|
1880
|
+
const read = (await stmts.selectTokensStateStmt.get(id)) as StoredTokens & Parameters<typeof storageStateOf>[0] & { userId: string | null; stateId: number | null }
|
|
1881
|
+
const state = storageStateOf(read.stateId == null ? undefined : read, key)
|
|
1882
|
+
const row = read.userId == null ? undefined : read
|
|
1735
1883
|
if (row == null || row.access == null) return null
|
|
1736
1884
|
if (row.encrypted && !state) throw new Error('Encrypted tokens without storage encryption state')
|
|
1737
1885
|
if (!row.encrypted && state?.complete) throw new Error('Unexpected plaintext GitHub tokens')
|
|
@@ -1749,7 +1897,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1749
1897
|
...activity,
|
|
1750
1898
|
...comments,
|
|
1751
1899
|
async getAnnotations(ids) {
|
|
1752
|
-
|
|
1900
|
+
// listTriage, listComments and listManagedIssues in one statement.
|
|
1901
|
+
const row = await stmts.selectAnnotationsStmt.get(JSON.stringify(ids)) as { triage: unknown; comments: unknown; issues: unknown }
|
|
1902
|
+
return { triage: ids.length > 0 ? jsonRows<TriageDbRow>(row.triage).map(triageRow) : [], comments: ids.length > 0 ? jsonRows<Awaited<ReturnType<CommentStore['listComments']>>[number]>(row.comments) : [],
|
|
1903
|
+
issues: jsonRows<ManagedIssue>(row.issues) }
|
|
1753
1904
|
},
|
|
1754
1905
|
async applyManagedIssueFixes(sessionId, snapshot, updates) {
|
|
1755
1906
|
// Recheck access inside the same write transaction as the derived field.
|