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,137 @@
1
+ /**
2
+ * Pause-alarm escalation — notes' `syncAlarm` generalized. A sync that is version-
3
+ * skew paused or auth-refused for a sustained stretch is a real problem the owner
4
+ * must hear about (the 2026-07-31 notes incident: prod ran stale code most of a day
5
+ * and only an owner eyeballing counts caught it). An UNREACHABLE peer neither counts
6
+ * nor resets — the nightly EC2 sleep must never false-alarm.
7
+ *
8
+ * State is a small JSON file so the alarm survives process restarts; the notifier is
9
+ * a seam (macOS `osascript` by default) so tests and non-Mac hosts inject their own.
10
+ */
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname } from "node:path";
13
+
14
+ /**
15
+ * 🔴 `error` joined on 2026-08-21, and it is the outcome that was MISSING.
16
+ *
17
+ * The console's subdomain cutover left the Mac's `STATION_SYNC_URL` pointing at
18
+ * a host that had become a 301; the dialer followed it into a Cloudflare Access
19
+ * login page and threw `Failed to parse JSON` every round. That is neither
20
+ * `paused` (nothing is version-skewed) nor `auth` (no token was refused) nor
21
+ * `unreachable` (the peer answered), so it recorded NOTHING — the timer logged
22
+ * `[sync] failed (ignored)` and the alarm's counter sat at zero while every
23
+ * mirrored report on prod froze for days, each still rendering a confident age.
24
+ *
25
+ * A sustained hard error is exactly as bad as a sustained pause and is now
26
+ * counted the same. `unreachable` remains the one outcome that neither counts
27
+ * nor resets, because the nightly EC2 sleep must never false-alarm.
28
+ */
29
+ export type AlarmOutcome = "ok" | "paused" | "auth" | "error" | "unreachable";
30
+
31
+ interface AlarmState {
32
+ consecutiveBad: number;
33
+ firstBadAt: number | null;
34
+ lastRaisedAt: number | null;
35
+ lastReason: string | null;
36
+ }
37
+
38
+ const EMPTY: AlarmState = {
39
+ consecutiveBad: 0,
40
+ firstBadAt: null,
41
+ lastRaisedAt: null,
42
+ lastReason: null,
43
+ };
44
+
45
+ export interface SyncAlarmOptions {
46
+ /** Where the counter lives (survives restarts). */
47
+ statePath: string;
48
+ /** Rounds of paused/auth before raising (with the round cadence, pick so the
49
+ * threshold lands around ~45 min of sustained pause). */
50
+ threshold?: number;
51
+ /** Re-raise cadence while the condition persists. Default 4h. */
52
+ reRaiseMs?: number;
53
+ /** Present the alarm. Default: macOS notification via osascript. */
54
+ notify?: (title: string, body: string) => void;
55
+ now?: () => number;
56
+ appName?: string;
57
+ }
58
+
59
+ export interface SyncAlarm {
60
+ /** Record one sync round's outcome; raises when the threshold crosses. */
61
+ record(outcome: AlarmOutcome, reason?: string): void;
62
+ }
63
+
64
+ function defaultNotify(title: string, body: string): void {
65
+ try {
66
+ const esc = (s: string) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
67
+ Bun.spawnSync([
68
+ "osascript",
69
+ "-e",
70
+ `display notification "${esc(body)}" with title "${esc(title)}"`,
71
+ ]);
72
+ } catch {
73
+ // A notifier must never take the loop down.
74
+ }
75
+ }
76
+
77
+ export function createSyncAlarm(options: SyncAlarmOptions): SyncAlarm {
78
+ const threshold = options.threshold ?? 9;
79
+ const reRaiseMs = options.reRaiseMs ?? 4 * 60 * 60_000;
80
+ const notify = options.notify ?? defaultNotify;
81
+ const now = options.now ?? Date.now;
82
+ const appName = options.appName ?? "sync";
83
+
84
+ const load = (): AlarmState => {
85
+ try {
86
+ if (!existsSync(options.statePath)) return { ...EMPTY };
87
+ return {
88
+ ...EMPTY,
89
+ ...(JSON.parse(readFileSync(options.statePath, "utf8")) as Partial<AlarmState>),
90
+ };
91
+ } catch {
92
+ return { ...EMPTY };
93
+ }
94
+ };
95
+ const save = (state: AlarmState): void => {
96
+ try {
97
+ mkdirSync(dirname(options.statePath), { recursive: true });
98
+ writeFileSync(options.statePath, JSON.stringify(state));
99
+ } catch {
100
+ // Losing the counter degrades to a later alarm, never to a crash.
101
+ }
102
+ };
103
+
104
+ return {
105
+ record(outcome, reason) {
106
+ const state = load();
107
+ if (outcome === "ok") {
108
+ save({ ...EMPTY });
109
+ return;
110
+ }
111
+ if (outcome === "unreachable") return; // neither counts nor resets
112
+ state.consecutiveBad += 1;
113
+ state.lastReason = reason ?? outcome;
114
+ if (state.firstBadAt === null) state.firstBadAt = now();
115
+ const due =
116
+ state.consecutiveBad >= threshold &&
117
+ (state.lastRaisedAt === null || now() - state.lastRaisedAt >= reRaiseMs);
118
+ if (due) {
119
+ state.lastRaisedAt = now();
120
+ const since = state.firstBadAt
121
+ ? new Date(state.firstBadAt).toLocaleTimeString()
122
+ : "a while";
123
+ notify(
124
+ outcome === "error" ? `${appName}: sync is FAILING` : `${appName}: sync is paused`,
125
+ outcome === "auth"
126
+ ? `The sync token is being refused (since ${since}). Re-link the peer.`
127
+ : outcome === "error"
128
+ ? // Names the real error, because this alarm's whole reason for
129
+ // existing is that "failed (ignored)" was invisible.
130
+ `Every sync round has failed since ${since}: ${state.lastReason}`
131
+ : `Push has been paused on "${state.lastReason}" since ${since} — the peer likely needs a deploy.`,
132
+ );
133
+ }
134
+ save(state);
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,145 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Database } from "bun:sqlite";
3
+ import { Hono } from "hono";
4
+ import {
5
+ createCommandQueue,
6
+ createCommandReceiver,
7
+ createCommandRemote,
8
+ startCommandWorker,
9
+ type SyncCommand,
10
+ } from "./commands";
11
+
12
+ const queue = () => createCommandQueue(new Database(":memory:"));
13
+
14
+ describe("command queue", () => {
15
+ test("enqueue → claim → complete lifecycle", () => {
16
+ const q = queue();
17
+ const cmd = q.enqueue("app.start", { key: "notes" }, "prod-ui");
18
+ expect(cmd.status).toBe("queued");
19
+ expect(q.pendingCount()).toBe(1);
20
+ const claimed = q.claimNext("mac");
21
+ expect(claimed?.uid).toBe(cmd.uid);
22
+ expect(claimed?.status).toBe("claimed");
23
+ // Nothing else to claim while it is held.
24
+ expect(q.claimNext("other")).toBeNull();
25
+ expect(q.complete(cmd.uid, { started: true })).toBe(true);
26
+ expect(q.get(cmd.uid)?.status).toBe("done");
27
+ expect(q.pendingCount()).toBe(0);
28
+ // Completing twice is refused — the row already finished.
29
+ expect(q.complete(cmd.uid, {})).toBe(false);
30
+ });
31
+
32
+ test("claims are ordered oldest-first and atomic across workers", () => {
33
+ const q = queue();
34
+ const first = q.enqueue("a", null, "ui");
35
+ const second = q.enqueue("b", null, "ui");
36
+ expect(q.claimNext("w1")?.uid).toBe(first.uid);
37
+ expect(q.claimNext("w2")?.uid).toBe(second.uid);
38
+ expect(q.claimNext("w3")).toBeNull();
39
+ });
40
+
41
+ test("a stale claim is reclaimable (a dead worker cannot strand the queue)", async () => {
42
+ const q = queue();
43
+ q.enqueue("a", null, "ui");
44
+ expect(q.claimNext("w1", 60_000)).not.toBeNull();
45
+ // Let the claim age past a 1ms stale window, then reclaim it.
46
+ await new Promise((resolve) => setTimeout(resolve, 5));
47
+ expect(q.claimNext("w2", 1)?.claimedBy).toBe("w2");
48
+ });
49
+
50
+ test("fail records the error", () => {
51
+ const q = queue();
52
+ const cmd = q.enqueue("a", null, "ui");
53
+ q.claimNext("w");
54
+ expect(q.fail(cmd.uid, "boom")).toBe(true);
55
+ const row = q.get(cmd.uid);
56
+ expect(row?.status).toBe("failed");
57
+ expect(row?.error).toBe("boom");
58
+ });
59
+ });
60
+
61
+ describe("command receiver + remote + worker", () => {
62
+ function mount(q: ReturnType<typeof createCommandQueue>) {
63
+ const app = new Hono();
64
+ app.route(
65
+ "/api/relay",
66
+ createCommandReceiver({
67
+ queue: q,
68
+ verifyToken: (t) => t === "tok",
69
+ maxHoldMs: 300,
70
+ pollStepMs: 20,
71
+ }),
72
+ );
73
+ const remote = createCommandRemote({
74
+ basePath: "http://peer/api/relay",
75
+ token: "tok",
76
+ fetchImpl: ((input: RequestInfo | URL, init?: RequestInit) =>
77
+ app.request(input as string, init)) as typeof fetch,
78
+ });
79
+ return { app, remote };
80
+ }
81
+
82
+ test("held poll returns a queued command immediately, null after the hold", async () => {
83
+ const q = queue();
84
+ const { remote } = mount(q);
85
+ const empty = await remote.poll("mac", 50);
86
+ expect(empty).toBeNull();
87
+ q.enqueue("repo.sync", null, "ui");
88
+ const got = await remote.poll("mac", 50);
89
+ expect(got?.kind).toBe("repo.sync");
90
+ await remote.report(got?.uid ?? "", { ok: true, result: { healed: 3 } });
91
+ expect(q.get(got?.uid ?? "")?.status).toBe("done");
92
+ });
93
+
94
+ test("a command enqueued DURING the hold is delivered within it", async () => {
95
+ const q = queue();
96
+ const { remote } = mount(q);
97
+ const pending = remote.poll("mac", 250);
98
+ setTimeout(() => q.enqueue("late", null, "ui"), 40);
99
+ const got = await pending;
100
+ expect(got?.kind).toBe("late");
101
+ });
102
+
103
+ test("the gate refuses a wrong token uniformly", async () => {
104
+ const q = queue();
105
+ const app = new Hono();
106
+ app.route("/api/relay", createCommandReceiver({ queue: q, verifyToken: (t) => t === "tok" }));
107
+ const res = await app.request("http://peer/api/relay/pending?wait=0", {
108
+ headers: { authorization: "Bearer wrong" },
109
+ });
110
+ expect(res.status).toBe(401);
111
+ });
112
+
113
+ test("the worker loop polls, executes, reports, and keeps cycling", async () => {
114
+ const q = queue();
115
+ const { remote } = mount(q);
116
+ const executed: string[] = [];
117
+ const worker = startCommandWorker({
118
+ resolveRemote: () => remote,
119
+ execute: async (command: SyncCommand) => {
120
+ executed.push(command.kind);
121
+ if (command.kind === "explode") throw new Error("kaboom");
122
+ return { ok: true };
123
+ },
124
+ holdMs: 60,
125
+ idleGapMs: 5,
126
+ log: () => {},
127
+ warn: () => {},
128
+ });
129
+ q.enqueue("good", null, "ui");
130
+ q.enqueue("explode", null, "ui");
131
+ const deadline = Date.now() + 3_000;
132
+ while (Date.now() < deadline) {
133
+ const rows = q.list();
134
+ if (rows.length === 2 && rows.every((r) => r.status === "done" || r.status === "failed"))
135
+ break;
136
+ await new Promise((resolve) => setTimeout(resolve, 20));
137
+ }
138
+ worker.stop();
139
+ expect(executed.sort()).toEqual(["explode", "good"]);
140
+ const byKind = new Map(q.list().map((r) => [r.kind, r]));
141
+ expect(byKind.get("good")?.status).toBe("done");
142
+ expect(byKind.get("explode")?.status).toBe("failed");
143
+ expect(byKind.get("explode")?.error).toContain("kaboom");
144
+ });
145
+ });
@@ -0,0 +1,361 @@
1
+ /**
2
+ * The command relay — notes' ask-responder shape generalized: the internet-facing
3
+ * instance PARKS an action it cannot perform (it has no Mac powers), and the Mac's
4
+ * resident worker long-polls for it, executes locally, and posts the outcome back.
5
+ * The Mac dials OUT; the receiving instance never reaches into the house.
6
+ *
7
+ * Delivery is a held GET (default hold 25s) — remember the two fleet traps: the
8
+ * host server needs `idleTimeoutSeconds` ABOVE the hold (Bun's 10s default closed
9
+ * notes' 25s poll and nginx reported a bogus 502), and the routes must be mounted
10
+ * BEFORE any browser-session gate.
11
+ *
12
+ * A command left `claimed` past the stale window is reclaimable — a worker that
13
+ * died mid-execution must not strand the queue. Executors must therefore be
14
+ * idempotent or cheap to re-run.
15
+ */
16
+ import type { Database } from "bun:sqlite";
17
+ import { randomUUID } from "node:crypto";
18
+ import { Hono } from "hono";
19
+
20
+ export type CommandStatus = "queued" | "claimed" | "done" | "failed";
21
+
22
+ export interface SyncCommand {
23
+ uid: string;
24
+ kind: string;
25
+ args: unknown;
26
+ status: CommandStatus;
27
+ requestedBy: string;
28
+ createdAt: number;
29
+ claimedAt: number | null;
30
+ claimedBy: string | null;
31
+ finishedAt: number | null;
32
+ result: unknown;
33
+ error: string | null;
34
+ }
35
+
36
+ const STALE_CLAIM_MS = 5 * 60_000;
37
+
38
+ export interface CommandQueue {
39
+ enqueue(kind: string, args: unknown, requestedBy: string): SyncCommand;
40
+ /** Atomically claim the oldest queued (or stale-claimed) command. */
41
+ claimNext(claimedBy: string, staleMs?: number): SyncCommand | null;
42
+ complete(uid: string, result: unknown): boolean;
43
+ fail(uid: string, error: string): boolean;
44
+ get(uid: string): SyncCommand | null;
45
+ list(limit?: number): SyncCommand[];
46
+ pendingCount(): number;
47
+ }
48
+
49
+ interface Row {
50
+ uid: string;
51
+ kind: string;
52
+ args: string;
53
+ status: CommandStatus;
54
+ requested_by: string;
55
+ created_at: number;
56
+ claimed_at: number | null;
57
+ claimed_by: string | null;
58
+ finished_at: number | null;
59
+ result: string | null;
60
+ error: string | null;
61
+ }
62
+
63
+ const toCommand = (r: Row): SyncCommand => ({
64
+ uid: r.uid,
65
+ kind: r.kind,
66
+ args: JSON.parse(r.args) as unknown,
67
+ status: r.status,
68
+ requestedBy: r.requested_by,
69
+ createdAt: r.created_at,
70
+ claimedAt: r.claimed_at,
71
+ claimedBy: r.claimed_by,
72
+ finishedAt: r.finished_at,
73
+ result: r.result === null ? null : (JSON.parse(r.result) as unknown),
74
+ error: r.error,
75
+ });
76
+
77
+ export function createCommandQueue(db: Database): CommandQueue {
78
+ db.exec(`
79
+ CREATE TABLE IF NOT EXISTS sync_commands (
80
+ uid TEXT PRIMARY KEY,
81
+ kind TEXT NOT NULL,
82
+ args TEXT NOT NULL,
83
+ status TEXT NOT NULL DEFAULT 'queued',
84
+ requested_by TEXT NOT NULL,
85
+ created_at INTEGER NOT NULL,
86
+ claimed_at INTEGER,
87
+ claimed_by TEXT,
88
+ finished_at INTEGER,
89
+ result TEXT,
90
+ error TEXT
91
+ );
92
+ CREATE INDEX IF NOT EXISTS idx_sync_commands_status ON sync_commands(status, created_at);
93
+ `);
94
+ const insert = db.prepare(
95
+ "INSERT INTO sync_commands (uid, kind, args, status, requested_by, created_at) VALUES (?, ?, ?, 'queued', ?, ?)",
96
+ );
97
+ const getStmt = db.prepare("SELECT * FROM sync_commands WHERE uid = ?");
98
+ const listStmt = db.prepare("SELECT * FROM sync_commands ORDER BY created_at DESC LIMIT ?");
99
+ const pendingStmt = db.prepare(
100
+ "SELECT COUNT(*) AS n FROM sync_commands WHERE status IN ('queued','claimed')",
101
+ );
102
+ // The atomic claim: one UPDATE whose WHERE picks the oldest eligible row. sqlite
103
+ // serializes writers, so two workers can never claim the same row.
104
+ const claimStmt = db.prepare(`
105
+ UPDATE sync_commands SET status = 'claimed', claimed_at = ?, claimed_by = ?
106
+ WHERE uid = (
107
+ SELECT uid FROM sync_commands
108
+ WHERE status = 'queued' OR (status = 'claimed' AND claimed_at < ?)
109
+ ORDER BY created_at ASC LIMIT 1
110
+ )
111
+ RETURNING *
112
+ `);
113
+ const finishStmt = db.prepare(
114
+ "UPDATE sync_commands SET status = ?, finished_at = ?, result = ?, error = ? WHERE uid = ? AND status IN ('queued','claimed')",
115
+ );
116
+
117
+ return {
118
+ enqueue(kind, args, requestedBy) {
119
+ const uid = `cmd-${randomUUID().slice(0, 18)}`;
120
+ insert.run(uid, kind, JSON.stringify(args ?? null), requestedBy, Date.now());
121
+ return toCommand(getStmt.get(uid) as Row);
122
+ },
123
+ claimNext(claimedBy, staleMs = STALE_CLAIM_MS) {
124
+ const now = Date.now();
125
+ const row = claimStmt.get(now, claimedBy, now - staleMs) as Row | null;
126
+ return row ? toCommand(row) : null;
127
+ },
128
+ complete: (uid, result) =>
129
+ finishStmt.run("done", Date.now(), JSON.stringify(result ?? null), null, uid).changes > 0,
130
+ fail: (uid, error) => finishStmt.run("failed", Date.now(), null, error, uid).changes > 0,
131
+ get: (uid) => {
132
+ const row = getStmt.get(uid) as Row | null;
133
+ return row ? toCommand(row) : null;
134
+ },
135
+ list: (limit = 100) => (listStmt.all(limit) as Row[]).map(toCommand),
136
+ pendingCount: () => (pendingStmt.get() as { n: number }).n,
137
+ };
138
+ }
139
+
140
+ // ── the receiver routes (mount beside the sync receiver, same token gate) ────────
141
+
142
+ const noStore = { "cache-control": "no-store" } as const;
143
+ export const DEFAULT_HOLD_MS = 25_000;
144
+
145
+ export interface CommandRoutesOptions {
146
+ queue: CommandQueue;
147
+ verifyToken: (presented: string) => boolean;
148
+ /** Cap on the held poll. The HOST SERVER's idleTimeout must exceed this. */
149
+ maxHoldMs?: number;
150
+ pollStepMs?: number;
151
+ }
152
+
153
+ /** GET /pending (held) + POST /pending/:uid — the worker's two verbs. */
154
+ export function createCommandReceiver(options: CommandRoutesOptions): Hono {
155
+ const maxHold = options.maxHoldMs ?? DEFAULT_HOLD_MS;
156
+ const step = options.pollStepMs ?? 1_000;
157
+ const api = new Hono();
158
+ api.use("*", async (c, next) => {
159
+ const header = c.req.header("authorization") ?? "";
160
+ const presented = header.startsWith("Bearer ") ? header.slice(7) : "";
161
+ if (presented.length === 0 || !options.verifyToken(presented)) {
162
+ return c.json({ error: "unauthorized" }, 401, noStore);
163
+ }
164
+ await next();
165
+ });
166
+
167
+ api.get("/pending", async (c) => {
168
+ const worker = c.req.query("worker") ?? "worker";
169
+ const waitRaw = c.req.query("wait");
170
+ const wait = Math.min(
171
+ maxHold,
172
+ waitRaw !== undefined && /^\d+$/.test(waitRaw) ? Number(waitRaw) : maxHold,
173
+ );
174
+ const deadline = Date.now() + wait;
175
+ for (;;) {
176
+ const command = options.queue.claimNext(worker);
177
+ if (command) return c.json({ command }, 200, noStore);
178
+ if (Date.now() >= deadline) return c.json({ command: null }, 200, noStore);
179
+ await new Promise((resolve) =>
180
+ setTimeout(resolve, Math.min(step, Math.max(1, deadline - Date.now()))),
181
+ );
182
+ }
183
+ });
184
+
185
+ api.post("/pending/:uid", async (c) => {
186
+ const uid = c.req.param("uid");
187
+ let body: unknown;
188
+ try {
189
+ body = await c.req.json();
190
+ } catch {
191
+ return c.json({ error: "bad_request" }, 400, noStore);
192
+ }
193
+ const { ok, result, error } = (body ?? {}) as {
194
+ ok?: unknown;
195
+ result?: unknown;
196
+ error?: unknown;
197
+ };
198
+ if (typeof ok !== "boolean") return c.json({ error: "bad_request" }, 400, noStore);
199
+ const changed = ok
200
+ ? options.queue.complete(uid, result ?? null)
201
+ : options.queue.fail(uid, typeof error === "string" ? error : "failed");
202
+ if (!changed) return c.json({ error: "not_found" }, 404, noStore);
203
+ return c.json({ ok: true }, 200, noStore);
204
+ });
205
+
206
+ return api;
207
+ }
208
+
209
+ // ── the dialing worker (runs on the Mac, inside the resident daemon) ─────────────
210
+
211
+ export interface CommandRemote {
212
+ poll(worker: string, waitMs: number): Promise<SyncCommand | null>;
213
+ report(uid: string, outcome: { ok: boolean; result?: unknown; error?: string }): Promise<void>;
214
+ }
215
+
216
+ export function createCommandRemote(options: {
217
+ basePath: string;
218
+ token: string;
219
+ fetchImpl?: typeof fetch;
220
+ /** Host-scoped extras for this peer — see `createHttpRemote`'s note. */
221
+ extraHeaders?: Record<string, string>;
222
+ }): CommandRemote {
223
+ const base = options.basePath.replace(/\/$/, "");
224
+ const doFetch = options.fetchImpl ?? fetch;
225
+ const headers = (extra?: Record<string, string>) => ({
226
+ authorization: `Bearer ${options.token}`,
227
+ ...(options.extraHeaders ?? {}),
228
+ ...(extra ?? {}),
229
+ });
230
+ /** 🔴 `redirect: "manual"`, for the reason in `http.ts`'s header: the relay
231
+ * travels the same road as the sync, so a moved peer hostname must fail here
232
+ * with the destination in the message rather than silently landing on a
233
+ * Cloudflare Access login page and reporting a JSON parse error. */
234
+ const refuseRedirect = (r: Response, path: string): void => {
235
+ if (r.status >= 300 && r.status < 400)
236
+ throw new Error(
237
+ `${path}: ${r.status} — the peer address redirects to ` +
238
+ `${r.headers.get("location") ?? "an unnamed destination"}; the relay URL points at a host that has moved.`,
239
+ );
240
+ };
241
+ return {
242
+ async poll(worker, waitMs) {
243
+ const r = await doFetch(
244
+ `${base}/pending?worker=${encodeURIComponent(worker)}&wait=${waitMs}`,
245
+ {
246
+ headers: headers(),
247
+ redirect: "manual",
248
+ // The abort budget must clear the hold plus network slack.
249
+ signal: AbortSignal.timeout(waitMs + 15_000),
250
+ },
251
+ );
252
+ refuseRedirect(r, "commands/pending");
253
+ if (!r.ok) throw new Error(`commands/pending: ${r.status}`);
254
+ const body = (await r.json()) as { command: SyncCommand | null };
255
+ return body.command;
256
+ },
257
+ async report(uid, outcome) {
258
+ const r = await doFetch(`${base}/pending/${encodeURIComponent(uid)}`, {
259
+ method: "POST",
260
+ headers: headers({ "content-type": "application/json" }),
261
+ redirect: "manual",
262
+ body: JSON.stringify(outcome),
263
+ signal: AbortSignal.timeout(30_000),
264
+ });
265
+ refuseRedirect(r, "commands/report");
266
+ if (!r.ok) throw new Error(`commands/report: ${r.status}`);
267
+ },
268
+ };
269
+ }
270
+
271
+ export interface CommandWorkerDeps {
272
+ /** Re-read every cycle, like the sync timer's peer — null = not linked. */
273
+ resolveRemote: () => CommandRemote | null;
274
+ execute: (command: SyncCommand) => Promise<unknown>;
275
+ worker?: string;
276
+ holdMs?: number;
277
+ /** Idle gap between an empty poll and the next (on top of the hold). */
278
+ idleGapMs?: number;
279
+ backoffMaxMs?: number;
280
+ log?: (msg: string) => void;
281
+ warn?: (msg: string) => void;
282
+ setTimer?: (fn: () => void, ms: number) => unknown;
283
+ clearTimer?: (handle: unknown) => void;
284
+ }
285
+
286
+ export interface CommandWorkerHandle {
287
+ stop(): void;
288
+ }
289
+
290
+ /** The resident long-poll → execute → report loop. Unreachable remote backs off to
291
+ * `backoffMaxMs` and recovers silently — the nightly window rule. */
292
+ export function startCommandWorker(deps: CommandWorkerDeps): CommandWorkerHandle {
293
+ const worker = deps.worker ?? "mac";
294
+ const holdMs = deps.holdMs ?? DEFAULT_HOLD_MS;
295
+ const idleGapMs = deps.idleGapMs ?? 1_000;
296
+ const backoffMax = deps.backoffMaxMs ?? 10 * 60_000;
297
+ const log = deps.log ?? ((m: string) => console.log(m));
298
+ const warn = deps.warn ?? ((m: string) => console.error(m));
299
+ const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
300
+ const clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));
301
+
302
+ let stopped = false;
303
+ let handle: unknown = null;
304
+ let failures = 0;
305
+ let reportedUnreachable = false;
306
+
307
+ const schedule = (ms: number): void => {
308
+ if (stopped) return;
309
+ if (handle !== null) clearTimer(handle);
310
+ handle = setTimer(cycle, ms);
311
+ (handle as { unref?: () => void })?.unref?.();
312
+ };
313
+
314
+ const cycle = (): void => {
315
+ if (stopped) return;
316
+ const remote = deps.resolveRemote();
317
+ if (remote === null) {
318
+ schedule(60_000);
319
+ return;
320
+ }
321
+ void remote
322
+ .poll(worker, holdMs)
323
+ .then(async (command) => {
324
+ failures = 0;
325
+ reportedUnreachable = false;
326
+ if (command === null) return;
327
+ log(`[commands] executing ${command.kind} (${command.uid})`);
328
+ try {
329
+ const result = await deps.execute(command);
330
+ await remote.report(command.uid, { ok: true, result });
331
+ } catch (err) {
332
+ const message = err instanceof Error ? err.message : String(err);
333
+ warn(`[commands] ${command.kind} failed: ${message}`);
334
+ // Best-effort: a failed report leaves the row claimed; the stale-claim
335
+ // window re-serves it, which is why executors must tolerate a re-run.
336
+ await remote.report(command.uid, { ok: false, error: message }).catch(() => {});
337
+ }
338
+ })
339
+ .then(() => {
340
+ if (!stopped) schedule(idleGapMs);
341
+ })
342
+ .catch((err) => {
343
+ failures += 1;
344
+ const wait = Math.min(backoffMax, 5_000 * 2 ** Math.min(failures - 1, 7));
345
+ if (!reportedUnreachable) {
346
+ const message = err instanceof Error ? err.message : String(err);
347
+ log(`[commands] remote unreachable (${message}) — backing off. This is normal offline.`);
348
+ reportedUnreachable = true;
349
+ }
350
+ schedule(wait);
351
+ });
352
+ };
353
+
354
+ schedule(1_000);
355
+ return {
356
+ stop: () => {
357
+ stopped = true;
358
+ if (handle !== null) clearTimer(handle);
359
+ },
360
+ };
361
+ }