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,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store: grants, groups, revocation and the audit trail, over `bun:sqlite`.
|
|
3
|
+
*
|
|
4
|
+
* ── How an app uses it ──────────────────────────────────────────────────────
|
|
5
|
+
* ```ts
|
|
6
|
+
* const shares = createShareStore({ db, app: "family", onEvent: notify });
|
|
7
|
+
*
|
|
8
|
+
* shares.grant({ kind: "person", id, subject: user("aunt@example.com"),
|
|
9
|
+
* level: "read", grantedBy: viewer.email });
|
|
10
|
+
* shares.accessFor({ kind: "person", id }, { subjectId: viewer.email }); // "read"
|
|
11
|
+
* shares.revoke({ kind: "person", id, subject: user("aunt@example.com"),
|
|
12
|
+
* revokedBy: viewer.email });
|
|
13
|
+
* ```
|
|
14
|
+
* …and inside the app's own queries, `sharedResourceSql` (see `./sql.ts`) is the
|
|
15
|
+
* WHERE clause, so a filtered row never surfaces as a count either.
|
|
16
|
+
*
|
|
17
|
+
* ── 🔴 Five properties this file exists to guarantee ────────────────────────
|
|
18
|
+
* 1. **Every mutation writes its own audit row, in the same transaction.** There
|
|
19
|
+
* is no `recordEvent` in the public API and no way to grant without logging.
|
|
20
|
+
* An audit trail a caller has to remember to write is a trail with holes
|
|
21
|
+
* exactly where somebody was in a hurry.
|
|
22
|
+
* 2. **A revoked grant's history survives it.** Revocation deletes the grant row
|
|
23
|
+
* and appends an event; nothing here deletes an event, ever.
|
|
24
|
+
* 3. **Deleting the RESOURCE is a revocation, not an orphan.** `revokeResource`
|
|
25
|
+
* exists so an app deleting a row has one call to make. A grant left behind
|
|
26
|
+
* for a deleted id silently becomes access to whatever later reuses that id.
|
|
27
|
+
* 4. **The notification hook cannot break a write.** `onEvent` runs AFTER the
|
|
28
|
+
* transaction commits and its throw is swallowed with a warning: a mail server
|
|
29
|
+
* being down must not lose the grant the owner just made.
|
|
30
|
+
* 5. **A subject is normalized once, here.** Callers pass whatever the form gave
|
|
31
|
+
* them; `normalizeSubjectId` is applied on the way in AND on the way to a
|
|
32
|
+
* lookup, so a grant made to `" Ameena@Example.com "` is found by the session
|
|
33
|
+
* that signs in as `ameena@example.com`.
|
|
34
|
+
*/
|
|
35
|
+
import type { Database } from "bun:sqlite";
|
|
36
|
+
import {
|
|
37
|
+
type ShareAccess,
|
|
38
|
+
type ShareAction,
|
|
39
|
+
type ShareEvent,
|
|
40
|
+
type ShareGrant,
|
|
41
|
+
type ShareGroup,
|
|
42
|
+
type ShareGroupMember,
|
|
43
|
+
type ShareLevel,
|
|
44
|
+
type ShareResource,
|
|
45
|
+
type ShareSubject,
|
|
46
|
+
type ShareViewer,
|
|
47
|
+
levelsMeeting,
|
|
48
|
+
normalizeSubjectId,
|
|
49
|
+
strongerAccess,
|
|
50
|
+
} from "cursedbelt-core/sharing/model";
|
|
51
|
+
import { runMigrations } from "../migration/runner";
|
|
52
|
+
import {
|
|
53
|
+
SHARE_EVENTS_TABLE,
|
|
54
|
+
SHARE_GRANTS_TABLE,
|
|
55
|
+
SHARE_GROUPS_TABLE,
|
|
56
|
+
SHARE_GROUP_MEMBERS_TABLE,
|
|
57
|
+
SHARE_MIGRATIONS,
|
|
58
|
+
} from "./migrations";
|
|
59
|
+
import { sharedResourceSql } from "./sql";
|
|
60
|
+
|
|
61
|
+
/** Shorthand for the common subject. `group(id)` is its sibling. */
|
|
62
|
+
export const user = (id: string): ShareSubject => ({ type: "user", id });
|
|
63
|
+
export const group = (id: string): ShareSubject => ({ type: "group", id });
|
|
64
|
+
|
|
65
|
+
/** A resource within the store's own app — the `app` half is supplied by the
|
|
66
|
+
* store, so a caller cannot mistakenly write a grant into another app's rows. */
|
|
67
|
+
export interface ShareTarget {
|
|
68
|
+
kind: string;
|
|
69
|
+
id: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ShareStoreOptions {
|
|
73
|
+
db: Database;
|
|
74
|
+
/** The satellite this store speaks for. Fixed at construction so `app` cannot
|
|
75
|
+
* be got wrong per call — the commonest way a grant becomes invisible. */
|
|
76
|
+
app: string;
|
|
77
|
+
/**
|
|
78
|
+
* Notified after every committed mutation — the notification hook ca's model
|
|
79
|
+
* had, as a seam rather than a dependency.
|
|
80
|
+
*
|
|
81
|
+
* 🔴 It stays a seam now that the other half EXISTS. `cursedbelt/server/
|
|
82
|
+
* notifications` landed the same day this did, so a consumer can wire the two
|
|
83
|
+
* in one line — `onEvent: (e) => notifications.notify({ userId: e.subject.id,
|
|
84
|
+
* … })` — and `describeShareEvent` in the vocabulary is the wording both the
|
|
85
|
+
* panel and that notification use. What must NOT happen is this store
|
|
86
|
+
* importing that one: an app is entitled to a sharing model with no bell (the
|
|
87
|
+
* grant is still audited), the subject of a GROUP grant is a group id and not
|
|
88
|
+
* a person to notify, and an app knows which of its users to address where
|
|
89
|
+
* this primitive only knows a subject id.
|
|
90
|
+
*/
|
|
91
|
+
onEvent?: (event: ShareEvent) => void;
|
|
92
|
+
/** Injectable clock, for tests that assert ordering. ISO-8601 strings. */
|
|
93
|
+
now?: () => string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface GrantRow {
|
|
97
|
+
app: string;
|
|
98
|
+
kind: string;
|
|
99
|
+
resource_id: string;
|
|
100
|
+
subject_type: "user" | "group";
|
|
101
|
+
subject_id: string;
|
|
102
|
+
level: ShareLevel;
|
|
103
|
+
granted_by: string;
|
|
104
|
+
granted_at: string;
|
|
105
|
+
note: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface EventRow {
|
|
109
|
+
event_id: number;
|
|
110
|
+
at: string;
|
|
111
|
+
action: ShareAction;
|
|
112
|
+
app: string | null;
|
|
113
|
+
kind: string | null;
|
|
114
|
+
resource_id: string | null;
|
|
115
|
+
subject_type: "user" | "group";
|
|
116
|
+
subject_id: string;
|
|
117
|
+
level: ShareLevel | null;
|
|
118
|
+
previous_level: ShareLevel | null;
|
|
119
|
+
actor: string;
|
|
120
|
+
note: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const toGrant = (row: GrantRow): ShareGrant => ({
|
|
124
|
+
resource: { app: row.app, kind: row.kind, id: row.resource_id },
|
|
125
|
+
subject: { type: row.subject_type, id: row.subject_id },
|
|
126
|
+
level: row.level,
|
|
127
|
+
grantedBy: row.granted_by,
|
|
128
|
+
grantedAt: row.granted_at,
|
|
129
|
+
note: row.note,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const toEvent = (row: EventRow): ShareEvent => ({
|
|
133
|
+
id: row.event_id,
|
|
134
|
+
at: row.at,
|
|
135
|
+
action: row.action,
|
|
136
|
+
resource:
|
|
137
|
+
row.app === null || row.kind === null || row.resource_id === null
|
|
138
|
+
? null
|
|
139
|
+
: { app: row.app, kind: row.kind, id: row.resource_id },
|
|
140
|
+
subject: { type: row.subject_type, id: row.subject_id },
|
|
141
|
+
level: row.level,
|
|
142
|
+
previousLevel: row.previous_level,
|
|
143
|
+
actor: row.actor,
|
|
144
|
+
note: row.note,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export interface GrantInput extends ShareTarget {
|
|
148
|
+
subject: ShareSubject;
|
|
149
|
+
level: ShareLevel;
|
|
150
|
+
grantedBy: string;
|
|
151
|
+
note?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RevokeInput extends ShareTarget {
|
|
155
|
+
subject: ShareSubject;
|
|
156
|
+
revokedBy: string;
|
|
157
|
+
note?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ShareEventQuery {
|
|
161
|
+
kind?: string;
|
|
162
|
+
id?: string;
|
|
163
|
+
subject?: ShareSubject;
|
|
164
|
+
/** Newest first, capped. Default 200 — a panel shows a page, and an unbounded
|
|
165
|
+
* audit read on a busy resource is the query that times out in production. */
|
|
166
|
+
limit?: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ShareStore = ReturnType<typeof createShareStore>;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Open (and migrate) the sharing tables in this database, returning the store.
|
|
173
|
+
*
|
|
174
|
+
* Idempotent: safe to call once per boot per `db`, and safe to call in a database
|
|
175
|
+
* that already has the tables (the migration runner records what it applied).
|
|
176
|
+
*/
|
|
177
|
+
export function createShareStore(options: ShareStoreOptions) {
|
|
178
|
+
const { db, app } = options;
|
|
179
|
+
const clock = options.now ?? (() => new Date().toISOString());
|
|
180
|
+
if (!app.trim()) throw new Error("[cursedbelt-core/sharing] a share store needs an `app`");
|
|
181
|
+
runMigrations(db, SHARE_MIGRATIONS);
|
|
182
|
+
|
|
183
|
+
/** Collected inside a transaction, delivered after it commits — see property 4
|
|
184
|
+
* in the file header. */
|
|
185
|
+
const emit = (events: readonly ShareEvent[]): void => {
|
|
186
|
+
const hook = options.onEvent;
|
|
187
|
+
if (!hook) return;
|
|
188
|
+
for (const event of events) {
|
|
189
|
+
try {
|
|
190
|
+
hook(event);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn(`[cursedbelt-core/sharing] a share-event listener threw: ${String(error)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const recordEvent = (input: {
|
|
198
|
+
action: ShareAction;
|
|
199
|
+
resource: ShareResource | null;
|
|
200
|
+
subject: ShareSubject;
|
|
201
|
+
level: ShareLevel | null;
|
|
202
|
+
previousLevel?: ShareLevel | null;
|
|
203
|
+
actor: string;
|
|
204
|
+
note?: string;
|
|
205
|
+
at: string;
|
|
206
|
+
}): ShareEvent => {
|
|
207
|
+
db.query(
|
|
208
|
+
`INSERT INTO ${SHARE_EVENTS_TABLE}
|
|
209
|
+
(at, action, app, kind, resource_id, subject_type, subject_id, level, previous_level, actor, note)
|
|
210
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
211
|
+
).run(
|
|
212
|
+
input.at,
|
|
213
|
+
input.action,
|
|
214
|
+
input.resource?.app ?? null,
|
|
215
|
+
input.resource?.kind ?? null,
|
|
216
|
+
input.resource?.id ?? null,
|
|
217
|
+
input.subject.type,
|
|
218
|
+
input.subject.id,
|
|
219
|
+
input.level,
|
|
220
|
+
input.previousLevel ?? null,
|
|
221
|
+
input.actor,
|
|
222
|
+
input.note ?? "",
|
|
223
|
+
);
|
|
224
|
+
const id = (db.query("SELECT last_insert_rowid() AS id").get() as { id: number }).id;
|
|
225
|
+
return {
|
|
226
|
+
id,
|
|
227
|
+
at: input.at,
|
|
228
|
+
action: input.action,
|
|
229
|
+
resource: input.resource,
|
|
230
|
+
subject: input.subject,
|
|
231
|
+
level: input.level,
|
|
232
|
+
previousLevel: input.previousLevel ?? null,
|
|
233
|
+
actor: input.actor,
|
|
234
|
+
note: input.note ?? "",
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const normalized = (subject: ShareSubject): ShareSubject => ({
|
|
239
|
+
type: subject.type,
|
|
240
|
+
id: normalizeSubjectId(subject.type, subject.id),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const readGrant = (target: ShareTarget, subject: ShareSubject): ShareGrant | null => {
|
|
244
|
+
const row = db
|
|
245
|
+
.query(
|
|
246
|
+
`SELECT * FROM ${SHARE_GRANTS_TABLE}
|
|
247
|
+
WHERE app = ? AND kind = ? AND resource_id = ? AND subject_type = ? AND subject_id = ?`,
|
|
248
|
+
)
|
|
249
|
+
.get(app, target.kind, target.id, subject.type, subject.id) as GrantRow | null;
|
|
250
|
+
return row ? toGrant(row) : null;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
/** The app this store speaks for — exposed so a consumer can build its own
|
|
255
|
+
* `sharedResourceSql` call without re-declaring the name. */
|
|
256
|
+
app,
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Give `subject` access to a resource, or change the access they have.
|
|
260
|
+
*
|
|
261
|
+
* An unchanged re-grant is still recorded as a `grant` event: "the owner
|
|
262
|
+
* confirmed this on Tuesday" is a fact somebody reading the trail wants, and
|
|
263
|
+
* suppressing it would make an idempotent API silently unauditable.
|
|
264
|
+
*/
|
|
265
|
+
grant(input: GrantInput): ShareGrant {
|
|
266
|
+
const subject = normalized(input.subject);
|
|
267
|
+
if (!subject.id) throw new Error("[cursedbelt-core/sharing] a grant needs a subject");
|
|
268
|
+
if (!input.id) throw new Error("[cursedbelt-core/sharing] a grant needs a resource id");
|
|
269
|
+
const at = clock();
|
|
270
|
+
const events: ShareEvent[] = [];
|
|
271
|
+
const grant = db.transaction((): ShareGrant => {
|
|
272
|
+
const existing = readGrant(input, subject);
|
|
273
|
+
db.query(
|
|
274
|
+
`INSERT INTO ${SHARE_GRANTS_TABLE}
|
|
275
|
+
(app, kind, resource_id, subject_type, subject_id, level, granted_by, granted_at, note)
|
|
276
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
277
|
+
ON CONFLICT (app, kind, resource_id, subject_type, subject_id)
|
|
278
|
+
DO UPDATE SET level = excluded.level, granted_by = excluded.granted_by,
|
|
279
|
+
granted_at = excluded.granted_at, note = excluded.note`,
|
|
280
|
+
).run(
|
|
281
|
+
app,
|
|
282
|
+
input.kind,
|
|
283
|
+
input.id,
|
|
284
|
+
subject.type,
|
|
285
|
+
subject.id,
|
|
286
|
+
input.level,
|
|
287
|
+
input.grantedBy,
|
|
288
|
+
at,
|
|
289
|
+
input.note ?? "",
|
|
290
|
+
);
|
|
291
|
+
const resource: ShareResource = { app, kind: input.kind, id: input.id };
|
|
292
|
+
events.push(
|
|
293
|
+
recordEvent({
|
|
294
|
+
action: existing && existing.level !== input.level ? "regrade" : "grant",
|
|
295
|
+
resource,
|
|
296
|
+
subject,
|
|
297
|
+
level: input.level,
|
|
298
|
+
previousLevel: existing && existing.level !== input.level ? existing.level : null,
|
|
299
|
+
actor: input.grantedBy,
|
|
300
|
+
note: input.note,
|
|
301
|
+
at,
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
return {
|
|
305
|
+
resource,
|
|
306
|
+
subject,
|
|
307
|
+
level: input.level,
|
|
308
|
+
grantedBy: input.grantedBy,
|
|
309
|
+
grantedAt: at,
|
|
310
|
+
note: input.note ?? "",
|
|
311
|
+
};
|
|
312
|
+
})();
|
|
313
|
+
emit(events);
|
|
314
|
+
return grant;
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
/** Take access away. Returns the level that was removed, or null when there
|
|
318
|
+
* was nothing to remove — in which case nothing is logged, because an audit
|
|
319
|
+
* trail full of "revoked nothing" is one nobody reads. */
|
|
320
|
+
revoke(input: RevokeInput): ShareLevel | null {
|
|
321
|
+
const subject = normalized(input.subject);
|
|
322
|
+
const at = clock();
|
|
323
|
+
const events: ShareEvent[] = [];
|
|
324
|
+
const removed = db.transaction((): ShareLevel | null => {
|
|
325
|
+
const existing = readGrant(input, subject);
|
|
326
|
+
if (!existing) return null;
|
|
327
|
+
db.query(
|
|
328
|
+
`DELETE FROM ${SHARE_GRANTS_TABLE}
|
|
329
|
+
WHERE app = ? AND kind = ? AND resource_id = ? AND subject_type = ? AND subject_id = ?`,
|
|
330
|
+
).run(app, input.kind, input.id, subject.type, subject.id);
|
|
331
|
+
events.push(
|
|
332
|
+
recordEvent({
|
|
333
|
+
action: "revoke",
|
|
334
|
+
resource: { app, kind: input.kind, id: input.id },
|
|
335
|
+
subject,
|
|
336
|
+
level: existing.level,
|
|
337
|
+
actor: input.revokedBy,
|
|
338
|
+
note: input.note,
|
|
339
|
+
at,
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
return existing.level;
|
|
343
|
+
})();
|
|
344
|
+
emit(events);
|
|
345
|
+
return removed;
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Revoke EVERY grant on one resource — what an app calls when it deletes the
|
|
350
|
+
* record. Returns how many were removed.
|
|
351
|
+
*
|
|
352
|
+
* 🔴 Not optional housekeeping. Resource ids are app-minted, and a grant left
|
|
353
|
+
* behind for a deleted id becomes access to whatever is created with that id
|
|
354
|
+
* later. `apps/family` calls this in the same transaction as deleting a
|
|
355
|
+
* person.
|
|
356
|
+
*/
|
|
357
|
+
revokeResource(target: ShareTarget, revokedBy: string): number {
|
|
358
|
+
const at = clock();
|
|
359
|
+
const events: ShareEvent[] = [];
|
|
360
|
+
const count = db.transaction((): number => {
|
|
361
|
+
const rows = db
|
|
362
|
+
.query(`SELECT * FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND kind = ? AND resource_id = ?`)
|
|
363
|
+
.all(app, target.kind, target.id) as GrantRow[];
|
|
364
|
+
if (rows.length === 0) return 0;
|
|
365
|
+
db.query(
|
|
366
|
+
`DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND kind = ? AND resource_id = ?`,
|
|
367
|
+
).run(app, target.kind, target.id);
|
|
368
|
+
for (const row of rows) {
|
|
369
|
+
events.push(
|
|
370
|
+
recordEvent({
|
|
371
|
+
action: "revoke",
|
|
372
|
+
resource: { app, kind: row.kind, id: row.resource_id },
|
|
373
|
+
subject: { type: row.subject_type, id: row.subject_id },
|
|
374
|
+
level: row.level,
|
|
375
|
+
actor: revokedBy,
|
|
376
|
+
note: "the record was deleted",
|
|
377
|
+
at,
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return rows.length;
|
|
382
|
+
})();
|
|
383
|
+
emit(events);
|
|
384
|
+
return count;
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
/** Revoke everything shared with one subject, across every kind — the
|
|
388
|
+
* offboarding call. A person losing their fleet account keeps nothing. */
|
|
389
|
+
revokeSubject(subject: ShareSubject, revokedBy: string): number {
|
|
390
|
+
const normal = normalized(subject);
|
|
391
|
+
const at = clock();
|
|
392
|
+
const events: ShareEvent[] = [];
|
|
393
|
+
const count = db.transaction((): number => {
|
|
394
|
+
const rows = db
|
|
395
|
+
.query(
|
|
396
|
+
`SELECT * FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = ? AND subject_id = ?`,
|
|
397
|
+
)
|
|
398
|
+
.all(app, normal.type, normal.id) as GrantRow[];
|
|
399
|
+
if (rows.length === 0) return 0;
|
|
400
|
+
db.query(
|
|
401
|
+
`DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = ? AND subject_id = ?`,
|
|
402
|
+
).run(app, normal.type, normal.id);
|
|
403
|
+
for (const row of rows) {
|
|
404
|
+
events.push(
|
|
405
|
+
recordEvent({
|
|
406
|
+
action: "revoke",
|
|
407
|
+
resource: { app, kind: row.kind, id: row.resource_id },
|
|
408
|
+
subject: normal,
|
|
409
|
+
level: row.level,
|
|
410
|
+
actor: revokedBy,
|
|
411
|
+
note: "every share with this subject was revoked",
|
|
412
|
+
at,
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return rows.length;
|
|
417
|
+
})();
|
|
418
|
+
emit(events);
|
|
419
|
+
return count;
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/** Who can see this record, strongest first then oldest — the panel's read. */
|
|
423
|
+
listForResource(target: ShareTarget): ShareGrant[] {
|
|
424
|
+
const rows = db
|
|
425
|
+
.query(
|
|
426
|
+
`SELECT * FROM ${SHARE_GRANTS_TABLE}
|
|
427
|
+
WHERE app = ? AND kind = ? AND resource_id = ?
|
|
428
|
+
-- Strongest first, then oldest. Spelled as a CASE rather than an
|
|
429
|
+
-- ORDER BY on level, which happens to work only because 'write'
|
|
430
|
+
-- sorts after 'read' and would silently mis-order a third level.
|
|
431
|
+
ORDER BY CASE level WHEN 'write' THEN 0 ELSE 1 END, granted_at ASC`,
|
|
432
|
+
)
|
|
433
|
+
.all(app, target.kind, target.id) as GrantRow[];
|
|
434
|
+
return rows.map(toGrant);
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
/** Everything shared with one subject — "shared with you". Group grants are
|
|
438
|
+
* NOT expanded here; use {@link resourceIdsFor} for the resolved answer. */
|
|
439
|
+
listForSubject(subject: ShareSubject): ShareGrant[] {
|
|
440
|
+
const normal = normalized(subject);
|
|
441
|
+
const rows = db
|
|
442
|
+
.query(
|
|
443
|
+
`SELECT * FROM ${SHARE_GRANTS_TABLE}
|
|
444
|
+
WHERE app = ? AND subject_type = ? AND subject_id = ?
|
|
445
|
+
ORDER BY granted_at DESC`,
|
|
446
|
+
)
|
|
447
|
+
.all(app, normal.type, normal.id) as GrantRow[];
|
|
448
|
+
return rows.map(toGrant);
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* What may this viewer do with this resource — the resolver.
|
|
453
|
+
*
|
|
454
|
+
* The strongest of their own grant and every group they are in, computed in
|
|
455
|
+
* SQL. `resolveAccess` in `cursedbelt-core/sharing` is the same rule written as a
|
|
456
|
+
* pure function, and `store.spec.ts` checks the two agree.
|
|
457
|
+
*/
|
|
458
|
+
accessFor(target: ShareTarget, viewer: ShareViewer): ShareAccess {
|
|
459
|
+
const subjectId = normalizeSubjectId("user", viewer.subjectId);
|
|
460
|
+
if (!subjectId) return "none";
|
|
461
|
+
const rows = db
|
|
462
|
+
.query(
|
|
463
|
+
`SELECT level FROM ${SHARE_GRANTS_TABLE}
|
|
464
|
+
WHERE app = ? AND kind = ? AND resource_id = ?
|
|
465
|
+
AND ( (subject_type = 'user' AND subject_id = ?)
|
|
466
|
+
OR (subject_type = 'group' AND subject_id IN
|
|
467
|
+
(SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ?)) )`,
|
|
468
|
+
)
|
|
469
|
+
.all(app, target.kind, target.id, subjectId, subjectId) as { level: ShareLevel }[];
|
|
470
|
+
let access: ShareAccess = "none";
|
|
471
|
+
for (const row of rows) access = strongerAccess(access, row.level);
|
|
472
|
+
return access;
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
/** Every resource of `kind` this viewer reaches at `atLeast` or above.
|
|
476
|
+
* For filtering rows in the app's OWN query, prefer `sharedResourceSql`. */
|
|
477
|
+
resourceIdsFor(kind: string, viewer: ShareViewer, atLeast: ShareLevel = "read"): string[] {
|
|
478
|
+
const subjectId = normalizeSubjectId("user", viewer.subjectId);
|
|
479
|
+
if (!subjectId) return [];
|
|
480
|
+
const levels = levelsMeeting(atLeast);
|
|
481
|
+
const rows = db
|
|
482
|
+
.query(
|
|
483
|
+
`SELECT DISTINCT resource_id FROM ${SHARE_GRANTS_TABLE}
|
|
484
|
+
WHERE app = ? AND kind = ? AND level IN (${levels.map(() => "?").join(", ")})
|
|
485
|
+
AND ( (subject_type = 'user' AND subject_id = ?)
|
|
486
|
+
OR (subject_type = 'group' AND subject_id IN
|
|
487
|
+
(SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ?)) )
|
|
488
|
+
ORDER BY resource_id`,
|
|
489
|
+
)
|
|
490
|
+
.all(app, kind, ...levels, subjectId, subjectId) as { resource_id: string }[];
|
|
491
|
+
return rows.map((row) => row.resource_id);
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
/** The WHERE-clause half, bound to this store's app. See `./sql.ts` for why
|
|
495
|
+
* a fragment rather than a list of ids. */
|
|
496
|
+
sharedSql(input: { kind: string; subjectId: string; column: string; atLeast?: ShareLevel }): string {
|
|
497
|
+
return sharedResourceSql({ app, ...input });
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
// ── Groups ───────────────────────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
createGroup(input: { id: string; name: string; createdBy: string; note?: string }): ShareGroup {
|
|
503
|
+
const id = normalizeSubjectId("group", input.id);
|
|
504
|
+
if (!id) throw new Error("[cursedbelt-core/sharing] a group needs an id");
|
|
505
|
+
const at = clock();
|
|
506
|
+
const events: ShareEvent[] = [];
|
|
507
|
+
const created = db.transaction((): ShareGroup => {
|
|
508
|
+
db.query(
|
|
509
|
+
`INSERT INTO ${SHARE_GROUPS_TABLE} (group_id, name, created_by, created_at, note)
|
|
510
|
+
VALUES (?, ?, ?, ?, ?)
|
|
511
|
+
ON CONFLICT (group_id) DO UPDATE SET name = excluded.name, note = excluded.note`,
|
|
512
|
+
).run(id, input.name, input.createdBy, at, input.note ?? "");
|
|
513
|
+
events.push(
|
|
514
|
+
recordEvent({
|
|
515
|
+
action: "group.create",
|
|
516
|
+
resource: null,
|
|
517
|
+
subject: { type: "group", id },
|
|
518
|
+
level: null,
|
|
519
|
+
actor: input.createdBy,
|
|
520
|
+
note: input.name,
|
|
521
|
+
at,
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
return {
|
|
525
|
+
id,
|
|
526
|
+
name: input.name,
|
|
527
|
+
createdBy: input.createdBy,
|
|
528
|
+
createdAt: at,
|
|
529
|
+
note: input.note ?? "",
|
|
530
|
+
};
|
|
531
|
+
})();
|
|
532
|
+
emit(events);
|
|
533
|
+
return created;
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
renameGroup(id: string, name: string, actor: string): void {
|
|
537
|
+
const groupId = normalizeSubjectId("group", id);
|
|
538
|
+
const at = clock();
|
|
539
|
+
const events: ShareEvent[] = [];
|
|
540
|
+
db.transaction(() => {
|
|
541
|
+
db.query(`UPDATE ${SHARE_GROUPS_TABLE} SET name = ? WHERE group_id = ?`).run(name, groupId);
|
|
542
|
+
events.push(
|
|
543
|
+
recordEvent({
|
|
544
|
+
action: "group.rename",
|
|
545
|
+
resource: null,
|
|
546
|
+
subject: { type: "group", id: groupId },
|
|
547
|
+
level: null,
|
|
548
|
+
actor,
|
|
549
|
+
note: name,
|
|
550
|
+
at,
|
|
551
|
+
}),
|
|
552
|
+
);
|
|
553
|
+
})();
|
|
554
|
+
emit(events);
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Delete a group — and every grant made TO it.
|
|
559
|
+
*
|
|
560
|
+
* 🔴 The grants go with it, explicitly, each with its own revoke event. The
|
|
561
|
+
* `ON DELETE CASCADE` on membership does not reach `share_grants` (a group
|
|
562
|
+
* subject is a string, not a foreign key, because the same column holds user
|
|
563
|
+
* addresses), so a group deleted without this leaves rows nobody can see in
|
|
564
|
+
* any UI and nothing can revoke.
|
|
565
|
+
*/
|
|
566
|
+
deleteGroup(id: string, actor: string): void {
|
|
567
|
+
const groupId = normalizeSubjectId("group", id);
|
|
568
|
+
const at = clock();
|
|
569
|
+
const events: ShareEvent[] = [];
|
|
570
|
+
db.transaction(() => {
|
|
571
|
+
const grants = db
|
|
572
|
+
.query(
|
|
573
|
+
`SELECT * FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = 'group' AND subject_id = ?`,
|
|
574
|
+
)
|
|
575
|
+
.all(app, groupId) as GrantRow[];
|
|
576
|
+
db.query(
|
|
577
|
+
`DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = 'group' AND subject_id = ?`,
|
|
578
|
+
).run(app, groupId);
|
|
579
|
+
for (const row of grants) {
|
|
580
|
+
events.push(
|
|
581
|
+
recordEvent({
|
|
582
|
+
action: "revoke",
|
|
583
|
+
resource: { app, kind: row.kind, id: row.resource_id },
|
|
584
|
+
subject: { type: "group", id: groupId },
|
|
585
|
+
level: row.level,
|
|
586
|
+
actor,
|
|
587
|
+
note: "the group was deleted",
|
|
588
|
+
at,
|
|
589
|
+
}),
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
db.query(`DELETE FROM ${SHARE_GROUPS_TABLE} WHERE group_id = ?`).run(groupId);
|
|
593
|
+
events.push(
|
|
594
|
+
recordEvent({
|
|
595
|
+
action: "group.delete",
|
|
596
|
+
resource: null,
|
|
597
|
+
subject: { type: "group", id: groupId },
|
|
598
|
+
level: null,
|
|
599
|
+
actor,
|
|
600
|
+
note: "",
|
|
601
|
+
at,
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
})();
|
|
605
|
+
emit(events);
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
listGroups(): ShareGroup[] {
|
|
609
|
+
const rows = db
|
|
610
|
+
.query(`SELECT * FROM ${SHARE_GROUPS_TABLE} ORDER BY name`)
|
|
611
|
+
.all() as {
|
|
612
|
+
group_id: string;
|
|
613
|
+
name: string;
|
|
614
|
+
created_by: string;
|
|
615
|
+
created_at: string;
|
|
616
|
+
note: string;
|
|
617
|
+
}[];
|
|
618
|
+
return rows.map((row) => ({
|
|
619
|
+
id: row.group_id,
|
|
620
|
+
name: row.name,
|
|
621
|
+
createdBy: row.created_by,
|
|
622
|
+
createdAt: row.created_at,
|
|
623
|
+
note: row.note,
|
|
624
|
+
}));
|
|
625
|
+
},
|
|
626
|
+
|
|
627
|
+
addGroupMember(groupId: string, subjectId: string, actor: string): void {
|
|
628
|
+
const id = normalizeSubjectId("group", groupId);
|
|
629
|
+
const member = normalizeSubjectId("user", subjectId);
|
|
630
|
+
if (!member) throw new Error("[cursedbelt-core/sharing] a group member needs an address");
|
|
631
|
+
const at = clock();
|
|
632
|
+
const events: ShareEvent[] = [];
|
|
633
|
+
db.transaction(() => {
|
|
634
|
+
db.query(
|
|
635
|
+
`INSERT INTO ${SHARE_GROUP_MEMBERS_TABLE} (group_id, subject_id, added_by, added_at)
|
|
636
|
+
VALUES (?, ?, ?, ?) ON CONFLICT (group_id, subject_id) DO NOTHING`,
|
|
637
|
+
).run(id, member, actor, at);
|
|
638
|
+
events.push(
|
|
639
|
+
recordEvent({
|
|
640
|
+
action: "group.join",
|
|
641
|
+
resource: null,
|
|
642
|
+
subject: { type: "group", id },
|
|
643
|
+
level: null,
|
|
644
|
+
actor,
|
|
645
|
+
note: member,
|
|
646
|
+
at,
|
|
647
|
+
}),
|
|
648
|
+
);
|
|
649
|
+
})();
|
|
650
|
+
emit(events);
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
removeGroupMember(groupId: string, subjectId: string, actor: string): void {
|
|
654
|
+
const id = normalizeSubjectId("group", groupId);
|
|
655
|
+
const member = normalizeSubjectId("user", subjectId);
|
|
656
|
+
const at = clock();
|
|
657
|
+
const events: ShareEvent[] = [];
|
|
658
|
+
db.transaction(() => {
|
|
659
|
+
db.query(
|
|
660
|
+
`DELETE FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE group_id = ? AND subject_id = ?`,
|
|
661
|
+
).run(id, member);
|
|
662
|
+
events.push(
|
|
663
|
+
recordEvent({
|
|
664
|
+
action: "group.leave",
|
|
665
|
+
resource: null,
|
|
666
|
+
subject: { type: "group", id },
|
|
667
|
+
level: null,
|
|
668
|
+
actor,
|
|
669
|
+
note: member,
|
|
670
|
+
at,
|
|
671
|
+
}),
|
|
672
|
+
);
|
|
673
|
+
})();
|
|
674
|
+
emit(events);
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
listGroupMembers(groupId: string): ShareGroupMember[] {
|
|
678
|
+
const rows = db
|
|
679
|
+
.query(
|
|
680
|
+
`SELECT * FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE group_id = ? ORDER BY subject_id`,
|
|
681
|
+
)
|
|
682
|
+
.all(normalizeSubjectId("group", groupId)) as {
|
|
683
|
+
group_id: string;
|
|
684
|
+
subject_id: string;
|
|
685
|
+
added_by: string;
|
|
686
|
+
added_at: string;
|
|
687
|
+
}[];
|
|
688
|
+
return rows.map((row) => ({
|
|
689
|
+
groupId: row.group_id,
|
|
690
|
+
subjectId: row.subject_id,
|
|
691
|
+
addedBy: row.added_by,
|
|
692
|
+
addedAt: row.added_at,
|
|
693
|
+
}));
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
/** Which groups is this person in — the one place membership is resolved. */
|
|
697
|
+
groupIdsFor(subjectId: string): string[] {
|
|
698
|
+
const member = normalizeSubjectId("user", subjectId);
|
|
699
|
+
if (!member) return [];
|
|
700
|
+
const rows = db
|
|
701
|
+
.query(
|
|
702
|
+
`SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ? ORDER BY group_id`,
|
|
703
|
+
)
|
|
704
|
+
.all(member) as { group_id: string }[];
|
|
705
|
+
return rows.map((row) => row.group_id);
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
// ── The audit trail ──────────────────────────────────────────────────────
|
|
709
|
+
|
|
710
|
+
/** The trail, newest first. Filter by resource, by subject, or neither. */
|
|
711
|
+
events(query: ShareEventQuery = {}): ShareEvent[] {
|
|
712
|
+
const clauses = ["app IS NULL OR app = ?"];
|
|
713
|
+
const params: (string | number)[] = [app];
|
|
714
|
+
if (query.kind !== undefined) {
|
|
715
|
+
clauses.push("kind = ?");
|
|
716
|
+
params.push(query.kind);
|
|
717
|
+
}
|
|
718
|
+
if (query.id !== undefined) {
|
|
719
|
+
clauses.push("resource_id = ?");
|
|
720
|
+
params.push(query.id);
|
|
721
|
+
}
|
|
722
|
+
if (query.subject) {
|
|
723
|
+
const normal = normalized(query.subject);
|
|
724
|
+
clauses.push("subject_type = ? AND subject_id = ?");
|
|
725
|
+
params.push(normal.type, normal.id);
|
|
726
|
+
}
|
|
727
|
+
const where = clauses.map((clause) => `(${clause})`).join(" AND ");
|
|
728
|
+
const limit = Math.max(1, Math.min(query.limit ?? 200, 1000));
|
|
729
|
+
const rows = db
|
|
730
|
+
.query(
|
|
731
|
+
`SELECT * FROM ${SHARE_EVENTS_TABLE} WHERE ${where} ORDER BY event_id DESC LIMIT ?`,
|
|
732
|
+
)
|
|
733
|
+
.all(...params, limit) as EventRow[];
|
|
734
|
+
return rows.map(toEvent);
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|