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

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 (311) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +2 -0
  6. package/client/dependency-paths.js +24 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +112 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +522 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-metadata.d.ts +14 -0
  17. package/common/bundle-metadata.js +226 -0
  18. package/common/bundle-pkg-of.d.ts +1 -0
  19. package/common/bundle-pkg-of.js +90 -0
  20. package/common/bundle-reasons.d.ts +2 -0
  21. package/common/bundle-reasons.js +21 -0
  22. package/common/bundle-source-links.d.ts +6 -0
  23. package/common/bundle-source-links.js +216 -0
  24. package/common/bundle-sourcemap.d.ts +10 -0
  25. package/common/bundle-sourcemap.js +100 -0
  26. package/common/bundle-sources.d.ts +7 -0
  27. package/common/bundle-sources.js +310 -0
  28. package/common/bundle-stats.js +41 -0
  29. package/common/bundle-tabs.js +1 -0
  30. package/common/code-language.js +65 -0
  31. package/common/default-scan-models.ts +31 -0
  32. package/common/dependency-reviews.d.ts +23 -0
  33. package/common/dependency-reviews.js +81 -0
  34. package/common/finding-duplicates.js +58 -0
  35. package/common/finding-filters.js +274 -0
  36. package/common/finding-id.js +47 -0
  37. package/common/github-issue-labels.d.ts +1 -0
  38. package/common/github-issue-labels.js +11 -0
  39. package/common/github-pr.ts +56 -0
  40. package/common/gzip.js +59 -0
  41. package/common/line-diff.d.ts +8 -0
  42. package/common/line-diff.js +56 -0
  43. package/common/linked-workspace-groups.js +63 -0
  44. package/common/managed/comments.ts +34 -0
  45. package/common/managed/npm-packages.d.ts +5 -0
  46. package/common/managed/npm-packages.js +34 -0
  47. package/common/managed/permissions.ts +31 -0
  48. package/common/managed/report-content.ts +72 -0
  49. package/common/managed/report-filter.ts +108 -0
  50. package/common/managed/repository-alias.ts +65 -0
  51. package/common/managed/roles.ts +28 -0
  52. package/common/managed/routes.d.ts +2 -0
  53. package/common/managed/routes.js +254 -0
  54. package/common/managed/scan-models.ts +6 -0
  55. package/common/managed/team-app.d.ts +2 -0
  56. package/common/managed/team-app.js +31 -0
  57. package/common/managed/triage.ts +83 -0
  58. package/common/managed/upload-seal.ts +126 -0
  59. package/common/pretty-print.d.ts +5 -0
  60. package/common/pretty-print.js +24 -0
  61. package/common/revalidation-conflicts.js +48 -0
  62. package/common/revalidation-input-groups.js +56 -0
  63. package/common/save-error-reason.ts +20 -7
  64. package/common/scan-server.ts +13 -0
  65. package/common/server-info.ts +35 -0
  66. package/common/utf8.d.ts +3 -0
  67. package/common/utf8.js +45 -0
  68. package/common/workspace-app.js +23 -0
  69. package/common/workspace-groups.js +176 -0
  70. package/out/brotli-fallback.js +3 -3
  71. package/out/chunk-2KBF5TZ7.js +2 -0
  72. package/out/chunk-2XUEZHIP.js +28 -0
  73. package/out/chunk-35VE6CDH.js +2 -0
  74. package/out/chunk-3BZ7JERB.js +1 -0
  75. package/out/chunk-4F6JSF57.js +3 -0
  76. package/out/chunk-4XCDFGDP.js +10 -0
  77. package/out/chunk-AQ75QITE.js +1 -0
  78. package/out/chunk-B2RQY6OP.js +2 -0
  79. package/out/chunk-C4Z6ZBXV.js +10 -0
  80. package/out/chunk-CMQA4DM6.js +1 -0
  81. package/out/chunk-CVVJAXVY.js +2 -0
  82. package/out/chunk-DIJJNTRH.js +10 -0
  83. package/out/chunk-GJ4IUCNS.js +1 -0
  84. package/out/chunk-GNXOKIGP.js +80 -0
  85. package/out/chunk-GZ4MS3TE.js +13 -0
  86. package/out/chunk-IISEF2BZ.js +1 -0
  87. package/out/chunk-IU3CCINR.js +3 -0
  88. package/out/chunk-K3X5EJBQ.js +2 -0
  89. package/out/chunk-KEX2ATEW.js +2 -0
  90. package/out/chunk-KYCNJVB7.js +1 -0
  91. package/out/chunk-MYDDTA7W.js +225 -0
  92. package/out/chunk-NRPC74AM.js +1 -0
  93. package/out/chunk-OOJM4CTU.js +1 -0
  94. package/out/chunk-PNHBZGDD.js +1 -0
  95. package/out/chunk-R5O6FAW4.js +2 -0
  96. package/out/chunk-RFZQQXSG.js +17 -0
  97. package/out/chunk-RSHMVYNS.js +1 -0
  98. package/out/chunk-TGANXVTA.js +1 -0
  99. package/out/chunk-TUII34K2.js +1 -0
  100. package/out/chunk-TZ5KB2Y5.js +10 -0
  101. package/out/chunk-UN5ZSYLE.js +1 -0
  102. package/out/chunk-UNTRGE26.js +2 -0
  103. package/out/chunk-UXNUAIIC.js +33 -0
  104. package/out/chunk-UYAXVRVL.js +1 -0
  105. package/out/chunk-V3ME6FI5.js +2 -0
  106. package/out/chunk-V5O7M2YT.js +1 -0
  107. package/out/chunk-VIHGO77Y.js +1 -0
  108. package/out/chunk-VQJCXGIY.js +7 -0
  109. package/out/chunk-W4J3MGLB.js +1 -0
  110. package/out/chunk-WGG7KQUH.js +1 -0
  111. package/out/chunk-WZDTZE64.js +1 -0
  112. package/out/chunk-XYDGU6BM.js +51 -0
  113. package/out/chunk-XYFXXB2G.js +10 -0
  114. package/out/chunk-YVZH5SHP.js +1 -0
  115. package/out/chunk-ZBBOBQ43.js +1 -0
  116. package/out/chunk-ZMQQCGF6.js +2 -0
  117. package/out/client-managed-import.js +4 -0
  118. package/out/client-managed.js +37 -0
  119. package/out/client-sync.js +10 -13
  120. package/out/github-issue-authorized.html +3 -0
  121. package/out/github-issue-authorized.js +1 -0
  122. package/out/graph.js +17 -4
  123. package/out/index.html +90 -8
  124. package/out/prism.js +6 -2
  125. package/out/stasis.svg +20 -0
  126. package/out/terminal.js +365 -39
  127. package/out/view.css +1 -1
  128. package/out/view.js +83 -117
  129. package/package.json +297 -59
  130. package/risk/files/characters.js +196 -0
  131. package/risk/files/hosts.js +205 -0
  132. package/risk/files/readability.js +171 -0
  133. package/risk/files/runs-code.js +358 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +142 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +219 -0
  139. package/risk/npm/combined.js +65 -0
  140. package/risk/npm/compare.js +228 -0
  141. package/risk/npm/dependencies.js +153 -0
  142. package/risk/npm/diff.js +43 -0
  143. package/risk/npm/downloads.js +84 -0
  144. package/risk/npm/files.js +34 -0
  145. package/risk/npm/held.js +60 -0
  146. package/risk/npm/install-scripts.js +34 -0
  147. package/risk/npm/malware.js +36 -0
  148. package/risk/npm/manifest.js +65 -0
  149. package/risk/npm/new-dependency.js +30 -0
  150. package/risk/npm/old-version.js +108 -0
  151. package/risk/npm/registry.js +282 -0
  152. package/risk/npm/release.js +85 -0
  153. package/risk/npm/specs.js +124 -0
  154. package/risk/npm/tarball.js +113 -0
  155. package/server-common/database-config.ts +16 -0
  156. package/server-common/initialize.ts +18 -0
  157. package/server-common/npm-advisories.ts +101 -0
  158. package/{server → server-common}/origin.ts +5 -5
  159. package/server-common/reap.ts +48 -0
  160. package/server-common/scan-config.ts +19 -0
  161. package/server-common/standalone.ts +29 -0
  162. package/server-common/storage-crypto.ts +182 -0
  163. package/server-common/storage-log.ts +38 -0
  164. package/server-common/vercel-blob.ts +103 -0
  165. package/server-e2e/app.ts +486 -0
  166. package/{server → server-e2e}/auth.ts +5 -1
  167. package/{server → server-e2e}/bus-receiver.ts +2 -1
  168. package/{server → server-e2e}/cli.js +9 -4
  169. package/{server → server-e2e}/config.ts +56 -35
  170. package/{server → server-e2e}/db-neon.ts +2 -2
  171. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  172. package/{server → server-e2e}/db-stmt.ts +2 -2
  173. package/{server → server-e2e}/db.ts +7 -7
  174. package/{server → server-e2e}/http.ts +116 -18
  175. package/{server → server-e2e}/hub.ts +40 -9
  176. package/server-e2e/index.ts +17 -0
  177. package/server-e2e/lifecycle.ts +95 -0
  178. package/{server → server-e2e}/neon-driver.ts +2 -2
  179. package/{server → server-e2e}/npm-proxy.ts +12 -145
  180. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  181. package/{server → server-e2e}/objstore/blob.ts +1 -1
  182. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  183. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  184. package/{server → server-e2e}/objstore/init.ts +35 -18
  185. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  186. package/server-e2e/objstore/rest-deny.ts +28 -0
  187. package/server-e2e/objstore/rest-mint.ts +224 -0
  188. package/{server → server-e2e}/objstore/rest.ts +27 -26
  189. package/{server → server-e2e}/objstore/sign.ts +105 -0
  190. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  191. package/{server → server-e2e}/objstore/store.ts +7 -7
  192. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  193. package/{server → server-e2e}/pubsub.ts +12 -9
  194. package/{server → server-e2e}/sign.ts +4 -4
  195. package/{server → server-e2e}/sse-server.ts +102 -68
  196. package/{server → server-e2e}/sse-session.ts +28 -8
  197. package/{server → server-e2e}/static.ts +31 -21
  198. package/server-e2e/sync-handlers.ts +408 -0
  199. package/{server → server-e2e}/ws-server.ts +20 -9
  200. package/server-managed/activity.ts +245 -0
  201. package/server-managed/avatar-store.ts +12 -0
  202. package/server-managed/blob-store.ts +17 -0
  203. package/server-managed/brotli.ts +10 -0
  204. package/server-managed/bundle-advisories.ts +120 -0
  205. package/server-managed/bundle-advisory-inventory.ts +77 -0
  206. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  207. package/server-managed/bundle-build-diagnostics.js +23 -0
  208. package/server-managed/bundle-build-leases.ts +41 -0
  209. package/server-managed/bundle-build-worker.js +111 -0
  210. package/server-managed/bundle-build.ts +198 -0
  211. package/server-managed/bundle-cache.ts +199 -0
  212. package/server-managed/bundle-catalog.ts +29 -0
  213. package/server-managed/bundle-commits.ts +140 -0
  214. package/server-managed/bundle-store.ts +24 -0
  215. package/server-managed/bundle-summary-cache.ts +130 -0
  216. package/server-managed/bundle.ts +120 -0
  217. package/server-managed/cache-storage.ts +19 -0
  218. package/server-managed/cli.js +21 -0
  219. package/server-managed/combined.ts +46 -0
  220. package/server-managed/comments.ts +151 -0
  221. package/server-managed/config.ts +158 -0
  222. package/server-managed/content-access.ts +15 -0
  223. package/server-managed/crypto.ts +25 -0
  224. package/server-managed/db-methods.ts +1725 -0
  225. package/server-managed/db-neon.ts +367 -0
  226. package/server-managed/db-schema.ts +227 -0
  227. package/server-managed/db-table-names.ts +22 -0
  228. package/server-managed/db.ts +132 -0
  229. package/server-managed/dependency-reviews.ts +248 -0
  230. package/server-managed/github-app.ts +391 -0
  231. package/server-managed/github-errors.ts +68 -0
  232. package/server-managed/github-issue-links.ts +127 -0
  233. package/server-managed/github-issue-oauth.ts +47 -0
  234. package/server-managed/github-issues.ts +152 -0
  235. package/server-managed/github-metadata.ts +213 -0
  236. package/server-managed/github-oauth.ts +235 -0
  237. package/server-managed/github-pulls.ts +227 -0
  238. package/server-managed/http-response.ts +50 -0
  239. package/server-managed/http.ts +3472 -0
  240. package/server-managed/import-triage.ts +51 -0
  241. package/server-managed/index.ts +187 -0
  242. package/server-managed/kept.ts +28 -0
  243. package/server-managed/link-reports.ts +158 -0
  244. package/server-managed/locked-packages.ts +85 -0
  245. package/server-managed/lru.ts +30 -0
  246. package/server-managed/managed-issues.ts +75 -0
  247. package/server-managed/management-catalog.ts +45 -0
  248. package/server-managed/management.ts +99 -0
  249. package/server-managed/npm-insights.ts +440 -0
  250. package/server-managed/npm-kept.ts +65 -0
  251. package/server-managed/npm-loads.ts +167 -0
  252. package/server-managed/npm-manifest.ts +115 -0
  253. package/server-managed/npm-packages.ts +378 -0
  254. package/server-managed/npm-provenance.ts +123 -0
  255. package/server-managed/npm-risk.ts +177 -0
  256. package/server-managed/object-storage-disk.ts +148 -0
  257. package/server-managed/object-storage-vercel.ts +67 -0
  258. package/server-managed/object-storage.ts +74 -0
  259. package/server-managed/package-entry-points.ts +51 -0
  260. package/server-managed/pretty-print.ts +352 -0
  261. package/server-managed/public-workspace.ts +220 -0
  262. package/server-managed/rename-diff-worker.js +26 -0
  263. package/server-managed/rename-diff.ts +270 -0
  264. package/server-managed/repo-path.ts +21 -0
  265. package/server-managed/report-migration.ts +35 -0
  266. package/server-managed/report-query.ts +10 -0
  267. package/server-managed/report-response.ts +16 -0
  268. package/server-managed/report-sources.ts +178 -0
  269. package/server-managed/report-store.ts +47 -0
  270. package/server-managed/repository-aliases.ts +91 -0
  271. package/server-managed/repository-browser.ts +153 -0
  272. package/server-managed/repository-discovery.ts +82 -0
  273. package/server-managed/repository-policy.ts +27 -0
  274. package/server-managed/repository-risk.ts +418 -0
  275. package/server-managed/revisions.ts +35 -0
  276. package/server-managed/session.ts +92 -0
  277. package/server-managed/slugs.ts +38 -0
  278. package/server-managed/solidity-entry-points.ts +69 -0
  279. package/server-managed/sql-postgres.ts +31 -0
  280. package/server-managed/sql.ts +76 -0
  281. package/server-managed/static.ts +28 -0
  282. package/server-managed/storage-command.ts +16 -0
  283. package/server-managed/storage-db.ts +175 -0
  284. package/server-managed/storage-encryption.ts +279 -0
  285. package/server-managed/storage-maintenance.ts +256 -0
  286. package/server-managed/storage-payload.ts +103 -0
  287. package/server-managed/storage-stores.ts +85 -0
  288. package/server-managed/storage.ts +78 -0
  289. package/server-managed/team-app.ts +94 -0
  290. package/server-managed/team-catalog.ts +7 -0
  291. package/server-managed/team-feed.ts +173 -0
  292. package/server-managed/team-npm-scopes.ts +77 -0
  293. package/server-managed/team-reports.ts +228 -0
  294. package/server-managed/triage-response.ts +16 -0
  295. package/server-managed/type-only-worker.js +19 -0
  296. package/server-managed/type-only.ts +103 -0
  297. package/server-managed/uploads.ts +86 -0
  298. package/server-managed/upstream-cache.ts +215 -0
  299. package/server-managed/view-as.ts +58 -0
  300. package/server-managed/workspace-shares.ts +166 -0
  301. package/server.ts +50 -0
  302. package/server/index.ts +0 -477
  303. package/server/lifecycle.ts +0 -204
  304. package/server/sync-handlers.ts +0 -320
  305. /package/{LICENSE → LICENSE-MIT} +0 -0
  306. /package/{server → server-e2e}/config.example.json +0 -0
  307. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  308. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  309. /package/{server → server-e2e}/peer.ts +0 -0
  310. /package/{server → server-e2e}/util.ts +0 -0
  311. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,124 @@
1
+ // Where npm installs a dependency from, read off its spec as npm-package-arg
2
+ // reads one: the registry (a version, a range, a tag, an `npm:` alias), a
3
+ // path, a repository on a host npm knows, a git URL or a tarball's.
4
+
5
+ // The hosts npm takes a repository on (hosted-git-info, which npm-package-arg
6
+ // reads a spec with): by name (`github:owner/repo`), or by its domain over
7
+ // the protocols each takes, where its path names a repository, `[user,
8
+ // project, committish]`, and not a tarball or a file.
9
+ const trim = project => project?.replace(/\.git$/u, '')
10
+ const GIT_HOSTS = new Map([
11
+ ['github', { label: 'GitHub', domain: 'github.com', protocols: ['git:', 'http:', 'git+ssh:', 'git+https:', 'ssh:', 'https:'], repo({ pathname, hash }) {
12
+ const [, user, project, type, committish] = pathname.split('/', 5)
13
+ return (!type || type === 'tree') && user && trim(project) ? [user, trim(project), type ? committish : hash.slice(1)] : null
14
+ } }],
15
+ ['bitbucket', { label: 'Bitbucket', domain: 'bitbucket.org', protocols: ['git+ssh:', 'git+https:', 'ssh:', 'https:'], repo({ pathname, hash }) {
16
+ const [, user, project, aux] = pathname.split('/', 4)
17
+ return aux !== 'get' && user && trim(project) ? [user, trim(project), hash.slice(1)] : null
18
+ } }],
19
+ ['gitlab', { label: 'GitLab', domain: 'gitlab.com', protocols: ['git+ssh:', 'git+https:', 'ssh:', 'https:'], repo({ pathname, hash }) {
20
+ const path = pathname.slice(1)
21
+ const at = path.lastIndexOf('/')
22
+ if (path.includes('/-/') || path.includes('/archive.tar.gz')) return null
23
+ return path.slice(0, Math.max(at, 0)) && trim(path.slice(at + 1)) ? [path.slice(0, at), trim(path.slice(at + 1)), hash.slice(1)] : null
24
+ } }],
25
+ ['gist', { label: 'Gist', domain: 'gist.github.com', protocols: ['git:', 'git+ssh:', 'git+https:', 'ssh:', 'https:'], repo({ pathname, hash }) {
26
+ const [, user, project, aux] = pathname.split('/', 4)
27
+ return aux !== 'raw' && (user || project) ? [project ? user : null, trim(project || user), hash.slice(1)] : null
28
+ } }],
29
+ ['sourcehut', { label: 'SourceHut', domain: 'git.sr.ht', protocols: ['git+ssh:', 'https:'], repo({ pathname, hash }) {
30
+ const [, user, project, aux] = pathname.split('/', 4)
31
+ return aux !== 'archive' && user && trim(project) ? [user, trim(project), hash.slice(1)] : null
32
+ } }],
33
+ ])
34
+ const BY_DOMAIN = new Map([...GIT_HOSTS.values()].map(host => [host.domain, host]))
35
+ // The protocols hosted-git-info reads a URL by as it is, the hosts' names too.
36
+ const PROTOCOLS = new Set(['git+ssh:', 'ssh:', 'git+https:', 'git:', 'http:', 'https:', 'git+http:', ...[...GIT_HOSTS.keys()].map(name => `${name}:`)])
37
+
38
+ // GitHub's `owner/repo` shorthand, before a commit, branch or tag
39
+ // (`#v1.0.0`) or not, as hosted-git-info tells it: a slash, not first, nor
40
+ // last before any `#`; no `.` first; and no space, `@`, `:` or second slash
41
+ // before any `#`.
42
+ function githubShorthand(spec) {
43
+ const hash = spec.indexOf('#'), slash = spec.indexOf('/')
44
+ const beforeHash = at => at > -1 && !(hash > -1 && at > hash)
45
+ return slash > 0 && !spec.startsWith('.') && (hash > -1 ? spec[hash - 1] !== '/' : !spec.endsWith('/'))
46
+ && ![/\s/u.exec(spec)?.index ?? -1, spec.indexOf('@'), spec.indexOf(':'), spec.indexOf('/', slash + 1)].some(beforeHash)
47
+ }
48
+
49
+ // A URL as hosted-git-info parses one: a protocol it doesn't know put
50
+ // before a `git@host:path` (as `git+ssh://`) or given its `//`, and else
51
+ // `host:path` read as `host/path`.
52
+ const safeUrl = text => { try { return new URL(text) } catch { return null } }
53
+ function hostedUrl(spec) {
54
+ const at = spec.indexOf('@'), colon = spec.indexOf(':')
55
+ const corrected = PROTOCOLS.has(spec.slice(0, colon + 1)) || at > -1 && at < colon ? spec : at > -1 ? `git+ssh://${spec}`
56
+ : spec.indexOf('//') === colon + 1 ? spec : `${spec.slice(0, colon + 1)}//${spec.slice(colon + 1)}`
57
+ if (safeUrl(corrected)) return safeUrl(corrected)
58
+ const before = char => { const hash = corrected.indexOf('#'); return corrected.lastIndexOf(char, hash > -1 ? hash : Infinity) }
59
+ let scp = before(':') > before('@') ? `${corrected.slice(0, before(':'))}/${corrected.slice(before(':') + 1)}` : corrected
60
+ if (scp.lastIndexOf(':', scp.indexOf('#') > -1 ? scp.indexOf('#') : Infinity) === -1 && !scp.includes('//')) scp = `git+ssh://${scp}`
61
+ return safeUrl(scp)
62
+ }
63
+
64
+ // The host npm takes a spec's repository on (hosted-git-info's fromUrl), or
65
+ // null: one by its name, whatever its path, or by its domain over a protocol
66
+ // it takes, where its path names a repository; none whose parts don't
67
+ // decode.
68
+ function gitHost(spec) {
69
+ if (!spec) return null
70
+ const url = hostedUrl(githubShorthand(spec) ? `github:${spec}` : spec)
71
+ if (!url) return null
72
+ const named = GIT_HOSTS.get(url.protocol.slice(0, -1))
73
+ const host = named ?? BY_DOMAIN.get(url.hostname.replace(/^www\./u, ''))
74
+ const parts = named ? [url.pathname.replace(/^\/?[^@]*@|^\//u, ''), url.hash.slice(1)] : host?.protocols.includes(url.protocol) && host.repo(url)
75
+ try { return parts ? (parts.forEach(part => part && decodeURIComponent(part)), host) : null } catch { return null }
76
+ }
77
+
78
+ // Where npm installs a dependency's spec from, read once in
79
+ // npm-package-arg's resolve order: `path` for a path (`./x`, `~/x`, `/x`,
80
+ // `c:/x`, `file:`); `registry` for an `npm:` alias of a registry package,
81
+ // or a spec with no `/` or `:` that names no tarball, which no later step
82
+ // takes; a repository on a host npm knows (gitHost), named for it
83
+ // (`GitHub`); for a URL, `git` for a repository's (`git:`, `git+https:`,
84
+ // `git+ssh:`, `git+http:`, `git+rsync:`, `git+ftp:`, `git+file:`, and
85
+ // `git+ssh://` to `user@host:path`), `URL` for a tarball's (`https:`,
86
+ // `http:`), and null for one npm doesn't install (`workspace:`, `ssh:` on
87
+ // another host); then `path` again for one with a `/` or a tarball's name
88
+ // (`a/b/c`, `x.tgz`, `git@example.com:x/y`), and else `registry`: a
89
+ // version, a range or a tag.
90
+ function specFrom(spec) {
91
+ const value = String(spec ?? '').trim()
92
+ if (/^(?:[.]|~\/|\/|[a-z]:|file:)/iu.test(value)) return 'path'
93
+ if (/^npm:/iu.test(value) || !/[/:]|[.](?:tgz|tar\.gz|tar)$/iu.test(value)) return 'registry'
94
+ const host = gitHost(value)
95
+ if (host) return host.label
96
+ if (/^(?:git\+)?[a-z]+:/iu.test(value)) {
97
+ const scp = /^git\+ssh:\/\/([^:#]+:[^#]+(?:\.git)?)(?:#.*)?$/iu.exec(value)
98
+ if (value.startsWith('git+ssh:') && scp && !/:\d+(?:\/|$)/u.test(scp[1])) return 'git'
99
+ const protocol = safeUrl(value)?.protocol
100
+ return /^(?:git|git\+(?:https?|rsync|ftp|file|ssh)):$/u.test(protocol) ? 'git' : /^https?:$/u.test(protocol) ? 'URL' : null
101
+ }
102
+ return /\/|[.](?:tgz|tar\.gz|tar)$/iu.test(value) ? 'path' : 'registry'
103
+ }
104
+
105
+ // Where a dependency's spec has npm fetch it from, where that isn't its
106
+ // registry (specFrom): a host's name (`GitHub`), `git` or `URL`. Null for
107
+ // the registry's ranges and tags, a path, and what npm doesn't install.
108
+ export function npmDependencySource(spec) {
109
+ const from = specFrom(spec)
110
+ return from === 'registry' || from === 'path' ? null : from
111
+ }
112
+
113
+ // The package and spec an `npm:` alias names (`npm:pkg@^1.2.3`), as
114
+ // specFrom reads one; null for a spec that isn't one.
115
+ export function npmAlias(spec) {
116
+ const [, name, range = ''] = /^npm:((?:@[^/@]+\/)?[^/@]+)(?:@(.*))?$/iu.exec(String(spec ?? '').trim()) ?? []
117
+ return name ? { name, spec: range } : null
118
+ }
119
+
120
+ // Whether npm installs a dependency's spec from its registry (specFrom):
121
+ // not from a path, a repository or a URL, nor a scheme npm doesn't install
122
+ // (`workspace:`), the registry's package of that name being what npm
123
+ // installs for none of those.
124
+ export const npmRegistrySpec = spec => specFrom(spec) === 'registry'
@@ -0,0 +1,113 @@
1
+ // A package's files from its tarball, as npm installs them: gunzipped, read as
2
+ // a tar archive (ustar, pax and GNU long names), each file by the path npm
3
+ // installs it at, its first component dropped, none escaping the package; a
4
+ // text where it is UTF-8 with no NUL, else null. Held to the npm viewer's
5
+ // bounds (server-managed/npm-packages.ts readNpmTar).
6
+ const MAX_PACKAGE_FILES = 20_000
7
+ export const MAX_PACKAGE_BYTES = 64 * 1024 * 1024
8
+ // What its tar stream may unpack to, its headers and padding beside its
9
+ // files, as npm-loads.ts bounds it (MAX_TAR_BYTES).
10
+ export const MAX_TAR_BYTES = 96 * 1024 * 1024
11
+
12
+ const latin1 = new TextDecoder('latin1')
13
+ const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true })
14
+ const utf8Lenient = bytes => new TextDecoder().decode(bytes)
15
+ // A header's text field, to its first NUL.
16
+ const textOf = (bytes, start, length) => {
17
+ const field = bytes.subarray(start, start + length)
18
+ const end = field.indexOf(0)
19
+ return utf8Lenient(end === -1 ? field : field.subarray(0, end))
20
+ }
21
+
22
+ // A header's size field: octal, or GNU base-256 past its 11 digits.
23
+ function sizeOf(bytes, offset) {
24
+ if (bytes[offset] & 0x80) {
25
+ let size = bytes[offset] & 0x7f
26
+ for (let i = 1; i < 12; i++) size = size * 256 + bytes[offset + i]
27
+ return size
28
+ }
29
+ const text = textOf(bytes, offset, 12).trim()
30
+ return /^[0-7]+$/u.test(text) ? Number.parseInt(text, 8) : Number.NaN
31
+ }
32
+
33
+ // A pax header's path: records `<length> <key>=<value>\n`, their lengths in
34
+ // bytes.
35
+ function paxPath(data) {
36
+ let at = 0, path = null
37
+ while (at < data.length) {
38
+ const space = data.indexOf(0x20, at)
39
+ const length = Number.parseInt(latin1.decode(data.subarray(at, space)), 10)
40
+ if (space === -1 || !Number.isSafeInteger(length) || length <= 0) break
41
+ const record = utf8Lenient(data.subarray(space + 1, at + length - 1))
42
+ if (record.startsWith('path=')) path = record.slice(5)
43
+ at += length
44
+ }
45
+ return path
46
+ }
47
+
48
+ const packagePath = raw => {
49
+ const parts = raw.split('/').slice(1).filter(part => part !== '' && part !== '.')
50
+ return parts.length === 0 || parts.includes('..') ? null : parts.join('/')
51
+ }
52
+
53
+ // The files of a tar archive, by path: their bytes.
54
+ function npmTarFiles(tar) {
55
+ const files = new Map()
56
+ let longName = null, offset = 0, total = 0
57
+ while (offset + 512 <= tar.length) {
58
+ const header = tar.subarray(offset, offset + 512)
59
+ if (header.every(byte => byte === 0)) break
60
+ const size = sizeOf(header, 124), start = offset + 512, type = String.fromCodePoint(header[156])
61
+ if (!Number.isSafeInteger(size) || size < 0 || start + size > tar.length) throw new Error('bad-tarball')
62
+ const data = tar.subarray(start, start + size)
63
+ offset = start + Math.ceil(size / 512) * 512
64
+ if (type === 'x' || type === 'L') { longName = type === 'x' ? paxPath(data) : textOf(data, 0, data.length); continue }
65
+ if (type === 'g' || type === 'K') continue
66
+ const prefix = textOf(header, 257, 6) === 'ustar' ? textOf(header, 345, 155) : ''
67
+ const name = longName ?? (prefix ? `${prefix}/${textOf(header, 0, 100)}` : textOf(header, 0, 100))
68
+ longName = null
69
+ const path = ['0', '\0', '7'].includes(type) ? packagePath(name) : null
70
+ if (path === null) continue
71
+ total += size - (files.get(path)?.length ?? 0)
72
+ files.set(path, data)
73
+ if (files.size > MAX_PACKAGE_FILES || total > MAX_PACKAGE_BYTES) throw new Error('package-too-large')
74
+ }
75
+ return files
76
+ }
77
+
78
+ // A file's text: UTF-8 with no NUL, else null.
79
+ export function npmFileText(bytes) {
80
+ if (bytes.includes(0)) return null
81
+ try { return utf8.decode(bytes) } catch { return null }
82
+ }
83
+
84
+ // A gzipped tarball's tar stream, given up on as `package-too-large` once it
85
+ // unpacks past MAX_TAR_BYTES, before any more of it is held.
86
+ async function gunzip(gzipped) {
87
+ const reader = new Blob([gzipped]).stream().pipeThrough(new DecompressionStream('gzip')).getReader()
88
+ const chunks = []
89
+ let size = 0
90
+ for (let read = await reader.read(); !read.done; read = await reader.read()) {
91
+ size += read.value.byteLength
92
+ if (size > MAX_TAR_BYTES) { await reader.cancel(); throw new Error('package-too-large') }
93
+ chunks.push(read.value)
94
+ }
95
+ return new Uint8Array(await new Blob(chunks).arrayBuffer())
96
+ }
97
+
98
+ // A file's bytes' digest, as the managed server gives one of a file that
99
+ // isn't text (server-managed/npm-packages.ts).
100
+ const sha256 = async bytes => `sha256-${btoa(String.fromCodePoint(...new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))))}`
101
+
102
+ // A gzipped tarball's files as a version holds them for risk, as the npm
103
+ // viewer holds a version (ui/view/npm-package.js npmPackageDetails), a source
104
+ // map listing them (`kind: 'sourcemap'`): `json.sources` their paths, sorted,
105
+ // `json.sourcesContent` their texts, `fileSizes` by path, and `npmBinaries`,
106
+ // those that aren't text, `{ size, digest }` by path, which compare.js and
107
+ // diff.js tell them by.
108
+ export async function npmTarballVersion(gzipped) {
109
+ const files = [...npmTarFiles(await gunzip(gzipped))].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
110
+ const texts = files.map(([, bytes]) => npmFileText(bytes))
111
+ const binaries = await Promise.all(files.filter((_, at) => texts[at] === null).map(async ([path, bytes]) => [path, { size: bytes.length, digest: await sha256(bytes) }]))
112
+ return { kind: 'sourcemap', json: { version: 3, sources: files.map(([path]) => path), sourcesContent: texts }, fileSizes: new Map(files.map(([path, bytes]) => [path, bytes.length])), npmBinaries: new Map(binaries) }
113
+ }
@@ -0,0 +1,16 @@
1
+ import { env } from 'node:process'
2
+
3
+ // One shared URL or per-mode URLs. Validate before opening either store so
4
+ // an ambiguous configuration cannot send application traffic and GC elsewhere.
5
+ export function databaseUrls({ combined = false } = {}) {
6
+ const shared = env['DATABASE_URL'] || null
7
+ const e2e = env['E2E_DATABASE_URL'] || null
8
+ const managed = env['MANAGED_DATABASE_URL'] || null
9
+ if (shared && (e2e || managed)) {
10
+ throw new Error('DATABASE_URL cannot be combined with E2E_DATABASE_URL or MANAGED_DATABASE_URL. Use the shared URL alone, or only the per-mode URLs.')
11
+ }
12
+ if (combined && Boolean(e2e) !== Boolean(managed)) {
13
+ throw new Error('Combined mode requires both E2E_DATABASE_URL and MANAGED_DATABASE_URL, DATABASE_URL alone, or no database URLs. Mixing Neon and SQLite is not supported.')
14
+ }
15
+ return { e2e: shared ?? e2e, managed: shared ?? managed }
16
+ }
@@ -0,0 +1,18 @@
1
+ // Register resources as they are acquired. Failed assembly rolls them back in
2
+ // reverse order; successful assembly transfers ownership to the returned app.
3
+ export async function initializeApp<T>(assemble: (rollback: AsyncDisposableStack) => T | Promise<T>): Promise<T> {
4
+ const rollback = new AsyncDisposableStack()
5
+ try {
6
+ const app = await assemble(rollback)
7
+ rollback.move()
8
+ return app
9
+ } catch (error) {
10
+ try { await rollback.disposeAsync() }
11
+ catch (cleanupError) {
12
+ // AsyncDisposableStack runs every cleanup even if one fails. Preserve
13
+ // both failures so cleanup errors cannot hide the initialization error.
14
+ throw new SuppressedError(cleanupError, error, 'App initialization and cleanup failed')
15
+ }
16
+ throw error
17
+ }
18
+ }
@@ -0,0 +1,101 @@
1
+ import { Buffer } from 'node:buffer'
2
+
3
+ const UPSTREAM_URL = 'https://registry.npmjs.org/-/npm/v1/security/advisories/bulk'
4
+ const RESPONSE_BODY_LIMIT = 4 * 1024 * 1024
5
+ export const NPM_ADVISORIES_TIMEOUT_MS = 30_000
6
+
7
+ export async function fetchNpmAdvisories(body: Buffer, signal: AbortSignal, debug = false): Promise<{ status: number; body: unknown }> {
8
+ let upstream: Response
9
+ try {
10
+ upstream = await fetch(UPSTREAM_URL, {
11
+ method: 'POST',
12
+ // Force JSON — the bulk endpoint requires it. Drop every
13
+ // client-supplied header to keep an upstream fingerprint from
14
+ // leaking through (cookies, auth, custom UA, ...). The
15
+ // registry's bulk endpoint doesn't need any of them for a
16
+ // public lookup.
17
+ headers: { 'content-type': 'application/json', 'accept': 'application/json' },
18
+ // Re-wrap as a plain Uint8Array — Buffer's underlying
19
+ // ArrayBufferLike type doesn't satisfy fetch's BodyInit
20
+ // narrowing (it can't statically rule out SharedArrayBuffer),
21
+ // but a copy through Uint8Array is zero-cost in practice and
22
+ // unambiguously typed.
23
+ body: new Uint8Array(body),
24
+ signal,
25
+ })
26
+ } catch (err: unknown) {
27
+ if (debug) console.warn('npm-advisories upstream error:', err)
28
+ return { status: 502, body: { error: 'upstream-unreachable' } }
29
+ }
30
+ // Assert JSON on the upstream body. The Content-Type header is
31
+ // unreliable (Cloudflare in front of registry.npmjs.org strips it
32
+ // from some responses; a captive portal / WAF can declare HTML on
33
+ // a body that's actually JSON or vice-versa), so we don't lean on
34
+ // it — instead we buffer the body and parse. A successful
35
+ // JSON.parse is the strongest guarantee we can hand the UI's
36
+ // `await res.json()`. Buffering is bounded by
37
+ // `RESPONSE_BODY_LIMIT`; the advisories endpoint's payloads sit
38
+ // well under that.
39
+ const upstreamContentType = upstream.headers.get('content-type') ?? ''
40
+ let buffered: Buffer | null
41
+ try {
42
+ buffered = await readUpstreamBody(upstream)
43
+ } catch (err: unknown) {
44
+ if (debug) console.warn('npm-advisories upstream body error:', err)
45
+ return { status: 502, body: { error: 'upstream-unreachable' } }
46
+ }
47
+ if (buffered === null) {
48
+ if (debug) console.warn(`npm-advisories upstream too large: status=${upstream.status}`)
49
+ return { status: 502, body: { error: 'upstream-too-large', upstreamStatus: upstream.status } }
50
+ }
51
+ // Treat the body as UTF-8 — `JSON.parse` operates on a string and
52
+ // the registry's responses are always UTF-8 in practice. A
53
+ // non-UTF-8 byte sequence still decodes (with U+FFFD
54
+ // substitution); the subsequent JSON.parse fails and routes
55
+ // through the error branch.
56
+ const text = buffered.toString('utf8')
57
+ let parsed: unknown
58
+ try {
59
+ parsed = JSON.parse(text)
60
+ } catch {
61
+ if (debug) console.warn(`npm-advisories upstream non-JSON: status=${upstream.status} ct=${upstreamContentType || '<none>'} bytes=${buffered.byteLength}`)
62
+ return { status: 502, body: {
63
+ error: 'upstream-not-json',
64
+ upstreamStatus: upstream.status,
65
+ upstreamContentType: upstreamContentType || null,
66
+ } }
67
+ }
68
+ return { status: upstream.status, body: parsed }
69
+ }
70
+
71
+ // Buffer the upstream response body up to RESPONSE_BODY_LIMIT.
72
+ // Returns null if the cap is exceeded (caller maps to a 502
73
+ // `upstream-too-large`), or the cumulative Buffer otherwise. A
74
+ // transport error mid-read (e.g. AbortSignal fired by the deadline
75
+ // timer or by `req` close) throws — the caller catches it.
76
+ //
77
+ // `finally { reader.cancel() }` is load-bearing on the error and
78
+ // cap-exceeded paths: leaving the reader locked to the body holds
79
+ // the underlying undici TCP socket out of the connection pool until
80
+ // GC, and the cap-exceeded path explicitly needs to tear the
81
+ // transfer down so we don't keep buffering bytes we'll never use.
82
+ // On the clean-drain path (done:true), cancel() is a no-op.
83
+ async function readUpstreamBody(upstream: Response): Promise<Buffer | null> {
84
+ if (!upstream.body) return Buffer.alloc(0)
85
+ const reader = upstream.body.getReader()
86
+ const chunks: Uint8Array[] = []
87
+ let received = 0
88
+ try {
89
+ for (;;) {
90
+ const { done, value } = await reader.read()
91
+ if (done) break
92
+ if (!value) continue
93
+ received += value.byteLength
94
+ if (received > RESPONSE_BODY_LIMIT) return null
95
+ chunks.push(value)
96
+ }
97
+ return Buffer.concat(chunks)
98
+ } finally {
99
+ try { await reader.cancel() } catch {}
100
+ }
101
+ }
@@ -1,8 +1,8 @@
1
- // Same-origin gate for the WS upgrade and REST data plane. We don't
2
- // support cross-origin browser clients, so any Origin header present
3
- // on an incoming request MUST match the server's own host (derived
4
- // from `req.headers.host`, or from `X-Forwarded-Host` /
5
- // `X-Forwarded-Proto` when a trusted reverse proxy is in front).
1
+ // Same-origin gate, shared by the e2e and managed servers (server-common).
2
+ // We don't support cross-origin browser clients, so any Origin header present
3
+ // on an incoming request MUST match the server's own host (derived from
4
+ // `req.headers.host`, or from `X-Forwarded-Host` / `X-Forwarded-Proto` when a
5
+ // trusted reverse proxy is in front).
6
6
  //
7
7
  // Why "present-must-match" rather than "always required":
8
8
  // - Browser WebSocket handshakes always carry Origin (RFC 6455), so a
@@ -0,0 +1,48 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
2
+ import type { IncomingMessage, ServerResponse } from 'node:http'
3
+
4
+ type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
5
+ type Reapers = Record<string, () => Promise<unknown>>
6
+ type Options = { secret?: string; isShuttingDown?: () => boolean }
7
+
8
+ // Start every cleanup and wait for all of them, including when one fails.
9
+ // The caller must not close storage or return success with work still running.
10
+ export async function runReapers(reapers: Reapers): Promise<void> {
11
+ const results = await Promise.allSettled(Object.values(reapers).map(reap => Promise.resolve().then(reap)))
12
+ const errors = results.filter(result => result.status === 'rejected').map(result => result.reason)
13
+ if (errors.length > 0) throw new AggregateError(errors, 'Cleanup failed')
14
+ }
15
+
16
+ export function createReapHandler(reapers: Reapers, { secret = process.env['CRON_SECRET'], isShuttingDown = () => false }: Options = {}) {
17
+ const key = randomBytes(32)
18
+ const hash = (value: string) => createHmac('sha256', key).update(value).digest()
19
+ const expected = secret ? hash(`Bearer ${secret}`) : null
20
+ return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
21
+ const send = (status: number, body: object, headers = {}) => {
22
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store', ...headers })
23
+ res.end(JSON.stringify(body))
24
+ }
25
+ const authorization = req.headers.authorization
26
+ if (req.headers['x-deepview-share'] !== undefined) { send(403, { error: 'share-scope-required' }); return }
27
+ if (!expected || typeof authorization !== 'string' || !timingSafeEqual(hash(authorization), expected)) {
28
+ send(401, { error: 'unauthorized' }); return
29
+ }
30
+ if (req.method !== 'GET') { send(405, { error: 'method-not-allowed' }, { allow: 'GET' }); return }
31
+ if (isShuttingDown()) { send(503, { error: 'shutting-down' }); return }
32
+ const startedAt = Date.now()
33
+ try {
34
+ await runReapers(reapers)
35
+ send(200, { ok: true, reaped: Object.keys(reapers), ms: Date.now() - startedAt })
36
+ } catch (err) {
37
+ console.error('reap failed:', err)
38
+ send(500, { error: 'reap-failed' })
39
+ }
40
+ }
41
+ }
42
+
43
+ // Only top-level servers compose this route. Domain routers stay unaware of
44
+ // other modes, and cleanup uses the stores those servers have already opened.
45
+ export function withReap(next: Handler, reapers: Reapers, options: Options = {}): Handler {
46
+ const reap = createReapHandler(reapers, options)
47
+ return (req, res) => req.url?.split('?', 1)[0] === '/api/reap' ? reap(req, res) : next(req, res)
48
+ }
@@ -0,0 +1,19 @@
1
+ import { normalizeScanServer } from '../common/scan-server.ts'
2
+
3
+ export function configuredScanServer(value: string | undefined): string | null {
4
+ if (!value?.trim()) return null
5
+ const server = normalizeScanServer(value)
6
+ if (!server) throw new Error('DEEPVIEW_SCAN_SERVER must be an absolute HTTP(S) URL without credentials, query, or fragment')
7
+ return server
8
+ }
9
+
10
+ // Apply at serve time, before compression and ETag generation. Packaged HTML
11
+ // stays strict and does not bake in the build machine's configuration.
12
+ export function scanServerHtml(html: string, server: string | null, { advertise = false } = {}): string {
13
+ if (!server) return html
14
+ const origin = new URL(server).origin
15
+ const result = html.replace(/(connect-src 'self')(?=[;"])/u, `$1 ${origin}`)
16
+ if (!advertise) return result
17
+ const content = server.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;')
18
+ return result.replace('</head>', `<meta name="deepview-scan-server" content="${content}">\n</head>`)
19
+ }
@@ -0,0 +1,29 @@
1
+ import type { Server } from 'node:http'
2
+
3
+ // Only CLI entry points own process signals and termination. Embedded servers
4
+ // expose async disposal and error events to their host instead.
5
+ export function startServer(server: Server, config: { port: number; host: string }): void {
6
+ let shuttingDown = false
7
+ let exitCode = 0
8
+ function shutdown(code: number): void {
9
+ if (code !== 0) exitCode = code
10
+ if (shuttingDown) return
11
+ shuttingDown = true
12
+ console.log('Shutting down…')
13
+ void server[Symbol.asyncDispose]().catch(err => {
14
+ console.error('Shutdown failed:', err)
15
+ exitCode = 1
16
+ }).finally(() => process.exit(exitCode))
17
+ }
18
+ function onSignal(): void { shutdown(0) }
19
+ function onError(err: unknown): void {
20
+ console.error('Server error:', err)
21
+ shutdown(1)
22
+ }
23
+ server.on('error', onError)
24
+ process.on('SIGINT', onSignal)
25
+ process.on('SIGTERM', onSignal)
26
+ process.on('unhandledRejection', onError)
27
+ process.on('uncaughtException', onError)
28
+ server.listen(config.port, config.host)
29
+ }