@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,106 @@
1
+ // Who made a commit beside its author, as the Commits tab shows them
2
+ // (repository-history.ts): its co-authors, as its `Co-authored-by:` trailers
3
+ // name them, and the agents its message marks.
4
+ import { agentSignals, trailerBlock } from '../common/managed/agent-tasks.ts'
5
+ import { GITHUB_AGENTS, type GithubAgentId, emailAgent, githubAgent } from '../common/managed/github-agents.ts'
6
+ import { APP_AGENTS } from './app-agents.ts'
7
+ import { type NamedAccount, commitAccount } from './github-accounts.ts'
8
+
9
+ export const MAX_COAUTHORS = 3
10
+ // The rules who a commit names beside its author is read by (coauthorFacts),
11
+ // and how they are kept: what was kept read by others (history pages,
12
+ // squashed pull requests) read again by these, once raised. The agents'
13
+ // accounts, addresses and names are none of them (coauthorsOf).
14
+ export const COAUTHORS_READ = 5
15
+ const MAX_NAME = 256
16
+ const textOf = (value: unknown, max: number) => typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null
17
+
18
+ // One who made a commit beside its author: by name and account, as its
19
+ // author is, and the agent it is where known (common/managed/github-agents.ts),
20
+ // by its account, its commits' address or name, or the marks it leaves.
21
+ export interface Coauthor { name: string | null; login: string | null; id: number | null; agent?: string }
22
+
23
+ // `Co-authored-by: <name> <address>` trailers, GitHub matching the key
24
+ // regardless of case; Cursor's agents' links, as their work carries them;
25
+ // the app agents known by the fingerprints they leave.
26
+ const COAUTHOR_TRAILER = /^co-authored-by:[ \t]*([^<\n]*?)[ \t]*<([^<>\s]+)>[ \t]*$/gimu
27
+ const CURSOR_AGENT = /https:\/\/cursor\.com\/(?:agents\?id=|background-agent\?bcId=)bc-/u
28
+ const FINGERPRINTED = APP_AGENTS.filter(agent => agent.fingerprint)
29
+
30
+ // One named as a commit's author or co-author: by name, the address its
31
+ // commit gives (where it is read), and account.
32
+ export type Maker = { name: unknown; email?: unknown; account: NamedAccount }
33
+
34
+ // Those a commit's message names in its `Co-authored-by:` trailers
35
+ // (agent-tasks.ts trailerBlock), each by name and address.
36
+ export const coauthorTrailers = (message: unknown) => typeof message === 'string'
37
+ ? [...trailerBlock(message.replaceAll('\r\n', '\n')).matchAll(COAUTHOR_TRAILER)].map(([, name, email]) => ({ name: name!, email: email! })) : []
38
+
39
+ // The agent one who made a commit is: by their account, else the address
40
+ // their commit names them by; else, GitHub naming no account for them, by
41
+ // their name.
42
+ const makerAgent = ({ name, email, account }: Maker): GithubAgentId | null => githubAgent(account.login, account.id) ?? emailAgent(email)
43
+ ?? (account.login ? null : githubAgent(null, null, typeof name === 'string' ? name : null))
44
+
45
+ // Who a commit names beside its author, as its history keeps them
46
+ // (repository-history.ts, `coauthorFacts`), its message never kept: its
47
+ // co-authors (`named`), those GitHub's GraphQL API lists past the author
48
+ // (`listed`, the addresses its `Co-authored-by:` trailers give, by the
49
+ // accounts GitHub knows them by), else its trailers (agent-tasks.ts
50
+ // trailerBlock), an account's no-reply address naming it, as many as
51
+ // MAX_NAMED, each by name, address and account; and the agents its message
52
+ // marks (`markedBy`), Claude's and Codex's as the Agents tab reads them
53
+ // (agent-tasks.ts agentSignals: their sessions' and tasks' links, Claude's
54
+ // co-author and "Generated with" lines), the app agents' by their
55
+ // fingerprints, Cursor's by its agents' links. Which of them are agents,
56
+ // and so the same as another, is told as they are answered (coauthorsOf), so
57
+ // that the agents' accounts, addresses and names change none kept.
58
+ export interface Named { name: string | null; email: string | null; login: string | null; id: number | null }
59
+ export const MAX_NAMED = 10
60
+ export function coauthorFacts(message: unknown, listed: Maker[] | null = null): { named: Named[]; markedBy: string[] } {
61
+ const text = typeof message === 'string' ? message.replaceAll('\r\n', '\n') : ''
62
+ const makers = listed ?? coauthorTrailers(text).map(({ name, email }) => ({ name, email, account: commitAccount({ email }) }))
63
+ const signals = agentSignals([text], FINGERPRINTED)
64
+ return {
65
+ named: makers.slice(0, MAX_NAMED).map(({ name, email, account }) => ({ name: textOf(name, MAX_NAME), email: textOf(email, MAX_NAME), login: account.login, id: account.id })),
66
+ markedBy: [...signals.claude || signals.sessions.length > 0 ? ['claude'] : [], ...signals.tasks.length > 0 || signals.codex ? ['codex'] : [],
67
+ ...CURSOR_AGENT.test(text) ? ['cursor'] : [], ...signals.agents],
68
+ }
69
+ }
70
+
71
+ // A commit as its history keeps it: its author by name, address and account,
72
+ // and who it names beside them (coauthorFacts), absent from pages kept before.
73
+ export interface CommitFacts { authorName: string | null; authorEmail?: string | null; authorLogin: string | null; authorId?: number | null; named?: Named[]
74
+ markedBy?: string[] }
75
+
76
+ // Who made a commit, past its author, as its history keeps them
77
+ // (`commit`, coauthorFacts): its co-authors, then the agents its message
78
+ // marks, each the agent it is where known, by the agents the app knows now,
79
+ // and once, the author none of them, as many as MAX_COAUTHORS: two are one by
80
+ // the first of their agent, account, address and name both name, two people
81
+ // of one name with accounts or addresses of their own two.
82
+ export function coauthorsOf({ authorName, authorEmail = null, authorLogin, authorId = null, named = [], markedBy = [] }: CommitFacts): Coauthor[] {
83
+ type Identity = { agent: string | null; login: string | null; email: string | null; name: string | null }
84
+ const identity = (agent: string | null, login: string | null, name: string | null, email: unknown = null): Identity => ({ agent, login: login?.toLowerCase() ?? null,
85
+ email: typeof email === 'string' && email.trim() ? email.trim().toLowerCase() : null, name: name?.toLowerCase() ?? null })
86
+ const same = (a: Identity, b: Identity) => a.agent && b.agent ? a.agent === b.agent : a.login && b.login ? a.login === b.login
87
+ : a.email && b.email ? a.email === b.email : a.name !== null && a.name === b.name
88
+ const author: Maker = { name: authorName, email: authorEmail, account: { login: authorLogin, id: authorId } }
89
+ const seen = [identity(makerAgent(author), authorLogin, textOf(authorName, MAX_NAME), authorEmail)]
90
+ const coauthors: Coauthor[] = []
91
+ const add = (coauthor: Coauthor, email: unknown = null) => {
92
+ const who = identity(coauthor.agent ?? null, coauthor.login, coauthor.name, email)
93
+ if (seen.some(other => same(other, who)) || coauthors.length >= MAX_COAUTHORS || (!coauthor.login && !coauthor.name && !coauthor.agent)) return
94
+ seen.push(who)
95
+ coauthors.push(coauthor)
96
+ }
97
+ for (const { name, email, ...account } of named) {
98
+ const agent = makerAgent({ name, email, account })
99
+ // An agent known by its address where GitHub names someone else's
100
+ // account for it (Codex's), as itself, not that account.
101
+ const { login, id } = agent && account.login && githubAgent(account.login, account.id) !== agent ? { login: null, id: null } : account
102
+ add({ name, login, id, ...agent ? { agent } : {} }, email)
103
+ }
104
+ for (const agent of markedBy) if (Object.hasOwn(GITHUB_AGENTS, agent)) add({ name: GITHUB_AGENTS[agent as GithubAgentId].name, login: null, id: null, agent })
105
+ return coauthors
106
+ }
@@ -0,0 +1,73 @@
1
+ // The versions a repository's lockfiles of npm's ecosystem lock at a commit,
2
+ // as their review reads them (pull-dependency-reviews.ts): every lockfile
3
+ // there (findRepoLockfiles) a review holds to (`held`, lockfile-scope.ts),
4
+ // read as a pull request's are (pull-dependencies.ts readLockfile), but kept
5
+ // by its blob, in this instance and beyond it (`kept`), as many as
6
+ // MAX_LOCKFILES of those, each's versions (`lockfiles`), and those its reader
7
+ // refuses, of Bun's, or past MAX_LOCKFILES (`unread`), as many read at once
8
+ // as READ_AT_ONCE; and those of some of them (lockedIn), each version once by
9
+ // what its reviews are held to (lockedWith), dev only where it is wherever it
10
+ // is locked. GitHub having none of a lockfile it lists there fails the
11
+ // whole, as it would for any file.
12
+ import { createClient } from '@preventive/upstream/github.js'
13
+ import { upstreamGithub } from './github-errors.ts'
14
+ import { GithubApiError } from './github-app.ts'
15
+ import type { LockedPackage } from './locked-packages.ts'
16
+ import { type Lru, lru } from './lru.ts'
17
+ import { type GithubAsked, type ListedLockfile, type LockfileStore, eachOf, lockedWith, lockfileFormat, readLockfile } from './pull-dependencies.ts'
18
+
19
+ export const MAX_LOCKFILES = 100
20
+ const READ_AT_ONCE = 4
21
+
22
+ export interface CommitLockfiles { lockfiles: { path: string; packages: LockedPackage[] }[]; unread: string[] }
23
+
24
+ // The lockfiles at commit `sha` its review reads, by their paths, each with
25
+ // its format (pull-dependencies.ts lockfileFormat) and its blob's id, kept by
26
+ // the commit, in this instance and beyond it (`kept`); `asked` told where
27
+ // GitHub was.
28
+ const listings: Lru<ListedLockfile[]> = lru(20_000, listed => listed.length + 1)
29
+ export async function lockfilesAt(fullName: string, token: string, sha: string, kept: LockfileStore | null, asked?: GithubAsked) {
30
+ const key = `${fullName}\0${sha}`
31
+ const known = listings.get(key)
32
+ if (known) return known
33
+ const stored = await kept?.listings.read(sha)
34
+ if (stored) { listings.set(key, stored); return stored }
35
+ if (asked) asked.github = true
36
+ const { lockfiles } = await upstreamGithub(() => createClient({ token, userAgent: 'deepview-triage' }).findRepoLockfiles({ repo: fullName, sha }))
37
+ const listed = lockfiles.flatMap(({ path, sha: blob }) => { const format = lockfileFormat(path); return format ? [{ path, format, sha: blob }] : [] })
38
+ listings.set(key, listed)
39
+ await kept?.listings.write(sha, listed)
40
+ return listed
41
+ }
42
+
43
+ export async function lockedAt(fullName: string, token: string, sha: string, kept: LockfileStore | null, held: (path: string) => boolean = () => true,
44
+ asked?: GithubAsked): Promise<CommitLockfiles> {
45
+ const listed = (await lockfilesAt(fullName, token, sha, kept, asked)).filter(({ path }) => held(path))
46
+ const unread = listed.slice(MAX_LOCKFILES).map(({ path }) => path)
47
+ const lockfiles: CommitLockfiles['lockfiles'] = []
48
+ const reading = listed.slice(0, MAX_LOCKFILES)
49
+ const reads = await eachOf(reading, READ_AT_ONCE, lockfile => readLockfile(fullName, token, sha, lockfile, kept, asked))
50
+ for (const [at, lockfile] of reading.entries()) {
51
+ const read = reads[at]!
52
+ if ('error' in read && read.error === 'github-not-found') throw new GithubApiError(404, 'github-not-found')
53
+ if ('error' in read) unread.push(lockfile.path)
54
+ else lockfiles.push({ path: lockfile.path, packages: read.packages })
55
+ }
56
+ return { lockfiles, unread: unread.toSorted() }
57
+ }
58
+
59
+ // The versions those of `read`'s lockfiles `held` holds to lock, each once,
60
+ // and their paths.
61
+ export function lockedIn(read: CommitLockfiles, held: (path: string) => boolean = () => true) {
62
+ const packages = new Map<string, LockedPackage>()
63
+ const paths: string[] = []
64
+ for (const lockfile of read.lockfiles.filter(({ path }) => held(path))) {
65
+ paths.push(lockfile.path)
66
+ for (const pkg of lockfile.packages) {
67
+ const kept = packages.get(lockedWith(pkg))
68
+ if (!kept) packages.set(lockedWith(pkg), { ...pkg })
69
+ else if (kept.dev && !pkg.dev) delete kept.dev
70
+ }
71
+ }
72
+ return { packages: [...packages.values()], paths }
73
+ }
@@ -0,0 +1,41 @@
1
+ // Compare's comparison of two managed bundles or two versions of an npm
2
+ // package (GET /api/bundles/:id/comparison?other=, GET /api/npm/comparison):
3
+ // their files paired, at one path or renamed, with the sizes, resolutions,
4
+ // dependency versions and line counts its Overview and Code read beside them,
5
+ // as common/bundle-comparison.js prepareComparison works them out. The viewer
6
+ // of a managed bundle or a package has every comparison worked out here and
7
+ // loads none of the pairing itself. Answered as Brotli-encoded JSON, its
8
+ // `files.basePaths` (a Map) as `[path, as laid out]` pairs where the two
9
+ // sides' roots differ and none else, as the viewer reads none then. Access is
10
+ // the caller's to check.
11
+ import { Buffer } from 'node:buffer'
12
+ import { Readable } from 'node:stream'
13
+ import { prepareComparison } from '../common/bundle-comparison.js'
14
+ import type { BundleDetails } from '../common/bundle-metadata.js'
15
+ import type { OpenedBlob } from './blob-store.ts'
16
+ import { encodeBrotli } from './brotli.ts'
17
+ import { type NpmPackageFile, npmFileRows } from './npm-packages.ts'
18
+
19
+ // A side with no files, as Review compares a package's first version with:
20
+ // one for each comparison, as working one out keeps its line counts on it.
21
+ export const emptySide = (): BundleDetails => ({ integrity: '(empty)', kind: 'sourcemap', size: 0, json: { version: 3, sources: [], sourcesContent: [] } })
22
+
23
+ // A version's files as the viewer reads them for a comparison (ui/view/npm-package.js
24
+ // npmPackageDetails): its texts, and those that are not text by their size and digest.
25
+ export function npmComparedSide(integrity: string, files: NpmPackageFile[]): BundleDetails & { npmBinaries: Map<string, { size: number; digest: string }> } {
26
+ const rows = npmFileRows(files)
27
+ return {
28
+ integrity, kind: 'sourcemap', size: 0,
29
+ json: { version: 3, sources: rows.map(([path]) => path), sourcesContent: rows.map(([, , text]) => text) },
30
+ npmBinaries: new Map(rows.flatMap(([path, size, text, digest]) => (text === null ? [[path, { size, digest: digest! }] as const] : []))),
31
+ }
32
+ }
33
+
34
+ // The comparison of `base` with `other`, within a reason `scope` where given, as sent.
35
+ export async function comparisonBody(base: BundleDetails, other: BundleDetails, scope = ''): Promise<OpenedBlob> {
36
+ const comparison = prepareComparison(base, other, scope)
37
+ const { files } = comparison
38
+ const json = JSON.stringify({ ...comparison, files: { ...files, basePaths: files.root ? [...files.basePaths] : [] } })
39
+ const body = await encodeBrotli(Buffer.from(json))
40
+ return { size: body.byteLength, stream: Readable.from([body]) }
41
+ }
@@ -0,0 +1,164 @@
1
+ // Managed-server boot config. GitHub user authorization establishes identity;
2
+ // optional App installation credentials enable repository access. A single
3
+ // GitHub App can provide both flows, with repository grants at installation.
4
+ import { env } from 'node:process'
5
+ import { databaseUrls } from '../server-common/database-config.ts'
6
+ import { MAX_UPLOAD_BYTES } from './uploads.ts'
7
+ import { parseStorageKey } from '../server-common/storage-crypto.ts'
8
+ import { defaultCacheDir } from '@preventive/upstream/npm.js'
9
+ import { RESERVED_COOKIES } from './session.ts'
10
+
11
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost'])
12
+
13
+ export interface ManagedConfig {
14
+ port: number
15
+ host: string
16
+ dbPath: string
17
+ neonUrl?: string | null
18
+ blobToken?: string | null
19
+ storageEncryptionKey?: string | null
20
+ storageEncryptionMigrate?: boolean
21
+ storageEncryptionMigrateMaxMs?: number
22
+ vercelPreview?: boolean
23
+ serverless?: boolean
24
+ // Upstream's disk cache (setCacheDir) for audits and bundle builds: its default
25
+ // location, or null for none, as on Vercel, where the database keeps listings.
26
+ upstreamCacheDir?: string | null
27
+ debug: boolean
28
+ allowShare: boolean
29
+ trustProxyEnv: string | undefined
30
+ // GitHub App user-authorization (identity) credentials — the App's client id
31
+ // + secret, used by the login flow.
32
+ githubClientId: string
33
+ githubClientSecret: string
34
+ // Preapproved GitHub identity promoted on login when it is the only user
35
+ // and its role is No access, including a login after registration.
36
+ initialAdminGithubId: number | null
37
+ // Absolute callback registered with GitHub, e.g.
38
+ // 'https://triage.example.com/api/oauth/github/callback'.
39
+ oauthCallbackUrl: string
40
+ // Whether cookies carry `Secure` (callback is https). Also gates the
41
+ // `__Host-` prefix, so loopback http dev can run with a plain cookie name.
42
+ cookieSecure: boolean
43
+ sessionCookieName: string
44
+ sessionTtlMs: number
45
+ // How long GitHub's answer on a user's access to a repository (its
46
+ // visibility, their permission) is kept for that user and repository
47
+ // (repository-browser.ts), asked again past it; 0 asks on every request
48
+ // (MANAGED_REPOSITORY_ACCESS_CACHE_MS, five minutes by default).
49
+ repositoryAccessCacheMs: number
50
+ // Optional installation credentials for the same GitHub App as login.
51
+ // Repository permissions are approved when connecting repositories.
52
+ githubAppId: string | null
53
+ githubAppPrivateKey: string | null
54
+ githubAppSlug: string | null
55
+ githubNewIssueLabels?: string
56
+ // Max accepted size (bytes) for an uploaded report on the "Manage reports"
57
+ // page, before storage compression (default 25 MiB).
58
+ maxReportBytes: number
59
+ // Max accepted size (bytes) for an uploaded bundle (sourcemap / stasis
60
+ // archive). Bundles run larger than reports, so a higher cap (default 200 MiB).
61
+ maxBundleBytes: number
62
+ // How many triage-trail events to keep per finding (managed_finding_triage_event):
63
+ // 0, the default, keeps everything — the trail is the record. An operator
64
+ // who would rather bound the store sets a positive count; older events of a
65
+ // finding are then trimmed as new ones land.
66
+ triageHistoryLimit: number
67
+ }
68
+
69
+ function fail(msg: string): never {
70
+ throw new Error(msg)
71
+ }
72
+
73
+ function requireStr(name: string): string {
74
+ const v = env[name]
75
+ if (v == null || v === '') fail(`Missing required env ${name} (managed mode).`)
76
+ return v
77
+ }
78
+
79
+ function intEnv(name: string, def: number, min: number, max: number): number {
80
+ const raw = env[name]
81
+ const n = raw == null ? def : Number(raw)
82
+ if (!Number.isSafeInteger(n) || n < min || n > max) {
83
+ fail(`Invalid ${name}: ${raw} — must be an integer in [${min}, ${max}].`)
84
+ }
85
+ return n
86
+ }
87
+
88
+ function optionalGithubId(name: string): number | null {
89
+ const raw = env[name]
90
+ if (raw == null || raw === '') return null
91
+ const id = Number(raw)
92
+ if (!/^[1-9]\d*$/u.test(raw) || !Number.isSafeInteger(id)) {
93
+ fail(`Invalid ${name}: must be a single positive numeric GitHub user ID.`)
94
+ }
95
+ return id
96
+ }
97
+
98
+ function urlOrFail(name: string, raw: string): URL {
99
+ try { return new URL(raw) } catch { fail(`${name} is not a valid URL: ${raw}`) }
100
+ }
101
+
102
+ export function loadManagedConfig({ combined = false } = {}): ManagedConfig {
103
+ const serverless = env['VERCEL'] === '1'
104
+ const neonUrl = databaseUrls({ combined }).managed
105
+ const blobToken = env['BLOB_READ_WRITE_TOKEN'] || null
106
+ const storageEncryptionKey = env['MANAGED_STORAGE_ENCRYPTION_KEY'] || null
107
+ parseStorageKey(storageEncryptionKey)
108
+ const storageEncryptionMigrate = env['MANAGED_STORAGE_ENCRYPTION_MIGRATE'] === '1'
109
+ if (storageEncryptionMigrate && !storageEncryptionKey) fail('MANAGED_STORAGE_ENCRYPTION_MIGRATE requires MANAGED_STORAGE_ENCRYPTION_KEY.')
110
+ if (serverless && !neonUrl) fail('Vercel managed mode requires DATABASE_URL or MANAGED_DATABASE_URL.')
111
+ if (neonUrl && !blobToken) fail('Managed Neon mode requires BLOB_READ_WRITE_TOKEN.')
112
+ const host = env['HOST'] ?? '127.0.0.1'
113
+ const oauthCallbackUrl = requireStr('OAUTH_CALLBACK_URL')
114
+ const callback = urlOrFail('OAUTH_CALLBACK_URL', oauthCallbackUrl)
115
+ const cookieSecure = callback.protocol === 'https:'
116
+ // A `__Host-` cookie mandates `Secure`, so any non-loopback bind must be
117
+ // HTTPS — fail fast (mirrors server-e2e's boot check). Loopback dev over
118
+ // http is allowed with a non-prefixed cookie name.
119
+ if ((serverless || !LOOPBACK_HOSTS.has(host)) && !cookieSecure) {
120
+ fail(`Vercel or non-loopback HOST=${host} requires an https OAUTH_CALLBACK_URL — the session cookie needs Secure.`)
121
+ }
122
+ const sessionCookieName = env['SESSION_COOKIE_NAME'] ?? '__Host-dvsid'
123
+ if (sessionCookieName.startsWith('__Host-') && !cookieSecure) {
124
+ fail(`SESSION_COOKIE_NAME=${sessionCookieName} uses the __Host- prefix but the callback is not https. Use a non-prefixed name for loopback http dev.`)
125
+ }
126
+ if (RESERVED_COOKIES.includes(sessionCookieName.replace(/^__Host-/u, ''))) {
127
+ fail(`SESSION_COOKIE_NAME=${sessionCookieName} is reserved for another managed cookie. Choose a different name.`)
128
+ }
129
+ return {
130
+ neonUrl, blobToken, serverless, storageEncryptionKey, storageEncryptionMigrate,
131
+ storageEncryptionMigrateMaxMs: intEnv('MANAGED_STORAGE_ENCRYPTION_MIGRATE_MAX_MS', 150_000, 1, serverless ? 240_000 : 3_600_000),
132
+ vercelPreview: env['VERCEL_ENV'] === 'preview',
133
+ upstreamCacheDir: serverless ? null : defaultCacheDir ?? null,
134
+ port: intEnv('PORT', 8765, 0, 65535),
135
+ host,
136
+ // DB_PATH belongs to e2e in a combined process. Keep the two stores apart.
137
+ dbPath: env['MANAGED_DB_PATH'] ?? (combined ? undefined : env['DB_PATH']) ?? 'server-managed/data/managed.db',
138
+ debug: env['DEBUG'] === '1' || env['DEBUG'] === 'true',
139
+ allowShare: env['DEEPVIEW_ALLOW_SHARE'] === '1',
140
+ trustProxyEnv: env['TRUST_PROXY'] ?? (serverless ? '1' : undefined),
141
+ githubClientId: requireStr('GITHUB_CLIENT_ID'),
142
+ githubClientSecret: requireStr('GITHUB_CLIENT_SECRET'),
143
+ initialAdminGithubId: optionalGithubId('MANAGED_INITIAL_ADMIN_GITHUB_ID'),
144
+ oauthCallbackUrl,
145
+ cookieSecure,
146
+ sessionCookieName,
147
+ sessionTtlMs: intEnv('SESSION_TTL_MS', 1_209_600_000, 60_000, 7_776_000_000),
148
+ repositoryAccessCacheMs: intEnv('MANAGED_REPOSITORY_ACCESS_CACHE_MS', 300_000, 0, 3_600_000),
149
+ githubAppId: env['GITHUB_APP_ID'] ?? null,
150
+ githubAppPrivateKey: normalizePem(env['GITHUB_APP_PRIVATE_KEY']),
151
+ githubAppSlug: env['GITHUB_APP_SLUG'] ?? null,
152
+ githubNewIssueLabels: env['GITHUB_NEW_ISSUE_LABELS'] ?? '',
153
+ maxReportBytes: intEnv('MAX_REPORT_BYTES', 26_214_400, 1, 104_857_600),
154
+ maxBundleBytes: intEnv('MAX_BUNDLE_BYTES', 209_715_200, 1, MAX_UPLOAD_BYTES),
155
+ triageHistoryLimit: intEnv('TRIAGE_HISTORY_LIMIT', 0, 0, 1_000_000_000),
156
+ }
157
+ }
158
+
159
+ // PEM private keys are awkward in env vars; accept a literal multi-line value or
160
+ // one with escaped newlines (`\n`). Empty/absent → null (private repos off).
161
+ function normalizePem(raw: string | undefined): string | null {
162
+ if (raw == null || raw === '') return null
163
+ return raw.includes('\\n') ? raw.replaceAll('\\n', '\n') : raw
164
+ }
@@ -0,0 +1,15 @@
1
+ import type { ManagedDb, StoredUser } from './db.ts'
2
+
3
+ export type RepoScope = { repoId: number; path: string | null }
4
+ type Content = { repoId: number | null; repoDirectory?: string }
5
+
6
+ // Read grants for each request: neither the manager role nor a cached page
7
+ // confers repository access. Bundles have repository scope; reports also have
8
+ // a directory scope. Unassigned content is only available to administrators.
9
+ export async function contentAccess(db: Pick<ManagedDb, 'listRepoScopesForUser'>, user: StoredUser) {
10
+ const scopes = user.role === 'admin' ? null : await db.listRepoScopesForUser(user.id)
11
+ const bundle = (item: Content) => scopes == null || scopes.some(scope => scope.repoId === item.repoId)
12
+ const report = (item: Content) => scopes == null || scopes.some(scope => scope.repoId === item.repoId
13
+ && (!scope.path || item.repoDirectory === scope.path || item.repoDirectory?.startsWith(scope.path + '/')))
14
+ return { scopes, bundle, report }
15
+ }
@@ -0,0 +1,25 @@
1
+ // Token helpers for the managed auth server. The raw session token lives ONLY
2
+ // in the cookie; the DB row id is its SHA-256 (see db.ts / session.ts), so a
3
+ // DB read can't reconstruct a live cookie. No HMAC pepper is needed — the
4
+ // tokens are 256-bit random, not guessable or reversible from their hash.
5
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
6
+
7
+ // 256-bit URL-safe random token: the session cookie value and the OAuth CSRF
8
+ // `state` nonce.
9
+ export function randomToken(): string {
10
+ return randomBytes(32).toString('base64url')
11
+ }
12
+
13
+ // At-rest id for a session cookie token (stored as the managed_session PK).
14
+ export function hashToken(token: string): string {
15
+ return createHash('sha256').update(token, 'utf8').digest('base64url')
16
+ }
17
+
18
+ // Constant-time equality for CSRF / OAuth-state checks. A length mismatch
19
+ // short-circuits (the length of these fixed-width tokens isn't a secret).
20
+ export function safeEqual(a: string, b: string): boolean {
21
+ const ab = Buffer.from(a, 'utf8')
22
+ const bb = Buffer.from(b, 'utf8')
23
+ if (ab.length !== bb.length) return false
24
+ return timingSafeEqual(ab, bb)
25
+ }