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,128 @@
1
+ /**
2
+ * The four tables behind the fleet's sharing model, as migrations an app runs
3
+ * inside its OWN database.
4
+ *
5
+ * ── πŸ”΄ Why they live in the app's database and not in a shared one ────────────
6
+ * A grant answers "may this person open THIS row", and the only place that
7
+ * question can be answered in the same transaction as the row is read is the
8
+ * database holding the row. A central share service would mean every list query
9
+ * in every satellite is a network round trip that cannot participate in a
10
+ * `COUNT(*)`, and the first thing an app would do is cache the answer β€” at which
11
+ * point revocation is eventually-consistent, which is the one thing revocation
12
+ * must not be. So the SCHEMA is shared (this file), the data is local, and
13
+ * `apps/auth` remains the only registry of who exists.
14
+ *
15
+ * Names are prefixed `share_` for the same reason: these tables land in a
16
+ * database that already has its own, and `shares` alone is a name three apps
17
+ * could plausibly have wanted.
18
+ *
19
+ * ── The shape, and the two constraints that are load-bearing ────────────────
20
+ * `share_grants` one row per (subject, resource). UNIQUE on exactly that, so a
21
+ * second grant is an UPDATE (a regrade) rather than a duplicate
22
+ * nobody can revoke half of.
23
+ * `share_groups` a named set of people.
24
+ * `share_group_members` who is in one.
25
+ * `share_events` APPEND ONLY, and nothing in this package ever deletes from it.
26
+ *
27
+ * πŸ”΄ `share_events` deliberately has **no foreign key** onto `share_grants` or
28
+ * `share_groups`. A trail whose rows are cascaded away when the grant is revoked
29
+ * can only describe access that still exists β€” it would erase itself at exactly
30
+ * the moment it becomes evidence. Revoking deletes the grant and appends an event;
31
+ * deleting a group deletes its membership and appends one per removal.
32
+ */
33
+ import type { Database } from "bun:sqlite";
34
+ import type { Migration } from "../migration/types";
35
+
36
+ export const SHARE_GRANTS_TABLE = "share_grants";
37
+ export const SHARE_GROUPS_TABLE = "share_groups";
38
+ export const SHARE_GROUP_MEMBERS_TABLE = "share_group_members";
39
+ export const SHARE_EVENTS_TABLE = "share_events";
40
+
41
+ export const SHARE_MIGRATIONS: readonly Migration[] = [
42
+ {
43
+ name: "0001_share_grants",
44
+ up(db: Database): void {
45
+ db.run(`
46
+ CREATE TABLE IF NOT EXISTS ${SHARE_GRANTS_TABLE} (
47
+ grant_id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ app TEXT NOT NULL,
49
+ kind TEXT NOT NULL,
50
+ resource_id TEXT NOT NULL,
51
+ subject_type TEXT NOT NULL CHECK (subject_type IN ('user', 'group')),
52
+ subject_id TEXT NOT NULL,
53
+ level TEXT NOT NULL CHECK (level IN ('read', 'write')),
54
+ granted_by TEXT NOT NULL,
55
+ granted_at TEXT NOT NULL,
56
+ note TEXT NOT NULL DEFAULT '',
57
+ -- One grant per subject per resource. This is what makes raising
58
+ -- read to write an UPDATE the audit trail can describe, instead of
59
+ -- two rows where revoking one leaves access in place.
60
+ UNIQUE (app, kind, resource_id, subject_type, subject_id)
61
+ )
62
+ `);
63
+ // The hot read is the resolver's: "does this subject hold a grant on this
64
+ // row" β€” correlated per row of a list query, so it must be an index hit.
65
+ db.run(
66
+ `CREATE INDEX IF NOT EXISTS idx_share_grants_subject
67
+ ON ${SHARE_GRANTS_TABLE} (subject_type, subject_id, app, kind, resource_id)`,
68
+ );
69
+ // …and the panel's: "who can see this record".
70
+ db.run(
71
+ `CREATE INDEX IF NOT EXISTS idx_share_grants_resource
72
+ ON ${SHARE_GRANTS_TABLE} (app, kind, resource_id)`,
73
+ );
74
+
75
+ db.run(`
76
+ CREATE TABLE IF NOT EXISTS ${SHARE_GROUPS_TABLE} (
77
+ group_id TEXT PRIMARY KEY,
78
+ name TEXT NOT NULL,
79
+ created_by TEXT NOT NULL,
80
+ created_at TEXT NOT NULL,
81
+ note TEXT NOT NULL DEFAULT ''
82
+ )
83
+ `);
84
+
85
+ db.run(`
86
+ CREATE TABLE IF NOT EXISTS ${SHARE_GROUP_MEMBERS_TABLE} (
87
+ group_id TEXT NOT NULL REFERENCES ${SHARE_GROUPS_TABLE} (group_id) ON DELETE CASCADE,
88
+ subject_id TEXT NOT NULL,
89
+ added_by TEXT NOT NULL,
90
+ added_at TEXT NOT NULL,
91
+ PRIMARY KEY (group_id, subject_id)
92
+ )
93
+ `);
94
+ // "Which groups is this person in" β€” read by every shared-row query.
95
+ db.run(
96
+ `CREATE INDEX IF NOT EXISTS idx_share_group_members_subject
97
+ ON ${SHARE_GROUP_MEMBERS_TABLE} (subject_id)`,
98
+ );
99
+
100
+ db.run(`
101
+ CREATE TABLE IF NOT EXISTS ${SHARE_EVENTS_TABLE} (
102
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
103
+ at TEXT NOT NULL,
104
+ action TEXT NOT NULL,
105
+ app TEXT,
106
+ kind TEXT,
107
+ resource_id TEXT,
108
+ subject_type TEXT NOT NULL CHECK (subject_type IN ('user', 'group')),
109
+ subject_id TEXT NOT NULL,
110
+ level TEXT,
111
+ previous_level TEXT,
112
+ actor TEXT NOT NULL,
113
+ note TEXT NOT NULL DEFAULT ''
114
+ )
115
+ `);
116
+ // "Everything that ever happened to this record", newest first.
117
+ db.run(
118
+ `CREATE INDEX IF NOT EXISTS idx_share_events_resource
119
+ ON ${SHARE_EVENTS_TABLE} (app, kind, resource_id, event_id DESC)`,
120
+ );
121
+ // "Everything this person was ever given" β€” the offboarding question.
122
+ db.run(
123
+ `CREATE INDEX IF NOT EXISTS idx_share_events_subject
124
+ ON ${SHARE_EVENTS_TABLE} (subject_id, event_id DESC)`,
125
+ );
126
+ },
127
+ },
128
+ ];
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The share ROUTER's contract, over a real Hono app and a real in-memory store.
3
+ *
4
+ * White-box by STANDARDS Β§6a's named exception β€” these are permission checks, and a
5
+ * permissive bug here is invisible until it is a disclosure. Every case below is a
6
+ * property one of the five call sites this replaced depended on, and the three marked πŸ”΄
7
+ * are the ones that were WRITTEN AS FAILURES first: comment out the gate, comment out the
8
+ * `group:` split, hand the read to a recipient, and watch each one go red.
9
+ */
10
+ import { Database } from "bun:sqlite";
11
+ import { beforeEach, describe, expect, test } from "bun:test";
12
+ import { Hono } from "hono";
13
+ import { mountShareRoutes, parseSubjectParam, subjectParam } from "./router";
14
+ import { createShareStore, user } from "./store";
15
+
16
+ let db: Database;
17
+ let shares: ReturnType<typeof createShareStore>;
18
+ /** Who the next request is from, and who owns the resource. */
19
+ let asEmail: string | null;
20
+ let owners: Record<string, string>;
21
+
22
+ function app() {
23
+ const a = new Hono();
24
+ mountShareRoutes(a, {
25
+ base: "/api/things/:id",
26
+ kind: "thing",
27
+ shares,
28
+ viewerOf: () => (asEmail ? { email: asEmail } : null),
29
+ // The app's policy, exactly as apps/life and apps/family supply it: 404 rather
30
+ // than 403 for a resource the viewer may not know exists.
31
+ mustOwn: (c, id, viewer) =>
32
+ owners[id] === viewer.email ? null : c.json({ error: "not found" }, 404),
33
+ extra: () => ({ groups: shares.listGroups() }),
34
+ });
35
+ return a;
36
+ }
37
+
38
+ const get = (id: string) => app().request(`/api/things/${id}/shares`);
39
+ const post = (id: string, body: unknown) =>
40
+ app().request(`/api/things/${id}/shares`, {
41
+ method: "POST",
42
+ headers: { "content-type": "application/json" },
43
+ body: JSON.stringify(body),
44
+ });
45
+ const del = (id: string, subject: string) =>
46
+ app().request(`/api/things/${id}/shares/${subject}`, { method: "DELETE" });
47
+
48
+ beforeEach(() => {
49
+ db = new Database(":memory:");
50
+ db.exec("PRAGMA foreign_keys = ON");
51
+ shares = createShareStore({ db, app: "test" });
52
+ owners = { t1: "owner@example.com" };
53
+ asEmail = "owner@example.com";
54
+ });
55
+
56
+ describe("the subject codec", () => {
57
+ test("πŸ”΄ a `group:` prefix is what makes a subject a GROUP, and only the prefix", () => {
58
+ // The sharp edge: a path segment cannot carry a {type, id} pair, so the prefix IS
59
+ // the type. Drop the split and a group revoke silently removes nothing and reports
60
+ // success β€” the worst possible outcome for a permission removal.
61
+ expect(parseSubjectParam("group:household")).toEqual({ type: "group", id: "household" });
62
+ expect(parseSubjectParam("aunt@example.com")).toEqual({
63
+ type: "user",
64
+ id: "aunt@example.com",
65
+ });
66
+ });
67
+
68
+ test("it round-trips through a URL segment, including an address with a +", () => {
69
+ const subject = { type: "user", id: "a+b@example.com" } as const;
70
+ expect(parseSubjectParam(subjectParam(subject))).toEqual(subject);
71
+ const g = { type: "group", id: "the house" } as const;
72
+ expect(parseSubjectParam(subjectParam(g))).toEqual(g);
73
+ });
74
+ });
75
+
76
+ describe("the owner gate", () => {
77
+ test("πŸ”΄ it covers the READ, not only the writes", async () => {
78
+ // "Who else can see this" is not part of what "shared with you" grants: the roster
79
+ // of other grantees is itself a disclosure. Five hand-rolled copies is five chances
80
+ // to gate POST/DELETE and forget GET.
81
+ shares.grant({
82
+ kind: "thing",
83
+ id: "t1",
84
+ subject: user("guest@example.com"),
85
+ level: "read",
86
+ grantedBy: "owner@example.com",
87
+ });
88
+ asEmail = "guest@example.com";
89
+ expect((await get("t1")).status).toBe(404);
90
+ expect((await post("t1", { subject: { id: "x@example.com" } })).status).toBe(404);
91
+ expect((await del("t1", "guest@example.com")).status).toBe(404);
92
+ // …and the grant it could not see is still there.
93
+ expect(shares.listForResource({ kind: "thing", id: "t1" })).toHaveLength(1);
94
+ });
95
+
96
+ test("no session is 401 on every route, before ownership is even asked", async () => {
97
+ asEmail = null;
98
+ expect((await get("t1")).status).toBe(401);
99
+ expect((await post("t1", { subject: { id: "x@example.com" } })).status).toBe(401);
100
+ expect((await del("t1", "x@example.com")).status).toBe(401);
101
+ });
102
+ });
103
+
104
+ describe("read / grant / revoke", () => {
105
+ test("the read carries the store's own rows plus the app's extra", async () => {
106
+ shares.grant({
107
+ kind: "thing",
108
+ id: "t1",
109
+ subject: user("aunt@example.com"),
110
+ level: "write",
111
+ grantedBy: "owner@example.com",
112
+ });
113
+ const body = (await (await get("t1")).json()) as {
114
+ grants: { subject: { id: string } }[];
115
+ events: unknown[];
116
+ groups: unknown[];
117
+ };
118
+ expect(body.grants.map((g) => g.subject.id)).toEqual(["aunt@example.com"]);
119
+ expect(body.events).toHaveLength(1);
120
+ // `extra` is the seam the two apps' divergent payloads fold through.
121
+ expect(body.groups).toEqual([]);
122
+ });
123
+
124
+ test("a grant defaults to read, and an unknown level is refused rather than coerced", async () => {
125
+ expect((await post("t1", { subject: { id: "a@example.com" } })).status).toBe(201);
126
+ expect(shares.accessFor({ kind: "thing", id: "t1" }, { subjectId: "a@example.com" })).toBe(
127
+ "read",
128
+ );
129
+ const bad = await post("t1", { subject: { id: "b@example.com" }, level: "admin" });
130
+ expect(bad.status).toBe(400);
131
+ expect(shares.accessFor({ kind: "thing", id: "t1" }, { subjectId: "b@example.com" })).toBe(
132
+ "none",
133
+ );
134
+ });
135
+
136
+ test("an empty subject is a 400, never a grant to nobody", async () => {
137
+ expect((await post("t1", {})).status).toBe(400);
138
+ expect((await post("t1", { subject: { id: " " } })).status).toBe(400);
139
+ expect(shares.listForResource({ kind: "thing", id: "t1" })).toHaveLength(0);
140
+ });
141
+
142
+ test("πŸ”΄ revoking a GROUP grant through the path actually removes it", async () => {
143
+ const g = shares.createGroup({
144
+ id: "household",
145
+ name: "The household",
146
+ createdBy: "owner@example.com",
147
+ });
148
+ shares.grant({
149
+ kind: "thing",
150
+ id: "t1",
151
+ subject: { type: "group", id: g.id },
152
+ level: "read",
153
+ grantedBy: "owner@example.com",
154
+ });
155
+ expect(shares.listForResource({ kind: "thing", id: "t1" })).toHaveLength(1);
156
+ expect((await del("t1", encodeURIComponent(`group:${g.id}`))).status).toBe(200);
157
+ expect(shares.listForResource({ kind: "thing", id: "t1" })).toHaveLength(0);
158
+ });
159
+
160
+ test("the audit trail outlives the grant it describes", async () => {
161
+ await post("t1", { subject: { id: "a@example.com" } });
162
+ await del("t1", "a@example.com");
163
+ const body = (await (await get("t1")).json()) as { grants: unknown[]; events: unknown[] };
164
+ expect(body.grants).toHaveLength(0);
165
+ expect(body.events).toHaveLength(2);
166
+ });
167
+ });
@@ -0,0 +1,194 @@
1
+ /**
2
+ * `mountShareRoutes` β€” the three HTTP routes a shareable resource needs, once.
3
+ *
4
+ * ── Why this exists ─────────────────────────────────────────────────────────────────────
5
+ * The store (`./store.ts`) was already the fleet's one sharing model; the ROUTES over it
6
+ * were not. `apps/life` wrote them once and mounted them four times (calendars, lists,
7
+ * to-dos, board cards) and `apps/family` wrote a fifth copy for people β€” five hand-rolled
8
+ * copies of read/grant/revoke, each re-deciding the same three policy questions:
9
+ *
10
+ * 1. **All three routes are OWNER-only, including the READ.** "Who else can see this" is
11
+ * not part of what "shared with you" grants β€” a recipient must never reach
12
+ * `listForResource`, because the roster of other grantees is itself a disclosure.
13
+ * Five copies is five chances to write the gate on POST/DELETE and forget it on GET.
14
+ * 2. **`group:<id>` is how a group subject travels in a URL.** A path segment cannot
15
+ * carry a `{type, id}` pair, so the prefix IS the type. Getting that wrong revokes a
16
+ * USER grant whose address happens to start with `group:` β€” or, worse, silently
17
+ * revokes nothing and reports success.
18
+ * 3. **A grant only ever widens, and the audit trail outlives it.** Both are the store's,
19
+ * and both are only true if every route actually goes through the store.
20
+ *
21
+ * A bug in any of those has to be fixable by one commit. That is the placement test in
22
+ * `cursedbelt/STANDARDS.md`, and it passes here.
23
+ *
24
+ * ── πŸ”΄ The wire shape is LIFE's, and family's client changed to match ────────────────────
25
+ * The two apps genuinely disagreed and one had to give:
26
+ *
27
+ * Β· revoke β€” family put the subject in the QUERY (`DELETE …/shares?subject=&type=`),
28
+ * life in the PATH (`DELETE …/shares/:subject`, `group:` prefixed). The path wins.
29
+ * family's own comment argued for the query on the grounds that the target should be
30
+ * visible in the request line β€” which a path segment satisfies just as well, while
31
+ * also being the shape four of the five mounts already used. Changing one client beat
32
+ * changing four.
33
+ * Β· read β€” family returned `{grants, events, labels}`, life `{grants, events, groups}`.
34
+ * Neither extra is general: `labels` resolves addresses to names out of the family
35
+ * tree *inside the viewer's space*, `groups` is life's household roster. So the
36
+ * primitive owns `{grants, events}` and an app folds its own half in through
37
+ * {@link ShareRouterOptions.extra}.
38
+ *
39
+ * ── What stays in the app ───────────────────────────────────────────────────────────────
40
+ * Ownership. `mustOwn` is the app's, because "who owns a calendar" and "who may curate a
41
+ * family file" are policy this module has no business knowing β€” and because the 404/403
42
+ * ORDER is policy too (family answers 404 before 403 on a person the viewer may not know
43
+ * exists). The hook returns the refusal it wants, or null to continue.
44
+ */
45
+ import type { Context, Hono } from 'hono';
46
+ import type { ShareEvent, ShareGrant, ShareLevel } from 'cursedbelt-core/sharing/model';
47
+ import type { ShareStore } from './store';
48
+
49
+ /** The `group:` prefix that lets a group subject travel in a path segment. Exported so a
50
+ * client and a route cannot disagree about it. */
51
+ export const GROUP_SUBJECT_PREFIX = 'group:';
52
+
53
+ /** Split a URL subject segment into the store's `{type, id}` pair. */
54
+ export function parseSubjectParam(raw: string): { type: 'user' | 'group'; id: string } {
55
+ const decoded = decodeURIComponent(raw);
56
+ return decoded.startsWith(GROUP_SUBJECT_PREFIX)
57
+ ? { type: 'group', id: decoded.slice(GROUP_SUBJECT_PREFIX.length) }
58
+ : { type: 'user', id: decoded };
59
+ }
60
+
61
+ /** The inverse β€” what a client puts in the path. */
62
+ export const subjectParam = (subject: { type: 'user' | 'group'; id: string }): string =>
63
+ encodeURIComponent(subject.type === 'group' ? GROUP_SUBJECT_PREFIX + subject.id : subject.id);
64
+
65
+ /** What every route needs about who is asking. Structural, so an app hands its own viewer
66
+ * type straight in. */
67
+ export interface ShareRouterViewer {
68
+ /** The fleet address; the subject id of every `user` grant. */
69
+ email: string;
70
+ }
71
+
72
+ export interface ShareRouterOptions<V extends ShareRouterViewer> {
73
+ /**
74
+ * The resource path, with `:id` in it β€” e.g. `/api/calendars/:id`. The three routes
75
+ * hang off `${base}/shares`; revoke adds `/:subject`.
76
+ */
77
+ base: string;
78
+ /** The `kind` half of the store's `(app, kind, id)` triple. */
79
+ kind: string;
80
+ /** The store. A function when it is per-request (a multi-tenant app). */
81
+ shares: ShareStore | ((c: Context) => ShareStore);
82
+ /** Who is asking. Returning null refuses every route with 401. */
83
+ viewerOf: (c: Context) => V | null;
84
+ /**
85
+ * πŸ”΄ The OWNER gate, run on ALL THREE routes including the read. Return a `Response` to
86
+ * refuse (the app owns the status AND the 404-before-403 ordering), or null to allow.
87
+ * `throw`ing is fine too β€” an app with an error middleware can keep using it.
88
+ */
89
+ mustOwn: (c: Context, id: string, viewer: V) => Response | null | Promise<Response | null>;
90
+ /** Extra fields folded into the READ payload beside `{grants, events}` β€” family's
91
+ * `labels`, life's `groups`. */
92
+ extra?: (c: Context, id: string, viewer: V) => Record<string, unknown>;
93
+ /** How many audit lines the read carries. Default 50. */
94
+ trailLimit?: number;
95
+ }
96
+
97
+ /** The read payload's guaranteed half. An app's `extra` is spread in beside it. */
98
+ export interface ShareRoutesPayload {
99
+ grants: ShareGrant[];
100
+ /** Newest first, and it OUTLIVES the grants β€” a revoked share still has a line. */
101
+ events: ShareEvent[];
102
+ }
103
+
104
+ const isLevel = (v: unknown): v is ShareLevel => v === 'read' || v === 'write';
105
+
106
+ /**
107
+ * Mount GET / POST / DELETE for one `kind` on one app.
108
+ *
109
+ * ```ts
110
+ * mountShareRoutes(app, {
111
+ * base: '/api/calendars/:id',
112
+ * kind: CALENDAR_KIND,
113
+ * shares,
114
+ * viewerOf,
115
+ * mustOwn: (c, id, viewer) => (ownsCalendar(db, viewer, id) ? null : c.json({ error: 'not found' }, 404)),
116
+ * extra: () => ({ groups: shares.listGroups() }),
117
+ * });
118
+ * ```
119
+ */
120
+ export function mountShareRoutes<V extends ShareRouterViewer>(
121
+ // biome-ignore lint/suspicious/noExplicitAny: the caller's Hono env is theirs, and this
122
+ // module reads nothing off it β€” narrowing it would force every app to name its own Env
123
+ // here for no gain.
124
+ app: Hono<any>,
125
+ options: ShareRouterOptions<V>,
126
+ ): void {
127
+ const { base, kind, viewerOf, mustOwn, extra, trailLimit = 50 } = options;
128
+ const storeOf = (c: Context): ShareStore =>
129
+ typeof options.shares === 'function' ? options.shares(c) : options.shares;
130
+
131
+ /** The three-step preamble every route shares: identity, id, ownership. */
132
+ async function enter(
133
+ c: Context,
134
+ ): Promise<{ refused: Response } | { id: string; viewer: V; shares: ShareStore }> {
135
+ const viewer = viewerOf(c);
136
+ if (!viewer) return { refused: c.json({ error: 'sign in first' }, 401) };
137
+ const id = c.req.param('id') ?? '';
138
+ const refused = await mustOwn(c, id, viewer);
139
+ if (refused) return { refused };
140
+ return { id, viewer, shares: storeOf(c) };
141
+ }
142
+
143
+ app.get(`${base}/shares`, async (c) => {
144
+ const step = await enter(c);
145
+ if ('refused' in step) return step.refused;
146
+ const { id, viewer, shares } = step;
147
+ const payload: ShareRoutesPayload & Record<string, unknown> = {
148
+ // The store's own rows, straight through β€” the sharing panel consumes exactly
149
+ // these, and a DTO in the middle is a place for a field to go missing.
150
+ grants: shares.listForResource({ kind, id }),
151
+ events: shares.events({ kind, id, limit: trailLimit }),
152
+ ...(extra ? extra(c, id, viewer) : {}),
153
+ };
154
+ return c.json(payload);
155
+ });
156
+
157
+ app.post(`${base}/shares`, async (c) => {
158
+ const step = await enter(c);
159
+ if ('refused' in step) return step.refused;
160
+ const { id, viewer, shares } = step;
161
+ const body = (await c.req.json().catch(() => ({}))) as {
162
+ subject?: { type?: string; id?: string };
163
+ /** family's older spelling β€” an address at the top level. Accepted so the two
164
+ * apps' clients could converge one at a time rather than in one commit. */
165
+ email?: string;
166
+ level?: unknown;
167
+ note?: unknown;
168
+ };
169
+ const subjectId = (body.subject?.id ?? body.email ?? '').trim();
170
+ if (!subjectId) return c.json({ error: 'who should this be shared with?' }, 400);
171
+ if (body.level !== undefined && !isLevel(body.level)) {
172
+ return c.json({ error: "level must be 'read' or 'write'" }, 400);
173
+ }
174
+ const grant = shares.grant({
175
+ kind,
176
+ id,
177
+ subject: { type: body.subject?.type === 'group' ? 'group' : 'user', id: subjectId },
178
+ level: isLevel(body.level) ? body.level : 'read',
179
+ grantedBy: viewer.email,
180
+ ...(typeof body.note === 'string' ? { note: body.note } : {}),
181
+ });
182
+ return c.json(grant, 201);
183
+ });
184
+
185
+ app.delete(`${base}/shares/:subject`, async (c) => {
186
+ const step = await enter(c);
187
+ if ('refused' in step) return step.refused;
188
+ const { id, viewer, shares } = step;
189
+ const raw = c.req.param('subject') ?? '';
190
+ if (!raw.trim()) return c.json({ error: 'which share should be removed?' }, 400);
191
+ shares.revoke({ kind, id, subject: parseSubjectParam(raw), revokedBy: viewer.email });
192
+ return c.json({ ok: true });
193
+ });
194
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The half a consuming app needs INSIDE its own queries: a SQL boolean saying
3
+ * "this row is shared with the viewer".
4
+ *
5
+ * ── πŸ”΄ Why a SQL fragment and not a list of ids ──────────────────────────────
6
+ * The obvious API is `sharedIds(kind, viewer): string[]`, and it is wrong for the
7
+ * consumers this exists to serve. `apps/family`'s rule, written in `people.ts`
8
+ * before this primitive existed, is that a row the viewer may not see *"must not
9
+ * surface as a snippet, a COUNT, or a tree node either"* β€” so its visibility test
10
+ * has to be a WHERE clause that a `COUNT(*)`, a JOIN and a recursive tree walk all
11
+ * inherit. Handing the app an array means the app writes `id IN (…)` itself, in
12
+ * every one of those places, with its own quoting; the first one somebody forgets
13
+ * is a disclosure, and it will be the count.
14
+ *
15
+ * So the primitive emits the clause. `sharedResourceSql` returns a correlated
16
+ * `EXISTS (…)` over the grant tables that can be dropped into any query where the
17
+ * resource's id column is in scope, including one whose only bound parameter is
18
+ * something else entirely.
19
+ *
20
+ * ── πŸ”΄ …which means this module quotes literals, so it does it in ONE place ───
21
+ * A fragment with no bindable parameters has to inline its values. Every value
22
+ * that reaches SQL here goes through {@link sqlLiteral} (double every apostrophe β€”
23
+ * `o'brien@example.com` is an ordinary address) or {@link sqlColumn} (an
24
+ * identifier, matched against a strict pattern and REFUSED otherwise, so a column
25
+ * expression can never smuggle a subquery in). Both are exported and both are
26
+ * tested with hostile input; a consumer never quotes anything itself.
27
+ */
28
+ import { levelsMeeting, normalizeSubjectId, type ShareLevel } from "cursedbelt-core/sharing/model";
29
+ import { SHARE_GRANTS_TABLE, SHARE_GROUP_MEMBERS_TABLE } from "./migrations";
30
+
31
+ /**
32
+ * Quote a string as a SQL literal.
33
+ *
34
+ * SQLite escapes an apostrophe by doubling it, and that is the whole rule for a
35
+ * TEXT literal β€” there is no backslash escape to worry about. A NUL byte is
36
+ * refused rather than quoted: SQLite truncates a string at one, so a value
37
+ * containing it would compare equal to a shorter value and silently widen a grant.
38
+ */
39
+ export function sqlLiteral(value: string): string {
40
+ if (value.includes("\u0000")) {
41
+ throw new Error("[cursedbelt-core/sharing] a SQL literal may not contain a NUL byte");
42
+ }
43
+ return `'${value.replaceAll("'", "''")}'`;
44
+ }
45
+
46
+ /** A column reference β€” `id`, `p.id`, `"people"."id"` is NOT accepted. Anything
47
+ * that is not a bare (optionally table-qualified) identifier is refused, because
48
+ * this value is interpolated and a caller passing a computed string is how an
49
+ * injection reaches a fragment builder. */
50
+ const COLUMN_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/;
51
+
52
+ export function sqlColumn(column: string): string {
53
+ if (!COLUMN_PATTERN.test(column)) {
54
+ throw new Error(
55
+ `[cursedbelt-core/sharing] ${JSON.stringify(column)} is not a plain column reference ` +
56
+ "(expected `id` or `alias.id`)",
57
+ );
58
+ }
59
+ return column;
60
+ }
61
+
62
+ export interface SharedResourceSqlOptions {
63
+ /** The satellite the resource belongs to β€” the `app` of the `(app, kind, id)`
64
+ * triple. */
65
+ app: string;
66
+ /** The app's own word for this sort of record ("person", "budget"). */
67
+ kind: string;
68
+ /** The viewer's fleet address. An EMPTY id yields `0=1` β€” see below. */
69
+ subjectId: string;
70
+ /** The column holding the resource's id, in scope of the query being built. */
71
+ column: string;
72
+ /** Minimum level the grant must carry. Default `read`. */
73
+ atLeast?: ShareLevel;
74
+ }
75
+
76
+ /**
77
+ * A SQL boolean: true for a row the viewer holds a grant on, at `atLeast` or
78
+ * above, directly or through a group.
79
+ *
80
+ * The group half is a nested `SELECT` rather than a pre-resolved list, so a
81
+ * membership change takes effect on the next query with no request-scoped cache to
82
+ * invalidate β€” the failure mode of caching it is a relative who was removed from
83
+ * the family group this morning and can still read until something restarts.
84
+ *
85
+ * πŸ”΄ **An empty subject id yields `0=1`, never `1=1`.** An unauthenticated or
86
+ * machine caller has no address, and the arithmetic of `subject_id = ''` against a
87
+ * table where nothing has an empty id happens to be safe today β€” but a single
88
+ * malformed row would turn that into "shared with everybody". Failing closed on the
89
+ * missing identity is the property, not a coincidence of the data.
90
+ */
91
+ export function sharedResourceSql(options: SharedResourceSqlOptions): string {
92
+ const subjectId = normalizeSubjectId("user", options.subjectId);
93
+ if (!subjectId) return "0=1";
94
+ const column = sqlColumn(options.column);
95
+ const levels = levelsMeeting(options.atLeast ?? "read")
96
+ .map((level) => sqlLiteral(level))
97
+ .join(", ");
98
+ const subject = sqlLiteral(subjectId);
99
+ return (
100
+ `EXISTS (SELECT 1 FROM ${SHARE_GRANTS_TABLE} sg` +
101
+ ` WHERE sg.app = ${sqlLiteral(options.app)}` +
102
+ ` AND sg.kind = ${sqlLiteral(options.kind)}` +
103
+ ` AND sg.resource_id = ${column}` +
104
+ ` AND sg.level IN (${levels})` +
105
+ ` AND ((sg.subject_type = 'user' AND sg.subject_id = ${subject})` +
106
+ ` OR (sg.subject_type = 'group' AND sg.subject_id IN` +
107
+ ` (SELECT gm.group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} gm WHERE gm.subject_id = ${subject}))))`
108
+ );
109
+ }