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,92 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ /**
4
+ * In-memory, batched writer for `request_metrics`. To keep request latency
5
+ * untouched we never write per request: rows land in a buffer and a single batched
6
+ * transaction flushes them on an interval (and eagerly when the buffer is large).
7
+ *
8
+ * This is the reusable successor to ca's `metricsBuffer` module, retargeted at the
9
+ * task-22 `request_metrics` schema (no `created_at` column — `ts` is the epoch-ms
10
+ * timestamp). The flush timer is `unref()`'d so it never keeps the process alive.
11
+ */
12
+
13
+ export interface MetricRow {
14
+ /** Epoch ms. */
15
+ ts: number;
16
+ method: string | null;
17
+ /** Normalized route label (see `normalizeRoute`). */
18
+ route: string | null;
19
+ status: number | null;
20
+ durationMs: number | null;
21
+ bytesOut: number | null;
22
+ userId: string | null;
23
+ }
24
+
25
+ export interface MetricsBufferOptions {
26
+ db: Database;
27
+ /** Flush cadence in ms. Default: 1500. */
28
+ flushIntervalMs?: number;
29
+ /** Eager-flush threshold — caps memory if a flush stalls. Default: 5000. */
30
+ maxSize?: number;
31
+ }
32
+
33
+ export interface MetricsBuffer {
34
+ /** Enqueue one request's metrics (non-blocking; flushed in batches). */
35
+ record(row: MetricRow): void;
36
+ /** Force-flush the buffer now (also used on shutdown + in tests). */
37
+ flush(): void;
38
+ /** Stop the flush timer and drain whatever remains. */
39
+ stop(): void;
40
+ /** Rows currently buffered (not yet flushed). */
41
+ readonly size: number;
42
+ }
43
+
44
+ export function createMetricsBuffer(opts: MetricsBufferOptions): MetricsBuffer {
45
+ const { db, flushIntervalMs = 1500, maxSize = 5000 } = opts;
46
+ let buffer: MetricRow[] = [];
47
+
48
+ const insert = db.prepare(
49
+ `INSERT INTO request_metrics (id, ts, method, route, status, duration_ms, bytes_out, user_id)
50
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
51
+ );
52
+ const insertMany = db.transaction((rows: MetricRow[]) => {
53
+ for (const r of rows) {
54
+ insert.run(
55
+ crypto.randomUUID(),
56
+ r.ts,
57
+ r.method,
58
+ r.route,
59
+ r.status,
60
+ r.durationMs,
61
+ r.bytesOut,
62
+ r.userId,
63
+ );
64
+ }
65
+ });
66
+
67
+ const flush = (): void => {
68
+ if (buffer.length === 0) return;
69
+ const batch = buffer;
70
+ buffer = [];
71
+ insertMany(batch);
72
+ };
73
+
74
+ const timer = setInterval(flush, flushIntervalMs);
75
+ // Never let the metrics flusher hold the event loop / process open.
76
+ timer.unref?.();
77
+
78
+ return {
79
+ record(row) {
80
+ buffer.push(row);
81
+ if (buffer.length >= maxSize) flush();
82
+ },
83
+ flush,
84
+ stop() {
85
+ clearInterval(timer);
86
+ flush();
87
+ },
88
+ get size() {
89
+ return buffer.length;
90
+ },
91
+ };
92
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { Hono } from 'hono';
3
+ import { normalizeRoute } from './normalizeRoute';
4
+
5
+ // Probe through a real Hono app so `c.req.routePath` is the genuine matched pattern.
6
+ async function labelFor(register: (app: Hono) => void, request: Request): Promise<string> {
7
+ const app = new Hono();
8
+ register(app);
9
+ const res = await app.fetch(request); // Hono may return Response or Promise<Response>
10
+ return res.text();
11
+ }
12
+
13
+ const probe = (c: import('hono').Context) => {
14
+ const { method, route } = normalizeRoute(c);
15
+ return c.text(`${method} ${route}`);
16
+ };
17
+
18
+ describe('normalizeRoute', () => {
19
+ test('uses the matched route pattern (with :params), not the concrete path', async () => {
20
+ const label = await labelFor(
21
+ (app) => app.get('/api/v1/notes/:id', probe),
22
+ new Request('http://x/api/v1/notes/abc-123'),
23
+ );
24
+ expect(label).toBe('GET /api/v1/notes/:id');
25
+ });
26
+
27
+ test('collapses dynamic-looking segments on unmatched paths (bounds cardinality)', async () => {
28
+ const label = await labelFor(
29
+ (app) => app.notFound(probe),
30
+ new Request('http://x/files/550e8400-e29b-41d4-a716-446655440000/raw'),
31
+ );
32
+ expect(label).toBe('GET /files/:id/raw');
33
+ });
34
+
35
+ test('collapses long numeric + opaque segments', async () => {
36
+ const label = await labelFor(
37
+ (app) => app.notFound(probe),
38
+ new Request('http://x/u/12345/blob/deadbeefdeadbeefdeadbeef'),
39
+ );
40
+ expect(label).toBe('GET /u/:id/blob/:id');
41
+ });
42
+
43
+ test('maps a ctgr GET-twin back to its real verb + path', async () => {
44
+ const label = await labelFor(
45
+ (app) => app.get('/api/ctgrch/notes/:id', probe),
46
+ new Request('http://x/api/ctgrch/notes/n1'),
47
+ );
48
+ // /ctgrch = PATCH twin → aggregates with the real PATCH /api/notes/:id
49
+ expect(label).toBe('PATCH /api/notes/:id');
50
+ });
51
+ });
@@ -0,0 +1,63 @@
1
+ import type { Context } from 'hono';
2
+
3
+ /**
4
+ * Turn a request into a stable, low-cardinality route label for metric aggregation
5
+ * (e.g. `/api/v1/notes/:id`). The method is tracked separately.
6
+ *
7
+ * Two wrinkles, same as the old Express normalizer:
8
+ * 1. ctgr GET-tunnelled mutations match the `/ctgr<xx>` twin route — we map them
9
+ * back to the real verb + path so a POST and its ctgr twin aggregate together.
10
+ * 2. Unmatched requests (404s, static, the SPA fallback) have no matched route
11
+ * pattern, so we collapse dynamic-looking segments to avoid unbounded
12
+ * cardinality blowing up the metrics table.
13
+ */
14
+
15
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
+ const LONG_HEX_RE = /^[0-9a-f]{16,}$/i;
17
+
18
+ // The ctgr twin URL is `/ctgr<last-2-of-method><url>`, so the two-letter suffix
19
+ // recovers the original verb (POST→st, PUT→ut, PATCH→ch, DELETE→te).
20
+ const CTGR_METHOD: Record<string, string> = { st: 'POST', ut: 'PUT', ch: 'PATCH', te: 'DELETE' };
21
+
22
+ function collapseSegment(seg: string): string {
23
+ if (!seg) return seg;
24
+ if (/^\d+$/.test(seg)) return ':id';
25
+ if (UUID_RE.test(seg)) return ':id';
26
+ if (LONG_HEX_RE.test(seg)) return ':id';
27
+ // Long opaque tokens (encoded paths, blobs) collapse so they don't explode route
28
+ // cardinality. Plain words stay as-is.
29
+ if (seg.length >= 24) return ':id';
30
+ return seg;
31
+ }
32
+
33
+ function collapsePath(path: string): string {
34
+ const parts = path.split('/').filter(Boolean).map(collapseSegment);
35
+ return `/${parts.join('/')}`;
36
+ }
37
+
38
+ export function normalizeRoute(c: Context): { method: string; route: string } {
39
+ let method = c.req.method;
40
+
41
+ // Hono's matched route pattern (with :params) is the ideal label. A wildcard
42
+ // (`/*`, `*`) or empty pattern means "no real match" → collapse the raw path.
43
+ // `c.req.routePath` THROWS when nothing matched (e.g. a 404 / the SPA fallback),
44
+ // so guard it — an unmatched request must still produce a metrics label.
45
+ let matched: string | undefined;
46
+ try {
47
+ matched = c.req.routePath;
48
+ } catch {
49
+ matched = undefined;
50
+ }
51
+ let route =
52
+ matched && !matched.includes('*') ? matched : collapsePath(c.req.path.split('?')[0]) || '/';
53
+
54
+ // Map a ctgr GET-twin back to its real verb + url:
55
+ // /api/ctgrch/notes/:id → PATCH /api/notes/:id
56
+ const ctgr = route.match(/\/ctgr([a-z]{2})(?=\/|$)/i);
57
+ if (ctgr) {
58
+ method = CTGR_METHOD[ctgr[1].toLowerCase()] ?? method;
59
+ route = route.replace(/\/ctgr[a-z]{2}/i, '') || '/';
60
+ }
61
+
62
+ return { method, route };
63
+ }
@@ -0,0 +1,24 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { CursedbeltEnv } from '../context';
3
+
4
+ const HEADER = 'x-correlation-id';
5
+ // Accept a client-supplied id only if it is a sane, header-safe token (no CRLF /
6
+ // control chars / unbounded length) — otherwise mint a fresh one. This both
7
+ // preserves trace ids across a proxy hop and blocks response-header injection.
8
+ const SAFE_ID = /^[A-Za-z0-9._:-]{1,200}$/;
9
+
10
+ /**
11
+ * Assign every request a correlation id: reuse a safe inbound `x-correlation-id`
12
+ * (so a trace survives the proxy hop), else mint a `crypto.randomUUID()`. Stored on
13
+ * `c.set('correlationId', id)` for downstream middleware (request logger, error
14
+ * envelope) and echoed on the response header. Mount this FIRST.
15
+ */
16
+ export function correlationId(): MiddlewareHandler<CursedbeltEnv> {
17
+ return async (c, next) => {
18
+ const inbound = c.req.header(HEADER);
19
+ const id = inbound && SAFE_ID.test(inbound) ? inbound : crypto.randomUUID();
20
+ c.set('correlationId', id);
21
+ c.header(HEADER, id);
22
+ await next();
23
+ };
24
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createApp } from '../app';
3
+ import { corsAllowlist, corsOptsFromEnv } from './corsAllowlist';
4
+
5
+ const SITE = 'example.com';
6
+ const APEX = 'https://example.com';
7
+ const API_PATH = '/api/thing';
8
+
9
+ const appWithCors = () => {
10
+ const app = createApp({ cors: { siteDomain: SITE, origins: ['http://localhost:5173'] } });
11
+ app.get(API_PATH, (c) => c.json({ ok: true }));
12
+ return app;
13
+ };
14
+
15
+ const preflight = (app: ReturnType<typeof createApp>, origin: string) =>
16
+ app.request(API_PATH, {
17
+ method: 'OPTIONS',
18
+ headers: {
19
+ origin,
20
+ 'access-control-request-method': 'GET',
21
+ 'access-control-request-headers': 'content-type',
22
+ },
23
+ });
24
+
25
+ describe('corsAllowlist via createApp', () => {
26
+ test('preflight from the apex: allowed, credentialed, methods advertised', async () => {
27
+ const res = await preflight(appWithCors(), APEX);
28
+ expect(res.status).toBe(204);
29
+ expect(res.headers.get('access-control-allow-origin')).toBe(APEX);
30
+ expect(res.headers.get('access-control-allow-credentials')).toBe('true');
31
+ expect(res.headers.get('access-control-allow-methods')).toContain('POST');
32
+ });
33
+
34
+ test('preflight from any subdomain (present or future): allowed via siteDomain', async () => {
35
+ for (const origin of [
36
+ 'https://api.example.com',
37
+ 'https://sandbox.example.com',
38
+ 'https://brand-new-subdomain.example.com',
39
+ ]) {
40
+ const res = await preflight(appWithCors(), origin);
41
+ expect(res.status).toBe(204);
42
+ expect(res.headers.get('access-control-allow-origin')).toBe(origin);
43
+ }
44
+ });
45
+
46
+ test('extra origins rule (dev Vite) allowed', async () => {
47
+ const res = await preflight(appWithCors(), 'http://localhost:5173');
48
+ expect(res.headers.get('access-control-allow-origin')).toBe('http://localhost:5173');
49
+ });
50
+
51
+ test('unknown origin: NO allow-origin header (browser-visible rejection), never *', async () => {
52
+ for (const origin of [
53
+ 'https://evil.com',
54
+ 'https://example.com.evil.com',
55
+ 'http://example.com',
56
+ ]) {
57
+ const res = await preflight(appWithCors(), origin);
58
+ expect(res.headers.get('access-control-allow-origin')).toBeNull();
59
+ }
60
+ });
61
+
62
+ test('simple GET from an allowed origin echoes the origin on the response', async () => {
63
+ const res = await appWithCors().request(API_PATH, {
64
+ headers: { origin: 'https://api.example.com' },
65
+ });
66
+ expect(res.status).toBe(200);
67
+ expect(res.headers.get('access-control-allow-origin')).toBe('https://api.example.com');
68
+ expect(res.headers.get('vary')).toContain('Origin');
69
+ });
70
+
71
+ test('no-Origin request (curl / same-origin) is untouched', async () => {
72
+ const res = await appWithCors().request(API_PATH);
73
+ expect(res.status).toBe(200);
74
+ expect(res.headers.get('access-control-allow-origin')).toBeNull();
75
+ });
76
+
77
+ test('onReject fires with the rejected origin', async () => {
78
+ const rejected: string[] = [];
79
+ const app = createApp({ cors: { siteDomain: SITE, onReject: (o) => rejected.push(o) } });
80
+ app.get(API_PATH, (c) => c.json({ ok: true }));
81
+ await preflight(app, 'https://evil.com');
82
+ expect(rejected).toEqual(['https://evil.com']);
83
+ });
84
+
85
+ test('createApp without cors mounts nothing (same-origin deploy unchanged)', async () => {
86
+ const app = createApp();
87
+ app.get(API_PATH, (c) => c.json({ ok: true }));
88
+ const res = await app.request(API_PATH, { headers: { origin: APEX } });
89
+ expect(res.headers.get('access-control-allow-origin')).toBeNull();
90
+ });
91
+
92
+ test('a bad rule fails loudly at build time (boot-time typo detection)', () => {
93
+ expect(() => corsAllowlist({ origins: ['https://'] })).toThrow(/Invalid CORS origin rule/);
94
+ });
95
+ });
96
+
97
+ describe('corsOptsFromEnv', () => {
98
+ test('undefined when neither var set', () => {
99
+ expect(corsOptsFromEnv({})).toBeUndefined();
100
+ });
101
+
102
+ test('CORS_SITE_DOMAIN alone', () => {
103
+ expect(corsOptsFromEnv({ CORS_SITE_DOMAIN: 'example.com' })).toEqual({
104
+ siteDomain: 'example.com',
105
+ origins: [],
106
+ });
107
+ });
108
+
109
+ test('CORS_ORIGINS alone (legacy shape) and combined', () => {
110
+ expect(corsOptsFromEnv({ CORS_ORIGINS: 'https://a.com, *.b.com' })).toEqual({
111
+ siteDomain: undefined,
112
+ origins: ['https://a.com', '*.b.com'],
113
+ });
114
+ expect(
115
+ corsOptsFromEnv({ CORS_SITE_DOMAIN: 'example.com', CORS_ORIGINS: 'http://localhost:5173' }),
116
+ ).toEqual({ siteDomain: 'example.com', origins: ['http://localhost:5173'] });
117
+ });
118
+ });
@@ -0,0 +1,69 @@
1
+ import { compileOriginMatcher, type OriginRule, parseOriginList, siteOriginRules } from 'cwip/http';
2
+ import type { MiddlewareHandler } from 'hono';
3
+ import { cors } from 'hono/cors';
4
+
5
+ /**
6
+ * Cross-app CORS for the split deploy shape every cursedbelt app shares (SPA on
7
+ * one origin, API on another — e.g. `example.com` → `api.example.com`), built on
8
+ * Hono's `cors` + cwip's `compileOriginMatcher` rather than a per-app allow-list
9
+ * reimplementation.
10
+ *
11
+ * The one-line configuration is `siteDomain`: it allows the https apex AND every
12
+ * subdomain (`siteOriginRules`), so standing up a new subdomain can never break
13
+ * CORS again. `origins` adds exact origins / `*.domain` wildcards / RegExps on
14
+ * top (dev Vite ports, a partner origin). Responses to allowed origins echo the
15
+ * origin with `Access-Control-Allow-Credentials: true` (cookie-auth apps);
16
+ * disallowed origins get NO CORS headers, which is the browser-visible rejection.
17
+ */
18
+ export interface CorsOpts {
19
+ /**
20
+ * The site's apex domain (`'example.com'`) — allows `https://example.com` and
21
+ * `https://<any-subdomain>.example.com` in one rule.
22
+ */
23
+ siteDomain?: string;
24
+ /** Additional origin rules: exact, `*.domain.tld` wildcard, or RegExp. */
25
+ origins?: OriginRule[];
26
+ /** Send `Access-Control-Allow-Credentials: true` (default `true` — cookie auth). */
27
+ credentials?: boolean;
28
+ /** Called with each rejected `Origin` (for logging/metrics). */
29
+ onReject?: (origin: string) => void;
30
+ }
31
+
32
+ /** Build the Hono middleware from {@link CorsOpts}. Compiles the matcher once. */
33
+ export const corsAllowlist = (opts: CorsOpts): MiddlewareHandler => {
34
+ const rules: OriginRule[] = [
35
+ ...(opts.siteDomain ? siteOriginRules(opts.siteDomain) : []),
36
+ ...(opts.origins ?? []),
37
+ ];
38
+ const isAllowed = compileOriginMatcher(rules);
39
+ return cors({
40
+ origin: (origin) => {
41
+ if (isAllowed(origin)) {
42
+ return origin;
43
+ }
44
+ if (origin) {
45
+ opts.onReject?.(origin);
46
+ }
47
+ return null;
48
+ },
49
+ credentials: opts.credentials ?? true,
50
+ });
51
+ };
52
+
53
+ /**
54
+ * Resolve {@link CorsOpts} from the conventional env vars, `undefined` when
55
+ * neither is set (same-origin deploy — no CORS middleware needed):
56
+ * - `CORS_SITE_DOMAIN=example.com` — the systemic knob: apex + every subdomain.
57
+ * - `CORS_ORIGINS=https://a.com,*.b.com` — exact/wildcard extras (or the whole
58
+ * list, for apps that predate `CORS_SITE_DOMAIN`).
59
+ */
60
+ export const corsOptsFromEnv = (
61
+ env: Record<string, string | undefined> = process.env,
62
+ ): CorsOpts | undefined => {
63
+ const siteDomain = env.CORS_SITE_DOMAIN?.trim() || undefined;
64
+ const origins = parseOriginList(env.CORS_ORIGINS);
65
+ if (!siteDomain && origins.length === 0) {
66
+ return undefined;
67
+ }
68
+ return { siteDomain, origins };
69
+ };
@@ -0,0 +1,38 @@
1
+ import type { ErrorHandler } from 'hono';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
4
+ import type { CursedbeltEnv } from '../context';
5
+ import { ApiError, isAppError, toErrorEnvelope } from '../errors';
6
+
7
+ /**
8
+ * Hono `onError` handler. Converts ANY thrown value into the ONE canonical rich
9
+ * envelope — `{ error: { name, message, code?, status?, category?, context?,
10
+ * isOperational, timestamp }, correlationId? }` (the shared `toErrorEnvelope` from
11
+ * `cwip/error`, re-exported via cursedbelt-core/errors) — plus the right HTTP status,
12
+ * and echoes the correlation id header so a client can quote it in a bug report.
13
+ * This is the Hono twin of cwip's express `errorHandler`; both funnel through the
14
+ * same formatter, so their JSON is byte-identical for the same thrown error.
15
+ *
16
+ * - `ApiError` / any `AppError` → its `status` (else 500) + full summary.
17
+ * - `HTTPException` → normalized to an `ApiError` (its status + message).
18
+ * - anything else → 500 + a generic message; the real error is
19
+ * logged with the correlation id (never leaked).
20
+ */
21
+ export const errorEnvelope: ErrorHandler<CursedbeltEnv> = (err, c) => {
22
+ const cid = c.get('correlationId');
23
+ if (cid) c.header('x-correlation-id', cid);
24
+
25
+ // Hono's own thrown errors carry a status + safe message — normalize to an
26
+ // `ApiError` so they flow through the single rich-envelope path.
27
+ const normalized = err instanceof HTTPException ? new ApiError(err.status, err.message || 'Error') : err;
28
+
29
+ // Unexpected (non-`AppError`) errors: log the real thing (with the correlation
30
+ // id for cross-reference). The envelope stays sanitized — `toErrorEnvelope`
31
+ // returns a generic 500 message for non-`AppError`s, so internals never leak.
32
+ if (!isAppError(normalized)) {
33
+ console.error(`[error]${cid ? ` [${cid}]` : ''}`, err);
34
+ }
35
+
36
+ const envelope = toErrorEnvelope(normalized, { correlationId: cid });
37
+ return c.json(envelope, (envelope.error.status ?? 500) as ContentfulStatusCode);
38
+ };
@@ -0,0 +1,19 @@
1
+ // The cross-cutting cursedbelt server middleware set.
2
+ export { correlationId } from './correlationId';
3
+ export { type CorsOpts, corsAllowlist, corsOptsFromEnv } from './corsAllowlist';
4
+ export { errorEnvelope } from './errorEnvelope';
5
+ export { crossOriginRefusal } from './localOrigin';
6
+ export { clientIp, createRateLimiter, type RateLimitOpts } from './rateLimit';
7
+ export { type RequestLoggerOpts, requestLogger } from './requestLogger';
8
+ export {
9
+ type CspDirectives,
10
+ type SecurityHeadersOpts,
11
+ securityHeaders,
12
+ } from './securityHeaders';
13
+ export {
14
+ toValidationIssues,
15
+ VALIDATION_ERROR_CODE,
16
+ type ValidationIssue,
17
+ validationEnvelope,
18
+ } from '../errors';
19
+ export { type ZValidatorTarget, zValidator } from './zValidator';
@@ -0,0 +1,168 @@
1
+ // Both halves matter equally here. A guard that refuses the attack but also
2
+ // refuses curl would be reverted within a day, so the 'legitimate callers'
3
+ // block is not padding — it is the reason this can ship without a login.
4
+
5
+ import { describe, expect, test } from 'bun:test';
6
+ import { crossOriginRefusal } from './localOrigin';
7
+
8
+ function req(headers: Record<string, string>, url = 'http://127.0.0.1:4747/api/overview'): Request {
9
+ return new Request(url, { headers });
10
+ }
11
+
12
+ describe('the browser attacks that loopback binding does not stop', () => {
13
+ test('a cross-site POST from a website is refused (the measured CSRF vector)', () => {
14
+ // Verified by hand against the live daemon on 2026-08-06: this exact shape
15
+ // was accepted, parsed and reached field validation. `text/plain` is a
16
+ // SIMPLE content type, so no CORS preflight is required and the request
17
+ // fires; req.json() then parses it anyway.
18
+ const refusal = crossOriginRefusal(
19
+ req({
20
+ host: '127.0.0.1:4747',
21
+ origin: 'https://evil.example.com',
22
+ 'sec-fetch-site': 'cross-site',
23
+ 'content-type': 'text/plain',
24
+ }),
25
+ );
26
+ expect(refusal).toContain('evil.example.com');
27
+ });
28
+
29
+ test('a cross-origin Origin alone is enough — Sec-Fetch-Site may be absent', () => {
30
+ expect(
31
+ crossOriginRefusal(req({ host: '127.0.0.1:4747', origin: 'https://evil.example.com' })),
32
+ ).toContain('local-only');
33
+ });
34
+
35
+ test('a cross-site GET navigation with NO Origin is still refused', () => {
36
+ // A cross-site GET omits Origin, so Sec-Fetch-Site is the only signal.
37
+ expect(
38
+ crossOriginRefusal(req({ host: '127.0.0.1:4747', 'sec-fetch-site': 'cross-site' })),
39
+ ).toContain('does not serve other sites');
40
+ });
41
+
42
+ test('DNS rebinding is refused: a non-loopback Host cannot reach the machine API', () => {
43
+ // The attack that would otherwise make an attacker domain SAME-origin and
44
+ // lift the opaque-response limit, turning every GET into exfiltration.
45
+ expect(crossOriginRefusal(req({ host: 'attacker.example.com' }))).toContain('not a loopback');
46
+ });
47
+
48
+ test('Host is judged before Origin — a rebound host must not look same-origin', () => {
49
+ const refusal = crossOriginRefusal(
50
+ req({ host: 'rebind.example.com', origin: 'http://rebind.example.com' }),
51
+ );
52
+ expect(refusal).toContain('Host');
53
+ });
54
+
55
+ test('an unparseable Origin is refused rather than ignored', () => {
56
+ expect(crossOriginRefusal(req({ host: '127.0.0.1:4747', origin: '!!!' }))).toContain(
57
+ 'unparseable',
58
+ );
59
+ });
60
+ });
61
+
62
+ describe('every legitimate local caller still passes', () => {
63
+ test('curl / a bun fetch / the server-side machine proxy — no browser headers at all', () => {
64
+ expect(crossOriginRefusal(req({ host: '127.0.0.1:4747' }))).toBeNull();
65
+ });
66
+
67
+ test('no Host header at all (HTTP/1.0, some local clients)', () => {
68
+ expect(crossOriginRefusal(req({}))).toBeNull();
69
+ });
70
+
71
+ test('the status page served from :4747 itself — same-origin', () => {
72
+ expect(
73
+ crossOriginRefusal(
74
+ req({
75
+ host: '127.0.0.1:4747',
76
+ origin: 'http://127.0.0.1:4747',
77
+ 'sec-fetch-site': 'same-origin',
78
+ }),
79
+ ),
80
+ ).toBeNull();
81
+ });
82
+
83
+ test('a loopback SPA on ANOTHER port — cross-origin but same-site, and legitimate', () => {
84
+ // The wall is 'not this machine', not 'not another port'. The station SPA
85
+ // on :3333 and the vault UI are exactly this shape.
86
+ expect(
87
+ crossOriginRefusal(
88
+ req({
89
+ host: '127.0.0.1:4747',
90
+ origin: 'http://127.0.0.1:3333',
91
+ 'sec-fetch-site': 'same-site',
92
+ }),
93
+ ),
94
+ ).toBeNull();
95
+ });
96
+
97
+ test('localhost and IPv6 loopback are the same machine', () => {
98
+ expect(crossOriginRefusal(req({ host: 'localhost:4747' }))).toBeNull();
99
+ expect(crossOriginRefusal(req({ host: '[::1]:4747' }))).toBeNull();
100
+ expect(
101
+ crossOriginRefusal(req({ host: '127.0.0.1:4747', origin: 'http://localhost:3333' })),
102
+ ).toBeNull();
103
+ });
104
+
105
+ test('all of 127.0.0.0/8 is loopback, not just 127.0.0.1', () => {
106
+ expect(crossOriginRefusal(req({ host: '127.0.0.53:4747' }))).toBeNull();
107
+ });
108
+
109
+ test('Origin: "null" (a sandboxed iframe / file://) is not treated as a hostname', () => {
110
+ // It carries no origin information, so the Sec-Fetch-Site check decides.
111
+ expect(crossOriginRefusal(req({ host: '127.0.0.1:4747', origin: 'null' }))).toBeNull();
112
+ expect(
113
+ crossOriginRefusal(
114
+ req({ host: '127.0.0.1:4747', origin: 'null', 'sec-fetch-site': 'cross-site' }),
115
+ ),
116
+ ).toContain('does not serve other sites');
117
+ });
118
+
119
+ test('a host that merely CONTAINS a loopback name is not loopback', () => {
120
+ expect(crossOriginRefusal(req({ host: 'localhost.evil.example.com' }))).toContain(
121
+ 'not a loopback',
122
+ );
123
+ expect(crossOriginRefusal(req({ host: '127.0.0.1.evil.example.com' }))).toContain(
124
+ 'not a loopback',
125
+ );
126
+ });
127
+ });
128
+
129
+ describe('the label appears in the refusal, so a log line names the daemon', () => {
130
+ test('each daemon says which surface refused', () => {
131
+ // Three loopback daemons share this guard (station :4747, orch :3021,
132
+ // cloud :3022) and the owner reads these refusals in a log with no other
133
+ // context about which one spoke.
134
+ expect(
135
+ crossOriginRefusal(req({ host: '127.0.0.1:3021', origin: 'https://evil.example.com' }), 'the orchestrator engine'),
136
+ ).toContain('the orchestrator engine');
137
+ expect(crossOriginRefusal(req({ host: 'attacker.example.com' }), 'the cloud dashboard')).toContain(
138
+ 'the cloud dashboard',
139
+ );
140
+ });
141
+ });
142
+
143
+ describe('allowHosts — a local host-header proxy name', () => {
144
+ test('an allowed extra name passes as Host AND as Origin', () => {
145
+ // The dev gateway (:3300) serves the satellites under `<app>.localhost`.
146
+ const opts = { label: 'the cloud dashboard', allowHosts: ['cloud.localhost'] };
147
+ expect(crossOriginRefusal(req({ host: 'cloud.localhost:3300' }), opts)).toBeNull();
148
+ expect(
149
+ crossOriginRefusal(req({ host: 'cloud.localhost', origin: 'http://cloud.localhost' }), opts),
150
+ ).toBeNull();
151
+ });
152
+
153
+ test('it is matched WHOLE — a suffix is still not loopback', () => {
154
+ // The trap the base rule already avoids, re-pinned for the opt-in path:
155
+ // allowing `cloud.localhost` must not allow `cloud.localhost.evil.com`.
156
+ expect(
157
+ crossOriginRefusal(req({ host: 'cloud.localhost.evil.example.com' }), {
158
+ allowHosts: ['cloud.localhost'],
159
+ }),
160
+ ).toContain('not a loopback');
161
+ });
162
+
163
+ test('an unlisted name is refused even when another is allowed', () => {
164
+ expect(crossOriginRefusal(req({ host: 'orch.localhost' }), { allowHosts: ['cloud.localhost'] })).toContain(
165
+ 'not a loopback',
166
+ );
167
+ });
168
+ });