@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.51

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 (368) 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 +3 -0
  6. package/client/dependency-paths.js +36 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +115 -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 +357 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-compare-versions.js +172 -0
  17. package/common/bundle-comparison.d.ts +8 -0
  18. package/common/bundle-comparison.js +62 -0
  19. package/common/bundle-metadata.d.ts +15 -0
  20. package/common/bundle-metadata.js +226 -0
  21. package/common/bundle-pkg-of.d.ts +1 -0
  22. package/common/bundle-pkg-of.js +90 -0
  23. package/common/bundle-reasons.d.ts +2 -0
  24. package/common/bundle-reasons.js +21 -0
  25. package/common/bundle-source-links.d.ts +6 -0
  26. package/common/bundle-source-links.js +216 -0
  27. package/common/bundle-sourcemap.d.ts +10 -0
  28. package/common/bundle-sourcemap.js +100 -0
  29. package/common/bundle-sources.d.ts +7 -0
  30. package/common/bundle-sources.js +333 -0
  31. package/common/bundle-stats.js +41 -0
  32. package/common/bundle-tabs.js +1 -0
  33. package/common/code-language.js +65 -0
  34. package/common/default-scan-models.ts +31 -0
  35. package/common/dependency-reviews.d.ts +18 -0
  36. package/common/dependency-reviews.js +64 -0
  37. package/common/finding-duplicates.js +58 -0
  38. package/common/finding-filters.js +274 -0
  39. package/common/finding-id.js +47 -0
  40. package/common/github-issue-labels.d.ts +1 -0
  41. package/common/github-issue-labels.js +11 -0
  42. package/common/github-pr.ts +56 -0
  43. package/common/gzip.js +59 -0
  44. package/common/line-diff.d.ts +8 -0
  45. package/common/line-diff.js +57 -0
  46. package/common/linked-workspace-groups.js +63 -0
  47. package/common/managed/agent-tasks.ts +292 -0
  48. package/common/managed/comments.ts +34 -0
  49. package/common/managed/finding-detail.d.ts +3 -0
  50. package/common/managed/finding-detail.js +66 -0
  51. package/common/managed/github-agents.ts +114 -0
  52. package/common/managed/github-logins.ts +49 -0
  53. package/common/managed/npm-packages.d.ts +7 -0
  54. package/common/managed/npm-packages.js +51 -0
  55. package/common/managed/permissions.ts +31 -0
  56. package/common/managed/report-content.ts +72 -0
  57. package/common/managed/report-filter.ts +109 -0
  58. package/common/managed/repository-alias.ts +65 -0
  59. package/common/managed/roles.ts +35 -0
  60. package/common/managed/routes.d.ts +2 -0
  61. package/common/managed/routes.js +359 -0
  62. package/common/managed/scan-models.ts +6 -0
  63. package/common/managed/team-app.d.ts +6 -0
  64. package/common/managed/team-app.js +47 -0
  65. package/common/managed/triage.ts +83 -0
  66. package/common/managed/upload-seal.ts +126 -0
  67. package/common/pretty-print.d.ts +5 -0
  68. package/common/pretty-print.js +24 -0
  69. package/common/revalidation-conflicts.js +48 -0
  70. package/common/revalidation-input-groups.js +56 -0
  71. package/common/save-error-reason.ts +20 -7
  72. package/common/scan-server.ts +13 -0
  73. package/common/security-links.d.ts +5 -0
  74. package/common/security-links.js +33 -0
  75. package/common/server-info.ts +46 -0
  76. package/common/utf8.d.ts +3 -0
  77. package/common/utf8.js +45 -0
  78. package/common/workspace-app.js +39 -0
  79. package/common/workspace-groups.js +176 -0
  80. package/out/activity.js +4 -0
  81. package/out/brotli-fallback.js +3 -3
  82. package/out/chunk-2K6UN3PC.js +3 -0
  83. package/out/chunk-2XUEZHIP.js +28 -0
  84. package/out/chunk-2ZDZWLFY.js +1 -0
  85. package/out/chunk-4HI2MSAV.js +17 -0
  86. package/out/chunk-6LNQTKAV.js +52 -0
  87. package/out/chunk-7BKKYVWR.js +7 -0
  88. package/out/chunk-7C7DWL7I.js +1 -0
  89. package/out/chunk-7JBSSRD7.js +3 -0
  90. package/out/chunk-7VAWINQF.js +6 -0
  91. package/out/chunk-C4Z6ZBXV.js +10 -0
  92. package/out/chunk-FMXWXDR4.js +36 -0
  93. package/out/chunk-GDPCLSOS.js +2 -0
  94. package/out/chunk-GO7FT4OD.js +1 -0
  95. package/out/chunk-HT2SSA3V.js +1 -0
  96. package/out/chunk-IPAH4LKF.js +232 -0
  97. package/out/chunk-IYPY47MC.js +1 -0
  98. package/out/chunk-IZ7POWI3.js +18 -0
  99. package/out/chunk-JCIIBLKR.js +1 -0
  100. package/out/chunk-KOGNQDSJ.js +1 -0
  101. package/out/chunk-NRPC74AM.js +1 -0
  102. package/out/chunk-OOJM4CTU.js +1 -0
  103. package/out/chunk-PBZPYIG7.js +1 -0
  104. package/out/chunk-PNHBZGDD.js +1 -0
  105. package/out/chunk-PWFUOLZK.js +17 -0
  106. package/out/chunk-PXK7EU4V.js +1 -0
  107. package/out/chunk-QB4CBOX2.js +1 -0
  108. package/out/chunk-R3NZFXVR.js +1 -0
  109. package/out/chunk-R5O6FAW4.js +2 -0
  110. package/out/chunk-SXV2T5C4.js +80 -0
  111. package/out/chunk-TQJDYHVE.js +2 -0
  112. package/out/chunk-TUII34K2.js +1 -0
  113. package/out/chunk-VDRXHIHN.js +3 -0
  114. package/out/chunk-VFWMQ3LG.js +2 -0
  115. package/out/chunk-VH4XMZIW.js +2 -0
  116. package/out/chunk-WCPJDAYA.js +10 -0
  117. package/out/chunk-WFYLMF6Z.js +10 -0
  118. package/out/chunk-WRAJVD35.js +1 -0
  119. package/out/chunk-WYLQWYLA.js +1 -0
  120. package/out/client-managed-import.js +4 -0
  121. package/out/client-managed.js +33 -0
  122. package/out/client-sync.js +11 -17
  123. package/out/compare.js +35 -0
  124. package/out/dialogs.js +55 -0
  125. package/out/github-issue-authorized.html +3 -0
  126. package/out/github-issue-authorized.js +1 -0
  127. package/out/graph.js +17 -4
  128. package/out/index.html +90 -8
  129. package/out/prism.js +14 -10
  130. package/out/scan.js +2 -0
  131. package/out/stasis.svg +20 -0
  132. package/out/terminal.js +366 -39
  133. package/out/view.css +1 -1
  134. package/out/view.js +19 -144
  135. package/package.json +364 -59
  136. package/risk/actions/findings.js +161 -0
  137. package/risk/actions/resolve.js +219 -0
  138. package/risk/actions/scripts/commands.js +182 -0
  139. package/risk/actions/scripts/grade.js +113 -0
  140. package/risk/actions/scripts/packages.js +123 -0
  141. package/risk/actions/scripts/shell.js +169 -0
  142. package/risk/actions/uses.js +76 -0
  143. package/risk/actions/workflow.js +110 -0
  144. package/risk/files/characters.js +196 -0
  145. package/risk/files/hosts.js +205 -0
  146. package/risk/files/readability.js +177 -0
  147. package/risk/files/runs-code.js +615 -0
  148. package/risk/files/script.js +118 -0
  149. package/risk/files/stamps.js +47 -0
  150. package/risk/index.d.ts +254 -0
  151. package/risk/index.js +11 -0
  152. package/risk/npm/assess.js +285 -0
  153. package/risk/npm/combined.js +68 -0
  154. package/risk/npm/compare.js +257 -0
  155. package/risk/npm/dependencies.js +172 -0
  156. package/risk/npm/diff.js +79 -0
  157. package/risk/npm/downloads.js +84 -0
  158. package/risk/npm/files.js +445 -0
  159. package/risk/npm/held.js +63 -0
  160. package/risk/npm/install-scripts.js +34 -0
  161. package/risk/npm/malware.js +36 -0
  162. package/risk/npm/manifest.js +65 -0
  163. package/risk/npm/new-dependency.js +60 -0
  164. package/risk/npm/old-version.js +108 -0
  165. package/risk/npm/registry.js +388 -0
  166. package/risk/npm/release.js +85 -0
  167. package/risk/npm/score.js +59 -0
  168. package/risk/npm/specs.js +124 -0
  169. package/risk/npm/tarball.js +96 -0
  170. package/server-common/database-config.ts +16 -0
  171. package/server-common/initialize.ts +18 -0
  172. package/server-common/npm-advisories.ts +101 -0
  173. package/{server → server-common}/origin.ts +5 -5
  174. package/server-common/reap.ts +48 -0
  175. package/server-common/scan-config.ts +19 -0
  176. package/server-common/standalone.ts +29 -0
  177. package/server-common/storage-crypto.ts +182 -0
  178. package/server-common/storage-log.ts +38 -0
  179. package/server-common/vercel-blob.ts +103 -0
  180. package/server-e2e/app.ts +487 -0
  181. package/{server → server-e2e}/bus-receiver.ts +2 -1
  182. package/{server → server-e2e}/cli.js +9 -4
  183. package/{server → server-e2e}/config.ts +50 -42
  184. package/{server → server-e2e}/db-neon.ts +2 -2
  185. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  186. package/{server → server-e2e}/db-stmt.ts +2 -2
  187. package/{server → server-e2e}/db.ts +7 -7
  188. package/{server → server-e2e}/http.ts +44 -13
  189. package/{server → server-e2e}/hub.ts +40 -9
  190. package/server-e2e/index.ts +17 -0
  191. package/server-e2e/lifecycle.ts +95 -0
  192. package/{server → server-e2e}/neon-driver.ts +2 -2
  193. package/{server → server-e2e}/npm-proxy.ts +12 -145
  194. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  195. package/{server → server-e2e}/objstore/blob.ts +1 -1
  196. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  197. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  198. package/{server → server-e2e}/objstore/init.ts +19 -27
  199. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  200. package/server-e2e/objstore/rest-deny.ts +28 -0
  201. package/server-e2e/objstore/rest-mint.ts +224 -0
  202. package/{server → server-e2e}/objstore/rest.ts +3 -3
  203. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  204. package/{server → server-e2e}/objstore/store.ts +7 -7
  205. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  206. package/{server → server-e2e}/pubsub.ts +12 -9
  207. package/{server → server-e2e}/sign.ts +4 -4
  208. package/{server → server-e2e}/sse-server.ts +60 -33
  209. package/{server → server-e2e}/static.ts +31 -21
  210. package/{server → server-e2e}/sync-handlers.ts +64 -30
  211. package/{server → server-e2e}/ws-server.ts +20 -9
  212. package/server-managed/activity.ts +258 -0
  213. package/server-managed/agent-merges.ts +109 -0
  214. package/server-managed/app-agents.ts +23 -0
  215. package/server-managed/avatar-store.ts +13 -0
  216. package/server-managed/blob-store.ts +17 -0
  217. package/server-managed/brotli.ts +10 -0
  218. package/server-managed/bundle-advisories.ts +120 -0
  219. package/server-managed/bundle-advisory-inventory.ts +77 -0
  220. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  221. package/server-managed/bundle-build-diagnostics.js +23 -0
  222. package/server-managed/bundle-build-leases.ts +41 -0
  223. package/server-managed/bundle-build-worker.js +115 -0
  224. package/server-managed/bundle-build.ts +200 -0
  225. package/server-managed/bundle-cache.ts +211 -0
  226. package/server-managed/bundle-catalog.ts +29 -0
  227. package/server-managed/bundle-commits.ts +140 -0
  228. package/server-managed/bundle-store.ts +24 -0
  229. package/server-managed/bundle-summary-cache.ts +130 -0
  230. package/server-managed/bundle.ts +119 -0
  231. package/server-managed/cache-storage.ts +20 -0
  232. package/server-managed/cli.js +21 -0
  233. package/server-managed/combined.ts +47 -0
  234. package/server-managed/comments.ts +151 -0
  235. package/server-managed/commit-coauthors.ts +106 -0
  236. package/server-managed/commit-lockfiles.ts +76 -0
  237. package/server-managed/comparison.ts +52 -0
  238. package/server-managed/config.ts +164 -0
  239. package/server-managed/content-access.ts +15 -0
  240. package/server-managed/crypto.ts +25 -0
  241. package/server-managed/db-methods.ts +1923 -0
  242. package/server-managed/db-neon.ts +468 -0
  243. package/server-managed/db-schema.ts +238 -0
  244. package/server-managed/db-table-names.ts +22 -0
  245. package/server-managed/db.ts +143 -0
  246. package/server-managed/dependabot-access.ts +60 -0
  247. package/server-managed/dependabot-alerts.ts +185 -0
  248. package/server-managed/dependency-review-comments.ts +98 -0
  249. package/server-managed/dependency-reviews.ts +364 -0
  250. package/server-managed/github-accounts.ts +60 -0
  251. package/server-managed/github-actions.ts +71 -0
  252. package/server-managed/github-app.ts +376 -0
  253. package/server-managed/github-errors.ts +70 -0
  254. package/server-managed/github-graphql.ts +39 -0
  255. package/server-managed/github-issue-links.ts +140 -0
  256. package/server-managed/github-issue-oauth.ts +47 -0
  257. package/server-managed/github-issues.ts +152 -0
  258. package/server-managed/github-link-oauth.ts +69 -0
  259. package/server-managed/github-links.ts +204 -0
  260. package/server-managed/github-metadata.ts +213 -0
  261. package/server-managed/github-oauth.ts +238 -0
  262. package/server-managed/github-pulls.ts +227 -0
  263. package/server-managed/github-requests.ts +136 -0
  264. package/server-managed/github-users.ts +30 -0
  265. package/server-managed/http-response.ts +76 -0
  266. package/server-managed/http.ts +5503 -0
  267. package/server-managed/import-triage.ts +51 -0
  268. package/server-managed/index.ts +192 -0
  269. package/server-managed/kept-briefly.ts +37 -0
  270. package/server-managed/kept.ts +28 -0
  271. package/server-managed/link-reports.ts +162 -0
  272. package/server-managed/listed-activity.ts +39 -0
  273. package/server-managed/locked-packages.ts +152 -0
  274. package/server-managed/lockfile-scope.ts +49 -0
  275. package/server-managed/lru.ts +30 -0
  276. package/server-managed/managed-issues.ts +78 -0
  277. package/server-managed/management-catalog.ts +45 -0
  278. package/server-managed/management.ts +99 -0
  279. package/server-managed/npm-delta-worker.js +23 -0
  280. package/server-managed/npm-delta.ts +194 -0
  281. package/server-managed/npm-insights.ts +449 -0
  282. package/server-managed/npm-kept.ts +225 -0
  283. package/server-managed/npm-loads.ts +264 -0
  284. package/server-managed/npm-manifest.ts +115 -0
  285. package/server-managed/npm-packages.ts +454 -0
  286. package/server-managed/npm-patch.ts +325 -0
  287. package/server-managed/npm-provenance.ts +123 -0
  288. package/server-managed/npm-risk.ts +285 -0
  289. package/server-managed/object-storage-disk.ts +148 -0
  290. package/server-managed/object-storage-vercel.ts +67 -0
  291. package/server-managed/object-storage.ts +75 -0
  292. package/server-managed/package-entry-points.ts +51 -0
  293. package/server-managed/pretty-print.ts +434 -0
  294. package/server-managed/public-workspace.ts +238 -0
  295. package/server-managed/pull-comment-reviews.ts +161 -0
  296. package/server-managed/pull-dependencies.ts +351 -0
  297. package/server-managed/pull-dependency-reviews.ts +334 -0
  298. package/server-managed/pull-reviews.ts +205 -0
  299. package/server-managed/queue.ts +39 -0
  300. package/server-managed/release-commits.ts +84 -0
  301. package/server-managed/rename-diff-worker.js +26 -0
  302. package/server-managed/rename-diff.ts +279 -0
  303. package/server-managed/repo-path.ts +21 -0
  304. package/server-managed/report-migration.ts +35 -0
  305. package/server-managed/report-query.ts +10 -0
  306. package/server-managed/report-response.ts +16 -0
  307. package/server-managed/report-sources.ts +178 -0
  308. package/server-managed/report-store.ts +85 -0
  309. package/server-managed/repository-actions.ts +218 -0
  310. package/server-managed/repository-agents.ts +448 -0
  311. package/server-managed/repository-aliases.ts +91 -0
  312. package/server-managed/repository-browser.ts +294 -0
  313. package/server-managed/repository-changes.ts +284 -0
  314. package/server-managed/repository-code.ts +215 -0
  315. package/server-managed/repository-dependency-reviews.ts +166 -0
  316. package/server-managed/repository-discovery.ts +82 -0
  317. package/server-managed/repository-history.ts +432 -0
  318. package/server-managed/repository-issues.ts +195 -0
  319. package/server-managed/repository-policy.ts +27 -0
  320. package/server-managed/repository-pulls.ts +212 -0
  321. package/server-managed/repository-pulse.ts +460 -0
  322. package/server-managed/repository-risk-assess.ts +105 -0
  323. package/server-managed/repository-risk-audit.ts +79 -0
  324. package/server-managed/repository-risk-read.ts +170 -0
  325. package/server-managed/repository-risk.ts +491 -0
  326. package/server-managed/repository-squashes.ts +168 -0
  327. package/server-managed/request-timing.ts +63 -0
  328. package/server-managed/reviewed-dependencies.ts +149 -0
  329. package/server-managed/revisions.ts +35 -0
  330. package/server-managed/scan-downloads.ts +87 -0
  331. package/server-managed/session.ts +132 -0
  332. package/server-managed/slugs.ts +38 -0
  333. package/server-managed/solidity-entry-points.ts +71 -0
  334. package/server-managed/sql-postgres.ts +34 -0
  335. package/server-managed/sql.ts +88 -0
  336. package/server-managed/static.ts +28 -0
  337. package/server-managed/storage-command.ts +16 -0
  338. package/server-managed/storage-db.ts +183 -0
  339. package/server-managed/storage-encryption.ts +290 -0
  340. package/server-managed/storage-maintenance.ts +259 -0
  341. package/server-managed/storage-payload.ts +121 -0
  342. package/server-managed/storage-stores.ts +91 -0
  343. package/server-managed/storage.ts +82 -0
  344. package/server-managed/team-app.ts +134 -0
  345. package/server-managed/team-catalog.ts +7 -0
  346. package/server-managed/team-feed.ts +180 -0
  347. package/server-managed/team-npm-scopes.ts +106 -0
  348. package/server-managed/team-reports.ts +319 -0
  349. package/server-managed/triage-response.ts +16 -0
  350. package/server-managed/type-only-worker.js +19 -0
  351. package/server-managed/type-only.ts +105 -0
  352. package/server-managed/uploads.ts +106 -0
  353. package/server-managed/upstream-cache.ts +243 -0
  354. package/server-managed/view-as.ts +58 -0
  355. package/server-managed/workspace-shares.ts +169 -0
  356. package/server.ts +70 -0
  357. package/server/index.ts +0 -480
  358. package/server/lifecycle.ts +0 -208
  359. /package/{LICENSE → LICENSE-MIT} +0 -0
  360. /package/{server → server-e2e}/auth.ts +0 -0
  361. /package/{server → server-e2e}/config.example.json +0 -0
  362. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  363. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  364. /package/{server → server-e2e}/objstore/sign.ts +0 -0
  365. /package/{server → server-e2e}/peer.ts +0 -0
  366. /package/{server → server-e2e}/sse-session.ts +0 -0
  367. /package/{server → server-e2e}/util.ts +0 -0
  368. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,152 @@
1
+ // The packages a lockfile of npm's ecosystem locks, as @preventive/lockfile
2
+ // reads package-lock.json and npm-shrinkwrap.json of `lockfileVersion: 3`,
3
+ // pnpm-lock.yaml of pnpm 9 to 12 and Yarn 1's yarn.lock, each with where it
4
+ // is installed from and whether only devDependencies need it
5
+ // (repository-risk.ts).
6
+ import { LockfileError, parseNpmLockfile } from '@preventive/lockfile/npm.js'
7
+ import { parsePnpmLockfile } from '@preventive/lockfile/pnpm.js'
8
+ import { parseYarn1Lockfile } from '@preventive/lockfile/yarn1.js'
9
+ import type { LockfileFormat } from '@preventive/upstream/github.js'
10
+ import type { ReviewedLockfile } from './reviewed-dependencies.ts'
11
+
12
+ export interface LockedPackage {
13
+ name: string
14
+ // None for a git dependency whose package.json has none.
15
+ version: string | null
16
+ // Where it is installed from: npm's registry, another host's tarball with
17
+ // an integrity, such as a company's mirror of npm's, a git repository, a
18
+ // tarball with none, or a file of the project.
19
+ source: 'registry' | 'mirror' | 'git' | 'tarball' | 'file'
20
+ // Where one from elsewhere than npm's registry comes from, without any
21
+ // credentials its URL has.
22
+ from?: string
23
+ // As the lockfile has it: a mirror's tarball's, npm's package of its name
24
+ // and version only where npm's integrity is among it; and, where its
25
+ // reviews ask (`review`), npm's registry's too.
26
+ integrity?: string
27
+ // Where its reviews ask: the sha1 Yarn 1 writes after a tarball's `#`, in
28
+ // hex (`shasum`); the hash of the patch pnpm applies to it
29
+ // (`patchHash`, its patchedDependencies'), and the patch's file, where the
30
+ // lockfile names one (`patchPath`, pnpm 9's and 10's): from the lockfile's
31
+ // directory as read, from the repository's root once that is known
32
+ // (patchedIn); and the lockfile's format.
33
+ shasum?: string
34
+ patchHash?: string
35
+ patchPath?: string
36
+ lockfile?: ReviewedLockfile
37
+ // Where only the importers' devDependencies reach it, as npm's `dev` flag
38
+ // says, and an install of no devDependencies leaves it out: in npm's and
39
+ // pnpm's lockfiles. Yarn 1's says nothing of it, which only the
40
+ // package.json files do, and none of its are.
41
+ dev?: true
42
+ }
43
+
44
+ // A package as each reader resolves one, its name the one it is published
45
+ // under, and a registry tarball's URL held to its name and version.
46
+ type Resolution =
47
+ | { type: 'tarball'; tarball: string | undefined; integrity: string | undefined; sha1?: string | undefined; gitHosted?: boolean }
48
+ | { type: 'git'; repo: string }
49
+ | { type: 'directory' }
50
+ interface ReadPackage { name: string; version: string | undefined; resolution: Resolution | undefined; dev?: boolean | undefined; patchHash?: string | undefined
51
+ patchPath?: string | undefined }
52
+
53
+ // The packages of a lockfile of `format`, as its reader resolves them, a
54
+ // patched one of pnpm's with its patch's file where the lockfile names it.
55
+ // Versions go unchecked against ranges, as only what is locked is asked for.
56
+ function packagesOf(format: LockfileFormat, text: string): ReadPackage[] {
57
+ if (format === 'npm') return Object.values(parseNpmLockfile(text, { checkVersions: false }).packages)
58
+ if (format === 'pnpm') {
59
+ const { lockfile, env } = parsePnpmLockfile(text)
60
+ const files = new Map(Object.values(lockfile?.patchedDependencies ?? {}).flatMap(({ hash, path }) => path ? [[hash, path] as const] : []))
61
+ return [...Object.values(env?.packages ?? {}), ...Object.values(lockfile?.packages ?? {})]
62
+ .map(pkg => pkg.patchHash !== undefined && files.has(pkg.patchHash) ? { ...pkg, patchPath: files.get(pkg.patchHash) } : pkg)
63
+ }
64
+ if (format !== 'yarn') throw new TypeError(`lockedPackages: ${format} lockfiles are not read`)
65
+ // Yarn 2 and later write another format, which only the text tells apart.
66
+ if (/^__metadata:/mu.test(text)) throw new LockfileError('a yarn.lock of Yarn 2 or later, which is not read')
67
+ // Yarn 1 names an alias, `my-q@npm:q@^1.5.1`, by its own name.
68
+ return Object.values(parseYarn1Lockfile(text, { checkVersions: false }).packages)
69
+ .map(pkg => ({ ...pkg, name: /@npm:((?:@[^/@]+\/)?[^/@]+)/u.exec(pkg.patterns[0] ?? '')?.[1] ?? pkg.name }))
70
+ }
71
+
72
+ // npm's registry, by either of its names, and yarn's mirror of it, as
73
+ // @preventive/lockfile holds their tarballs to a package's name and version:
74
+ // its link to one, as it names it, a hash after it alone (Yarn 1's sha1).
75
+ const REGISTRY_HOSTS = new Set(['registry.npmjs.org', 'registry.npmjs.com', 'registry.yarnpkg.com'])
76
+ const isRegistry = (url: URL) => url.protocol === 'https:' && !url.port && !url.username && !url.password && !url.search
77
+ && REGISTRY_HOSTS.has(url.hostname.replace(/\.+$/u, ''))
78
+ // Where a URL is, as it may be shown: its credentials, query and fragment
79
+ // left out.
80
+ const shown = (url: URL | null) => url ? { from: `${url.protocol}//${url.host}${url.pathname}` } : {}
81
+
82
+ // Where a package read comes from (LockedPackage), with npm's registry's
83
+ // integrity where `review`; null for one a package bundles, with no
84
+ // resolution, or one of the project's own directories.
85
+ function located({ name, version, resolution }: ReadPackage, review: boolean): LockedPackage | null {
86
+ const at = { name, version: version ?? null }
87
+ if (resolution === undefined || resolution.type === 'directory') return null
88
+ if (resolution.type === 'git') return { ...at, source: 'git', ...shown(URL.parse(resolution.repo)) }
89
+ const { tarball, integrity } = resolution
90
+ if (resolution.gitHosted) return { ...at, source: 'git', ...shown(URL.parse(tarball ?? '')) }
91
+ const registry = { ...at, source: 'registry' as const, ...review && integrity ? { integrity } : {} }
92
+ if (tarball === undefined) return registry
93
+ if (tarball.startsWith('file:')) return { ...at, source: 'file' }
94
+ const url = URL.parse(tarball)
95
+ if (url && isRegistry(url)) return registry
96
+ return integrity ? { ...at, ...shown(url), source: 'mirror', integrity } : { ...at, ...shown(url), source: 'tarball' }
97
+ }
98
+ // As above, and with what its reviews ask (LockedPackage `shasum`,
99
+ // `patchHash`, `patchPath`, `lockfile`) where `review`.
100
+ function locked(pkg: ReadPackage, format: LockfileFormat, review: boolean): LockedPackage | null {
101
+ const one = located(pkg, review)
102
+ if (!one || !review) return one
103
+ const sha1 = pkg.resolution?.type === 'tarball' ? pkg.resolution.sha1 : undefined
104
+ return { ...one, ...sha1 ? { shasum: sha1 } : {}, ...pkg.patchHash ? { patchHash: pkg.patchHash, ...pkg.patchPath ? { patchPath: pkg.patchPath } : {} } : {},
105
+ lockfile: format === 'yarn' ? 'yarn1' : format as ReviewedLockfile }
106
+ }
107
+
108
+ // The packages a lockfile of `format` (npm, pnpm or yarn, as
109
+ // findRepoLockfiles names them) locks, each once, in the lockfile's order:
110
+ // `dev` only where it is wherever it is installed, at each path of npm's
111
+ // tree, and with each of pnpm's peers; with `review`, each with what its
112
+ // reviews hold it to (LockedPackage `integrity`, `shasum`, `patchHash`),
113
+ // each patch of it apart. Throws the reader's LockfileError or YamlError for
114
+ // one it refuses.
115
+ export function lockedPackages(format: LockfileFormat, text: string, { review = false } = {}): LockedPackage[] {
116
+ const found = new Map<string, LockedPackage>()
117
+ for (const pkg of packagesOf(format, text)) {
118
+ const one = locked(pkg, format, review)
119
+ if (!one) continue
120
+ if (pkg.dev === true) one.dev = true
121
+ const key = `${one.source}\0${one.name}@${one.version ?? ''}\0${one.from ?? ''}\0${one.integrity ?? ''}\0${one.shasum ?? ''}\0${one.patchHash ?? ''}`
122
+ const had = found.get(key)
123
+ if (!had) found.set(key, one)
124
+ else if (had.dev && !one.dev) delete had.dev
125
+ }
126
+ return [...found.values()]
127
+ }
128
+
129
+ // `pkg` as a lockfile at `path` locks it, read with its patch's file from
130
+ // the lockfile's directory (LockedPackage `patchPath`): that file from the
131
+ // repository's root, none where it names one outside it.
132
+ export function patchedIn<T extends LockedPackage>(path: string, pkg: T): T {
133
+ if (pkg.patchPath === undefined) return pkg
134
+ const { patchPath, ...rest } = pkg
135
+ const parts = path.split('/').slice(0, -1)
136
+ for (const part of patchPath.split('/')) {
137
+ if (part === '..') { if (parts.pop() === undefined) return rest as T }
138
+ else if (part !== '.' && part !== '') parts.push(part)
139
+ }
140
+ return parts.length > 0 ? { ...rest, patchPath: parts.join('/') } as T : rest as T
141
+ }
142
+
143
+ // Whether a lockfile at `path` is a test's fixture or example: under a
144
+ // directory of tests (`test`, `tests`, `__tests__` and such), in one of
145
+ // fixtures or examples (`fixture`, `fixtures`, `example`, `examples`) inside
146
+ // it, as a test may keep one its readers refuse on purpose.
147
+ const FIXTURES_DIR = /^(?:fixture|example)s?$/u, TESTS_DIR = /^_*tests?_*$/u
148
+ export function isTestFixture(path: string) {
149
+ const dirs = path.split('/').slice(0, -1)
150
+ const tests = dirs.findIndex(dir => TESTS_DIR.test(dir))
151
+ return tests !== -1 && dirs.slice(tests + 1).some(dir => FIXTURES_DIR.test(dir))
152
+ }
@@ -0,0 +1,49 @@
1
+ // The lockfiles a repository's dependency review holds to, as an admin sets
2
+ // it (repository-dependency-reviews.ts): every one but those it names
3
+ // (`exclude`, naming none for all of them), or those it names alone
4
+ // (`include`). Each is named by its path in the repository, or, ending in
5
+ // `/`, by a directory, every lockfile under it. A directory of a pull
6
+ // request's lockfiles (pull-dependencies.ts) is reviewed where one of its
7
+ // lockfiles is held to; one none of whose are, not at all (`excluded`), nor
8
+ // failing the review where it couldn't be read.
9
+ export interface LockfileScope { mode: 'exclude' | 'include'; paths: string[] }
10
+ export const EVERY_LOCKFILE: LockfileScope = { mode: 'exclude', paths: [] }
11
+ export const MAX_SCOPED_LOCKFILES = 100
12
+ const MAX_PATH = 300
13
+
14
+ // A path as one is named: relative, of `/`-separated names none of which is
15
+ // empty, `.` or `..`, nor holding a backslash or a control character; a
16
+ // directory's ending in `/`.
17
+ function namedPath(path: unknown): path is string {
18
+ if (typeof path !== 'string' || path.length === 0 || path.length > MAX_PATH || path.startsWith('/')) return false
19
+ if (/[\\\p{Cc}]/u.test(path)) return false
20
+ const names = path.endsWith('/') ? path.slice(0, -1).split('/') : path.split('/')
21
+ return names.every(name => name !== '' && name !== '.' && name !== '..')
22
+ }
23
+
24
+ // A scope as a request sends it, each path once, in order; null for one
25
+ // that isn't one.
26
+ export function scopeOf(value: unknown): LockfileScope | null {
27
+ const { mode, paths } = (value ?? {}) as { mode?: unknown; paths?: unknown }
28
+ if ((mode !== 'exclude' && mode !== 'include') || !Array.isArray(paths) || paths.length > MAX_SCOPED_LOCKFILES || !paths.every(namedPath)) return null
29
+ return { mode, paths: [...new Set(paths as string[])].toSorted() }
30
+ }
31
+
32
+ // A scope as stored (JSON, none for every lockfile), every lockfile where
33
+ // it can't be read.
34
+ export function storedScope(text: string | null): LockfileScope {
35
+ if (!text) return EVERY_LOCKFILE
36
+ try { return scopeOf(JSON.parse(text)) ?? EVERY_LOCKFILE } catch { return EVERY_LOCKFILE }
37
+ }
38
+ export const scopeText = (scope: LockfileScope) => isEvery(scope) ? null : JSON.stringify({ mode: scope.mode, paths: scope.paths })
39
+ export const isEvery = (scope: LockfileScope) => scope.mode === 'exclude' && scope.paths.length === 0
40
+
41
+ const names = (path: string, named: string) => named.endsWith('/') ? path.startsWith(named) : path === named
42
+ // Whether the lockfile at `path` is held to.
43
+ export function heldTo(scope: LockfileScope, path: string) {
44
+ const named = scope.paths.some(one => names(path, one))
45
+ return scope.mode === 'include' ? named : !named
46
+ }
47
+ // Whether a directory of lockfiles at `paths` is reviewed: where one is
48
+ // held to, or it names none.
49
+ export const reviewedIn = (scope: LockfileScope, paths: string[]) => paths.length === 0 || paths.some(path => heldTo(scope, path))
@@ -0,0 +1,30 @@
1
+ // Values by key, the least recently used going first once their weights
2
+ // (`weight`, 1 each unless given) are past `limit`.
3
+ export interface Lru<V> { get(key: string): V | undefined; set(key: string, value: V): void }
4
+ export function lru<V>(limit: number, weight: (value: V) => number = () => 1): Lru<V> {
5
+ const kept = new Map<string, V>()
6
+ let total = 0
7
+ const forget = (key: string) => {
8
+ const value = kept.get(key)
9
+ if (value === undefined) return
10
+ kept.delete(key)
11
+ total -= weight(value)
12
+ }
13
+ return {
14
+ get(key) {
15
+ const value = kept.get(key)
16
+ if (value !== undefined) { kept.delete(key); kept.set(key, value) }
17
+ return value
18
+ },
19
+ set(key, value) {
20
+ forget(key)
21
+ if (weight(value) > limit) return
22
+ kept.set(key, value)
23
+ total += weight(value)
24
+ for (const first of kept.keys()) {
25
+ if (total <= limit) break
26
+ forget(first)
27
+ }
28
+ },
29
+ }
30
+ }
@@ -0,0 +1,78 @@
1
+ import type { ManagedSql } from './sql.ts'
2
+ import type { TeamReportAccessSnapshot } from './db.ts'
3
+ import { parseGithubPrUrl } from '../common/github-pr.ts'
4
+
5
+ // One immutable reference per finding, shared across reports and teams. Keep
6
+ // reservations across crashes/uncertain GitHub responses to prevent duplicates.
7
+ // No triage/import/admin patch accepts these fields.
8
+ export const MANAGED_ISSUE_SCHEMA = `
9
+ CREATE TABLE IF NOT EXISTS managed_finding_issue (
10
+ finding_id TEXT PRIMARY KEY,
11
+ repo_id INTEGER NOT NULL,
12
+ repository TEXT NOT NULL,
13
+ request_id TEXT NOT NULL,
14
+ issue_url TEXT,
15
+ auto_fix_url TEXT,
16
+ auto_fix_checked_at INTEGER,
17
+ created_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
18
+ created_at INTEGER NOT NULL
19
+ ) STRICT;
20
+ `
21
+ export interface ManagedIssue {
22
+ findingId: string
23
+ repoId: number
24
+ repository: string
25
+ requestId: string
26
+ issueUrl: string | null
27
+ autoFixUrl: string | null
28
+ autoFixCheckedAt: number | null
29
+ createdBy: string | null
30
+ createdAt: number
31
+ }
32
+ export interface ManagedIssueStore {
33
+ getManagedIssue(findingId: string): Promise<ManagedIssue | null>
34
+ listManagedIssues(findingIds: readonly string[]): Promise<ManagedIssue[]>
35
+ claimManagedIssue(issue: Omit<ManagedIssue, 'issueUrl' | 'autoFixUrl' | 'autoFixCheckedAt'>): Promise<boolean>
36
+ finishManagedIssue(findingId: string, requestId: string, url: string): Promise<boolean>
37
+ releaseManagedIssue(findingId: string, requestId: string): Promise<void>
38
+ setManagedIssueAutoFix(findingId: string, issueUrl: string, previous: string | null, next: string | null, checkedAt: number): Promise<boolean>
39
+ }
40
+
41
+ // A repeated finding ID never grants access to another team's repository.
42
+ export function visibleManagedIssues(issues: ManagedIssue[], snapshot: TeamReportAccessSnapshot) {
43
+ if (snapshot.user.id.startsWith('share:')) return []
44
+ const repos = new Set(snapshot.repositories.map(repo => repo.repoId))
45
+ const names = new Set(snapshot.repositories.map(repo => repo.github.toLowerCase()))
46
+ return issues.filter(issue => issue.issueUrl && repos.has(issue.repoId)).map(issue => {
47
+ const ref = parseGithubPrUrl(issue.autoFixUrl)
48
+ return { ...issue, autoFixUrl: ref && names.has(ref.repo.toLowerCase()) ? issue.autoFixUrl : null }
49
+ })
50
+ }
51
+ export function managedIssueMethods(db: ManagedSql): ManagedIssueStore {
52
+ const fields = `finding_id AS findingId, repo_id AS repoId, repository, request_id AS requestId,
53
+ issue_url AS issueUrl, auto_fix_url AS autoFixUrl, auto_fix_checked_at AS autoFixCheckedAt, created_by AS createdBy, created_at AS createdAt`
54
+ const get = db.prepare(`SELECT ${fields} FROM managed_finding_issue WHERE finding_id = ?`)
55
+ const list = db.prepare(`SELECT ${fields} FROM managed_finding_issue WHERE finding_id IN (SELECT value FROM json_each(?)) ORDER BY finding_id`)
56
+ // A lookup's derived Fix replaces the one it read, unless one started later
57
+ // changed it since: the check time, which only changes stamp
58
+ // (github-issue-links.ts issueUpdates), orders them.
59
+ const autoFix = db.prepare(`UPDATE managed_finding_issue SET auto_fix_url = ?, auto_fix_checked_at = ?
60
+ WHERE finding_id = ? AND issue_url = ? AND COALESCE(auto_fix_url, '') = COALESCE(?, '')
61
+ AND (auto_fix_checked_at IS NULL OR auto_fix_checked_at <= ?)`)
62
+ const claim = db.prepare(`INSERT INTO managed_finding_issue (finding_id, repo_id, repository, request_id, created_by, created_at)
63
+ VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (finding_id) DO NOTHING`)
64
+ const finish = db.prepare(`UPDATE managed_finding_issue SET issue_url = ? WHERE finding_id = ? AND request_id = ? AND issue_url IS NULL`)
65
+ const release = db.prepare(`DELETE FROM managed_finding_issue WHERE finding_id = ? AND request_id = ? AND issue_url IS NULL`)
66
+ return {
67
+ async getManagedIssue(findingId) { return (await get.get(findingId) as ManagedIssue | undefined) ?? null },
68
+ async listManagedIssues(ids) { return await list.all(JSON.stringify(ids)) as ManagedIssue[] },
69
+ async setManagedIssueAutoFix(findingId, issueUrl, previous, next, checkedAt) {
70
+ return Number((await autoFix.run(next, checkedAt, findingId, issueUrl, previous, checkedAt)).changes) > 0
71
+ },
72
+ async claimManagedIssue(issue) {
73
+ return Number((await claim.run(issue.findingId, issue.repoId, issue.repository, issue.requestId, issue.createdBy, issue.createdAt)).changes) > 0
74
+ },
75
+ async finishManagedIssue(findingId, requestId, url) { return Number((await finish.run(url, findingId, requestId)).changes) > 0 },
76
+ async releaseManagedIssue(findingId, requestId) { await release.run(findingId, requestId) },
77
+ }
78
+ }
@@ -0,0 +1,45 @@
1
+ import { roleAtLeast } from '../common/managed/roles.ts'
2
+ import { type RepoScope, contentAccess } from './content-access.ts'
3
+ import type { AdminBundle, AdminReport, ManagedDb } from './db-methods.ts'
4
+ import { ManagedMutationError } from './management.ts'
5
+
6
+ type CatalogAccess = { repos: { repoId: number; fullName: string }[]; repoScopes: RepoScope[] | null }
7
+ export interface ManagementCatalogStore {
8
+ getReportCatalog(sessionId: string, now: number): Promise<CatalogAccess & { reports: (AdminReport & { canChangeRepo: boolean })[] }>
9
+ getBundleCatalog(sessionId: string, now: number): Promise<CatalogAccess & { bundles: (AdminBundle & { canChangeRepo: boolean })[] }>
10
+ }
11
+ type CatalogDb = Pick<ManagedDb, 'getReportAccessSnapshot' | 'listRepoScopesForUser' | 'listSelectedRepos'
12
+ | 'listReports' | 'listBundles' | 'listReadableBundleIds'>
13
+
14
+ // The outer store method owns one read snapshot. Use raw methods here so the
15
+ // session, grants, rows and action flags agree without per-row transactions.
16
+ export function managementCatalogMethods(db: CatalogDb): ManagementCatalogStore {
17
+ async function access(sessionId: string, now: number) {
18
+ const snapshot = await db.getReportAccessSnapshot(sessionId, now, [])
19
+ if (!snapshot) throw new ManagedMutationError(401, 'unauthenticated')
20
+ if (!roleAtLeast(snapshot.user.role, 'manage')) throw new ManagedMutationError(403, 'forbidden')
21
+ const scope = await contentAccess(db, snapshot.user)
22
+ const repos = (await db.listSelectedRepos()).filter(scope.bundle).map(({ repoId, fullName }) => ({ repoId, fullName }))
23
+ return { userId: snapshot.user.role === 'admin' ? undefined : snapshot.user.id, scope, repos }
24
+ }
25
+ return {
26
+ async getReportCatalog(sessionId, now) {
27
+ const { userId, scope, repos } = await access(sessionId, now)
28
+ const reports = await db.listReports(userId)
29
+ const linked = reports.flatMap(report => report.bundleId ? [report.bundleId] : [])
30
+ const readable = userId === undefined ? new Set(linked) : new Set(await db.listReadableBundleIds(userId, linked))
31
+ return { repos, repoScopes: scope.scopes, reports: reports.map(report => ({ ...report,
32
+ canChangeRepo: report.repoId === null || scope.report(report),
33
+ bundleId: report.bundleId && readable.has(report.bundleId) ? report.bundleId : null,
34
+ bundleFilename: report.bundleId && readable.has(report.bundleId) ? report.bundleFilename : null,
35
+ })) }
36
+ },
37
+ async getBundleCatalog(sessionId, now) {
38
+ const { userId, scope, repos } = await access(sessionId, now)
39
+ const bundles = await db.listBundles(userId)
40
+ return { repos, repoScopes: scope.scopes, bundles: bundles.map(bundle => ({ ...bundle,
41
+ canChangeRepo: bundle.repoId === null || scope.report(bundle),
42
+ })) }
43
+ },
44
+ }
45
+ }
@@ -0,0 +1,99 @@
1
+ import type { AdminReport, BundleReuse, ManagedBundle, ManagedDb, ManagedRepo, RepoDataItem, RepoReportItem, ReportRecord, StoredUser } from './db-methods.ts'
2
+ import { roleAtLeast } from '../common/managed/roles.ts'
3
+
4
+ export class ManagedMutationError extends Error {
5
+ status: number
6
+ constructor(status: number, message: string) { super(message); this.status = status }
7
+ }
8
+
9
+ export type ContentMutation = { type: 'delete' } | { type: 'repo'; repoId: number | null; directory: string } | { type: 'visibility'; visible: boolean }
10
+ export type ReportMutation = ContentMutation
11
+ type RemovalRepo = Pick<ManagedRepo, 'repoId' | 'fullName' | 'addedAt'>
12
+ type RemovalAnnotations = { reports: string; ids: string[] }
13
+ export interface ManagementStore {
14
+ mutateReport(sessionId: string, id: string, change: ReportMutation): Promise<{ report: ReportRecord; user: StoredUser }>
15
+ mutateBundle(sessionId: string, id: string, change: ContentMutation): Promise<{ bundle: ManagedBundle; user: StoredUser }>
16
+ // A repeated upload/build of stored bytes (see reuseBundle). Renames need the
17
+ // access other bundle mutations need; a server build labels the row anyway.
18
+ reuseBundleUpload(sessionId: string, id: string, reuse: Omit<BundleReuse, 'rename'>): Promise<boolean>
19
+ removeRepository(sessionId: string, expected: RemovalRepo, annotations: RemovalAnnotations | null): Promise<{
20
+ reports: RepoReportItem[]; bundles: RepoDataItem[]; deletedReports: number; deletedBundles: number; deletedTriage: number
21
+ }>
22
+ }
23
+ type ManagementDb = Pick<ManagedDb, 'sessionWithUser' | 'getReport' | 'getBundle' | 'listReports' | 'listReportsForRepo'
24
+ | 'listBundlesForRepo' | 'listAllRepos' | 'listSelectedRepos' | 'userCanReadReport' | 'userCanReadBundle' | 'userCanReadRepoPath'
25
+ | 'deleteReport' | 'setReportRepo' | 'setReportVisible' | 'deleteBundle' | 'setBundleRepo' | 'setBundleVisible' | 'reuseBundle'
26
+ | 'deleteReportsForRepo' | 'deleteBundlesForRepo' | 'deleteRepo' | 'deleteTriage' | 'recordActivity'>
27
+
28
+ // Only immutable bytes/format and repository membership affect the overlap
29
+ // scan. Ignore publication, user display names, and unrelated bundle links.
30
+ export function reportReferenceSnapshot(reports: readonly Pick<AdminReport, 'id' | 'sha256' | 'filename' | 'repoId'>[]): string {
31
+ return JSON.stringify(reports.map(r => [r.id, r.sha256, r.filename, r.repoId]).toSorted((a, b) => String(a[0]).localeCompare(String(b[0]))))
32
+ }
33
+
34
+ // These methods are wrapped by scopeManagedMethods: authorization, comparison,
35
+ // metadata changes (and repository-removal audit) share the writer transaction. Blob reads
36
+ // and deletes stay outside it. Use the raw store here, never nested scopes.
37
+ export function managementMethods(db: ManagementDb): ManagementStore {
38
+ async function authorize(sessionId: string, role: 'admin' | 'manage') {
39
+ const s = await db.sessionWithUser(sessionId, Date.now())
40
+ if (!s) throw new ManagedMutationError(401, 'unauthenticated')
41
+ if (!roleAtLeast(s.user.role, role)) throw new ManagedMutationError(403, 'forbidden')
42
+ return s.user
43
+ }
44
+ async function destination(user: { id: string; role: string }, change: ContentMutation) {
45
+ if (change.type !== 'repo' || change.repoId === null) return
46
+ if (!(await db.listSelectedRepos()).some(repo => repo.repoId === change.repoId)) throw new ManagedMutationError(400, 'bad-repo')
47
+ if (user.role !== 'admin' && !await db.userCanReadRepoPath(user.id, change.repoId, change.directory)) throw new ManagedMutationError(403, 'repo-forbidden')
48
+ }
49
+ return {
50
+ async mutateReport(sessionId: string, id: string, change: ReportMutation) {
51
+ const user = await authorize(sessionId, 'manage')
52
+ const report = await db.getReport(id)
53
+ if (!report || (user.role !== 'admin' && report.uploadedBy !== user.id && !await db.userCanReadReport(user.id, id))) throw new ManagedMutationError(404, 'no-report')
54
+ if (user.role !== 'admin' && report.repoId !== null && !await db.userCanReadReport(user.id, id)) throw new ManagedMutationError(403, 'repo-forbidden')
55
+ if (change.type === 'repo' && report.repoEmbedded) throw new ManagedMutationError(409, 'repo-in-report')
56
+ if (change.type !== 'visibility') await destination(user, change)
57
+ if (change.type === 'delete') await db.deleteReport(id)
58
+ else if (change.type === 'visibility') await db.setReportVisible(id, change.visible)
59
+ else await db.setReportRepo(id, change.repoId, change.directory)
60
+ return { report, user }
61
+ },
62
+ async mutateBundle(sessionId: string, id: string, change: ContentMutation) {
63
+ const user = await authorize(sessionId, 'manage')
64
+ const bundle = await db.getBundle(id)
65
+ if (!bundle || (user.role !== 'admin' && !await db.userCanReadBundle(user.id, id))) throw new ManagedMutationError(404, 'no-bundle')
66
+ if (user.role !== 'admin' && bundle.repoId !== null && !await db.userCanReadRepoPath(user.id, bundle.repoId, bundle.repoDirectory)) throw new ManagedMutationError(403, 'repo-forbidden')
67
+ await destination(user, change)
68
+ if (change.type === 'delete') await db.deleteBundle(id)
69
+ else if (change.type === 'visibility') await db.setBundleVisible(id, change.visible)
70
+ else await db.setBundleRepo(id, change.repoId, change.directory)
71
+ return { bundle, user }
72
+ },
73
+ async reuseBundleUpload(sessionId: string, id: string, reuse: Omit<BundleReuse, 'rename'>) {
74
+ const user = await authorize(sessionId, 'manage')
75
+ const bundle = await db.getBundle(id)
76
+ if (!bundle) return false
77
+ const rename = user.role === 'admin' || (await db.userCanReadBundle(user.id, id)
78
+ && (bundle.repoId === null || await db.userCanReadRepoPath(user.id, bundle.repoId, bundle.repoDirectory)))
79
+ if (!rename && reuse.provenance !== 'build') return false
80
+ return db.reuseBundle(id, { ...reuse, rename })
81
+ },
82
+ async removeRepository(sessionId: string, expected: RemovalRepo, annotations: RemovalAnnotations | null) {
83
+ const user = await authorize(sessionId, 'admin')
84
+ const repo = (await db.listAllRepos()).find(r => r.repoId === expected.repoId)
85
+ if (!repo) throw new ManagedMutationError(404, 'no-repo')
86
+ if (repo.fullName !== expected.fullName || repo.addedAt !== expected.addedAt) throw new ManagedMutationError(409, 'repository-changed')
87
+ if (annotations && reportReferenceSnapshot(await db.listReports()) !== annotations.reports) throw new ManagedMutationError(409, 'repository-changed')
88
+ const reports = await db.listReportsForRepo(repo.repoId)
89
+ const bundles = await db.listBundlesForRepo(repo.repoId)
90
+ const deletedReports = await db.deleteReportsForRepo(repo.repoId)
91
+ const deletedBundles = await db.deleteBundlesForRepo(repo.repoId)
92
+ const deletedTriage = await db.deleteTriage(annotations?.ids ?? [])
93
+ await db.deleteRepo(repo.repoId)
94
+ await db.recordActivity({ kind: 'delete', actor: user.login, actorId: user.id, repo: repo.fullName,
95
+ action: `removed a repository (${deletedReports} reports, ${deletedBundles} bundles, ${deletedTriage} triage entries)` }, Date.now())
96
+ return { reports, bundles, deletedReports, deletedBundles, deletedTriage }
97
+ },
98
+ }
99
+ }
@@ -0,0 +1,23 @@
1
+ // The lines a package's files change from one side to another (npm-delta.ts),
2
+ // as @preventive/compare's compareTrees counts them (git's --numstat), off
3
+ // the server's event loop: of its text files, renames found as git finds
4
+ // them, a side not there empty; and, apart, those of its changes it stamps
5
+ // `typeOnly`, declaration files changed in types alone.
6
+ import { parentPort, workerData } from 'node:worker_threads'
7
+ import { compareTrees } from '@preventive/compare'
8
+
9
+ const tree = files => files.map(([name, data]) => ({ name, data }))
10
+ const changes = compareTrees(tree(workerData.base), tree(workerData.other), { paths: 'keys', lines: true, renames: true })
11
+ const answer = { additions: 0, deletions: 0, typeAdditions: 0, typeDeletions: 0, typeFiles: 0 }
12
+ for (const change of changes) {
13
+ if (change.binary) continue
14
+ answer.additions += change.insertions ?? 0
15
+ answer.deletions += change.deletions ?? 0
16
+ if (change.typeOnly !== true) continue
17
+ answer.typeAdditions += change.insertions ?? 0
18
+ answer.typeDeletions += change.deletions ?? 0
19
+ answer.typeFiles++
20
+ }
21
+ // Node worker messages have no browser target origin.
22
+ // oxlint-disable-next-line unicorn/require-post-message-target-origin
23
+ parentPort.postMessage(answer)