@preventive/triage 1.0.0-alpha.37 → 1.0.0-alpha.39
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/README.md +16 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +6 -2
- package/common/bundle-metadata.js +63 -20
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-source-links.js +7 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +3 -1
- package/common/bundle-sources.js +47 -19
- package/common/code-language.js +16 -2
- package/common/default-scan-models.ts +2 -1
- package/common/dependency-reviews.d.ts +23 -0
- package/common/dependency-reviews.js +81 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/github-pr.ts +1 -1
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +1 -5
- package/common/managed/routes.js +123 -12
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +1 -1
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-45WIN3SW.js +1 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-6C76GAXK.js +1 -0
- package/out/chunk-7ZKDKGAH.js +1 -0
- package/out/chunk-ABW5FVSN.js +1 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-F4OMDNEY.js +1 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GMFXFEUA.js +2 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-HIV7SVI7.js +1 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-JVBB4LZX.js +1 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-QSLWGJBQ.js +223 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-TINZZKP5.js +33 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-XNGEQO7Y.js +2 -0
- package/out/chunk-XRWAV3DX.js +7 -0
- package/out/chunk-XYDGU6BM.js +51 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZPD5R5RP.js +3 -0
- package/out/client-managed-import.js +3 -87
- package/out/client-managed.js +26 -338
- package/out/client-sync.js +8 -14
- package/out/graph.js +17 -30
- package/out/index.html +34 -0
- package/out/prism.js +6 -2
- package/out/terminal.js +241 -257
- package/out/view.css +1 -1
- package/out/view.js +76 -520
- package/package.json +89 -7
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +358 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +142 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +219 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +113 -0
- package/server-e2e/http.ts +7 -7
- package/server-e2e/npm-proxy.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +2 -2
- package/server-e2e/objstore/handlers.ts +1 -1
- package/server-e2e/objstore/tokens.ts +1 -1
- package/server-e2e/sign.ts +1 -1
- package/server-managed/bundle-advisories.ts +74 -12
- package/server-managed/bundle-cache.ts +37 -4
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-summary-cache.ts +24 -10
- package/server-managed/config.ts +4 -0
- package/server-managed/db-methods.ts +98 -11
- package/server-managed/db-neon.ts +39 -4
- package/server-managed/db-schema.ts +14 -2
- package/server-managed/db-table-names.ts +1 -1
- package/server-managed/db.ts +7 -3
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +9 -3
- package/server-managed/github-errors.ts +21 -2
- package/server-managed/github-issue-oauth.ts +5 -6
- package/server-managed/github-metadata.ts +115 -0
- package/server-managed/github-oauth.ts +38 -2
- package/server-managed/http-response.ts +26 -0
- package/server-managed/http.ts +952 -53
- package/server-managed/index.ts +11 -5
- package/server-managed/kept.ts +28 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +78 -7
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/report-sources.ts +13 -2
- package/server-managed/repository-browser.ts +19 -5
- package/server-managed/repository-risk.ts +418 -0
- package/server-managed/session.ts +16 -2
- package/server-managed/sql.ts +5 -2
- package/server-managed/storage-encryption.ts +5 -3
- package/server-managed/storage-payload.ts +4 -0
- package/server-managed/storage-stores.ts +2 -0
- package/server-managed/storage.ts +6 -3
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-feed.ts +1 -1
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +25 -4
- package/server-managed/upstream-cache.ts +147 -5
- package/server-managed/view-as.ts +58 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { setImmediate } from 'node:timers/promises'
|
|
2
|
+
import { bundleCommitHash } from '../common/bundle-commit.js'
|
|
3
|
+
import type { BundleSummary } from './bundle-cache.ts'
|
|
4
|
+
import type { ManagedDb } from './db.ts'
|
|
5
|
+
import { type GithubCommit, githubCommitKey } from './github-metadata.ts'
|
|
6
|
+
|
|
7
|
+
const MAX_MESSAGE_LENGTH = 65_536
|
|
8
|
+
const MAX_NAME_LENGTH = 256
|
|
9
|
+
// GitHub logins, and the `[bot]` accounts apps commit as.
|
|
10
|
+
const GITHUB_LOGIN = /^[A-Za-z\d][A-Za-z\d-]{0,38}(?:\[bot\])?$/u
|
|
11
|
+
const BACKFILL_LIMIT = 4
|
|
12
|
+
const RETRY_MS = 5 * 60_000
|
|
13
|
+
|
|
14
|
+
export type CommitDetails = Omit<GithubCommit, 'key' | 'fetchedAt'>
|
|
15
|
+
// What viewers see of a cached commit: its subject in place of the message.
|
|
16
|
+
export type CatalogCommitDetails = Omit<CommitDetails, 'message'> & { subject: string }
|
|
17
|
+
// What a catalog sends with a bundle whose summary records a commit, when
|
|
18
|
+
// the cache holds its details, tags pointing to it, or both. `github` is the
|
|
19
|
+
// repository they were cached for: the one the bundle is stored at.
|
|
20
|
+
export interface BundleCommitInfo { sha: string; github: string; tags: string[]; details: CatalogCommitDetails | null }
|
|
21
|
+
interface CatalogBundle { integrity: string; repoId: number | null; repoFullName: string | null }
|
|
22
|
+
type Summaries = ReadonlyMap<string, { summary: BundleSummary | null }>
|
|
23
|
+
export interface CatalogCommit { repoId: number; sha: string; key: string }
|
|
24
|
+
export interface CommitReader { commitDetails(sha: string): Promise<CommitDetails | null> }
|
|
25
|
+
|
|
26
|
+
function truncate(value: string, max: number): string {
|
|
27
|
+
if (value.length <= max) return value
|
|
28
|
+
// Never leave half of a surrogate pair at the end.
|
|
29
|
+
return value.slice(0, /[\uD800-\uDBFF]/u.test(value[max - 1]!) ? max - 1 : max)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function name(value: unknown): string | null {
|
|
33
|
+
return typeof value === 'string' && value.trim() ? truncate(value.trim(), MAX_NAME_LENGTH) : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function date(value: unknown): number | null {
|
|
37
|
+
const time = typeof value === 'string' ? Date.parse(value) : NaN
|
|
38
|
+
return Number.isSafeInteger(time) ? time : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// `body` is a GitHub list-commits page that starts at `sha`, so its first
|
|
42
|
+
// commit is that one, without the files GET /commits/:sha would add.
|
|
43
|
+
export function parseGithubCommit(sha: string, body: unknown): CommitDetails | null {
|
|
44
|
+
const item = Array.isArray(body) ? body[0] as {
|
|
45
|
+
sha?: unknown; author?: { login?: unknown } | null
|
|
46
|
+
commit?: { message?: unknown; author?: { name?: unknown; date?: unknown } | null; committer?: { date?: unknown } | null }
|
|
47
|
+
} | null : null
|
|
48
|
+
if (item?.sha !== sha || typeof item.commit?.message !== 'string') return null
|
|
49
|
+
const login = item.author?.login
|
|
50
|
+
return {
|
|
51
|
+
message: truncate(item.commit.message, MAX_MESSAGE_LENGTH),
|
|
52
|
+
authorName: name(item.commit.author?.name),
|
|
53
|
+
authorLogin: typeof login === 'string' && GITHUB_LOGIN.test(login) ? login : null,
|
|
54
|
+
authoredAt: date(item.commit.author?.date),
|
|
55
|
+
committedAt: date(item.commit.committer?.date),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A commit's subject, as `git log --format=%s` gives it: the message's first
|
|
60
|
+
// paragraph on one line, here without the Claude-Session lines Claude Code
|
|
61
|
+
// adds. Viewers see no more of the message, so catalogs send no more.
|
|
62
|
+
export function commitSubject(message: string): string {
|
|
63
|
+
const paragraph = message.replaceAll('\r\n', '\n').trim().split(/\n[ \t]*\n/u, 1)[0] ?? ''
|
|
64
|
+
return paragraph.split('\n').map(line => line.trim()).filter(line => line && !/^Claude-Session:/iu.test(line)).join(' ')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The commit a bundle's own summary records, in the repository it is stored at.
|
|
68
|
+
function catalogCommit(bundle: CatalogBundle, summaries: Summaries): CatalogCommit | null {
|
|
69
|
+
const sha = bundleCommitHash(summaries.get(bundle.integrity)?.summary?.commit)
|
|
70
|
+
return sha && bundle.repoId != null ? { repoId: bundle.repoId, sha, key: githubCommitKey(bundle.repoId, sha) } : null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Reads only the cache: viewing a bundle never requests its commit's tags,
|
|
74
|
+
// and a commit missing from the cache is left for backfillBundleCommits.
|
|
75
|
+
// Call it before the catalog's final access check: `commitInfo` answers for
|
|
76
|
+
// the bundles of that last snapshot without another await, and has nothing
|
|
77
|
+
// for a commit or repository this read did not cover.
|
|
78
|
+
export async function bundleCommits(db: ManagedDb, bundles: readonly CatalogBundle[], summaries: Summaries) {
|
|
79
|
+
const wanted = new Map<string, CatalogCommit>()
|
|
80
|
+
for (const bundle of bundles) {
|
|
81
|
+
const commit = catalogCommit(bundle, summaries)
|
|
82
|
+
if (commit) wanted.set(commit.key, commit)
|
|
83
|
+
}
|
|
84
|
+
const keys = [...wanted.keys()]
|
|
85
|
+
const details = new Map<string, CatalogCommitDetails>()
|
|
86
|
+
const tags = new Map<string, string[]>()
|
|
87
|
+
if (keys.length > 0) {
|
|
88
|
+
for (const { key, fetchedAt: _fetchedAt, message, ...commit } of await db.listGithubCommits(keys)) {
|
|
89
|
+
details.set(key, { subject: commitSubject(message), ...commit })
|
|
90
|
+
}
|
|
91
|
+
for (const tag of await db.listGithubCommitTags(keys)) tags.set(tag.key, [...tags.get(tag.key) ?? [], tag.name])
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
commitInfo(bundle: CatalogBundle): BundleCommitInfo | null {
|
|
95
|
+
const commit = catalogCommit(bundle, summaries)
|
|
96
|
+
if (!commit || !bundle.repoFullName || (!details.has(commit.key) && !tags.has(commit.key))) return null
|
|
97
|
+
return { sha: commit.sha, github: bundle.repoFullName, tags: tags.get(commit.key) ?? [], details: details.get(commit.key) ?? null }
|
|
98
|
+
},
|
|
99
|
+
missing: [...wanted.values()].filter(commit => !details.has(commit.key)),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Store the details of a commit, read with the reader's verified repository
|
|
104
|
+
// access, unless the cache already has them. Best effort: true once cached.
|
|
105
|
+
export async function cacheCommitDetails(db: ManagedDb, reader: CommitReader, repoId: number, sha: string): Promise<boolean> {
|
|
106
|
+
const key = githubCommitKey(repoId, sha)
|
|
107
|
+
try {
|
|
108
|
+
if ((await db.listGithubCommits([key])).length > 0) return true
|
|
109
|
+
const details = await reader.commitDetails(sha)
|
|
110
|
+
if (details) await db.setGithubCommits([{ key, ...details, fetchedAt: Date.now() }])
|
|
111
|
+
return details !== null
|
|
112
|
+
} catch { return false }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const active = new Set<string>()
|
|
116
|
+
const retries = new Map<string, number>()
|
|
117
|
+
|
|
118
|
+
// Called after the catalog response is sent: the next one has the details.
|
|
119
|
+
// Reads each commit with the viewer's own repository access, so a viewer who
|
|
120
|
+
// cannot read it on GitHub leaves it for another (retried after a delay for
|
|
121
|
+
// them). Commits never change, so each is stored once and kept.
|
|
122
|
+
export async function backfillBundleCommits(db: ManagedDb, userId: string, missing: readonly CatalogCommit[],
|
|
123
|
+
readerFor: (repoId: number) => Promise<CommitReader | null>): Promise<void> {
|
|
124
|
+
if (missing.length === 0) return
|
|
125
|
+
await setImmediate()
|
|
126
|
+
const now = Date.now()
|
|
127
|
+
for (const [key, retryAt] of retries) if (retryAt <= now) retries.delete(key)
|
|
128
|
+
const batch = missing.filter(commit => !active.has(commit.key) && !retries.has(`${userId}\n${commit.key}`)).slice(0, BACKFILL_LIMIT)
|
|
129
|
+
for (const commit of batch) active.add(commit.key)
|
|
130
|
+
try {
|
|
131
|
+
for (const repoId of new Set(batch.map(commit => commit.repoId))) {
|
|
132
|
+
const reader = await readerFor(repoId).catch(() => null)
|
|
133
|
+
for (const commit of batch.filter(entry => entry.repoId === repoId)) {
|
|
134
|
+
if (!reader || !await cacheCommitDetails(db, reader, repoId, commit.sha)) retries.set(`${userId}\n${commit.key}`, Date.now() + RETRY_MS)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
for (const commit of batch) active.delete(commit.key)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -5,14 +5,27 @@ import { decodeUtf8 } from '../common/utf8.js'
|
|
|
5
5
|
import { bundleCommitHash } from '../common/bundle-commit.js'
|
|
6
6
|
import type { BundleCacheRecord, BundleCacheStorage, BundleSummary } from './bundle-cache.ts'
|
|
7
7
|
|
|
8
|
-
// Version 3 adds the optional source commit;
|
|
9
|
-
|
|
8
|
+
// Version 3 adds the optional source commit; version 4 counts lines of code
|
|
9
|
+
// without blank lines; version 5 adds a Stasis bundle's format version and
|
|
10
|
+
// versioned package count. Older bundles use bounded backfill; until it
|
|
11
|
+
// reaches one, catalogs serve its version 4 summary, which lacks only those.
|
|
12
|
+
export const SUMMARY_FILENAME = 'v5-summary.json'
|
|
13
|
+
const PREVIOUS_SUMMARY_FILENAME = 'v4-summary.json'
|
|
10
14
|
const RETRY_MS = 5 * 60_000
|
|
11
15
|
const BACKFILL_LIMIT = 4
|
|
12
|
-
type CachedSummary = BundleSummary | { retryAt: number }
|
|
16
|
+
type CachedSummary = BundleSummary | { retryAt: number } | { previous: BundleSummary }
|
|
13
17
|
|
|
14
|
-
async function
|
|
15
|
-
|
|
18
|
+
async function readCurrentSummary(storage: BundleCacheStorage, id: string): Promise<CachedSummary> {
|
|
19
|
+
try { return await readSummary(storage, id, SUMMARY_FILENAME) }
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (!(error instanceof CacheMissError)) throw error
|
|
22
|
+
const previous = await readSummary(storage, id, PREVIOUS_SUMMARY_FILENAME)
|
|
23
|
+
return 'retryAt' in previous ? previous : { previous }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readSummary(storage: BundleCacheStorage, id: string, file: string): Promise<BundleSummary | { retryAt: number }> {
|
|
28
|
+
const cached = await storage.open(id, file)
|
|
16
29
|
try {
|
|
17
30
|
const chunks: Buffer[] = []
|
|
18
31
|
let size = 0
|
|
@@ -21,11 +34,12 @@ async function readSummary(storage: BundleCacheStorage, id: string): Promise<Cac
|
|
|
21
34
|
if (size > 1024) throw new Error('Invalid bundle summary')
|
|
22
35
|
chunks.push(Buffer.from(chunk))
|
|
23
36
|
}
|
|
24
|
-
const summary = JSON.parse(decodeUtf8(Buffer.concat(chunks))) as
|
|
37
|
+
const summary = JSON.parse(decodeUtf8(Buffer.concat(chunks))) as BundleSummary | { retryAt: number }
|
|
25
38
|
if (summary && 'retryAt' in summary && Number.isSafeInteger(summary.retryAt) && summary.retryAt > 0) return summary
|
|
26
39
|
const counts = summary as BundleSummary
|
|
27
40
|
if (!counts || ![counts.files, counts.codeFiles, counts.lines].every(value => Number.isSafeInteger(value) && value >= 0)
|
|
28
|
-
|| counts.codeFiles > counts.files || (counts.commit !== undefined && !bundleCommitHash(counts.commit))
|
|
41
|
+
|| counts.codeFiles > counts.files || (counts.commit !== undefined && !bundleCommitHash(counts.commit))
|
|
42
|
+
|| [counts.stasisVersion, counts.versionedPackages].some(value => value !== undefined && !(Number.isSafeInteger(value) && value >= 0))) throw new Error('Invalid bundle summary')
|
|
29
43
|
return counts
|
|
30
44
|
} finally { cached.stream.destroy() }
|
|
31
45
|
}
|
|
@@ -50,7 +64,7 @@ export function createBundleSummaryCache(storage: BundleCacheStorage, build: (re
|
|
|
50
64
|
if (existing) return existing.job
|
|
51
65
|
// Concurrent catalogs share the decoded summary, never the response stream.
|
|
52
66
|
// Only in-flight work is shared: misses and failures remain retryable.
|
|
53
|
-
const job: Promise<CachedSummary | null> =
|
|
67
|
+
const job: Promise<CachedSummary | null> = readCurrentSummary(storage, record.id).then(value => {
|
|
54
68
|
if (reads.get(record.integrity)?.job === job) remember(record, value)
|
|
55
69
|
return entries.get(record.integrity)?.value ?? value
|
|
56
70
|
}).catch(error => {
|
|
@@ -72,7 +86,7 @@ export function createBundleSummaryCache(storage: BundleCacheStorage, build: (re
|
|
|
72
86
|
}
|
|
73
87
|
async function summaryStatus(record: BundleCacheRecord) {
|
|
74
88
|
const value = ['stasis', 'sourcemap'].includes(record.kind ?? '') ? await read(record) : null
|
|
75
|
-
return { summary: value
|
|
89
|
+
return { summary: !value || 'retryAt' in value ? null : 'previous' in value ? value.previous : value,
|
|
76
90
|
summaryRetryAt: value && 'retryAt' in value ? value.retryAt : null }
|
|
77
91
|
}
|
|
78
92
|
async function backfill(records: readonly BundleCacheRecord[]) {
|
|
@@ -83,7 +97,7 @@ export function createBundleSummaryCache(storage: BundleCacheStorage, build: (re
|
|
|
83
97
|
for (const record of records) {
|
|
84
98
|
if (!['stasis', 'sourcemap'].includes(record.kind ?? '')) continue
|
|
85
99
|
const cached = await read(record).catch(() => null)
|
|
86
|
-
if (cached && (!('retryAt' in cached) || cached.retryAt > Date.now())) continue
|
|
100
|
+
if (cached && !('previous' in cached) && (!('retryAt' in cached) || cached.retryAt > Date.now())) continue
|
|
87
101
|
if (!await exists(record.id)) continue
|
|
88
102
|
if (attempts++ >= BACKFILL_LIMIT) break
|
|
89
103
|
const job = buildOne(record)
|
package/server-managed/config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { databaseUrls } from '../server-common/database-config.ts'
|
|
|
6
6
|
import { MAX_UPLOAD_BYTES } from './uploads.ts'
|
|
7
7
|
import { parseStorageKey } from '../server-common/storage-crypto.ts'
|
|
8
8
|
import { defaultCacheDir } from '@preventive/upstream/npm.js'
|
|
9
|
+
import { RESERVED_COOKIES } from './session.ts'
|
|
9
10
|
|
|
10
11
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost'])
|
|
11
12
|
|
|
@@ -117,6 +118,9 @@ export function loadManagedConfig({ combined = false } = {}): ManagedConfig {
|
|
|
117
118
|
if (sessionCookieName.startsWith('__Host-') && !cookieSecure) {
|
|
118
119
|
fail(`SESSION_COOKIE_NAME=${sessionCookieName} uses the __Host- prefix but the callback is not https. Use a non-prefixed name for loopback http dev.`)
|
|
119
120
|
}
|
|
121
|
+
if (RESERVED_COOKIES.includes(sessionCookieName.replace(/^__Host-/u, ''))) {
|
|
122
|
+
fail(`SESSION_COOKIE_NAME=${sessionCookieName} is reserved for another managed cookie. Choose a different name.`)
|
|
123
|
+
}
|
|
120
124
|
return {
|
|
121
125
|
neonUrl, blobToken, serverless, storageEncryptionKey, storageEncryptionMigrate,
|
|
122
126
|
storageEncryptionMigrateMaxMs: intEnv('MANAGED_STORAGE_ENCRYPTION_MIGRATE_MAX_MS', 150_000, 1, serverless ? 240_000 : 3_600_000),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type LinkReportStore, linkReportMethods, linkRevision } from './link-reports.ts'
|
|
2
2
|
import { type RepositoryAliasStore, repositoryAliasMethods } from './repository-aliases.ts'
|
|
3
|
+
import { type DependencyReviewStore, dependencyReviewMethods } from './dependency-reviews.ts'
|
|
4
|
+
import { type TeamNpmScopeStore, teamNpmScopeMethods } from './team-npm-scopes.ts'
|
|
3
5
|
import { createHash, randomUUID } from 'node:crypto'
|
|
4
6
|
import { teamCatalogRevision } from './team-catalog.ts'
|
|
5
7
|
import { type Role, roleAtLeast } from '../common/managed/roles.ts'
|
|
@@ -8,7 +10,7 @@ import type { TriageEntryPatch } from '../common/managed/triage.ts'
|
|
|
8
10
|
import { preferredSlug } from './slugs.ts'
|
|
9
11
|
import { type CommentStore, commentMethods } from './comments.ts'
|
|
10
12
|
import { type ActivityStore, activityMethods } from './activity.ts'
|
|
11
|
-
import { type GithubMetadataStore, githubMetadataMethods } from './github-metadata.ts'
|
|
13
|
+
import { type GithubCommitStore, type GithubMetadataStore, githubCommitMethods, githubMetadataMethods } from './github-metadata.ts'
|
|
12
14
|
import { type ManagedIssueStore, managedIssueMethods } from './managed-issues.ts'
|
|
13
15
|
import type { IssueFixUpdate } from './github-issue-links.ts'
|
|
14
16
|
import { type BundleBuildLeaseStore, bundleBuildLeaseMethods } from './bundle-build-leases.ts'
|
|
@@ -65,6 +67,16 @@ export interface ManagedSession {
|
|
|
65
67
|
userId: string
|
|
66
68
|
csrfToken: string
|
|
67
69
|
expiresAt: number
|
|
70
|
+
// Private JWK for sealed uploads; null until the session first asks for it.
|
|
71
|
+
uploadKey?: string | null
|
|
72
|
+
// Set on an administrator's read-only view as `userId` (see view-as.ts).
|
|
73
|
+
viewer?: ManagedViewer
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ManagedViewer {
|
|
77
|
+
id: string
|
|
78
|
+
login: string
|
|
79
|
+
name: string | null
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
// A repository selected for the workspace to operate on, with the context to
|
|
@@ -343,6 +355,7 @@ export interface UserTeam {
|
|
|
343
355
|
id: string
|
|
344
356
|
slug: string
|
|
345
357
|
name: string
|
|
358
|
+
dependencyReview?: boolean
|
|
346
359
|
permissions: TeamUserPermissions
|
|
347
360
|
cacheKey?: string
|
|
348
361
|
reports: UserTeamReport[]
|
|
@@ -350,7 +363,7 @@ export interface UserTeam {
|
|
|
350
363
|
}
|
|
351
364
|
|
|
352
365
|
// Backend-agnostic store surface (SQLite + PostgreSQL implementations).
|
|
353
|
-
export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataStore, ManagedIssueStore, BundleBuildLeaseStore, UpstreamCacheStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb, LinkReportStore, RepositoryAliasStore {
|
|
366
|
+
export interface ManagedDb extends DependencyReviewStore, ActivityStore, CommentStore, GithubMetadataStore, GithubCommitStore, ManagedIssueStore, BundleBuildLeaseStore, UpstreamCacheStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb, LinkReportStore, RepositoryAliasStore, TeamNpmScopeStore {
|
|
354
367
|
claimMaintenanceLease(owner: string, now: number, until: number, migration?: boolean): Promise<boolean>
|
|
355
368
|
finishMaintenanceLease(owner: string, until: number): Promise<void>
|
|
356
369
|
getFeedState(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; catalog: number; annotations: number } | null>
|
|
@@ -361,7 +374,20 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
361
374
|
// promotes a matching No access identity only while it is the sole user.
|
|
362
375
|
upsertUser(user: ManagedUser, now: number, initialAdminGithubId?: number | null): Promise<string>
|
|
363
376
|
createSession(session: ManagedSession, now: number): Promise<void>
|
|
377
|
+
// A sign-in session; never a view session, so a view token in the session
|
|
378
|
+
// cookie authenticates nothing.
|
|
364
379
|
sessionWithUser(id: string, now: number): Promise<{ session: ManagedSession; user: StoredUser } | null>
|
|
380
|
+
// Replace the view opened by an admin's session with a view as `userId`.
|
|
381
|
+
// False, keeping any earlier view, unless that session is current, its user
|
|
382
|
+
// is still an admin, and `userId` is another existing account. The view
|
|
383
|
+
// expires with that session.
|
|
384
|
+
createViewSession(view: { id: string; viewerSessionId: string; userId: string; csrfToken: string }, now: number): Promise<boolean>
|
|
385
|
+
// A view session opened by `viewerSessionId`, while that session is current
|
|
386
|
+
// and its user is still an admin. Presence is the admin's, not the viewed user's.
|
|
387
|
+
viewSessionWithUser(id: string, viewerSessionId: string, now: number): Promise<{ session: ManagedSession; user: StoredUser } | null>
|
|
388
|
+
// Store `candidate` unless the session already has an upload key; returns
|
|
389
|
+
// the stored key, or null when the session no longer exists.
|
|
390
|
+
ensureSessionUploadKey(id: string, candidate: string): Promise<string | null>
|
|
365
391
|
deleteSession(id: string): Promise<void>
|
|
366
392
|
deleteExpiredSessions(now: number): Promise<number>
|
|
367
393
|
listUsers(): Promise<AdminUser[]>
|
|
@@ -506,9 +532,10 @@ export interface ManagedDb extends ActivityStore, CommentStore, GithubMetadataSt
|
|
|
506
532
|
}
|
|
507
533
|
|
|
508
534
|
type SessionRow = {
|
|
509
|
-
id: string; csrf: string; exp: number
|
|
535
|
+
id: string; csrf: string; exp: number; uploadKey: string | null
|
|
510
536
|
uid: string; login: string; name: string | null; avatar: string | null; role: Role
|
|
511
537
|
}
|
|
538
|
+
type ViewSessionRow = SessionRow & { viewerId: string; viewerLogin: string; viewerName: string | null }
|
|
512
539
|
|
|
513
540
|
type UserRow = { id: string; login: string; name: string | null; role: Role; created: number; lastSeen: number | null; lastActivity: number | null }
|
|
514
541
|
|
|
@@ -552,12 +579,43 @@ function prepareStatements(db: ManagedSql) {
|
|
|
552
579
|
`INSERT INTO managed_session (id, user_id, csrf_token, created_at, expires_at) VALUES (?, ?, ?, ?, ?)`,
|
|
553
580
|
),
|
|
554
581
|
selectSessionStmt: db.prepare(
|
|
555
|
-
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp,
|
|
582
|
+
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
556
583
|
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
557
584
|
FROM managed_session s
|
|
558
585
|
JOIN managed_user u ON u.id = s.user_id
|
|
559
586
|
WHERE s.id = ? AND s.expires_at > ?`,
|
|
560
587
|
),
|
|
588
|
+
selectOwnSessionStmt: db.prepare(
|
|
589
|
+
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
590
|
+
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
591
|
+
FROM managed_session s
|
|
592
|
+
JOIN managed_user u ON u.id = s.user_id
|
|
593
|
+
WHERE s.id = ? AND s.expires_at > ? AND s.viewer_session IS NULL`,
|
|
594
|
+
),
|
|
595
|
+
selectViewSessionStmt: db.prepare(
|
|
596
|
+
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
597
|
+
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role,
|
|
598
|
+
a.id AS viewerId, a.login AS viewerLogin, a.name AS viewerName
|
|
599
|
+
FROM managed_session s
|
|
600
|
+
JOIN managed_user u ON u.id = s.user_id
|
|
601
|
+
JOIN managed_session p ON p.id = s.viewer_session
|
|
602
|
+
JOIN managed_user a ON a.id = p.user_id
|
|
603
|
+
WHERE s.id = ? AND s.viewer_session = ? AND s.expires_at > ?
|
|
604
|
+
AND p.expires_at > ? AND p.viewer_session IS NULL AND a.role = 'admin'`,
|
|
605
|
+
),
|
|
606
|
+
deleteOtherViewsStmt: db.prepare(`DELETE FROM managed_session WHERE viewer_session = ? AND id <> ?`),
|
|
607
|
+
insertViewSessionStmt: db.prepare(
|
|
608
|
+
`INSERT INTO managed_session (id, user_id, csrf_token, created_at, expires_at, viewer_session)
|
|
609
|
+
SELECT ?, u.id, ?, ?, p.expires_at, p.id
|
|
610
|
+
FROM managed_session p
|
|
611
|
+
JOIN managed_user a ON a.id = p.user_id
|
|
612
|
+
JOIN managed_user u ON u.id = ?
|
|
613
|
+
WHERE p.id = ? AND p.expires_at > ? AND p.viewer_session IS NULL AND a.role = 'admin' AND u.id <> a.id`,
|
|
614
|
+
),
|
|
615
|
+
// A former admin's views end with the role, even if it is granted again.
|
|
616
|
+
deleteDemotedViewsStmt: db.prepare(
|
|
617
|
+
`DELETE FROM managed_session WHERE viewer_session IN (SELECT id FROM managed_session WHERE user_id = ?)`,
|
|
618
|
+
),
|
|
561
619
|
selectUsersStmt: db.prepare(
|
|
562
620
|
`SELECT u.id, u.login, u.name, u.role, u.created_at AS created, u.last_seen_at AS lastSeen,
|
|
563
621
|
(SELECT MAX(at) FROM (
|
|
@@ -580,6 +638,10 @@ function prepareStatements(db: ManagedSql) {
|
|
|
580
638
|
`SELECT gh_access_token AS access, gh_refresh_token AS refresh, gh_token_expires_at AS exp, gh_tokens_encrypted AS encrypted
|
|
581
639
|
FROM managed_user WHERE id = ?`,
|
|
582
640
|
),
|
|
641
|
+
// Concurrent first requests keep whichever key was stored first.
|
|
642
|
+
ensureUploadKeyStmt: db.prepare(
|
|
643
|
+
`UPDATE managed_session SET upload_key = COALESCE(upload_key, ?) WHERE id = ? RETURNING upload_key AS uploadKey`,
|
|
644
|
+
),
|
|
583
645
|
deleteSessionStmt: db.prepare(`DELETE FROM managed_session WHERE id = ?`),
|
|
584
646
|
deleteExpiredStmt: db.prepare(`DELETE FROM managed_session WHERE expires_at <= ?`),
|
|
585
647
|
upsertRepoStmt: db.prepare(
|
|
@@ -849,7 +911,7 @@ function prepareStatements(db: ManagedSql) {
|
|
|
849
911
|
setTeamHiddenStmt: db.prepare(`UPDATE managed_team SET hidden = ?, updated_at = ? WHERE id = ?`),
|
|
850
912
|
selectTeamsStmt: db.prepare(`SELECT id, slug, name, hidden FROM managed_team ORDER BY name ASC`),
|
|
851
913
|
selectTeamsForUserStmt: db.prepare(
|
|
852
|
-
`SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
914
|
+
`SELECT t.id AS id, t.slug AS slug, t.name AS name, t.dependency_review AS dependencyReview,
|
|
853
915
|
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
854
916
|
FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id
|
|
855
917
|
WHERE tu.user_id = ? AND t.hidden = 0 ORDER BY t.name ASC, t.id ASC`,
|
|
@@ -1033,7 +1095,7 @@ type ReportRow = {
|
|
|
1033
1095
|
async function authorizeUpload(stmts: ReturnType<typeof prepareStatements>, sessionId: string | undefined,
|
|
1034
1096
|
item: { uploadedBy: string | null; repoId: number | null; repoDirectory?: string }) {
|
|
1035
1097
|
if (sessionId === undefined) return // Trusted imports and fixtures use the raw store API.
|
|
1036
|
-
const user = await stmts.
|
|
1098
|
+
const user = await stmts.selectOwnSessionStmt.get(sessionId, Date.now()) as SessionRow | undefined
|
|
1037
1099
|
if (!user) throw new ManagedMutationError(401, 'unauthenticated')
|
|
1038
1100
|
if (user.uid !== item.uploadedBy || !roleAtLeast(user.role, 'manage')) throw new ManagedMutationError(403, 'forbidden')
|
|
1039
1101
|
if (item.repoId === null) return
|
|
@@ -1422,7 +1484,7 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1422
1484
|
return (await selectUserRepoScopesStmt.all(userId)) as { repoId: number; path: string | null }[]
|
|
1423
1485
|
},
|
|
1424
1486
|
async listTeamsForUser(userId: string): Promise<UserTeam[]> {
|
|
1425
|
-
const teams = (await selectTeamsForUserStmt.all(userId)) as { id: string; slug: string; name: string; dependencies: number; security: number }[]
|
|
1487
|
+
const teams = (await selectTeamsForUserStmt.all(userId)) as { id: string; slug: string; name: string; dependencyReview: number; dependencies: number; security: number }[]
|
|
1426
1488
|
const scopes = await stmts.selectUserTeamScopesStmt.all(userId) as { teamId: string; repoId: number; path: string; fullName: string }[]
|
|
1427
1489
|
const links = await linkRevision(db)
|
|
1428
1490
|
const reportsByTeam = new Map<string, UserTeamReport[]>()
|
|
@@ -1446,6 +1508,7 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1446
1508
|
}
|
|
1447
1509
|
return teams.map((t) => ({
|
|
1448
1510
|
id: t.id, slug: t.slug, name: t.name,
|
|
1511
|
+
...(t.dependencyReview === 1 ? { dependencyReview: true } : {}),
|
|
1449
1512
|
permissions: { dependencies: t.dependencies === 1, security: t.security === 1 },
|
|
1450
1513
|
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
1451
1514
|
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
@@ -1534,14 +1597,17 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1534
1597
|
const reports = reportMethods(stmts, db, key)
|
|
1535
1598
|
const triage = triageMethods(stmts, options.triageHistoryLimit ?? 0, db)
|
|
1536
1599
|
const {
|
|
1537
|
-
upsertUserStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt,
|
|
1538
|
-
touchUserSeenStmt, updateRoleStmt, updateTokensStmt, selectTokensStmt, deleteSessionStmt, deleteExpiredStmt,
|
|
1600
|
+
upsertUserStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectOwnSessionStmt, selectUsersStmt,
|
|
1601
|
+
touchUserSeenStmt, updateRoleStmt, updateTokensStmt, selectTokensStmt, ensureUploadKeyStmt, deleteSessionStmt, deleteExpiredStmt,
|
|
1602
|
+
selectViewSessionStmt, deleteOtherViewsStmt, insertViewSessionStmt, deleteDemotedViewsStmt,
|
|
1539
1603
|
} = stmts
|
|
1540
1604
|
|
|
1541
1605
|
const methods: Omit<ManagedDb, keyof ManagementStore | keyof ManagementCatalogStore> = {
|
|
1542
1606
|
...storageMethods(db, key),
|
|
1543
1607
|
...linkReportMethods(db, key),
|
|
1544
1608
|
...repositoryAliasMethods(db),
|
|
1609
|
+
...teamNpmScopeMethods(db),
|
|
1610
|
+
...dependencyReviewMethods(db),
|
|
1545
1611
|
...bundleBuildLeaseMethods(db),
|
|
1546
1612
|
...upstreamCacheMethods(db),
|
|
1547
1613
|
async claimMaintenanceLease(owner, now, until, migration = false) {
|
|
@@ -1570,14 +1636,33 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1570
1636
|
await touchUserSeenStmt.run(now, session.userId, now)
|
|
1571
1637
|
},
|
|
1572
1638
|
async sessionWithUser(id, now) {
|
|
1573
|
-
const row = (await
|
|
1639
|
+
const row = (await selectOwnSessionStmt.get(id, now)) as SessionRow | undefined
|
|
1574
1640
|
if (row == null) return null
|
|
1575
1641
|
await touchUserSeenStmt.run(now, row.uid, now)
|
|
1576
1642
|
return {
|
|
1577
|
-
session: { id: row.id, userId: row.uid, csrfToken: row.csrf, expiresAt: row.exp },
|
|
1643
|
+
session: { id: row.id, userId: row.uid, csrfToken: row.csrf, expiresAt: row.exp, uploadKey: row.uploadKey },
|
|
1578
1644
|
user: { id: row.uid, login: row.login, name: row.name, avatarUrl: row.avatar, role: row.role },
|
|
1579
1645
|
}
|
|
1580
1646
|
},
|
|
1647
|
+
async createViewSession(view, now) {
|
|
1648
|
+
if (Number((await insertViewSessionStmt.run(view.id, view.csrfToken, now, view.userId, view.viewerSessionId, now)).changes) === 0) return false
|
|
1649
|
+
await deleteOtherViewsStmt.run(view.viewerSessionId, view.id)
|
|
1650
|
+
return true
|
|
1651
|
+
},
|
|
1652
|
+
async viewSessionWithUser(id, viewerSessionId, now) {
|
|
1653
|
+
const row = (await selectViewSessionStmt.get(id, viewerSessionId, now, now)) as ViewSessionRow | undefined
|
|
1654
|
+
if (row == null) return null
|
|
1655
|
+
await touchUserSeenStmt.run(now, row.viewerId, now)
|
|
1656
|
+
return {
|
|
1657
|
+
session: { id: row.id, userId: row.uid, csrfToken: row.csrf, expiresAt: row.exp, uploadKey: row.uploadKey,
|
|
1658
|
+
viewer: { id: row.viewerId, login: row.viewerLogin, name: row.viewerName } },
|
|
1659
|
+
user: { id: row.uid, login: row.login, name: row.name, avatarUrl: row.avatar, role: row.role },
|
|
1660
|
+
}
|
|
1661
|
+
},
|
|
1662
|
+
async ensureSessionUploadKey(id, candidate) {
|
|
1663
|
+
const row = (await ensureUploadKeyStmt.get(candidate, id)) as { uploadKey: string } | undefined
|
|
1664
|
+
return row?.uploadKey ?? null
|
|
1665
|
+
},
|
|
1581
1666
|
async deleteSession(id) {
|
|
1582
1667
|
await deleteSessionStmt.run(id)
|
|
1583
1668
|
},
|
|
@@ -1589,6 +1674,7 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1589
1674
|
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 }))
|
|
1590
1675
|
},
|
|
1591
1676
|
async setUserRole(id, role) {
|
|
1677
|
+
if (role !== 'admin') await deleteDemotedViewsStmt.run(id)
|
|
1592
1678
|
return Number((await updateRoleStmt.run(role, Date.now(), id)).changes) > 0
|
|
1593
1679
|
},
|
|
1594
1680
|
async setUserTokens(id, tokens) {
|
|
@@ -1610,6 +1696,7 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1610
1696
|
return row?.githubId ?? null
|
|
1611
1697
|
},
|
|
1612
1698
|
...githubMetadataMethods(db),
|
|
1699
|
+
...githubCommitMethods(db),
|
|
1613
1700
|
...managedIssueMethods(db),
|
|
1614
1701
|
...selectedRepoMethods(stmts),
|
|
1615
1702
|
...activity,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
|
|
1
2
|
import { LINK_REPORT_SCHEMA } from './link-reports.ts'
|
|
2
3
|
import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
|
|
4
|
+
import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
|
|
3
5
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
4
6
|
import { WebSocket } from 'ws'
|
|
5
7
|
import { type ManagedDb, type ManagedDbOptions, createManagedMethods } from './db-methods.ts'
|
|
6
8
|
import { revisionSchema } from './revisions.ts'
|
|
7
|
-
import { MANAGED_SCHEMA } from './db-schema.ts'
|
|
9
|
+
import { MANAGED_SCHEMA, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
8
10
|
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
9
|
-
import { GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
11
|
+
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
10
12
|
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
11
13
|
import { BUNDLE_BUILD_LEASE_SCHEMA } from './bundle-build-leases.ts'
|
|
12
14
|
import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
|
|
@@ -34,7 +36,7 @@ async function currentSchema(db: PgConnection): Promise<boolean> {
|
|
|
34
36
|
const exists = (await db.query("SELECT to_regclass('managed_schema_version') AS name")).rows[0]?.['name']
|
|
35
37
|
if (!exists) return false
|
|
36
38
|
const versions = new Set((await db.query('SELECT version FROM managed_schema_version')).rows.map(row => Number(row['version'])))
|
|
37
|
-
return Array.from({ length:
|
|
39
|
+
return Array.from({ length: 27 }, (_, i) => i + 1).every(version => versions.has(version))
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
async function migrateRepositoryDefaultCache(db: PgConnection): Promise<void> {
|
|
@@ -107,6 +109,39 @@ async function migrateBundleBuildConditions(db: PgConnection): Promise<void> {
|
|
|
107
109
|
await db.query('INSERT INTO managed_schema_version VALUES (22)')
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
// Existing sessions create their upload key on first use.
|
|
113
|
+
async function migrateSessionUploadKeys(db: PgConnection): Promise<void> {
|
|
114
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 23')).rows.length > 0) return
|
|
115
|
+
await db.query('ALTER TABLE managed_session ADD COLUMN IF NOT EXISTS upload_key TEXT')
|
|
116
|
+
await db.query('INSERT INTO managed_schema_version VALUES (23)')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function migrateViewSessions(db: PgConnection): Promise<void> {
|
|
120
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 24')).rows.length > 0) return
|
|
121
|
+
await db.query('ALTER TABLE managed_session ADD COLUMN IF NOT EXISTS viewer_session TEXT REFERENCES managed_session(id) ON DELETE CASCADE')
|
|
122
|
+
await db.query(VIEW_SESSION_INDEX)
|
|
123
|
+
await db.query('INSERT INTO managed_schema_version VALUES (24)')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function migrateGithubCommits(db: PgConnection): Promise<void> {
|
|
127
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 25')).rows.length > 0) return
|
|
128
|
+
await db.query(postgresSchema(GITHUB_COMMIT_SCHEMA))
|
|
129
|
+
await db.query('INSERT INTO managed_schema_version VALUES (25)')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function migrateTeamNpmScopes(db: PgConnection): Promise<void> {
|
|
133
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 26')).rows.length > 0) return
|
|
134
|
+
await db.query(postgresSchema(TEAM_NPM_SCOPE_SCHEMA))
|
|
135
|
+
await db.query('INSERT INTO managed_schema_version VALUES (26)')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function migrateDependencyReviews(db: PgConnection): Promise<void> {
|
|
139
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 27')).rows.length > 0) return
|
|
140
|
+
await db.query('ALTER TABLE managed_team ADD COLUMN IF NOT EXISTS dependency_review INTEGER NOT NULL DEFAULT 0')
|
|
141
|
+
await db.query(postgresSchema(DEPENDENCY_REVIEW_SCHEMA))
|
|
142
|
+
await db.query('INSERT INTO managed_schema_version VALUES (27)')
|
|
143
|
+
}
|
|
144
|
+
|
|
110
145
|
async function initialize(db: PgConnection): Promise<void> {
|
|
111
146
|
if (await currentSchema(db)) return
|
|
112
147
|
await db.query('BEGIN')
|
|
@@ -180,7 +215,7 @@ async function initialize(db: PgConnection): Promise<void> {
|
|
|
180
215
|
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 11')).rows.length === 0) {
|
|
181
216
|
await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (11)')
|
|
182
217
|
}
|
|
183
|
-
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions]) await migrate(db)
|
|
218
|
+
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews]) await migrate(db)
|
|
184
219
|
await db.query('COMMIT')
|
|
185
220
|
} catch (err) {
|
|
186
221
|
await db.query('ROLLBACK')
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
|
|
1
2
|
import { LINK_REPORT_SCHEMA } from './link-reports.ts'
|
|
2
3
|
import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
|
|
3
4
|
import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
|
|
5
|
+
import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
|
|
6
|
+
|
|
7
|
+
// Created after the column migration, which predates it on older databases.
|
|
8
|
+
export const VIEW_SESSION_INDEX = 'CREATE INDEX IF NOT EXISTS managed_session_viewer_idx ON managed_session(viewer_session);'
|
|
4
9
|
|
|
5
10
|
export const MANAGED_SCHEMA = `
|
|
6
11
|
CREATE TABLE IF NOT EXISTS managed_user (
|
|
@@ -28,7 +33,13 @@ CREATE TABLE IF NOT EXISTS managed_session (
|
|
|
28
33
|
user_id TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
|
|
29
34
|
csrf_token TEXT NOT NULL,
|
|
30
35
|
created_at INTEGER NOT NULL,
|
|
31
|
-
expires_at INTEGER NOT NULL
|
|
36
|
+
expires_at INTEGER NOT NULL,
|
|
37
|
+
-- Private X25519 JWK that this session's uploads are sealed to (see
|
|
38
|
+
-- common/managed/upload-seal.ts). Created on first use.
|
|
39
|
+
upload_key TEXT,
|
|
40
|
+
-- An administrator's read-only view as user_id: the admin's own session,
|
|
41
|
+
-- which must stay valid. Ending that session ends the view.
|
|
42
|
+
viewer_session TEXT REFERENCES managed_session(id) ON DELETE CASCADE
|
|
32
43
|
) STRICT;
|
|
33
44
|
|
|
34
45
|
CREATE INDEX IF NOT EXISTS managed_session_user_idx ON managed_session(user_id);
|
|
@@ -183,6 +194,7 @@ CREATE TABLE IF NOT EXISTS managed_team (
|
|
|
183
194
|
slug TEXT NOT NULL,
|
|
184
195
|
name TEXT NOT NULL UNIQUE,
|
|
185
196
|
hidden INTEGER NOT NULL DEFAULT 0,
|
|
197
|
+
dependency_review INTEGER NOT NULL DEFAULT 0,
|
|
186
198
|
created_at INTEGER NOT NULL,
|
|
187
199
|
updated_at INTEGER NOT NULL
|
|
188
200
|
) STRICT;
|
|
@@ -212,4 +224,4 @@ CREATE TABLE IF NOT EXISTS managed_team_user (
|
|
|
212
224
|
) STRICT;
|
|
213
225
|
|
|
214
226
|
CREATE INDEX IF NOT EXISTS team_user_user_idx ON managed_team_user(user_id);
|
|
215
|
-
` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA
|
|
227
|
+
` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA + TEAM_NPM_SCOPE_SCHEMA + DEPENDENCY_REVIEW_SCHEMA
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Table identifiers are code constants. Renames retain rows and foreign keys;
|
|
2
2
|
// an ambiguous old/new pair must be resolved before the server can start.
|
|
3
|
-
|
|
3
|
+
const MANAGED_TABLE_RENAMES = [
|
|
4
4
|
['selected_repo', 'managed_selected_repo'],
|
|
5
5
|
['team_repo', 'managed_team_repo'],
|
|
6
6
|
['team_user', 'managed_team_user'],
|
package/server-managed/db.ts
CHANGED
|
@@ -7,9 +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 } from './db-schema.ts'
|
|
10
|
+
import { MANAGED_SCHEMA, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
11
11
|
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
12
|
-
import { GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
12
|
+
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
13
13
|
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
14
14
|
import { BUNDLE_BUILD_LEASE_SCHEMA } from './bundle-build-leases.ts'
|
|
15
15
|
import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
|
|
@@ -69,8 +69,11 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
69
69
|
db.exec('PRAGMA synchronous = FULL;')
|
|
70
70
|
db.exec('PRAGMA foreign_keys = ON;')
|
|
71
71
|
migrateManagedTableNames(db)
|
|
72
|
-
db.exec(MANAGED_SCHEMA + GITHUB_METADATA_SCHEMA + MANAGED_ISSUE_SCHEMA + STORAGE_SCHEMA + BUNDLE_BUILD_LEASE_SCHEMA + UPSTREAM_CACHE_SCHEMA)
|
|
72
|
+
db.exec(MANAGED_SCHEMA + GITHUB_METADATA_SCHEMA + GITHUB_COMMIT_SCHEMA + MANAGED_ISSUE_SCHEMA + STORAGE_SCHEMA + BUNDLE_BUILD_LEASE_SCHEMA + UPSTREAM_CACHE_SCHEMA)
|
|
73
73
|
ensureColumn(db, 'managed_selected_repo', 'cached_default_branch', 'TEXT')
|
|
74
|
+
ensureColumn(db, 'managed_session', 'upload_key', 'TEXT')
|
|
75
|
+
ensureColumn(db, 'managed_session', 'viewer_session', 'TEXT REFERENCES managed_session(id) ON DELETE CASCADE')
|
|
76
|
+
db.exec(VIEW_SESSION_INDEX)
|
|
74
77
|
for (const table of ['managed_report', 'managed_bundle']) {
|
|
75
78
|
ensureColumn(db, table, 'data_key', 'TEXT')
|
|
76
79
|
ensureColumn(db, table, 'storage_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
@@ -109,6 +112,7 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
109
112
|
ensureColumn(db, 'managed_bundle', 'provenance', 'TEXT')
|
|
110
113
|
ensureColumn(db, 'managed_bundle', 'build_conditions', 'TEXT')
|
|
111
114
|
ensureColumn(db, 'managed_team', 'hidden', 'INTEGER NOT NULL DEFAULT 0')
|
|
115
|
+
ensureColumn(db, 'managed_team', 'dependency_review', 'INTEGER NOT NULL DEFAULT 0')
|
|
112
116
|
ensureColumn(db, 'managed_finding_issue', 'auto_fix_url', 'TEXT')
|
|
113
117
|
ensureColumn(db, 'managed_finding_issue', 'auto_fix_checked_at', 'INTEGER')
|
|
114
118
|
ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
|