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,338 @@
1
+ // search-writer-allow-file: schema-lifecycle suite — it DROPs and rebuilds the saas
2
+ // search schema per run and TRUNCATEs between tests, because the schema and its RLS
3
+ // policy ARE the subject here. No statement below writes an index row outside the
4
+ // provider.
5
+ /**
6
+ * The saas search tier against a REAL Postgres (04 §3).
7
+ *
8
+ * Everything this provider claims is a claim about Postgres, not about
9
+ * TypeScript: that a `GENERATED ALWAYS AS … STORED` tsvector stays in step
10
+ * through an `ON CONFLICT DO UPDATE`, that `websearch_to_tsquery` cannot be made
11
+ * to raise on user input, that `ts_rank` with the 4× title weighting orders a
12
+ * corpus the way the SQLite side does, that `FORCE ROW LEVEL SECURITY` binds the
13
+ * owning role, and that `bytea` round-trips an embedding bit-for-bit. A fake
14
+ * pool can prove the plumbing and nothing else — it would be answering those
15
+ * questions with our own assumptions, which is how the previous arc shipped a
16
+ * search tier that had never run.
17
+ *
18
+ * bun run test:pg (starts a disposable container, runs, tears down)
19
+ * CC_PG_URL=postgres://… (or point it at any throwaway database)
20
+ *
21
+ * SKIPPED, loudly, when `CC_PG_URL` is absent — the skip is a missing proof,
22
+ * never a passing one. It runs nightly (`.github/workflows/nightly.yml`).
23
+ */
24
+ import { SQL } from 'bun';
25
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
26
+ import { createPgSearchProvider } from './pgSearchProvider';
27
+ import { ensureSaasSearchSchema } from './pgSearchSchema';
28
+ const ADMIN_URL = process.env.CC_PG_URL;
29
+ /**
30
+ * A dedicated NON-SUPERUSER owner, and the reason is the finding this suite
31
+ * produced.
32
+ *
33
+ * Connecting as the container's default `postgres` role, the RLS test passed
34
+ * with BOTH tenants' rows: a **superuser bypasses row-level security
35
+ * unconditionally**, and `FORCE ROW LEVEL SECURITY` does not change that — FORCE
36
+ * subjects the table's OWNER to its policy, not a superuser. So the whole
37
+ * isolation guarantee is invisible to a suite that connects as one, and it would
38
+ * be equally invisible in a deployment whose application connects as one. This
39
+ * suite therefore runs as the shape a deployment should have (a non-superuser
40
+ * role that owns its own schema), which makes the policy load-bearing here
41
+ * rather than decorative.
42
+ */
43
+ const ROLE = 'cc_search_rls';
44
+ const PASSWORD = 'cc_search_rls_pw';
45
+ const DATABASE = 'cc_search_rls_db';
46
+ const appUrl = () => {
47
+ const admin = new URL(ADMIN_URL);
48
+ admin.username = ROLE;
49
+ admin.password = PASSWORD;
50
+ admin.pathname = `/${DATABASE}`;
51
+ return admin.toString();
52
+ };
53
+ let sql;
54
+ let pool;
55
+ const TENANT = 't_saas';
56
+ const OTHER = 't_other';
57
+ const CORPUS = 'knowledge';
58
+ const MODEL = 'vocab-v1';
59
+ /** Deterministic token-count embedder — see the local provider's suite for why. */
60
+ const VOCAB = ['canine', 'puppy', 'feline', 'kitten', 'ledger', 'invoice'];
61
+ const embed = async (text) => {
62
+ const lower = text.toLowerCase();
63
+ return Float32Array.from(VOCAB.map((word) => lower.split(word).length - 1));
64
+ };
65
+ const doc = (id, title, body, tenantId = TENANT) => ({
66
+ corpus: CORPUS,
67
+ id,
68
+ tenantId,
69
+ title,
70
+ body,
71
+ });
72
+ // SKIPPED unless CC_PG_URL names a throwaway Postgres — this suite's subject IS the engine. `bun run test:pg` supplies one.
73
+ describe.skipIf(!ADMIN_URL)('createPgSearchProvider against a real Postgres (04 §3)', () => {
74
+ // 120s for the same reason the control-plane suite gives: this hook drops and
75
+ // rebuilds a whole role, database and schema, and on a shared CI runner a 5s
76
+ // default times out mid-migration and reports the failure as though the SQL
77
+ // were wrong.
78
+ beforeAll(async () => {
79
+ const admin = new SQL(ADMIN_URL);
80
+ await admin.unsafe(`DROP DATABASE IF EXISTS ${DATABASE}`);
81
+ await admin.unsafe(`DROP ROLE IF EXISTS ${ROLE}`);
82
+ await admin.unsafe(`CREATE ROLE ${ROLE} LOGIN PASSWORD '${PASSWORD}' NOSUPERUSER NOBYPASSRLS`);
83
+ await admin.unsafe(`CREATE DATABASE ${DATABASE} OWNER ${ROLE}`);
84
+ await admin.close();
85
+ // `max: 1` is load-bearing, not tuning. The provider issues
86
+ // BEGIN / set_config / … / COMMIT, and a driver free to move the next
87
+ // statement onto a different physical connection would scope one connection
88
+ // and query another — which is the exact bug the transaction-local setting
89
+ // exists to prevent, arriving from the test harness instead of the code.
90
+ sql = new SQL(appUrl(), { max: 1 });
91
+ const connection = {
92
+ async query(text, params = []) {
93
+ return (await sql.unsafe(text, [...params]));
94
+ },
95
+ async execute(text, params = []) {
96
+ const result = await sql.unsafe(text, [...params]);
97
+ return { rowCount: result.count ?? 0 };
98
+ },
99
+ };
100
+ pool = {
101
+ async withConnection(fn) {
102
+ return await fn(connection);
103
+ },
104
+ };
105
+ await sql.unsafe('DROP TABLE IF EXISTS search_chunks');
106
+ await sql.unsafe('DROP TABLE IF EXISTS search_docs');
107
+ await ensureSaasSearchSchema(pool);
108
+ }, 120_000);
109
+ afterAll(async () => {
110
+ await sql?.end();
111
+ });
112
+ beforeEach(async () => {
113
+ // TRUNCATE, not DELETE — and the difference is the whole point of running as
114
+ // a non-superuser. A `DELETE` issued outside a tenant scope is checked
115
+ // against the policy, matches nothing, and SILENTLY LEAVES EVERY ROW, so
116
+ // state leaks from test to test and the failures read as ranking bugs.
117
+ // TRUNCATE requires ownership rather than a policy match, so it is the one
118
+ // cleanup that is honest under FORCE RLS.
119
+ await sql.unsafe('TRUNCATE search_chunks, search_docs RESTART IDENTITY');
120
+ });
121
+ const keywordOnly = () => createPgSearchProvider({ pool });
122
+ const hybrid = () => createPgSearchProvider({ pool, embed, embedModel: MODEL });
123
+ describe('schema', () => {
124
+ it('creates itself idempotently — a second deployment run is not a hard failure', async () => {
125
+ // `CREATE POLICY` has no `IF NOT EXISTS`, so this is the assertion that the
126
+ // skip-if-present path works rather than throwing 42710.
127
+ await ensureSaasSearchSchema(pool);
128
+ const policies = (await sql.unsafe("SELECT policyname FROM pg_policies WHERE tablename = 'search_docs'"));
129
+ expect(policies.map((row) => row.policyname)).toEqual(['tenant_isolation']);
130
+ });
131
+ it('FORCES row level security, so the owning role is subject to it too', async () => {
132
+ const [row] = (await sql.unsafe("SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname = 'search_docs'"));
133
+ // ENABLE alone is decoration for the role that owns the schema — which, in
134
+ // this platform, is the role that serves every request.
135
+ expect(row?.relrowsecurity).toBe(true);
136
+ expect(row?.relforcerowsecurity).toBe(true);
137
+ });
138
+ it('keeps the tsvector in step through an ON CONFLICT DO UPDATE', async () => {
139
+ const search = keywordOnly();
140
+ await search.index(doc('d1', 'First title', 'canine body'));
141
+ await search.index(doc('d1', 'Second title', 'feline body'));
142
+ // A trigger written for INSERT only would leave the old postings here. A
143
+ // generated column cannot, because no code path writes it — which is the
144
+ // reason the schema uses one.
145
+ await expect(search.query('feline', { tenantId: TENANT })).resolves.toHaveLength(1);
146
+ await expect(search.query('canine', { tenantId: TENANT })).resolves.toEqual([]);
147
+ });
148
+ });
149
+ describe('the keyword tier', () => {
150
+ it('finds a document and reports a higher-is-better score', async () => {
151
+ const search = keywordOnly();
152
+ await search.index(doc('d1', 'Dogs', 'canine puppy notes'));
153
+ const hits = await search.query('canine', { tenantId: TENANT });
154
+ expect(hits).toHaveLength(1);
155
+ expect(hits[0]?.id).toBe('d1');
156
+ // `ts_rank` is already higher-is-better. The available mistake is copying
157
+ // the SQLite provider's sign flip, which would invert every ranking.
158
+ expect(hits[0]?.score).toBeGreaterThan(0);
159
+ expect(hits[0]?.snippet.length).toBeGreaterThan(0);
160
+ });
161
+ it('weights a title hit above a body hit, exactly as the SQLite profile does', async () => {
162
+ const search = keywordOnly();
163
+ await search.index(doc('body', 'Notes', 'ledger ledger ledger canine'));
164
+ await search.index(doc('title', 'Canine', 'ledger ledger ledger ledger'));
165
+ const hits = await search.query('canine', { tenantId: TENANT });
166
+ // Two profiles that ranked one corpus differently would make "search ranks
167
+ // wrong in production" unreproducible on a laptop.
168
+ expect(hits.map((hit) => hit.id)).toEqual(['title', 'body']);
169
+ });
170
+ it('survives every punctuation a real person types into a search box', async () => {
171
+ const search = keywordOnly();
172
+ await search.index(doc('d1', 'Quotes', 'canine puppy'));
173
+ // The SQLite side needs a phrase-literal retry for these because FTS5
174
+ // raises. `websearch_to_tsquery` is total, so a 500 on a hyphen — a product
175
+ // defect, not user error — is not reachable here at all.
176
+ for (const query of ['"canine', 'canine -', 'AND OR NEAR', 'canine)("', '""""', '-canine']) {
177
+ await expect(search.query(query, { tenantId: TENANT })).resolves.toBeArray();
178
+ }
179
+ });
180
+ it('returns nothing for an empty box rather than the whole corpus', async () => {
181
+ const search = keywordOnly();
182
+ await search.index(doc('d1', 'Any', 'canine'));
183
+ await expect(search.query(' ', { tenantId: TENANT })).resolves.toEqual([]);
184
+ });
185
+ });
186
+ describe('tenancy', () => {
187
+ it('never returns another tenant’s document', async () => {
188
+ const search = keywordOnly();
189
+ await search.index(doc('mine', 'Mine', 'canine puppy'));
190
+ await search.index(doc('theirs', 'Theirs', 'canine puppy', OTHER));
191
+ await expect(search.count(CORPUS, TENANT)).resolves.toBe(1);
192
+ const hits = await search.query('canine', { tenantId: TENANT });
193
+ expect(hits.map((hit) => hit.id)).toEqual(['mine']);
194
+ });
195
+ it('is protected by the DATABASE, not only by the predicate', async () => {
196
+ const search = keywordOnly();
197
+ await search.index(doc('mine', 'Mine', 'canine'));
198
+ await search.index(doc('theirs', 'Theirs', 'canine', OTHER));
199
+ // The same statement the provider runs, minus its tenant predicate, inside
200
+ // the same tenant scope. If the policy were merely ENABLEd (not FORCEd) or
201
+ // the predicate were the only control, this would return both rows.
202
+ const rows = await pool.withConnection(async (connection) => {
203
+ await connection.execute('BEGIN');
204
+ await connection.execute('SELECT set_config($1, $2, true)', ['app.tenant_id', TENANT]);
205
+ const all = await connection.query('SELECT source_id FROM search_docs');
206
+ await connection.execute('COMMIT');
207
+ return all;
208
+ });
209
+ expect(rows.map((row) => row.source_id)).toEqual(['mine']);
210
+ });
211
+ it('refuses an operation with no tenant', async () => {
212
+ const search = keywordOnly();
213
+ await expect(search.query('canine', { tenantId: '' })).rejects.toThrow(/tenantId/);
214
+ });
215
+ });
216
+ describe('the vector tier', () => {
217
+ it('writes chunks only when an embedder is wired', async () => {
218
+ await keywordOnly().index(doc('kw', 'Keyword', 'canine puppy'));
219
+ await expect(keywordOnly().countChunks(CORPUS, TENANT)).resolves.toBe(0);
220
+ const search = hybrid();
221
+ await search.index(doc('vec', 'Vector', 'canine puppy'));
222
+ await expect(search.countChunks(CORPUS, TENANT)).resolves.toBe(1);
223
+ });
224
+ it('round-trips an embedding through bytea bit-for-bit', async () => {
225
+ const search = hybrid();
226
+ await search.index(doc('d1', 'Dogs', 'canine puppy'));
227
+ const [row] = (await sql.unsafe('SELECT embedding, dims, model FROM search_chunks'));
228
+ const expected = await embed('Dogs\n\ncanine puppy');
229
+ // Decoded through the same little-endian codec the reader uses. A driver
230
+ // that handed back a hex string or a differently-offset view would fail
231
+ // here rather than silently ranking by noise.
232
+ const { decodeEmbedding } = await import('cwip/search');
233
+ expect([...decodeEmbedding(row?.embedding)]).toEqual([...expected]);
234
+ expect(row?.dims).toBe(VOCAB.length);
235
+ expect(row?.model).toBe(MODEL);
236
+ });
237
+ it('ranks the semantically nearest document above a keyword-only match', async () => {
238
+ const search = hybrid();
239
+ // Same fixture shape as the SQLite suite, and for the same reason — read
240
+ // that suite's comment before simplifying this to two documents.
241
+ await search.index(doc('decoy', 'Canine Puppy Ledger', 'ledger invoice ledger invoice ledger invoice ledger invoice canine puppy'));
242
+ await search.index(doc('near', 'Notes', 'canine puppy canine puppy canine puppy'));
243
+ await search.index(doc('canines', 'Canines', 'canine canine canine'));
244
+ await search.index(doc('pups', 'Pups', 'puppy puppy puppy'));
245
+ const keyword = await keywordOnly().query('canine puppy', { tenantId: TENANT });
246
+ const fused = await search.query('canine puppy', { tenantId: TENANT });
247
+ // The negative control first: BM25/ts_rank alone puts `decoy` on top.
248
+ expect(keyword[0]?.id).toBe('decoy');
249
+ expect(fused[0]?.id).toBe('near');
250
+ expect(fused.map((hit) => hit.id)).toContain('decoy');
251
+ });
252
+ it('surfaces a document the keyword tier cannot find, with a hydrated title', async () => {
253
+ const search = hybrid();
254
+ await search.index(doc('semantic', 'Puppy notes', 'canine puppy'));
255
+ await expect(keywordOnly().query('kitten feline', { tenantId: TENANT })).resolves.toEqual([]);
256
+ const fused = await search.query('canine kitten', { tenantId: TENANT });
257
+ expect(fused.map((hit) => hit.id)).toContain('semantic');
258
+ expect(fused.find((hit) => hit.id === 'semantic')?.title).toBe('Puppy notes');
259
+ });
260
+ it('never ranks another tenant’s chunks', async () => {
261
+ const search = hybrid();
262
+ await search.index(doc('mine', 'Mine', 'canine puppy'));
263
+ await search.index(doc('theirs', 'Theirs', 'canine puppy', OTHER));
264
+ const hits = await search.query('canine puppy', { tenantId: TENANT });
265
+ expect(hits.map((hit) => hit.id)).toEqual(['mine']);
266
+ });
267
+ it('replaces a document’s chunks on re-index rather than accumulating them', async () => {
268
+ const search = hybrid();
269
+ await search.index(doc('r1', 'First', 'canine puppy'));
270
+ await search.index(doc('r1', 'Second', 'feline kitten'));
271
+ await expect(search.countChunks(CORPUS, TENANT)).resolves.toBe(1);
272
+ await expect(search.count(CORPUS, TENANT)).resolves.toBe(1);
273
+ });
274
+ it('degrades to the keyword tier when the embedder is down', async () => {
275
+ const phases = [];
276
+ const flaky = createPgSearchProvider({
277
+ pool,
278
+ embed: async () => {
279
+ throw new Error('embedding provider is down');
280
+ },
281
+ onVectorError: (_error, phase) => phases.push(phase),
282
+ });
283
+ await flaky.index(doc('d1', 'Dogs', 'canine puppy'));
284
+ // The document still landed — a failing embedder must not look like a
285
+ // write failure to the CDC tap, which would retry into an outage.
286
+ await expect(flaky.count(CORPUS, TENANT)).resolves.toBe(1);
287
+ await expect(flaky.query('canine', { tenantId: TENANT })).resolves.toHaveLength(1);
288
+ expect(phases).toEqual(['index', 'query']);
289
+ });
290
+ it('reports a truncated candidate scan', async () => {
291
+ const truncations = [];
292
+ const search = createPgSearchProvider({
293
+ pool,
294
+ embed,
295
+ embedModel: MODEL,
296
+ vectorCandidateLimit: 1,
297
+ onVectorTruncated: (detail) => truncations.push(detail),
298
+ });
299
+ await search.index(doc('t1', 'One', 'canine puppy'));
300
+ await search.index(doc('t2', 'Two', 'canine kitten'));
301
+ await search.query('canine', { tenantId: TENANT });
302
+ expect(truncations).toHaveLength(1);
303
+ });
304
+ });
305
+ describe('rebuild operations', () => {
306
+ it('reads its epoch from the DATABASE clock, in the index’s own format', async () => {
307
+ const epoch = await keywordOnly().now();
308
+ // Same format the SQLite provider's `strftime` produces — the reindexer
309
+ // compares this string against `indexed_at`, and two formats would make
310
+ // that comparison quietly wrong rather than loudly broken.
311
+ expect(epoch).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
312
+ });
313
+ it('sweeps documents older than the epoch, and their chunks with them', async () => {
314
+ const search = hybrid();
315
+ await search.index(doc('old', 'Old', 'canine puppy'));
316
+ await Bun.sleep(5);
317
+ const epoch = await search.now();
318
+ await search.index(doc('new', 'New', 'canine puppy'));
319
+ // Counted as DOCUMENTS. Reading the delete's row count would report the
320
+ // cascaded chunks too, and the reindex job's log would lie about how much
321
+ // it removed.
322
+ await expect(search.sweepStale(CORPUS, TENANT, epoch)).resolves.toBe(1);
323
+ await expect(search.count(CORPUS, TENANT)).resolves.toBe(1);
324
+ await expect(search.countChunks(CORPUS, TENANT)).resolves.toBe(1);
325
+ await expect(search.query('canine', { tenantId: TENANT })).resolves.toHaveLength(1);
326
+ });
327
+ it('removes a document and its chunks together', async () => {
328
+ const search = hybrid();
329
+ await search.index(doc('gone', 'Gone', 'canine puppy'));
330
+ await search.remove(CORPUS, 'gone', TENANT);
331
+ await expect(search.count(CORPUS, TENANT)).resolves.toBe(0);
332
+ await expect(search.countChunks(CORPUS, TENANT)).resolves.toBe(0);
333
+ });
334
+ it('does not treat removing an unindexed document as an error', async () => {
335
+ await expect(keywordOnly().remove(CORPUS, 'never-indexed', TENANT)).resolves.toBeUndefined();
336
+ });
337
+ });
338
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The saas search schema (04 §3) — `search_docs` + `search_chunks` on Postgres.
3
+ *
4
+ * `domains/search.ts` refuses to compile its SQLite chain for Postgres
5
+ * (`PG_DDL_UNSUPPORTED`) and says why: `search_fts` is an FTS5 external-content
6
+ * virtual table maintained by SQLite triggers, which is not a dialect
7
+ * difference — it is a different search engine. So the saas keyword tier is
8
+ * authored here, natively, and the two providers share only the vector tier
9
+ * (`cwip`'s `searchVectorTier`, one implementation for both).
10
+ *
11
+ * ## Why a GENERATED column rather than a trigger
12
+ *
13
+ * The SQLite side keeps its FTS shadow in step with three triggers, and its own
14
+ * header explains the hazard: "application-maintained shadow tables drift the
15
+ * moment one code path forgets". Postgres offers something stronger than a
16
+ * trigger for this — a `GENERATED ALWAYS AS (…) STORED` column cannot drift at
17
+ * all, because there is no code path that writes it. A trigger can be dropped, a
18
+ * migration can forget to recreate it, and `INSERT ... ON CONFLICT DO UPDATE`
19
+ * can bypass one that was written for the wrong event. A generated column has
20
+ * none of those failure modes: the tsvector is a function of the row, enforced
21
+ * by the storage engine.
22
+ *
23
+ * ## `simple` rather than `english`
24
+ *
25
+ * The text config is `simple` — no stemming, no stopwords. Two reasons, and the
26
+ * second is the load-bearing one:
27
+ *
28
+ * 1. This platform's corpora are multi-lingual by construction (a customer's
29
+ * notes, filenames, contact names). Stemming with the WRONG language is
30
+ * worse than not stemming: `english` drops "die"/"das" as if they were
31
+ * stopwords and stems "arbeiten" to nothing useful.
32
+ * 2. `english` discards stopwords, so a search for a title that IS a stopword
33
+ * phrase ("The Who", "It") returns nothing at all — and the SQLite side's
34
+ * `unicode61` tokenizer has no stopword list either. Two tiers that
35
+ * disagree about whether a document is findable is a bug report a customer
36
+ * files as "search works on my laptop".
37
+ *
38
+ * ## RLS
39
+ *
40
+ * `search_docs` is tenant-rooted (`SEARCH_TENANCY`), so it carries the same
41
+ * `tenant_isolation` policy every domain table does, and `search_chunks`
42
+ * inherits through its `doc_id` FK. That is defense in depth, not the primary
43
+ * control: the provider binds `tenant_id` into every statement it issues, the
44
+ * way the SQLite provider does. The policy is what makes a future statement that
45
+ * forgets return zero rows instead of everyone's.
46
+ *
47
+ * DDL is NEVER issued on a process start — {@link ensureSaasSearchSchema} is a
48
+ * named call a deployment makes once, for the same reason
49
+ * `ensureSaasControlPlaneSchema` is: a shared database has one schema owner, and
50
+ * a library that migrated on boot would be N of them racing.
51
+ */
52
+ import type { SaasPool } from 'cwip/storage';
53
+ /**
54
+ * Weights fed to `ts_rank`, in Postgres' `{D, C, B, A}` order. Title 4× body,
55
+ * matching the SQLite provider's `bm25(search_fts, 4.0, 1.0)` exactly — two
56
+ * profiles that ranked the same corpus differently would make "it ranks wrong in
57
+ * production" unreproducible on a laptop.
58
+ */
59
+ export declare const PG_RANK_WEIGHTS = "{0.1, 0.1, 0.25, 1.0}";
60
+ export declare const PG_SEARCH_DOCS_DDL = "CREATE TABLE IF NOT EXISTS search_docs (\n id BIGSERIAL PRIMARY KEY,\n corpus TEXT NOT NULL,\n source_id TEXT NOT NULL,\n tenant_id TEXT NOT NULL,\n user_id TEXT,\n title TEXT NOT NULL DEFAULT '',\n body TEXT NOT NULL DEFAULT '',\n rev INTEGER NOT NULL DEFAULT 0,\n indexed_at TEXT NOT NULL,\n tsv tsvector GENERATED ALWAYS AS (setweight(to_tsvector('simple', coalesce(title, '')), 'A') || setweight(to_tsvector('simple', coalesce(body, '')), 'B')) STORED\n)";
61
+ export declare const PG_SEARCH_CHUNKS_DDL = "CREATE TABLE IF NOT EXISTS search_chunks (\n id BIGSERIAL PRIMARY KEY,\n doc_id BIGINT NOT NULL REFERENCES search_docs(id) ON DELETE CASCADE,\n seq INTEGER NOT NULL,\n text TEXT NOT NULL,\n embedding BYTEA,\n dims INTEGER,\n model TEXT,\n indexed_at TEXT NOT NULL\n)";
62
+ /**
63
+ * Every statement that builds the saas search schema, in dependency order.
64
+ *
65
+ * The unique index on `(corpus, source_id, tenant_id)` is what makes the
66
+ * provider's upsert idempotent — it is the same natural key the SQLite schema
67
+ * declares, and 05 §A1's cross-tenant-uniqueness lesson is why `tenant_id` is
68
+ * in it rather than assumed.
69
+ */
70
+ export declare const PG_SEARCH_DDL: readonly string[];
71
+ /**
72
+ * Create the saas search schema. Idempotent, and called EXPLICITLY by a
73
+ * deployment — never on a process start. See the file header.
74
+ *
75
+ * `CREATE POLICY` has no `IF NOT EXISTS` in Postgres, so the policy statements
76
+ * are skipped when the policy is already there rather than being wrapped in a
77
+ * swallow-everything `DO` block: a block that ignores the duplicate-object error
78
+ * also ignores a typo in the predicate, which would leave the table enabled,
79
+ * forced, and isolated by a policy that never matches.
80
+ */
81
+ export declare const ensureSaasSearchSchema: (pool: SaasPool) => Promise<void>;
@@ -0,0 +1,89 @@
1
+ import { assertRowLevelSecurity, createPostgresControlExecutor, rlsStatements } from 'cwip/storage';
2
+ /** The tsvector expression. Title weighted `A`, body `B` — `ts_rank` reads the weights. */
3
+ const TSV_EXPRESSION = `setweight(to_tsvector('simple', coalesce(title, '')), 'A') || setweight(to_tsvector('simple', coalesce(body, '')), 'B')`;
4
+ /**
5
+ * Weights fed to `ts_rank`, in Postgres' `{D, C, B, A}` order. Title 4× body,
6
+ * matching the SQLite provider's `bm25(search_fts, 4.0, 1.0)` exactly — two
7
+ * profiles that ranked the same corpus differently would make "it ranks wrong in
8
+ * production" unreproducible on a laptop.
9
+ */
10
+ export const PG_RANK_WEIGHTS = '{0.1, 0.1, 0.25, 1.0}';
11
+ export const PG_SEARCH_DOCS_DDL = `CREATE TABLE IF NOT EXISTS search_docs (
12
+ id BIGSERIAL PRIMARY KEY,
13
+ corpus TEXT NOT NULL,
14
+ source_id TEXT NOT NULL,
15
+ tenant_id TEXT NOT NULL,
16
+ user_id TEXT,
17
+ title TEXT NOT NULL DEFAULT '',
18
+ body TEXT NOT NULL DEFAULT '',
19
+ rev INTEGER NOT NULL DEFAULT 0,
20
+ indexed_at TEXT NOT NULL,
21
+ tsv tsvector GENERATED ALWAYS AS (${TSV_EXPRESSION}) STORED
22
+ )`;
23
+ export const PG_SEARCH_CHUNKS_DDL = `CREATE TABLE IF NOT EXISTS search_chunks (
24
+ id BIGSERIAL PRIMARY KEY,
25
+ doc_id BIGINT NOT NULL REFERENCES search_docs(id) ON DELETE CASCADE,
26
+ seq INTEGER NOT NULL,
27
+ text TEXT NOT NULL,
28
+ embedding BYTEA,
29
+ dims INTEGER,
30
+ model TEXT,
31
+ indexed_at TEXT NOT NULL
32
+ )`;
33
+ /**
34
+ * Every statement that builds the saas search schema, in dependency order.
35
+ *
36
+ * The unique index on `(corpus, source_id, tenant_id)` is what makes the
37
+ * provider's upsert idempotent — it is the same natural key the SQLite schema
38
+ * declares, and 05 §A1's cross-tenant-uniqueness lesson is why `tenant_id` is
39
+ * in it rather than assumed.
40
+ */
41
+ export const PG_SEARCH_DDL = [
42
+ PG_SEARCH_DOCS_DDL,
43
+ 'CREATE UNIQUE INDEX IF NOT EXISTS search_docs_source ON search_docs(corpus, source_id, tenant_id)',
44
+ 'CREATE INDEX IF NOT EXISTS search_docs_tenant ON search_docs(tenant_id, corpus, indexed_at DESC)',
45
+ 'CREATE INDEX IF NOT EXISTS search_docs_tsv ON search_docs USING GIN(tsv)',
46
+ PG_SEARCH_CHUNKS_DDL,
47
+ 'CREATE UNIQUE INDEX IF NOT EXISTS search_chunks_seq ON search_chunks(doc_id, seq)',
48
+ // The vector tier's candidate scan filters on the join plus (dims, model);
49
+ // without this it is a sequential scan of every tenant's chunks per query.
50
+ 'CREATE INDEX IF NOT EXISTS search_chunks_space ON search_chunks(doc_id, dims, model)',
51
+ // The platform's OWN policy text, not a hand-written copy. `rlsStatements`
52
+ // also emits `FORCE ROW LEVEL SECURITY`, which is the half a hand-written
53
+ // chain forgets and the half that matters here: a table's OWNER bypasses RLS
54
+ // unless it is FORCEd, and in this platform the application role owns its own
55
+ // schema. Without it the policy is decoration for exactly the connection that
56
+ // serves every request.
57
+ ...rlsStatements('search_docs'),
58
+ ];
59
+ // Self-check at module load, the same one `toPostgresChain` performs on a
60
+ // compiled chain. This chain is hand-authored (SQLite's FTS5 shadow has no
61
+ // Postgres translation), so it does NOT go through the compiler that would
62
+ // normally apply this gate — and a hand-authored chain is precisely the one that
63
+ // can ship a tenant-rooted table with no policy.
64
+ assertRowLevelSecurity(PG_SEARCH_DDL);
65
+ /**
66
+ * Create the saas search schema. Idempotent, and called EXPLICITLY by a
67
+ * deployment — never on a process start. See the file header.
68
+ *
69
+ * `CREATE POLICY` has no `IF NOT EXISTS` in Postgres, so the policy statements
70
+ * are skipped when the policy is already there rather than being wrapped in a
71
+ * swallow-everything `DO` block: a block that ignores the duplicate-object error
72
+ * also ignores a typo in the predicate, which would leave the table enabled,
73
+ * forced, and isolated by a policy that never matches.
74
+ */
75
+ export const ensureSaasSearchSchema = async (pool) => {
76
+ await pool.withConnection(async (connection) => {
77
+ const executor = createPostgresControlExecutor(connection);
78
+ const existing = await executor.all({
79
+ sql: "SELECT policyname FROM pg_policies WHERE tablename = 'search_docs'",
80
+ });
81
+ const present = new Set(existing.map((row) => row.policyname));
82
+ for (const statement of PG_SEARCH_DDL) {
83
+ const policy = /^CREATE\s+POLICY\s+([a-z_]+)\s+ON\b/i.exec(statement.trim());
84
+ if (policy?.[1] !== undefined && present.has(policy[1]))
85
+ continue;
86
+ await executor.write({ sql: statement });
87
+ }
88
+ });
89
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `cursedbelt-server/sharing` — the fleet's ONE per-record sharing/access model.
3
+ *
4
+ * The vocabulary is `cursedbelt-core/sharing` (pure), the panel is
5
+ * `cursedbelt/react/sharing`, and this is the storage + resolver half. Read
6
+ * `./store.ts`'s header for the five properties it guarantees and `./sql.ts` for
7
+ * why the row filter is a SQL fragment rather than a list of ids.
8
+ */
9
+ export { SHARE_MIGRATIONS } from "./migrations";
10
+ export { SHARE_EVENTS_TABLE, SHARE_GRANTS_TABLE, SHARE_GROUPS_TABLE, SHARE_GROUP_MEMBERS_TABLE, } from "./migrations";
11
+ export { GROUP_SUBJECT_PREFIX, mountShareRoutes, parseSubjectParam, subjectParam, } from "./router";
12
+ export type { ShareRouterOptions, ShareRouterViewer, ShareRoutesPayload } from "./router";
13
+ export { sharedResourceSql, sqlColumn, sqlLiteral } from "./sql";
14
+ export type { SharedResourceSqlOptions } from "./sql";
15
+ export { createShareStore, group, user } from "./store";
16
+ export type { GrantInput, RevokeInput, ShareEventQuery, ShareStore, ShareStoreOptions, ShareTarget, } from "./store";
17
+ export { accessMeets, describeAccess, describeShareEvent, isShareAccess, isShareLevel, isShareSubjectType, levelsMeeting, normalizeSubject, normalizeSubjectId, resolveAccess, resourceKey, sameResource, sameSubject, SHARE_ACTIONS, strongerAccess, } from "cursedbelt-core/sharing/model";
18
+ export type { ShareAccess, ShareAction, ShareEvent, ShareGrant, ShareGroup, ShareGroupMember, ShareLevel, ShareResource, ShareSubject, ShareSubjectType, ShareViewer, } from "cursedbelt-core/sharing/model";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `cursedbelt-server/sharing` — the fleet's ONE per-record sharing/access model.
3
+ *
4
+ * The vocabulary is `cursedbelt-core/sharing` (pure), the panel is
5
+ * `cursedbelt/react/sharing`, and this is the storage + resolver half. Read
6
+ * `./store.ts`'s header for the five properties it guarantees and `./sql.ts` for
7
+ * why the row filter is a SQL fragment rather than a list of ids.
8
+ */
9
+ export { SHARE_MIGRATIONS } from "./migrations";
10
+ export { SHARE_EVENTS_TABLE, SHARE_GRANTS_TABLE, SHARE_GROUPS_TABLE, SHARE_GROUP_MEMBERS_TABLE, } from "./migrations";
11
+ export { GROUP_SUBJECT_PREFIX, mountShareRoutes, parseSubjectParam, subjectParam, } from "./router";
12
+ export { sharedResourceSql, sqlColumn, sqlLiteral } from "./sql";
13
+ export { createShareStore, group, user } from "./store";
14
+ // Re-exported so a server consumer needs ONE import for the whole contract; the
15
+ // pure entry (`cursedbelt-core/sharing`) stays available for a browser or a test.
16
+ export { accessMeets, describeAccess, describeShareEvent, isShareAccess, isShareLevel, isShareSubjectType, levelsMeeting, normalizeSubject, normalizeSubjectId, resolveAccess, resourceKey, sameResource, sameSubject, SHARE_ACTIONS, strongerAccess, } from "cursedbelt-core/sharing/model";
@@ -0,0 +1,6 @@
1
+ import type { Migration } from "../migration/types";
2
+ export declare const SHARE_GRANTS_TABLE = "share_grants";
3
+ export declare const SHARE_GROUPS_TABLE = "share_groups";
4
+ export declare const SHARE_GROUP_MEMBERS_TABLE = "share_group_members";
5
+ export declare const SHARE_EVENTS_TABLE = "share_events";
6
+ export declare const SHARE_MIGRATIONS: readonly Migration[];
@@ -0,0 +1,79 @@
1
+ export const SHARE_GRANTS_TABLE = "share_grants";
2
+ export const SHARE_GROUPS_TABLE = "share_groups";
3
+ export const SHARE_GROUP_MEMBERS_TABLE = "share_group_members";
4
+ export const SHARE_EVENTS_TABLE = "share_events";
5
+ export const SHARE_MIGRATIONS = [
6
+ {
7
+ name: "0001_share_grants",
8
+ up(db) {
9
+ db.run(`
10
+ CREATE TABLE IF NOT EXISTS ${SHARE_GRANTS_TABLE} (
11
+ grant_id INTEGER PRIMARY KEY AUTOINCREMENT,
12
+ app TEXT NOT NULL,
13
+ kind TEXT NOT NULL,
14
+ resource_id TEXT NOT NULL,
15
+ subject_type TEXT NOT NULL CHECK (subject_type IN ('user', 'group')),
16
+ subject_id TEXT NOT NULL,
17
+ level TEXT NOT NULL CHECK (level IN ('read', 'write')),
18
+ granted_by TEXT NOT NULL,
19
+ granted_at TEXT NOT NULL,
20
+ note TEXT NOT NULL DEFAULT '',
21
+ -- One grant per subject per resource. This is what makes raising
22
+ -- read to write an UPDATE the audit trail can describe, instead of
23
+ -- two rows where revoking one leaves access in place.
24
+ UNIQUE (app, kind, resource_id, subject_type, subject_id)
25
+ )
26
+ `);
27
+ // The hot read is the resolver's: "does this subject hold a grant on this
28
+ // row" — correlated per row of a list query, so it must be an index hit.
29
+ db.run(`CREATE INDEX IF NOT EXISTS idx_share_grants_subject
30
+ ON ${SHARE_GRANTS_TABLE} (subject_type, subject_id, app, kind, resource_id)`);
31
+ // …and the panel's: "who can see this record".
32
+ db.run(`CREATE INDEX IF NOT EXISTS idx_share_grants_resource
33
+ ON ${SHARE_GRANTS_TABLE} (app, kind, resource_id)`);
34
+ db.run(`
35
+ CREATE TABLE IF NOT EXISTS ${SHARE_GROUPS_TABLE} (
36
+ group_id TEXT PRIMARY KEY,
37
+ name TEXT NOT NULL,
38
+ created_by TEXT NOT NULL,
39
+ created_at TEXT NOT NULL,
40
+ note TEXT NOT NULL DEFAULT ''
41
+ )
42
+ `);
43
+ db.run(`
44
+ CREATE TABLE IF NOT EXISTS ${SHARE_GROUP_MEMBERS_TABLE} (
45
+ group_id TEXT NOT NULL REFERENCES ${SHARE_GROUPS_TABLE} (group_id) ON DELETE CASCADE,
46
+ subject_id TEXT NOT NULL,
47
+ added_by TEXT NOT NULL,
48
+ added_at TEXT NOT NULL,
49
+ PRIMARY KEY (group_id, subject_id)
50
+ )
51
+ `);
52
+ // "Which groups is this person in" — read by every shared-row query.
53
+ db.run(`CREATE INDEX IF NOT EXISTS idx_share_group_members_subject
54
+ ON ${SHARE_GROUP_MEMBERS_TABLE} (subject_id)`);
55
+ db.run(`
56
+ CREATE TABLE IF NOT EXISTS ${SHARE_EVENTS_TABLE} (
57
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ at TEXT NOT NULL,
59
+ action TEXT NOT NULL,
60
+ app TEXT,
61
+ kind TEXT,
62
+ resource_id TEXT,
63
+ subject_type TEXT NOT NULL CHECK (subject_type IN ('user', 'group')),
64
+ subject_id TEXT NOT NULL,
65
+ level TEXT,
66
+ previous_level TEXT,
67
+ actor TEXT NOT NULL,
68
+ note TEXT NOT NULL DEFAULT ''
69
+ )
70
+ `);
71
+ // "Everything that ever happened to this record", newest first.
72
+ db.run(`CREATE INDEX IF NOT EXISTS idx_share_events_resource
73
+ ON ${SHARE_EVENTS_TABLE} (app, kind, resource_id, event_id DESC)`);
74
+ // "Everything this person was ever given" — the offboarding question.
75
+ db.run(`CREATE INDEX IF NOT EXISTS idx_share_events_subject
76
+ ON ${SHARE_EVENTS_TABLE} (subject_id, event_id DESC)`);
77
+ },
78
+ },
79
+ ];