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

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 (366) 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 +2 -0
  50. package/common/managed/finding-detail.js +32 -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 +5 -0
  54. package/common/managed/npm-packages.js +34 -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 +347 -0
  62. package/common/managed/scan-models.ts +6 -0
  63. package/common/managed/team-app.d.ts +5 -0
  64. package/common/managed/team-app.js +31 -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-2WDZPXVL.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-4XMP7YVH.js +36 -0
  87. package/out/chunk-6LNQTKAV.js +52 -0
  88. package/out/chunk-6S2U2UYG.js +232 -0
  89. package/out/chunk-75YLHBCM.js +3 -0
  90. package/out/chunk-7BKKYVWR.js +7 -0
  91. package/out/chunk-7C7DWL7I.js +1 -0
  92. package/out/chunk-7VAWINQF.js +6 -0
  93. package/out/chunk-C4Z6ZBXV.js +10 -0
  94. package/out/chunk-FC65TPUB.js +1 -0
  95. package/out/chunk-FKBXNUW4.js +1 -0
  96. package/out/chunk-GDPCLSOS.js +2 -0
  97. package/out/chunk-GO7FT4OD.js +1 -0
  98. package/out/chunk-HTBYHSPG.js +1 -0
  99. package/out/chunk-IYPY47MC.js +1 -0
  100. package/out/chunk-IZ7POWI3.js +18 -0
  101. package/out/chunk-JCIIBLKR.js +1 -0
  102. package/out/chunk-NRPC74AM.js +1 -0
  103. package/out/chunk-OOJM4CTU.js +1 -0
  104. package/out/chunk-PBZPYIG7.js +1 -0
  105. package/out/chunk-PNHBZGDD.js +1 -0
  106. package/out/chunk-PWFUOLZK.js +17 -0
  107. package/out/chunk-PXK7EU4V.js +1 -0
  108. package/out/chunk-QB4CBOX2.js +1 -0
  109. package/out/chunk-R3NZFXVR.js +1 -0
  110. package/out/chunk-R5O6FAW4.js +2 -0
  111. package/out/chunk-SXV2T5C4.js +80 -0
  112. package/out/chunk-TQJDYHVE.js +2 -0
  113. package/out/chunk-TUII34K2.js +1 -0
  114. package/out/chunk-UK7RGJT6.js +3 -0
  115. package/out/chunk-VFWMQ3LG.js +2 -0
  116. package/out/chunk-VH4XMZIW.js +2 -0
  117. package/out/chunk-WCPJDAYA.js +10 -0
  118. package/out/chunk-WFYLMF6Z.js +10 -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 +362 -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 +21 -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 +73 -0
  237. package/server-managed/comparison.ts +41 -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 +458 -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 +142 -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 +100 -0
  249. package/server-managed/dependency-reviews.ts +346 -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 +415 -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 +39 -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 +5329 -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/locked-packages.ts +129 -0
  273. package/server-managed/lockfile-scope.ts +49 -0
  274. package/server-managed/lru.ts +30 -0
  275. package/server-managed/managed-issues.ts +78 -0
  276. package/server-managed/management-catalog.ts +45 -0
  277. package/server-managed/management.ts +99 -0
  278. package/server-managed/npm-delta-worker.js +23 -0
  279. package/server-managed/npm-delta.ts +194 -0
  280. package/server-managed/npm-insights.ts +449 -0
  281. package/server-managed/npm-kept.ts +215 -0
  282. package/server-managed/npm-loads.ts +264 -0
  283. package/server-managed/npm-manifest.ts +115 -0
  284. package/server-managed/npm-packages.ts +454 -0
  285. package/server-managed/npm-provenance.ts +123 -0
  286. package/server-managed/npm-risk.ts +285 -0
  287. package/server-managed/object-storage-disk.ts +148 -0
  288. package/server-managed/object-storage-vercel.ts +67 -0
  289. package/server-managed/object-storage.ts +75 -0
  290. package/server-managed/package-entry-points.ts +51 -0
  291. package/server-managed/pretty-print.ts +434 -0
  292. package/server-managed/public-workspace.ts +238 -0
  293. package/server-managed/pull-comment-reviews.ts +161 -0
  294. package/server-managed/pull-dependencies.ts +348 -0
  295. package/server-managed/pull-dependency-reviews.ts +328 -0
  296. package/server-managed/pull-reviews.ts +205 -0
  297. package/server-managed/queue.ts +39 -0
  298. package/server-managed/release-commits.ts +84 -0
  299. package/server-managed/rename-diff-worker.js +26 -0
  300. package/server-managed/rename-diff.ts +279 -0
  301. package/server-managed/repo-path.ts +21 -0
  302. package/server-managed/report-migration.ts +35 -0
  303. package/server-managed/report-query.ts +10 -0
  304. package/server-managed/report-response.ts +16 -0
  305. package/server-managed/report-sources.ts +178 -0
  306. package/server-managed/report-store.ts +85 -0
  307. package/server-managed/repository-actions.ts +218 -0
  308. package/server-managed/repository-agents.ts +355 -0
  309. package/server-managed/repository-aliases.ts +91 -0
  310. package/server-managed/repository-browser.ts +294 -0
  311. package/server-managed/repository-changes.ts +284 -0
  312. package/server-managed/repository-code.ts +215 -0
  313. package/server-managed/repository-dependency-reviews.ts +165 -0
  314. package/server-managed/repository-discovery.ts +82 -0
  315. package/server-managed/repository-history.ts +432 -0
  316. package/server-managed/repository-issues.ts +195 -0
  317. package/server-managed/repository-policy.ts +27 -0
  318. package/server-managed/repository-pulls.ts +156 -0
  319. package/server-managed/repository-pulse.ts +460 -0
  320. package/server-managed/repository-risk-assess.ts +105 -0
  321. package/server-managed/repository-risk-audit.ts +79 -0
  322. package/server-managed/repository-risk-read.ts +170 -0
  323. package/server-managed/repository-risk.ts +491 -0
  324. package/server-managed/repository-squashes.ts +168 -0
  325. package/server-managed/request-timing.ts +63 -0
  326. package/server-managed/reviewed-dependencies.ts +150 -0
  327. package/server-managed/revisions.ts +35 -0
  328. package/server-managed/scan-downloads.ts +87 -0
  329. package/server-managed/session.ts +132 -0
  330. package/server-managed/slugs.ts +38 -0
  331. package/server-managed/solidity-entry-points.ts +71 -0
  332. package/server-managed/sql-postgres.ts +34 -0
  333. package/server-managed/sql.ts +88 -0
  334. package/server-managed/static.ts +28 -0
  335. package/server-managed/storage-command.ts +16 -0
  336. package/server-managed/storage-db.ts +183 -0
  337. package/server-managed/storage-encryption.ts +290 -0
  338. package/server-managed/storage-maintenance.ts +259 -0
  339. package/server-managed/storage-payload.ts +121 -0
  340. package/server-managed/storage-stores.ts +91 -0
  341. package/server-managed/storage.ts +82 -0
  342. package/server-managed/team-app.ts +134 -0
  343. package/server-managed/team-catalog.ts +7 -0
  344. package/server-managed/team-feed.ts +180 -0
  345. package/server-managed/team-npm-scopes.ts +106 -0
  346. package/server-managed/team-reports.ts +307 -0
  347. package/server-managed/triage-response.ts +16 -0
  348. package/server-managed/type-only-worker.js +19 -0
  349. package/server-managed/type-only.ts +105 -0
  350. package/server-managed/uploads.ts +106 -0
  351. package/server-managed/upstream-cache.ts +243 -0
  352. package/server-managed/view-as.ts +58 -0
  353. package/server-managed/workspace-shares.ts +169 -0
  354. package/server.ts +70 -0
  355. package/server/index.ts +0 -480
  356. package/server/lifecycle.ts +0 -208
  357. /package/{LICENSE → LICENSE-MIT} +0 -0
  358. /package/{server → server-e2e}/auth.ts +0 -0
  359. /package/{server → server-e2e}/config.example.json +0 -0
  360. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  361. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  362. /package/{server → server-e2e}/objstore/sign.ts +0 -0
  363. /package/{server → server-e2e}/peer.ts +0 -0
  364. /package/{server → server-e2e}/sse-session.ts +0 -0
  365. /package/{server → server-e2e}/util.ts +0 -0
  366. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,114 @@
1
+ // Agents by their own GitHub accounts, where a repository's activity names
2
+ // them: each its name, and how the app draws it, by its mark, as the Agents
3
+ // tab does (ui/view/provider-icons.js), or by its avatar, which
4
+ // /api/avatar/<avatar> serves; its accounts, each by the logins GitHub's REST and GraphQL APIs
5
+ // name it by and its numeric id where known, which GitHub never gives
6
+ // another; and the names its commits carry where GitHub names no account for
7
+ // them. Claude Code's commits are authored as Claude <noreply@anthropic.com>,
8
+ // which GitHub names `claude`; the Claude GitHub App's as `claude[bot]`.
9
+ // Codex's GitHub connector's are its own; Codex otherwise commits as whoever
10
+ // ran it, and those commits are theirs, with a
11
+ // `Co-authored-by: Codex <codex@openai.com>` trailer, an address GitHub
12
+ // names someone else's account for, so Codex is known by the address, not
13
+ // that account. Copilot's coding agent authors its commits as its account,
14
+ // Copilot; Copilot's code review reviews as another, its bot, which GitHub
15
+ // names Copilot as well where it is asked to review and in its threads, and
16
+ // `copilot-pull-request-reviewer` (`[bot]` to GitHub's REST API) on its
17
+ // reviews, the accounts' ids telling the two apart: a login may so be more
18
+ // than one account's. Dependabot, its GitHub App, as its bot,
19
+ // `dependabot` to GitHub's GraphQL API, `dependabot[bot]` to its REST API and
20
+ // in its address. GitHub's Agentic Workflows (gh-aw) post, and approve or
21
+ // review pull requests, as GitHub Actions' bot, `github-actions` (41898282,
22
+ // `[bot]` to the REST API), its mark the GitHub Actions App's avatar; other
23
+ // workflows the bot runs are as much its own, and the commits workflows make
24
+ // as the names they are commonly given, `github-actions` and `GitHub Action`,
25
+ // GitHub naming no account for their addresses. Neo, its GitHub App (3115942),
26
+ // as its account (274065190) and its App's bot (268952163), each known by
27
+ // its id alone, whatever GitHub names it, and drawn by its App's avatar,
28
+ // wherever either or its fingerprint names it. Cursor's agents commit as whoever ran them, with a
29
+ // `Co-authored-by: Cursor Agent <cursoragent@cursor.com>` trailer; the app
30
+ // has no mark of Cursor's, and draws it by its name's letter. Each by the
31
+ // addresses (`emails`) its commits name it by, as author or co-author.
32
+ //
33
+ // Those the Agents tab tells beside Claude and Codex (whose sessions and
34
+ // tasks it tells them by, common/managed/agent-tasks.ts), the server's app
35
+ // agents (server-managed/app-agents.ts APP_AGENTS), by their branches'
36
+ // prefix (`prefix`), the accounts their work is authored by, and the
37
+ // fingerprint (`fingerprint`) they leave as a line of their pull
38
+ // requests' bodies and their commits' messages, where they leave one:
39
+ // Neo's, on `neo/` branches; Copilot's coding agent's, on `copilot/`;
40
+ // Dependabot's, on `dependabot/<ecosystem>/…`. Their work may carry
41
+ // Claude's or Codex's marks too, those its tools.
42
+ export interface GithubAgentAccount { id?: number; logins: readonly string[] }
43
+ export interface GithubAgent { name: string; mark?: 'anthropic' | 'openai' | 'github-copilot'; avatar?: string; accounts: readonly GithubAgentAccount[]; names: readonly string[]
44
+ emails?: readonly string[]; prefix?: string; fingerprint?: string }
45
+
46
+ export const GITHUB_AGENTS = {
47
+ claude: { name: 'Claude', mark: 'anthropic', accounts: [{ id: 81847, logins: ['claude'] }, { logins: ['claude[bot]'] }], names: ['Claude'], emails: ['noreply@anthropic.com'] },
48
+ codex: { name: 'Codex', mark: 'openai', accounts: [{ id: 199175422, logins: ['chatgpt-codex-connector', 'chatgpt-codex-connector[bot]'] }], names: ['Codex'],
49
+ emails: ['codex@openai.com'] },
50
+ neo: { name: 'Neo', avatar: 'app-3115942', accounts: [{ id: 274065190, logins: [] }, { id: 268952163, logins: [] }], names: [], prefix: 'neo/',
51
+ fingerprint: '🤖 Generated with [Neo](' },
52
+ copilot: { name: 'Copilot', mark: 'github-copilot', accounts: [{ id: 198982749, logins: ['Copilot', 'copilot-swe-agent'] },
53
+ { id: 175728472, logins: ['Copilot', 'copilot-pull-request-reviewer', 'copilot-pull-request-reviewer[bot]'] }], names: ['Copilot'], prefix: 'copilot/' },
54
+ dependabot: { name: 'Dependabot', avatar: 'app-29110', accounts: [{ id: 49699333, logins: ['dependabot', 'dependabot[bot]'] }], names: ['dependabot[bot]'],
55
+ prefix: 'dependabot/' },
56
+ actions: { name: 'GitHub Actions', avatar: 'app-15368', accounts: [{ id: 41898282, logins: ['github-actions', 'github-actions[bot]'] }],
57
+ names: ['github-actions[bot]', 'github-actions', 'GitHub Action'] },
58
+ cursor: { name: 'Cursor', accounts: [], names: ['Cursor Agent'], emails: ['cursoragent@cursor.com'] },
59
+ } as const satisfies Record<string, GithubAgent>
60
+ export type GithubAgentId = keyof typeof GITHUB_AGENTS
61
+
62
+ const agents = Object.entries(GITHUB_AGENTS) as [GithubAgentId, GithubAgent][]
63
+ // Each login's agent, and the ids of the accounts it names, undefined for
64
+ // one whose id the app doesn't know.
65
+ const byLogin = new Map<string, { agent: GithubAgentId; ids: (number | undefined)[] }>()
66
+ for (const [agent, { accounts }] of agents) {
67
+ for (const { id, logins } of accounts) {
68
+ for (const login of logins) byLogin.set(login.toLowerCase(), { agent, ids: [...byLogin.get(login.toLowerCase())?.ids ?? [], id] })
69
+ }
70
+ }
71
+ // The accounts known by their ids alone, no login of theirs: each its agent.
72
+ const byId = new Map(agents.flatMap(([agent, { accounts }]) => accounts.flatMap(({ id, logins }) => id !== undefined && logins.length === 0 ? [[id, agent] as const] : [])))
73
+ const byName = new Map(agents.flatMap(([agent, { names }]) => names.map(name => [name.toLowerCase(), agent] as const)))
74
+ const byEmail = new Map(agents.flatMap(([agent, { emails = [] }]) => emails.map(email => [email, agent] as const)))
75
+
76
+ // The agent whose address `email` is, GitHub matching addresses regardless of
77
+ // case; null for anyone else's.
78
+ export const emailAgent = (email: unknown): GithubAgentId | null => typeof email === 'string' ? byEmail.get(email.trim().toLowerCase()) ?? null : null
79
+
80
+ // The agent an author is: by their login, GitHub matching logins regardless
81
+ // of case, their account's id the agent's where both are known, so another
82
+ // account GitHub names by the same login (a user's, the bot's GraphQL login
83
+ // may be) is not; an account known by its id alone, by that, whatever its
84
+ // login; for an author GitHub names no account for, by the name they carry.
85
+ // Null for anyone else: a human's account is theirs, whatever their commits
86
+ // name them.
87
+ export function githubAgent(login: string | null, account: number | null = null, name: string | null = null): GithubAgentId | null {
88
+ if (typeof login !== 'string') return typeof name === 'string' ? byName.get(name.trim().toLowerCase()) ?? null : null
89
+ const known = byLogin.get(login.toLowerCase())
90
+ if (known && (account === null || known.ids.includes(undefined) || known.ids.includes(account))) return known.agent
91
+ return account === null ? null : byId.get(account) ?? null
92
+ }
93
+
94
+ // Whether the accounts `ids` a login is named by are all one agent's, as
95
+ // Copilot's coding agent's and its code review's, both named Copilot, are:
96
+ // the login is that agent's whichever it is, rather than no one's.
97
+ export function oneAgent(login: string, ids: Iterable<number>): boolean {
98
+ const theirs = new Set([...ids].map(id => githubAgent(login, id)))
99
+ return theirs.size === 1 && !theirs.has(null)
100
+ }
101
+
102
+ // The accounts agents work as, a person's to GitHub, as the app is told of
103
+ // them, each by its id and logins: the pull requests they open and the
104
+ // commits they push to a branch directly are agents' (the Pulse's,
105
+ // server-managed/repository-pulls.ts readPulls, repository-pulse.ts
106
+ // commitStats), whatever their branches and labels, the rest of what they do
107
+ // theirs. Christopher Turing's.
108
+ export const AGENT_OPERATED = [{ id: 188797125, logins: ['ChristopherTuring'] }] as const satisfies readonly GithubAgentAccount[]
109
+
110
+ // Whether an author is an account agents work as (AGENT_OPERATED): by its id
111
+ // where GitHub gives it, whatever it is renamed to, else by its login,
112
+ // regardless of case.
113
+ export const agentOperated = (login: string | null, id: number | null = null): boolean => AGENT_OPERATED.some(account => id === null
114
+ ? typeof login === 'string' && account.logins.some(name => name.toLowerCase() === login.toLowerCase()) : account.id === id)
@@ -0,0 +1,49 @@
1
+ // The GitHub accounts an answer about a repository's activity names (its
2
+ // commits', issues', pull requests', agents' and pulse's): as an author, one
3
+ // who merged or a contributor, by login, and by the account's numeric id
4
+ // where the answer names it beside the login. The server resolves them to the
5
+ // app's users and agents (server-managed/github-users.ts), and the client
6
+ // keeps those it answered with in place of what it held for them
7
+ // (ui/view/github-users.js).
8
+ import { oneAgent } from './github-agents.ts'
9
+
10
+ // Where answers name a login, each with where the account's id is named
11
+ // beside it.
12
+ const LOGIN_KEYS = new Map([['author', 'authorId'], ['authorLogin', 'authorId'], ['mergedBy', 'mergedById'], ['login', 'id']])
13
+ const LOGIN = /^[A-Za-z\d][A-Za-z\d-]{0,38}(?:\[bot\])?$/u
14
+ const MAX_DEPTH = 8
15
+ const accountId = (value: unknown) => Number.isSafeInteger(value) && (value as number) > 0 ? value as number : null
16
+
17
+ // The logins `answer` names, each once, with the numeric id of the account
18
+ // named beside them (null where none is, or where those named are all one
19
+ // agent's, oneAgent); `ambiguous` where a login, GitHub matching logins
20
+ // regardless of case, is named beside more than one: by an account GitHub
21
+ // named by it once, as what was kept of the history says, and one it names by
22
+ // it now, which is no telling whose each is.
23
+ export function namedAccounts(answer: unknown): Map<string, number | 'ambiguous' | null> {
24
+ const named = new Map<string, Set<number>>()
25
+ collect(answer, named, 0)
26
+ const ids = new Map<string, Set<number>>()
27
+ for (const [login, theirs] of named) ids.set(login.toLowerCase(), new Set([...ids.get(login.toLowerCase()) ?? [], ...theirs]))
28
+ return new Map([...named.keys()].map((login): [string, number | 'ambiguous' | null] => {
29
+ const theirs = ids.get(login.toLowerCase())!
30
+ if (theirs.size <= 1) return [login, [...theirs][0] ?? null]
31
+ return [login, oneAgent(login, theirs) ? null : 'ambiguous']
32
+ }))
33
+ }
34
+
35
+ function collect(answer: unknown, named: Map<string, Set<number>>, depth: number): void {
36
+ if (depth > MAX_DEPTH || answer === null || typeof answer !== 'object') return
37
+ if (Array.isArray(answer)) {
38
+ for (const value of answer) collect(value, named, depth + 1)
39
+ return
40
+ }
41
+ const fields = answer as Record<string, unknown>
42
+ for (const [key, value] of Object.entries(fields)) {
43
+ const idKey = LOGIN_KEYS.get(key)
44
+ if (idKey && typeof value === 'string' && LOGIN.test(value)) {
45
+ const id = accountId(fields[idKey])
46
+ named.set(value, new Set([...named.get(value) ?? [], ...id === null ? [] : [id]]))
47
+ } else collect(value, named, depth + 1)
48
+ }
49
+ }
@@ -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,109 @@
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
+ import { revalidationLinks, securityLinks } from '../security-links.js'
7
+
8
+ export interface ViewerPermissions {
9
+ dependencies: boolean
10
+ security: boolean
11
+ }
12
+
13
+ type Finding = Record<string, unknown>
14
+ function object(value: unknown): Finding | null {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Finding : null
16
+ }
17
+ function pathOf(finding: Finding): string {
18
+ return typeof finding['file'] === 'string' ? finding['file'].replaceAll('\\', '/') : ''
19
+ }
20
+ function isDependency(finding: Finding): boolean {
21
+ return /(?:^|\/)(?:node_modules|vendor|dependencies)(?:\/|$)/u.test(pathOf(finding))
22
+ }
23
+ function githubOrg(value: unknown): string | null {
24
+ return reportRepoGithub(value)?.split('/')[0]?.toLowerCase() ?? null
25
+ }
26
+ function npmScope(finding: Finding, fromPath = false): string | null {
27
+ let name = object(object(finding['package'])?.['npm'])?.['name']
28
+ if (typeof name !== 'string' && fromPath) {
29
+ const parts = pathOf(finding).split('/')
30
+ for (let i = parts.length - 3; i >= 0; i--) {
31
+ if (['node_modules', 'vendor', 'dependencies'].includes(parts[i]!)) {
32
+ name = `${parts[i + 1]}/${parts[i + 2]}`
33
+ break
34
+ }
35
+ }
36
+ }
37
+ return typeof name === 'string' ? /^(@[^/\s]+)\/[^/\s]+$/u.exec(name)?.[1] ?? null : null
38
+ }
39
+
40
+ // Parsed projections keep run inheritance and derived flags out of cached
41
+ // reports. A known positive security stamp may include linked findings outside
42
+ // this report; a negative stamp never overrides the intrinsic classifier.
43
+ export function projectFinding(value: unknown, report: Finding): Finding | null {
44
+ const original = object(value)
45
+ if (!original) return null
46
+ const finding = { ...original }
47
+ inheritReportMeta(finding, report)
48
+ finding['_source'] = finding['source'] ?? report['source'] ?? null
49
+ if (!('isApp' in finding)) finding['isApp'] = isAppFinding(finding, finding['_source'])
50
+ if (finding['isSecurity'] === true) finding['security'] = true
51
+ return finding
52
+ }
53
+
54
+ // Native JSON and supported text imports obey the same permission rules.
55
+ // A filtered text import is returned as JSON under its original filename, which
56
+ // readManagedReport already accepts. Unchanged inputs keep their original bytes.
57
+ export function filterReportContent(content: string, perms: ViewerPermissions, filename = '', repo?: unknown): string {
58
+ if (perms.dependencies && perms.security) return content
59
+ let data: unknown
60
+ try { data = JSON.parse(content) } catch {
61
+ const parsed = readManagedReport(content, filename)
62
+ if (parsed.data == null) {
63
+ if (/\.csv$/iu.test(filename)) throw new Error('Cannot filter unreadable CSV report')
64
+ return content
65
+ }
66
+ data = parsed.data
67
+ }
68
+ const filtered = filterReportData(data, perms, repo)
69
+ return filtered === data ? content : JSON.stringify(filtered)
70
+ }
71
+
72
+ // repo, when supplied, is the server's authoritative assignment (including an
73
+ // explicit unassigned value). Finding-specific repos still describe their source.
74
+ export function filterReportData(data: unknown, perms: ViewerPermissions, repo?: unknown, securityIds?: Set<string>): unknown {
75
+ if (perms.dependencies && perms.security) return data
76
+ const report = object(data)
77
+ if (!report) return data
78
+ const key = Array.isArray(report['findings']) ? 'findings' : Array.isArray(report['groups']) ? 'groups' : null
79
+ if (!key) return data
80
+ const entries = (report[key] as unknown[]).map(entry => ({
81
+ entry, members: (Array.isArray(entry) ? entry : [entry]).map(original => ({ original, finding: projectFinding(original, report) })),
82
+ }))
83
+ const groups = entries.map(({ members }) => members.flatMap(({ finding }) => finding ? [finding] : []))
84
+ if (!perms.security) stampSecurityGroups(groups, { linkedIds: securityLinks(revalidationLinks(groups.flat())).linkedIds })
85
+ const githubOrgs = new Set<string>(), npmScopes = new Set<string>()
86
+ const addOrgs = (finding: Finding) => {
87
+ const github = githubOrg(finding), npm = npmScope(finding)
88
+ if (github) githubOrgs.add(github)
89
+ if (npm) npmScopes.add(npm)
90
+ }
91
+ addOrgs({ ...report, repo: repo === undefined ? report['repo'] : repo })
92
+ for (const finding of groups.flat()) {
93
+ if (finding['isApp'] !== true && !isDependency(finding)) addOrgs(finding)
94
+ }
95
+ const kept: unknown[] = []
96
+ let changed = false
97
+ for (const { entry, members } of entries) {
98
+ // Classify the complete row before hiding dependency components.
99
+ if (!perms.security && members.some(({ finding }) => finding?.['isSecurity'] === true || securityIds?.has(String(finding?.['id'])))) { changed = true; continue }
100
+ const visible = members.filter(({ finding }) => !finding || perms.dependencies || finding['isApp'] === true || !isDependency(finding)
101
+ || githubOrgs.has(githubOrg(finding) ?? '') || npmScopes.has(npmScope(finding, true) ?? ''))
102
+ if (visible.length === members.length) kept.push(entry)
103
+ else {
104
+ changed = true
105
+ if (visible.length > 0) kept.push(Array.isArray(entry) ? visible.map(({ original }) => original) : visible[0]!.original)
106
+ }
107
+ }
108
+ return changed ? { ...report, [key]: kept } : data
109
+ }
@@ -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,35 @@
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
+ }
29
+
30
+ // Who reads connected repositories' code (the code viewer): a team member,
31
+ // view or above, the server reading only what their team grants reach and
32
+ // their own GitHub account may read. A public link reads none.
33
+ export function canReadRepositoryCode(session: { role: Role; publicShare?: boolean } | null | undefined): boolean {
34
+ return session != null && !session.publicShare && roleAtLeast(session.role, 'view')
35
+ }
@@ -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