@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.40
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/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +2 -0
- package/client/dependency-paths.js +24 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +112 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -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 +14 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +310 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- 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/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- 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/comments.ts +34 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +254 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- 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/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +35 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -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-4F6JSF57.js +3 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-AQ75QITE.js +1 -0
- package/out/chunk-B2RQY6OP.js +2 -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-GJ4IUCNS.js +1 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-IISEF2BZ.js +1 -0
- package/out/chunk-IU3CCINR.js +3 -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-MYDDTA7W.js +225 -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-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-RSHMVYNS.js +1 -0
- package/out/chunk-TGANXVTA.js +1 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-UN5ZSYLE.js +1 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UXNUAIIC.js +33 -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-VQJCXGIY.js +7 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-WZDTZE64.js +1 -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-ZBBOBQ43.js +1 -0
- package/out/chunk-ZMQQCGF6.js +2 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +37 -0
- package/out/client-sync.js +10 -13
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +6 -2
- package/out/stasis.svg +20 -0
- package/out/terminal.js +365 -39
- package/out/view.css +1 -1
- package/out/view.js +83 -117
- package/package.json +297 -59
- 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-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +486 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +56 -35
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +116 -18
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +35 -18
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +27 -26
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +102 -68
- package/{server → server-e2e}/sse-session.ts +28 -8
- package/{server → server-e2e}/static.ts +31 -21
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +245 -0
- package/server-managed/avatar-store.ts +12 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +111 -0
- package/server-managed/bundle-build.ts +198 -0
- package/server-managed/bundle-cache.ts +199 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +120 -0
- package/server-managed/cache-storage.ts +19 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +158 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1725 -0
- package/server-managed/db-neon.ts +367 -0
- package/server-managed/db-schema.ts +227 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +132 -0
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +391 -0
- package/server-managed/github-errors.ts +68 -0
- package/server-managed/github-issue-links.ts +127 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +235 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/http-response.ts +50 -0
- package/server-managed/http.ts +3472 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +187 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +158 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +75 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -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/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +74 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +220 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +47 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +153 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-risk.ts +418 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/session.ts +92 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +31 -0
- package/server-managed/sql.ts +76 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +279 -0
- package/server-managed/storage-maintenance.ts +256 -0
- package/server-managed/storage-payload.ts +103 -0
- package/server-managed/storage-stores.ts +85 -0
- package/server-managed/storage.ts +78 -0
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +173 -0
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/team-reports.ts +228 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +86 -0
- package/server-managed/upstream-cache.ts +215 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +166 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -477
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -320
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// SQLite bootstrap and legacy upgrades. Runtime methods are shared with Neon.
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
5
|
+
import { migrateSlugs } from './slugs.ts'
|
|
6
|
+
import { migrateReportLocations } from './report-migration.ts'
|
|
7
|
+
import { initCommentMethods } from './comments.ts'
|
|
8
|
+
import { initActivityMethods } from './activity.ts'
|
|
9
|
+
import { revisionSchema } from './revisions.ts'
|
|
10
|
+
import { MANAGED_SCHEMA, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
11
|
+
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
12
|
+
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
13
|
+
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
14
|
+
import { BUNDLE_BUILD_LEASE_SCHEMA } from './bundle-build-leases.ts'
|
|
15
|
+
import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
|
|
16
|
+
import { managedTableRenames } from './db-table-names.ts'
|
|
17
|
+
import { type ManagedDb, type ManagedDbOptions, createManagedMethods } from './db-methods.ts'
|
|
18
|
+
import { createSqliteDriver, scopeManagedMethods } from './sql.ts'
|
|
19
|
+
export type * from './db-methods.ts'
|
|
20
|
+
|
|
21
|
+
function migrateManagedTableNames(db: DatabaseSync): void {
|
|
22
|
+
db.exec('BEGIN IMMEDIATE')
|
|
23
|
+
try {
|
|
24
|
+
db.exec('CREATE TABLE IF NOT EXISTS managed_schema_version (version INTEGER PRIMARY KEY) STRICT;')
|
|
25
|
+
if (!db.prepare('SELECT version FROM managed_schema_version WHERE version = 3').get()) {
|
|
26
|
+
const rows = db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all() as { name: string }[]
|
|
27
|
+
for (const [from, to] of managedTableRenames(new Set(rows.map(row => row.name)))) {
|
|
28
|
+
db.exec(`ALTER TABLE ${from} RENAME TO ${to}`)
|
|
29
|
+
}
|
|
30
|
+
db.exec('INSERT INTO managed_schema_version VALUES (3)')
|
|
31
|
+
}
|
|
32
|
+
db.exec('COMMIT')
|
|
33
|
+
} catch (err) { db.exec('ROLLBACK'); throw err }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Preserve the legacy single-path links while adding path to their identity.
|
|
37
|
+
function migrateTeamRepoPaths(db: DatabaseSync): void {
|
|
38
|
+
const columns = db.prepare('PRAGMA table_info(managed_team_repo)').all() as { name: string; pk: number }[]
|
|
39
|
+
if (columns.some(column => column.name === 'path' && column.pk === 3)) return
|
|
40
|
+
db.exec('BEGIN IMMEDIATE')
|
|
41
|
+
try {
|
|
42
|
+
db.exec(`CREATE TABLE managed_team_repo_paths (
|
|
43
|
+
team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
|
|
44
|
+
repo_id INTEGER NOT NULL REFERENCES managed_selected_repo(repo_id) ON DELETE CASCADE,
|
|
45
|
+
path TEXT NOT NULL DEFAULT '', PRIMARY KEY (team_id, repo_id, path)
|
|
46
|
+
) STRICT;
|
|
47
|
+
INSERT INTO managed_team_repo_paths SELECT team_id, repo_id, COALESCE(path, '') FROM managed_team_repo;
|
|
48
|
+
DROP TABLE managed_team_repo;
|
|
49
|
+
ALTER TABLE managed_team_repo_paths RENAME TO managed_team_repo;
|
|
50
|
+
CREATE INDEX team_repo_repo_idx ON managed_team_repo(repo_id);`)
|
|
51
|
+
db.exec('COMMIT')
|
|
52
|
+
} catch (err) { db.exec('ROLLBACK'); throw err }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add `column` to `table` if it's missing (a lightweight migration for DBs that
|
|
56
|
+
// predate the column; `table`/`column` are code constants, never user input).
|
|
57
|
+
function ensureColumn(db: DatabaseSync, table: string, column: string, type: string): boolean {
|
|
58
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[]
|
|
59
|
+
if (cols.some((c) => c.name === column)) return false
|
|
60
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`)
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}): ManagedDb {
|
|
65
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
66
|
+
const db = new DatabaseSync(path)
|
|
67
|
+
try {
|
|
68
|
+
db.exec('PRAGMA journal_mode = WAL;')
|
|
69
|
+
db.exec('PRAGMA synchronous = FULL;')
|
|
70
|
+
db.exec('PRAGMA foreign_keys = ON;')
|
|
71
|
+
migrateManagedTableNames(db)
|
|
72
|
+
db.exec(MANAGED_SCHEMA + GITHUB_METADATA_SCHEMA + GITHUB_COMMIT_SCHEMA + MANAGED_ISSUE_SCHEMA + STORAGE_SCHEMA + BUNDLE_BUILD_LEASE_SCHEMA + UPSTREAM_CACHE_SCHEMA)
|
|
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)
|
|
77
|
+
for (const table of ['managed_report', 'managed_bundle']) {
|
|
78
|
+
ensureColumn(db, table, 'data_key', 'TEXT')
|
|
79
|
+
ensureColumn(db, table, 'storage_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
80
|
+
}
|
|
81
|
+
ensureColumn(db, 'managed_user', 'gh_tokens_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
82
|
+
ensureColumn(db, 'managed_github_metadata', 'state_reason', GITHUB_STATE_REASON_COLUMN)
|
|
83
|
+
ensureColumn(db, 'managed_github_metadata', 'attempted_at', 'INTEGER')
|
|
84
|
+
// Existing public links must opt in too; never preserve an implicit grant.
|
|
85
|
+
ensureColumn(db, 'managed_workspace_share', 'dependencies', 'INTEGER NOT NULL DEFAULT 0')
|
|
86
|
+
ensureColumn(db, 'managed_workspace_share', 'security', 'INTEGER NOT NULL DEFAULT 0')
|
|
87
|
+
migrateTeamRepoPaths(db)
|
|
88
|
+
migrateSlugs(db)
|
|
89
|
+
// Migrate DBs created before a column existed (CREATE TABLE IF NOT EXISTS
|
|
90
|
+
// never alters an already-present table). Idempotent — skipped on fresh DBs.
|
|
91
|
+
const addedLastSeen = ensureColumn(db, 'managed_user', 'last_seen_at', 'INTEGER')
|
|
92
|
+
if (addedLastSeen) {
|
|
93
|
+
// Session creation is known authentication activity. updated_at is not:
|
|
94
|
+
// role changes and token refreshes also wrote it. Keep unknowns NULL.
|
|
95
|
+
db.exec(`UPDATE managed_user SET last_seen_at = (
|
|
96
|
+
SELECT MAX(s.created_at) FROM managed_session s WHERE s.user_id = managed_user.id
|
|
97
|
+
)`)
|
|
98
|
+
}
|
|
99
|
+
ensureColumn(db, 'managed_report', 'uploaded_by_login', 'TEXT')
|
|
100
|
+
ensureColumn(db, 'managed_report', 'repo_embedded', 'INTEGER NOT NULL DEFAULT 0')
|
|
101
|
+
migrateReportLocations(db, join(dirname(path), 'reports'))
|
|
102
|
+
ensureColumn(db, 'managed_report', 'analyzer', 'TEXT')
|
|
103
|
+
// Existing rows predate publication controls and were already visible.
|
|
104
|
+
// Backfill only when adding the column; newly inserted rows still use the
|
|
105
|
+
// hidden default and must be explicitly published.
|
|
106
|
+
const addedVisible = ensureColumn(db, 'managed_report', 'visible', 'INTEGER NOT NULL DEFAULT 0')
|
|
107
|
+
if (addedVisible) db.prepare('UPDATE managed_report SET visible = 1 WHERE visible = 0').run()
|
|
108
|
+
ensureColumn(db, 'managed_bundle', 'uploaded_by_login', 'TEXT')
|
|
109
|
+
ensureColumn(db, 'managed_bundle', 'repo_directory', "TEXT NOT NULL DEFAULT ''")
|
|
110
|
+
ensureColumn(db, 'managed_bundle', 'visible', 'INTEGER NOT NULL DEFAULT 1')
|
|
111
|
+
// Existing rows stay NULL (unknown): some may have been built on the server.
|
|
112
|
+
ensureColumn(db, 'managed_bundle', 'provenance', 'TEXT')
|
|
113
|
+
ensureColumn(db, 'managed_bundle', 'build_conditions', 'TEXT')
|
|
114
|
+
ensureColumn(db, 'managed_team', 'hidden', 'INTEGER NOT NULL DEFAULT 0')
|
|
115
|
+
ensureColumn(db, 'managed_team', 'dependency_review', 'INTEGER NOT NULL DEFAULT 0')
|
|
116
|
+
ensureColumn(db, 'managed_finding_issue', 'auto_fix_url', 'TEXT')
|
|
117
|
+
ensureColumn(db, 'managed_finding_issue', 'auto_fix_checked_at', 'INTEGER')
|
|
118
|
+
ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
|
|
119
|
+
ensureColumn(db, 'managed_finding_triage_event', 'report_id', 'TEXT')
|
|
120
|
+
ensureColumn(db, 'managed_finding_triage_event', 'report', 'TEXT')
|
|
121
|
+
ensureColumn(db, 'managed_finding_triage_event', 'repo', 'TEXT')
|
|
122
|
+
initCommentMethods(db)
|
|
123
|
+
initActivityMethods(db)
|
|
124
|
+
db.exec(revisionSchema())
|
|
125
|
+
} catch (err) {
|
|
126
|
+
try { db.close() } catch {}
|
|
127
|
+
throw err
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const driver = createSqliteDriver(db)
|
|
131
|
+
return scopeManagedMethods(createManagedMethods(driver, options), driver)
|
|
132
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { ManagedMutationError } from './management.ts'
|
|
3
|
+
import type { ManagedSql } from './sql.ts'
|
|
4
|
+
import { REVIEW_VERDICTS, type ReviewSummary, type ReviewVerdict, allowedReviewVerdicts, needsPackageJsonReview, summarizeReview } from '../common/dependency-reviews.js'
|
|
5
|
+
|
|
6
|
+
export interface ReviewContext {
|
|
7
|
+
ecosystem: 'npm'; package: string; version: string; integrity: string
|
|
8
|
+
basePackage: string | null; baseVersion: string | null; baseIntegrity: string | null
|
|
9
|
+
}
|
|
10
|
+
export interface ReviewInput { file: string; comment: string; status: ReviewVerdict | null }
|
|
11
|
+
export interface ReviewEntry extends ReviewInput { id: string; time: number }
|
|
12
|
+
export interface ReviewTeam { id: string; name: string }
|
|
13
|
+
export interface DependencyReview extends ReviewContext, ReviewSummary {
|
|
14
|
+
id: string; user: string | null; userLogin: string; time: number; revision: number
|
|
15
|
+
publishedAt: number | null; revokedAt: number | null; entries: ReviewEntry[]; teams: ReviewTeam[]; files: string[] | null
|
|
16
|
+
nonTextFiles: string[] | null
|
|
17
|
+
status: ReviewVerdict | null; comment: string
|
|
18
|
+
}
|
|
19
|
+
export type ReviewChange = { action: 'save'; reviewId: string | null; revision: number | null; entryId: string | null; entry: ReviewInput }
|
|
20
|
+
| { action: 'start'; reviewId: null; revision: null }
|
|
21
|
+
| { action: 'publish'; reviewId: string; revision: number; teamIds: string[]; files: string[]; nonTextFiles: string[]; final: { status: ReviewVerdict; comment: string } | null }
|
|
22
|
+
| { action: 'continue'; reviewId: string; revision: number }
|
|
23
|
+
| { action: 'revoke'; reviewId: string; revision: number }
|
|
24
|
+
export interface DependencyReviewStore {
|
|
25
|
+
listDependencyReviews(context: ReviewContext, user: string, allBases?: boolean): Promise<DependencyReview[]>
|
|
26
|
+
getLatestOwnDependencyReview(user: string, target: Pick<ReviewContext, 'ecosystem' | 'package' | 'version' | 'integrity'>): Promise<DependencyReview | null>
|
|
27
|
+
listOwnDependencyReviews(user: string, before: { time: number; id: string } | null): Promise<{ reviews: DependencyReview[]; next: { time: number; id: string } | null }>
|
|
28
|
+
listDependencyReviewTeams(user: string): Promise<ReviewTeam[]>
|
|
29
|
+
listDependencyReviewEnabledTeams(): Promise<string[]>
|
|
30
|
+
setTeamDependencyReview(sessionId: string, teamId: string, enabled: boolean): Promise<boolean>
|
|
31
|
+
discardDependencyReview(sessionId: string, reviewId: string, revision: number): Promise<void>
|
|
32
|
+
changeDependencyReview(context: ReviewContext, sessionId: string, change: ReviewChange, now: number): Promise<DependencyReview>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// An unpublished review belongs only to its author. Publication freezes all
|
|
36
|
+
// entries together and attaches that snapshot to its selected teams. Continuing
|
|
37
|
+
// makes a new review; published entries and their timestamps never change.
|
|
38
|
+
export const DEPENDENCY_REVIEW_SCHEMA = `
|
|
39
|
+
CREATE TABLE IF NOT EXISTS managed_dependency_review (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
user_id TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
42
|
+
user_login TEXT NOT NULL, ecosystem TEXT NOT NULL,
|
|
43
|
+
package TEXT NOT NULL, version TEXT NOT NULL, integrity TEXT NOT NULL,
|
|
44
|
+
base_package TEXT NOT NULL, base_version TEXT NOT NULL, base_integrity TEXT NOT NULL,
|
|
45
|
+
time INTEGER NOT NULL, revision INTEGER NOT NULL, published_at INTEGER, revoked_at INTEGER, files TEXT,
|
|
46
|
+
status TEXT CHECK (status IN ('accept', 'accept-with-nits', 'request-changes', 'reject')), comment TEXT NOT NULL DEFAULT ''
|
|
47
|
+
) STRICT;
|
|
48
|
+
CREATE INDEX IF NOT EXISTS managed_dependency_review_context_idx
|
|
49
|
+
ON managed_dependency_review(ecosystem, package, version, integrity, base_package, base_version, base_integrity, time);
|
|
50
|
+
CREATE UNIQUE INDEX IF NOT EXISTS managed_dependency_review_unpublished_idx
|
|
51
|
+
ON managed_dependency_review(user_id, ecosystem, package, version, integrity, base_package, base_version, base_integrity)
|
|
52
|
+
WHERE published_at IS NULL;
|
|
53
|
+
CREATE INDEX IF NOT EXISTS managed_dependency_review_author_idx ON managed_dependency_review(user_id, time, id);
|
|
54
|
+
CREATE TABLE IF NOT EXISTS managed_dependency_file_review (
|
|
55
|
+
id TEXT PRIMARY KEY, review_id TEXT NOT NULL REFERENCES managed_dependency_review(id) ON DELETE CASCADE,
|
|
56
|
+
file TEXT NOT NULL, comment TEXT NOT NULL,
|
|
57
|
+
status TEXT CHECK (status IN ('accept', 'accept-with-nits', 'request-changes', 'reject')), time INTEGER NOT NULL
|
|
58
|
+
) STRICT;
|
|
59
|
+
CREATE INDEX IF NOT EXISTS managed_dependency_file_review_idx ON managed_dependency_file_review(review_id);
|
|
60
|
+
CREATE TABLE IF NOT EXISTS managed_dependency_review_team (
|
|
61
|
+
review_id TEXT NOT NULL REFERENCES managed_dependency_review(id) ON DELETE CASCADE,
|
|
62
|
+
team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
|
|
63
|
+
PRIMARY KEY (review_id, team_id)
|
|
64
|
+
) STRICT;
|
|
65
|
+
`
|
|
66
|
+
const values = (c: ReviewContext) => [c.ecosystem, c.package, c.version, c.integrity, c.basePackage ?? '', c.baseVersion ?? '', c.baseIntegrity ?? '']
|
|
67
|
+
const targetWhere = 'ecosystem = ? AND package = ? AND version = ? AND integrity = ?'
|
|
68
|
+
const contextWhere = `${targetWhere} AND base_package = ? AND base_version = ? AND base_integrity = ?`
|
|
69
|
+
const columns = `id, user_id AS user, user_login AS userLogin, ecosystem, package, version, integrity,
|
|
70
|
+
NULLIF(base_package, '') AS basePackage, NULLIF(base_version, '') AS baseVersion, NULLIF(base_integrity, '') AS baseIntegrity,
|
|
71
|
+
time, revision, published_at AS publishedAt, revoked_at AS revokedAt, files, status, comment`
|
|
72
|
+
const conflict = () => { throw new ManagedMutationError(409, 'review-conflict') }
|
|
73
|
+
|
|
74
|
+
// Fold legacy multi-note files into one text field without changing a frozen
|
|
75
|
+
// snapshot's stored rows. Continuing or saving uses one row per file.
|
|
76
|
+
function fileEntries(entries: ReviewEntry[]): ReviewEntry[] {
|
|
77
|
+
const files = new Map<string, ReviewEntry>(), verdicts = Object.keys(REVIEW_VERDICTS)
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (entry.status === null) continue
|
|
80
|
+
const current = files.get(entry.file)
|
|
81
|
+
if (!current) { files.set(entry.file, { ...entry }); continue }
|
|
82
|
+
current.comment = [current.comment, entry.comment].filter(Boolean).join('\n\n')
|
|
83
|
+
if (verdicts.indexOf(entry.status ?? '') > verdicts.indexOf(current.status ?? '')) current.status = entry.status
|
|
84
|
+
current.time = Math.max(current.time, entry.time)
|
|
85
|
+
}
|
|
86
|
+
return [...files.values()]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function actor(db: ManagedSql, sessionId: string, admin = false) {
|
|
90
|
+
const user = await db.prepare(`SELECT u.id, u.login, u.role FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
91
|
+
WHERE s.id = ? AND s.expires_at > ? AND s.viewer_session IS NULL`).get(sessionId, Date.now()) as { id: string; login: string; role: string } | undefined
|
|
92
|
+
if (!user) throw new ManagedMutationError(401, 'unauthenticated')
|
|
93
|
+
if (!(admin ? ['admin'] : ['triage', 'manage', 'admin']).includes(user.role)) throw new ManagedMutationError(403, 'forbidden')
|
|
94
|
+
return user
|
|
95
|
+
}
|
|
96
|
+
async function teams(db: ManagedSql, user: string): Promise<ReviewTeam[]> {
|
|
97
|
+
return await db.prepare(`SELECT t.id, t.name FROM managed_team t
|
|
98
|
+
JOIN managed_team_user m ON m.team_id = t.id AND m.user_id = ?
|
|
99
|
+
WHERE t.hidden = 0 AND t.dependency_review = 1 ORDER BY t.name, t.id`).all(user) as ReviewTeam[]
|
|
100
|
+
}
|
|
101
|
+
async function expandReviews(db: ManagedSql, rows: unknown[], user: string): Promise<DependencyReview[]> {
|
|
102
|
+
const reviews = rows as (Omit<DependencyReview, 'files'> & { files: string | null })[]
|
|
103
|
+
if (reviews.length === 0) return []
|
|
104
|
+
const ids = reviews.map(review => review.id), placeholders = ids.map(() => '?').join(',')
|
|
105
|
+
const entries = await db.prepare(`SELECT review_id AS reviewId, id, file, comment, status, time
|
|
106
|
+
FROM managed_dependency_file_review WHERE review_id IN (${placeholders}) ORDER BY time, id`).all(...ids) as (ReviewEntry & { reviewId: string })[]
|
|
107
|
+
const linked = await db.prepare(`SELECT rt.review_id AS reviewId, t.id, t.name FROM managed_dependency_review_team rt
|
|
108
|
+
JOIN managed_dependency_review r ON r.id = rt.review_id JOIN managed_team t ON t.id = rt.team_id
|
|
109
|
+
WHERE rt.review_id IN (${placeholders}) AND (r.user_id = ? OR (t.hidden = 0 AND EXISTS (
|
|
110
|
+
SELECT 1 FROM managed_team_user m WHERE m.team_id = t.id AND m.user_id = ?)))
|
|
111
|
+
ORDER BY t.name, t.id`).all(...ids, user, user) as (ReviewTeam & { reviewId: string })[]
|
|
112
|
+
return reviews.map(review => {
|
|
113
|
+
const snapshot = review.files === null ? null : JSON.parse(review.files) as string[] | { paths: string[]; nonTextFiles: string[] }
|
|
114
|
+
const files = Array.isArray(snapshot) ? snapshot : snapshot?.paths ?? null
|
|
115
|
+
const nonTextFiles = Array.isArray(snapshot) ? null : snapshot?.nonTextFiles ?? []
|
|
116
|
+
const ownEntries = fileEntries(entries.filter(entry => entry.reviewId === review.id).map(({ reviewId: _reviewId, ...entry }) => entry))
|
|
117
|
+
return { ...review, files, nonTextFiles, entries: ownEntries, ...summarizeReview(ownEntries, files, nonTextFiles),
|
|
118
|
+
teams: linked.filter(team => team.reviewId === review.id).map(({ reviewId: _reviewId, ...team }) => team) }
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function publish(db: ManagedSql, current: DependencyReview | undefined, user: string, change: Extract<ReviewChange, { action: 'publish' }>, now: number) {
|
|
123
|
+
if (!current || current.revokedAt !== null) return conflict()
|
|
124
|
+
const allowed = new Set((await teams(db, user)).map(team => team.id))
|
|
125
|
+
const selected = [...new Set(change.teamIds)]
|
|
126
|
+
if (selected.length === 0 || selected.some(team => !allowed.has(team))) throw new ManagedMutationError(400, 'bad-review-teams')
|
|
127
|
+
const entries = await db.prepare('SELECT file, status FROM managed_dependency_file_review WHERE review_id = ?').all(current.id) as ReviewInput[]
|
|
128
|
+
if (current.publishedAt === null) {
|
|
129
|
+
if (!summarizeReview(entries, change.files, change.nonTextFiles).checkedFiles) throw new ManagedMutationError(400, 'no-reviewed-files')
|
|
130
|
+
if (!change.final || !allowedReviewVerdicts(entries.filter(entry => !change.nonTextFiles.includes(entry.file))).includes(change.final.status)) throw new ManagedMutationError(400, 'bad-final-verdict')
|
|
131
|
+
if (needsPackageJsonReview(entries, change.files, change.nonTextFiles)) throw new ManagedMutationError(400, 'package-json-not-reviewed')
|
|
132
|
+
await db.prepare(`UPDATE managed_dependency_review SET published_at = ?, files = ?, status = ?, comment = ?, revision = revision + 1 WHERE id = ?`)
|
|
133
|
+
.run(now, JSON.stringify({ paths: [...new Set(change.files)].toSorted(), nonTextFiles: [...new Set(change.nonTextFiles)].toSorted() }), change.final.status, change.final.comment, current.id)
|
|
134
|
+
} else {
|
|
135
|
+
if (change.final) return conflict()
|
|
136
|
+
await db.prepare('UPDATE managed_dependency_review SET revision = revision + 1 WHERE id = ?').run(current.id)
|
|
137
|
+
}
|
|
138
|
+
for (const team of selected) await db.prepare('INSERT INTO managed_dependency_review_team (review_id, team_id) VALUES (?, ?) ON CONFLICT DO NOTHING').run(current.id, team)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function listOwn(db: ManagedSql, user: string, before: { time: number; id: string } | null) {
|
|
142
|
+
const rows = await db.prepare(`SELECT ${columns} FROM managed_dependency_review WHERE user_id = ?
|
|
143
|
+
${before ? 'AND (time < ? OR (time = ? AND id < ?))' : ''} ORDER BY time DESC, id DESC LIMIT 51`)
|
|
144
|
+
.all(user, ...before ? [before.time, before.time, before.id] : []) as DependencyReview[]
|
|
145
|
+
const page = rows.slice(0, 50)
|
|
146
|
+
const last = page.at(-1)
|
|
147
|
+
return { reviews: await expandReviews(db, page, user), next: rows.length > 50 && last ? { time: last.time, id: last.id } : null }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function saveFile(db: ManagedSql, reviewId: string, entry: ReviewInput, requestedId: string | null, now: number) {
|
|
151
|
+
const existing = await db.prepare(`SELECT id FROM managed_dependency_file_review
|
|
152
|
+
WHERE review_id = ? AND file = ? ORDER BY time, id`).all(reviewId, entry.file) as { id: string }[]
|
|
153
|
+
if (requestedId && !existing.some(row => row.id === requestedId)) return conflict()
|
|
154
|
+
if (entry.status === null) {
|
|
155
|
+
await db.prepare('DELETE FROM managed_dependency_file_review WHERE review_id = ? AND file = ?').run(reviewId, entry.file)
|
|
156
|
+
} else if (existing.length > 0) {
|
|
157
|
+
const entryId = existing[0]!.id
|
|
158
|
+
await db.prepare('UPDATE managed_dependency_file_review SET comment = ?, status = ?, time = ? WHERE id = ?')
|
|
159
|
+
.run(entry.comment, entry.status, now, entryId)
|
|
160
|
+
if (existing.length > 1) {
|
|
161
|
+
await db.prepare(`DELETE FROM managed_dependency_file_review
|
|
162
|
+
WHERE review_id = ? AND file = ? AND id <> ?`).run(reviewId, entry.file, entryId)
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
await db.prepare('INSERT INTO managed_dependency_file_review (id, review_id, file, comment, status, time) VALUES (?, ?, ?, ?, ?, ?)')
|
|
166
|
+
.run(randomUUID(), reviewId, entry.file, entry.comment, entry.status, now)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function dependencyReviewMethods(db: ManagedSql): DependencyReviewStore {
|
|
171
|
+
const read = (id: string) => db.prepare(`SELECT ${columns} FROM managed_dependency_review WHERE id = ?`).get(id)
|
|
172
|
+
return {
|
|
173
|
+
listOwnDependencyReviews: (user, before) => listOwn(db, user, before),
|
|
174
|
+
async getLatestOwnDependencyReview(user, target) {
|
|
175
|
+
const row = await db.prepare(`SELECT ${columns} FROM managed_dependency_review
|
|
176
|
+
WHERE user_id = ? AND ecosystem = ? AND package = ? AND version = ? AND integrity = ? ORDER BY time DESC, id DESC LIMIT 1`)
|
|
177
|
+
.get(user, target.ecosystem, target.package, target.version, target.integrity)
|
|
178
|
+
return row ? (await expandReviews(db, [row], user))[0]! : null
|
|
179
|
+
},
|
|
180
|
+
async discardDependencyReview(sessionId, reviewId, revision) {
|
|
181
|
+
const user = await actor(db, sessionId)
|
|
182
|
+
const { changes } = await db.prepare(`DELETE FROM managed_dependency_review
|
|
183
|
+
WHERE id = ? AND user_id = ? AND revision = ? AND published_at IS NULL`).run(reviewId, user.id, revision)
|
|
184
|
+
if (Number(changes) !== 1) conflict()
|
|
185
|
+
},
|
|
186
|
+
listDependencyReviewEnabledTeams: async () => (await db.prepare('SELECT id FROM managed_team WHERE dependency_review = 1 ORDER BY id').all() as { id: string }[]).map(row => row.id),
|
|
187
|
+
async setTeamDependencyReview(sessionId, teamId, enabled) {
|
|
188
|
+
await actor(db, sessionId, true)
|
|
189
|
+
if (!await db.prepare('SELECT id FROM managed_team WHERE id = ?').get(teamId)) return false
|
|
190
|
+
await db.prepare('UPDATE managed_team SET dependency_review = ? WHERE id = ?').run(enabled ? 1 : 0, teamId)
|
|
191
|
+
return true
|
|
192
|
+
},
|
|
193
|
+
listDependencyReviewTeams: user => teams(db, user),
|
|
194
|
+
async listDependencyReviews(context, user, allBases = false) {
|
|
195
|
+
const rows = await db.prepare(`SELECT ${columns} FROM managed_dependency_review r WHERE ${allBases ? targetWhere : contextWhere}
|
|
196
|
+
AND (user_id = ? OR (published_at IS NOT NULL AND EXISTS (
|
|
197
|
+
SELECT 1 FROM managed_dependency_review_team rt JOIN managed_team t ON t.id = rt.team_id AND t.hidden = 0
|
|
198
|
+
JOIN managed_team_user m ON m.team_id = rt.team_id WHERE rt.review_id = r.id AND m.user_id = ?)))
|
|
199
|
+
ORDER BY time, id`).all(...values(context).slice(0, allBases ? 4 : 7), user, user)
|
|
200
|
+
return expandReviews(db, rows, user)
|
|
201
|
+
},
|
|
202
|
+
async changeDependencyReview(context, sessionId, change, now) {
|
|
203
|
+
const user = await actor(db, sessionId)
|
|
204
|
+
let id = change.reviewId
|
|
205
|
+
let current: DependencyReview | undefined
|
|
206
|
+
if (id) {
|
|
207
|
+
current = await db.prepare(`SELECT ${columns} FROM managed_dependency_review WHERE id = ? AND user_id = ? AND ${contextWhere}`)
|
|
208
|
+
.get(id, user.id, ...values(context)) as DependencyReview | undefined
|
|
209
|
+
if (!current || current.revision !== change.revision) return conflict()
|
|
210
|
+
}
|
|
211
|
+
const unpublished = () => db.prepare(`SELECT id FROM managed_dependency_review WHERE user_id = ? AND ${contextWhere} AND published_at IS NULL`)
|
|
212
|
+
.get(user.id, ...values(context)) as Promise<{ id: string } | undefined>
|
|
213
|
+
const create = async () => {
|
|
214
|
+
if (await unpublished()) return conflict()
|
|
215
|
+
const next = randomUUID()
|
|
216
|
+
await db.prepare(`INSERT INTO managed_dependency_review
|
|
217
|
+
(id, user_id, user_login, ecosystem, package, version, integrity, base_package, base_version, base_integrity, time, revision)
|
|
218
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`).run(next, user.id, user.login, ...values(context), now)
|
|
219
|
+
return next
|
|
220
|
+
}
|
|
221
|
+
if (change.action === 'start') {
|
|
222
|
+
id = await create()
|
|
223
|
+
} else if (change.action === 'continue') {
|
|
224
|
+
if (!current?.publishedAt) return conflict()
|
|
225
|
+
id = await create()
|
|
226
|
+
await db.prepare('UPDATE managed_dependency_review SET comment = ? WHERE id = ?').run(current.comment, id)
|
|
227
|
+
const entries = await db.prepare('SELECT id, file, comment, status, time FROM managed_dependency_file_review WHERE review_id = ? ORDER BY time, id').all(current.id) as ReviewEntry[]
|
|
228
|
+
for (const entry of fileEntries(entries)) {
|
|
229
|
+
await db.prepare(`INSERT INTO managed_dependency_file_review
|
|
230
|
+
(id, review_id, file, comment, status, time) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
231
|
+
.run(randomUUID(), id, entry.file, entry.comment, entry.status, entry.time)
|
|
232
|
+
}
|
|
233
|
+
} else if (change.action === 'revoke') {
|
|
234
|
+
if (!current || current.publishedAt === null || current.revokedAt !== null) return conflict()
|
|
235
|
+
await db.prepare('UPDATE managed_dependency_review SET revoked_at = ?, revision = revision + 1 WHERE id = ?').run(now, id)
|
|
236
|
+
} else if (change.action === 'publish') {
|
|
237
|
+
await publish(db, current, user.id, change, now)
|
|
238
|
+
} else {
|
|
239
|
+
if (current?.publishedAt != null) return conflict()
|
|
240
|
+
if (change.entry.status === null && !change.entryId) throw new ManagedMutationError(400, 'bad-review')
|
|
241
|
+
if (id) await db.prepare('UPDATE managed_dependency_review SET time = ?, revision = revision + 1 WHERE id = ?').run(now, id)
|
|
242
|
+
else { if (change.entryId) return conflict(); id = await create() }
|
|
243
|
+
await saveFile(db, id, change.entry, change.entryId, now)
|
|
244
|
+
}
|
|
245
|
+
return (await expandReviews(db, [await read(id!)], user.id))[0]!
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|