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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mountShareRoutes` — the three HTTP routes a shareable resource needs, once.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists ─────────────────────────────────────────────────────────────────────
|
|
5
|
+
* The store (`./store.ts`) was already the fleet's one sharing model; the ROUTES over it
|
|
6
|
+
* were not. `apps/life` wrote them once and mounted them four times (calendars, lists,
|
|
7
|
+
* to-dos, board cards) and `apps/family` wrote a fifth copy for people — five hand-rolled
|
|
8
|
+
* copies of read/grant/revoke, each re-deciding the same three policy questions:
|
|
9
|
+
*
|
|
10
|
+
* 1. **All three routes are OWNER-only, including the READ.** "Who else can see this" is
|
|
11
|
+
* not part of what "shared with you" grants — a recipient must never reach
|
|
12
|
+
* `listForResource`, because the roster of other grantees is itself a disclosure.
|
|
13
|
+
* Five copies is five chances to write the gate on POST/DELETE and forget it on GET.
|
|
14
|
+
* 2. **`group:<id>` is how a group subject travels in a URL.** A path segment cannot
|
|
15
|
+
* carry a `{type, id}` pair, so the prefix IS the type. Getting that wrong revokes a
|
|
16
|
+
* USER grant whose address happens to start with `group:` — or, worse, silently
|
|
17
|
+
* revokes nothing and reports success.
|
|
18
|
+
* 3. **A grant only ever widens, and the audit trail outlives it.** Both are the store's,
|
|
19
|
+
* and both are only true if every route actually goes through the store.
|
|
20
|
+
*
|
|
21
|
+
* A bug in any of those has to be fixable by one commit. That is the placement test in
|
|
22
|
+
* `cursedbelt/STANDARDS.md`, and it passes here.
|
|
23
|
+
*
|
|
24
|
+
* ── 🔴 The wire shape is LIFE's, and family's client changed to match ────────────────────
|
|
25
|
+
* The two apps genuinely disagreed and one had to give:
|
|
26
|
+
*
|
|
27
|
+
* · revoke — family put the subject in the QUERY (`DELETE …/shares?subject=&type=`),
|
|
28
|
+
* life in the PATH (`DELETE …/shares/:subject`, `group:` prefixed). The path wins.
|
|
29
|
+
* family's own comment argued for the query on the grounds that the target should be
|
|
30
|
+
* visible in the request line — which a path segment satisfies just as well, while
|
|
31
|
+
* also being the shape four of the five mounts already used. Changing one client beat
|
|
32
|
+
* changing four.
|
|
33
|
+
* · read — family returned `{grants, events, labels}`, life `{grants, events, groups}`.
|
|
34
|
+
* Neither extra is general: `labels` resolves addresses to names out of the family
|
|
35
|
+
* tree *inside the viewer's space*, `groups` is life's household roster. So the
|
|
36
|
+
* primitive owns `{grants, events}` and an app folds its own half in through
|
|
37
|
+
* {@link ShareRouterOptions.extra}.
|
|
38
|
+
*
|
|
39
|
+
* ── What stays in the app ───────────────────────────────────────────────────────────────
|
|
40
|
+
* Ownership. `mustOwn` is the app's, because "who owns a calendar" and "who may curate a
|
|
41
|
+
* family file" are policy this module has no business knowing — and because the 404/403
|
|
42
|
+
* ORDER is policy too (family answers 404 before 403 on a person the viewer may not know
|
|
43
|
+
* exists). The hook returns the refusal it wants, or null to continue.
|
|
44
|
+
*/
|
|
45
|
+
import type { Context, Hono } from 'hono';
|
|
46
|
+
import type { ShareEvent, ShareGrant } from 'cursedbelt-core/sharing/model';
|
|
47
|
+
import type { ShareStore } from './store';
|
|
48
|
+
/** The `group:` prefix that lets a group subject travel in a path segment. Exported so a
|
|
49
|
+
* client and a route cannot disagree about it. */
|
|
50
|
+
export declare const GROUP_SUBJECT_PREFIX = "group:";
|
|
51
|
+
/** Split a URL subject segment into the store's `{type, id}` pair. */
|
|
52
|
+
export declare function parseSubjectParam(raw: string): {
|
|
53
|
+
type: 'user' | 'group';
|
|
54
|
+
id: string;
|
|
55
|
+
};
|
|
56
|
+
/** The inverse — what a client puts in the path. */
|
|
57
|
+
export declare const subjectParam: (subject: {
|
|
58
|
+
type: "user" | "group";
|
|
59
|
+
id: string;
|
|
60
|
+
}) => string;
|
|
61
|
+
/** What every route needs about who is asking. Structural, so an app hands its own viewer
|
|
62
|
+
* type straight in. */
|
|
63
|
+
export interface ShareRouterViewer {
|
|
64
|
+
/** The fleet address; the subject id of every `user` grant. */
|
|
65
|
+
email: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ShareRouterOptions<V extends ShareRouterViewer> {
|
|
68
|
+
/**
|
|
69
|
+
* The resource path, with `:id` in it — e.g. `/api/calendars/:id`. The three routes
|
|
70
|
+
* hang off `${base}/shares`; revoke adds `/:subject`.
|
|
71
|
+
*/
|
|
72
|
+
base: string;
|
|
73
|
+
/** The `kind` half of the store's `(app, kind, id)` triple. */
|
|
74
|
+
kind: string;
|
|
75
|
+
/** The store. A function when it is per-request (a multi-tenant app). */
|
|
76
|
+
shares: ShareStore | ((c: Context) => ShareStore);
|
|
77
|
+
/** Who is asking. Returning null refuses every route with 401. */
|
|
78
|
+
viewerOf: (c: Context) => V | null;
|
|
79
|
+
/**
|
|
80
|
+
* 🔴 The OWNER gate, run on ALL THREE routes including the read. Return a `Response` to
|
|
81
|
+
* refuse (the app owns the status AND the 404-before-403 ordering), or null to allow.
|
|
82
|
+
* `throw`ing is fine too — an app with an error middleware can keep using it.
|
|
83
|
+
*/
|
|
84
|
+
mustOwn: (c: Context, id: string, viewer: V) => Response | null | Promise<Response | null>;
|
|
85
|
+
/** Extra fields folded into the READ payload beside `{grants, events}` — family's
|
|
86
|
+
* `labels`, life's `groups`. */
|
|
87
|
+
extra?: (c: Context, id: string, viewer: V) => Record<string, unknown>;
|
|
88
|
+
/** How many audit lines the read carries. Default 50. */
|
|
89
|
+
trailLimit?: number;
|
|
90
|
+
}
|
|
91
|
+
/** The read payload's guaranteed half. An app's `extra` is spread in beside it. */
|
|
92
|
+
export interface ShareRoutesPayload {
|
|
93
|
+
grants: ShareGrant[];
|
|
94
|
+
/** Newest first, and it OUTLIVES the grants — a revoked share still has a line. */
|
|
95
|
+
events: ShareEvent[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Mount GET / POST / DELETE for one `kind` on one app.
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* mountShareRoutes(app, {
|
|
102
|
+
* base: '/api/calendars/:id',
|
|
103
|
+
* kind: CALENDAR_KIND,
|
|
104
|
+
* shares,
|
|
105
|
+
* viewerOf,
|
|
106
|
+
* mustOwn: (c, id, viewer) => (ownsCalendar(db, viewer, id) ? null : c.json({ error: 'not found' }, 404)),
|
|
107
|
+
* extra: () => ({ groups: shares.listGroups() }),
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare function mountShareRoutes<V extends ShareRouterViewer>(app: Hono<any>, options: ShareRouterOptions<V>): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** The `group:` prefix that lets a group subject travel in a path segment. Exported so a
|
|
2
|
+
* client and a route cannot disagree about it. */
|
|
3
|
+
export const GROUP_SUBJECT_PREFIX = 'group:';
|
|
4
|
+
/** Split a URL subject segment into the store's `{type, id}` pair. */
|
|
5
|
+
export function parseSubjectParam(raw) {
|
|
6
|
+
const decoded = decodeURIComponent(raw);
|
|
7
|
+
return decoded.startsWith(GROUP_SUBJECT_PREFIX)
|
|
8
|
+
? { type: 'group', id: decoded.slice(GROUP_SUBJECT_PREFIX.length) }
|
|
9
|
+
: { type: 'user', id: decoded };
|
|
10
|
+
}
|
|
11
|
+
/** The inverse — what a client puts in the path. */
|
|
12
|
+
export const subjectParam = (subject) => encodeURIComponent(subject.type === 'group' ? GROUP_SUBJECT_PREFIX + subject.id : subject.id);
|
|
13
|
+
const isLevel = (v) => v === 'read' || v === 'write';
|
|
14
|
+
/**
|
|
15
|
+
* Mount GET / POST / DELETE for one `kind` on one app.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* mountShareRoutes(app, {
|
|
19
|
+
* base: '/api/calendars/:id',
|
|
20
|
+
* kind: CALENDAR_KIND,
|
|
21
|
+
* shares,
|
|
22
|
+
* viewerOf,
|
|
23
|
+
* mustOwn: (c, id, viewer) => (ownsCalendar(db, viewer, id) ? null : c.json({ error: 'not found' }, 404)),
|
|
24
|
+
* extra: () => ({ groups: shares.listGroups() }),
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function mountShareRoutes(
|
|
29
|
+
// biome-ignore lint/suspicious/noExplicitAny: the caller's Hono env is theirs, and this
|
|
30
|
+
// module reads nothing off it — narrowing it would force every app to name its own Env
|
|
31
|
+
// here for no gain.
|
|
32
|
+
app, options) {
|
|
33
|
+
const { base, kind, viewerOf, mustOwn, extra, trailLimit = 50 } = options;
|
|
34
|
+
const storeOf = (c) => typeof options.shares === 'function' ? options.shares(c) : options.shares;
|
|
35
|
+
/** The three-step preamble every route shares: identity, id, ownership. */
|
|
36
|
+
async function enter(c) {
|
|
37
|
+
const viewer = viewerOf(c);
|
|
38
|
+
if (!viewer)
|
|
39
|
+
return { refused: c.json({ error: 'sign in first' }, 401) };
|
|
40
|
+
const id = c.req.param('id') ?? '';
|
|
41
|
+
const refused = await mustOwn(c, id, viewer);
|
|
42
|
+
if (refused)
|
|
43
|
+
return { refused };
|
|
44
|
+
return { id, viewer, shares: storeOf(c) };
|
|
45
|
+
}
|
|
46
|
+
app.get(`${base}/shares`, async (c) => {
|
|
47
|
+
const step = await enter(c);
|
|
48
|
+
if ('refused' in step)
|
|
49
|
+
return step.refused;
|
|
50
|
+
const { id, viewer, shares } = step;
|
|
51
|
+
const payload = {
|
|
52
|
+
// The store's own rows, straight through — the sharing panel consumes exactly
|
|
53
|
+
// these, and a DTO in the middle is a place for a field to go missing.
|
|
54
|
+
grants: shares.listForResource({ kind, id }),
|
|
55
|
+
events: shares.events({ kind, id, limit: trailLimit }),
|
|
56
|
+
...(extra ? extra(c, id, viewer) : {}),
|
|
57
|
+
};
|
|
58
|
+
return c.json(payload);
|
|
59
|
+
});
|
|
60
|
+
app.post(`${base}/shares`, async (c) => {
|
|
61
|
+
const step = await enter(c);
|
|
62
|
+
if ('refused' in step)
|
|
63
|
+
return step.refused;
|
|
64
|
+
const { id, viewer, shares } = step;
|
|
65
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
66
|
+
const subjectId = (body.subject?.id ?? body.email ?? '').trim();
|
|
67
|
+
if (!subjectId)
|
|
68
|
+
return c.json({ error: 'who should this be shared with?' }, 400);
|
|
69
|
+
if (body.level !== undefined && !isLevel(body.level)) {
|
|
70
|
+
return c.json({ error: "level must be 'read' or 'write'" }, 400);
|
|
71
|
+
}
|
|
72
|
+
const grant = shares.grant({
|
|
73
|
+
kind,
|
|
74
|
+
id,
|
|
75
|
+
subject: { type: body.subject?.type === 'group' ? 'group' : 'user', id: subjectId },
|
|
76
|
+
level: isLevel(body.level) ? body.level : 'read',
|
|
77
|
+
grantedBy: viewer.email,
|
|
78
|
+
...(typeof body.note === 'string' ? { note: body.note } : {}),
|
|
79
|
+
});
|
|
80
|
+
return c.json(grant, 201);
|
|
81
|
+
});
|
|
82
|
+
app.delete(`${base}/shares/:subject`, async (c) => {
|
|
83
|
+
const step = await enter(c);
|
|
84
|
+
if ('refused' in step)
|
|
85
|
+
return step.refused;
|
|
86
|
+
const { id, viewer, shares } = step;
|
|
87
|
+
const raw = c.req.param('subject') ?? '';
|
|
88
|
+
if (!raw.trim())
|
|
89
|
+
return c.json({ error: 'which share should be removed?' }, 400);
|
|
90
|
+
shares.revoke({ kind, id, subject: parseSubjectParam(raw), revokedBy: viewer.email });
|
|
91
|
+
return c.json({ ok: true });
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half a consuming app needs INSIDE its own queries: a SQL boolean saying
|
|
3
|
+
* "this row is shared with the viewer".
|
|
4
|
+
*
|
|
5
|
+
* ── 🔴 Why a SQL fragment and not a list of ids ──────────────────────────────
|
|
6
|
+
* The obvious API is `sharedIds(kind, viewer): string[]`, and it is wrong for the
|
|
7
|
+
* consumers this exists to serve. `apps/family`'s rule, written in `people.ts`
|
|
8
|
+
* before this primitive existed, is that a row the viewer may not see *"must not
|
|
9
|
+
* surface as a snippet, a COUNT, or a tree node either"* — so its visibility test
|
|
10
|
+
* has to be a WHERE clause that a `COUNT(*)`, a JOIN and a recursive tree walk all
|
|
11
|
+
* inherit. Handing the app an array means the app writes `id IN (…)` itself, in
|
|
12
|
+
* every one of those places, with its own quoting; the first one somebody forgets
|
|
13
|
+
* is a disclosure, and it will be the count.
|
|
14
|
+
*
|
|
15
|
+
* So the primitive emits the clause. `sharedResourceSql` returns a correlated
|
|
16
|
+
* `EXISTS (…)` over the grant tables that can be dropped into any query where the
|
|
17
|
+
* resource's id column is in scope, including one whose only bound parameter is
|
|
18
|
+
* something else entirely.
|
|
19
|
+
*
|
|
20
|
+
* ── 🔴 …which means this module quotes literals, so it does it in ONE place ───
|
|
21
|
+
* A fragment with no bindable parameters has to inline its values. Every value
|
|
22
|
+
* that reaches SQL here goes through {@link sqlLiteral} (double every apostrophe —
|
|
23
|
+
* `o'brien@example.com` is an ordinary address) or {@link sqlColumn} (an
|
|
24
|
+
* identifier, matched against a strict pattern and REFUSED otherwise, so a column
|
|
25
|
+
* expression can never smuggle a subquery in). Both are exported and both are
|
|
26
|
+
* tested with hostile input; a consumer never quotes anything itself.
|
|
27
|
+
*/
|
|
28
|
+
import { type ShareLevel } from "cursedbelt-core/sharing/model";
|
|
29
|
+
/**
|
|
30
|
+
* Quote a string as a SQL literal.
|
|
31
|
+
*
|
|
32
|
+
* SQLite escapes an apostrophe by doubling it, and that is the whole rule for a
|
|
33
|
+
* TEXT literal — there is no backslash escape to worry about. A NUL byte is
|
|
34
|
+
* refused rather than quoted: SQLite truncates a string at one, so a value
|
|
35
|
+
* containing it would compare equal to a shorter value and silently widen a grant.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sqlLiteral(value: string): string;
|
|
38
|
+
export declare function sqlColumn(column: string): string;
|
|
39
|
+
export interface SharedResourceSqlOptions {
|
|
40
|
+
/** The satellite the resource belongs to — the `app` of the `(app, kind, id)`
|
|
41
|
+
* triple. */
|
|
42
|
+
app: string;
|
|
43
|
+
/** The app's own word for this sort of record ("person", "budget"). */
|
|
44
|
+
kind: string;
|
|
45
|
+
/** The viewer's fleet address. An EMPTY id yields `0=1` — see below. */
|
|
46
|
+
subjectId: string;
|
|
47
|
+
/** The column holding the resource's id, in scope of the query being built. */
|
|
48
|
+
column: string;
|
|
49
|
+
/** Minimum level the grant must carry. Default `read`. */
|
|
50
|
+
atLeast?: ShareLevel;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A SQL boolean: true for a row the viewer holds a grant on, at `atLeast` or
|
|
54
|
+
* above, directly or through a group.
|
|
55
|
+
*
|
|
56
|
+
* The group half is a nested `SELECT` rather than a pre-resolved list, so a
|
|
57
|
+
* membership change takes effect on the next query with no request-scoped cache to
|
|
58
|
+
* invalidate — the failure mode of caching it is a relative who was removed from
|
|
59
|
+
* the family group this morning and can still read until something restarts.
|
|
60
|
+
*
|
|
61
|
+
* 🔴 **An empty subject id yields `0=1`, never `1=1`.** An unauthenticated or
|
|
62
|
+
* machine caller has no address, and the arithmetic of `subject_id = ''` against a
|
|
63
|
+
* table where nothing has an empty id happens to be safe today — but a single
|
|
64
|
+
* malformed row would turn that into "shared with everybody". Failing closed on the
|
|
65
|
+
* missing identity is the property, not a coincidence of the data.
|
|
66
|
+
*/
|
|
67
|
+
export declare function sharedResourceSql(options: SharedResourceSqlOptions): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half a consuming app needs INSIDE its own queries: a SQL boolean saying
|
|
3
|
+
* "this row is shared with the viewer".
|
|
4
|
+
*
|
|
5
|
+
* ── 🔴 Why a SQL fragment and not a list of ids ──────────────────────────────
|
|
6
|
+
* The obvious API is `sharedIds(kind, viewer): string[]`, and it is wrong for the
|
|
7
|
+
* consumers this exists to serve. `apps/family`'s rule, written in `people.ts`
|
|
8
|
+
* before this primitive existed, is that a row the viewer may not see *"must not
|
|
9
|
+
* surface as a snippet, a COUNT, or a tree node either"* — so its visibility test
|
|
10
|
+
* has to be a WHERE clause that a `COUNT(*)`, a JOIN and a recursive tree walk all
|
|
11
|
+
* inherit. Handing the app an array means the app writes `id IN (…)` itself, in
|
|
12
|
+
* every one of those places, with its own quoting; the first one somebody forgets
|
|
13
|
+
* is a disclosure, and it will be the count.
|
|
14
|
+
*
|
|
15
|
+
* So the primitive emits the clause. `sharedResourceSql` returns a correlated
|
|
16
|
+
* `EXISTS (…)` over the grant tables that can be dropped into any query where the
|
|
17
|
+
* resource's id column is in scope, including one whose only bound parameter is
|
|
18
|
+
* something else entirely.
|
|
19
|
+
*
|
|
20
|
+
* ── 🔴 …which means this module quotes literals, so it does it in ONE place ───
|
|
21
|
+
* A fragment with no bindable parameters has to inline its values. Every value
|
|
22
|
+
* that reaches SQL here goes through {@link sqlLiteral} (double every apostrophe —
|
|
23
|
+
* `o'brien@example.com` is an ordinary address) or {@link sqlColumn} (an
|
|
24
|
+
* identifier, matched against a strict pattern and REFUSED otherwise, so a column
|
|
25
|
+
* expression can never smuggle a subquery in). Both are exported and both are
|
|
26
|
+
* tested with hostile input; a consumer never quotes anything itself.
|
|
27
|
+
*/
|
|
28
|
+
import { levelsMeeting, normalizeSubjectId } from "cursedbelt-core/sharing/model";
|
|
29
|
+
import { SHARE_GRANTS_TABLE, SHARE_GROUP_MEMBERS_TABLE } from "./migrations";
|
|
30
|
+
/**
|
|
31
|
+
* Quote a string as a SQL literal.
|
|
32
|
+
*
|
|
33
|
+
* SQLite escapes an apostrophe by doubling it, and that is the whole rule for a
|
|
34
|
+
* TEXT literal — there is no backslash escape to worry about. A NUL byte is
|
|
35
|
+
* refused rather than quoted: SQLite truncates a string at one, so a value
|
|
36
|
+
* containing it would compare equal to a shorter value and silently widen a grant.
|
|
37
|
+
*/
|
|
38
|
+
export function sqlLiteral(value) {
|
|
39
|
+
if (value.includes("\u0000")) {
|
|
40
|
+
throw new Error("[cursedbelt-core/sharing] a SQL literal may not contain a NUL byte");
|
|
41
|
+
}
|
|
42
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
43
|
+
}
|
|
44
|
+
/** A column reference — `id`, `p.id`, `"people"."id"` is NOT accepted. Anything
|
|
45
|
+
* that is not a bare (optionally table-qualified) identifier is refused, because
|
|
46
|
+
* this value is interpolated and a caller passing a computed string is how an
|
|
47
|
+
* injection reaches a fragment builder. */
|
|
48
|
+
const COLUMN_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/;
|
|
49
|
+
export function sqlColumn(column) {
|
|
50
|
+
if (!COLUMN_PATTERN.test(column)) {
|
|
51
|
+
throw new Error(`[cursedbelt-core/sharing] ${JSON.stringify(column)} is not a plain column reference ` +
|
|
52
|
+
"(expected `id` or `alias.id`)");
|
|
53
|
+
}
|
|
54
|
+
return column;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A SQL boolean: true for a row the viewer holds a grant on, at `atLeast` or
|
|
58
|
+
* above, directly or through a group.
|
|
59
|
+
*
|
|
60
|
+
* The group half is a nested `SELECT` rather than a pre-resolved list, so a
|
|
61
|
+
* membership change takes effect on the next query with no request-scoped cache to
|
|
62
|
+
* invalidate — the failure mode of caching it is a relative who was removed from
|
|
63
|
+
* the family group this morning and can still read until something restarts.
|
|
64
|
+
*
|
|
65
|
+
* 🔴 **An empty subject id yields `0=1`, never `1=1`.** An unauthenticated or
|
|
66
|
+
* machine caller has no address, and the arithmetic of `subject_id = ''` against a
|
|
67
|
+
* table where nothing has an empty id happens to be safe today — but a single
|
|
68
|
+
* malformed row would turn that into "shared with everybody". Failing closed on the
|
|
69
|
+
* missing identity is the property, not a coincidence of the data.
|
|
70
|
+
*/
|
|
71
|
+
export function sharedResourceSql(options) {
|
|
72
|
+
const subjectId = normalizeSubjectId("user", options.subjectId);
|
|
73
|
+
if (!subjectId)
|
|
74
|
+
return "0=1";
|
|
75
|
+
const column = sqlColumn(options.column);
|
|
76
|
+
const levels = levelsMeeting(options.atLeast ?? "read")
|
|
77
|
+
.map((level) => sqlLiteral(level))
|
|
78
|
+
.join(", ");
|
|
79
|
+
const subject = sqlLiteral(subjectId);
|
|
80
|
+
return (`EXISTS (SELECT 1 FROM ${SHARE_GRANTS_TABLE} sg` +
|
|
81
|
+
` WHERE sg.app = ${sqlLiteral(options.app)}` +
|
|
82
|
+
` AND sg.kind = ${sqlLiteral(options.kind)}` +
|
|
83
|
+
` AND sg.resource_id = ${column}` +
|
|
84
|
+
` AND sg.level IN (${levels})` +
|
|
85
|
+
` AND ((sg.subject_type = 'user' AND sg.subject_id = ${subject})` +
|
|
86
|
+
` OR (sg.subject_type = 'group' AND sg.subject_id IN` +
|
|
87
|
+
` (SELECT gm.group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} gm WHERE gm.subject_id = ${subject}))))`);
|
|
88
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { type ShareAccess, type ShareEvent, type ShareGrant, type ShareGroup, type ShareGroupMember, type ShareLevel, type ShareSubject, type ShareViewer } from "cursedbelt-core/sharing/model";
|
|
37
|
+
/** Shorthand for the common subject. `group(id)` is its sibling. */
|
|
38
|
+
export declare const user: (id: string) => ShareSubject;
|
|
39
|
+
export declare const group: (id: string) => ShareSubject;
|
|
40
|
+
/** A resource within the store's own app — the `app` half is supplied by the
|
|
41
|
+
* store, so a caller cannot mistakenly write a grant into another app's rows. */
|
|
42
|
+
export interface ShareTarget {
|
|
43
|
+
kind: string;
|
|
44
|
+
id: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ShareStoreOptions {
|
|
47
|
+
db: Database;
|
|
48
|
+
/** The satellite this store speaks for. Fixed at construction so `app` cannot
|
|
49
|
+
* be got wrong per call — the commonest way a grant becomes invisible. */
|
|
50
|
+
app: string;
|
|
51
|
+
/**
|
|
52
|
+
* Notified after every committed mutation — the notification hook ca's model
|
|
53
|
+
* had, as a seam rather than a dependency.
|
|
54
|
+
*
|
|
55
|
+
* 🔴 It stays a seam now that the other half EXISTS. `cursedbelt/server/
|
|
56
|
+
* notifications` landed the same day this did, so a consumer can wire the two
|
|
57
|
+
* in one line — `onEvent: (e) => notifications.notify({ userId: e.subject.id,
|
|
58
|
+
* … })` — and `describeShareEvent` in the vocabulary is the wording both the
|
|
59
|
+
* panel and that notification use. What must NOT happen is this store
|
|
60
|
+
* importing that one: an app is entitled to a sharing model with no bell (the
|
|
61
|
+
* grant is still audited), the subject of a GROUP grant is a group id and not
|
|
62
|
+
* a person to notify, and an app knows which of its users to address where
|
|
63
|
+
* this primitive only knows a subject id.
|
|
64
|
+
*/
|
|
65
|
+
onEvent?: (event: ShareEvent) => void;
|
|
66
|
+
/** Injectable clock, for tests that assert ordering. ISO-8601 strings. */
|
|
67
|
+
now?: () => string;
|
|
68
|
+
}
|
|
69
|
+
export interface GrantInput extends ShareTarget {
|
|
70
|
+
subject: ShareSubject;
|
|
71
|
+
level: ShareLevel;
|
|
72
|
+
grantedBy: string;
|
|
73
|
+
note?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface RevokeInput extends ShareTarget {
|
|
76
|
+
subject: ShareSubject;
|
|
77
|
+
revokedBy: string;
|
|
78
|
+
note?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface ShareEventQuery {
|
|
81
|
+
kind?: string;
|
|
82
|
+
id?: string;
|
|
83
|
+
subject?: ShareSubject;
|
|
84
|
+
/** Newest first, capped. Default 200 — a panel shows a page, and an unbounded
|
|
85
|
+
* audit read on a busy resource is the query that times out in production. */
|
|
86
|
+
limit?: number;
|
|
87
|
+
}
|
|
88
|
+
export type ShareStore = ReturnType<typeof createShareStore>;
|
|
89
|
+
/**
|
|
90
|
+
* Open (and migrate) the sharing tables in this database, returning the store.
|
|
91
|
+
*
|
|
92
|
+
* Idempotent: safe to call once per boot per `db`, and safe to call in a database
|
|
93
|
+
* that already has the tables (the migration runner records what it applied).
|
|
94
|
+
*/
|
|
95
|
+
export declare function createShareStore(options: ShareStoreOptions): {
|
|
96
|
+
/** The app this store speaks for — exposed so a consumer can build its own
|
|
97
|
+
* `sharedResourceSql` call without re-declaring the name. */
|
|
98
|
+
app: string;
|
|
99
|
+
/**
|
|
100
|
+
* Give `subject` access to a resource, or change the access they have.
|
|
101
|
+
*
|
|
102
|
+
* An unchanged re-grant is still recorded as a `grant` event: "the owner
|
|
103
|
+
* confirmed this on Tuesday" is a fact somebody reading the trail wants, and
|
|
104
|
+
* suppressing it would make an idempotent API silently unauditable.
|
|
105
|
+
*/
|
|
106
|
+
grant(input: GrantInput): ShareGrant;
|
|
107
|
+
/** Take access away. Returns the level that was removed, or null when there
|
|
108
|
+
* was nothing to remove — in which case nothing is logged, because an audit
|
|
109
|
+
* trail full of "revoked nothing" is one nobody reads. */
|
|
110
|
+
revoke(input: RevokeInput): ShareLevel | null;
|
|
111
|
+
/**
|
|
112
|
+
* Revoke EVERY grant on one resource — what an app calls when it deletes the
|
|
113
|
+
* record. Returns how many were removed.
|
|
114
|
+
*
|
|
115
|
+
* 🔴 Not optional housekeeping. Resource ids are app-minted, and a grant left
|
|
116
|
+
* behind for a deleted id becomes access to whatever is created with that id
|
|
117
|
+
* later. `apps/family` calls this in the same transaction as deleting a
|
|
118
|
+
* person.
|
|
119
|
+
*/
|
|
120
|
+
revokeResource(target: ShareTarget, revokedBy: string): number;
|
|
121
|
+
/** Revoke everything shared with one subject, across every kind — the
|
|
122
|
+
* offboarding call. A person losing their fleet account keeps nothing. */
|
|
123
|
+
revokeSubject(subject: ShareSubject, revokedBy: string): number;
|
|
124
|
+
/** Who can see this record, strongest first then oldest — the panel's read. */
|
|
125
|
+
listForResource(target: ShareTarget): ShareGrant[];
|
|
126
|
+
/** Everything shared with one subject — "shared with you". Group grants are
|
|
127
|
+
* NOT expanded here; use {@link resourceIdsFor} for the resolved answer. */
|
|
128
|
+
listForSubject(subject: ShareSubject): ShareGrant[];
|
|
129
|
+
/**
|
|
130
|
+
* What may this viewer do with this resource — the resolver.
|
|
131
|
+
*
|
|
132
|
+
* The strongest of their own grant and every group they are in, computed in
|
|
133
|
+
* SQL. `resolveAccess` in `cursedbelt-core/sharing` is the same rule written as a
|
|
134
|
+
* pure function, and `store.spec.ts` checks the two agree.
|
|
135
|
+
*/
|
|
136
|
+
accessFor(target: ShareTarget, viewer: ShareViewer): ShareAccess;
|
|
137
|
+
/** Every resource of `kind` this viewer reaches at `atLeast` or above.
|
|
138
|
+
* For filtering rows in the app's OWN query, prefer `sharedResourceSql`. */
|
|
139
|
+
resourceIdsFor(kind: string, viewer: ShareViewer, atLeast?: ShareLevel): string[];
|
|
140
|
+
/** The WHERE-clause half, bound to this store's app. See `./sql.ts` for why
|
|
141
|
+
* a fragment rather than a list of ids. */
|
|
142
|
+
sharedSql(input: {
|
|
143
|
+
kind: string;
|
|
144
|
+
subjectId: string;
|
|
145
|
+
column: string;
|
|
146
|
+
atLeast?: ShareLevel;
|
|
147
|
+
}): string;
|
|
148
|
+
createGroup(input: {
|
|
149
|
+
id: string;
|
|
150
|
+
name: string;
|
|
151
|
+
createdBy: string;
|
|
152
|
+
note?: string;
|
|
153
|
+
}): ShareGroup;
|
|
154
|
+
renameGroup(id: string, name: string, actor: string): void;
|
|
155
|
+
/**
|
|
156
|
+
* Delete a group — and every grant made TO it.
|
|
157
|
+
*
|
|
158
|
+
* 🔴 The grants go with it, explicitly, each with its own revoke event. The
|
|
159
|
+
* `ON DELETE CASCADE` on membership does not reach `share_grants` (a group
|
|
160
|
+
* subject is a string, not a foreign key, because the same column holds user
|
|
161
|
+
* addresses), so a group deleted without this leaves rows nobody can see in
|
|
162
|
+
* any UI and nothing can revoke.
|
|
163
|
+
*/
|
|
164
|
+
deleteGroup(id: string, actor: string): void;
|
|
165
|
+
listGroups(): ShareGroup[];
|
|
166
|
+
addGroupMember(groupId: string, subjectId: string, actor: string): void;
|
|
167
|
+
removeGroupMember(groupId: string, subjectId: string, actor: string): void;
|
|
168
|
+
listGroupMembers(groupId: string): ShareGroupMember[];
|
|
169
|
+
/** Which groups is this person in — the one place membership is resolved. */
|
|
170
|
+
groupIdsFor(subjectId: string): string[];
|
|
171
|
+
/** The trail, newest first. Filter by resource, by subject, or neither. */
|
|
172
|
+
events(query?: ShareEventQuery): ShareEvent[];
|
|
173
|
+
};
|