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,30 @@
1
+ /**
2
+ * `cursedbelt-server/dropzone` โ€” the reusable drop-folder scraper.
3
+ *
4
+ * Drop a tree of files into a folder; a sweep walks it, hands each file to the app's handler,
5
+ * deletes only what the handler confirmed was filed somewhere else, and prunes the folders it
6
+ * empties. Nothing app-specific lives here: what a file MEANS is the app's, what happens to a
7
+ * file on disk is this module's.
8
+ *
9
+ * Consumers (2026-08-13): `apps/music`, `apps/family`, `apps/collections` in
10
+ * cursed-satellites. Each supplies a `DropHandler` and nothing else.
11
+ */
12
+ export {
13
+ describeSweep,
14
+ isJunkName,
15
+ isStable,
16
+ pruneEmptyDirs,
17
+ scanDropzone,
18
+ sweepDropzone,
19
+ type DropHandler,
20
+ type DropOutcome,
21
+ type DroppedFile,
22
+ type ScanOptions,
23
+ type SweepReport,
24
+ } from "./scan";
25
+ export {
26
+ askBinaryServerAboutDuplicate,
27
+ hashFile,
28
+ type DuplicateVerdict,
29
+ type DuplicateQuestion,
30
+ } from "./duplicates";
@@ -0,0 +1,255 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ describeSweep,
7
+ isJunkName,
8
+ pruneEmptyDirs,
9
+ scanDropzone,
10
+ sweepDropzone,
11
+ type DropOutcome,
12
+ type DroppedFile,
13
+ } from "./scan";
14
+
15
+ /**
16
+ * The drop-folder scraper.
17
+ *
18
+ * Almost every test here is about DESTRUCTION โ€” what the sweep deletes and, more importantly,
19
+ * what it must not. The dropzone is the one place the owner's copy may be the only copy: they
20
+ * dragged the file in expecting it to be taken, and if the sweep is wrong the original is
21
+ * gone with nothing to restore from.
22
+ */
23
+
24
+ let root: string;
25
+
26
+ const write = (rel: string, bytes = 8): void => {
27
+ const full = join(root, rel);
28
+ mkdirSync(join(full, ".."), { recursive: true });
29
+ writeFileSync(full, Buffer.alloc(bytes, 7));
30
+ };
31
+
32
+ beforeEach(() => {
33
+ root = mkdtempSync(join(tmpdir(), "dropzone-spec-"));
34
+ });
35
+ afterEach(() => rmSync(root, { recursive: true, force: true }));
36
+
37
+ // A stability window of 0 keeps these tests fast; rule 2 gets its own test below.
38
+ const sweep = (handle: (f: DroppedFile) => Promise<DropOutcome>, opts = {}) =>
39
+ sweepDropzone(root, handle, { stabilityMs: 0, ...opts });
40
+
41
+ describe("scanDropzone", () => {
42
+ test("finds files at every depth, with their path relative to the root", async () => {
43
+ write("a.mp3");
44
+ write("Album/b.mp3");
45
+ write("Album/Disc 2/c.mp3");
46
+ const found = await scanDropzone(root);
47
+ expect(found.map((f) => f.relativePath).sort()).toEqual([
48
+ "Album/Disc 2/c.mp3",
49
+ "Album/b.mp3",
50
+ "a.mp3",
51
+ ]);
52
+ });
53
+
54
+ test("๐Ÿ”ด returns junk too, because a sweep that must DELETE junk has to see it", async () => {
55
+ // "if it's a wrong type of file delete it" โ€” a scanner that filtered these out would
56
+ // leave them in the folder forever, and the owner's requirement is an EMPTY folder.
57
+ write(".DS_Store");
58
+ write("track.mp3");
59
+ expect((await scanDropzone(root)).map((f) => f.name).sort()).toEqual([".DS_Store", "track.mp3"]);
60
+ });
61
+
62
+ test("captureSiblings records each directory's file list on the ONE walk", async () => {
63
+ // A folder's cover.jpg is only reachable while the directory still exists, and filing
64
+ // the audio out of it removes the neighbours. Re-reading later finds nothing.
65
+ write("Album/track.mp3");
66
+ write("Album/cover.jpg");
67
+ const found = await scanDropzone(root, { captureSiblings: true });
68
+ const track = found.find((f) => f.name === "track.mp3");
69
+ expect(track?.siblings.sort()).toEqual(["cover.jpg", "track.mp3"]);
70
+ });
71
+
72
+ test("skipDirs keeps the sweep out of another app's queue", async () => {
73
+ write("collections/video.mp4");
74
+ write("mine.mp3");
75
+ const found = await scanDropzone(root, { skipDirs: ["collections"] });
76
+ expect(found.map((f) => f.name)).toEqual(["mine.mp3"]);
77
+ });
78
+
79
+ test("the extension is lowercased and includes the dot; a dotless name has none", async () => {
80
+ write("LOUD.MP3");
81
+ write("README");
82
+ const byName = new Map((await scanDropzone(root)).map((f) => [f.name, f.ext]));
83
+ expect(byName.get("LOUD.MP3")).toBe(".mp3");
84
+ expect(byName.get("README")).toBe("");
85
+ });
86
+ });
87
+
88
+ describe("isJunkName", () => {
89
+ test("OS bookkeeping, resource forks and in-flight downloads", () => {
90
+ for (const name of [".DS_Store", "Thumbs.db", "._track.mp3", "movie.mp4.part", ".hidden"])
91
+ expect(isJunkName(name)).toBe(true);
92
+ });
93
+ test("real content is not junk, whatever it is called", () => {
94
+ for (const name of ["track.mp3", "cover.jpg", "Holiday.MP4", "notes"])
95
+ expect(isJunkName(name)).toBe(false);
96
+ });
97
+ });
98
+
99
+ describe("sweepDropzone โ€” what gets deleted", () => {
100
+ test("๐Ÿ”ด a CONFIRMED file is deleted; a FAILED one is left exactly where it is", async () => {
101
+ // Rule 1, and the whole reason this module exists. Deleting on "the upload didn't throw"
102
+ // turns any catalog failure into permanent data loss.
103
+ write("good.mp3");
104
+ write("bad.mp3");
105
+ const report = await sweep(async (f) =>
106
+ f.name === "good.mp3"
107
+ ? { kind: "stored" }
108
+ : { kind: "failed", detail: "the catalog said no" },
109
+ );
110
+ expect(existsSync(join(root, "good.mp3"))).toBe(false);
111
+ expect(existsSync(join(root, "bad.mp3"))).toBe(true);
112
+ expect(report.stored).toBe(1);
113
+ expect(report.failed).toHaveLength(1);
114
+ });
115
+
116
+ test("๐Ÿ”ด a DUPLICATE is deleted โ€” that is the point of recognising one", async () => {
117
+ // "if it's a dup skip it then delete it". The bytes are already safe at the destination,
118
+ // so keeping the local copy is exactly the two-copies problem the owner is solving.
119
+ write("already-have.mp3");
120
+ await sweep(async () => ({ kind: "duplicate" }));
121
+ expect(existsSync(join(root, "already-have.mp3"))).toBe(false);
122
+ });
123
+
124
+ test("๐Ÿ”ด a HELD file survives โ€” it is a question, not a failure", async () => {
125
+ write("maybe-dup.mp3");
126
+ const report = await sweep(async () => ({ kind: "held", detail: "waiting on the owner" }));
127
+ expect(existsSync(join(root, "maybe-dup.mp3"))).toBe(true);
128
+ expect(report.held).toBe(1);
129
+ expect(report.failed).toHaveLength(0);
130
+ });
131
+
132
+ test("a DISCARD is deleted, so junk cannot accumulate", async () => {
133
+ write("junk.txt");
134
+ await sweep(async () => ({ kind: "discard", detail: "not audio" }));
135
+ expect(existsSync(join(root, "junk.txt"))).toBe(false);
136
+ });
137
+
138
+ test("๐Ÿ”ด a handler that THROWS costs one file, never the sweep", async () => {
139
+ // "This should not stall uploading the rest of the in progress uploads" โ€” stated by the
140
+ // owner about duplicates, and just as true of a corrupt file in the middle of 4,000.
141
+ write("one.mp3");
142
+ write("two.mp3");
143
+ write("three.mp3");
144
+ const report = await sweep(async (f) => {
145
+ if (f.name === "two.mp3") throw new Error("kaboom");
146
+ return { kind: "stored" };
147
+ });
148
+ expect(report.stored).toBe(2);
149
+ expect(report.failed).toHaveLength(1);
150
+ expect(report.failed[0]?.error).toContain("kaboom");
151
+ expect(existsSync(join(root, "two.mp3"))).toBe(true);
152
+ });
153
+
154
+ test("dryRun deletes nothing at all", async () => {
155
+ write("a.mp3");
156
+ write("b.mp3");
157
+ await sweep(async () => ({ kind: "stored" }), { dryRun: true });
158
+ expect(existsSync(join(root, "a.mp3"))).toBe(true);
159
+ expect(existsSync(join(root, "b.mp3"))).toBe(true);
160
+ });
161
+
162
+ test("๐Ÿ”ด a genuinely EMPTY file is handled, not stranded as forever-copying", async () => {
163
+ // The obvious "same size twice AND non-empty" test treats a real 0-byte file as
164
+ // permanently mid-copy: never handled, never deleted, so the folder never empties โ€”
165
+ // which is the one thing the owner's brief measures success by. Measured on a first
166
+ // run: three real files, all 0 bytes, reported "still copying" on every sweep.
167
+ writeFileSync(join(root, "empty.txt"), "");
168
+ const old = new Date(Date.now() - 5 * 60_000);
169
+ utimesSync(join(root, "empty.txt"), old, old);
170
+ const report = await sweep(async () => ({ kind: "discard" }));
171
+ expect(report.unstable).toEqual([]);
172
+ expect(report.discarded).toBe(1);
173
+ expect(existsSync(join(root, "empty.txt"))).toBe(false);
174
+ });
175
+
176
+ test("a BRAND-NEW empty file is still treated as arriving", async () => {
177
+ // Emptiness is evidence of an unfinished copy only while the file is new.
178
+ writeFileSync(join(root, "arriving.bin"), "");
179
+ const report = await sweep(async () => ({ kind: "discard" }));
180
+ expect(report.unstable).toEqual(["arriving.bin"]);
181
+ expect(existsSync(join(root, "arriving.bin"))).toBe(true);
182
+ });
183
+
184
+ test("๐Ÿ”ด an unstable file is skipped and reported as still-copying, not as failed", async () => {
185
+ // Dropping a 4 GB video is not atomic. Calling this a failure would send the owner
186
+ // looking for a bug in a file that is arriving perfectly.
187
+ write("growing.mp4", 10);
188
+ let handled = 0;
189
+ const report = await sweepDropzone(
190
+ root,
191
+ async () => {
192
+ handled++;
193
+ return { kind: "stored" };
194
+ },
195
+ {
196
+ // A real gap, and the file grows during it โ€” exactly the live case.
197
+ stabilityMs: 30,
198
+ onProgress: () => writeFileSync(join(root, "growing.mp4"), Buffer.alloc(9999)),
199
+ },
200
+ );
201
+ expect(handled).toBe(0);
202
+ expect(report.unstable).toEqual(["growing.mp4"]);
203
+ expect(existsSync(join(root, "growing.mp4"))).toBe(true);
204
+ });
205
+ });
206
+
207
+ describe("sweepDropzone โ€” leaving nothing behind", () => {
208
+ test("๐Ÿ”ด an emptied tree is removed and the report says the folder is empty", async () => {
209
+ // The owner's actual acceptance test: "In the end there should be nothing left in the
210
+ // myMusic dir."
211
+ write("Album/Disc 1/a.mp3");
212
+ write("Album/Disc 1/b.mp3");
213
+ write("Album/cover.jpg");
214
+ const report = await sweep(async () => ({ kind: "stored" }));
215
+ expect(existsSync(join(root, "Album"))).toBe(false);
216
+ expect(existsSync(root)).toBe(true); // never the root itself
217
+ expect(report.remaining).toBe(0);
218
+ expect(describeSweep(report)).toContain("folder is empty");
219
+ });
220
+
221
+ test("๐Ÿ”ด a directory holding a HELD or FAILED file is NOT removed", async () => {
222
+ // rmdir refusing a non-empty directory is the safety property. A recursive delete here
223
+ // would take exactly the files the sweep decided to keep.
224
+ write("Keep/held.mp3");
225
+ write("Gone/stored.mp3");
226
+ await sweep(async (f) => (f.name === "held.mp3" ? { kind: "held" } : { kind: "stored" }));
227
+ expect(existsSync(join(root, "Keep/held.mp3"))).toBe(true);
228
+ expect(existsSync(join(root, "Gone"))).toBe(false);
229
+ });
230
+
231
+ test("the report counts the bytes reclaimed from the queue", async () => {
232
+ write("a.mp3", 1000);
233
+ write("b.mp3", 2000);
234
+ const report = await sweep(async () => ({ kind: "stored" }));
235
+ expect(report.bytesRemoved).toBe(3000);
236
+ });
237
+ });
238
+
239
+ describe("pruneEmptyDirs", () => {
240
+ test("collapses a deep empty chain in ONE pass and never removes the root", async () => {
241
+ mkdirSync(join(root, "a/b/c/d"), { recursive: true });
242
+ const removed = await pruneEmptyDirs(root);
243
+ expect(removed).toBe(4);
244
+ expect(existsSync(join(root, "a"))).toBe(false);
245
+ expect(existsSync(root)).toBe(true);
246
+ });
247
+
248
+ test("stops at a directory that still holds something", async () => {
249
+ write("a/b/keep.txt");
250
+ mkdirSync(join(root, "a/b/empty"), { recursive: true });
251
+ await pruneEmptyDirs(root);
252
+ expect(existsSync(join(root, "a/b/empty"))).toBe(false);
253
+ expect(existsSync(join(root, "a/b/keep.txt"))).toBe(true);
254
+ });
255
+ });
@@ -0,0 +1,342 @@
1
+ /**
2
+ * The drop-folder scraper โ€” the reusable half.
3
+ *
4
+ * Owner's ask, 2026-08-13: *"Do this to prepare a reusable strategy for multiple apps to get
5
+ * intelligent processing for binary-server. Some functionality may be reusable in multiple
6
+ * apps in different ways and can go in cb where standardized app functionality is shared. The
7
+ * specific ingestion logic that only binary-server or one app needs can go to the specific
8
+ * app."* โ€ฆ *"Make this tool reusable in case we need something like it on other folders
9
+ * later."*
10
+ *
11
+ * So: WALKING a drop folder, deciding a file has finished copying, sorting content from junk,
12
+ * deleting only what has been confirmed elsewhere, and leaving nothing behind โ€” all of that is
13
+ * the same for music, family photographs, collections videos and notes documents, and all of
14
+ * it is here. What a file MEANS โ€” which album, which artist, which collection โ€” is the app's,
15
+ * and none of it is here.
16
+ *
17
+ * โ”€โ”€ ๐Ÿ”ด Three rules that look like paranoia and are not โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
+ *
19
+ * 1. **A file is deleted only after the destination CONFIRMS it.** The dropzone is the one
20
+ * place the owner's copy may be the only copy: they dragged it in expecting it to be taken.
21
+ * A sweep that deletes on "the upload didn't throw" turns any catalog failure into data
22
+ * loss. The handler returns `stored`/`duplicate` and only then does the original go.
23
+ *
24
+ * 2. **A file must measure the same size twice before it is touched.** Dropping a 4 GB video
25
+ * into a folder is not atomic. Without this the sweep ingests a truncated file, reports
26
+ * success, and deletes the real one as it is still being written. notes' ingester and
27
+ * collections' both learned this the same way.
28
+ *
29
+ * 3. **One bad file never stops the sweep.** Every handler outcome is per-file, and a throw is
30
+ * caught and recorded. The owner's instruction is explicit about the duplicate case โ€” *"This
31
+ * should not stall uploading the rest of the in progress uploads"* โ€” and it generalizes: a
32
+ * dropzone with one unreadable file in it must still drain the other 4,000.
33
+ */
34
+
35
+ import type { Dirent } from "node:fs";
36
+ import { readdir, rm, rmdir, stat } from "node:fs/promises";
37
+ import { join, relative } from "node:path";
38
+
39
+ /** A file the sweep found, with everything a handler needs to decide about it. */
40
+ export interface DroppedFile {
41
+ /** Absolute path on disk. */
42
+ path: string;
43
+ /** Path relative to the dropzone root โ€” the app's only clue about where it was filed. */
44
+ relativePath: string;
45
+ /** The filename, no directories. */
46
+ name: string;
47
+ /** Lowercased extension INCLUDING the dot, or `""` for a file that has none. */
48
+ ext: string;
49
+ size: number;
50
+ mtimeMs: number;
51
+ /** Every other FILE name in the same directory โ€” see {@link ScanOptions.captureSiblings}. */
52
+ siblings: string[];
53
+ }
54
+
55
+ export type DropOutcome =
56
+ /** Filed successfully. The original is deleted. */
57
+ | { kind: "stored"; detail?: string }
58
+ /** Already present at the destination. The original is deleted โ€” that is the point. */
59
+ | { kind: "duplicate"; detail?: string }
60
+ /** Not content: junk, an unusable sidecar, a format this app does not take. DELETED. */
61
+ | { kind: "discard"; detail?: string }
62
+ /** Deliberately parked (a duplicate awaiting a decision). LEFT ALONE, not retried loudly. */
63
+ | { kind: "held"; detail?: string }
64
+ /** Could not be filed. LEFT IN PLACE and reported. */
65
+ | { kind: "failed"; detail: string };
66
+
67
+ export interface SweepReport {
68
+ scanned: number;
69
+ stored: number;
70
+ duplicate: number;
71
+ discarded: number;
72
+ held: number;
73
+ failed: { relativePath: string; error: string }[];
74
+ /** Files still being copied in โ€” they are picked up by the next sweep, not an error. */
75
+ unstable: string[];
76
+ bytesRemoved: number;
77
+ emptyDirsRemoved: number;
78
+ /** Anything left in the root when the sweep finished. Empty is the goal. */
79
+ remaining: number;
80
+ }
81
+
82
+ export interface ScanOptions {
83
+ /** Two size samples this far apart prove a copy finished. */
84
+ stabilityMs?: number;
85
+ /** Names that are never content, on top of the built-in OS list. */
86
+ reservedNames?: string[];
87
+ /**
88
+ * Capture each directory's file listing on the ONE walk.
89
+ *
90
+ * ๐Ÿ”ด Load-bearing for sidecar art, and it cannot be done later. A folder's `cover.jpg` is
91
+ * only reachable while the directory still exists, and filing the audio out of it removes
92
+ * the neighbours โ€” by the time anything else could look, they are gone. Capturing on the
93
+ * walk also costs one `readdir` per directory instead of one per file, which on a 6,000
94
+ * track library is the difference between seconds and minutes.
95
+ */
96
+ captureSiblings?: boolean;
97
+ /** Directory names to skip entirely at any depth (another app's queue, our own state). */
98
+ skipDirs?: string[];
99
+ /** Stop after this many files. Absent means everything. */
100
+ limit?: number;
101
+ }
102
+
103
+ /** OS bookkeeping and editor scratch. Never content, in any app. */
104
+ const OS_JUNK = new Set([
105
+ ".DS_Store",
106
+ ".localized",
107
+ "Icon\r",
108
+ "Thumbs.db",
109
+ "desktop.ini",
110
+ "README.md",
111
+ ]);
112
+
113
+ const PARTIAL_SUFFIXES = [".download", ".crdownload", ".part", ".partial", ".tmp"];
114
+
115
+ /** A name that is OS noise, editor scratch, or a copy still in flight. */
116
+ export function isJunkName(name: string): boolean {
117
+ if (OS_JUNK.has(name)) return true;
118
+ // A dotfile in a drop folder is the operating system's or another tool's state. The owner
119
+ // drops content, and content does not begin with a dot.
120
+ if (name.startsWith(".")) return true;
121
+ if (name.startsWith("._")) return true; // AppleDouble resource forks
122
+ return PARTIAL_SUFFIXES.some((s) => name.toLowerCase().endsWith(s));
123
+ }
124
+
125
+ const extOf = (name: string): string => {
126
+ const dot = name.lastIndexOf(".");
127
+ return dot > 0 ? name.slice(dot).toLowerCase() : "";
128
+ };
129
+
130
+ /**
131
+ * Every candidate file under `root`, deepest-last, with each one's sibling listing.
132
+ *
133
+ * Junk is NOT filtered out here โ€” it is returned like anything else, because a sweep whose job
134
+ * includes *"if it's a wrong type of file delete it"* has to see it in order to delete it.
135
+ * {@link isJunkName} is exported so a handler can classify without re-deriving the list.
136
+ */
137
+ export async function scanDropzone(root: string, opts: ScanOptions = {}): Promise<DroppedFile[]> {
138
+ const skip = new Set(opts.skipDirs ?? []);
139
+ const out: DroppedFile[] = [];
140
+
141
+ const walk = async (dir: string): Promise<void> => {
142
+ if (opts.limit !== undefined && out.length >= opts.limit) return;
143
+ let entries: Dirent[];
144
+ try {
145
+ entries = await readdir(dir, { withFileTypes: true });
146
+ } catch {
147
+ return; // vanished mid-walk โ€” a dropzone is a moving target by nature
148
+ }
149
+ const siblings = opts.captureSiblings
150
+ ? entries.filter((e) => e.isFile()).map((e) => e.name)
151
+ : [];
152
+
153
+ for (const entry of entries) {
154
+ if (opts.limit !== undefined && out.length >= opts.limit) return;
155
+ const full = join(dir, entry.name);
156
+ if (entry.isDirectory()) {
157
+ if (skip.has(entry.name)) continue;
158
+ await walk(full);
159
+ continue;
160
+ }
161
+ if (!entry.isFile()) continue;
162
+ try {
163
+ const info = await stat(full);
164
+ out.push({
165
+ path: full,
166
+ relativePath: relative(root, full),
167
+ name: entry.name,
168
+ ext: extOf(entry.name),
169
+ size: info.size,
170
+ mtimeMs: info.mtimeMs,
171
+ siblings,
172
+ });
173
+ } catch {
174
+ /* gone between readdir and stat */
175
+ }
176
+ }
177
+ };
178
+
179
+ await walk(root);
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * How old an EMPTY file has to be before it is believed. See {@link isStable}.
185
+ *
186
+ * A file that has just been created and has no bytes yet is almost certainly mid-copy; the
187
+ * same file a minute later really is empty.
188
+ */
189
+ export const EMPTY_FILE_GRACE_MS = 60_000;
190
+
191
+ /**
192
+ * Has this file finished being copied in? See rule 2 in the header.
193
+ *
194
+ * ๐Ÿ”ด A zero-byte file needs its own answer, and getting it wrong strands the folder forever.
195
+ * The obvious test โ€” "same size twice AND non-empty" โ€” treats a genuinely empty file as
196
+ * permanently mid-copy: it is never handled, never deleted, and so the dropzone never empties,
197
+ * which is the one outcome the owner's brief measures success by (*"in the end there should be
198
+ * nothing left"*). Measured on a first run: three real files, all 0 bytes, reported as "still
199
+ * copying" on every sweep for as long as they sat there.
200
+ *
201
+ * Emptiness is only evidence of an unfinished copy while the file is NEW, so age decides. A
202
+ * file created a minute ago with no bytes in it is empty, not arriving.
203
+ */
204
+ export async function isStable(file: DroppedFile, stabilityMs = 750): Promise<boolean> {
205
+ await new Promise((resolve) => setTimeout(resolve, stabilityMs));
206
+ try {
207
+ const now = await stat(file.path);
208
+ if (now.size !== file.size) return false; // still growing
209
+ if (now.size > 0) return true;
210
+ return Date.now() - now.mtimeMs > EMPTY_FILE_GRACE_MS;
211
+ } catch {
212
+ return false; // taken by something else, or never really there
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Remove directories left empty by a sweep, deepest first โ€” and never the root.
218
+ *
219
+ * ๐Ÿ”ด `rmdir` refuses a non-empty directory, and that refusal IS the safety property. A
220
+ * recursive delete here would take a file the sweep deliberately chose not to ingest โ€” a held
221
+ * duplicate, a failure left in place for the owner to look at โ€” which is precisely the set of
222
+ * files that must survive.
223
+ */
224
+ export async function pruneEmptyDirs(root: string, dir = root): Promise<number> {
225
+ let removed = 0;
226
+ let entries: string[];
227
+ try {
228
+ entries = await readdir(dir);
229
+ } catch {
230
+ return 0;
231
+ }
232
+ for (const name of entries) {
233
+ const full = join(dir, name);
234
+ try {
235
+ if ((await stat(full)).isDirectory()) removed += await pruneEmptyDirs(root, full);
236
+ } catch {
237
+ /* gone */
238
+ }
239
+ }
240
+ if (dir === root) return removed;
241
+ try {
242
+ await rmdir(dir);
243
+ return removed + 1;
244
+ } catch {
245
+ return removed; // not empty โ€” the right outcome, and not an error
246
+ }
247
+ }
248
+
249
+ /** What the app supplies: what to do with one file. */
250
+ export type DropHandler = (file: DroppedFile) => Promise<DropOutcome>;
251
+
252
+ /**
253
+ * Walk a dropzone, hand every file to `handle`, and leave nothing behind that was dealt with.
254
+ *
255
+ * ๐Ÿ”ด The DELETION is here, and it is deliberately not the handler's job. Every app would
256
+ * otherwise reimplement rule 1, and the one that got it wrong would lose the owner's only copy
257
+ * silently. A handler says what happened; this decides what that means for the file on disk.
258
+ */
259
+ export async function sweepDropzone(
260
+ root: string,
261
+ handle: DropHandler,
262
+ opts: ScanOptions & { dryRun?: boolean; onProgress?: (done: number, total: number) => void } = {},
263
+ ): Promise<SweepReport> {
264
+ const report: SweepReport = {
265
+ scanned: 0,
266
+ stored: 0,
267
+ duplicate: 0,
268
+ discarded: 0,
269
+ held: 0,
270
+ failed: [],
271
+ unstable: [],
272
+ bytesRemoved: 0,
273
+ emptyDirsRemoved: 0,
274
+ remaining: 0,
275
+ };
276
+
277
+ const files = await scanDropzone(root, opts);
278
+ for (const [index, file] of files.entries()) {
279
+ report.scanned++;
280
+ opts.onProgress?.(index + 1, files.length);
281
+
282
+ if (!(await isStable(file, opts.stabilityMs))) {
283
+ report.unstable.push(file.relativePath);
284
+ continue;
285
+ }
286
+
287
+ let outcome: DropOutcome;
288
+ try {
289
+ outcome = opts.dryRun ? { kind: "held", detail: "dry run" } : await handle(file);
290
+ } catch (error) {
291
+ // Rule 3: a handler that throws costs one file, never the sweep.
292
+ outcome = {
293
+ kind: "failed",
294
+ detail: error instanceof Error ? error.message : String(error),
295
+ };
296
+ }
297
+
298
+ switch (outcome.kind) {
299
+ case "stored":
300
+ case "duplicate":
301
+ case "discard": {
302
+ if (outcome.kind === "stored") report.stored++;
303
+ else if (outcome.kind === "duplicate") report.duplicate++;
304
+ else report.discarded++;
305
+ if (opts.dryRun) break;
306
+ try {
307
+ await rm(file.path);
308
+ report.bytesRemoved += file.size;
309
+ } catch (error) {
310
+ // The bytes are safe at the destination; only the tidying failed. Reporting it
311
+ // as a FAILURE would send the owner looking for a file that arrived perfectly.
312
+ report.failed.push({
313
+ relativePath: file.relativePath,
314
+ error: `filed, but the original could not be removed: ${
315
+ error instanceof Error ? error.message : String(error)
316
+ }`,
317
+ });
318
+ }
319
+ break;
320
+ }
321
+ case "held":
322
+ report.held++;
323
+ break;
324
+ case "failed":
325
+ report.failed.push({ relativePath: file.relativePath, error: outcome.detail });
326
+ break;
327
+ }
328
+ }
329
+
330
+ if (!opts.dryRun) report.emptyDirsRemoved = await pruneEmptyDirs(root);
331
+ report.remaining = (await scanDropzone(root, { skipDirs: opts.skipDirs })).length;
332
+ return report;
333
+ }
334
+
335
+ /** A one-line summary โ€” every consumer prints one, and they should read alike. */
336
+ export const describeSweep = (report: SweepReport): string =>
337
+ `${report.stored} added, ${report.duplicate} already had, ${report.discarded} discarded, ` +
338
+ `${report.held} held, ${report.failed.length} failed, ${report.unstable.length} still copying` +
339
+ (report.bytesRemoved > 0
340
+ ? ` ยท ${(report.bytesRemoved / 1024 ** 3).toFixed(2)} GB freed from the queue`
341
+ : "") +
342
+ (report.remaining > 0 ? ` ยท ${report.remaining} left in the folder` : " ยท folder is empty");