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,139 @@
1
+ /**
2
+ * The second lock on a LOOPBACK-ONLY HTTP daemon — the one binding to 127.0.0.1
3
+ * does not provide.
4
+ *
5
+ * ── The gap, measured 2026-08-06 against a live daemon ──────────────────────
6
+ * A machine-local daemon's whole trust model is "it only listens on 127.0.0.1",
7
+ * and for the NETWORK that is a real wall. For the owner's BROWSER it is not,
8
+ * for two reasons that have nothing to do with binding:
9
+ *
10
+ * 1. **CSRF.** Any page on any website can `POST http://127.0.0.1:<port>/...`.
11
+ * A cross-origin POST normally trips a CORS preflight the server would fail,
12
+ * but a preflight is only required for a NON-simple request — and
13
+ * `<form enctype="text/plain">` is simple, so it fires with no preflight at
14
+ * all. A body parser that calls `req.json()` parses it regardless of
15
+ * Content-Type, and the classic `text/plain` form trick smuggles valid JSON
16
+ * straight through. Verified by hand against the station daemon on :4747: a
17
+ * POST carrying `Content-Type: text/plain`, `Origin: https://evil.example.com`
18
+ * and `Sec-Fetch-Site: cross-site` was accepted, parsed, and reached field
19
+ * validation. The attacker cannot READ the reply (no CORS headers, so it
20
+ * stays opaque) — but destruction does not need a reply.
21
+ *
22
+ * 2. **DNS rebinding.** With no Host check, an attacker domain whose DNS flips
23
+ * to 127.0.0.1 becomes same-origin in the browser's eyes — which lifts the
24
+ * opaque-response limit above and turns every GET into an exfiltration
25
+ * channel.
26
+ *
27
+ * ── What this refuses, and why it costs nothing ─────────────────────────────
28
+ * A refusal needs a browser-only signal to be present and WRONG. Real local
29
+ * clients — curl, a bun `fetch` in an agent, a server-side proxy — send no
30
+ * `Origin` and no `Sec-Fetch-Site`, so they are untouched. A page served from
31
+ * the daemon itself is same-origin. A loopback SPA on ANOTHER port is
32
+ * `same-site` with a loopback `Origin`, and is allowed on purpose: the wall
33
+ * being defended is "not the local machine", not "not another port".
34
+ *
35
+ * This deliberately does NOT add a login. Loopback + owner-only remains the
36
+ * design for the daemons that use it; this only closes the hole where
37
+ * "loopback" silently included "every website the owner visits".
38
+ *
39
+ * ── Placement ───────────────────────────────────────────────────────────────
40
+ * Landed in station's own tree on 2026-08-06 and lifted here the same day, when
41
+ * orch (:3021) and cloud (:3022) turned out to need the identical guard —
42
+ * STANDARDS.md §1: three copies is one too many, and a request-header security
43
+ * check is not app-shaped. It sits beside `corsAllowlist` and `rateLimit`
44
+ * because it is the same kind of thing: a pure, header-only verdict about
45
+ * whether a request may be served.
46
+ */
47
+
48
+ /**
49
+ * Hosts that mean THIS machine. IPv6 arrives bracketed in a Host header.
50
+ *
51
+ * `extra` is for a daemon reached through a local host-header proxy under a
52
+ * name that is not itself a loopback literal — the dev gateway serves
53
+ * `cloud.localhost`, and note that the SUFFIX is deliberately not enough on its
54
+ * own (`localhost.evil.example.com` must stay refused), so each such name is
55
+ * listed exactly rather than pattern-matched.
56
+ */
57
+ function isLoopbackHost(hostname: string, extra: ReadonlySet<string>): boolean {
58
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, '');
59
+ return (
60
+ h === 'localhost' ||
61
+ h === '::1' ||
62
+ h === '0:0:0:0:0:0:0:1' ||
63
+ // 127.0.0.0/8 — all of it is loopback, not just 127.0.0.1.
64
+ /^127(\.\d{1,3}){3}$/.test(h) ||
65
+ extra.has(h)
66
+ );
67
+ }
68
+
69
+ export interface LocalOriginOpts {
70
+ /**
71
+ * Names the surface in the refusal text ("the machine API", "the orchestrator
72
+ * engine"). The owner reads these in a log line with no other context about
73
+ * which daemon spoke.
74
+ */
75
+ label?: string;
76
+ /**
77
+ * Extra EXACT hostnames that also mean this machine — a local host-header
78
+ * proxy's names (`cloud.localhost`). Matched whole, never as a suffix.
79
+ */
80
+ allowHosts?: readonly string[];
81
+ }
82
+
83
+ /** The hostname of a Host header value (`127.0.0.1:4747`, `[::1]:4747`). */
84
+ function hostnameOf(hostHeader: string): string {
85
+ const trimmed = hostHeader.trim();
86
+ if (trimmed.startsWith('[')) return trimmed.slice(0, trimmed.indexOf(']') + 1);
87
+ const colon = trimmed.lastIndexOf(':');
88
+ return colon === -1 ? trimmed : trimmed.slice(0, colon);
89
+ }
90
+
91
+ /**
92
+ * Why this request must be refused, or `null` to let it through.
93
+ *
94
+ * Pure and header-only so it is exhaustively testable without a socket. The
95
+ * caller decides the response; every current caller answers `403` with the
96
+ * reason as the body, and logs it — a silent refusal on a local daemon is
97
+ * indistinguishable from the daemon being down.
98
+ *
99
+ * @param opts see {@link LocalOriginOpts}. A bare string is accepted as the
100
+ * label, since that is the only thing most callers need.
101
+ */
102
+ export function crossOriginRefusal(req: Request, opts: LocalOriginOpts | string = {}): string | null {
103
+ const { label = 'this daemon', allowHosts = [] } = typeof opts === 'string' ? { label: opts } : opts;
104
+ const extra = new Set(allowHosts.map((h) => h.toLowerCase()));
105
+ // ── DNS rebinding: the Host must name this machine ──────────────────────
106
+ // Checked first: a rebound Host is the case where the OTHER signals start
107
+ // looking legitimately same-origin, so it has to fail before they are read.
108
+ const host = req.headers.get('host');
109
+ if (host && !isLoopbackHost(hostnameOf(host), extra)) {
110
+ return `Host "${host}" is not a loopback name — ${label} answers only to 127.0.0.1/localhost`;
111
+ }
112
+
113
+ // ── CSRF: a browser told us where this came from; believe it ─────────────
114
+ const origin = req.headers.get('origin');
115
+ if (origin && origin !== 'null') {
116
+ let originHost: string;
117
+ try {
118
+ originHost = new URL(origin).hostname;
119
+ } catch {
120
+ return `unparseable Origin "${origin}"`;
121
+ }
122
+ if (!isLoopbackHost(originHost, extra)) {
123
+ return `cross-origin request from "${origin}" — ${label} is local-only`;
124
+ }
125
+ }
126
+
127
+ // `same-site` is deliberately allowed: a loopback SPA on another port is
128
+ // cross-ORIGIN but same-SITE, and it is a legitimate local caller. Anything
129
+ // genuinely external reports `cross-site` and never reaches here anyway,
130
+ // because its Origin failed the loopback test above — this is the belt to
131
+ // that braces, for a browser that sends Sec-Fetch-Site but omits Origin
132
+ // (which is what a cross-site GET navigation does).
133
+ const site = req.headers.get('sec-fetch-site');
134
+ if (site && !['same-origin', 'same-site', 'none'].includes(site.toLowerCase())) {
135
+ return `Sec-Fetch-Site: ${site} — ${label} does not serve other sites`;
136
+ }
137
+
138
+ return null;
139
+ }
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { Hono } from 'hono';
3
+ import { clientIp, createRateLimiter } from './rateLimit';
4
+
5
+ /**
6
+ * `x-forwarded-for` is written by the caller. If the limiter keys on it without
7
+ * knowing how many proxies it actually trusts, an attacker gets a brand-new
8
+ * bucket on every request just by varying the header — which is the whole
9
+ * protection, gone. These lock the trust model down.
10
+ */
11
+ const app = (opts: Parameters<typeof createRateLimiter>[0]) => {
12
+ const a = new Hono();
13
+ a.use('*', createRateLimiter(opts));
14
+ a.get('/', (c) => c.text('ok'));
15
+ return a;
16
+ };
17
+
18
+ const hit = (a: Hono, xff?: string) =>
19
+ a.request('/', { headers: xff ? { 'x-forwarded-for': xff } : {} });
20
+
21
+ describe('createRateLimiter — forwarded headers are not trusted by default', () => {
22
+ it('cannot be bypassed by varying x-forwarded-for', async () => {
23
+ const a = app({ limit: 2, windowMs: 60_000 });
24
+ expect((await hit(a, '1.1.1.1')).status).toBe(200);
25
+ expect((await hit(a, '2.2.2.2')).status).toBe(200);
26
+ // A third request with yet another forged address must NOT get a fresh bucket.
27
+ expect((await hit(a, '3.3.3.3')).status).toBe(429);
28
+ expect((await hit(a, '4.4.4.4')).status).toBe(429);
29
+ });
30
+
31
+ it('still limits when no forwarded header is present at all', async () => {
32
+ const a = app({ limit: 1, windowMs: 60_000 });
33
+ expect((await hit(a)).status).toBe(200);
34
+ expect((await hit(a)).status).toBe(429);
35
+ });
36
+ });
37
+
38
+ describe('clientIp — trust is explicit and read right-to-left', () => {
39
+ const ctx = (xff?: string, xri?: string) => {
40
+ const headers = new Headers();
41
+ if (xff) headers.set('x-forwarded-for', xff);
42
+ if (xri) headers.set('x-real-ip', xri);
43
+ // Minimal Context stand-in: clientIp only reads headers, and getConnInfo
44
+ // throws without a real Bun socket (→ the documented 'unknown' fallback).
45
+ return { req: { header: (n: string) => headers.get(n) ?? undefined } } as never;
46
+ };
47
+
48
+ it('ignores the chain entirely with no trusted hops (the default)', () => {
49
+ expect(clientIp(ctx('9.9.9.9'))).toBe('unknown');
50
+ expect(clientIp(ctx('9.9.9.9'), { trustedProxyHops: 0 })).toBe('unknown');
51
+ });
52
+
53
+ it('with one trusted hop takes the entry that hop observed — the RIGHTMOST', () => {
54
+ // "forged, forged, <what our nginx saw>" — the attacker can prepend freely.
55
+ expect(clientIp(ctx('1.1.1.1, 2.2.2.2, 203.0.113.7'), { trustedProxyHops: 1 })).toBe('203.0.113.7');
56
+ });
57
+
58
+ it('counts hops from the right for deeper chains', () => {
59
+ expect(clientIp(ctx('1.1.1.1, 203.0.113.7, 10.0.0.1'), { trustedProxyHops: 2 })).toBe('203.0.113.7');
60
+ });
61
+
62
+ it('does not fall back to the attacker-controlled leftmost entry when the chain is short', () => {
63
+ // Two trusted hops declared but only one entry sent: believing '1.1.1.1'
64
+ // would be trusting exactly the forgeable part.
65
+ expect(clientIp(ctx('1.1.1.1'), { trustedProxyHops: 2 })).toBe('unknown');
66
+ });
67
+
68
+ it('uses x-real-ip only when hops are trusted', () => {
69
+ expect(clientIp(ctx(undefined, '203.0.113.9'), { trustedProxyHops: 1 })).toBe('203.0.113.9');
70
+ expect(clientIp(ctx(undefined, '203.0.113.9'))).toBe('unknown');
71
+ });
72
+ });
@@ -0,0 +1,125 @@
1
+ import type { Context, MiddlewareHandler } from 'hono';
2
+ import { getConnInfo } from 'hono/bun';
3
+ import { ApiError, toErrorEnvelope } from '../errors';
4
+
5
+ /** How much of the forwarded-header chain, if any, this server may believe. */
6
+ export interface ClientIpOptions {
7
+ /**
8
+ * How many TRUSTED reverse proxies sit in front of this server.
9
+ *
10
+ * `0` (the default) means none: use the socket peer and ignore forwarded
11
+ * headers entirely. Set it to the real number of hops you control (nginx in
12
+ * front of Bun ⇒ `1`).
13
+ */
14
+ trustedProxyHops?: number;
15
+ }
16
+
17
+ /**
18
+ * The client IP for rate-limiting / logging.
19
+ *
20
+ * `X-Forwarded-For` is written by whoever is calling — a client can send any
21
+ * value it likes. Reading it unconditionally means an attacker gets a NEW
22
+ * rate-limit bucket on every request simply by varying the header, which
23
+ * silently removes the protection the limiter exists to provide. So forwarded
24
+ * headers are IGNORED unless you declare how many proxies you actually trust.
25
+ *
26
+ * When you do, the chain is read RIGHT-to-LEFT. A proxy appends the peer it saw
27
+ * to any existing header (nginx's `$proxy_add_x_forwarded_for`), so with one
28
+ * trusted hop the last entry is the address YOUR proxy observed, while
29
+ * everything to its left is caller-supplied and unverifiable. Taking the first
30
+ * entry — the previous behavior — is taking exactly the part an attacker
31
+ * controls.
32
+ *
33
+ * Falls back to `'unknown'` when no connection info is available (e.g.
34
+ * `app.fetch(new Request(...))` in tests).
35
+ */
36
+ export function clientIp(c: Context, options: ClientIpOptions = {}): string {
37
+ const hops = Math.max(0, Math.trunc(options.trustedProxyHops ?? 0));
38
+ if (hops > 0) {
39
+ const chain = (c.req.header('x-forwarded-for') ?? '')
40
+ .split(',')
41
+ .map((part) => part.trim())
42
+ .filter(Boolean);
43
+ // The entry your own trusted hop observed. If the chain is shorter than the
44
+ // configured hop count the client sent fewer entries than expected — fall
45
+ // through to the socket rather than believing the leftmost value.
46
+ const trusted = chain[chain.length - hops];
47
+ if (trusted) return trusted;
48
+ // `x-real-ip` is single-valued and set by the same trusted proxy.
49
+ const xri = c.req.header('x-real-ip');
50
+ if (xri?.trim()) return xri.trim();
51
+ }
52
+ try {
53
+ return getConnInfo(c).remote.address ?? 'unknown';
54
+ } catch {
55
+ return 'unknown';
56
+ }
57
+ }
58
+
59
+ export interface RateLimitOpts {
60
+ /** Max requests allowed per window per key. */
61
+ limit: number;
62
+ /** Sliding window length in ms. */
63
+ windowMs: number;
64
+ /** 429 body message. */
65
+ message?: string;
66
+ /** Key function (default: client IP). Use to throttle per authenticated user. */
67
+ keyFn?: (c: Context) => string;
68
+ /**
69
+ * How many trusted reverse proxies sit in front of this server, for the
70
+ * default IP key. Default `0` — forwarded headers are ignored, because a
71
+ * caller can forge them to mint a fresh bucket per request. See
72
+ * {@link ClientIpOptions}.
73
+ */
74
+ trustedProxyHops?: number;
75
+ }
76
+
77
+ /**
78
+ * A sliding-window, in-memory rate limiter as Hono middleware. Keeps a timestamp log
79
+ * per key, prunes entries older than the window on access, and a cheap periodic
80
+ * sweep drops idle keys (no dangling timers). On limit: 429 + `Retry-After` + the
81
+ * canonical `{ error }` envelope. Used by auth routes (task 26) + vault unlock.
82
+ */
83
+ export function createRateLimiter(opts: RateLimitOpts): MiddlewareHandler {
84
+ const {
85
+ limit,
86
+ windowMs,
87
+ message = 'Too many requests. Please try again later.',
88
+ keyFn,
89
+ trustedProxyHops = 0,
90
+ } = opts;
91
+ const hits = new Map<string, number[]>();
92
+ let lastSweep = Date.now();
93
+
94
+ const sweep = (now: number): void => {
95
+ const cutoff = now - windowMs;
96
+ for (const [key, times] of hits) {
97
+ const live = times.filter((t) => t > cutoff);
98
+ if (live.length === 0) hits.delete(key);
99
+ else hits.set(key, live);
100
+ }
101
+ lastSweep = now;
102
+ };
103
+
104
+ return async (c, next) => {
105
+ const key = keyFn ? keyFn(c) : clientIp(c, { trustedProxyHops });
106
+ const now = Date.now();
107
+ const cutoff = now - windowMs;
108
+
109
+ // Periodically evict idle keys so the map can't grow unbounded.
110
+ if (now - lastSweep > windowMs) sweep(now);
111
+
112
+ const times = (hits.get(key) ?? []).filter((t) => t > cutoff);
113
+
114
+ if (times.length >= limit) {
115
+ hits.set(key, times);
116
+ const retryMs = (times[0] ?? now) + windowMs - now;
117
+ c.header('Retry-After', String(Math.max(1, Math.ceil(retryMs / 1000))));
118
+ return c.json(toErrorEnvelope(new ApiError(429, message, { code: 'rate_limited' })), 429);
119
+ }
120
+
121
+ times.push(now);
122
+ hits.set(key, times);
123
+ await next();
124
+ };
125
+ }
@@ -0,0 +1,95 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { MiddlewareHandler } from 'hono';
3
+ import { HTTPException } from 'hono/http-exception';
4
+ import type { CursedbeltEnv } from '../context';
5
+ import { isApiError } from '../errors';
6
+ import { createMetricsBuffer, type MetricsBuffer } from '../metrics/metricsBuffer';
7
+ import { normalizeRoute } from '../metrics/normalizeRoute';
8
+
9
+ /**
10
+ * Structured request/response logging. After each request it records one slim
11
+ * `request_metrics` row (via the batched {@link MetricsBuffer}, off the hot path)
12
+ * and, for errors (status ≥ 400) or slow requests, one `event_logs` row tagged with
13
+ * the correlation id. Attributes rows to `c.get('userId')` when the app's auth
14
+ * middleware has set it.
15
+ *
16
+ * It records metrics even when a downstream handler THROWS: the error is caught,
17
+ * the would-be status derived, the row written, then the error re-thrown so the
18
+ * error-envelope `onError` still produces the response.
19
+ */
20
+
21
+ export interface RequestLoggerOpts {
22
+ /**
23
+ * The metrics buffer to write through. Provide one (and own its lifecycle via
24
+ * `buffer.stop()`) to share it / flush deterministically; omit to create one from
25
+ * `db`. Its flush timer is `unref()`'d, so an internally-created buffer is fine for
26
+ * long-running servers.
27
+ */
28
+ buffer?: MetricsBuffer;
29
+ /** Duration (ms) at/above which a 2xx request also logs an `event_logs` row. Default: 2000. */
30
+ slowMs?: number;
31
+ /** Also write `event_logs` rows for status ≥ 400. Default: true. */
32
+ logErrors?: boolean;
33
+ }
34
+
35
+ function statusOfThrown(err: unknown): number {
36
+ if (isApiError(err)) return err.status;
37
+ if (err instanceof HTTPException) return err.status;
38
+ return 500;
39
+ }
40
+
41
+ export function requestLogger(
42
+ db: Database,
43
+ opts: RequestLoggerOpts = {},
44
+ ): MiddlewareHandler<CursedbeltEnv> {
45
+ const buffer = opts.buffer ?? createMetricsBuffer({ db });
46
+ const slowMs = opts.slowMs ?? 2000;
47
+ const logErrors = opts.logErrors ?? true;
48
+
49
+ const insertEvent = db.prepare(
50
+ `INSERT INTO event_logs
51
+ (id, correlation_id, event_name, type, message, details, duration_ms, user_id, created_at)
52
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
53
+ );
54
+
55
+ return async (c, next) => {
56
+ const start = performance.now();
57
+ let thrown: unknown;
58
+ let didThrow = false;
59
+ try {
60
+ await next();
61
+ } catch (err) {
62
+ thrown = err;
63
+ didThrow = true;
64
+ }
65
+ const durationMs = performance.now() - start;
66
+
67
+ const { method, route } = normalizeRoute(c);
68
+ const status = didThrow ? statusOfThrown(thrown) : c.res.status;
69
+ const lenHeader = didThrow ? null : c.res.headers.get('content-length');
70
+ const bytesOut = lenHeader ? Number(lenHeader) : null;
71
+ const userId = c.get('userId') ?? null;
72
+
73
+ buffer.record({ ts: Date.now(), method, route, status, durationMs, bytesOut, userId });
74
+
75
+ const isError = status >= 400;
76
+ const isSlow = durationMs >= slowMs;
77
+ if ((logErrors && isError) || isSlow) {
78
+ const type = status >= 500 ? 'error' : isError ? 'warn' : 'info';
79
+ insertEvent.run(
80
+ crypto.randomUUID(),
81
+ c.get('correlationId') ?? null,
82
+ isError ? 'http_error' : 'http_slow',
83
+ type,
84
+ `${method} ${route} → ${status}`,
85
+ JSON.stringify({ method, route, status, durationMs: Math.round(durationMs), bytesOut }),
86
+ Math.round(durationMs),
87
+ userId,
88
+ new Date().toISOString(),
89
+ );
90
+ }
91
+
92
+ // Re-throw so the error-envelope onError still builds the response.
93
+ if (didThrow) throw thrown;
94
+ };
95
+ }
@@ -0,0 +1,78 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import { secureHeaders } from 'hono/secure-headers';
3
+
4
+ /**
5
+ * Security headers, built on Hono's `secureHeaders` (helmet-equivalent) rather than
6
+ * a hand-rolled header pass — robust + maintained, per the "prefer configured
7
+ * libraries" mandate. cursedbelt supplies sensible cross-app defaults (strict CSP,
8
+ * `X-Frame-Options: DENY`, COOP, HSTS in production, …); the consuming app extends
9
+ * just the directives it needs (CDN hosts, embed frames) via {@link SecurityHeadersOpts}.
10
+ */
11
+
12
+ // Derive the option types from Hono itself (it does not export them by name).
13
+ type SecureHeadersOptions = NonNullable<Parameters<typeof secureHeaders>[0]>;
14
+ export type CspDirectives = NonNullable<SecureHeadersOptions['contentSecurityPolicy']>;
15
+
16
+ export interface SecurityHeadersOpts {
17
+ /**
18
+ * Extend/override the default CSP directives. Shallow-merged per directive, so a
19
+ * supplied directive REPLACES that default's value (pass the full list). Set to
20
+ * `false` to omit the CSP header entirely.
21
+ */
22
+ csp?: CspDirectives | false;
23
+ /**
24
+ * `Strict-Transport-Security` value. Default: enabled in production
25
+ * (`NODE_ENV==='production'`) as `max-age=31536000; includeSubDomains`, off
26
+ * otherwise. Pass a string to force a value, or `false` to disable.
27
+ */
28
+ hsts?: boolean | string;
29
+ /** Escape hatch: any other `secureHeaders` option, merged last (wins). */
30
+ overrides?: Partial<SecureHeadersOptions>;
31
+ }
32
+
33
+ // A strict baseline that works for a same-origin SPA + JSON API. Apps widen the
34
+ // few directives they need (script/img/connect hosts, frame embeds) via `opts.csp`.
35
+ const DEFAULT_CSP: CspDirectives = {
36
+ defaultSrc: ["'self'"],
37
+ baseUri: ["'self'"],
38
+ objectSrc: ["'none'"],
39
+ frameAncestors: ["'none'"],
40
+ formAction: ["'self'"],
41
+ scriptSrc: ["'self'"],
42
+ scriptSrcAttr: ["'none'"],
43
+ // Inline styles are needed for Tailwind / chart / drag transforms; inline SCRIPTS
44
+ // stay blocked (the real XSS barrier).
45
+ styleSrc: ["'self'", "'unsafe-inline'"],
46
+ imgSrc: ["'self'", 'data:', 'blob:'],
47
+ fontSrc: ["'self'", 'data:'],
48
+ connectSrc: ["'self'"],
49
+ upgradeInsecureRequests: [],
50
+ };
51
+
52
+ export function securityHeaders(opts: SecurityHeadersOpts = {}): MiddlewareHandler {
53
+ // A runtime escape hatch matching ca's ops convention: CSP_DISABLED=true drops CSP.
54
+ const cspOff = opts.csp === false || process.env.CSP_DISABLED === 'true';
55
+ const csp = cspOff ? undefined : { ...DEFAULT_CSP, ...(opts.csp ?? {}) };
56
+
57
+ const isProd = process.env.NODE_ENV === 'production';
58
+ const hsts = opts.hsts ?? (isProd ? 'max-age=31536000; includeSubDomains' : false);
59
+
60
+ return secureHeaders({
61
+ contentSecurityPolicy: csp,
62
+ xContentTypeOptions: 'nosniff',
63
+ // DENY (not SAMEORIGIN) to agree with CSP `frame-ancestors 'none'`.
64
+ xFrameOptions: 'DENY',
65
+ referrerPolicy: 'strict-origin-when-cross-origin',
66
+ crossOriginOpenerPolicy: 'same-origin',
67
+ // We serve cross-origin media/fonts, so keep CORP permissive.
68
+ crossOriginResourcePolicy: 'cross-origin',
69
+ // COEP breaks third-party embeds (YouTube, CDNs) — off by default.
70
+ crossOriginEmbedderPolicy: false,
71
+ originAgentCluster: true,
72
+ xDnsPrefetchControl: 'off',
73
+ xPermittedCrossDomainPolicies: 'none',
74
+ strictTransportSecurity: hsts,
75
+ removePoweredBy: true,
76
+ ...opts.overrides,
77
+ });
78
+ }