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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one revocation store, in two shapes for two hot paths (DEDUP-7).
|
|
3
|
+
*
|
|
4
|
+
* "Was this session/token withdrawn early?" is a single concern, but it is
|
|
5
|
+
* consulted from two verify paths whose constraints do not meet in the middle,
|
|
6
|
+
* so this file settles ONE contract per constraint and gives the async one a
|
|
7
|
+
* bridge back to the sync one:
|
|
8
|
+
*
|
|
9
|
+
* • {@link RevocationStore} — SYNCHRONOUS (`isRevoked`/`revoke`/`sweep`, epoch
|
|
10
|
+
* **milliseconds**). The single-owner satellites' `createOwnerAuth` consults
|
|
11
|
+
* it inside `isAuthenticated`, which is called on every request and cannot
|
|
12
|
+
* become a promise without turning every route async. Its durable
|
|
13
|
+
* implementation is a local sqlite file — the denylist a redeploy must not
|
|
14
|
+
* forget (`apps/roms`'s deploy smoke caught a logout that did not log out).
|
|
15
|
+
*
|
|
16
|
+
* • {@link AsyncRevocationStore} — ASYNCHRONOUS (`set`/`get`/`prune`, epoch
|
|
17
|
+
* **seconds**). The cross-node seam (03 §4a, audit G6): a Redis hash or a
|
|
18
|
+
* Postgres row that every node in a saas deployment reads, so a `lock()` on
|
|
19
|
+
* node A closes the token on node B too. `createSessionSigner` consumes THIS
|
|
20
|
+
* one and keeps its own read-through cache so its `verify` stays sync.
|
|
21
|
+
*
|
|
22
|
+
* Before DEDUP-7 these lived in two packages, BOTH exporting a
|
|
23
|
+
* `createMemoryRevocationStore` of a different shape — a live footgun the moment
|
|
24
|
+
* a file imported from the wrong one. They are one file now; `satellite-kit`
|
|
25
|
+
* re-exports the sync half so its callers are unchanged.
|
|
26
|
+
*
|
|
27
|
+
* The bridge between them is {@link createAsyncBackedRevocationStore}: a SYNC
|
|
28
|
+
* store whose durable backing is an async cross-node store, using the same
|
|
29
|
+
* read-through-cache trick the signer does. It is what lets a satellite opt into
|
|
30
|
+
* the cross-node seam without its `isAuthenticated` becoming a promise.
|
|
31
|
+
*/
|
|
32
|
+
import { mkdirSync } from 'node:fs';
|
|
33
|
+
import { dirname } from 'node:path';
|
|
34
|
+
import { Database } from 'bun:sqlite';
|
|
35
|
+
import { assertCcPragmas } from 'cwip/sqlite';
|
|
36
|
+
/**
|
|
37
|
+
* An in-memory sync {@link RevocationStore}.
|
|
38
|
+
*
|
|
39
|
+
* For tests, and for a single-process caller that does not need durability.
|
|
40
|
+
* NOT for a deployed satellite: an in-memory table would be worse than none —
|
|
41
|
+
* sign out, redeploy, and the session works again, which reads as the feature
|
|
42
|
+
* being broken rather than absent. Give a deployed door
|
|
43
|
+
* {@link createSqliteRevocationStore} (durable) or nothing.
|
|
44
|
+
*/
|
|
45
|
+
export function createMemoryRevocationStore() {
|
|
46
|
+
const entries = new Map();
|
|
47
|
+
return {
|
|
48
|
+
isRevoked: (id) => entries.has(id),
|
|
49
|
+
revoke: (id, expiresAtMs) => void entries.set(id, expiresAtMs),
|
|
50
|
+
sweep: (nowMs) => {
|
|
51
|
+
for (const [id, expiresAt] of entries)
|
|
52
|
+
if (expiresAt <= nowMs)
|
|
53
|
+
entries.delete(id);
|
|
54
|
+
},
|
|
55
|
+
size: () => entries.size,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A durable sync {@link RevocationStore} backed by a sqlite file.
|
|
60
|
+
*
|
|
61
|
+
* Because an in-memory table would be worse than none (see above), and the
|
|
62
|
+
* deploy engine persists each release's data directory, so durability costs one
|
|
63
|
+
* file. The table is self-limiting: an entry is meaningless once the session it
|
|
64
|
+
* names would have expired anyway, and `sweep` drops those on logout — rare,
|
|
65
|
+
* cheap, and exactly when the table has just grown, so there is no background
|
|
66
|
+
* timer to own.
|
|
67
|
+
*
|
|
68
|
+
* Its OWN file, not a table in an app's database: keeping a credential surface
|
|
69
|
+
* out of an app's data file bounds the blast radius of a corrupt or deleted
|
|
70
|
+
* file to "everyone signs in again".
|
|
71
|
+
*/
|
|
72
|
+
export function createSqliteRevocationStore(dbPath) {
|
|
73
|
+
if (dbPath !== ':memory:' && !dbPath.startsWith('file:')) {
|
|
74
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
const db = new Database(dbPath, { create: true });
|
|
77
|
+
// Route pragmas through cwip's one controller (applies WAL + busy_timeout +
|
|
78
|
+
// foreign_keys + wal_autocheckpoint and re-reads to prove each took), rather
|
|
79
|
+
// than hand-rolling a lone `journal_mode = WAL` the pragma sweep rejects.
|
|
80
|
+
assertCcPragmas(db);
|
|
81
|
+
db.exec(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS revoked_sessions (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
expires_at INTEGER NOT NULL
|
|
85
|
+
)
|
|
86
|
+
`);
|
|
87
|
+
const has = db.query('SELECT id FROM revoked_sessions WHERE id = ?');
|
|
88
|
+
const insert = db.query('INSERT OR REPLACE INTO revoked_sessions (id, expires_at) VALUES (?, ?)');
|
|
89
|
+
const prune = db.query('DELETE FROM revoked_sessions WHERE expires_at <= ?');
|
|
90
|
+
return {
|
|
91
|
+
isRevoked: (id) => has.get(id) !== null,
|
|
92
|
+
revoke: (id, expiresAtMs) => void insert.run(id, expiresAtMs),
|
|
93
|
+
sweep: (nowMs) => void prune.run(nowMs),
|
|
94
|
+
close: () => db.close(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* An in-memory {@link AsyncRevocationStore}.
|
|
99
|
+
*
|
|
100
|
+
* Two uses: a single-process saas test that wants the seam exercised without a
|
|
101
|
+
* real Redis, and — crucially — the **two-signer replay probe**, where one
|
|
102
|
+
* shared instance is handed to signer A and signer B so "revoke on A, verify on
|
|
103
|
+
* B" becomes provable in one process (V7.3). It is NOT a cross-process store:
|
|
104
|
+
* two OS processes do not share this Map, which is the whole reason a real
|
|
105
|
+
* deployment injects Redis/Postgres instead.
|
|
106
|
+
*
|
|
107
|
+
* (Named `…Async…` to stand apart from the sync {@link createMemoryRevocationStore};
|
|
108
|
+
* before DEDUP-7 both shapes shared the bare name across two packages and that
|
|
109
|
+
* collision was the footgun this file exists to remove.)
|
|
110
|
+
*/
|
|
111
|
+
export function createMemoryAsyncRevocationStore() {
|
|
112
|
+
const revoked = new Map();
|
|
113
|
+
return {
|
|
114
|
+
async set(jti, expEpochSeconds) {
|
|
115
|
+
const existing = revoked.get(jti);
|
|
116
|
+
// Keep the later exp: two nodes revoking the same jti with slightly
|
|
117
|
+
// different clocks must not shorten the window.
|
|
118
|
+
if (existing === undefined || expEpochSeconds > existing)
|
|
119
|
+
revoked.set(jti, expEpochSeconds);
|
|
120
|
+
},
|
|
121
|
+
async get(jti) {
|
|
122
|
+
return revoked.get(jti) ?? null;
|
|
123
|
+
},
|
|
124
|
+
async prune(nowEpochSeconds) {
|
|
125
|
+
let removed = 0;
|
|
126
|
+
for (const [jti, exp] of revoked) {
|
|
127
|
+
if (exp <= nowEpochSeconds) {
|
|
128
|
+
revoked.delete(jti);
|
|
129
|
+
removed += 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return removed;
|
|
133
|
+
},
|
|
134
|
+
size() {
|
|
135
|
+
return revoked.size;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A SYNC {@link RevocationStore} whose durable backing is an async cross-node
|
|
141
|
+
* {@link AsyncRevocationStore} — the bridge that gives a satellite the cross-node
|
|
142
|
+
* seam without its `isAuthenticated` becoming a promise.
|
|
143
|
+
*
|
|
144
|
+
* It is the same read-through-cache trick {@link createSessionSigner} keeps
|
|
145
|
+
* internally, packaged so a plain sync consumer can use it: `revoke` writes
|
|
146
|
+
* through fire-and-forget and caches locally so the id is refused HERE the
|
|
147
|
+
* instant it returns; `isRevoked` answers from the cache and schedules a refresh
|
|
148
|
+
* when the entry is missing or older than `cacheTtlMs`, never blocking. Ids and
|
|
149
|
+
* expiries cross the ms↔seconds boundary here (the sync contract is ms, the
|
|
150
|
+
* async one seconds).
|
|
151
|
+
*
|
|
152
|
+
* {@link flush} settles the fire-and-forget work — for shutdown/handover (a node
|
|
153
|
+
* draining must not lose an unwritten revocation) and for tests that need the
|
|
154
|
+
* cross-node state to have settled.
|
|
155
|
+
*/
|
|
156
|
+
export function createAsyncBackedRevocationStore(backing, options = {}) {
|
|
157
|
+
const now = options.now ?? (() => Date.now());
|
|
158
|
+
const cacheTtlMs = options.cacheTtlMs ?? 5000;
|
|
159
|
+
const onError = options.onError ?? (() => undefined);
|
|
160
|
+
/** id → the last backing verdict (exp in ms, or null) and when it was read. */
|
|
161
|
+
const cache = new Map();
|
|
162
|
+
const pending = new Set();
|
|
163
|
+
/** Reads in flight per id, so a burst of `isRevoked` coalesces into one read. */
|
|
164
|
+
const inflightReads = new Map();
|
|
165
|
+
const track = (work) => {
|
|
166
|
+
pending.add(work);
|
|
167
|
+
void work.finally(() => pending.delete(work));
|
|
168
|
+
};
|
|
169
|
+
const refresh = (id) => {
|
|
170
|
+
if (inflightReads.has(id))
|
|
171
|
+
return;
|
|
172
|
+
const read = backing
|
|
173
|
+
.get(id)
|
|
174
|
+
.then((expSec) => {
|
|
175
|
+
cache.set(id, { expMs: expSec === null ? null : expSec * 1000, readAtMs: now() });
|
|
176
|
+
})
|
|
177
|
+
.catch((error) => onError('get', error))
|
|
178
|
+
.finally(() => inflightReads.delete(id));
|
|
179
|
+
inflightReads.set(id, read);
|
|
180
|
+
track(read);
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
isRevoked(id) {
|
|
184
|
+
const cached = cache.get(id);
|
|
185
|
+
if (cached === undefined || now() - cached.readAtMs >= cacheTtlMs)
|
|
186
|
+
refresh(id);
|
|
187
|
+
// A fresh cached exp still in the future ⇒ revoked. An expired cached exp
|
|
188
|
+
// is treated as not-revoked: the token's own expiry already refuses it, so
|
|
189
|
+
// the denylist has nothing left to add.
|
|
190
|
+
return cached?.expMs != null && cached.expMs > now();
|
|
191
|
+
},
|
|
192
|
+
revoke(id, expiresAtMs) {
|
|
193
|
+
// Cache locally FIRST so this node refuses the id the instant this returns,
|
|
194
|
+
// regardless of whether the write-through has propagated yet.
|
|
195
|
+
cache.set(id, { expMs: expiresAtMs, readAtMs: now() });
|
|
196
|
+
track(backing.set(id, Math.ceil(expiresAtMs / 1000)).catch((error) => onError('set', error)));
|
|
197
|
+
},
|
|
198
|
+
sweep(nowMs) {
|
|
199
|
+
for (const [id, cached] of cache)
|
|
200
|
+
if (cached.expMs !== null && cached.expMs <= nowMs)
|
|
201
|
+
cache.delete(id);
|
|
202
|
+
track(backing.prune(Math.floor(nowMs / 1000)).catch((error) => onError('prune', error)));
|
|
203
|
+
},
|
|
204
|
+
async flush() {
|
|
205
|
+
// Settle in-flight work, then whatever it scheduled (a read can resolve
|
|
206
|
+
// after we snapshot), so the caller sees a fully-quiesced state.
|
|
207
|
+
while (pending.size > 0)
|
|
208
|
+
await Promise.allSettled([...pending]);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The production session signer (02 §4.2) — Ed25519-signed `CCUserClaims` plus
|
|
3
|
+
* the jti denylist that makes `lock()` mean something.
|
|
4
|
+
*
|
|
5
|
+
* The verify harness ships an HMAC stand-in because a probe suite tests the
|
|
6
|
+
* DECISION table (signature validity, expiry, revocation), not the algorithm.
|
|
7
|
+
* This is the real one, and the differences are the parts a stand-in cannot
|
|
8
|
+
* model:
|
|
9
|
+
*
|
|
10
|
+
* * **Asymmetric.** Only the server that holds the private key can mint a
|
|
11
|
+
* session. A verifier can be handed the public key alone, so a read-only
|
|
12
|
+
* replica or an edge worker can verify without being able to forge.
|
|
13
|
+
* * **Revocation is bounded, not unbounded.** A denylist that only ever grows
|
|
14
|
+
* is a memory leak wearing a security hat. An entry is dropped once its
|
|
15
|
+
* token would have expired anyway, because after `exp` the signature check
|
|
16
|
+
* already refuses it.
|
|
17
|
+
* * **Expiry before revocation.** An expired token is expired whether or not
|
|
18
|
+
* anyone bothered to revoke it — checking in the other order makes the
|
|
19
|
+
* denylist load-bearing for a case it does not need to cover.
|
|
20
|
+
*/
|
|
21
|
+
import { type KeyObject } from 'node:crypto';
|
|
22
|
+
import { type CCUserClaims } from 'cursedbelt-core/session/claims';
|
|
23
|
+
import type { GuardVerifier } from './verifier';
|
|
24
|
+
import type { AsyncRevocationStore } from './revocationStore';
|
|
25
|
+
export interface SessionSignerOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Ed25519 private key (PEM/DER/`KeyObject`). Omitted ⇒ an ephemeral pair is
|
|
28
|
+
* generated, which is correct ONLY for a single-process dev server: every
|
|
29
|
+
* restart invalidates every session, and a second process cannot verify.
|
|
30
|
+
*/
|
|
31
|
+
privateKey?: KeyObject | string | Buffer;
|
|
32
|
+
/** Public half, when the verifier is separate from the minter. */
|
|
33
|
+
publicKey?: KeyObject | string | Buffer;
|
|
34
|
+
/** Default session lifetime. `guard.features.sessionTtlMinutes` feeds this. */
|
|
35
|
+
ttlSeconds?: number;
|
|
36
|
+
/** Clock injection for deterministic tests. */
|
|
37
|
+
now?: () => number;
|
|
38
|
+
/** Id generator for `jti`. */
|
|
39
|
+
mintJti?: () => string;
|
|
40
|
+
/**
|
|
41
|
+
* The cross-node denylist (03 §4a). Absent ⇒ the in-process `Map` is the whole
|
|
42
|
+
* story, which is correct for the local profile. Present (saas) ⇒ a `revoke`
|
|
43
|
+
* ALSO writes through to the shared store, and `verify` consults it for jtis
|
|
44
|
+
* revoked on OTHER nodes — so `lock()` on node A closes the token everywhere,
|
|
45
|
+
* not just where the request landed.
|
|
46
|
+
*/
|
|
47
|
+
revocationStore?: AsyncRevocationStore;
|
|
48
|
+
/**
|
|
49
|
+
* How long a store verdict is trusted before `verify` re-reads it (ms). This is
|
|
50
|
+
* the cross-node revocation LATENCY BOUND: a jti revoked on another node is
|
|
51
|
+
* seen here within this window (plus the store's own read latency), because
|
|
52
|
+
* `verify` stays synchronous — it answers from the cache and refreshes it off
|
|
53
|
+
* the hot path rather than awaiting a network round trip on every route.
|
|
54
|
+
* Default 5000 (03 §4a's ≤5s). Ignored when no `revocationStore` is set.
|
|
55
|
+
*/
|
|
56
|
+
revocationCacheTtlMs?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Reports a store read/write that failed. A failed write-through means a
|
|
59
|
+
* revocation did not reach other nodes — the local Map still refuses the token
|
|
60
|
+
* HERE, but the host needs to know propagation failed (and may retry). Default:
|
|
61
|
+
* swallow, because a signer must never throw out of `verify`/`revoke` on the
|
|
62
|
+
* request path.
|
|
63
|
+
*/
|
|
64
|
+
onRevocationStoreError?: (op: 'get' | 'set' | 'prune', error: unknown) => void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One live session this signer minted (02 §4) — the `(userId, tenantId,
|
|
68
|
+
* deviceLabel)` association the spec asks for, keyed by `jti`.
|
|
69
|
+
*
|
|
70
|
+
* The device label is deliberately NOT a claim: `CCUserClaimsSchema` is
|
|
71
|
+
* `.strict()`, and widening the SIGNED payload with a human-readable string a
|
|
72
|
+
* client supplies would put attacker-controlled text inside the token every
|
|
73
|
+
* verifier parses. It is display metadata for "your devices", so it lives here,
|
|
74
|
+
* server-side, beside the jti it describes.
|
|
75
|
+
*/
|
|
76
|
+
export interface SessionRecord {
|
|
77
|
+
jti: string;
|
|
78
|
+
userId: string;
|
|
79
|
+
tenantId: string;
|
|
80
|
+
/** What the client called itself (`x-cc-device`), if anything. */
|
|
81
|
+
deviceLabel?: string;
|
|
82
|
+
/** Epoch SECONDS, matching `exp`'s unit. */
|
|
83
|
+
issuedAt: number;
|
|
84
|
+
exp: number;
|
|
85
|
+
}
|
|
86
|
+
/** Non-claim session metadata recorded at mint time (02 §4). */
|
|
87
|
+
export interface SessionMintMeta {
|
|
88
|
+
/** Free-text device name for the sessions panel. Never signed, never trusted. */
|
|
89
|
+
deviceLabel?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface SessionSigner extends GuardVerifier {
|
|
92
|
+
/** Mint a signed session token. `exp`/`jti` are stamped here, never by a caller. */
|
|
93
|
+
mint(claims: Omit<CCUserClaims, 'exp' | 'jti'> & Partial<Pick<CCUserClaims, 'exp' | 'jti'>>, meta?: SessionMintMeta): {
|
|
94
|
+
token: string;
|
|
95
|
+
claims: CCUserClaims;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Revoke every live session this signer minted for `userId`, returning the
|
|
99
|
+
* jtis it closed. The primitive behind a role/permission change taking effect
|
|
100
|
+
* NOW (02 §3 — authorization is decided from the token's claims, so a grant
|
|
101
|
+
* change that leaves the old token alive is a grant change that did not
|
|
102
|
+
* happen) and behind "log out everywhere" (02 §4).
|
|
103
|
+
*
|
|
104
|
+
* Node-local by construction: it can only enumerate what THIS process minted.
|
|
105
|
+
* Each revocation still writes through to the shared `revocationStore`, so the
|
|
106
|
+
* jtis it does know about die fleet-wide; a session minted on another node is
|
|
107
|
+
* closed when that node calls this (or by its own next login).
|
|
108
|
+
*/
|
|
109
|
+
revokeUser(userId: string): readonly string[];
|
|
110
|
+
/**
|
|
111
|
+
* The live sessions this signer minted for `userId`, newest first — the
|
|
112
|
+
* "your devices" list (02 §4). Node-local for the same reason `revokeUser` is:
|
|
113
|
+
* a process can only enumerate what it minted. Expired and revoked entries are
|
|
114
|
+
* dropped, so what it returns is what a request would still be accepted on.
|
|
115
|
+
*/
|
|
116
|
+
sessionsOf(userId: string): readonly SessionRecord[];
|
|
117
|
+
/** Revoked-but-not-yet-expired jtis. Exposed for the shutdown/handover path. */
|
|
118
|
+
activeRevocations(): readonly string[];
|
|
119
|
+
/** The public half, for a verifier in another process. */
|
|
120
|
+
publicKeyPem(): string;
|
|
121
|
+
/**
|
|
122
|
+
* Settle every pending revocation-store read and write, then prune the store.
|
|
123
|
+
*
|
|
124
|
+
* The store writes on `revoke` and the read-through refreshes on `verify` are
|
|
125
|
+
* fire-and-forget so neither blocks the request path. This awaits them — for
|
|
126
|
+
* the shutdown/handover path (a node draining must not lose an unwritten
|
|
127
|
+
* revocation), for the two-signer replay probe, and for any test that needs the
|
|
128
|
+
* cross-node state to have settled. A no-op when no `revocationStore` is wired.
|
|
129
|
+
*/
|
|
130
|
+
flushRevocations(): Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
export declare const createSessionSigner: (options?: SessionSignerOptions) => SessionSigner;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The production session signer (02 §4.2) — Ed25519-signed `CCUserClaims` plus
|
|
3
|
+
* the jti denylist that makes `lock()` mean something.
|
|
4
|
+
*
|
|
5
|
+
* The verify harness ships an HMAC stand-in because a probe suite tests the
|
|
6
|
+
* DECISION table (signature validity, expiry, revocation), not the algorithm.
|
|
7
|
+
* This is the real one, and the differences are the parts a stand-in cannot
|
|
8
|
+
* model:
|
|
9
|
+
*
|
|
10
|
+
* * **Asymmetric.** Only the server that holds the private key can mint a
|
|
11
|
+
* session. A verifier can be handed the public key alone, so a read-only
|
|
12
|
+
* replica or an edge worker can verify without being able to forge.
|
|
13
|
+
* * **Revocation is bounded, not unbounded.** A denylist that only ever grows
|
|
14
|
+
* is a memory leak wearing a security hat. An entry is dropped once its
|
|
15
|
+
* token would have expired anyway, because after `exp` the signature check
|
|
16
|
+
* already refuses it.
|
|
17
|
+
* * **Expiry before revocation.** An expired token is expired whether or not
|
|
18
|
+
* anyone bothered to revoke it — checking in the other order makes the
|
|
19
|
+
* denylist load-bearing for a case it does not need to cover.
|
|
20
|
+
*/
|
|
21
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto';
|
|
22
|
+
import { CCUserClaimsSchema } from 'cursedbelt-core/session/claims';
|
|
23
|
+
import { AppError } from '../errors';
|
|
24
|
+
const b64url = (input) => Buffer.from(input).toString('base64url');
|
|
25
|
+
/** Ceiling on a client-supplied device label — display text, not a payload. */
|
|
26
|
+
const MAX_DEVICE_LABEL = 64;
|
|
27
|
+
let jtiSequence = 0;
|
|
28
|
+
const defaultJti = () => {
|
|
29
|
+
jtiSequence += 1;
|
|
30
|
+
return `jti_${Date.now().toString(36)}_${jtiSequence.toString(36)}`;
|
|
31
|
+
};
|
|
32
|
+
export const createSessionSigner = (options = {}) => {
|
|
33
|
+
const now = options.now ?? (() => Date.now());
|
|
34
|
+
const mintJti = options.mintJti ?? defaultJti;
|
|
35
|
+
const ttlSeconds = options.ttlSeconds ?? 30 * 60;
|
|
36
|
+
let privateKey = null;
|
|
37
|
+
let publicKey;
|
|
38
|
+
if (options.privateKey !== undefined) {
|
|
39
|
+
privateKey =
|
|
40
|
+
typeof options.privateKey === 'string' || Buffer.isBuffer(options.privateKey)
|
|
41
|
+
? createPrivateKey(options.privateKey)
|
|
42
|
+
: options.privateKey;
|
|
43
|
+
publicKey =
|
|
44
|
+
options.publicKey === undefined
|
|
45
|
+
? createPublicKey(privateKey)
|
|
46
|
+
: typeof options.publicKey === 'string' || Buffer.isBuffer(options.publicKey)
|
|
47
|
+
? createPublicKey(options.publicKey)
|
|
48
|
+
: options.publicKey;
|
|
49
|
+
}
|
|
50
|
+
else if (options.publicKey !== undefined) {
|
|
51
|
+
publicKey =
|
|
52
|
+
typeof options.publicKey === 'string' || Buffer.isBuffer(options.publicKey)
|
|
53
|
+
? createPublicKey(options.publicKey)
|
|
54
|
+
: options.publicKey;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const pair = generateKeyPairSync('ed25519');
|
|
58
|
+
privateKey = pair.privateKey;
|
|
59
|
+
publicKey = pair.publicKey;
|
|
60
|
+
}
|
|
61
|
+
/** jti → the epoch-seconds `exp` after which the entry is pointless. */
|
|
62
|
+
const revoked = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* Live jtis this signer minted, per user — the index behind
|
|
65
|
+
* {@link SessionSigner.revokeUser} (02 §3's role change, 02 §4's "log out
|
|
66
|
+
* everywhere"). Deliberately NOT the cross-node picture: a node can only
|
|
67
|
+
* enumerate what it minted, so under saas the authority for "is this token
|
|
68
|
+
* dead" stays the shared `revocationStore` that `revokeUser` writes through
|
|
69
|
+
* to. Entries are dropped as they expire (`pruneMinted`), so this is bounded
|
|
70
|
+
* by live sessions, not by all sessions ever.
|
|
71
|
+
*/
|
|
72
|
+
const mintedByUser = new Map();
|
|
73
|
+
/** jti → its minter, so a revoke can drop the record without scanning users. */
|
|
74
|
+
const userByJti = new Map();
|
|
75
|
+
const rememberMinted = (record) => {
|
|
76
|
+
const forUser = mintedByUser.get(record.userId) ?? new Map();
|
|
77
|
+
forUser.set(record.jti, record);
|
|
78
|
+
mintedByUser.set(record.userId, forUser);
|
|
79
|
+
userByJti.set(record.jti, record.userId);
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Drop a jti from the live index. Called on every revoke, so a revoked session
|
|
83
|
+
* stops being a live one for BOTH `sessionsOf` (the panel) and `revokeUser`
|
|
84
|
+
* (whose count is then "sessions actually closed", not "jtis ever minted").
|
|
85
|
+
*/
|
|
86
|
+
const forgetMinted = (jti) => {
|
|
87
|
+
const userId = userByJti.get(jti);
|
|
88
|
+
if (userId === undefined)
|
|
89
|
+
return;
|
|
90
|
+
userByJti.delete(jti);
|
|
91
|
+
const forUser = mintedByUser.get(userId);
|
|
92
|
+
forUser?.delete(jti);
|
|
93
|
+
if (forUser && forUser.size === 0)
|
|
94
|
+
mintedByUser.delete(userId);
|
|
95
|
+
};
|
|
96
|
+
const pruneMinted = () => {
|
|
97
|
+
const cutoff = Math.floor(now() / 1000);
|
|
98
|
+
for (const [userId, sessions] of mintedByUser) {
|
|
99
|
+
for (const [jti, record] of sessions)
|
|
100
|
+
if (record.exp <= cutoff) {
|
|
101
|
+
sessions.delete(jti);
|
|
102
|
+
userByJti.delete(jti);
|
|
103
|
+
}
|
|
104
|
+
if (sessions.size === 0)
|
|
105
|
+
mintedByUser.delete(userId);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
// ── the cross-node denylist (03 §4a) ────────────────────────────────────────
|
|
109
|
+
const store = options.revocationStore;
|
|
110
|
+
const cacheTtlMs = options.revocationCacheTtlMs ?? 5000;
|
|
111
|
+
const onStoreError = options.onRevocationStoreError ?? (() => undefined);
|
|
112
|
+
/** jti → the last store verdict and when it was read, so `verify` stays sync. */
|
|
113
|
+
const storeCache = new Map();
|
|
114
|
+
/** In-flight async work, so `flushRevocations` (and shutdown) can await it. */
|
|
115
|
+
const pending = new Set();
|
|
116
|
+
/** Reads in flight per jti, so a burst of verifies coalesces into one store read. */
|
|
117
|
+
const inflightReads = new Map();
|
|
118
|
+
const track = (work) => {
|
|
119
|
+
pending.add(work);
|
|
120
|
+
void work.finally(() => pending.delete(work));
|
|
121
|
+
};
|
|
122
|
+
/** Read the store for `jti` off the hot path and fold the verdict into the cache. */
|
|
123
|
+
const refreshFromStore = (jti) => {
|
|
124
|
+
if (!store || inflightReads.has(jti))
|
|
125
|
+
return;
|
|
126
|
+
const read = store
|
|
127
|
+
.get(jti)
|
|
128
|
+
.then((exp) => {
|
|
129
|
+
storeCache.set(jti, { exp, readAtMs: now() });
|
|
130
|
+
// A confirmed revocation is promoted into the authoritative Map so every
|
|
131
|
+
// later verify is instant and `activeRevocations()` (the handover set)
|
|
132
|
+
// includes jtis this node learned about from its peers.
|
|
133
|
+
if (exp !== null) {
|
|
134
|
+
revoked.set(jti, exp);
|
|
135
|
+
// A peer revoked it, so it is no longer a live session here either.
|
|
136
|
+
forgetMinted(jti);
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.catch((error) => onStoreError('get', error))
|
|
140
|
+
.finally(() => inflightReads.delete(jti));
|
|
141
|
+
inflightReads.set(jti, read);
|
|
142
|
+
track(read);
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Is `jti` revoked, per the shared store? Answered synchronously from the cache;
|
|
146
|
+
* a stale or missing entry schedules a refresh but does NOT block. The bound on
|
|
147
|
+
* how stale the answer can be is `cacheTtlMs` — the cross-node latency the seam
|
|
148
|
+
* documents.
|
|
149
|
+
*/
|
|
150
|
+
const storeSaysRevoked = (jti) => {
|
|
151
|
+
if (!store)
|
|
152
|
+
return false;
|
|
153
|
+
const cached = storeCache.get(jti);
|
|
154
|
+
if (cached === undefined || now() - cached.readAtMs >= cacheTtlMs)
|
|
155
|
+
refreshFromStore(jti);
|
|
156
|
+
// A fresh cached exp that is still in the future ⇒ revoked. An expired cached
|
|
157
|
+
// exp is treated as not-revoked: the signature check already refuses the
|
|
158
|
+
// token past `exp`, so the denylist has nothing left to add.
|
|
159
|
+
return cached?.exp != null && cached.exp * 1000 > now();
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* The one revocation write path — a free function rather than a method so
|
|
163
|
+
* `revokeUser` cannot depend on `this` (a destructured `const { revokeUser } =
|
|
164
|
+
* signer` would silently lose it, and losing a revocation is the one bug this
|
|
165
|
+
* seam must not have).
|
|
166
|
+
*/
|
|
167
|
+
const revokeJti = (jti) => {
|
|
168
|
+
pruneRevocations();
|
|
169
|
+
// An unknown jti is still recorded: `lock()` may revoke a token this
|
|
170
|
+
// process never minted (another node in the fleet did), and refusing to
|
|
171
|
+
// remember it would let that token keep working here.
|
|
172
|
+
const exp = Math.floor(now() / 1000) + ttlSeconds;
|
|
173
|
+
revoked.set(jti, exp);
|
|
174
|
+
forgetMinted(jti);
|
|
175
|
+
// Write through so every OTHER node sees it too. Fire-and-forget: `revoke`
|
|
176
|
+
// is called on the request path (a `lock`), and the local Map already
|
|
177
|
+
// refuses the token HERE the instant this returns — propagation is what the
|
|
178
|
+
// store adds, and `flushRevocations`/the error hook cover its durability.
|
|
179
|
+
if (store) {
|
|
180
|
+
storeCache.set(jti, { exp, readAtMs: now() });
|
|
181
|
+
track(store.set(jti, exp).catch((error) => onStoreError('set', error)));
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const pruneRevocations = () => {
|
|
185
|
+
const cutoff = Math.floor(now() / 1000);
|
|
186
|
+
for (const [jti, exp] of revoked)
|
|
187
|
+
if (exp <= cutoff)
|
|
188
|
+
revoked.delete(jti);
|
|
189
|
+
for (const [jti, cached] of storeCache)
|
|
190
|
+
if (cached.exp !== null && cached.exp <= cutoff)
|
|
191
|
+
storeCache.delete(jti);
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
mint(input, meta) {
|
|
195
|
+
if (privateKey === null)
|
|
196
|
+
throw new AppError('This session signer holds only a public key and cannot mint tokens.', {
|
|
197
|
+
code: 'GUARD_SIGNER_READONLY',
|
|
198
|
+
status: 500,
|
|
199
|
+
category: 'security',
|
|
200
|
+
isOperational: false,
|
|
201
|
+
});
|
|
202
|
+
const claims = CCUserClaimsSchema.parse({
|
|
203
|
+
...input,
|
|
204
|
+
exp: input.exp ?? Math.floor(now() / 1000) + ttlSeconds,
|
|
205
|
+
jti: input.jti ?? mintJti(),
|
|
206
|
+
});
|
|
207
|
+
const payload = b64url(JSON.stringify(claims));
|
|
208
|
+
const signature = b64url(sign(null, Buffer.from(payload), privateKey));
|
|
209
|
+
pruneMinted();
|
|
210
|
+
rememberMinted({
|
|
211
|
+
jti: claims.jti,
|
|
212
|
+
userId: claims.userId,
|
|
213
|
+
tenantId: claims.tenantId,
|
|
214
|
+
// A label is truncated rather than rejected: it is display text, and a
|
|
215
|
+
// login must never fail because a browser sent a long device string.
|
|
216
|
+
...(meta?.deviceLabel ? { deviceLabel: meta.deviceLabel.slice(0, MAX_DEVICE_LABEL) } : {}),
|
|
217
|
+
issuedAt: Math.floor(now() / 1000),
|
|
218
|
+
exp: claims.exp,
|
|
219
|
+
});
|
|
220
|
+
return { token: `${payload}.${signature}`, claims };
|
|
221
|
+
},
|
|
222
|
+
verify(token) {
|
|
223
|
+
if (!token)
|
|
224
|
+
return { ok: false, failure: 'missing' };
|
|
225
|
+
const dot = token.indexOf('.');
|
|
226
|
+
if (dot <= 0 || dot === token.length - 1)
|
|
227
|
+
return { ok: false, failure: 'malformed' };
|
|
228
|
+
const payload = token.slice(0, dot);
|
|
229
|
+
const signature = token.slice(dot + 1);
|
|
230
|
+
let valid = false;
|
|
231
|
+
try {
|
|
232
|
+
valid = verify(null, Buffer.from(payload), publicKey, Buffer.from(signature, 'base64url'));
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// A signature that is not even decodable is malformed, not merely wrong.
|
|
236
|
+
return { ok: false, failure: 'malformed' };
|
|
237
|
+
}
|
|
238
|
+
if (!valid)
|
|
239
|
+
return { ok: false, failure: 'invalid-signature' };
|
|
240
|
+
let parsedJson;
|
|
241
|
+
try {
|
|
242
|
+
parsedJson = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return { ok: false, failure: 'malformed' };
|
|
246
|
+
}
|
|
247
|
+
const parsed = CCUserClaimsSchema.safeParse(parsedJson);
|
|
248
|
+
// `.strict()`: an unknown claim key is a forged or version-skewed token,
|
|
249
|
+
// and silently ignoring it is how a `permissions` typo becomes a grant.
|
|
250
|
+
if (!parsed.success)
|
|
251
|
+
return { ok: false, failure: 'malformed' };
|
|
252
|
+
if (parsed.data.exp * 1000 <= now())
|
|
253
|
+
return { ok: false, failure: 'expired' };
|
|
254
|
+
// Local Map first (this node's own revokes, and peers' revokes it has
|
|
255
|
+
// already learned) — always authoritative and free. Then the shared store,
|
|
256
|
+
// consulted through the sync cache, for a jti revoked on another node.
|
|
257
|
+
if (revoked.has(parsed.data.jti) || storeSaysRevoked(parsed.data.jti))
|
|
258
|
+
return { ok: false, failure: 'revoked' };
|
|
259
|
+
return { ok: true, claims: parsed.data };
|
|
260
|
+
},
|
|
261
|
+
revoke(jti) {
|
|
262
|
+
revokeJti(jti);
|
|
263
|
+
},
|
|
264
|
+
revokeUser(userId) {
|
|
265
|
+
pruneMinted();
|
|
266
|
+
const jtis = [...(mintedByUser.get(userId)?.keys() ?? [])];
|
|
267
|
+
for (const jti of jtis)
|
|
268
|
+
revokeJti(jti);
|
|
269
|
+
mintedByUser.delete(userId);
|
|
270
|
+
return jtis;
|
|
271
|
+
},
|
|
272
|
+
sessionsOf(userId) {
|
|
273
|
+
pruneMinted();
|
|
274
|
+
pruneRevocations();
|
|
275
|
+
return [...(mintedByUser.get(userId)?.values() ?? [])]
|
|
276
|
+
// A revoked-but-unexpired jti is a dead session; listing it would invite
|
|
277
|
+
// "log this device out" on a device that is already out.
|
|
278
|
+
.filter((record) => !revoked.has(record.jti) && !storeSaysRevoked(record.jti))
|
|
279
|
+
.sort((left, right) => right.issuedAt - left.issuedAt);
|
|
280
|
+
},
|
|
281
|
+
isRevoked(jti) {
|
|
282
|
+
pruneRevocations();
|
|
283
|
+
return revoked.has(jti) || storeSaysRevoked(jti);
|
|
284
|
+
},
|
|
285
|
+
activeRevocations() {
|
|
286
|
+
pruneRevocations();
|
|
287
|
+
return [...revoked.keys()];
|
|
288
|
+
},
|
|
289
|
+
async flushRevocations() {
|
|
290
|
+
// Settle in-flight work, then whatever it scheduled (a read can resolve
|
|
291
|
+
// after we snapshot), so the caller sees a fully-quiesced state.
|
|
292
|
+
while (pending.size > 0)
|
|
293
|
+
await Promise.allSettled([...pending]);
|
|
294
|
+
if (store)
|
|
295
|
+
track(store.prune(Math.floor(now() / 1000)).catch((error) => onStoreError('prune', error)));
|
|
296
|
+
while (pending.size > 0)
|
|
297
|
+
await Promise.allSettled([...pending]);
|
|
298
|
+
},
|
|
299
|
+
publicKeyPem() {
|
|
300
|
+
return publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CCUserClaims } from 'cursedbelt-core/session/claims';
|
|
2
|
+
/** Why a presented session token was refused. Mapped to status by `guardVerify`. */
|
|
3
|
+
export type GuardFailure = 'missing' | 'malformed' | 'invalid-signature' | 'expired' | 'revoked';
|
|
4
|
+
export type GuardVerification = {
|
|
5
|
+
ok: true;
|
|
6
|
+
claims: CCUserClaims;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
failure: GuardFailure;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Per-request token verification (02 §4.2). The wire only signals; this is what
|
|
13
|
+
* decides. `verify` is called on every private or mutating route — a pane's
|
|
14
|
+
* unlock is never cached for another pane's request.
|
|
15
|
+
*/
|
|
16
|
+
export interface GuardVerifier {
|
|
17
|
+
verify(token: string | undefined): GuardVerification;
|
|
18
|
+
/** Revoke a jti until its `exp` — what lock does, across every tab. */
|
|
19
|
+
revoke(jti: string): void;
|
|
20
|
+
isRevoked(jti: string): boolean;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|