@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,56 @@
1
+ // The change blocks between two texts' lines, found by @preventive/diff: what
2
+ // the viewer's Compare diffs (ui/view/bundle-compare-code-model.js); and the
3
+ // server's check, apart from @preventive/compare's own, that its diff with
4
+ // renamed names hidden takes the old side to the new (server-managed/rename-diff.ts).
5
+ //
6
+ // Lines are numbered the way the diff numbers them: from zero, a final
7
+ // newline ending the last line rather than starting an empty one.
8
+ import { diff, parseDiff } from '@preventive/diff'
9
+
10
+ export function textLines(text) {
11
+ if (text === '') return []
12
+ const lines = text.split('\n')
13
+ if (lines.at(-1) === '') lines.pop()
14
+ return lines
15
+ }
16
+
17
+ // Whether a text's last line ends in a newline. One added or dropped there is
18
+ // a change of that line, as diff has it (the viewer marks it on that line's
19
+ // row), but for diff -b, which ignores it as it does other trailing blanks.
20
+ const endsLine = text => text === '' || text.endsWith('\n')
21
+
22
+ // A line as diff -b reads it (@preventive/diff's): a run of the C locale's
23
+ // blanks as one space, and those that end it as none.
24
+ const spacing = line => line.replace(/[ \t\n\v\f\r]+$/u, '').replaceAll(/[ \t\n\v\f\r]+/gu, ' ')
25
+
26
+ // Each `a[a0..a1)` replaced by `b[b0..b1)`. `ignoreWhitespace` is diff -b:
27
+ // lines differing only in how much whitespace they have pair up as unchanged,
28
+ // whitespace added or removed between other characters still a change.
29
+ export function changeBlocks(before, after, ignoreWhitespace = false) {
30
+ const text = diff(before, after, { format: 'unified', context: 0, whitespace: ignoreWhitespace ? 'change' : 'none' })
31
+ return text ? (parseDiff(text)[0]?.blocks ?? []).map(({ a0, a1, b0, b1 }) => ({ a0, a1, b0, b1 })) : []
32
+ }
33
+
34
+ // Whether `blocks` take `before`'s lines to `after`'s: each in order and apart
35
+ // from the last, within both texts, and every line between them alike on both
36
+ // sides (alike but for how much whitespace they have where `ignoreWhitespace`,
37
+ // as diff -b has them), the last line in a block where a newline was added or
38
+ // dropped after it, so that a diff drawn from them hides no change. A check on
39
+ // what changeBlocks found, or on one found apart from the texts drawn.
40
+ export function blocksHold(before, after, blocks, ignoreWhitespace = false) {
41
+ if (!Array.isArray(blocks)) return false
42
+ const a = textLines(before), b = textLines(after)
43
+ const alike = ignoreWhitespace ? (x, y) => spacing(x) === spacing(y) : (x, y) => x === y
44
+ let i = 0, j = 0
45
+ for (const block of [...blocks, { a0: a.length, a1: a.length, b0: b.length, b1: b.length, last: true }]) {
46
+ const { a0, a1, b0, b1 } = block ?? {}
47
+ if (![a0, a1, b0, b1].every(Number.isInteger) || a0 < i || b0 < j || a1 < a0 || b1 < b0 || a1 > a.length || b1 > b.length) return false
48
+ // A block changes a line, and as many lines are alike before it on each side.
49
+ if ((!block.last && a1 === a0 && b1 === b0) || a0 - i !== b0 - j) return false
50
+ if (block.last && a0 > i && !ignoreWhitespace && endsLine(before) !== endsLine(after)) return false
51
+ for (; i < a0; i++, j++) if (!alike(a[i], b[j])) return false
52
+ i = a1
53
+ j = b1
54
+ }
55
+ return true
56
+ }
@@ -0,0 +1,63 @@
1
+ // Explicit links combine already-resolved workspace App rows. This is a view
2
+ // grouping, not report deduplication: each report's revalidation answer has
3
+ // already been checked in its own row, and links never create report conflicts.
4
+ //
5
+ // `visibleTabs` decides which tabs may BRIDGE two rows; `retainedTabs` (the
6
+ // same projection unless the caller says otherwise) decides which tabs the
7
+ // combined row keeps. They part when a display choice shows a row more of
8
+ // itself without meaning to re-draw the workspace's row boundaries.
9
+ export function mergeLinkedWorkspaceGroups(groups, duplicatesOf, visibleTabs, retainedTabs = visibleTabs) {
10
+ const parent = groups.map((_, i) => i)
11
+ const find = (i) => {
12
+ while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i] }
13
+ return i
14
+ }
15
+ const tabs = groups.map(visibleTabs)
16
+ const byId = new Map()
17
+ for (let i = 0; i < tabs.length; i++) {
18
+ for (const f of tabs[i]) {
19
+ if (!f.id) continue
20
+ if (!byId.has(f.id)) byId.set(f.id, [])
21
+ byId.get(f.id).push(i)
22
+ }
23
+ }
24
+ for (const [id, rows] of byId) {
25
+ for (const other of duplicatesOf(id)) {
26
+ if (other === id) continue
27
+ for (const a of rows) {
28
+ for (const b of byId.get(other) ?? []) {
29
+ const left = find(a), right = find(b)
30
+ if (left !== right) parent[right] = left
31
+ }
32
+ }
33
+ }
34
+ }
35
+ const components = new Map()
36
+ for (let i = 0; i < groups.length; i++) {
37
+ const root = find(i)
38
+ if (!components.has(root)) components.set(root, [])
39
+ components.get(root).push(i)
40
+ }
41
+ if (components.size === groups.length) return groups
42
+ const key = (f) => f.id ?? String(f._id)
43
+ const merged = [...components.values()].map((indices) => {
44
+ if (indices.length === 1) return groups[indices[0]]
45
+ // Retain the tabs visible in each original row. An imported App finding
46
+ // or an unjudged source row must not disappear when linked to a pass row.
47
+ const visible = new Map()
48
+ for (const i of indices) for (const f of retainedTabs(groups[i])) if (!visible.has(key(f))) visible.set(key(f), f)
49
+ const members = new Map()
50
+ for (const i of indices) {
51
+ for (const f of groups[i]) {
52
+ if (!members.has(key(f))) members.set(key(f), visible.get(key(f)) ?? f)
53
+ }
54
+ }
55
+ const group = [...members.values()]
56
+ const first = groups[indices[0]]
57
+ group.workspaceKey = first.workspaceKey ?? key(first[0])
58
+ group.linkedTabs = [...visible.values()]
59
+ return group
60
+ })
61
+ Object.defineProperty(merged, 'ruledOutIds', { value: groups.ruledOutIds, configurable: true })
62
+ return merged
63
+ }
@@ -0,0 +1,34 @@
1
+ // Managed comments are independent records, shared by reports containing the
2
+ // same finding. Imported and legacy comments may lack attribution or dates.
3
+ export interface ManagedComment {
4
+ id: string
5
+ findingId: string
6
+ body: string
7
+ authorId: string | null
8
+ authorLogin: string | null
9
+ createdAt: number | null
10
+ updatedAt: number | null
11
+ version: number
12
+ }
13
+
14
+ export const MAX_COMMENT_TEXT = 10_000
15
+
16
+ // Callers also enforce write access to the report containing the finding.
17
+ // Admins can remove comments with no linked user, but cannot claim authorship.
18
+ export function canDeleteComment(comment: Pick<ManagedComment, 'authorId'>, actor: { id: string; role: string } | null | undefined): boolean {
19
+ return actor != null && (comment.authorId === actor.id || (comment.authorId == null && actor.role === 'admin'))
20
+ }
21
+
22
+ export function compareManagedComments(a: ManagedComment, b: ManagedComment): number {
23
+ if (a.createdAt == null && b.createdAt != null) return -1
24
+ if (a.createdAt != null && b.createdAt == null) return 1
25
+ const byDate = (a.createdAt ?? 0) - (b.createdAt ?? 0)
26
+ if (byDate !== 0) return byDate
27
+ if (a.id === b.id) return 0
28
+ return a.id < b.id ? -1 : 1
29
+ }
30
+
31
+ export function parseCommentBody(value: unknown): string | null {
32
+ if (typeof value !== 'string' || value.length > MAX_COMMENT_TEXT) return null
33
+ return value.trim() || null
34
+ }
@@ -0,0 +1,5 @@
1
+ export const MAX_TEAM_NPM_SCOPES: number
2
+ export function isNpmPackageName(value: unknown): value is string
3
+ export function isNpmPackageSpec(value: unknown): value is string
4
+ export function npmPackageScope(name: string): string | null
5
+ export function normalizeNpmScope(value: unknown): string | null
@@ -0,0 +1,34 @@
1
+ // npm package names, versions and scopes as the managed npm viewer takes
2
+ // them, shared by its routes, the server and the Teams page.
3
+
4
+ // Upstream's rule (@preventive/upstream src/args.js): a scope, then a name
5
+ // starting with none of `.`, `_` and `-`, 214 characters in all.
6
+ const PACKAGE_NAME = /^(?=.{1,214}$)(?:@[\w.-]+\/[\w-]|[\dA-Za-z])[\w.-]*$/u
7
+ // An exact version or a dist-tag, as the registry resolves `/<name>/<spec>`.
8
+ const PACKAGE_SPEC = /^(?=.{1,256}$)[\dA-Za-z][\w.+-]*$/u
9
+ // npm's rule for a scope: lowercase, and not starting with `.` or `_`.
10
+ const SCOPE = /^@[\da-z~-][\w.~-]*$/u
11
+ export const MAX_TEAM_NPM_SCOPES = 100
12
+
13
+ export function isNpmPackageName(value) {
14
+ return typeof value === 'string' && PACKAGE_NAME.test(value)
15
+ }
16
+
17
+ export function isNpmPackageSpec(value) {
18
+ return typeof value === 'string' && PACKAGE_SPEC.test(value)
19
+ }
20
+
21
+ // `@scope` of a scoped name, lowercased as scopes are compared; null for an
22
+ // unscoped one.
23
+ export function npmPackageScope(name) {
24
+ return name.startsWith('@') && name.includes('/') ? name.slice(0, name.indexOf('/')).toLowerCase() : null
25
+ }
26
+
27
+ // A scope as typed: `scope` or `@scope`, either case, surrounding space
28
+ // ignored. Null when it is not one npm could publish under.
29
+ export function normalizeNpmScope(value) {
30
+ if (typeof value !== 'string') return null
31
+ const trimmed = value.trim().toLowerCase()
32
+ const scope = trimmed.startsWith('@') ? trimmed : `@${trimmed}`
33
+ return scope.length <= 214 && SCOPE.test(scope) ? scope : null
34
+ }
@@ -0,0 +1,31 @@
1
+ // Per-membership visibility permissions for a team's users — shared by the
2
+ // managed server (which stores + validates them) and the admin client (the
3
+ // team-member checkboxes). Each is an independent opt-in, BOTH DEFAULT OFF:
4
+ // dependencies — scan findings in dependencies' own code; App impact stays visible
5
+ // security — security findings and published bundle advisories
6
+ // The managed server filters report responses and authorizes advisory lookups
7
+ // using these independent permissions.
8
+ export type VisibilityPermission = 'dependencies' | 'security'
9
+
10
+ export const VISIBILITY_PERMISSIONS: readonly VisibilityPermission[] = ['dependencies', 'security']
11
+
12
+ // Human labels for the admin checkboxes (keyed by permission).
13
+ export const VISIBILITY_PERMISSION_LABELS: Record<VisibilityPermission, string> = {
14
+ dependencies: 'Dependencies',
15
+ security: 'Security',
16
+ }
17
+
18
+ // A membership's resolved permission set; every key present (false = off).
19
+ export type TeamUserPermissions = Record<VisibilityPermission, boolean>
20
+
21
+ // All-off — the default for a fresh membership.
22
+ const DEFAULT_TEAM_USER_PERMISSIONS: TeamUserPermissions = { dependencies: false, security: false }
23
+
24
+ // Coerce arbitrary input (a request body) into a full permission set: a key is
25
+ // granted only when explicitly `true`; anything else (absent, non-bool) is off.
26
+ export function parseTeamUserPermissions(x: unknown): TeamUserPermissions {
27
+ const o = (x == null || typeof x !== 'object') ? {} : x as Record<string, unknown>
28
+ const out = { ...DEFAULT_TEAM_USER_PERMISSIONS }
29
+ for (const p of VISIBILITY_PERMISSIONS) out[p] = o[p] === true
30
+ return out
31
+ }
@@ -0,0 +1,72 @@
1
+ import { parseLinkedFindings } from '../../client/linked-findings.js'
2
+ import { dependencyDirectory, isDependencyFile } from '../../client/dependency-paths.js'
3
+ import { detectFormat, loadFindings, parseCodexCsvToScans, readReport, reportEntries, reportRepoGithub } from '@preventive/report'
4
+ import { commonFileDirectory } from './repository-alias.ts'
5
+
6
+ // A managed blob retains one server identity even when a CSV contains several
7
+ // scans. Keep all their findings (and upstream ids) under that identity. CSV
8
+ // repository columns describe findings, not an embedded report-level location.
9
+ export function readManagedReport(content: string, filename: string): ReturnType<typeof readReport> {
10
+ const links = content.trimStart().startsWith('[') ? parseLinkedFindings(content) : null
11
+ if (links) return { data: { source: 'links', findings: [], links: links.groups }, format: 'links', reason: null }
12
+ const parsed = readReport(content)
13
+ // Permission-filtered responses can be JSON under the original CSV filename.
14
+ if (parsed.data != null || detectFormat(content, filename) !== 'codex') return parsed
15
+ try {
16
+ const scans = parseCodexCsvToScans(content)
17
+ return {
18
+ data: { type: 'security', source: 'codex-security', findings: scans.flatMap((scan) => scan.data.findings) },
19
+ format: 'codex', reason: null,
20
+ }
21
+ } catch (err) {
22
+ return { data: null, format: null, reason: err instanceof Error ? err.message : 'Invalid Codex CSV' }
23
+ }
24
+ }
25
+
26
+ export function loadManagedFindings(content: string, filename: string): ReturnType<typeof loadFindings> {
27
+ const parsed = readManagedReport(content, filename)
28
+ return parsed.data == null ? Promise.resolve(null) : loadFindings(JSON.stringify(parsed.data))
29
+ }
30
+
31
+ // Source access follows individual visible members, not their shared finding
32
+ // IDs: another member with the same ID can cite files the viewer cannot see.
33
+ export function managedFindingSourcePaths(findings: unknown[]): Set<string> {
34
+ const paths = new Set<string>()
35
+ for (const finding of findings) {
36
+ if (!finding || typeof finding !== 'object') continue
37
+ const f = finding as { file?: unknown; evidence?: { file?: unknown }[] }
38
+ if (typeof f.file === 'string' && f.file) paths.add(f.file)
39
+ for (const evidence of Array.isArray(f.evidence) ? f.evidence : []) {
40
+ if (typeof evidence?.file === 'string' && evidence.file) paths.add(evidence.file)
41
+ }
42
+ }
43
+ return paths
44
+ }
45
+
46
+ // The report's own findings, in either entry shape, outside the dependency
47
+ // directory the local report view would choose: node_modules, else vendor,
48
+ // else dependencies.
49
+ function ownFindings(data: unknown) {
50
+ const objects = (reportEntries(data) ?? []).flat().filter((finding): finding is { file?: unknown } => finding != null && typeof finding === 'object')
51
+ const directory = dependencyDirectory([{ groups: [objects], tree: (data as { tree?: unknown } | null)?.tree }])
52
+ return { directory, own: objects.filter(finding => !isDependencyFile(finding.file, directory)) }
53
+ }
54
+
55
+ // Findings name the repository of their own file. Like the local report view,
56
+ // a report without its own repository belongs to the one repository named by
57
+ // its own findings (Claude Security and Codex exports name one on every finding).
58
+ export function findingsRepository(data: unknown): string | null {
59
+ const repos = new Map<string, string>()
60
+ for (const finding of ownFindings(data).own) {
61
+ const github = reportRepoGithub(finding)
62
+ if (github) repos.set(github.toLowerCase(), github)
63
+ }
64
+ return repos.size === 1 ? [...repos.values()][0]! : null
65
+ }
66
+
67
+ // The directory shared by the files the report's own findings cite, for alias
68
+ // matching. Dependency paths, including cited evidence, cannot hide it.
69
+ export function ownFileDirectory(data: unknown): string {
70
+ const { directory, own } = ownFindings(data)
71
+ return commonFileDirectory([...managedFindingSourcePaths(own)].filter(path => !isDependencyFile(path, directory)))
72
+ }
@@ -0,0 +1,108 @@
1
+ // Apply report permissions before serving findings, triage, or source files.
2
+ // Security uses the same complete-row classification as the UI. Dependency
3
+ // access is per finding: App, own source, and the own-source organizations stay.
4
+ import { inheritReportMeta, isAppFinding, reportRepoGithub, stampSecurityGroups } from '@preventive/report'
5
+ import { readManagedReport } from './report-content.ts'
6
+
7
+ export interface ViewerPermissions {
8
+ dependencies: boolean
9
+ security: boolean
10
+ }
11
+
12
+ type Finding = Record<string, unknown>
13
+ function object(value: unknown): Finding | null {
14
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Finding : null
15
+ }
16
+ function pathOf(finding: Finding): string {
17
+ return typeof finding['file'] === 'string' ? finding['file'].replaceAll('\\', '/') : ''
18
+ }
19
+ function isDependency(finding: Finding): boolean {
20
+ return /(?:^|\/)(?:node_modules|vendor|dependencies)(?:\/|$)/u.test(pathOf(finding))
21
+ }
22
+ function githubOrg(value: unknown): string | null {
23
+ return reportRepoGithub(value)?.split('/')[0]?.toLowerCase() ?? null
24
+ }
25
+ function npmScope(finding: Finding, fromPath = false): string | null {
26
+ let name = object(object(finding['package'])?.['npm'])?.['name']
27
+ if (typeof name !== 'string' && fromPath) {
28
+ const parts = pathOf(finding).split('/')
29
+ for (let i = parts.length - 3; i >= 0; i--) {
30
+ if (['node_modules', 'vendor', 'dependencies'].includes(parts[i]!)) {
31
+ name = `${parts[i + 1]}/${parts[i + 2]}`
32
+ break
33
+ }
34
+ }
35
+ }
36
+ return typeof name === 'string' ? /^(@[^/\s]+)\/[^/\s]+$/u.exec(name)?.[1] ?? null : null
37
+ }
38
+
39
+ // Parsed projections keep run inheritance and derived flags out of cached
40
+ // reports. A known positive security stamp may include linked findings outside
41
+ // this report; a negative stamp never overrides the intrinsic classifier.
42
+ export function projectFinding(value: unknown, report: Finding): Finding | null {
43
+ const original = object(value)
44
+ if (!original) return null
45
+ const finding = { ...original }
46
+ inheritReportMeta(finding, report)
47
+ finding['_source'] = finding['source'] ?? report['source'] ?? null
48
+ if (!('isApp' in finding)) finding['isApp'] = isAppFinding(finding, finding['_source'])
49
+ if (finding['isSecurity'] === true) finding['security'] = true
50
+ return finding
51
+ }
52
+
53
+ // Native JSON and supported text imports obey the same permission rules.
54
+ // A filtered text import is returned as JSON under its original filename, which
55
+ // readManagedReport already accepts. Unchanged inputs keep their original bytes.
56
+ export function filterReportContent(content: string, perms: ViewerPermissions, filename = '', repo?: unknown): string {
57
+ if (perms.dependencies && perms.security) return content
58
+ let data: unknown
59
+ try { data = JSON.parse(content) } catch {
60
+ const parsed = readManagedReport(content, filename)
61
+ if (parsed.data == null) {
62
+ if (/\.csv$/iu.test(filename)) throw new Error('Cannot filter unreadable CSV report')
63
+ return content
64
+ }
65
+ data = parsed.data
66
+ }
67
+ const filtered = filterReportData(data, perms, repo)
68
+ return filtered === data ? content : JSON.stringify(filtered)
69
+ }
70
+
71
+ // repo, when supplied, is the server's authoritative assignment (including an
72
+ // explicit unassigned value). Finding-specific repos still describe their source.
73
+ export function filterReportData(data: unknown, perms: ViewerPermissions, repo?: unknown, securityIds?: Set<string>): unknown {
74
+ if (perms.dependencies && perms.security) return data
75
+ const report = object(data)
76
+ if (!report) return data
77
+ const key = Array.isArray(report['findings']) ? 'findings' : Array.isArray(report['groups']) ? 'groups' : null
78
+ if (!key) return data
79
+ const entries = (report[key] as unknown[]).map(entry => ({
80
+ entry, members: (Array.isArray(entry) ? entry : [entry]).map(original => ({ original, finding: projectFinding(original, report) })),
81
+ }))
82
+ const groups = entries.map(({ members }) => members.flatMap(({ finding }) => finding ? [finding] : []))
83
+ if (!perms.security) stampSecurityGroups(groups)
84
+ const githubOrgs = new Set<string>(), npmScopes = new Set<string>()
85
+ const addOrgs = (finding: Finding) => {
86
+ const github = githubOrg(finding), npm = npmScope(finding)
87
+ if (github) githubOrgs.add(github)
88
+ if (npm) npmScopes.add(npm)
89
+ }
90
+ addOrgs({ ...report, repo: repo === undefined ? report['repo'] : repo })
91
+ for (const finding of groups.flat()) {
92
+ if (finding['isApp'] !== true && !isDependency(finding)) addOrgs(finding)
93
+ }
94
+ const kept: unknown[] = []
95
+ let changed = false
96
+ for (const { entry, members } of entries) {
97
+ // Classify the complete row before hiding dependency components.
98
+ if (!perms.security && members.some(({ finding }) => finding?.['isSecurity'] === true || securityIds?.has(String(finding?.['id'])))) { changed = true; continue }
99
+ const visible = members.filter(({ finding }) => !finding || perms.dependencies || finding['isApp'] === true || !isDependency(finding)
100
+ || githubOrgs.has(githubOrg(finding) ?? '') || npmScopes.has(npmScope(finding, true) ?? ''))
101
+ if (visible.length === members.length) kept.push(entry)
102
+ else {
103
+ changed = true
104
+ if (visible.length > 0) kept.push(Array.isArray(entry) ? visible.map(({ original }) => original) : visible[0]!.original)
105
+ }
106
+ }
107
+ return changed ? { ...report, [key]: kept } : data
108
+ }
@@ -0,0 +1,65 @@
1
+ export interface RepositoryAliasInput {
2
+ oldRepo: string
3
+ oldPath: string
4
+ repoId: number
5
+ newPath: string
6
+ }
7
+
8
+ function containsPath(parent: string, path: string) {
9
+ return !parent || path === parent || path.startsWith(`${parent}/`)
10
+ }
11
+
12
+ function aliasLocation(alias: RepositoryAliasInput, directory: string) {
13
+ if (containsPath(alias.oldPath, directory)) {
14
+ const suffix = directory.slice(alias.oldPath.length).replace(/^\//u, '')
15
+ return { directory: [alias.newPath, suffix].filter(Boolean).join('/'), filePrefix: '' }
16
+ }
17
+ if (!containsPath(directory, alias.oldPath)) return null
18
+ const filePrefix = alias.oldPath.slice(directory.length).replace(/^\//u, '')
19
+ // This suffix is already in every file path. The destination must preserve
20
+ // it, so mapping needs only a new directory, never a file-path rewrite.
21
+ if (alias.newPath === filePrefix) return { directory: '', filePrefix }
22
+ if (!alias.newPath.endsWith(`/${filePrefix}`)) return null
23
+ return { directory: alias.newPath.slice(0, -filePrefix.length - 1), filePrefix }
24
+ }
25
+
26
+ // Match directory boundaries, keeping any subdirectory below the alias. A
27
+ // shared file prefix can prove a more specific alias applies without changing
28
+ // those files. The most specific applicable source wins, once, never chained.
29
+ export function matchRepositoryAlias(github: string, directory: string, aliases: readonly RepositoryAliasInput[], filePrefix = '') {
30
+ let match: { repoId: number; directory: string } | null = null, specificity = -1
31
+ for (const alias of aliases) {
32
+ if (alias.oldRepo.toLowerCase() !== github.toLowerCase()) continue
33
+ const location = aliasLocation(alias, directory)
34
+ if (!location || !containsPath(location.filePrefix, filePrefix) || alias.oldPath.length <= specificity) continue
35
+ match = { repoId: alias.repoId, directory: location.directory }
36
+ specificity = alias.oldPath.length
37
+ }
38
+ return match
39
+ }
40
+
41
+ // Run before decoding a bundle's inventory: unrelated repos/directories and
42
+ // mappings without the required common suffix cannot benefit from its paths.
43
+ export function repositoryAliasNeedsFilePrefix(github: string, directory: string, aliases: readonly RepositoryAliasInput[]) {
44
+ return aliases.some(alias => alias.oldRepo.toLowerCase() === github.toLowerCase()
45
+ && Boolean(aliasLocation(alias, directory)?.filePrefix))
46
+ }
47
+
48
+ // Whole directory segments only; no source bodies, hashing, or path rewriting.
49
+ // A single file still proves its parent prefix. Stop as soon as none is shared.
50
+ export function commonFileDirectory(paths: Iterable<string>): string {
51
+ let prefix: string[] | undefined
52
+ for (const path of paths) {
53
+ const parts = path.split('/')
54
+ if (parts.some(part => !part || part === '.' || part === '..') || path.includes('\\')) return ''
55
+ parts.pop()
56
+ if (prefix === undefined) prefix = parts
57
+ else {
58
+ let length = 0
59
+ while (length < prefix.length && length < parts.length && prefix[length] === parts[length]) length++
60
+ prefix.length = length
61
+ }
62
+ if (prefix.length === 0) return ''
63
+ }
64
+ return prefix?.join('/') ?? ''
65
+ }
@@ -0,0 +1,28 @@
1
+ // Managed-mode user roles, shared by the managed server (which stores +
2
+ // validates them) and the admin client (the role picker). A linear privilege
3
+ // ladder, highest first:
4
+ // admin — full access incl. managing other users' roles
5
+ // manage — owned/team content management
6
+ // triage — can triage findings
7
+ // view — read-only
8
+ // none — no access
9
+ // Admins manage workspace access. Managers handle owned uploads and content
10
+ // in their teams; repository changes also require repository/path access. View
11
+ // is the floor for reading content, triage for annotating accessible reports.
12
+ // None cannot read or mutate managed data, regardless of ownership or teams.
13
+ export type Role = 'admin' | 'manage' | 'triage' | 'view' | 'none'
14
+
15
+ export const ROLES: readonly Role[] = ['admin', 'manage', 'triage', 'view', 'none']
16
+
17
+ export function isRole(x: unknown): x is Role {
18
+ return typeof x === 'string' && (ROLES as readonly string[]).includes(x)
19
+ }
20
+
21
+ // True iff `role` sits at or above `min` in the privilege ladder. ROLES is
22
+ // ordered highest-first, so a lower index is more privileged; an unknown role
23
+ // (index -1) is never "at least" anything. e.g. roleAtLeast(role, 'view') is
24
+ // "has at least read access" (everything except 'none').
25
+ export function roleAtLeast(role: Role, min: Role): boolean {
26
+ const r = ROLES.indexOf(role)
27
+ return r !== -1 && r <= ROLES.indexOf(min)
28
+ }
@@ -0,0 +1,2 @@
1
+ // The static server only needs to recognise managed pages, not resolve them.
2
+ export function parseManagedRoute(url: URL): { view: string } | null