cursedbelt-server 1.0.0

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 (606) hide show
  1. package/LICENSE +15 -0
  2. package/dist/server/activity/index.d.ts +36 -0
  3. package/dist/server/activity/index.js +36 -0
  4. package/dist/server/activity/migrations.d.ts +21 -0
  5. package/dist/server/activity/migrations.js +55 -0
  6. package/dist/server/activity/shareSource.d.ts +11 -0
  7. package/dist/server/activity/shareSource.js +45 -0
  8. package/dist/server/activity/store.d.ts +123 -0
  9. package/dist/server/activity/store.js +178 -0
  10. package/dist/server/app.d.ts +56 -0
  11. package/dist/server/app.js +45 -0
  12. package/dist/server/audit/index.d.ts +2 -0
  13. package/dist/server/audit/index.js +2 -0
  14. package/dist/server/audit/migrations.d.ts +2 -0
  15. package/dist/server/audit/migrations.js +79 -0
  16. package/dist/server/audit/sqliteSink.d.ts +36 -0
  17. package/dist/server/audit/sqliteSink.js +95 -0
  18. package/dist/server/audit/sqliteSink.test.d.ts +1 -0
  19. package/dist/server/audit/sqliteSink.test.js +180 -0
  20. package/dist/server/auth/cookie.d.ts +22 -0
  21. package/dist/server/auth/cookie.js +35 -0
  22. package/dist/server/auth/csrf.d.ts +42 -0
  23. package/dist/server/auth/csrf.js +36 -0
  24. package/dist/server/auth/fileTokenSigner.d.ts +10 -0
  25. package/dist/server/auth/fileTokenSigner.js +22 -0
  26. package/dist/server/auth/getOnly.d.ts +20 -0
  27. package/dist/server/auth/getOnly.js +27 -0
  28. package/dist/server/auth/index.d.ts +12 -0
  29. package/dist/server/auth/index.js +15 -0
  30. package/dist/server/auth/jwt.d.ts +18 -0
  31. package/dist/server/auth/jwt.js +56 -0
  32. package/dist/server/auth/loginThrottle.d.ts +118 -0
  33. package/dist/server/auth/loginThrottle.js +204 -0
  34. package/dist/server/auth/middleware.d.ts +14 -0
  35. package/dist/server/auth/middleware.js +47 -0
  36. package/dist/server/auth/password.d.ts +14 -0
  37. package/dist/server/auth/password.js +65 -0
  38. package/dist/server/auth/sessionStore.d.ts +19 -0
  39. package/dist/server/auth/sessionStore.js +221 -0
  40. package/dist/server/auth/totp.d.ts +34 -0
  41. package/dist/server/auth/totp.js +81 -0
  42. package/dist/server/auth/types.d.ts +133 -0
  43. package/dist/server/auth/types.js +1 -0
  44. package/dist/server/auth/vault.d.ts +27 -0
  45. package/dist/server/auth/vault.js +57 -0
  46. package/dist/server/context.d.ts +35 -0
  47. package/dist/server/context.js +1 -0
  48. package/dist/server/ctgr.d.ts +32 -0
  49. package/dist/server/ctgr.js +69 -0
  50. package/dist/server/db/kysely.d.ts +14 -0
  51. package/dist/server/db/kysely.js +16 -0
  52. package/dist/server/db/plumbingTypes.d.ts +116 -0
  53. package/dist/server/db/plumbingTypes.js +12 -0
  54. package/dist/server/dropzone/duplicates.d.ts +82 -0
  55. package/dist/server/dropzone/duplicates.js +90 -0
  56. package/dist/server/dropzone/index.d.ts +13 -0
  57. package/dist/server/dropzone/index.js +13 -0
  58. package/dist/server/dropzone/scan.d.ts +166 -0
  59. package/dist/server/dropzone/scan.js +272 -0
  60. package/dist/server/errors.d.ts +7 -0
  61. package/dist/server/errors.js +7 -0
  62. package/dist/server/guard/argon2Binding.d.ts +97 -0
  63. package/dist/server/guard/argon2Binding.js +138 -0
  64. package/dist/server/guard/index.d.ts +4 -0
  65. package/dist/server/guard/index.js +6 -0
  66. package/dist/server/guard/kdfWorker.d.ts +101 -0
  67. package/dist/server/guard/kdfWorker.js +301 -0
  68. package/dist/server/guard/kdfWorkerEntry.d.ts +1 -0
  69. package/dist/server/guard/kdfWorkerEntry.js +15 -0
  70. package/dist/server/guard/passwordVerifier.d.ts +65 -0
  71. package/dist/server/guard/passwordVerifier.js +122 -0
  72. package/dist/server/guard/revocationStore.d.ts +132 -0
  73. package/dist/server/guard/revocationStore.js +211 -0
  74. package/dist/server/guard/sessionSigner.d.ts +132 -0
  75. package/dist/server/guard/sessionSigner.js +303 -0
  76. package/dist/server/guard/verifier.d.ts +21 -0
  77. package/dist/server/guard/verifier.js +1 -0
  78. package/dist/server/incidents/channels.d.ts +161 -0
  79. package/dist/server/incidents/channels.js +357 -0
  80. package/dist/server/incidents/checks.d.ts +61 -0
  81. package/dist/server/incidents/checks.js +55 -0
  82. package/dist/server/incidents/index.d.ts +51 -0
  83. package/dist/server/incidents/index.js +51 -0
  84. package/dist/server/incidents/migrations.d.ts +20 -0
  85. package/dist/server/incidents/migrations.js +58 -0
  86. package/dist/server/incidents/notifier.d.ts +79 -0
  87. package/dist/server/incidents/notifier.js +141 -0
  88. package/dist/server/incidents/reporter.d.ts +60 -0
  89. package/dist/server/incidents/reporter.js +49 -0
  90. package/dist/server/incidents/routes.d.ts +91 -0
  91. package/dist/server/incidents/routes.js +202 -0
  92. package/dist/server/incidents/store.d.ts +75 -0
  93. package/dist/server/incidents/store.js +176 -0
  94. package/dist/server/incidents/types.d.ts +107 -0
  95. package/dist/server/incidents/types.js +32 -0
  96. package/dist/server/index.d.ts +25 -0
  97. package/dist/server/index.js +53 -0
  98. package/dist/server/jobs/fireQueue.d.ts +12 -0
  99. package/dist/server/jobs/fireQueue.js +37 -0
  100. package/dist/server/jobs/index.d.ts +6 -0
  101. package/dist/server/jobs/index.js +10 -0
  102. package/dist/server/jobs/logger.d.ts +10 -0
  103. package/dist/server/jobs/logger.js +14 -0
  104. package/dist/server/jobs/plainjobAdapter.d.ts +19 -0
  105. package/dist/server/jobs/plainjobAdapter.js +48 -0
  106. package/dist/server/jobs/polling.d.ts +17 -0
  107. package/dist/server/jobs/polling.js +79 -0
  108. package/dist/server/jobs/runner.d.ts +24 -0
  109. package/dist/server/jobs/runner.js +186 -0
  110. package/dist/server/jobs/timeout.d.ts +28 -0
  111. package/dist/server/jobs/timeout.js +47 -0
  112. package/dist/server/jobs/types.d.ts +167 -0
  113. package/dist/server/jobs/types.js +12 -0
  114. package/dist/server/master-lock/guard.d.ts +53 -0
  115. package/dist/server/master-lock/guard.js +205 -0
  116. package/dist/server/master-lock/index.d.ts +21 -0
  117. package/dist/server/master-lock/index.js +21 -0
  118. package/dist/server/master-lock/lockPage.d.ts +34 -0
  119. package/dist/server/master-lock/lockPage.js +281 -0
  120. package/dist/server/master-lock/masterLock.d.ts +294 -0
  121. package/dist/server/master-lock/masterLock.js +523 -0
  122. package/dist/server/master-lock/principals.d.ts +76 -0
  123. package/dist/server/master-lock/principals.js +157 -0
  124. package/dist/server/master-lock/seed.d.ts +47 -0
  125. package/dist/server/master-lock/seed.js +93 -0
  126. package/dist/server/master-lock/store.d.ts +19 -0
  127. package/dist/server/master-lock/store.js +20 -0
  128. package/dist/server/media-bun/image.d.ts +7 -0
  129. package/dist/server/media-bun/image.js +96 -0
  130. package/dist/server/media-bun/index.d.ts +4 -0
  131. package/dist/server/media-bun/index.js +10 -0
  132. package/dist/server/media-bun/streaming.d.ts +86 -0
  133. package/dist/server/media-bun/streaming.js +237 -0
  134. package/dist/server/media-bun/video.d.ts +35 -0
  135. package/dist/server/media-bun/video.js +181 -0
  136. package/dist/server/metrics/metricsBuffer.d.ts +39 -0
  137. package/dist/server/metrics/metricsBuffer.js +36 -0
  138. package/dist/server/metrics/normalizeRoute.d.ts +5 -0
  139. package/dist/server/metrics/normalizeRoute.js +58 -0
  140. package/dist/server/middleware/correlationId.d.ts +9 -0
  141. package/dist/server/middleware/correlationId.js +20 -0
  142. package/dist/server/middleware/corsAllowlist.d.ts +38 -0
  143. package/dist/server/middleware/corsAllowlist.js +37 -0
  144. package/dist/server/middleware/errorEnvelope.d.ts +17 -0
  145. package/dist/server/middleware/errorEnvelope.js +32 -0
  146. package/dist/server/middleware/index.d.ts +9 -0
  147. package/dist/server/middleware/index.js +10 -0
  148. package/dist/server/middleware/localOrigin.d.ts +71 -0
  149. package/dist/server/middleware/localOrigin.js +119 -0
  150. package/dist/server/middleware/rateLimit.d.ts +56 -0
  151. package/dist/server/middleware/rateLimit.js +86 -0
  152. package/dist/server/middleware/requestLogger.d.ts +29 -0
  153. package/dist/server/middleware/requestLogger.js +47 -0
  154. package/dist/server/middleware/securityHeaders.d.ts +29 -0
  155. package/dist/server/middleware/securityHeaders.js +44 -0
  156. package/dist/server/middleware/zValidator.d.ts +29 -0
  157. package/dist/server/middleware/zValidator.js +57 -0
  158. package/dist/server/migration/runner.d.ts +42 -0
  159. package/dist/server/migration/runner.js +70 -0
  160. package/dist/server/migration/types.d.ts +16 -0
  161. package/dist/server/migration/types.js +1 -0
  162. package/dist/server/net/outboundGuard.d.ts +30 -0
  163. package/dist/server/net/outboundGuard.js +199 -0
  164. package/dist/server/notifications/hub.d.ts +67 -0
  165. package/dist/server/notifications/hub.js +187 -0
  166. package/dist/server/notifications/index.d.ts +47 -0
  167. package/dist/server/notifications/index.js +46 -0
  168. package/dist/server/notifications/migrations.d.ts +13 -0
  169. package/dist/server/notifications/migrations.js +20 -0
  170. package/dist/server/notifications/routes.d.ts +58 -0
  171. package/dist/server/notifications/routes.js +113 -0
  172. package/dist/server/notifications/service.d.ts +58 -0
  173. package/dist/server/notifications/service.js +41 -0
  174. package/dist/server/paths/appDataHome.d.ts +19 -0
  175. package/dist/server/paths/appDataHome.js +68 -0
  176. package/dist/server/private-media/crypto.d.ts +37 -0
  177. package/dist/server/private-media/crypto.js +111 -0
  178. package/dist/server/private-media/index-store.d.ts +31 -0
  179. package/dist/server/private-media/index-store.js +36 -0
  180. package/dist/server/private-media/mountPrivateRoutes.d.ts +26 -0
  181. package/dist/server/private-media/mountPrivateRoutes.js +219 -0
  182. package/dist/server/private-media/pipeline.d.ts +37 -0
  183. package/dist/server/private-media/pipeline.js +97 -0
  184. package/dist/server/private-media/unlock.d.ts +52 -0
  185. package/dist/server/private-media/unlock.js +55 -0
  186. package/dist/server/resume/index.d.ts +2 -0
  187. package/dist/server/resume/index.js +2 -0
  188. package/dist/server/resume/routes.d.ts +41 -0
  189. package/dist/server/resume/routes.js +50 -0
  190. package/dist/server/resume/store.d.ts +37 -0
  191. package/dist/server/resume/store.js +38 -0
  192. package/dist/server/retention/archive.d.ts +78 -0
  193. package/dist/server/retention/archive.js +284 -0
  194. package/dist/server/retention/fleet.d.ts +50 -0
  195. package/dist/server/retention/fleet.js +66 -0
  196. package/dist/server/retention/index.d.ts +38 -0
  197. package/dist/server/retention/index.js +38 -0
  198. package/dist/server/retention/policy.d.ts +263 -0
  199. package/dist/server/retention/policy.js +264 -0
  200. package/dist/server/retention/store.d.ts +101 -0
  201. package/dist/server/retention/store.js +251 -0
  202. package/dist/server/retention/sweeper.d.ts +69 -0
  203. package/dist/server/retention/sweeper.js +289 -0
  204. package/dist/server/retention/target.d.ts +143 -0
  205. package/dist/server/retention/target.js +438 -0
  206. package/dist/server/search/index.d.ts +2 -0
  207. package/dist/server/search/index.js +6 -0
  208. package/dist/server/search/pgSearchProvider.d.ts +91 -0
  209. package/dist/server/search/pgSearchProvider.js +330 -0
  210. package/dist/server/search/pgSearchProvider.pg.test.d.ts +1 -0
  211. package/dist/server/search/pgSearchProvider.pg.test.js +338 -0
  212. package/dist/server/search/pgSearchSchema.d.ts +81 -0
  213. package/dist/server/search/pgSearchSchema.js +89 -0
  214. package/dist/server/sharing/index.d.ts +18 -0
  215. package/dist/server/sharing/index.js +16 -0
  216. package/dist/server/sharing/migrations.d.ts +6 -0
  217. package/dist/server/sharing/migrations.js +79 -0
  218. package/dist/server/sharing/router.d.ts +111 -0
  219. package/dist/server/sharing/router.js +93 -0
  220. package/dist/server/sharing/sql.d.ts +67 -0
  221. package/dist/server/sharing/sql.js +88 -0
  222. package/dist/server/sharing/store.d.ts +173 -0
  223. package/dist/server/sharing/store.js +484 -0
  224. package/dist/server/sqlite/backup.d.ts +48 -0
  225. package/dist/server/sqlite/backup.js +97 -0
  226. package/dist/server/sqlite/browse.d.ts +42 -0
  227. package/dist/server/sqlite/browse.js +84 -0
  228. package/dist/server/sqlite/export.d.ts +32 -0
  229. package/dist/server/sqlite/export.js +42 -0
  230. package/dist/server/sqlite/fts5.d.ts +106 -0
  231. package/dist/server/sqlite/fts5.js +154 -0
  232. package/dist/server/sqlite/health.d.ts +44 -0
  233. package/dist/server/sqlite/health.js +52 -0
  234. package/dist/server/sqlite/import.d.ts +36 -0
  235. package/dist/server/sqlite/import.js +108 -0
  236. package/dist/server/sqlite/index.d.ts +9 -0
  237. package/dist/server/sqlite/index.js +15 -0
  238. package/dist/server/sqlite/readonlySql.d.ts +15 -0
  239. package/dist/server/sqlite/readonlySql.js +65 -0
  240. package/dist/server/sqlite/stats.d.ts +22 -0
  241. package/dist/server/sqlite/stats.js +32 -0
  242. package/dist/server/sqlite/viewer.d.ts +17 -0
  243. package/dist/server/sqlite/viewer.js +56 -0
  244. package/dist/server/storage/binaryServerProcessing.d.ts +23 -0
  245. package/dist/server/storage/binaryServerProcessing.js +81 -0
  246. package/dist/server/storage/binaryServerStore.d.ts +3 -0
  247. package/dist/server/storage/binaryServerStore.js +95 -0
  248. package/dist/server/storage/diskStore.d.ts +2 -0
  249. package/dist/server/storage/diskStore.js +88 -0
  250. package/dist/server/storage/files/adapter.d.ts +33 -0
  251. package/dist/server/storage/files/adapter.js +203 -0
  252. package/dist/server/storage/files/catalogue.d.ts +72 -0
  253. package/dist/server/storage/files/catalogue.js +1033 -0
  254. package/dist/server/storage/files/chunkedUpload.d.ts +56 -0
  255. package/dist/server/storage/files/chunkedUpload.js +70 -0
  256. package/dist/server/storage/files/fileRoutes.d.ts +51 -0
  257. package/dist/server/storage/files/fileRoutes.js +405 -0
  258. package/dist/server/storage/files/index.d.ts +9 -0
  259. package/dist/server/storage/files/index.js +11 -0
  260. package/dist/server/storage/files/migrations.d.ts +2 -0
  261. package/dist/server/storage/files/migrations.js +163 -0
  262. package/dist/server/storage/files/mimeByExt.d.ts +1 -0
  263. package/dist/server/storage/files/mimeByExt.js +13 -0
  264. package/dist/server/storage/files/stack.d.ts +136 -0
  265. package/dist/server/storage/files/stack.js +132 -0
  266. package/dist/server/storage/files/storageOps.d.ts +54 -0
  267. package/dist/server/storage/files/storageOps.js +110 -0
  268. package/dist/server/storage/files/types.d.ts +261 -0
  269. package/dist/server/storage/files/types.js +22 -0
  270. package/dist/server/storage/imageThumbnail.d.ts +3 -0
  271. package/dist/server/storage/imageThumbnail.js +50 -0
  272. package/dist/server/storage/index.d.ts +15 -0
  273. package/dist/server/storage/index.js +27 -0
  274. package/dist/server/storage/mountMediaRoutes.d.ts +28 -0
  275. package/dist/server/storage/mountMediaRoutes.js +141 -0
  276. package/dist/server/storage/objectStoreMediaStore.d.ts +4 -0
  277. package/dist/server/storage/objectStoreMediaStore.js +57 -0
  278. package/dist/server/storage/s3ObjectStore.d.ts +63 -0
  279. package/dist/server/storage/s3ObjectStore.js +187 -0
  280. package/dist/server/storage/sources.d.ts +47 -0
  281. package/dist/server/storage/sources.js +148 -0
  282. package/dist/server/storage/streamRange.d.ts +19 -0
  283. package/dist/server/storage/streamRange.js +69 -0
  284. package/dist/server/storage/streamToken.d.ts +16 -0
  285. package/dist/server/storage/streamToken.js +54 -0
  286. package/dist/server/storage/transcodeJobs.d.ts +65 -0
  287. package/dist/server/storage/transcodeJobs.js +104 -0
  288. package/dist/server/storage/types.d.ts +193 -0
  289. package/dist/server/storage/types.js +41 -0
  290. package/dist/server/storage/videoDerivativeKeys.d.ts +127 -0
  291. package/dist/server/storage/videoDerivativeKeys.js +149 -0
  292. package/dist/server/storage/videoProcessor.d.ts +30 -0
  293. package/dist/server/storage/videoProcessor.js +148 -0
  294. package/dist/server/sync/alarm.d.ts +34 -0
  295. package/dist/server/sync/alarm.js +91 -0
  296. package/dist/server/sync/commands.d.ts +87 -0
  297. package/dist/server/sync/commands.js +241 -0
  298. package/dist/server/sync/engine.d.ts +62 -0
  299. package/dist/server/sync/engine.js +184 -0
  300. package/dist/server/sync/http.d.ts +106 -0
  301. package/dist/server/sync/http.js +243 -0
  302. package/dist/server/sync/index.d.ts +41 -0
  303. package/dist/server/sync/index.js +41 -0
  304. package/dist/server/sync/opLog.d.ts +61 -0
  305. package/dist/server/sync/opLog.js +96 -0
  306. package/dist/server/sync/planner.d.ts +31 -0
  307. package/dist/server/sync/planner.js +30 -0
  308. package/dist/server/sync/status.d.ts +63 -0
  309. package/dist/server/sync/status.js +49 -0
  310. package/dist/server/sync/timer.d.ts +52 -0
  311. package/dist/server/sync/timer.js +170 -0
  312. package/dist/server/sync/tokens.d.ts +25 -0
  313. package/dist/server/sync/tokens.js +51 -0
  314. package/dist/server/sync/types.d.ts +101 -0
  315. package/dist/server/sync/types.js +29 -0
  316. package/dist/server/ws/eventBus.d.ts +14 -0
  317. package/dist/server/ws/eventBus.js +42 -0
  318. package/dist/server/ws/hub.d.ts +20 -0
  319. package/dist/server/ws/hub.js +154 -0
  320. package/dist/server/ws/index.d.ts +5 -0
  321. package/dist/server/ws/index.js +7 -0
  322. package/dist/server/ws/presence.d.ts +11 -0
  323. package/dist/server/ws/presence.js +20 -0
  324. package/dist/server/ws/room.d.ts +17 -0
  325. package/dist/server/ws/room.js +23 -0
  326. package/dist/server/ws/types.d.ts +122 -0
  327. package/dist/server/ws/types.js +1 -0
  328. package/package.json +234 -0
  329. package/src/declaredDepsAreImported.spec.ts +87 -0
  330. package/src/leafSubpathsImportNothing.spec.ts +358 -0
  331. package/src/lockfileIsTracked.spec.ts +50 -0
  332. package/src/namedSubpathsResolve.spec.ts +122 -0
  333. package/src/noPathDeps.spec.ts +24 -0
  334. package/src/publishShape.spec.ts +89 -0
  335. package/src/server/activity/index.ts +59 -0
  336. package/src/server/activity/migrations.ts +67 -0
  337. package/src/server/activity/shareSource.ts +57 -0
  338. package/src/server/activity/store.spec.ts +255 -0
  339. package/src/server/activity/store.ts +368 -0
  340. package/src/server/app.spec.ts +243 -0
  341. package/src/server/app.ts +89 -0
  342. package/src/server/audit/index.ts +2 -0
  343. package/src/server/audit/migrations.ts +88 -0
  344. package/src/server/audit/sqliteSink.test.ts +200 -0
  345. package/src/server/audit/sqliteSink.ts +162 -0
  346. package/src/server/auth/cookie.spec.ts +46 -0
  347. package/src/server/auth/cookie.ts +42 -0
  348. package/src/server/auth/csrf.spec.ts +81 -0
  349. package/src/server/auth/csrf.ts +83 -0
  350. package/src/server/auth/fileTokenSigner.ts +40 -0
  351. package/src/server/auth/getOnly.spec.ts +53 -0
  352. package/src/server/auth/getOnly.ts +32 -0
  353. package/src/server/auth/index.ts +51 -0
  354. package/src/server/auth/jwt.spec.ts +106 -0
  355. package/src/server/auth/jwt.ts +75 -0
  356. package/src/server/auth/loginThrottle.spec.ts +213 -0
  357. package/src/server/auth/loginThrottle.ts +261 -0
  358. package/src/server/auth/middleware.spec.ts +148 -0
  359. package/src/server/auth/middleware.ts +56 -0
  360. package/src/server/auth/password.spec.ts +48 -0
  361. package/src/server/auth/password.ts +71 -0
  362. package/src/server/auth/sessionStore.spec.ts +189 -0
  363. package/src/server/auth/sessionStore.ts +261 -0
  364. package/src/server/auth/totp.spec.ts +73 -0
  365. package/src/server/auth/totp.ts +92 -0
  366. package/src/server/auth/types.ts +146 -0
  367. package/src/server/auth/vault.spec.ts +51 -0
  368. package/src/server/auth/vault.ts +71 -0
  369. package/src/server/context.ts +37 -0
  370. package/src/server/ctgr.spec.ts +138 -0
  371. package/src/server/ctgr.ts +108 -0
  372. package/src/server/db/kysely.spec.ts +62 -0
  373. package/src/server/db/kysely.ts +21 -0
  374. package/src/server/db/plumbingTypes.ts +126 -0
  375. package/src/server/dropzone/duplicates.ts +121 -0
  376. package/src/server/dropzone/index.ts +30 -0
  377. package/src/server/dropzone/scan.spec.ts +255 -0
  378. package/src/server/dropzone/scan.ts +342 -0
  379. package/src/server/errors.spec.ts +76 -0
  380. package/src/server/errors.ts +22 -0
  381. package/src/server/guard/argon2Binding.spec.ts +117 -0
  382. package/src/server/guard/argon2Binding.ts +195 -0
  383. package/src/server/guard/argon2Kat.spec.ts +250 -0
  384. package/src/server/guard/guard.spec.ts +390 -0
  385. package/src/server/guard/index.ts +6 -0
  386. package/src/server/guard/kdfWorker.spec.ts +283 -0
  387. package/src/server/guard/kdfWorker.ts +367 -0
  388. package/src/server/guard/kdfWorkerEntry.ts +16 -0
  389. package/src/server/guard/passwordVerifier.spec.ts +71 -0
  390. package/src/server/guard/passwordVerifier.ts +175 -0
  391. package/src/server/guard/revocationStore.spec.ts +135 -0
  392. package/src/server/guard/revocationStore.ts +291 -0
  393. package/src/server/guard/sessionSigner.ts +425 -0
  394. package/src/server/guard/verifier.ts +27 -0
  395. package/src/server/incidents/channels.ts +487 -0
  396. package/src/server/incidents/checks.ts +120 -0
  397. package/src/server/incidents/incidents.spec.ts +463 -0
  398. package/src/server/incidents/index.ts +96 -0
  399. package/src/server/incidents/migrations.ts +66 -0
  400. package/src/server/incidents/notifier.ts +215 -0
  401. package/src/server/incidents/reporter.ts +110 -0
  402. package/src/server/incidents/routes.ts +314 -0
  403. package/src/server/incidents/store.ts +329 -0
  404. package/src/server/incidents/types.ts +116 -0
  405. package/src/server/index.ts +248 -0
  406. package/src/server/jobs/fireQueue.spec.ts +85 -0
  407. package/src/server/jobs/fireQueue.ts +37 -0
  408. package/src/server/jobs/index.ts +26 -0
  409. package/src/server/jobs/logger.ts +16 -0
  410. package/src/server/jobs/plainjobAdapter.spec.ts +34 -0
  411. package/src/server/jobs/plainjobAdapter.ts +54 -0
  412. package/src/server/jobs/polling.spec.ts +219 -0
  413. package/src/server/jobs/polling.ts +81 -0
  414. package/src/server/jobs/runner.spec.ts +213 -0
  415. package/src/server/jobs/runner.ts +213 -0
  416. package/src/server/jobs/timeout.spec.ts +35 -0
  417. package/src/server/jobs/timeout.ts +53 -0
  418. package/src/server/jobs/types.ts +188 -0
  419. package/src/server/master-lock/guard.spec.ts +347 -0
  420. package/src/server/master-lock/guard.ts +290 -0
  421. package/src/server/master-lock/index.ts +44 -0
  422. package/src/server/master-lock/lockPage.spec.ts +88 -0
  423. package/src/server/master-lock/lockPage.ts +313 -0
  424. package/src/server/master-lock/masterLock.spec.ts +338 -0
  425. package/src/server/master-lock/masterLock.ts +627 -0
  426. package/src/server/master-lock/principals.spec.ts +378 -0
  427. package/src/server/master-lock/principals.ts +175 -0
  428. package/src/server/master-lock/seed.spec.ts +69 -0
  429. package/src/server/master-lock/seed.ts +112 -0
  430. package/src/server/master-lock/store.ts +35 -0
  431. package/src/server/media-bun/image.spec.ts +115 -0
  432. package/src/server/media-bun/image.ts +134 -0
  433. package/src/server/media-bun/index.ts +52 -0
  434. package/src/server/media-bun/streaming.spec.ts +134 -0
  435. package/src/server/media-bun/streaming.ts +347 -0
  436. package/src/server/media-bun/video.spec.ts +115 -0
  437. package/src/server/media-bun/video.ts +232 -0
  438. package/src/server/metrics/metricsBuffer.ts +92 -0
  439. package/src/server/metrics/normalizeRoute.spec.ts +51 -0
  440. package/src/server/metrics/normalizeRoute.ts +63 -0
  441. package/src/server/middleware/correlationId.ts +24 -0
  442. package/src/server/middleware/corsAllowlist.spec.ts +118 -0
  443. package/src/server/middleware/corsAllowlist.ts +69 -0
  444. package/src/server/middleware/errorEnvelope.ts +38 -0
  445. package/src/server/middleware/index.ts +19 -0
  446. package/src/server/middleware/localOrigin.spec.ts +168 -0
  447. package/src/server/middleware/localOrigin.ts +139 -0
  448. package/src/server/middleware/rateLimit.spec.ts +72 -0
  449. package/src/server/middleware/rateLimit.ts +125 -0
  450. package/src/server/middleware/requestLogger.ts +95 -0
  451. package/src/server/middleware/securityHeaders.ts +78 -0
  452. package/src/server/middleware/zValidator.spec.ts +251 -0
  453. package/src/server/middleware/zValidator.ts +114 -0
  454. package/src/server/migration/runner.spec.ts +88 -0
  455. package/src/server/migration/runner.ts +87 -0
  456. package/src/server/migration/types.ts +17 -0
  457. package/src/server/net/outboundGuard.spec.ts +90 -0
  458. package/src/server/net/outboundGuard.ts +205 -0
  459. package/src/server/notifications/hub.ts +245 -0
  460. package/src/server/notifications/index.ts +77 -0
  461. package/src/server/notifications/migrations.ts +39 -0
  462. package/src/server/notifications/routes.ts +177 -0
  463. package/src/server/notifications/service.spec.ts +305 -0
  464. package/src/server/notifications/service.ts +112 -0
  465. package/src/server/paths/appDataHome.spec.ts +87 -0
  466. package/src/server/paths/appDataHome.ts +84 -0
  467. package/src/server/private-media/blobRoute.spec.ts +165 -0
  468. package/src/server/private-media/crypto.spec.ts +81 -0
  469. package/src/server/private-media/crypto.ts +155 -0
  470. package/src/server/private-media/gatePolicy.spec.ts +98 -0
  471. package/src/server/private-media/index-store.ts +83 -0
  472. package/src/server/private-media/mountPrivateRoutes.ts +265 -0
  473. package/src/server/private-media/pipeline.spec.ts +111 -0
  474. package/src/server/private-media/pipeline.ts +170 -0
  475. package/src/server/private-media/routes.spec.ts +265 -0
  476. package/src/server/private-media/unlock.ts +112 -0
  477. package/src/server/resume/index.ts +8 -0
  478. package/src/server/resume/resume.spec.ts +118 -0
  479. package/src/server/resume/routes.ts +92 -0
  480. package/src/server/resume/store.ts +82 -0
  481. package/src/server/retention/archive.spec.ts +181 -0
  482. package/src/server/retention/archive.ts +353 -0
  483. package/src/server/retention/fleet.ts +104 -0
  484. package/src/server/retention/index.ts +101 -0
  485. package/src/server/retention/policy.spec.ts +132 -0
  486. package/src/server/retention/policy.ts +392 -0
  487. package/src/server/retention/store.spec.ts +236 -0
  488. package/src/server/retention/store.ts +338 -0
  489. package/src/server/retention/sweeper.spec.ts +321 -0
  490. package/src/server/retention/sweeper.ts +367 -0
  491. package/src/server/retention/target.spec.ts +592 -0
  492. package/src/server/retention/target.ts +613 -0
  493. package/src/server/search/index.ts +12 -0
  494. package/src/server/search/pgSearchProvider.pg.test.ts +392 -0
  495. package/src/server/search/pgSearchProvider.ts +421 -0
  496. package/src/server/search/pgSearchSchema.ts +147 -0
  497. package/src/server/sharing/index.ts +65 -0
  498. package/src/server/sharing/migrations.ts +128 -0
  499. package/src/server/sharing/router.spec.ts +167 -0
  500. package/src/server/sharing/router.ts +194 -0
  501. package/src/server/sharing/sql.ts +109 -0
  502. package/src/server/sharing/store.spec.ts +408 -0
  503. package/src/server/sharing/store.ts +737 -0
  504. package/src/server/sqlite/backup.spec.ts +114 -0
  505. package/src/server/sqlite/backup.ts +154 -0
  506. package/src/server/sqlite/browse.spec.ts +70 -0
  507. package/src/server/sqlite/browse.ts +120 -0
  508. package/src/server/sqlite/export.spec.ts +43 -0
  509. package/src/server/sqlite/export.ts +74 -0
  510. package/src/server/sqlite/fts5.spec.ts +175 -0
  511. package/src/server/sqlite/fts5.ts +255 -0
  512. package/src/server/sqlite/health.spec.ts +79 -0
  513. package/src/server/sqlite/health.ts +105 -0
  514. package/src/server/sqlite/import.spec.ts +75 -0
  515. package/src/server/sqlite/import.ts +141 -0
  516. package/src/server/sqlite/index.ts +64 -0
  517. package/src/server/sqlite/readonlySql.spec.ts +58 -0
  518. package/src/server/sqlite/readonlySql.ts +67 -0
  519. package/src/server/sqlite/stats.spec.ts +21 -0
  520. package/src/server/sqlite/stats.ts +61 -0
  521. package/src/server/sqlite/viewer.spec.ts +127 -0
  522. package/src/server/sqlite/viewer.ts +72 -0
  523. package/src/server/storage/binaryServerProcessing.integration.spec.ts +222 -0
  524. package/src/server/storage/binaryServerProcessing.spec.ts +150 -0
  525. package/src/server/storage/binaryServerProcessing.ts +127 -0
  526. package/src/server/storage/binaryServerStore.ts +102 -0
  527. package/src/server/storage/chunked.integration.spec.ts +185 -0
  528. package/src/server/storage/diskStore.ts +83 -0
  529. package/src/server/storage/files/adapter.photo.display.spec.ts +169 -0
  530. package/src/server/storage/files/adapter.photo.spec.ts +128 -0
  531. package/src/server/storage/files/adapter.spec.ts +121 -0
  532. package/src/server/storage/files/adapter.ts +255 -0
  533. package/src/server/storage/files/catalogue.media-host.spec.ts +184 -0
  534. package/src/server/storage/files/catalogue.orderings.spec.ts +157 -0
  535. package/src/server/storage/files/catalogue.private.spec.ts +106 -0
  536. package/src/server/storage/files/catalogue.recover.spec.ts +131 -0
  537. package/src/server/storage/files/catalogue.sort.spec.ts +154 -0
  538. package/src/server/storage/files/catalogue.spec.ts +133 -0
  539. package/src/server/storage/files/catalogue.trash-import.spec.ts +125 -0
  540. package/src/server/storage/files/catalogue.ts +1326 -0
  541. package/src/server/storage/files/chunkedUpload.spec.ts +111 -0
  542. package/src/server/storage/files/chunkedUpload.ts +146 -0
  543. package/src/server/storage/files/collections.spec.ts +294 -0
  544. package/src/server/storage/files/fileRoutes.replace.spec.ts +88 -0
  545. package/src/server/storage/files/fileRoutes.spec.ts +327 -0
  546. package/src/server/storage/files/fileRoutes.ts +462 -0
  547. package/src/server/storage/files/index.ts +66 -0
  548. package/src/server/storage/files/migrations.ts +187 -0
  549. package/src/server/storage/files/mimeByExt.ts +14 -0
  550. package/src/server/storage/files/stack.spec.ts +258 -0
  551. package/src/server/storage/files/stack.ts +284 -0
  552. package/src/server/storage/files/storageOps.spec.ts +126 -0
  553. package/src/server/storage/files/storageOps.ts +213 -0
  554. package/src/server/storage/files/types.ts +285 -0
  555. package/src/server/storage/imageThumbnail.spec.ts +81 -0
  556. package/src/server/storage/imageThumbnail.ts +60 -0
  557. package/src/server/storage/index.ts +122 -0
  558. package/src/server/storage/mountMediaRoutes.ts +231 -0
  559. package/src/server/storage/objectStore.spec.ts +272 -0
  560. package/src/server/storage/objectStoreMediaStore.ts +63 -0
  561. package/src/server/storage/s3ObjectStore.ts +239 -0
  562. package/src/server/storage/sources.ts +245 -0
  563. package/src/server/storage/storage.spec.ts +219 -0
  564. package/src/server/storage/streamRange.ts +92 -0
  565. package/src/server/storage/streamToken.ts +76 -0
  566. package/src/server/storage/transcode.integration.spec.ts +134 -0
  567. package/src/server/storage/transcodeJobs.spec.ts +152 -0
  568. package/src/server/storage/transcodeJobs.ts +171 -0
  569. package/src/server/storage/types.ts +230 -0
  570. package/src/server/storage/videoDerivativeKeys.spec.ts +112 -0
  571. package/src/server/storage/videoDerivativeKeys.ts +172 -0
  572. package/src/server/storage/videoProcessor.posterOnly.spec.ts +106 -0
  573. package/src/server/storage/videoProcessor.ts +229 -0
  574. package/src/server/sync/alarm.spec.ts +149 -0
  575. package/src/server/sync/alarm.ts +137 -0
  576. package/src/server/sync/commands.spec.ts +145 -0
  577. package/src/server/sync/commands.ts +361 -0
  578. package/src/server/sync/engine.spec.ts +496 -0
  579. package/src/server/sync/engine.ts +255 -0
  580. package/src/server/sync/http.ts +316 -0
  581. package/src/server/sync/httpRemote.spec.ts +158 -0
  582. package/src/server/sync/index.ts +90 -0
  583. package/src/server/sync/opLog.spec.ts +110 -0
  584. package/src/server/sync/opLog.ts +189 -0
  585. package/src/server/sync/planner.spec.ts +53 -0
  586. package/src/server/sync/planner.ts +62 -0
  587. package/src/server/sync/status.spec.ts +92 -0
  588. package/src/server/sync/status.ts +108 -0
  589. package/src/server/sync/timer.spec.ts +150 -0
  590. package/src/server/sync/timer.ts +207 -0
  591. package/src/server/sync/tokens.spec.ts +38 -0
  592. package/src/server/sync/tokens.ts +94 -0
  593. package/src/server/sync/types.ts +108 -0
  594. package/src/server/ws/eventBus.spec.ts +67 -0
  595. package/src/server/ws/eventBus.ts +46 -0
  596. package/src/server/ws/hub.integration.spec.ts +93 -0
  597. package/src/server/ws/hub.spec.ts +282 -0
  598. package/src/server/ws/hub.ts +168 -0
  599. package/src/server/ws/index.ts +20 -0
  600. package/src/server/ws/presence.spec.ts +27 -0
  601. package/src/server/ws/presence.ts +26 -0
  602. package/src/server/ws/room.spec.ts +36 -0
  603. package/src/server/ws/room.ts +29 -0
  604. package/src/server/ws/types.ts +134 -0
  605. package/src/skippedTestsAreAnswered.spec.ts +215 -0
  606. package/src/testTmpRoot.spec.ts +102 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * SSRF guard for server-side outbound HTTP (03 §2.4 H-4 adjudication).
3
+ *
4
+ * The Request Builder send (`/api/system/http`) is server-side BY DESIGN: the
5
+ * pinned CSP's `connect-src` would block a browser-side request to an arbitrary
6
+ * host, so the send is performed by the server to keep the tool working without
7
+ * widening a single header. But moving the send server-side creates a
8
+ * server-side request forgery surface the CSP argument never addressed — an
9
+ * authenticated caller can now make the SERVER fetch:
10
+ *
11
+ * - `http://169.254.169.254/latest/meta-data/…` → cloud metadata → credentials
12
+ * - `http://localhost:4747/…` / `http://127.0.0.1` → the station and sibling apps
13
+ * - `http://10.x` / `172.16.x` / `192.168.x` → anything on the private network
14
+ * - `http://<internal-rds-or-redis-or-admin>` → internal-only services
15
+ *
16
+ * This module is the boundary that refuses those. It is fail-CLOSED and lives in
17
+ * the framework (not in a leaf `sendHttp` impl) so no implementation can forget
18
+ * it: the mixin runs {@link assertPublicOutboundUrl} on the caller-supplied URL
19
+ * BEFORE any impl sees it.
20
+ *
21
+ * Two layers, because neither alone is sufficient:
22
+ * 1. {@link assertPublicOutboundUrl} — synchronous, structural: scheme +
23
+ * credential + literal-IP-range + dangerous-hostname checks. Catches every
24
+ * IP-literal vector (including the decimal/hex/octal encodings that
25
+ * `new URL()` preserves and `fetch` still resolves) and the well-known
26
+ * internal hostnames. This is what the request path calls on every send.
27
+ * 2. {@link assertResolvedPublicUrl} — async, DNS-aware: additionally resolves
28
+ * the hostname and re-checks every resolved address, which is the only
29
+ * defense against a public name that resolves to a private IP (DNS
30
+ * rebinding). Opt-in for impls that can afford the lookup; the SaaS profile
31
+ * must ALSO enforce egress at the network layer, since even this races a
32
+ * rebinding attacker between resolve and connect.
33
+ */
34
+ import { isIP } from 'node:net';
35
+ import { lookup } from 'node:dns/promises';
36
+ import { AppError } from '../errors';
37
+
38
+ export const OUTBOUND_URL_BLOCKED = 'OUTBOUND_URL_BLOCKED';
39
+
40
+ export interface OutboundGuardOptions {
41
+ /**
42
+ * Exact hostnames (case-insensitive) explicitly permitted even if they would
43
+ * otherwise be blocked — for a deliberately-configured internal integration.
44
+ * Empty by default: nothing internal is reachable unless a host opts in.
45
+ */
46
+ allowHosts?: readonly string[];
47
+ /** Permitted URL schemes. Default `['http:', 'https:']`. */
48
+ allowSchemes?: readonly string[];
49
+ }
50
+
51
+ const blocked = (reason: string, context: Record<string, unknown>): AppError =>
52
+ new AppError(`Outbound request refused: ${reason}.`, {
53
+ code: OUTBOUND_URL_BLOCKED,
54
+ status: 400,
55
+ category: 'security',
56
+ // Deliberately terse context — do not echo internal topology back.
57
+ context,
58
+ });
59
+
60
+ /** Hostnames and suffixes that always resolve to a non-public / internal target. */
61
+ const BLOCKED_HOST_EXACT = new Set(['localhost', 'metadata.google.internal', 'metadata']);
62
+ const BLOCKED_HOST_SUFFIXES = ['.localhost', '.local', '.internal', '.intranet', '.cluster.local', '.svc'];
63
+
64
+ /**
65
+ * Parse a hostname into an IPv4 the way a permissive resolver would, covering
66
+ * the encodings SSRF filters classically miss: dotted-decimal (`127.0.0.1`), a
67
+ * bare 32-bit integer (`2130706433`), and hex (`0x7f000001`). Returns the four
68
+ * octets, or `null` when the host is not an IPv4 literal in any of these forms.
69
+ */
70
+ const looseIpv4Octets = (host: string): [number, number, number, number] | null => {
71
+ if (isIP(host) === 4) {
72
+ const parts = host.split('.').map(Number);
73
+ return [parts[0]!, parts[1]!, parts[2]!, parts[3]!];
74
+ }
75
+ // Bare integer or hex integer → a single 32-bit value.
76
+ const asInt = /^0x[0-9a-f]+$/i.test(host)
77
+ ? Number.parseInt(host, 16)
78
+ : /^[0-9]+$/.test(host)
79
+ ? Number.parseInt(host, 10)
80
+ : Number.NaN;
81
+ if (Number.isInteger(asInt) && asInt >= 0 && asInt <= 0xffffffff)
82
+ return [(asInt >>> 24) & 0xff, (asInt >>> 16) & 0xff, (asInt >>> 8) & 0xff, asInt & 0xff];
83
+ return null;
84
+ };
85
+
86
+ /** Whether an IPv4 (as octets) is in a private, loopback, link-local or reserved range. */
87
+ const isBlockedIpv4 = ([a, b]: [number, number, number, number]): boolean => {
88
+ if (a === 0 || a === 10 || a === 127) return true; // this-host, private, loopback
89
+ if (a === 169 && b === 254) return true; // link-local — INCLUDES 169.254.169.254 metadata
90
+ if (a === 172 && b >= 16 && b <= 31) return true; // private
91
+ if (a === 192 && b === 168) return true; // private
92
+ if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT (100.64/10)
93
+ if (a === 198 && (b === 18 || b === 19)) return true; // benchmarking
94
+ if (a >= 224) return true; // multicast (224/4) + reserved (240/4) + 255.255.255.255
95
+ return false;
96
+ };
97
+
98
+ /**
99
+ * Extract the embedded IPv4 of an IPv4-mapped IPv6 address. `new URL` normalizes
100
+ * `::ffff:127.0.0.1` to the HEX form `::ffff:7f00:1`, so both the dotted and the
101
+ * two-hextet forms have to be handled.
102
+ */
103
+ const mappedV4Octets = (head: string): [number, number, number, number] | null => {
104
+ const rest = /^::ffff:(.+)$/i.exec(head)?.[1];
105
+ if (!rest) return null;
106
+ if (rest.includes('.')) return looseIpv4Octets(rest);
107
+ const groups = rest.split(':');
108
+ if (groups.length !== 2) return null;
109
+ const hi = Number.parseInt(groups[0]!, 16);
110
+ const lo = Number.parseInt(groups[1]!, 16);
111
+ if (!Number.isInteger(hi) || !Number.isInteger(lo)) return null;
112
+ return [(hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff, lo & 0xff];
113
+ };
114
+
115
+ /** Whether an IPv6 literal (already `net.isIP`-classified) is non-public. */
116
+ const isBlockedIpv6 = (raw: string): boolean => {
117
+ const head = raw.toLowerCase().replace(/^\[|\]$/g, '');
118
+ if (head === '::1' || head === '::') return true; // loopback, unspecified
119
+ const mapped = mappedV4Octets(head);
120
+ if (mapped && isBlockedIpv4(mapped)) return true; // IPv4-mapped (`::ffff:…`)
121
+ // fe80::/10 link-local (fe80–febf) and fec0::/10 deprecated site-local (fec0–feff).
122
+ if (/^fe[89ab]/.test(head) || /^fe[c-f]/.test(head)) return true;
123
+ if (head.startsWith('fc') || head.startsWith('fd')) return true; // fc00::/7 unique-local (incl. fd00:ec2::254 metadata)
124
+ if (head.startsWith('ff')) return true; // ff00::/8 multicast
125
+ return false;
126
+ };
127
+
128
+ const isBlockedHostname = (host: string): boolean => {
129
+ if (BLOCKED_HOST_EXACT.has(host)) return true;
130
+ return BLOCKED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
131
+ };
132
+
133
+ /**
134
+ * Structural SSRF guard: parse `rawUrl`, enforce scheme, reject credentials, and
135
+ * refuse any host that is a private/loopback/link-local/reserved IP literal (in
136
+ * any encoding) or a well-known internal hostname. Returns the parsed URL.
137
+ *
138
+ * @throws {AppError} `OUTBOUND_URL_BLOCKED` on any violation.
139
+ */
140
+ export const assertPublicOutboundUrl = (rawUrl: string, options: OutboundGuardOptions = {}): URL => {
141
+ const allowSchemes = options.allowSchemes ?? ['http:', 'https:'];
142
+ const allowHosts = new Set((options.allowHosts ?? []).map((h) => h.toLowerCase()));
143
+
144
+ let url: URL;
145
+ try {
146
+ url = new URL(rawUrl);
147
+ } catch {
148
+ throw blocked('not a valid absolute URL', {});
149
+ }
150
+
151
+ if (!allowSchemes.includes(url.protocol))
152
+ throw blocked(`scheme '${url.protocol}' is not allowed`, { scheme: url.protocol });
153
+
154
+ // Credentials in the URL are a redirect/parsing-confusion vector and never
155
+ // legitimate for a tool send.
156
+ if (url.username !== '' || url.password !== '') throw blocked('credentials in URL are not allowed', {});
157
+
158
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
159
+ if (allowHosts.has(host)) return url; // explicit host opt-in bypasses the blocks
160
+
161
+ const v4 = looseIpv4Octets(host);
162
+ if (v4) {
163
+ if (isBlockedIpv4(v4)) throw blocked('target resolves to a private or reserved address', { host });
164
+ return url;
165
+ }
166
+ if (isIP(host) === 6) {
167
+ if (isBlockedIpv6(host)) throw blocked('target resolves to a private or reserved address', { host });
168
+ return url;
169
+ }
170
+ if (isBlockedHostname(host)) throw blocked('target host is internal-only', { host });
171
+
172
+ return url;
173
+ };
174
+
175
+ /**
176
+ * DNS-aware SSRF guard: the structural checks of {@link assertPublicOutboundUrl}
177
+ * PLUS a resolution pass — every address the hostname resolves to is re-checked,
178
+ * so a public name pointing at a private IP is refused. Still racy against an
179
+ * attacker who rebinds between this lookup and the actual connect, which is why
180
+ * the SaaS profile must also enforce network-layer egress; but it closes the
181
+ * naive `evil.example → 169.254.169.254` vector the structural pass cannot see.
182
+ *
183
+ * @throws {AppError} `OUTBOUND_URL_BLOCKED` on any violation.
184
+ */
185
+ export const assertResolvedPublicUrl = async (rawUrl: string, options: OutboundGuardOptions = {}): Promise<URL> => {
186
+ const url = assertPublicOutboundUrl(rawUrl, options);
187
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
188
+
189
+ // A literal IP or an explicitly-allowed host needs no resolution recheck.
190
+ if (isIP(host) !== 0 || looseIpv4Octets(host) || (options.allowHosts ?? []).some((h) => h.toLowerCase() === host))
191
+ return url;
192
+
193
+ let addresses: { address: string; family: number }[];
194
+ try {
195
+ addresses = await lookup(host, { all: true });
196
+ } catch {
197
+ throw blocked('target hostname did not resolve', { host });
198
+ }
199
+ for (const { address } of addresses) {
200
+ const octets = looseIpv4Octets(address);
201
+ if (octets ? isBlockedIpv4(octets) : isIP(address) === 6 && isBlockedIpv6(address))
202
+ throw blocked('target hostname resolves to a private or reserved address', { host });
203
+ }
204
+ return url;
205
+ };
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Live delivery for the notification center — the half that makes a bell badge
3
+ * change without anybody clicking anything.
4
+ *
5
+ * The center already has the seam (`NotificationSink`); what was missing was a
6
+ * transport behind it. This is that transport, as **SSE** rather than a
7
+ * WebSocket, and the reason is the shape of the traffic: notifications are
8
+ * server→client only, they must survive a laptop lid closing, and a browser
9
+ * reconnects an `EventSource` by itself. A WebSocket would mean writing the
10
+ * reconnect loop, the heartbeat and the backoff that `EventSource` already has
11
+ * — and `cursedbelt-server/ws` remains the right answer for anything the client
12
+ * needs to send back up.
13
+ *
14
+ * 🔴 **A hub is a cache of who is listening, never a queue.** A notification is
15
+ * already durable (it is a row) before a sink is called, so a subscriber that
16
+ * is asleep, disconnected or behind a proxy that ate the stream loses nothing:
17
+ * the next list/poll returns the same row. That is why every emit here is
18
+ * fire-and-forget and a failing subscriber is dropped rather than retried — the
19
+ * poll transport in `cursedbelt/react/notifications` is the backstop, and it is
20
+ * not optional.
21
+ */
22
+ import type { AppNotification, NotificationSink } from "cwip/notifications";
23
+
24
+ /** What a subscriber receives. `unread` rides alongside so a bell badge never
25
+ * has to make a second request to learn its own number. */
26
+ export type NotificationHubEvent =
27
+ | { kind: "notification"; notification: AppNotification }
28
+ | { kind: "unread"; userId: string; count: number };
29
+
30
+ export type NotificationHubListener = (event: NotificationHubEvent) => void;
31
+
32
+ export interface NotificationHubOptions {
33
+ /**
34
+ * Comment-line heartbeat cadence, in ms. Default 25s — under the 30s idle
35
+ * timeout nginx and Cloudflare both default to, which is what silently kills
36
+ * an otherwise healthy SSE stream. 0 disables it (tests).
37
+ */
38
+ heartbeatMs?: number;
39
+ /**
40
+ * Max concurrent streams per user. Default 8. A tab is a stream, and a
41
+ * browser left open for a week accumulates them; without a cap one person's
42
+ * tab habit is an unbounded server-side array.
43
+ */
44
+ maxStreamsPerUser?: number;
45
+ /** Reported when a listener throws. Never rethrown — see the module doc. */
46
+ onError?: (error: unknown, context: string) => void;
47
+ }
48
+
49
+ export interface NotificationHub {
50
+ /**
51
+ * Hand this to `createNotificationCenter({ sink })`. Every `notify*` then
52
+ * reaches the connected tabs of exactly the addressed user.
53
+ */
54
+ readonly sink: NotificationSink;
55
+ /** Register a listener for one user's events. Returns an unsubscribe fn. */
56
+ subscribe(userId: string, listener: NotificationHubListener): () => void;
57
+ /** An SSE `Response` for one user — the thing a route returns. */
58
+ stream(userId: string, options?: { signal?: AbortSignal }): Response;
59
+ /** How many live subscribers a user has (all users when omitted). */
60
+ subscriberCount(userId?: string): number;
61
+ /** Drop every subscriber and stop every heartbeat (shutdown, and tests). */
62
+ close(): void;
63
+ }
64
+
65
+ const SSE_HEADERS: Record<string, string> = {
66
+ "content-type": "text/event-stream; charset=utf-8",
67
+ "cache-control": "no-store, no-transform",
68
+ connection: "keep-alive",
69
+ // nginx buffers proxied responses by default, which turns a live stream into
70
+ // one delivered when it ends. The fleet's box is behind nginx; this is the
71
+ // header that opts out, and it is inert everywhere else.
72
+ "x-accel-buffering": "no",
73
+ };
74
+
75
+ const encodeEvent = (event: NotificationHubEvent): string => {
76
+ const name = event.kind;
77
+ const data = event.kind === "notification" ? event.notification : { count: event.count };
78
+ return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
79
+ };
80
+
81
+ export function createNotificationHub(options: NotificationHubOptions = {}): NotificationHub {
82
+ const heartbeatMs = options.heartbeatMs ?? 25_000;
83
+ const maxStreamsPerUser = options.maxStreamsPerUser ?? 8;
84
+ const onError = options.onError ?? (() => {});
85
+ /** userId → listeners. An empty set is deleted, so the map never grows with
86
+ * the user table — only with who is actually connected right now. */
87
+ const listeners = new Map<string, Set<NotificationHubListener>>();
88
+ let closed = false;
89
+
90
+ const emit = (userId: string, event: NotificationHubEvent): void => {
91
+ const set = listeners.get(userId);
92
+ if (!set) return;
93
+ // Snapshot: a listener may unsubscribe itself while being called (a
94
+ // stream that has just been cancelled does exactly that).
95
+ for (const listener of [...set]) {
96
+ try {
97
+ listener(event);
98
+ } catch (error) {
99
+ onError(error, "notificationHub.listener");
100
+ set.delete(listener);
101
+ }
102
+ }
103
+ if (set.size === 0) listeners.delete(userId);
104
+ };
105
+
106
+ const subscribe: NotificationHub["subscribe"] = (userId, listener) => {
107
+ if (closed) return () => {};
108
+ let set = listeners.get(userId);
109
+ if (!set) {
110
+ set = new Set();
111
+ listeners.set(userId, set);
112
+ }
113
+ // Oldest-first eviction: the tab that has been open longest is the one
114
+ // least likely to be looked at, and dropping it costs nothing (the poll
115
+ // backstop still runs there).
116
+ while (set.size >= maxStreamsPerUser) {
117
+ const oldest = set.values().next().value;
118
+ if (!oldest) break;
119
+ set.delete(oldest);
120
+ try {
121
+ oldest({ kind: "unread", userId, count: -1 });
122
+ } catch (error) {
123
+ onError(error, "notificationHub.evict");
124
+ }
125
+ }
126
+ set.add(listener);
127
+ return () => {
128
+ const current = listeners.get(userId);
129
+ if (!current) return;
130
+ current.delete(listener);
131
+ if (current.size === 0) listeners.delete(userId);
132
+ };
133
+ };
134
+
135
+ return {
136
+ sink: {
137
+ onNotification(notification) {
138
+ emit(notification.userId, { kind: "notification", notification });
139
+ },
140
+ onUnread(userId, count) {
141
+ emit(userId, { kind: "unread", userId, count });
142
+ },
143
+ },
144
+
145
+ subscribe,
146
+
147
+ stream(userId, streamOptions = {}) {
148
+ let unsubscribe: (() => void) | undefined;
149
+ let heartbeat: ReturnType<typeof setInterval> | undefined;
150
+ let onAbort: (() => void) | undefined;
151
+ const encoder = new TextEncoder();
152
+
153
+ const body = new ReadableStream<Uint8Array>({
154
+ start(controller) {
155
+ let live = true;
156
+ const send = (chunk: string) => {
157
+ if (!live) return;
158
+ try {
159
+ controller.enqueue(encoder.encode(chunk));
160
+ } catch (error) {
161
+ // The peer went away between our check and the write. Tear the
162
+ // subscription down rather than logging once per event forever.
163
+ live = false;
164
+ onError(error, "notificationHub.enqueue");
165
+ teardown();
166
+ }
167
+ };
168
+ const teardown = () => {
169
+ live = false;
170
+ unsubscribe?.();
171
+ unsubscribe = undefined;
172
+ if (heartbeat) clearInterval(heartbeat);
173
+ heartbeat = undefined;
174
+ if (onAbort) streamOptions.signal?.removeEventListener("abort", onAbort);
175
+ onAbort = undefined;
176
+ };
177
+
178
+ // A comment line first: it flushes proxy buffers and tells the
179
+ // client the stream is genuinely open before anything happens.
180
+ send(": connected\n\n");
181
+ unsubscribe = subscribe(userId, (event) => {
182
+ // The eviction poke (count -1) is not a real event — it means
183
+ // "this stream lost its slot", so close rather than report it.
184
+ if (event.kind === "unread" && event.count < 0) {
185
+ teardown();
186
+ try {
187
+ controller.close();
188
+ } catch {
189
+ /* already closed */
190
+ }
191
+ return;
192
+ }
193
+ send(encodeEvent(event));
194
+ });
195
+ if (heartbeatMs > 0) {
196
+ heartbeat = setInterval(() => send(": ping\n\n"), heartbeatMs);
197
+ // Never hold the process open for a heartbeat — a satellite that
198
+ // is shutting down must not wait out someone's open tab.
199
+ (heartbeat as unknown as { unref?: () => void }).unref?.();
200
+ }
201
+ if (streamOptions.signal) {
202
+ onAbort = () => {
203
+ teardown();
204
+ try {
205
+ controller.close();
206
+ } catch {
207
+ /* already closed */
208
+ }
209
+ };
210
+ streamOptions.signal.addEventListener("abort", onAbort, { once: true });
211
+ }
212
+ },
213
+ cancel() {
214
+ unsubscribe?.();
215
+ unsubscribe = undefined;
216
+ if (heartbeat) clearInterval(heartbeat);
217
+ heartbeat = undefined;
218
+ },
219
+ });
220
+
221
+ return new Response(body, { status: 200, headers: SSE_HEADERS });
222
+ },
223
+
224
+ subscriberCount(userId) {
225
+ if (userId !== undefined) return listeners.get(userId)?.size ?? 0;
226
+ let total = 0;
227
+ for (const set of listeners.values()) total += set.size;
228
+ return total;
229
+ },
230
+
231
+ close() {
232
+ closed = true;
233
+ for (const [userId, set] of [...listeners]) {
234
+ for (const listener of [...set]) {
235
+ try {
236
+ listener({ kind: "unread", userId, count: -1 });
237
+ } catch (error) {
238
+ onError(error, "notificationHub.close");
239
+ }
240
+ }
241
+ }
242
+ listeners.clear();
243
+ },
244
+ };
245
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `cursedbelt-server/notifications` — the SERVER half of the fleet's one
3
+ * notification primitive.
4
+ *
5
+ * ── What was already here, and what was missing ─────────────────────────────
6
+ * The dispatch engine has lived in `cwip/notifications` for months (a
7
+ * persistent per-user row, `groupKey` coalescing, unread counts, deep links, a
8
+ * live-delivery sink) and the whole client has lived in
9
+ * `cursedbelt/react/notifications` (store, bell, feed, inbox, poll transport).
10
+ * What did NOT exist was a way to MOUNT any of it: the only mounted surface in
11
+ * the fleet was `orch-ui/services/notifications`, written against
12
+ * `@satellites/satellite-rpc` inside a package only the orchestrator consumes.
13
+ * So a satellite wanting a bell had two options, and both were wrong — adopt
14
+ * orch's rpc layer, or write the routes again.
15
+ *
16
+ * This module is the third: ordinary Hono routes, an SSE hub behind the sink
17
+ * the center already had, and a migration for apps using cursedbelt's runner.
18
+ *
19
+ * const notifications = createNotificationService({ db });
20
+ * notifications.mount(app, { authorize: (c) => userOf(c)?.id ?? null });
21
+ *
22
+ * and on the client:
23
+ *
24
+ * const { store } = createNotificationsBinding(); // cursedbelt/react/notifications
25
+ * <NotificationBell store={store} />
26
+ *
27
+ * ── The three properties worth knowing ──────────────────────────────────────
28
+ * 1. **A notification is a row before it is an event.** Delivery is a cache of
29
+ * who is listening; the poll transport is the backstop and is not optional.
30
+ * Nothing is lost by a closed laptop.
31
+ * 2. **Coalescing is a database constraint, not a debounce.** The partial
32
+ * unique index on `(user_id, group_key) WHERE group_key IS NOT NULL AND
33
+ * done = 0` is what makes "twenty of the same thing" one row with a count,
34
+ * even under concurrent producers.
35
+ * 3. **The routes address the session's user and nothing else.** There is no
36
+ * route that notifies somebody else; producing is server-side code calling
37
+ * `center.notify*`.
38
+ */
39
+ export { createNotificationHub } from "./hub";
40
+ export type {
41
+ NotificationHub,
42
+ NotificationHubEvent,
43
+ NotificationHubListener,
44
+ NotificationHubOptions,
45
+ } from "./hub";
46
+ export { NOTIFICATION_MIGRATIONS, NOTIFICATIONS_TABLE } from "./migrations";
47
+ export { mountNotificationRoutes } from "./routes";
48
+ export type { NotificationRouteCenter, NotificationRoutesOptions } from "./routes";
49
+ export { createNotificationService } from "./service";
50
+ export type {
51
+ CreateNotificationServiceOptions,
52
+ NotificationService,
53
+ NotificationServiceMountOptions,
54
+ } from "./service";
55
+ // Re-exported so a server consumer needs ONE import for the whole contract —
56
+ // the same courtesy `server/sharing` extends over `cursedbelt-core/sharing`. The
57
+ // engine remains cwip's; this is a door, not a copy.
58
+ export {
59
+ createNotificationCenter,
60
+ createMemoryNotificationStore,
61
+ createSqliteNotificationStore,
62
+ migrateNotificationsSchema,
63
+ } from "cwip/notifications";
64
+ export type {
65
+ AppNotification,
66
+ ListNotificationsQuery,
67
+ NotificationAudience,
68
+ NotificationCenter,
69
+ NotificationCenterOptions,
70
+ NotificationDraft,
71
+ NotificationFilter,
72
+ NotificationPage,
73
+ NotificationSeverity,
74
+ NotificationSink,
75
+ NotificationStore,
76
+ Notifier,
77
+ } from "cwip/notifications";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The notification table, as a migration an app runs inside its OWN database.
3
+ *
4
+ * Same reasoning as `../sharing/migrations.ts`: the SCHEMA is shared, the DATA
5
+ * is local. A notification is about one app's rows and is read on the same page
6
+ * that reads them, so a central notification service would put a network hop in
7
+ * front of a badge count — and the first thing an app would do is cache it.
8
+ *
9
+ * 🔴 The DDL itself is NOT written here. `cwip/notifications` already owns it
10
+ * (`migrateNotificationsSchema`, which the sqlite store calls on construction),
11
+ * and a second copy in this file is a schema that agrees with the store until
12
+ * the day it doesn't. This module exists to give apps that use cursedbelt's
13
+ * migration RUNNER an ordered, once-only entry for it — the DDL is idempotent,
14
+ * so both routes converge on one table.
15
+ */
16
+ import type { Database } from "bun:sqlite";
17
+ import { migrateNotificationsSchema, type NotificationDb } from "cwip/notifications";
18
+ import type { Migration } from "../migration/types";
19
+
20
+ export const NOTIFICATIONS_TABLE = "notifications";
21
+
22
+ /**
23
+ * Ordered migrations for the notification table.
24
+ *
25
+ * applyRecommendedPragmas(db);
26
+ * runMigrations(db, [...APP_MIGRATIONS, ...NOTIFICATION_MIGRATIONS]);
27
+ *
28
+ * An app that instead constructs `createSqliteNotificationStore(db)` gets the
29
+ * same table without this — the store migrates on construction. Running both is
30
+ * harmless and is the normal state of an app that adopted the store first.
31
+ */
32
+ export const NOTIFICATION_MIGRATIONS: readonly Migration[] = [
33
+ {
34
+ name: "0001_notifications",
35
+ up(db: Database): void {
36
+ migrateNotificationsSchema(db as unknown as NotificationDb, NOTIFICATIONS_TABLE);
37
+ },
38
+ },
39
+ ];