@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
package/server/index.ts DELETED
@@ -1,480 +0,0 @@
1
- #!/usr/bin/env node
2
- // DeepView triage-sync relay server. WebSocket front-end, SQLite
3
- // backing store. Implements the protocol described in
4
- // `client/triage-sync.js` (and `server/sign.ts` for the canonical
5
- // signature payloads):
6
- //
7
- // server → client challenge { nonce } — emitted on every
8
- // accept, BEFORE any client
9
- // frame; per-socket random
10
- // 128-bit value the client
11
- // must bind into every
12
- // `workspace-subscribe`
13
- // signature (round-9 H2)
14
- // client → server workspace-save { workspaceTag, base,
15
- // keyframe, nonce, ciphertext,
16
- // signature } — `keyframe` is
17
- // a boolean (`true` exactly,
18
- // else falsy), bound into the
19
- // signed canonical
20
- // client → server workspace-subscribe { workspaceTag, from,
21
- // signature } — `from` is the
22
- // last revision id the client
23
- // claims to have applied (or
24
- // null for fresh)
25
- // client → server ping — heartbeat
26
- // server → client pong — heartbeat reply
27
- // server → client workspace-save-ack { workspaceTag, base, id }
28
- // server → client workspace-save-error { workspaceTag, base, reason }
29
- // — explicit failure surface for
30
- // the legit-signer case where
31
- // the server rejects a signed
32
- // save (e.g. `too-large` past
33
- // MAX_CIPHERTEXT_LEN). Sent
34
- // AFTER sig verify so the
35
- // response only reaches a
36
- // legitimate seed holder; shape
37
- // attacks still drop silently.
38
- // server → client workspace-subscribed { workspaceTag } — explicit
39
- // handshake-complete ack so
40
- // the client can flip its
41
- // status from `connecting` to
42
- // `online` only after the
43
- // server registered it as a
44
- // peer (not just on socket
45
- // open)
46
- // server → client workspace-state { workspaceTag, revisions:
47
- // [{ base, id, keyframe,
48
- // nonce, ciphertext,
49
- // signature }, ...] }
50
- //
51
- // Authentication: every signed message is checked against the
52
- // `workspaceTag` (= base64url Ed25519 public key) before any
53
- // state mutation. Unsigned / bad-sig messages are dropped silently
54
- // — the legitimate signer will retry, and an attacker who learns
55
- // the tag without holding the seed can't get past the verify.
56
- //
57
- // Content opacity: `nonce` and `ciphertext` are opaque to the
58
- // server. We store and forward them; we never inspect.
59
- //
60
- // Subscriber tracking: `subscribers: Map<workspaceTag, Set<socket>>`.
61
- // A socket joins the set ONLY via an explicit, signature-verified
62
- // `workspace-subscribe` (sole call site of `subscribe()` is in
63
- // `handleSubscribe`, gated by the per-connection challenge nonce
64
- // bound into the signature and a post-await `readyState` check).
65
- // It leaves on disconnect. Broadcasts go to every subscriber for
66
- // the workspaceTag except the originator.
67
- //
68
- // `workspace-save` deliberately does NOT auto-attach the sender,
69
- // even on a valid signature — see the audit note in `handleSave`
70
- // (round-9 H1): auto-subscribe-on-save let a passive observer
71
- // replay any captured save frame from any TCP connection to attach
72
- // as a silent mirror, since the duplicate-id path returns ack-only
73
- // and would not reject the attaching socket.
74
-
75
- import { type WebSocket, WebSocketServer } from 'ws'
76
- import { errMsg, errStack } from './util.ts'
77
- import type { PeerRegistry } from './peer.ts'
78
- import { LOOPBACK_HOSTS, createOriginGate } from './origin.ts'
79
- import { createHub } from './hub.ts'
80
- import { createAuth } from './auth.ts'
81
- import { createSyncHandlers } from './sync-handlers.ts'
82
- import { WS_UPGRADE_PATH, createHttpServer } from './http.ts'
83
- import { installWsServer } from './ws-server.ts'
84
- import { SSE_OPEN_PATH, installSseServer } from './sse-server.ts'
85
- import { createLifecycle } from './lifecycle.ts'
86
- import { loadConfig } from './config.ts'
87
- import { type Handle, openDb } from './db.ts'
88
- import { openNeonDb } from './db-neon.ts'
89
- import { initObjstore } from './objstore/init.ts'
90
- import { type Handle as ObjstoreHandle, listLive, objectMetaWire, openObjstore } from './objstore/store.ts'
91
- import { openNeonObjstore } from './objstore/store-neon.ts'
92
- import { openVercelBlobBackend } from './objstore/blob-vercel.ts'
93
- import {
94
- type NeonClientCtor, type PubSub,
95
- createNeonPubSub, createNoopPubSub,
96
- } from './pubsub.ts'
97
- import { createBusReceiver } from './bus-receiver.ts'
98
-
99
- // All external inputs (env vars + optional config.json) are parsed
100
- // and validated in ./config.ts; destructure into the uppercase names
101
- // the rest of this module uses.
102
- const config = loadConfig()
103
- const {
104
- port: PORT, host: HOST, dbPath: DB_PATH, objstoreDir: OBJSTORE_DIR,
105
- reapIntervalMs: OBJSTORE_REAP_INTERVAL_MS, reapDisabled: OBJSTORE_REAP_DISABLED,
106
- maxInflightPerSocket: MAX_INFLIGHT_PER_SOCKET, debug: DEBUG,
107
- neonUrl: NEON_URL, blobToken: BLOB_TOKEN, tokenSecret: TOKEN_SECRET,
108
- password: CONFIG_PASSWORD, trustProxyEnv: TRUST_PROXY_ENV,
109
- } = config
110
-
111
- // Same-origin gate for the WS upgrade and REST data plane (see
112
- // ./origin.ts). `TRUST_PROXY_ENV` also feeds the boot-time
113
- // misconfiguration fail-fast below.
114
- const { trustProxy: TRUST_PROXY, isOriginAllowed } = createOriginGate(HOST, TRUST_PROXY_ENV)
115
-
116
- // Per-socket buffered-bytes cap. `socket.send` returns synchronously
117
- // even when the kernel/ws library can't drain to the wire fast
118
- // enough; the unsent payload accumulates in `bufferedAmount`. A
119
- // slow / blackholed peer on a high-volume workspace can hold many
120
- // MB of fan-out broadcasts in this buffer with no backpressure on
121
- // the broadcast loop. Drop the message when the buffer crosses the
122
- // cap; the heartbeat will eventually close a peer that never
123
- // drains. Transport audit `server/index.ts:225`.
124
- const MAX_BUFFERED_BYTES = 16 * 1024 * 1024
125
- // Per-socket in-flight async-handler cap (MAX_INFLIGHT_PER_SOCKET,
126
- // env-validated in config). Each inbound text frame spawns a
127
- // `track(handler)` IIFE; an authorised peer firing valid frames could
128
- // otherwise grow the set without bound, stretching SIGTERM drain time.
129
- // Saves dropped at the cap surface as a typed `busy` NACK. Transport
130
- // audit `server/index.ts:590`.
131
-
132
- // Per-connection state registry. One `Peer` per accepted socket holds
133
- // the challenge nonce, auth flag, heartbeat liveness, in-flight count,
134
- // and subscribed tags (see ./peer.ts). The connection handler holds
135
- // the Peer in a closure for the hot paths; cross-function call sites
136
- // resolve it via `peers.get(socket)`.
137
- const peers: PeerRegistry = new WeakMap()
138
-
139
- // REST PUT idle-body timeout. A slow-loris client trickling bytes
140
- // within the declared Content-Length holds the staging fd and an
141
- // inFlightSids slot until the global staging TTL reaps it. Aborting
142
- // the per-chunk-idle period closes that window. Transport audit
143
- // `server/objstore/rest.ts:218`.
144
- const REST_PUT_IDLE_TIMEOUT_MS = 30_000
145
-
146
- // Server-driven WS heartbeat. Every `HEARTBEAT_INTERVAL_MS` we walk
147
- // `wss.clients`, terminate anyone who didn't pong since the last
148
- // tick, and ping the rest. The client-initiated `{type:'ping'}` /
149
- // `{type:'pong'}` JSON heartbeat the protocol already had only
150
- // catches the case where the CLIENT notices the socket's gone — it
151
- // can't recover an FD when the client itself has wandered off
152
- // (battery-killed background tab, mid-transfer NAT timeout, a
153
- // hostile non-browser client that opens the socket and never
154
- // speaks again). The same-origin upgrade gate allows missing
155
- // Origin headers through (legitimate non-browser callers), so a
156
- // hostile CLI can stack arbitrarily many idle sockets without it.
157
- // Kernel TCP keepalive is hours by default; without this interval
158
- // each abandoned socket pins its `wss.clients` Set entry, its `Peer`
159
- // state, and an FD until the kernel reclaims it. Two ticks max
160
- // from silence to termination, so the longest a dead socket
161
- // survives is ~2 × HEARTBEAT_INTERVAL_MS.
162
- const HEARTBEAT_INTERVAL_MS = 30_000
163
-
164
- // Backend selection. Both planes (workspace_revision DB + the
165
- // v1.objstore byte store) are picked from config at boot. Two
166
- // supported pairings:
167
- // 1. DATABASE_URL set → Neon (workspace_revision + objstore
168
- // tables) + Vercel Blob Private Storage (bytes). Requires
169
- // BLOB_READ_WRITE_TOKEN — fail fast at boot if missing, since
170
- // a local-FS byte plane can't back a multi-replica deployment
171
- // (one replica's writes wouldn't be visible to another).
172
- // 2. DATABASE_URL absent → SQLite + local FS bytes. Single-
173
- // process; the only pairing the SQLite plane supports.
174
- // The Neon / Vercel files import their peer deps lazily inside the
175
- // open functions, so static imports here are safe even on a SQLite-
176
- // only install where the optional peer deps aren't present. Explicit
177
- // branch (not a ternary) so the SQLite path keeps its `SqliteHandle`
178
- // narrowing — `sqliteHandle.db` is a non-optional `DatabaseSync` that
179
- // `openObjstore` accepts without a non-null assertion.
180
- let handle: Handle
181
- let objstoreHandle: ObjstoreHandle
182
- let objstoreBanner: string
183
- if (NEON_URL) {
184
- if (!BLOB_TOKEN) {
185
- console.error('DATABASE_URL is set but BLOB_READ_WRITE_TOKEN is not.')
186
- console.error('The Neon DB plane requires the Vercel Blob byte plane (local-FS bytes cannot back a multi-replica deployment).')
187
- console.error('Set BLOB_READ_WRITE_TOKEN to your Vercel Blob R/W token, or unset DATABASE_URL to fall back to SQLite + local FS.')
188
- process.exit(1)
189
- }
190
- if (!TOKEN_SECRET) {
191
- console.error('DATABASE_URL is set but OBJSTORE_TOKEN_SECRET is not.')
192
- console.error('Multi-replica deployments need a shared HMAC secret so REST bearer tokens minted on one replica validate on any other.')
193
- console.error('Generate one with: node -e \'console.log(require("crypto").randomBytes(32).toString("base64"))\'')
194
- process.exit(1)
195
- }
196
- handle = await openNeonDb(NEON_URL)
197
- const blob = await openVercelBlobBackend({ token: BLOB_TOKEN })
198
- objstoreHandle = await openNeonObjstore(NEON_URL, blob)
199
- objstoreBanner = 'objstore: vercel-blob (private)'
200
- } else {
201
- const sqliteHandle = openDb(DB_PATH)
202
- handle = sqliteHandle
203
- objstoreHandle = openObjstore(sqliteHandle.db, OBJSTORE_DIR)
204
- objstoreBanner = `objstore: ${OBJSTORE_DIR}`
205
- }
206
- // Multi-replica deployments behind a load balancer / TLS terminator
207
- // (the typical Vercel + Neon shape) need TRUST_PROXY=1 to honour
208
- // X-Forwarded-Host when computing the same-origin gate's expected
209
- // origin. Otherwise the gate derives the origin from the internal
210
- // container hostname and rejects every browser request as a
211
- // cross-origin attempt — silently from the operator's perspective
212
- // until users report 403s. Fail fast (parallels the
213
- // BLOB_READ_WRITE_TOKEN / OBJSTORE_TOKEN_SECRET checks above) so
214
- // a misconfigured deploy doesn't ship a 100%-403 fleet. An
215
- // operator who genuinely terminates TLS in the container without
216
- // X-Forwarded-* (rare) can set `TRUST_PROXY=0` to acknowledge.
217
- if (NEON_URL && !TRUST_PROXY && !LOOPBACK_HOSTS.has(HOST) && TRUST_PROXY_ENV !== '0' && TRUST_PROXY_ENV !== 'false') {
218
- console.error(`DATABASE_URL is set and HOST=${HOST} is not loopback, but TRUST_PROXY is not enabled.`)
219
- console.error('Browser requests through a load balancer / TLS terminator will be rejected by the same-origin gate (all 403).')
220
- console.error('Set TRUST_PROXY=1 to honour X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy.')
221
- console.error('Set TRUST_PROXY=0 if you really terminate TLS in the container without X-Forwarded-* headers (no proxy).')
222
- process.exit(1)
223
- }
224
-
225
- // "Workspace exists on the server" gate. The auth requirement only
226
- // kicks in for the FIRST action against a never-before-seen tag —
227
- // once any row lands (triage revision or objstore object), the
228
- // workspace is considered established and signature-gated. Checks
229
- // both planes so the gate stays consistent regardless of which
230
- // action the user picks first (workspace-save in the common case;
231
- // objstore-put-begin for a bundle-first flow).
232
- async function workspaceExists(tag: string): Promise<boolean> {
233
- if (await handle.headFor.get(tag)) return true
234
- const c = await objstoreHandle.countLive.get(tag)
235
- return (c?.c ?? 0) > 0
236
- }
237
-
238
- // WS fan-out hub: subscriber registry + backpressure-aware send /
239
- // broadcast (see ./hub.ts).
240
- const hub = createHub({ peers, maxBufferedBytes: MAX_BUFFERED_BYTES, debug: DEBUG })
241
- const { send, broadcast, subscribe, unsubscribeAll, broadcastLocalRaw } = hub
242
-
243
- // Cross-instance pub/sub for live broadcasts (see ./pubsub.ts). SQLite
244
- // mode is single-process by construction so it gets a no-op; Neon mode
245
- // uses Postgres LISTEN/NOTIFY on a dedicated long-lived Client
246
- // connection (separate from the HTTP `neon()` callable the queries flow
247
- // over — LISTEN needs a session-bound socket, the HTTP path is
248
- // stateless). The bus carries lookup hints, not the full wire payload:
249
- // the `workspace-state` broadcast's ciphertext alone can exceed the
250
- // ~8 KB NOTIFY payload cap, so the receiver re-fetches from the shared
251
- // DB to construct the wire frame. The bus is best-effort fan-out, not a
252
- // durability layer — the DB itself is the source of truth, and a
253
- // dropped publish only means peers on other instances miss the live
254
- // push (they still catch up via the chain on their next subscribe).
255
- let pubsub: PubSub = createNoopPubSub()
256
- if (NEON_URL) {
257
- // Dynamic import: the Client export lives in the same optional peer
258
- // dep as the HTTP `neon()` callable (see ./neon-driver.ts), but the
259
- // dep itself is only present on a Neon-mode deploy. The wrapper path
260
- // also lets tests swap in a PGlite-backed shim (`tests/pubsub.test.js`).
261
- const mod = (await import('./neon-driver.ts')) as unknown as { Client?: NeonClientCtor }
262
- if (!mod.Client) {
263
- console.error('DATABASE_URL is set but the @neondatabase/serverless Client export is not available.')
264
- console.error('Cross-instance broadcasts require the WebSocket-based Client (the HTTP `neon()` callable cannot LISTEN).')
265
- console.error('Reinstall the peer dep: pnpm add @neondatabase/serverless')
266
- process.exit(1)
267
- }
268
- const NeonClientImpl = mod.Client
269
- pubsub = createNeonPubSub({
270
- newClient: () => new NeonClientImpl(NEON_URL),
271
- debug: DEBUG,
272
- })
273
- }
274
-
275
- // Password gate (see ./auth.ts) — HMAC derivation + the `authenticate`
276
- // handshake.
277
- const auth = createAuth({ peers, password: CONFIG_PASSWORD, send, debug: DEBUG })
278
- const { requiresAuth, passwordConfigured, handleAuthenticate, sendUnauthorized } = auth
279
-
280
- // Triage-sync protocol handlers (see ./sync-handlers.ts). `getNonce`
281
- // resolves a socket's challenge nonce and is shared with the objstore
282
- // wiring below; `sendSaveError` is reused by the dispatcher's `busy`
283
- // inflight-cap NACK path.
284
- const getNonce = (socket: WebSocket): string | undefined => peers.get(socket)?.challenge
285
- const publishRevision = (tag: string, revisionId: string): void => {
286
- pubsub.publish({ kind: 'rev', tag, id: revisionId })
287
- }
288
- const publishObjPut = (tag: string, resourceTag: string): void => {
289
- pubsub.publish({ kind: 'objput', tag, res: resourceTag })
290
- }
291
- const publishObjDeleted = (tag: string, resourceTag: string, version: number): void => {
292
- pubsub.publish({ kind: 'objdel', tag, res: resourceTag, ver: version })
293
- }
294
-
295
- const { handleSave, handleSaveRest, handleSubscribe, sendSaveError } = createSyncHandlers({
296
- handle, send, broadcast, publishRevision, subscribe, getNonce,
297
- requiresAuth, passwordConfigured, sendUnauthorized, workspaceExists,
298
- // Folds the objstore inventory into the `workspace-subscribed` ack.
299
- // The objstore store keeps its own richer `Handle`, so we wire the
300
- // query here where both handles exist rather than coupling
301
- // sync-handlers to the store type.
302
- objstoreResources: async (tag) => (await listLive(objstoreHandle, tag)).map(objectMetaWire),
303
- debug: DEBUG,
304
- })
305
-
306
- const { handlers: objstore, restDeps: objstoreRestDeps, startupReap, stopReaper } = initObjstore({
307
- handle: objstoreHandle, reapIntervalMs: OBJSTORE_REAP_INTERVAL_MS,
308
- reapDisabled: OBJSTORE_REAP_DISABLED,
309
- send, broadcast, publishObjPut, publishObjDeleted,
310
- getNonce, debug: DEBUG,
311
- // Auth gate for the FIRST objstore-put-begin against a workspace
312
- // that doesn't yet exist on the server. Mirrors handleSave's gate
313
- // below; handlers.ts calls this AFTER sig verify so the
314
- // `unauthorized` frame only reaches a legitimate signer. Returns
315
- // `false` to allow, `true` to deny — handlers.ts emits the
316
- // `unauthorized` frame and bails on `true`.
317
- authGate: async (socket, tag) => requiresAuth(socket) && !await workspaceExists(tag),
318
- sendUnauthorized,
319
- // Socket-less analog of `authGate` for the REST put-begin mint: a REST
320
- // request can never be operator-authorized, so the gate collapses to
321
- // "password configured AND workspace new". A deny routes the client to
322
- // its in-band WS put-begin fallback.
323
- restPutGate: async (tag) => passwordConfigured && !await workspaceExists(tag),
324
- // `tokenSecret` is set only when OBJSTORE_TOKEN_SECRET was
325
- // provided in env (see TOKEN_SECRET resolution above). Omitted
326
- // → initObjstore mints a fresh per-process secret (fine for
327
- // single-replica).
328
- ...(TOKEN_SECRET ? { tokenSecret: TOKEN_SECRET } : {}),
329
- })
330
-
331
- // 4 MiB cap leaves headroom above MAX_CIPHERTEXT_LEN (2 MiB) for
332
- // the JSON envelope + base64 overhead. `ws` defaults to 100 MiB
333
- // which any unauthenticated peer could spam — every connection
334
- // accepts and JSON.parses up to that before the signature-fail drops
335
- // the frame.
336
- const wss = new WebSocketServer({ noServer: true, maxPayload: 4 * 1024 * 1024 })
337
-
338
- // Process lifecycle (see ./lifecycle.ts): `track` (in-flight request
339
- // drain) and `isShuttingDown` (the new-work gate) are consumed by the
340
- // HTTP + WS planes below; the teardown is `installLifecycle`d once
341
- // every server object exists.
342
- const { track, isShuttingDown, install: installLifecycle } = createLifecycle()
343
-
344
- // Shared per-connection dispatch surface. Both the WS plane
345
- // (installWsServer below) and the SSE+POST fallback (installSseServer
346
- // below) drive `setupPeerConnection` with this; one Peer per accepted
347
- // connection, one message-handler tree.
348
- const peerConnectionDeps = {
349
- peers, send, unsubscribeAll,
350
- handleSave, handleSubscribe, handleAuthenticate, sendSaveError, objstore,
351
- track, isShuttingDown,
352
- maxInflightPerSocket: MAX_INFLIGHT_PER_SOCKET,
353
- debug: DEBUG,
354
- }
355
-
356
- // SSE+POST fallback transport (see ./sse-server.ts). The WS upgrade is
357
- // the preferred path; clients fall back to SSE when `new WebSocket(…)`
358
- // fails to open (corporate proxies that strip the Upgrade header,
359
- // hostile network middleboxes, etc.). Same protocol on the wire — only
360
- // the byte transport differs.
361
- const sseServer = installSseServer({
362
- peerDeps: peerConnectionDeps, isShuttingDown,
363
- // Same cap shape as `wss` (no explicit per-process cap; OS FD limits
364
- // are the upstream bound). 1024 leaves room above the typical web
365
- // session count without being a soft DoS hammer.
366
- maxSessions: 1024,
367
- // Mirror the WS `maxPayload` so the SSE plane can't accept frames
368
- // the WS plane would reject.
369
- maxBodyBytes: 4 * 1024 * 1024,
370
- debug: DEBUG,
371
- })
372
-
373
- // HTTP plane: REST byte-transfer routing + SSE fallback + the WS
374
- // upgrade gate (see ./http.ts). Built after the lifecycle state above
375
- // because the REST shutdown gate reads `shuttingDown` and the request
376
- // drain uses `track`. The WS connection handler is wired on `wss`
377
- // below.
378
- const httpServer = createHttpServer({
379
- wss, restDeps: objstoreRestDeps, sseServer, isOriginAllowed,
380
- isShuttingDown, track, handleSaveRest,
381
- restPutIdleTimeoutMs: REST_PUT_IDLE_TIMEOUT_MS, debug: DEBUG,
382
- })
383
-
384
- // WS runtime: per-connection handler + message dispatch + heartbeat
385
- // sweep (see ./ws-server.ts). Returns the heartbeat timer so shutdown
386
- // can clear it.
387
- const { heartbeatTimer } = installWsServer({
388
- wss, heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
389
- ...peerConnectionDeps,
390
- })
391
-
392
- httpServer.on('listening', () => {
393
- // Read the actual bound port from `httpServer.address()` rather
394
- // than the `PORT` env constant. Operators (and the test harness)
395
- // can boot with `PORT=0` to get an OS-assigned ephemeral port;
396
- // the log line then carries the real bound number, not `0`.
397
- // Server-side bind failure took the error path above, so
398
- // `address()` is always a populated AddressInfo here.
399
- const addr = httpServer.address()
400
- const boundPort = typeof addr === 'object' && addr ? addr.port : PORT
401
- // Differentiate the storage banner by backend so the log line
402
- // doesn't claim a misleading DB_PATH under Neon, or a misleading
403
- // OBJSTORE_DIR under Vercel Blob.
404
- const dbBanner = NEON_URL ? 'db: neon-postgres' : `db: ${DB_PATH}`
405
- console.log(`DeepView triage-sync server: ws://${HOST}:${boundPort}${WS_UPGRADE_PATH} (sse fallback http://${HOST}:${boundPort}${SSE_OPEN_PATH}) http://${HOST}:${boundPort}/api/objstore/{workspaceTag}/{resourceTag} (${dbBanner}, ${objstoreBanner})`)
406
- })
407
-
408
- // Cross-instance bus receiver (see ./bus-receiver.ts): a remote NOTIFY
409
- // lands here, we re-fetch any data the bus payload only hinted at,
410
- // then broadcast to local peers via `broadcastLocalRaw`. Extracted so
411
- // the rev / objput / objdel mapping (including the keyframe boolean
412
- // coercion and the `objectMetaWire` shape) is testable without the
413
- // full server bring-up.
414
- const onBusMessage = createBusReceiver({
415
- handle, objstoreHandle, broadcastLocalRaw, debug: DEBUG,
416
- })
417
-
418
- // Kick off the LISTEN loop in the background. `pubsub.start` resolves
419
- // only after the FIRST successful connect, and the internal reconnect
420
- // loop retries indefinitely on transport / LISTEN failure — awaiting
421
- // it here would convert a transient Neon WS hiccup at boot into a
422
- // server that never binds (and a `.catch` that never runs). Instead
423
- // the server binds immediately and the bus comes up async; publishes
424
- // during the down window drop silently (best-effort semantics
425
- // documented in pubsub.ts). The SQLite no-op resolves immediately,
426
- // so the behaviour is identical in that mode.
427
- void pubsub.start(onBusMessage).catch((err) => {
428
- console.warn('pubsub: startup error:', errStack(err))
429
- })
430
-
431
- // App-specific shutdown step (run after the in-flight drain), wired
432
- // into the lifecycle teardown below.
433
- const closeDb = async (): Promise<void> => {
434
- // Stop the bus first so a publish from a still-draining handler
435
- // can't fire into a half-closed Client. The in-flight drain runs
436
- // before this (see lifecycle.ts), so by here all `broadcast` →
437
- // `publish*` calls are settled.
438
- try { await pubsub.stop() } catch (err) { console.warn('pubsub close error:', errMsg(err)) }
439
- // objstoreHandle has no close(): SQLite shares this DatabaseSync and
440
- // Neon has no persistent connection; `handle.close()` covers both.
441
- try { await handle.close() } catch (err) { console.warn('DB close error:', errMsg(err)) }
442
- }
443
- // Wire graceful shutdown + the signal / error / process-catchall
444
- // handlers (see ./lifecycle.ts). Installed last, once httpServer, wss,
445
- // and the heartbeat timer all exist.
446
- installLifecycle({
447
- httpServer, wss, heartbeatTimer, stopReaper,
448
- sseSessions: sseServer.sessions,
449
- sseKeepaliveTimer: sseServer.keepaliveTimer,
450
- closeDb,
451
- })
452
-
453
- // Bind only after the startup orphan sweep finishes — otherwise a
454
- // fresh boot could serve traffic against tags whose on-disk state
455
- // still has residue from a prior crash. `startupReap` already
456
- // resolves on any error (the reaper's own catch logs the failure
457
- // unconditionally and returns void), so no outer `.catch` is
458
- // needed here.
459
- await startupReap
460
-
461
- // Bind the HTTP/WS plane on the configured PORT/HOST. Exported so a
462
- // launcher (server/cli.js — the triage-server bin) or any `import`er can
463
- // start serving. The top-level `await startupReap` above means the server
464
- // is fully ready — DB open, DDL bootstrapped, objstore reaper swept — by
465
- // the time the import resolves.
466
- export function start(): void {
467
- httpServer.listen(PORT, HOST)
468
- }
469
-
470
- export { httpServer, wss }
471
-
472
- // Library mode: when this module is `import`ed (rather than run as the
473
- // entry script) skip the auto-start so consumers can own the bind — e.g.
474
- // wrap CF Access / framework-preset shims around the assembled
475
- // `httpServer`, or just call `start()` when ready. Direct invocation via
476
- // `node server/index.ts` (or the `triage-server` bin) still listens.
477
- if (import.meta.main) {
478
- start()
479
- }
480
-