@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.41

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.
Files changed (312) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +2 -0
  6. package/client/dependency-paths.js +24 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +112 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +522 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-metadata.d.ts +14 -0
  17. package/common/bundle-metadata.js +226 -0
  18. package/common/bundle-pkg-of.d.ts +1 -0
  19. package/common/bundle-pkg-of.js +90 -0
  20. package/common/bundle-reasons.d.ts +2 -0
  21. package/common/bundle-reasons.js +21 -0
  22. package/common/bundle-source-links.d.ts +6 -0
  23. package/common/bundle-source-links.js +216 -0
  24. package/common/bundle-sourcemap.d.ts +10 -0
  25. package/common/bundle-sourcemap.js +100 -0
  26. package/common/bundle-sources.d.ts +7 -0
  27. package/common/bundle-sources.js +310 -0
  28. package/common/bundle-stats.js +41 -0
  29. package/common/bundle-tabs.js +1 -0
  30. package/common/code-language.js +65 -0
  31. package/common/default-scan-models.ts +31 -0
  32. package/common/dependency-reviews.d.ts +23 -0
  33. package/common/dependency-reviews.js +81 -0
  34. package/common/finding-duplicates.js +58 -0
  35. package/common/finding-filters.js +274 -0
  36. package/common/finding-id.js +47 -0
  37. package/common/github-issue-labels.d.ts +1 -0
  38. package/common/github-issue-labels.js +11 -0
  39. package/common/github-pr.ts +56 -0
  40. package/common/gzip.js +59 -0
  41. package/common/line-diff.d.ts +8 -0
  42. package/common/line-diff.js +56 -0
  43. package/common/linked-workspace-groups.js +63 -0
  44. package/common/managed/comments.ts +34 -0
  45. package/common/managed/npm-packages.d.ts +5 -0
  46. package/common/managed/npm-packages.js +34 -0
  47. package/common/managed/permissions.ts +31 -0
  48. package/common/managed/report-content.ts +72 -0
  49. package/common/managed/report-filter.ts +108 -0
  50. package/common/managed/repository-alias.ts +65 -0
  51. package/common/managed/roles.ts +28 -0
  52. package/common/managed/routes.d.ts +2 -0
  53. package/common/managed/routes.js +254 -0
  54. package/common/managed/scan-models.ts +6 -0
  55. package/common/managed/team-app.d.ts +2 -0
  56. package/common/managed/team-app.js +31 -0
  57. package/common/managed/triage.ts +83 -0
  58. package/common/managed/upload-seal.ts +126 -0
  59. package/common/pretty-print.d.ts +5 -0
  60. package/common/pretty-print.js +24 -0
  61. package/common/revalidation-conflicts.js +48 -0
  62. package/common/revalidation-input-groups.js +56 -0
  63. package/common/save-error-reason.ts +20 -7
  64. package/common/scan-server.ts +13 -0
  65. package/common/server-info.ts +35 -0
  66. package/common/utf8.d.ts +3 -0
  67. package/common/utf8.js +45 -0
  68. package/common/workspace-app.js +23 -0
  69. package/common/workspace-groups.js +176 -0
  70. package/out/brotli-fallback.js +3 -3
  71. package/out/chunk-2AFQKRAM.js +51 -0
  72. package/out/chunk-2KBF5TZ7.js +2 -0
  73. package/out/chunk-2XUEZHIP.js +28 -0
  74. package/out/chunk-35VE6CDH.js +2 -0
  75. package/out/chunk-3BZ7JERB.js +1 -0
  76. package/out/chunk-4F6JSF57.js +3 -0
  77. package/out/chunk-4XCDFGDP.js +10 -0
  78. package/out/chunk-C4Z6ZBXV.js +10 -0
  79. package/out/chunk-CMQA4DM6.js +1 -0
  80. package/out/chunk-CVVJAXVY.js +2 -0
  81. package/out/chunk-DIJJNTRH.js +10 -0
  82. package/out/chunk-GJ4IUCNS.js +1 -0
  83. package/out/chunk-GNXOKIGP.js +80 -0
  84. package/out/chunk-GZ4MS3TE.js +13 -0
  85. package/out/chunk-IU3CCINR.js +3 -0
  86. package/out/chunk-K3X5EJBQ.js +2 -0
  87. package/out/chunk-KEX2ATEW.js +2 -0
  88. package/out/chunk-KVEUXIEU.js +1 -0
  89. package/out/chunk-KYCNJVB7.js +1 -0
  90. package/out/chunk-NRPC74AM.js +1 -0
  91. package/out/chunk-OOJM4CTU.js +1 -0
  92. package/out/chunk-PNHBZGDD.js +1 -0
  93. package/out/chunk-QZ3A376W.js +1 -0
  94. package/out/chunk-R5O6FAW4.js +2 -0
  95. package/out/chunk-RFZQQXSG.js +17 -0
  96. package/out/chunk-RSHMVYNS.js +1 -0
  97. package/out/chunk-TUII34K2.js +1 -0
  98. package/out/chunk-TY4RZD3Z.js +226 -0
  99. package/out/chunk-TZ5KB2Y5.js +10 -0
  100. package/out/chunk-U6OPEXIA.js +1 -0
  101. package/out/chunk-UNTRGE26.js +2 -0
  102. package/out/chunk-UXNUAIIC.js +33 -0
  103. package/out/chunk-UYAXVRVL.js +1 -0
  104. package/out/chunk-UZAZM2SX.js +2 -0
  105. package/out/chunk-V3ME6FI5.js +2 -0
  106. package/out/chunk-V5O7M2YT.js +1 -0
  107. package/out/chunk-VIHGO77Y.js +1 -0
  108. package/out/chunk-VQJCXGIY.js +7 -0
  109. package/out/chunk-W4J3MGLB.js +1 -0
  110. package/out/chunk-WGG7KQUH.js +1 -0
  111. package/out/chunk-WZDTZE64.js +1 -0
  112. package/out/chunk-XL35ZV6R.js +1 -0
  113. package/out/chunk-XYFXXB2G.js +10 -0
  114. package/out/chunk-YVZH5SHP.js +1 -0
  115. package/out/chunk-ZBBOBQ43.js +1 -0
  116. package/out/chunk-ZMQQCGF6.js +2 -0
  117. package/out/client-managed-import.js +4 -0
  118. package/out/client-managed.js +37 -0
  119. package/out/client-sync.js +10 -13
  120. package/out/github-issue-authorized.html +3 -0
  121. package/out/github-issue-authorized.js +1 -0
  122. package/out/graph.js +17 -4
  123. package/out/index.html +90 -8
  124. package/out/prism.js +6 -2
  125. package/out/stasis.svg +20 -0
  126. package/out/terminal.js +365 -39
  127. package/out/view.css +1 -1
  128. package/out/view.js +83 -117
  129. package/package.json +298 -59
  130. package/risk/files/characters.js +196 -0
  131. package/risk/files/hosts.js +205 -0
  132. package/risk/files/readability.js +171 -0
  133. package/risk/files/runs-code.js +595 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +156 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +223 -0
  139. package/risk/npm/combined.js +65 -0
  140. package/risk/npm/compare.js +228 -0
  141. package/risk/npm/dependencies.js +153 -0
  142. package/risk/npm/diff.js +43 -0
  143. package/risk/npm/downloads.js +84 -0
  144. package/risk/npm/files.js +34 -0
  145. package/risk/npm/held.js +60 -0
  146. package/risk/npm/install-scripts.js +34 -0
  147. package/risk/npm/malware.js +36 -0
  148. package/risk/npm/manifest.js +65 -0
  149. package/risk/npm/new-dependency.js +30 -0
  150. package/risk/npm/old-version.js +108 -0
  151. package/risk/npm/registry.js +282 -0
  152. package/risk/npm/release.js +85 -0
  153. package/risk/npm/score.js +59 -0
  154. package/risk/npm/specs.js +124 -0
  155. package/risk/npm/tarball.js +113 -0
  156. package/server-common/database-config.ts +16 -0
  157. package/server-common/initialize.ts +18 -0
  158. package/server-common/npm-advisories.ts +101 -0
  159. package/{server → server-common}/origin.ts +5 -5
  160. package/server-common/reap.ts +48 -0
  161. package/server-common/scan-config.ts +19 -0
  162. package/server-common/standalone.ts +29 -0
  163. package/server-common/storage-crypto.ts +182 -0
  164. package/server-common/storage-log.ts +38 -0
  165. package/server-common/vercel-blob.ts +103 -0
  166. package/server-e2e/app.ts +486 -0
  167. package/{server → server-e2e}/auth.ts +5 -1
  168. package/{server → server-e2e}/bus-receiver.ts +2 -1
  169. package/{server → server-e2e}/cli.js +9 -4
  170. package/{server → server-e2e}/config.ts +56 -35
  171. package/{server → server-e2e}/db-neon.ts +2 -2
  172. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  173. package/{server → server-e2e}/db-stmt.ts +2 -2
  174. package/{server → server-e2e}/db.ts +7 -7
  175. package/{server → server-e2e}/http.ts +116 -18
  176. package/{server → server-e2e}/hub.ts +40 -9
  177. package/server-e2e/index.ts +17 -0
  178. package/server-e2e/lifecycle.ts +95 -0
  179. package/{server → server-e2e}/neon-driver.ts +2 -2
  180. package/{server → server-e2e}/npm-proxy.ts +12 -145
  181. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  182. package/{server → server-e2e}/objstore/blob.ts +1 -1
  183. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  184. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  185. package/{server → server-e2e}/objstore/init.ts +35 -18
  186. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  187. package/server-e2e/objstore/rest-deny.ts +28 -0
  188. package/server-e2e/objstore/rest-mint.ts +224 -0
  189. package/{server → server-e2e}/objstore/rest.ts +27 -26
  190. package/{server → server-e2e}/objstore/sign.ts +105 -0
  191. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  192. package/{server → server-e2e}/objstore/store.ts +7 -7
  193. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  194. package/{server → server-e2e}/pubsub.ts +12 -9
  195. package/{server → server-e2e}/sign.ts +4 -4
  196. package/{server → server-e2e}/sse-server.ts +102 -68
  197. package/{server → server-e2e}/sse-session.ts +28 -8
  198. package/{server → server-e2e}/static.ts +31 -21
  199. package/server-e2e/sync-handlers.ts +408 -0
  200. package/{server → server-e2e}/ws-server.ts +20 -9
  201. package/server-managed/activity.ts +245 -0
  202. package/server-managed/avatar-store.ts +12 -0
  203. package/server-managed/blob-store.ts +17 -0
  204. package/server-managed/brotli.ts +10 -0
  205. package/server-managed/bundle-advisories.ts +120 -0
  206. package/server-managed/bundle-advisory-inventory.ts +77 -0
  207. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  208. package/server-managed/bundle-build-diagnostics.js +23 -0
  209. package/server-managed/bundle-build-leases.ts +41 -0
  210. package/server-managed/bundle-build-worker.js +111 -0
  211. package/server-managed/bundle-build.ts +198 -0
  212. package/server-managed/bundle-cache.ts +199 -0
  213. package/server-managed/bundle-catalog.ts +29 -0
  214. package/server-managed/bundle-commits.ts +140 -0
  215. package/server-managed/bundle-store.ts +24 -0
  216. package/server-managed/bundle-summary-cache.ts +130 -0
  217. package/server-managed/bundle.ts +120 -0
  218. package/server-managed/cache-storage.ts +19 -0
  219. package/server-managed/cli.js +21 -0
  220. package/server-managed/combined.ts +46 -0
  221. package/server-managed/comments.ts +151 -0
  222. package/server-managed/config.ts +158 -0
  223. package/server-managed/content-access.ts +15 -0
  224. package/server-managed/crypto.ts +25 -0
  225. package/server-managed/db-methods.ts +1725 -0
  226. package/server-managed/db-neon.ts +367 -0
  227. package/server-managed/db-schema.ts +227 -0
  228. package/server-managed/db-table-names.ts +22 -0
  229. package/server-managed/db.ts +132 -0
  230. package/server-managed/dependency-reviews.ts +248 -0
  231. package/server-managed/github-app.ts +391 -0
  232. package/server-managed/github-errors.ts +68 -0
  233. package/server-managed/github-issue-links.ts +127 -0
  234. package/server-managed/github-issue-oauth.ts +47 -0
  235. package/server-managed/github-issues.ts +152 -0
  236. package/server-managed/github-metadata.ts +213 -0
  237. package/server-managed/github-oauth.ts +235 -0
  238. package/server-managed/github-pulls.ts +227 -0
  239. package/server-managed/http-response.ts +50 -0
  240. package/server-managed/http.ts +3472 -0
  241. package/server-managed/import-triage.ts +51 -0
  242. package/server-managed/index.ts +187 -0
  243. package/server-managed/kept.ts +28 -0
  244. package/server-managed/link-reports.ts +158 -0
  245. package/server-managed/locked-packages.ts +85 -0
  246. package/server-managed/lru.ts +30 -0
  247. package/server-managed/managed-issues.ts +75 -0
  248. package/server-managed/management-catalog.ts +45 -0
  249. package/server-managed/management.ts +99 -0
  250. package/server-managed/npm-insights.ts +440 -0
  251. package/server-managed/npm-kept.ts +65 -0
  252. package/server-managed/npm-loads.ts +167 -0
  253. package/server-managed/npm-manifest.ts +115 -0
  254. package/server-managed/npm-packages.ts +378 -0
  255. package/server-managed/npm-provenance.ts +123 -0
  256. package/server-managed/npm-risk.ts +177 -0
  257. package/server-managed/object-storage-disk.ts +148 -0
  258. package/server-managed/object-storage-vercel.ts +67 -0
  259. package/server-managed/object-storage.ts +74 -0
  260. package/server-managed/package-entry-points.ts +51 -0
  261. package/server-managed/pretty-print.ts +352 -0
  262. package/server-managed/public-workspace.ts +220 -0
  263. package/server-managed/rename-diff-worker.js +26 -0
  264. package/server-managed/rename-diff.ts +270 -0
  265. package/server-managed/repo-path.ts +21 -0
  266. package/server-managed/report-migration.ts +35 -0
  267. package/server-managed/report-query.ts +10 -0
  268. package/server-managed/report-response.ts +16 -0
  269. package/server-managed/report-sources.ts +178 -0
  270. package/server-managed/report-store.ts +47 -0
  271. package/server-managed/repository-aliases.ts +91 -0
  272. package/server-managed/repository-browser.ts +153 -0
  273. package/server-managed/repository-discovery.ts +82 -0
  274. package/server-managed/repository-policy.ts +27 -0
  275. package/server-managed/repository-risk.ts +420 -0
  276. package/server-managed/revisions.ts +35 -0
  277. package/server-managed/session.ts +92 -0
  278. package/server-managed/slugs.ts +38 -0
  279. package/server-managed/solidity-entry-points.ts +69 -0
  280. package/server-managed/sql-postgres.ts +31 -0
  281. package/server-managed/sql.ts +76 -0
  282. package/server-managed/static.ts +28 -0
  283. package/server-managed/storage-command.ts +16 -0
  284. package/server-managed/storage-db.ts +175 -0
  285. package/server-managed/storage-encryption.ts +279 -0
  286. package/server-managed/storage-maintenance.ts +256 -0
  287. package/server-managed/storage-payload.ts +103 -0
  288. package/server-managed/storage-stores.ts +85 -0
  289. package/server-managed/storage.ts +78 -0
  290. package/server-managed/team-app.ts +94 -0
  291. package/server-managed/team-catalog.ts +7 -0
  292. package/server-managed/team-feed.ts +173 -0
  293. package/server-managed/team-npm-scopes.ts +77 -0
  294. package/server-managed/team-reports.ts +228 -0
  295. package/server-managed/triage-response.ts +16 -0
  296. package/server-managed/type-only-worker.js +19 -0
  297. package/server-managed/type-only.ts +103 -0
  298. package/server-managed/uploads.ts +86 -0
  299. package/server-managed/upstream-cache.ts +215 -0
  300. package/server-managed/view-as.ts +58 -0
  301. package/server-managed/workspace-shares.ts +166 -0
  302. package/server.ts +50 -0
  303. package/server/index.ts +0 -477
  304. package/server/lifecycle.ts +0 -204
  305. package/server/sync-handlers.ts +0 -320
  306. /package/{LICENSE → LICENSE-MIT} +0 -0
  307. /package/{server → server-e2e}/config.example.json +0 -0
  308. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  309. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  310. /package/{server → server-e2e}/peer.ts +0 -0
  311. /package/{server → server-e2e}/util.ts +0 -0
  312. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,367 @@
1
+ import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
2
+ import { LINK_REPORT_SCHEMA } from './link-reports.ts'
3
+ import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
4
+ import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
5
+ import { AsyncLocalStorage } from 'node:async_hooks'
6
+ import { WebSocket } from 'ws'
7
+ import { type ManagedDb, type ManagedDbOptions, createManagedMethods } from './db-methods.ts'
8
+ import { revisionSchema } from './revisions.ts'
9
+ import { MANAGED_SCHEMA, VIEW_SESSION_INDEX } from './db-schema.ts'
10
+ import { STORAGE_SCHEMA } from './storage-db.ts'
11
+ import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
12
+ import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
13
+ import { BUNDLE_BUILD_LEASE_SCHEMA } from './bundle-build-leases.ts'
14
+ import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
15
+ import { COMMENT_SCHEMA } from './comments.ts'
16
+ import { ACTIVITY_SCHEMA, uploadAction } from './activity.ts'
17
+ import { type ManagedSqlDriver, scopeManagedMethods } from './sql.ts'
18
+ import { postgresSchema, postgresSql } from './sql-postgres.ts'
19
+ import { managedTableRenames } from './db-table-names.ts'
20
+ import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
21
+ import { allocateMissingSlugs } from './slugs.ts'
22
+
23
+ export interface PgConnection {
24
+ query(sql: string, params?: unknown[]): Promise<{
25
+ rows: Record<string, unknown>[]; rowCount?: number | null
26
+ fields?: { name: string; dataTypeID: number }[]
27
+ }>
28
+ release(): Promise<void>
29
+ }
30
+ export type PgConnect = () => Promise<PgConnection>
31
+ // Content/access mutations cooperate on this transaction-scoped lock. Reads,
32
+ // presence updates, and maintenance leases do not take it. Also fences migrations.
33
+ const LOCK = 'SELECT pg_advisory_xact_lock(1937006964, 1835101793)'
34
+
35
+ async function currentSchema(db: PgConnection): Promise<boolean> {
36
+ const exists = (await db.query("SELECT to_regclass('managed_schema_version') AS name")).rows[0]?.['name']
37
+ if (!exists) return false
38
+ const versions = new Set((await db.query('SELECT version FROM managed_schema_version')).rows.map(row => Number(row['version'])))
39
+ return Array.from({ length: 27 }, (_, i) => i + 1).every(version => versions.has(version))
40
+ }
41
+
42
+ async function migrateRepositoryDefaultCache(db: PgConnection): Promise<void> {
43
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 12')).rows.length > 0) return
44
+ await db.query('ALTER TABLE managed_selected_repo ADD COLUMN IF NOT EXISTS cached_default_branch TEXT')
45
+ await db.query('INSERT INTO managed_schema_version VALUES (12)')
46
+ }
47
+
48
+ async function migrateBundleBuildLeases(db: PgConnection): Promise<void> {
49
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 13')).rows.length > 0) return
50
+ await db.query(postgresSchema(BUNDLE_BUILD_LEASE_SCHEMA))
51
+ await db.query('INSERT INTO managed_schema_version VALUES (13)')
52
+ }
53
+
54
+ async function migrateGithubMetadata(db: PgConnection): Promise<void> {
55
+ await db.query(postgresSchema(GITHUB_METADATA_SCHEMA + MANAGED_ISSUE_SCHEMA))
56
+ await db.query(`ALTER TABLE managed_github_metadata ADD COLUMN IF NOT EXISTS state_reason ${GITHUB_STATE_REASON_COLUMN}`)
57
+ await db.query('ALTER TABLE managed_github_metadata ADD COLUMN IF NOT EXISTS attempted_at BIGINT')
58
+ await db.query('INSERT INTO managed_schema_version VALUES (14) ON CONFLICT DO NOTHING')
59
+ }
60
+
61
+ async function migrateBundleVisibility(db: PgConnection): Promise<void> {
62
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 15')).rows.length === 0) {
63
+ await db.query('ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS visible INTEGER NOT NULL DEFAULT 1')
64
+ await db.query('INSERT INTO managed_schema_version VALUES (15)')
65
+ }
66
+ }
67
+
68
+ async function migrateLinkReports(db: PgConnection): Promise<void> {
69
+ await db.query(postgresSchema(revisionSchema(true)))
70
+ await db.query('INSERT INTO managed_schema_version VALUES (16) ON CONFLICT DO NOTHING')
71
+ }
72
+
73
+ async function migrateRepositoryAliases(db: PgConnection): Promise<void> {
74
+ await db.query(postgresSchema(REPOSITORY_ALIAS_SCHEMA))
75
+ await db.query('INSERT INTO managed_schema_version VALUES (17) ON CONFLICT DO NOTHING')
76
+ }
77
+
78
+ async function migrateHiddenTeams(db: PgConnection): Promise<void> {
79
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 18')).rows.length > 0) return
80
+ await db.query('ALTER TABLE managed_team ADD COLUMN IF NOT EXISTS hidden INTEGER NOT NULL DEFAULT 0')
81
+ await db.query('INSERT INTO managed_schema_version VALUES (18)')
82
+ }
83
+
84
+ async function migrateIssueFixes(db: PgConnection): Promise<void> {
85
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 19')).rows.length > 0) return
86
+ await db.query('ALTER TABLE managed_finding_issue ADD COLUMN IF NOT EXISTS auto_fix_url TEXT, ADD COLUMN IF NOT EXISTS auto_fix_checked_at BIGINT')
87
+ await db.query(postgresSchema(revisionSchema(true)))
88
+ await db.query('INSERT INTO managed_schema_version VALUES (19)')
89
+ }
90
+
91
+ // Existing rows stay NULL (unknown): some may have been built on the server.
92
+ async function migrateBundleProvenance(db: PgConnection): Promise<void> {
93
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 20')).rows.length > 0) return
94
+ await db.query('ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS provenance TEXT')
95
+ await createUploadTrigger(db, 'bundle', false)
96
+ await db.query('INSERT INTO managed_schema_version VALUES (20)')
97
+ }
98
+
99
+ async function migrateUpstreamCache(db: PgConnection): Promise<void> {
100
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 21')).rows.length > 0) return
101
+ await db.query(postgresSchema(UPSTREAM_CACHE_SCHEMA))
102
+ await db.query('INSERT INTO managed_schema_version VALUES (21)')
103
+ }
104
+
105
+ // Existing builds stay NULL: their conditions were never recorded.
106
+ async function migrateBundleBuildConditions(db: PgConnection): Promise<void> {
107
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 22')).rows.length > 0) return
108
+ await db.query('ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS build_conditions TEXT')
109
+ await db.query('INSERT INTO managed_schema_version VALUES (22)')
110
+ }
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
+
145
+ async function initialize(db: PgConnection): Promise<void> {
146
+ if (await currentSchema(db)) return
147
+ await db.query('BEGIN')
148
+ try {
149
+ await db.query(LOCK)
150
+ await db.query('CREATE TABLE IF NOT EXISTS managed_schema_version (version INTEGER PRIMARY KEY)')
151
+ const initialized = (await db.query('SELECT version FROM managed_schema_version WHERE version = 1')).rows.length > 0
152
+ const prefixed = (await db.query('SELECT version FROM managed_schema_version WHERE version = 3')).rows.length > 0
153
+ if (initialized && !prefixed) {
154
+ const { rows } = await db.query("SELECT relname AS name FROM pg_class WHERE relkind IN ('r', 'p') AND pg_table_is_visible(oid)")
155
+ for (const [from, to] of managedTableRenames(new Set(rows.map(row => String(row['name']))))) {
156
+ await db.query(`ALTER TABLE ${from} RENAME TO ${to}`)
157
+ }
158
+ // PL/pgSQL function bodies retain literal table names after ALTER TABLE.
159
+ for (const type of ['report', 'bundle']) await createUploadTrigger(db, type, false)
160
+ }
161
+ if (!initialized) {
162
+ await db.query(postgresSchema(MANAGED_SCHEMA + COMMENT_SCHEMA + ACTIVITY_SCHEMA))
163
+ await db.query(`ALTER TABLE managed_finding_triage_event ADD COLUMN report_id TEXT, ADD COLUMN report TEXT, ADD COLUMN repo TEXT;
164
+ CREATE UNIQUE INDEX managed_team_slug_idx ON managed_team(slug);
165
+ CREATE UNIQUE INDEX managed_report_slug_idx ON managed_report(slug);
166
+ CREATE INDEX managed_activity_actor_at_idx ON managed_activity(actor_id, at);`)
167
+ for (const type of ['report', 'bundle']) await createUploadTrigger(db, type)
168
+ await db.query('INSERT INTO managed_schema_version VALUES (1)')
169
+ }
170
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 2')).rows.length === 0) {
171
+ await db.query('CREATE INDEX IF NOT EXISTS managed_report_bundle_hash_idx ON managed_report(bundle_id, sha256)')
172
+ await db.query('INSERT INTO managed_schema_version VALUES (2)')
173
+ }
174
+ if (!prefixed) await db.query('INSERT INTO managed_schema_version VALUES (3)')
175
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 4')).rows.length === 0) {
176
+ await db.query(postgresSchema(WORKSPACE_SHARE_SCHEMA))
177
+ await db.query('INSERT INTO managed_schema_version VALUES (4)')
178
+ }
179
+ await migrateGithubMetadata(db)
180
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 5')).rows.length === 0) {
181
+ await db.query(`ALTER TABLE managed_workspace_share ADD COLUMN IF NOT EXISTS dependencies INTEGER NOT NULL DEFAULT 0,
182
+ ADD COLUMN IF NOT EXISTS security INTEGER NOT NULL DEFAULT 0`)
183
+ await db.query('INSERT INTO managed_schema_version VALUES (5)')
184
+ }
185
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 6')).rows.length === 0) {
186
+ await db.query('ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS slug TEXT')
187
+ const { rows } = await db.query('SELECT id, slug FROM managed_bundle ORDER BY id')
188
+ for (const row of allocateMissingSlugs(rows as { id: string; slug: string | null }[])) {
189
+ await db.query('UPDATE managed_bundle SET slug = $1 WHERE id = $2', [row.slug, row.id])
190
+ }
191
+ await db.query(`ALTER TABLE managed_bundle ALTER COLUMN slug SET NOT NULL;
192
+ CREATE UNIQUE INDEX IF NOT EXISTS managed_bundle_slug_idx ON managed_bundle(slug);
193
+ INSERT INTO managed_schema_version VALUES (6)`)
194
+ }
195
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 7')).rows.length === 0) {
196
+ await db.query("ALTER TABLE managed_bundle ADD COLUMN IF NOT EXISTS repo_directory TEXT NOT NULL DEFAULT ''")
197
+ await db.query('INSERT INTO managed_schema_version VALUES (7)')
198
+ }
199
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 8')).rows.length === 0) {
200
+ await db.query('CREATE INDEX IF NOT EXISTS managed_report_hash_idx ON managed_report(sha256, uploaded_at, id)')
201
+ await db.query('INSERT INTO managed_schema_version VALUES (8)')
202
+ }
203
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 9')).rows.length === 0) {
204
+ await db.query(postgresSchema(STORAGE_SCHEMA))
205
+ for (const table of ['managed_report', 'managed_bundle']) {
206
+ await db.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS data_key TEXT, ADD COLUMN IF NOT EXISTS storage_encrypted INTEGER NOT NULL DEFAULT 0`)
207
+ }
208
+ await db.query('ALTER TABLE managed_user ADD COLUMN IF NOT EXISTS gh_tokens_encrypted INTEGER NOT NULL DEFAULT 0')
209
+ await db.query('INSERT INTO managed_schema_version VALUES (9)')
210
+ }
211
+ await db.query(postgresSchema(LINK_REPORT_SCHEMA))
212
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 10')).rows.length === 0) {
213
+ await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (10)')
214
+ }
215
+ if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 11')).rows.length === 0) {
216
+ await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (11)')
217
+ }
218
+ for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews]) await migrate(db)
219
+ await db.query('COMMIT')
220
+ } catch (err) {
221
+ await db.query('ROLLBACK')
222
+ throw err
223
+ }
224
+ }
225
+
226
+ async function createUploadTrigger(db: PgConnection, type: string, createTrigger = true): Promise<void> {
227
+ await db.query(`CREATE OR REPLACE FUNCTION managed_${type}_activity_insert() RETURNS trigger LANGUAGE plpgsql AS $$
228
+ BEGIN
229
+ INSERT INTO managed_activity (id, kind, actor, action, repo, report_id, report, at, bundle_id, actor_id)
230
+ VALUES ('${type}-upload:' || NEW.id, 'upload',
231
+ COALESCE(NEW.uploaded_by_login, (SELECT login FROM managed_user WHERE id = NEW.uploaded_by)),
232
+ ${uploadAction(type, 'NEW')}, (SELECT full_name FROM managed_selected_repo WHERE repo_id = NEW.repo_id),
233
+ ${type === 'report' ? 'NEW.id' : 'NULL'}, NEW.filename, NEW.uploaded_at,
234
+ ${type === 'bundle' ? 'NEW.id' : 'NULL'}, NEW.uploaded_by);
235
+ RETURN NEW;
236
+ END $$;`)
237
+ if (createTrigger) {
238
+ await db.query(`CREATE TRIGGER managed_${type}_activity AFTER INSERT ON managed_${type}
239
+ FOR EACH ROW EXECUTE FUNCTION managed_${type}_activity_insert();`)
240
+ }
241
+ }
242
+
243
+ function requestConnections(connect: PgConnect) {
244
+ const requests = new AsyncLocalStorage<{ connection?: PgConnection; queue: Promise<unknown>; ended: boolean }>()
245
+ async function withConnection<T>(work: (db: PgConnection) => Promise<T>): Promise<T> {
246
+ const request = requests.getStore()
247
+ if (!request || request.ended) {
248
+ const connection = await connect()
249
+ let result: T
250
+ try { result = await work(connection) }
251
+ catch (error) { await connection.release().catch(() => {}); throw error }
252
+ await connection.release()
253
+ return result
254
+ }
255
+ // Serialize transactions on the leased connection, never interleave BEGINs
256
+ // from concurrent Blob/catalog tasks. Remote Blob reads remain concurrent.
257
+ const result = request.queue.then(async () => {
258
+ const connection = request.connection ??= await connect()
259
+ try { return await work(connection) }
260
+ catch (error) {
261
+ delete request.connection
262
+ await connection.release().catch(() => {})
263
+ throw error
264
+ }
265
+ })
266
+ request.queue = result.catch(() => {})
267
+ return result
268
+ }
269
+ async function withRequest<T>(work: () => Promise<T>): Promise<T> {
270
+ if (requests.getStore()) return work()
271
+ const request: { connection?: PgConnection; queue: Promise<unknown>; ended: boolean } = { queue: Promise.resolve(), ended: false }
272
+ let failed = false
273
+ try { return await requests.run(request, work) }
274
+ catch (error) { failed = true; throw error }
275
+ finally {
276
+ request.ended = true
277
+ await request.queue
278
+ if (failed) await request.connection?.release().catch(() => {})
279
+ else await request.connection?.release()
280
+ }
281
+ }
282
+ return { withConnection, withRequest }
283
+ }
284
+
285
+ // Exposed for parity tests against real PostgreSQL semantics via PGlite.
286
+ export async function openPostgresManagedDb(connect: PgConnect, options: ManagedDbOptions = {}, reuseConnections = false): Promise<ManagedDb> {
287
+ const initial = await connect()
288
+ try { await initialize(initial) } finally { await initial.release() }
289
+ const { withConnection, withRequest } = requestConnections(connect)
290
+ const context = new AsyncLocalStorage<PgConnection>()
291
+ let closed = false
292
+ const driver: ManagedSqlDriver = {
293
+ prepare(source) {
294
+ const { sql, names } = postgresSql(source)
295
+ async function query(args: unknown[]) {
296
+ const db = context.getStore()
297
+ if (!db) throw new Error('Managed query outside transaction')
298
+ const values = names.length > 0 ? names.map(name => (args[0] as Record<string, unknown>)[name])
299
+ : args.length === 1 && typeof args[0] === 'object' && args[0] !== null ? [] : args
300
+ const result = await db.query(sql, values)
301
+ // Postgres int8 is returned as text by node-postgres/Neon. All store
302
+ // numbers are JS safe integers (timestamps, IDs, sizes and sequences).
303
+ for (const field of result.fields ?? []) {
304
+ if (field.dataTypeID !== 20) continue
305
+ for (const row of result.rows) {
306
+ if (row[field.name] == null) continue
307
+ const value = Number(row[field.name])
308
+ if (!Number.isSafeInteger(value)) throw new Error('Managed integer exceeds safe range')
309
+ row[field.name] = value
310
+ }
311
+ }
312
+ return result
313
+ }
314
+ return {
315
+ get: async (...args) => (await query(args)).rows[0],
316
+ all: async (...args) => (await query(args)).rows,
317
+ run: async (...args) => ({ changes: (await query(args)).rowCount ?? 0 }),
318
+ }
319
+ },
320
+ async scope(write, work, { lock = write, statement = false } = {}) {
321
+ if (closed) throw new Error('Managed database is closed')
322
+ let committed = false
323
+ try { return await withConnection(async db => {
324
+ if (statement) return context.run(db, work)
325
+ let commitAttempted = false
326
+ try {
327
+ await db.query(write ? 'BEGIN' : 'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY')
328
+ if (lock) await db.query(LOCK)
329
+ const result = await context.run(db, work)
330
+ commitAttempted = true
331
+ await db.query('COMMIT')
332
+ committed = true
333
+ return result
334
+ } catch (err) {
335
+ await db.query('ROLLBACK').catch(() => {})
336
+ if (write && commitAttempted) throw new Error('Managed commit outcome is uncertain', { cause: err })
337
+ throw err
338
+ }
339
+ }) } catch (error) {
340
+ if (write && committed) throw new Error('Managed commit outcome is uncertain', { cause: error })
341
+ throw error
342
+ }
343
+ },
344
+ close() { closed = true },
345
+ }
346
+ const methods = scopeManagedMethods(createManagedMethods(driver, options), driver)
347
+ if (reuseConnections) methods.withRequest = withRequest
348
+ return methods
349
+ }
350
+
351
+ export async function openNeonManagedDb(url: string, options: ManagedDbOptions = {}): Promise<ManagedDb> {
352
+ // Reuse the e2e optional-driver boundary. Requests lease one connection and
353
+ // close it before returning; no socket must survive a frozen invocation.
354
+ const { Client, neonConfig } = await import('../server-e2e/neon-driver.ts') as unknown as {
355
+ Client: new (url: string) => { connect(): Promise<void>; end(): Promise<void>; query: PgConnection['query']; on?(event: 'error', listener: (error: Error) => void): void }
356
+ neonConfig: { webSocketConstructor: typeof WebSocket }
357
+ }
358
+ neonConfig.webSocketConstructor = WebSocket
359
+ return openPostgresManagedDb(async () => {
360
+ const client = new Client(url)
361
+ // A remote disconnect between operations must reject the next query, not
362
+ // become an unhandled EventEmitter error while the request is reading Blob.
363
+ client.on?.('error', () => {})
364
+ try { await client.connect() } catch (err) { await client.end().catch(() => {}); throw err }
365
+ return { query: (sql, params) => client.query(sql, params), release: () => client.end() }
366
+ }, options, true)
367
+ }
@@ -0,0 +1,227 @@
1
+ import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
2
+ import { LINK_REPORT_SCHEMA } from './link-reports.ts'
3
+ import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
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);'
9
+
10
+ export const MANAGED_SCHEMA = `
11
+ CREATE TABLE IF NOT EXISTS managed_user (
12
+ id TEXT PRIMARY KEY,
13
+ github_user_id INTEGER NOT NULL UNIQUE,
14
+ login TEXT NOT NULL,
15
+ name TEXT,
16
+ avatar_url TEXT,
17
+ role TEXT NOT NULL DEFAULT 'none',
18
+ -- GitHub user-to-server token, persisted so the repositories page can list
19
+ -- the user's repos (GET /user/repos) on demand. Refresh token + expiry are
20
+ -- null for non-expiring tokens (GitHub App with expiring tokens disabled).
21
+ gh_access_token TEXT,
22
+ gh_refresh_token TEXT,
23
+ gh_token_expires_at INTEGER,
24
+ created_at INTEGER NOT NULL,
25
+ updated_at INTEGER NOT NULL,
26
+ -- Presence is independent of admin edits and background token refreshes.
27
+ -- NULL for legacy accounts until a known session authenticates them.
28
+ last_seen_at INTEGER
29
+ ) STRICT;
30
+
31
+ CREATE TABLE IF NOT EXISTS managed_session (
32
+ id TEXT PRIMARY KEY,
33
+ user_id TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
34
+ csrf_token TEXT NOT NULL,
35
+ created_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
43
+ ) STRICT;
44
+
45
+ CREATE INDEX IF NOT EXISTS managed_session_user_idx ON managed_session(user_id);
46
+ CREATE INDEX IF NOT EXISTS managed_session_expires_idx ON managed_session(expires_at);
47
+
48
+ -- Repositories selected to operate on. Keyed by GitHub's numeric repo id
49
+ -- (stable across renames). The row carries everything needed to read the repo's
50
+ -- contents server-side later: installation_id mints an App installation token
51
+ -- (Contents: Read) for PRIVATE repos — NULL means a PUBLIC repo readable without
52
+ -- the App — and full_name + default_branch locate the contents. added_by is the
53
+ -- selector, nulled (not cascaded) if that user is removed so the selection
54
+ -- survives.
55
+ CREATE TABLE IF NOT EXISTS managed_selected_repo (
56
+ repo_id INTEGER PRIMARY KEY,
57
+ full_name TEXT NOT NULL,
58
+ is_private INTEGER NOT NULL,
59
+ installation_id INTEGER,
60
+ default_branch TEXT NOT NULL,
61
+ -- Live browser hint; independent of the default discovered at selection.
62
+ cached_default_branch TEXT,
63
+ html_url TEXT NOT NULL,
64
+ added_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
65
+ added_at INTEGER NOT NULL,
66
+ updated_at INTEGER NOT NULL,
67
+ active INTEGER NOT NULL DEFAULT 1
68
+ ) STRICT;
69
+
70
+ CREATE INDEX IF NOT EXISTS selected_repo_full_name_idx ON managed_selected_repo(full_name);
71
+
72
+ -- Bundles uploaded to the server (the "Manage bundles" page). Like reports, the
73
+ -- bytes are stored in the clear (blob-store, keyed by this opaque uuid id) for
74
+ -- the server to operate on. integrity is the content hash (sha512-<base64>,
75
+ -- byte-identical to the client's) so re-uploading the same bytes dedupes (UNIQUE);
76
+ -- it is also the key a report's bundleHashes match to auto-link.
77
+ -- uploaded_by / repo_id are the (nullable) user + repo links, both nulled (not
78
+ -- cascaded) when the referenced user / selected repo goes away.
79
+ CREATE TABLE IF NOT EXISTS managed_bundle (
80
+ id TEXT PRIMARY KEY,
81
+ slug TEXT NOT NULL,
82
+ integrity TEXT NOT NULL UNIQUE,
83
+ filename TEXT NOT NULL,
84
+ kind TEXT,
85
+ byte_size INTEGER NOT NULL,
86
+ uploaded_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
87
+ -- Durable snapshot of the uploader's login at upload time, so "who uploaded
88
+ -- this" survives the uploader being removed (when uploaded_by nulls out).
89
+ uploaded_by_login TEXT,
90
+ repo_id INTEGER REFERENCES managed_selected_repo(repo_id) ON DELETE SET NULL,
91
+ repo_directory TEXT NOT NULL DEFAULT '',
92
+ visible INTEGER NOT NULL DEFAULT 1,
93
+ uploaded_at INTEGER NOT NULL,
94
+ -- 'upload' or 'build' (generated on this server). A server build of bytes
95
+ -- already stored promotes the row to 'build'; NULL predates the column.
96
+ provenance TEXT,
97
+ -- JSON {preset, conditions, platforms} of a JS/TS server build; NULL otherwise.
98
+ build_conditions TEXT
99
+ ) STRICT;
100
+
101
+ CREATE INDEX IF NOT EXISTS managed_bundle_uploaded_at_idx ON managed_bundle(uploaded_at);
102
+
103
+ -- Reports uploaded to the server (the "Manage reports" page). A managed server
104
+ -- is TRUSTED, so the bytes are stored in the clear (blob-store, keyed by this
105
+ -- opaque uuid id) for the server to operate on later; this row carries the
106
+ -- metadata + attribution. uploaded_by / repo_id are the (nullable) user + repo
107
+ -- links, nulled (not cascaded) when the user / selected repo goes away. bundle_id
108
+ -- is the (nullable) auto-resolved link to a stored bundle; bundle_integrity is
109
+ -- the report's declared primary bundle (from its bundleHashes), kept so a later
110
+ -- bundle upload of that integrity can re-link.
111
+ CREATE TABLE IF NOT EXISTS managed_report (
112
+ id TEXT PRIMARY KEY,
113
+ slug TEXT NOT NULL,
114
+ filename TEXT NOT NULL,
115
+ content_type TEXT NOT NULL,
116
+ byte_size INTEGER NOT NULL,
117
+ sha256 TEXT NOT NULL,
118
+ uploaded_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
119
+ -- Durable snapshot of the uploader's login (see managed_bundle).
120
+ uploaded_by_login TEXT,
121
+ repo_id INTEGER REFERENCES managed_selected_repo(repo_id) ON DELETE SET NULL,
122
+ repo_directory TEXT NOT NULL DEFAULT '',
123
+ repo_embedded INTEGER NOT NULL DEFAULT 0,
124
+ analyzer TEXT,
125
+ visible INTEGER NOT NULL DEFAULT 0,
126
+ bundle_id TEXT REFERENCES managed_bundle(id) ON DELETE SET NULL,
127
+ bundle_integrity TEXT,
128
+ uploaded_at INTEGER NOT NULL
129
+ ) STRICT;
130
+
131
+ CREATE INDEX IF NOT EXISTS managed_report_uploaded_at_idx ON managed_report(uploaded_at);
132
+ CREATE INDEX IF NOT EXISTS managed_report_hash_idx ON managed_report(sha256, uploaded_at, id);
133
+ CREATE INDEX IF NOT EXISTS managed_report_bundle_integrity_idx ON managed_report(bundle_integrity);
134
+ CREATE INDEX IF NOT EXISTS managed_report_bundle_hash_idx ON managed_report(bundle_id, sha256);
135
+
136
+ -- Per-finding triage annotations — the managed (trusted-plaintext) counterpart
137
+ -- of the client's localStorage triage map, keyed the same way: by finding id
138
+ -- alone, not by report. Reports mostly repeat one another (a re-scan of the
139
+ -- same code carries the same finding ids) and a finding's triage is shared by
140
+ -- every report that carries it; which ids a viewer may read or write is
141
+ -- decided per report at the endpoint, not here. The wire shape lives in
142
+ -- common/managed/triage.ts. Whole-entry replace, NULL columns are unset;
143
+ -- flagged is tri-state: NULL unset / 1 flagged / 0 an explicit un-flag
144
+ -- tombstone. A cleared entry keeps its row with every field NULL — a
145
+ -- tombstone a reader adopts as "cleared", where a missing row means "never
146
+ -- annotated" (so a stale client copy can't resurrect a teammate's clear).
147
+ -- updated_by is the last writer (nulled when that user is removed);
148
+ -- updated_by_login is the durable login snapshot (see managed_bundle).
149
+ CREATE TABLE IF NOT EXISTS managed_finding_triage (
150
+ finding_id TEXT PRIMARY KEY,
151
+ color TEXT,
152
+ triage TEXT,
153
+ comment TEXT,
154
+ fix TEXT,
155
+ flagged INTEGER,
156
+ updated_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
157
+ updated_by_login TEXT,
158
+ updated_at INTEGER NOT NULL
159
+ ) STRICT;
160
+
161
+ -- The trail behind managed_finding_triage: one row per write that CHANGED an entry,
162
+ -- holding the entry as written (every field NULL = a clear), who wrote it and
163
+ -- when. managed_finding_triage stays the current-state projection reads hit; this is
164
+ -- walked only for a finding's history. The wire is whole-entry replace, so a
165
+ -- snapshot per write is exactly what arrived and "what changed" is the diff
166
+ -- against the previous row for the same id, computed on read. batch_id groups
167
+ -- the rows of one request. seq is the rowid: a total order that breaks ties
168
+ -- on at. actor_id / actor_login follow the uploaded_by / uploaded_by_login
169
+ -- convention (live account, durable login snapshot). Everything is kept by
170
+ -- default — the trail is the record; an operator may bound it per finding
171
+ -- (TRIAGE_HISTORY_LIMIT), in which case a finding's older events are trimmed
172
+ -- as new ones land. An id no report carries any more is the tombstone GC's
173
+ -- concern.
174
+ CREATE TABLE IF NOT EXISTS managed_finding_triage_event (
175
+ seq INTEGER PRIMARY KEY,
176
+ finding_id TEXT NOT NULL,
177
+ batch_id TEXT NOT NULL,
178
+ color TEXT,
179
+ triage TEXT,
180
+ comment TEXT,
181
+ fix TEXT,
182
+ flagged INTEGER,
183
+ actor_id TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
184
+ actor_login TEXT,
185
+ at INTEGER NOT NULL
186
+ ) STRICT;
187
+ CREATE INDEX IF NOT EXISTS finding_triage_event_finding_idx ON managed_finding_triage_event(finding_id, seq);
188
+ CREATE INDEX IF NOT EXISTS finding_triage_event_actor_at_idx ON managed_finding_triage_event(actor_id, at);
189
+
190
+ -- Teams group users + repos for access scoping. Hidden teams grant no access;
191
+ -- the two link tables below carry the many-many relations.
192
+ CREATE TABLE IF NOT EXISTS managed_team (
193
+ id TEXT PRIMARY KEY,
194
+ slug TEXT NOT NULL,
195
+ name TEXT NOT NULL UNIQUE,
196
+ hidden INTEGER NOT NULL DEFAULT 0,
197
+ dependency_review INTEGER NOT NULL DEFAULT 0,
198
+ created_at INTEGER NOT NULL,
199
+ updated_at INTEGER NOT NULL
200
+ ) STRICT;
201
+
202
+ -- Team <-> repo, many-many, with an OPTIONAL path (a subpath of the repo the
203
+ -- team is scoped to; empty path = the whole repo). Distinct paths can coexist;
204
+ -- adding the whole repo replaces them. CASCADE removes links with either side. repo_id
205
+ -- references the selected (operate-on) repos.
206
+ CREATE TABLE IF NOT EXISTS managed_team_repo (
207
+ team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
208
+ repo_id INTEGER NOT NULL REFERENCES managed_selected_repo(repo_id) ON DELETE CASCADE,
209
+ path TEXT NOT NULL DEFAULT '',
210
+ PRIMARY KEY (team_id, repo_id, path)
211
+ ) STRICT;
212
+
213
+ CREATE INDEX IF NOT EXISTS team_repo_repo_idx ON managed_team_repo(repo_id);
214
+
215
+ -- Team <-> user, many-many, with per-membership visibility permissions (see
216
+ -- common/managed/permissions.ts) — view_dependencies / view_security, both
217
+ -- default 0 (off). CASCADE so the membership dies with either side.
218
+ CREATE TABLE IF NOT EXISTS managed_team_user (
219
+ team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
220
+ user_id TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
221
+ view_dependencies INTEGER NOT NULL DEFAULT 0,
222
+ view_security INTEGER NOT NULL DEFAULT 0,
223
+ PRIMARY KEY (team_id, user_id)
224
+ ) STRICT;
225
+
226
+ CREATE INDEX IF NOT EXISTS team_user_user_idx ON managed_team_user(user_id);
227
+ ` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA + TEAM_NPM_SCOPE_SCHEMA + DEPENDENCY_REVIEW_SCHEMA
@@ -0,0 +1,22 @@
1
+ // Table identifiers are code constants. Renames retain rows and foreign keys;
2
+ // an ambiguous old/new pair must be resolved before the server can start.
3
+ const MANAGED_TABLE_RENAMES = [
4
+ ['selected_repo', 'managed_selected_repo'],
5
+ ['team_repo', 'managed_team_repo'],
6
+ ['team_user', 'managed_team_user'],
7
+ ['finding_triage', 'managed_finding_triage'],
8
+ ['finding_triage_event', 'managed_finding_triage_event'],
9
+ ['finding_comment', 'managed_finding_comment'],
10
+ ['finding_comment_event', 'managed_finding_comment_event'],
11
+ ] as const
12
+
13
+ export function managedTableRenames(tables: Set<string>) {
14
+ // A fresh installation must not claim similarly named unrelated tables in
15
+ // a shared database. managed_user anchors an existing managed installation.
16
+ if (!tables.has('managed_user')) return []
17
+ const renames = MANAGED_TABLE_RENAMES.filter(([from]) => tables.has(from))
18
+ for (const [from, to] of renames) {
19
+ if (tables.has(to)) throw new Error(`Cannot migrate managed tables: both ${from} and ${to} exist`)
20
+ }
21
+ return renames
22
+ }