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.
- package/LICENSE +15 -0
- package/dist/server/activity/index.d.ts +36 -0
- package/dist/server/activity/index.js +36 -0
- package/dist/server/activity/migrations.d.ts +21 -0
- package/dist/server/activity/migrations.js +55 -0
- package/dist/server/activity/shareSource.d.ts +11 -0
- package/dist/server/activity/shareSource.js +45 -0
- package/dist/server/activity/store.d.ts +123 -0
- package/dist/server/activity/store.js +178 -0
- package/dist/server/app.d.ts +56 -0
- package/dist/server/app.js +45 -0
- package/dist/server/audit/index.d.ts +2 -0
- package/dist/server/audit/index.js +2 -0
- package/dist/server/audit/migrations.d.ts +2 -0
- package/dist/server/audit/migrations.js +79 -0
- package/dist/server/audit/sqliteSink.d.ts +36 -0
- package/dist/server/audit/sqliteSink.js +95 -0
- package/dist/server/audit/sqliteSink.test.d.ts +1 -0
- package/dist/server/audit/sqliteSink.test.js +180 -0
- package/dist/server/auth/cookie.d.ts +22 -0
- package/dist/server/auth/cookie.js +35 -0
- package/dist/server/auth/csrf.d.ts +42 -0
- package/dist/server/auth/csrf.js +36 -0
- package/dist/server/auth/fileTokenSigner.d.ts +10 -0
- package/dist/server/auth/fileTokenSigner.js +22 -0
- package/dist/server/auth/getOnly.d.ts +20 -0
- package/dist/server/auth/getOnly.js +27 -0
- package/dist/server/auth/index.d.ts +12 -0
- package/dist/server/auth/index.js +15 -0
- package/dist/server/auth/jwt.d.ts +18 -0
- package/dist/server/auth/jwt.js +56 -0
- package/dist/server/auth/loginThrottle.d.ts +118 -0
- package/dist/server/auth/loginThrottle.js +204 -0
- package/dist/server/auth/middleware.d.ts +14 -0
- package/dist/server/auth/middleware.js +47 -0
- package/dist/server/auth/password.d.ts +14 -0
- package/dist/server/auth/password.js +65 -0
- package/dist/server/auth/sessionStore.d.ts +19 -0
- package/dist/server/auth/sessionStore.js +221 -0
- package/dist/server/auth/totp.d.ts +34 -0
- package/dist/server/auth/totp.js +81 -0
- package/dist/server/auth/types.d.ts +133 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/vault.d.ts +27 -0
- package/dist/server/auth/vault.js +57 -0
- package/dist/server/context.d.ts +35 -0
- package/dist/server/context.js +1 -0
- package/dist/server/ctgr.d.ts +32 -0
- package/dist/server/ctgr.js +69 -0
- package/dist/server/db/kysely.d.ts +14 -0
- package/dist/server/db/kysely.js +16 -0
- package/dist/server/db/plumbingTypes.d.ts +116 -0
- package/dist/server/db/plumbingTypes.js +12 -0
- package/dist/server/dropzone/duplicates.d.ts +82 -0
- package/dist/server/dropzone/duplicates.js +90 -0
- package/dist/server/dropzone/index.d.ts +13 -0
- package/dist/server/dropzone/index.js +13 -0
- package/dist/server/dropzone/scan.d.ts +166 -0
- package/dist/server/dropzone/scan.js +272 -0
- package/dist/server/errors.d.ts +7 -0
- package/dist/server/errors.js +7 -0
- package/dist/server/guard/argon2Binding.d.ts +97 -0
- package/dist/server/guard/argon2Binding.js +138 -0
- package/dist/server/guard/index.d.ts +4 -0
- package/dist/server/guard/index.js +6 -0
- package/dist/server/guard/kdfWorker.d.ts +101 -0
- package/dist/server/guard/kdfWorker.js +301 -0
- package/dist/server/guard/kdfWorkerEntry.d.ts +1 -0
- package/dist/server/guard/kdfWorkerEntry.js +15 -0
- package/dist/server/guard/passwordVerifier.d.ts +65 -0
- package/dist/server/guard/passwordVerifier.js +122 -0
- package/dist/server/guard/revocationStore.d.ts +132 -0
- package/dist/server/guard/revocationStore.js +211 -0
- package/dist/server/guard/sessionSigner.d.ts +132 -0
- package/dist/server/guard/sessionSigner.js +303 -0
- package/dist/server/guard/verifier.d.ts +21 -0
- package/dist/server/guard/verifier.js +1 -0
- package/dist/server/incidents/channels.d.ts +161 -0
- package/dist/server/incidents/channels.js +357 -0
- package/dist/server/incidents/checks.d.ts +61 -0
- package/dist/server/incidents/checks.js +55 -0
- package/dist/server/incidents/index.d.ts +51 -0
- package/dist/server/incidents/index.js +51 -0
- package/dist/server/incidents/migrations.d.ts +20 -0
- package/dist/server/incidents/migrations.js +58 -0
- package/dist/server/incidents/notifier.d.ts +79 -0
- package/dist/server/incidents/notifier.js +141 -0
- package/dist/server/incidents/reporter.d.ts +60 -0
- package/dist/server/incidents/reporter.js +49 -0
- package/dist/server/incidents/routes.d.ts +91 -0
- package/dist/server/incidents/routes.js +202 -0
- package/dist/server/incidents/store.d.ts +75 -0
- package/dist/server/incidents/store.js +176 -0
- package/dist/server/incidents/types.d.ts +107 -0
- package/dist/server/incidents/types.js +32 -0
- package/dist/server/index.d.ts +25 -0
- package/dist/server/index.js +53 -0
- package/dist/server/jobs/fireQueue.d.ts +12 -0
- package/dist/server/jobs/fireQueue.js +37 -0
- package/dist/server/jobs/index.d.ts +6 -0
- package/dist/server/jobs/index.js +10 -0
- package/dist/server/jobs/logger.d.ts +10 -0
- package/dist/server/jobs/logger.js +14 -0
- package/dist/server/jobs/plainjobAdapter.d.ts +19 -0
- package/dist/server/jobs/plainjobAdapter.js +48 -0
- package/dist/server/jobs/polling.d.ts +17 -0
- package/dist/server/jobs/polling.js +79 -0
- package/dist/server/jobs/runner.d.ts +24 -0
- package/dist/server/jobs/runner.js +186 -0
- package/dist/server/jobs/timeout.d.ts +28 -0
- package/dist/server/jobs/timeout.js +47 -0
- package/dist/server/jobs/types.d.ts +167 -0
- package/dist/server/jobs/types.js +12 -0
- package/dist/server/master-lock/guard.d.ts +53 -0
- package/dist/server/master-lock/guard.js +205 -0
- package/dist/server/master-lock/index.d.ts +21 -0
- package/dist/server/master-lock/index.js +21 -0
- package/dist/server/master-lock/lockPage.d.ts +34 -0
- package/dist/server/master-lock/lockPage.js +281 -0
- package/dist/server/master-lock/masterLock.d.ts +294 -0
- package/dist/server/master-lock/masterLock.js +523 -0
- package/dist/server/master-lock/principals.d.ts +76 -0
- package/dist/server/master-lock/principals.js +157 -0
- package/dist/server/master-lock/seed.d.ts +47 -0
- package/dist/server/master-lock/seed.js +93 -0
- package/dist/server/master-lock/store.d.ts +19 -0
- package/dist/server/master-lock/store.js +20 -0
- package/dist/server/media-bun/image.d.ts +7 -0
- package/dist/server/media-bun/image.js +96 -0
- package/dist/server/media-bun/index.d.ts +4 -0
- package/dist/server/media-bun/index.js +10 -0
- package/dist/server/media-bun/streaming.d.ts +86 -0
- package/dist/server/media-bun/streaming.js +237 -0
- package/dist/server/media-bun/video.d.ts +35 -0
- package/dist/server/media-bun/video.js +181 -0
- package/dist/server/metrics/metricsBuffer.d.ts +39 -0
- package/dist/server/metrics/metricsBuffer.js +36 -0
- package/dist/server/metrics/normalizeRoute.d.ts +5 -0
- package/dist/server/metrics/normalizeRoute.js +58 -0
- package/dist/server/middleware/correlationId.d.ts +9 -0
- package/dist/server/middleware/correlationId.js +20 -0
- package/dist/server/middleware/corsAllowlist.d.ts +38 -0
- package/dist/server/middleware/corsAllowlist.js +37 -0
- package/dist/server/middleware/errorEnvelope.d.ts +17 -0
- package/dist/server/middleware/errorEnvelope.js +32 -0
- package/dist/server/middleware/index.d.ts +9 -0
- package/dist/server/middleware/index.js +10 -0
- package/dist/server/middleware/localOrigin.d.ts +71 -0
- package/dist/server/middleware/localOrigin.js +119 -0
- package/dist/server/middleware/rateLimit.d.ts +56 -0
- package/dist/server/middleware/rateLimit.js +86 -0
- package/dist/server/middleware/requestLogger.d.ts +29 -0
- package/dist/server/middleware/requestLogger.js +47 -0
- package/dist/server/middleware/securityHeaders.d.ts +29 -0
- package/dist/server/middleware/securityHeaders.js +44 -0
- package/dist/server/middleware/zValidator.d.ts +29 -0
- package/dist/server/middleware/zValidator.js +57 -0
- package/dist/server/migration/runner.d.ts +42 -0
- package/dist/server/migration/runner.js +70 -0
- package/dist/server/migration/types.d.ts +16 -0
- package/dist/server/migration/types.js +1 -0
- package/dist/server/net/outboundGuard.d.ts +30 -0
- package/dist/server/net/outboundGuard.js +199 -0
- package/dist/server/notifications/hub.d.ts +67 -0
- package/dist/server/notifications/hub.js +187 -0
- package/dist/server/notifications/index.d.ts +47 -0
- package/dist/server/notifications/index.js +46 -0
- package/dist/server/notifications/migrations.d.ts +13 -0
- package/dist/server/notifications/migrations.js +20 -0
- package/dist/server/notifications/routes.d.ts +58 -0
- package/dist/server/notifications/routes.js +113 -0
- package/dist/server/notifications/service.d.ts +58 -0
- package/dist/server/notifications/service.js +41 -0
- package/dist/server/paths/appDataHome.d.ts +19 -0
- package/dist/server/paths/appDataHome.js +68 -0
- package/dist/server/private-media/crypto.d.ts +37 -0
- package/dist/server/private-media/crypto.js +111 -0
- package/dist/server/private-media/index-store.d.ts +31 -0
- package/dist/server/private-media/index-store.js +36 -0
- package/dist/server/private-media/mountPrivateRoutes.d.ts +26 -0
- package/dist/server/private-media/mountPrivateRoutes.js +219 -0
- package/dist/server/private-media/pipeline.d.ts +37 -0
- package/dist/server/private-media/pipeline.js +97 -0
- package/dist/server/private-media/unlock.d.ts +52 -0
- package/dist/server/private-media/unlock.js +55 -0
- package/dist/server/resume/index.d.ts +2 -0
- package/dist/server/resume/index.js +2 -0
- package/dist/server/resume/routes.d.ts +41 -0
- package/dist/server/resume/routes.js +50 -0
- package/dist/server/resume/store.d.ts +37 -0
- package/dist/server/resume/store.js +38 -0
- package/dist/server/retention/archive.d.ts +78 -0
- package/dist/server/retention/archive.js +284 -0
- package/dist/server/retention/fleet.d.ts +50 -0
- package/dist/server/retention/fleet.js +66 -0
- package/dist/server/retention/index.d.ts +38 -0
- package/dist/server/retention/index.js +38 -0
- package/dist/server/retention/policy.d.ts +263 -0
- package/dist/server/retention/policy.js +264 -0
- package/dist/server/retention/store.d.ts +101 -0
- package/dist/server/retention/store.js +251 -0
- package/dist/server/retention/sweeper.d.ts +69 -0
- package/dist/server/retention/sweeper.js +289 -0
- package/dist/server/retention/target.d.ts +143 -0
- package/dist/server/retention/target.js +438 -0
- package/dist/server/search/index.d.ts +2 -0
- package/dist/server/search/index.js +6 -0
- package/dist/server/search/pgSearchProvider.d.ts +91 -0
- package/dist/server/search/pgSearchProvider.js +330 -0
- package/dist/server/search/pgSearchProvider.pg.test.d.ts +1 -0
- package/dist/server/search/pgSearchProvider.pg.test.js +338 -0
- package/dist/server/search/pgSearchSchema.d.ts +81 -0
- package/dist/server/search/pgSearchSchema.js +89 -0
- package/dist/server/sharing/index.d.ts +18 -0
- package/dist/server/sharing/index.js +16 -0
- package/dist/server/sharing/migrations.d.ts +6 -0
- package/dist/server/sharing/migrations.js +79 -0
- package/dist/server/sharing/router.d.ts +111 -0
- package/dist/server/sharing/router.js +93 -0
- package/dist/server/sharing/sql.d.ts +67 -0
- package/dist/server/sharing/sql.js +88 -0
- package/dist/server/sharing/store.d.ts +173 -0
- package/dist/server/sharing/store.js +484 -0
- package/dist/server/sqlite/backup.d.ts +48 -0
- package/dist/server/sqlite/backup.js +97 -0
- package/dist/server/sqlite/browse.d.ts +42 -0
- package/dist/server/sqlite/browse.js +84 -0
- package/dist/server/sqlite/export.d.ts +32 -0
- package/dist/server/sqlite/export.js +42 -0
- package/dist/server/sqlite/fts5.d.ts +106 -0
- package/dist/server/sqlite/fts5.js +154 -0
- package/dist/server/sqlite/health.d.ts +44 -0
- package/dist/server/sqlite/health.js +52 -0
- package/dist/server/sqlite/import.d.ts +36 -0
- package/dist/server/sqlite/import.js +108 -0
- package/dist/server/sqlite/index.d.ts +9 -0
- package/dist/server/sqlite/index.js +15 -0
- package/dist/server/sqlite/readonlySql.d.ts +15 -0
- package/dist/server/sqlite/readonlySql.js +65 -0
- package/dist/server/sqlite/stats.d.ts +22 -0
- package/dist/server/sqlite/stats.js +32 -0
- package/dist/server/sqlite/viewer.d.ts +17 -0
- package/dist/server/sqlite/viewer.js +56 -0
- package/dist/server/storage/binaryServerProcessing.d.ts +23 -0
- package/dist/server/storage/binaryServerProcessing.js +81 -0
- package/dist/server/storage/binaryServerStore.d.ts +3 -0
- package/dist/server/storage/binaryServerStore.js +95 -0
- package/dist/server/storage/diskStore.d.ts +2 -0
- package/dist/server/storage/diskStore.js +88 -0
- package/dist/server/storage/files/adapter.d.ts +33 -0
- package/dist/server/storage/files/adapter.js +203 -0
- package/dist/server/storage/files/catalogue.d.ts +72 -0
- package/dist/server/storage/files/catalogue.js +1033 -0
- package/dist/server/storage/files/chunkedUpload.d.ts +56 -0
- package/dist/server/storage/files/chunkedUpload.js +70 -0
- package/dist/server/storage/files/fileRoutes.d.ts +51 -0
- package/dist/server/storage/files/fileRoutes.js +405 -0
- package/dist/server/storage/files/index.d.ts +9 -0
- package/dist/server/storage/files/index.js +11 -0
- package/dist/server/storage/files/migrations.d.ts +2 -0
- package/dist/server/storage/files/migrations.js +163 -0
- package/dist/server/storage/files/mimeByExt.d.ts +1 -0
- package/dist/server/storage/files/mimeByExt.js +13 -0
- package/dist/server/storage/files/stack.d.ts +136 -0
- package/dist/server/storage/files/stack.js +132 -0
- package/dist/server/storage/files/storageOps.d.ts +54 -0
- package/dist/server/storage/files/storageOps.js +110 -0
- package/dist/server/storage/files/types.d.ts +261 -0
- package/dist/server/storage/files/types.js +22 -0
- package/dist/server/storage/imageThumbnail.d.ts +3 -0
- package/dist/server/storage/imageThumbnail.js +50 -0
- package/dist/server/storage/index.d.ts +15 -0
- package/dist/server/storage/index.js +27 -0
- package/dist/server/storage/mountMediaRoutes.d.ts +28 -0
- package/dist/server/storage/mountMediaRoutes.js +141 -0
- package/dist/server/storage/objectStoreMediaStore.d.ts +4 -0
- package/dist/server/storage/objectStoreMediaStore.js +57 -0
- package/dist/server/storage/s3ObjectStore.d.ts +63 -0
- package/dist/server/storage/s3ObjectStore.js +187 -0
- package/dist/server/storage/sources.d.ts +47 -0
- package/dist/server/storage/sources.js +148 -0
- package/dist/server/storage/streamRange.d.ts +19 -0
- package/dist/server/storage/streamRange.js +69 -0
- package/dist/server/storage/streamToken.d.ts +16 -0
- package/dist/server/storage/streamToken.js +54 -0
- package/dist/server/storage/transcodeJobs.d.ts +65 -0
- package/dist/server/storage/transcodeJobs.js +104 -0
- package/dist/server/storage/types.d.ts +193 -0
- package/dist/server/storage/types.js +41 -0
- package/dist/server/storage/videoDerivativeKeys.d.ts +127 -0
- package/dist/server/storage/videoDerivativeKeys.js +149 -0
- package/dist/server/storage/videoProcessor.d.ts +30 -0
- package/dist/server/storage/videoProcessor.js +148 -0
- package/dist/server/sync/alarm.d.ts +34 -0
- package/dist/server/sync/alarm.js +91 -0
- package/dist/server/sync/commands.d.ts +87 -0
- package/dist/server/sync/commands.js +241 -0
- package/dist/server/sync/engine.d.ts +62 -0
- package/dist/server/sync/engine.js +184 -0
- package/dist/server/sync/http.d.ts +106 -0
- package/dist/server/sync/http.js +243 -0
- package/dist/server/sync/index.d.ts +41 -0
- package/dist/server/sync/index.js +41 -0
- package/dist/server/sync/opLog.d.ts +61 -0
- package/dist/server/sync/opLog.js +96 -0
- package/dist/server/sync/planner.d.ts +31 -0
- package/dist/server/sync/planner.js +30 -0
- package/dist/server/sync/status.d.ts +63 -0
- package/dist/server/sync/status.js +49 -0
- package/dist/server/sync/timer.d.ts +52 -0
- package/dist/server/sync/timer.js +170 -0
- package/dist/server/sync/tokens.d.ts +25 -0
- package/dist/server/sync/tokens.js +51 -0
- package/dist/server/sync/types.d.ts +101 -0
- package/dist/server/sync/types.js +29 -0
- package/dist/server/ws/eventBus.d.ts +14 -0
- package/dist/server/ws/eventBus.js +42 -0
- package/dist/server/ws/hub.d.ts +20 -0
- package/dist/server/ws/hub.js +154 -0
- package/dist/server/ws/index.d.ts +5 -0
- package/dist/server/ws/index.js +7 -0
- package/dist/server/ws/presence.d.ts +11 -0
- package/dist/server/ws/presence.js +20 -0
- package/dist/server/ws/room.d.ts +17 -0
- package/dist/server/ws/room.js +23 -0
- package/dist/server/ws/types.d.ts +122 -0
- package/dist/server/ws/types.js +1 -0
- package/package.json +234 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/leafSubpathsImportNothing.spec.ts +358 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/server/activity/index.ts +59 -0
- package/src/server/activity/migrations.ts +67 -0
- package/src/server/activity/shareSource.ts +57 -0
- package/src/server/activity/store.spec.ts +255 -0
- package/src/server/activity/store.ts +368 -0
- package/src/server/app.spec.ts +243 -0
- package/src/server/app.ts +89 -0
- package/src/server/audit/index.ts +2 -0
- package/src/server/audit/migrations.ts +88 -0
- package/src/server/audit/sqliteSink.test.ts +200 -0
- package/src/server/audit/sqliteSink.ts +162 -0
- package/src/server/auth/cookie.spec.ts +46 -0
- package/src/server/auth/cookie.ts +42 -0
- package/src/server/auth/csrf.spec.ts +81 -0
- package/src/server/auth/csrf.ts +83 -0
- package/src/server/auth/fileTokenSigner.ts +40 -0
- package/src/server/auth/getOnly.spec.ts +53 -0
- package/src/server/auth/getOnly.ts +32 -0
- package/src/server/auth/index.ts +51 -0
- package/src/server/auth/jwt.spec.ts +106 -0
- package/src/server/auth/jwt.ts +75 -0
- package/src/server/auth/loginThrottle.spec.ts +213 -0
- package/src/server/auth/loginThrottle.ts +261 -0
- package/src/server/auth/middleware.spec.ts +148 -0
- package/src/server/auth/middleware.ts +56 -0
- package/src/server/auth/password.spec.ts +48 -0
- package/src/server/auth/password.ts +71 -0
- package/src/server/auth/sessionStore.spec.ts +189 -0
- package/src/server/auth/sessionStore.ts +261 -0
- package/src/server/auth/totp.spec.ts +73 -0
- package/src/server/auth/totp.ts +92 -0
- package/src/server/auth/types.ts +146 -0
- package/src/server/auth/vault.spec.ts +51 -0
- package/src/server/auth/vault.ts +71 -0
- package/src/server/context.ts +37 -0
- package/src/server/ctgr.spec.ts +138 -0
- package/src/server/ctgr.ts +108 -0
- package/src/server/db/kysely.spec.ts +62 -0
- package/src/server/db/kysely.ts +21 -0
- package/src/server/db/plumbingTypes.ts +126 -0
- package/src/server/dropzone/duplicates.ts +121 -0
- package/src/server/dropzone/index.ts +30 -0
- package/src/server/dropzone/scan.spec.ts +255 -0
- package/src/server/dropzone/scan.ts +342 -0
- package/src/server/errors.spec.ts +76 -0
- package/src/server/errors.ts +22 -0
- package/src/server/guard/argon2Binding.spec.ts +117 -0
- package/src/server/guard/argon2Binding.ts +195 -0
- package/src/server/guard/argon2Kat.spec.ts +250 -0
- package/src/server/guard/guard.spec.ts +390 -0
- package/src/server/guard/index.ts +6 -0
- package/src/server/guard/kdfWorker.spec.ts +283 -0
- package/src/server/guard/kdfWorker.ts +367 -0
- package/src/server/guard/kdfWorkerEntry.ts +16 -0
- package/src/server/guard/passwordVerifier.spec.ts +71 -0
- package/src/server/guard/passwordVerifier.ts +175 -0
- package/src/server/guard/revocationStore.spec.ts +135 -0
- package/src/server/guard/revocationStore.ts +291 -0
- package/src/server/guard/sessionSigner.ts +425 -0
- package/src/server/guard/verifier.ts +27 -0
- package/src/server/incidents/channels.ts +487 -0
- package/src/server/incidents/checks.ts +120 -0
- package/src/server/incidents/incidents.spec.ts +463 -0
- package/src/server/incidents/index.ts +96 -0
- package/src/server/incidents/migrations.ts +66 -0
- package/src/server/incidents/notifier.ts +215 -0
- package/src/server/incidents/reporter.ts +110 -0
- package/src/server/incidents/routes.ts +314 -0
- package/src/server/incidents/store.ts +329 -0
- package/src/server/incidents/types.ts +116 -0
- package/src/server/index.ts +248 -0
- package/src/server/jobs/fireQueue.spec.ts +85 -0
- package/src/server/jobs/fireQueue.ts +37 -0
- package/src/server/jobs/index.ts +26 -0
- package/src/server/jobs/logger.ts +16 -0
- package/src/server/jobs/plainjobAdapter.spec.ts +34 -0
- package/src/server/jobs/plainjobAdapter.ts +54 -0
- package/src/server/jobs/polling.spec.ts +219 -0
- package/src/server/jobs/polling.ts +81 -0
- package/src/server/jobs/runner.spec.ts +213 -0
- package/src/server/jobs/runner.ts +213 -0
- package/src/server/jobs/timeout.spec.ts +35 -0
- package/src/server/jobs/timeout.ts +53 -0
- package/src/server/jobs/types.ts +188 -0
- package/src/server/master-lock/guard.spec.ts +347 -0
- package/src/server/master-lock/guard.ts +290 -0
- package/src/server/master-lock/index.ts +44 -0
- package/src/server/master-lock/lockPage.spec.ts +88 -0
- package/src/server/master-lock/lockPage.ts +313 -0
- package/src/server/master-lock/masterLock.spec.ts +338 -0
- package/src/server/master-lock/masterLock.ts +627 -0
- package/src/server/master-lock/principals.spec.ts +378 -0
- package/src/server/master-lock/principals.ts +175 -0
- package/src/server/master-lock/seed.spec.ts +69 -0
- package/src/server/master-lock/seed.ts +112 -0
- package/src/server/master-lock/store.ts +35 -0
- package/src/server/media-bun/image.spec.ts +115 -0
- package/src/server/media-bun/image.ts +134 -0
- package/src/server/media-bun/index.ts +52 -0
- package/src/server/media-bun/streaming.spec.ts +134 -0
- package/src/server/media-bun/streaming.ts +347 -0
- package/src/server/media-bun/video.spec.ts +115 -0
- package/src/server/media-bun/video.ts +232 -0
- package/src/server/metrics/metricsBuffer.ts +92 -0
- package/src/server/metrics/normalizeRoute.spec.ts +51 -0
- package/src/server/metrics/normalizeRoute.ts +63 -0
- package/src/server/middleware/correlationId.ts +24 -0
- package/src/server/middleware/corsAllowlist.spec.ts +118 -0
- package/src/server/middleware/corsAllowlist.ts +69 -0
- package/src/server/middleware/errorEnvelope.ts +38 -0
- package/src/server/middleware/index.ts +19 -0
- package/src/server/middleware/localOrigin.spec.ts +168 -0
- package/src/server/middleware/localOrigin.ts +139 -0
- package/src/server/middleware/rateLimit.spec.ts +72 -0
- package/src/server/middleware/rateLimit.ts +125 -0
- package/src/server/middleware/requestLogger.ts +95 -0
- package/src/server/middleware/securityHeaders.ts +78 -0
- package/src/server/middleware/zValidator.spec.ts +251 -0
- package/src/server/middleware/zValidator.ts +114 -0
- package/src/server/migration/runner.spec.ts +88 -0
- package/src/server/migration/runner.ts +87 -0
- package/src/server/migration/types.ts +17 -0
- package/src/server/net/outboundGuard.spec.ts +90 -0
- package/src/server/net/outboundGuard.ts +205 -0
- package/src/server/notifications/hub.ts +245 -0
- package/src/server/notifications/index.ts +77 -0
- package/src/server/notifications/migrations.ts +39 -0
- package/src/server/notifications/routes.ts +177 -0
- package/src/server/notifications/service.spec.ts +305 -0
- package/src/server/notifications/service.ts +112 -0
- package/src/server/paths/appDataHome.spec.ts +87 -0
- package/src/server/paths/appDataHome.ts +84 -0
- package/src/server/private-media/blobRoute.spec.ts +165 -0
- package/src/server/private-media/crypto.spec.ts +81 -0
- package/src/server/private-media/crypto.ts +155 -0
- package/src/server/private-media/gatePolicy.spec.ts +98 -0
- package/src/server/private-media/index-store.ts +83 -0
- package/src/server/private-media/mountPrivateRoutes.ts +265 -0
- package/src/server/private-media/pipeline.spec.ts +111 -0
- package/src/server/private-media/pipeline.ts +170 -0
- package/src/server/private-media/routes.spec.ts +265 -0
- package/src/server/private-media/unlock.ts +112 -0
- package/src/server/resume/index.ts +8 -0
- package/src/server/resume/resume.spec.ts +118 -0
- package/src/server/resume/routes.ts +92 -0
- package/src/server/resume/store.ts +82 -0
- package/src/server/retention/archive.spec.ts +181 -0
- package/src/server/retention/archive.ts +353 -0
- package/src/server/retention/fleet.ts +104 -0
- package/src/server/retention/index.ts +101 -0
- package/src/server/retention/policy.spec.ts +132 -0
- package/src/server/retention/policy.ts +392 -0
- package/src/server/retention/store.spec.ts +236 -0
- package/src/server/retention/store.ts +338 -0
- package/src/server/retention/sweeper.spec.ts +321 -0
- package/src/server/retention/sweeper.ts +367 -0
- package/src/server/retention/target.spec.ts +592 -0
- package/src/server/retention/target.ts +613 -0
- package/src/server/search/index.ts +12 -0
- package/src/server/search/pgSearchProvider.pg.test.ts +392 -0
- package/src/server/search/pgSearchProvider.ts +421 -0
- package/src/server/search/pgSearchSchema.ts +147 -0
- package/src/server/sharing/index.ts +65 -0
- package/src/server/sharing/migrations.ts +128 -0
- package/src/server/sharing/router.spec.ts +167 -0
- package/src/server/sharing/router.ts +194 -0
- package/src/server/sharing/sql.ts +109 -0
- package/src/server/sharing/store.spec.ts +408 -0
- package/src/server/sharing/store.ts +737 -0
- package/src/server/sqlite/backup.spec.ts +114 -0
- package/src/server/sqlite/backup.ts +154 -0
- package/src/server/sqlite/browse.spec.ts +70 -0
- package/src/server/sqlite/browse.ts +120 -0
- package/src/server/sqlite/export.spec.ts +43 -0
- package/src/server/sqlite/export.ts +74 -0
- package/src/server/sqlite/fts5.spec.ts +175 -0
- package/src/server/sqlite/fts5.ts +255 -0
- package/src/server/sqlite/health.spec.ts +79 -0
- package/src/server/sqlite/health.ts +105 -0
- package/src/server/sqlite/import.spec.ts +75 -0
- package/src/server/sqlite/import.ts +141 -0
- package/src/server/sqlite/index.ts +64 -0
- package/src/server/sqlite/readonlySql.spec.ts +58 -0
- package/src/server/sqlite/readonlySql.ts +67 -0
- package/src/server/sqlite/stats.spec.ts +21 -0
- package/src/server/sqlite/stats.ts +61 -0
- package/src/server/sqlite/viewer.spec.ts +127 -0
- package/src/server/sqlite/viewer.ts +72 -0
- package/src/server/storage/binaryServerProcessing.integration.spec.ts +222 -0
- package/src/server/storage/binaryServerProcessing.spec.ts +150 -0
- package/src/server/storage/binaryServerProcessing.ts +127 -0
- package/src/server/storage/binaryServerStore.ts +102 -0
- package/src/server/storage/chunked.integration.spec.ts +185 -0
- package/src/server/storage/diskStore.ts +83 -0
- package/src/server/storage/files/adapter.photo.display.spec.ts +169 -0
- package/src/server/storage/files/adapter.photo.spec.ts +128 -0
- package/src/server/storage/files/adapter.spec.ts +121 -0
- package/src/server/storage/files/adapter.ts +255 -0
- package/src/server/storage/files/catalogue.media-host.spec.ts +184 -0
- package/src/server/storage/files/catalogue.orderings.spec.ts +157 -0
- package/src/server/storage/files/catalogue.private.spec.ts +106 -0
- package/src/server/storage/files/catalogue.recover.spec.ts +131 -0
- package/src/server/storage/files/catalogue.sort.spec.ts +154 -0
- package/src/server/storage/files/catalogue.spec.ts +133 -0
- package/src/server/storage/files/catalogue.trash-import.spec.ts +125 -0
- package/src/server/storage/files/catalogue.ts +1326 -0
- package/src/server/storage/files/chunkedUpload.spec.ts +111 -0
- package/src/server/storage/files/chunkedUpload.ts +146 -0
- package/src/server/storage/files/collections.spec.ts +294 -0
- package/src/server/storage/files/fileRoutes.replace.spec.ts +88 -0
- package/src/server/storage/files/fileRoutes.spec.ts +327 -0
- package/src/server/storage/files/fileRoutes.ts +462 -0
- package/src/server/storage/files/index.ts +66 -0
- package/src/server/storage/files/migrations.ts +187 -0
- package/src/server/storage/files/mimeByExt.ts +14 -0
- package/src/server/storage/files/stack.spec.ts +258 -0
- package/src/server/storage/files/stack.ts +284 -0
- package/src/server/storage/files/storageOps.spec.ts +126 -0
- package/src/server/storage/files/storageOps.ts +213 -0
- package/src/server/storage/files/types.ts +285 -0
- package/src/server/storage/imageThumbnail.spec.ts +81 -0
- package/src/server/storage/imageThumbnail.ts +60 -0
- package/src/server/storage/index.ts +122 -0
- package/src/server/storage/mountMediaRoutes.ts +231 -0
- package/src/server/storage/objectStore.spec.ts +272 -0
- package/src/server/storage/objectStoreMediaStore.ts +63 -0
- package/src/server/storage/s3ObjectStore.ts +239 -0
- package/src/server/storage/sources.ts +245 -0
- package/src/server/storage/storage.spec.ts +219 -0
- package/src/server/storage/streamRange.ts +92 -0
- package/src/server/storage/streamToken.ts +76 -0
- package/src/server/storage/transcode.integration.spec.ts +134 -0
- package/src/server/storage/transcodeJobs.spec.ts +152 -0
- package/src/server/storage/transcodeJobs.ts +171 -0
- package/src/server/storage/types.ts +230 -0
- package/src/server/storage/videoDerivativeKeys.spec.ts +112 -0
- package/src/server/storage/videoDerivativeKeys.ts +172 -0
- package/src/server/storage/videoProcessor.posterOnly.spec.ts +106 -0
- package/src/server/storage/videoProcessor.ts +229 -0
- package/src/server/sync/alarm.spec.ts +149 -0
- package/src/server/sync/alarm.ts +137 -0
- package/src/server/sync/commands.spec.ts +145 -0
- package/src/server/sync/commands.ts +361 -0
- package/src/server/sync/engine.spec.ts +496 -0
- package/src/server/sync/engine.ts +255 -0
- package/src/server/sync/http.ts +316 -0
- package/src/server/sync/httpRemote.spec.ts +158 -0
- package/src/server/sync/index.ts +90 -0
- package/src/server/sync/opLog.spec.ts +110 -0
- package/src/server/sync/opLog.ts +189 -0
- package/src/server/sync/planner.spec.ts +53 -0
- package/src/server/sync/planner.ts +62 -0
- package/src/server/sync/status.spec.ts +92 -0
- package/src/server/sync/status.ts +108 -0
- package/src/server/sync/timer.spec.ts +150 -0
- package/src/server/sync/timer.ts +207 -0
- package/src/server/sync/tokens.spec.ts +38 -0
- package/src/server/sync/tokens.ts +94 -0
- package/src/server/sync/types.ts +108 -0
- package/src/server/ws/eventBus.spec.ts +67 -0
- package/src/server/ws/eventBus.ts +46 -0
- package/src/server/ws/hub.integration.spec.ts +93 -0
- package/src/server/ws/hub.spec.ts +282 -0
- package/src/server/ws/hub.ts +168 -0
- package/src/server/ws/index.ts +20 -0
- package/src/server/ws/presence.spec.ts +27 -0
- package/src/server/ws/presence.ts +26 -0
- package/src/server/ws/room.spec.ts +36 -0
- package/src/server/ws/room.ts +29 -0
- package/src/server/ws/types.ts +134 -0
- package/src/skippedTestsAreAnswered.spec.ts +215 -0
- 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
|
+
];
|