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,57 @@
|
|
|
1
|
+
import { validationEnvelope } from '../errors';
|
|
2
|
+
/**
|
|
3
|
+
* Read the value a target validates against. The body targets (`json`/`form`) prefer
|
|
4
|
+
* the ctgr-decoded body so a tunnelled mutation validates its real payload; the
|
|
5
|
+
* URL-bound targets (`query`/`param`) are never tunnelled (GETs go direct) and read
|
|
6
|
+
* from the live request. A query object collapses single-valued keys to a string so
|
|
7
|
+
* `z.string()`/`z.coerce.number()` field schemas see a scalar (repeated keys stay an
|
|
8
|
+
* array) — matching Hono's own validator semantics.
|
|
9
|
+
*/
|
|
10
|
+
async function readTarget(c, target) {
|
|
11
|
+
if (target === 'json' || target === 'form') {
|
|
12
|
+
const decoded = c.get('ctgrDecoded');
|
|
13
|
+
if (decoded)
|
|
14
|
+
return decoded.body;
|
|
15
|
+
if (target === 'json')
|
|
16
|
+
return c.req.json().catch(() => undefined);
|
|
17
|
+
return c.req.parseBody();
|
|
18
|
+
}
|
|
19
|
+
if (target === 'param')
|
|
20
|
+
return c.req.param();
|
|
21
|
+
return Object.fromEntries(Object.entries(c.req.queries()).map(([key, values]) => values.length === 1 ? [key, values[0]] : [key, values]));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate a request `target` against a zod `schema`. Mount per route ahead of the
|
|
25
|
+
* handler:
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* register('post', '/notes', zValidator(CreateNoteSchema), (c) => {
|
|
29
|
+
* const body = c.req.valid('json'); // typed z.infer<typeof CreateNoteSchema>
|
|
30
|
+
* // …
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* On failure responds `400` with the canonical envelope (field errors under
|
|
35
|
+
* `context.issues`); never throws, so it produces the same body whether or not the
|
|
36
|
+
* app mounts the {@link errorEnvelope} `onError` handler. On success the parsed value
|
|
37
|
+
* is available as both `c.req.valid(target)` and `c.get('validatedBody')`.
|
|
38
|
+
*/
|
|
39
|
+
export function zValidator(schema, target = 'json') {
|
|
40
|
+
const handler = async (c, next) => {
|
|
41
|
+
const raw = await readTarget(c, target);
|
|
42
|
+
const result = schema.safeParse(raw);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
return c.json(validationEnvelope(result.error), 400);
|
|
45
|
+
}
|
|
46
|
+
// Expose the parsed value both ways (typed `c.req.valid` + convenience getter).
|
|
47
|
+
c.req.addValidatedData(target, result.data);
|
|
48
|
+
c.set('validatedBody', result.data);
|
|
49
|
+
await next();
|
|
50
|
+
};
|
|
51
|
+
// The body is written against the concrete `CursedbeltEnv` (so the ctgr/validatedBody
|
|
52
|
+
// accessors type-check); the public signature is the generic, schema-typed
|
|
53
|
+
// MiddlewareHandler that flows `c.req.valid(target)` into the handler. Hono's Context
|
|
54
|
+
// is invariant in its Env, so this widening goes through `unknown` — the same cast
|
|
55
|
+
// a hand-rolled typed middleware (e.g. @hono/zod-validator) performs.
|
|
56
|
+
return handler;
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type { Migration } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* The versioned SQLite migration runner — a tiny `_migrations`-sentinel runner, NOT
|
|
5
|
+
* a schema-first ORM. It replaces the old scattered guarded-`ALTER` /
|
|
6
|
+
* `addColumnIfMissing` pattern with ordered, once-only migrations whose names are
|
|
7
|
+
* recorded once applied. Business tables stay raw (`db.query` + `data JSON`); only
|
|
8
|
+
* the *plumbing* tables get Kysely types (see `../db/kysely`).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Apply the recommended connection PRAGMAs. **Call this BEFORE {@link runMigrations}
|
|
12
|
+
* and OUTSIDE any transaction** — `PRAGMA foreign_keys` is silently ignored inside a
|
|
13
|
+
* transaction, and the runner wraps each migration in one. Idempotent; safe on boot.
|
|
14
|
+
*
|
|
15
|
+
* This is now a straight alias of cwip's `applyCcPragmas`: the CC storage tier pins
|
|
16
|
+
* ONE pragma profile monorepo-wide (`CC_PRAGMAS` — WAL + NORMAL + busy_timeout 5000 +
|
|
17
|
+
* foreign_keys ON + wal_autocheckpoint 1000), and the belt-local copy that used to
|
|
18
|
+
* live here was the second implementation the spec forbids. The per-pragma
|
|
19
|
+
* `PragmaOptions` overrides are gone with it — a handle that drifts from the profile
|
|
20
|
+
* is a `pragma-drift` violation, not a configuration choice.
|
|
21
|
+
*
|
|
22
|
+
* @deprecated Prefer cwip's `assertCcPragmas`, which additionally verifies each
|
|
23
|
+
* pragma took and installs the WAL transaction-lifetime guard.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyRecommendedPragmas(db: Database): void;
|
|
26
|
+
/**
|
|
27
|
+
* Run every pending migration exactly once, in array order, each inside its own
|
|
28
|
+
* transaction, recording its `name` in `_migrations`. Returns the names actually
|
|
29
|
+
* applied this call (empty when already up to date — re-running is a no-op).
|
|
30
|
+
*
|
|
31
|
+
* Set the connection PRAGMAs first via {@link applyRecommendedPragmas} (foreign-key
|
|
32
|
+
* enforcement cannot be turned on inside the per-migration transaction).
|
|
33
|
+
*/
|
|
34
|
+
export declare function runMigrations(db: Database, migrations: readonly Migration[]): string[];
|
|
35
|
+
/** Does `table` have a column named `column`? (via `PRAGMA table_info`). */
|
|
36
|
+
export declare function columnExists(db: Database, table: string, column: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Add a column if it is missing (idempotent). Returns true when it was added. A
|
|
39
|
+
* migration helper for the rare evolve-in-place case; the canonical schema is built
|
|
40
|
+
* fresh by `0001_initial`, so new migrations normally `CREATE` rather than `ALTER`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function addColumnIfMissing(db: Database, table: string, column: string, ddl: string): boolean;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { applyCcPragmas } from 'cwip/sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* The versioned SQLite migration runner — a tiny `_migrations`-sentinel runner, NOT
|
|
4
|
+
* a schema-first ORM. It replaces the old scattered guarded-`ALTER` /
|
|
5
|
+
* `addColumnIfMissing` pattern with ordered, once-only migrations whose names are
|
|
6
|
+
* recorded once applied. Business tables stay raw (`db.query` + `data JSON`); only
|
|
7
|
+
* the *plumbing* tables get Kysely types (see `../db/kysely`).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Apply the recommended connection PRAGMAs. **Call this BEFORE {@link runMigrations}
|
|
11
|
+
* and OUTSIDE any transaction** — `PRAGMA foreign_keys` is silently ignored inside a
|
|
12
|
+
* transaction, and the runner wraps each migration in one. Idempotent; safe on boot.
|
|
13
|
+
*
|
|
14
|
+
* This is now a straight alias of cwip's `applyCcPragmas`: the CC storage tier pins
|
|
15
|
+
* ONE pragma profile monorepo-wide (`CC_PRAGMAS` — WAL + NORMAL + busy_timeout 5000 +
|
|
16
|
+
* foreign_keys ON + wal_autocheckpoint 1000), and the belt-local copy that used to
|
|
17
|
+
* live here was the second implementation the spec forbids. The per-pragma
|
|
18
|
+
* `PragmaOptions` overrides are gone with it — a handle that drifts from the profile
|
|
19
|
+
* is a `pragma-drift` violation, not a configuration choice.
|
|
20
|
+
*
|
|
21
|
+
* @deprecated Prefer cwip's `assertCcPragmas`, which additionally verifies each
|
|
22
|
+
* pragma took and installs the WAL transaction-lifetime guard.
|
|
23
|
+
*/
|
|
24
|
+
export function applyRecommendedPragmas(db) {
|
|
25
|
+
applyCcPragmas(db);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Run every pending migration exactly once, in array order, each inside its own
|
|
29
|
+
* transaction, recording its `name` in `_migrations`. Returns the names actually
|
|
30
|
+
* applied this call (empty when already up to date — re-running is a no-op).
|
|
31
|
+
*
|
|
32
|
+
* Set the connection PRAGMAs first via {@link applyRecommendedPragmas} (foreign-key
|
|
33
|
+
* enforcement cannot be turned on inside the per-migration transaction).
|
|
34
|
+
*/
|
|
35
|
+
export function runMigrations(db, migrations) {
|
|
36
|
+
db.run('CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)');
|
|
37
|
+
const applied = new Set(db.query('SELECT name FROM _migrations').all().map((r) => r.name));
|
|
38
|
+
const insert = db.prepare('INSERT INTO _migrations (name, applied_at) VALUES (?, ?)');
|
|
39
|
+
const ran = [];
|
|
40
|
+
for (const m of migrations) {
|
|
41
|
+
if (applied.has(m.name))
|
|
42
|
+
continue;
|
|
43
|
+
const apply = db.transaction(() => {
|
|
44
|
+
m.up(db);
|
|
45
|
+
insert.run(m.name, new Date().toISOString());
|
|
46
|
+
});
|
|
47
|
+
apply();
|
|
48
|
+
ran.push(m.name);
|
|
49
|
+
}
|
|
50
|
+
return ran;
|
|
51
|
+
}
|
|
52
|
+
function quoteIdent(name) {
|
|
53
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
54
|
+
}
|
|
55
|
+
/** Does `table` have a column named `column`? (via `PRAGMA table_info`). */
|
|
56
|
+
export function columnExists(db, table, column) {
|
|
57
|
+
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
58
|
+
return rows.some((r) => r.name === column);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Add a column if it is missing (idempotent). Returns true when it was added. A
|
|
62
|
+
* migration helper for the rare evolve-in-place case; the canonical schema is built
|
|
63
|
+
* fresh by `0001_initial`, so new migrations normally `CREATE` rather than `ALTER`.
|
|
64
|
+
*/
|
|
65
|
+
export function addColumnIfMissing(db, table, column, ddl) {
|
|
66
|
+
if (columnExists(db, table, column))
|
|
67
|
+
return false;
|
|
68
|
+
db.run(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN ${quoteIdent(column)} ${ddl}`);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* One ordered, immutable schema migration. `name` is recorded in the `_migrations`
|
|
4
|
+
* sentinel once applied, so a migration's `up()` runs exactly once across the life
|
|
5
|
+
* of a database. Corrections land as NEW migrations — never edit a shipped one, and
|
|
6
|
+
* never rename one that has been applied (the name is its identity).
|
|
7
|
+
*
|
|
8
|
+
* `up()` runs inside the runner's per-migration transaction; throw to abort + roll
|
|
9
|
+
* back that migration (the sentinel row is not written, so it retries next boot).
|
|
10
|
+
*/
|
|
11
|
+
export interface Migration {
|
|
12
|
+
/** Unique, lexically-ordered name, e.g. `0001_initial`. Immutable once applied. */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/** Apply the migration. Runs inside the runner's transaction. */
|
|
15
|
+
up(db: Database): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const OUTBOUND_URL_BLOCKED = "OUTBOUND_URL_BLOCKED";
|
|
2
|
+
export interface OutboundGuardOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Exact hostnames (case-insensitive) explicitly permitted even if they would
|
|
5
|
+
* otherwise be blocked — for a deliberately-configured internal integration.
|
|
6
|
+
* Empty by default: nothing internal is reachable unless a host opts in.
|
|
7
|
+
*/
|
|
8
|
+
allowHosts?: readonly string[];
|
|
9
|
+
/** Permitted URL schemes. Default `['http:', 'https:']`. */
|
|
10
|
+
allowSchemes?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Structural SSRF guard: parse `rawUrl`, enforce scheme, reject credentials, and
|
|
14
|
+
* refuse any host that is a private/loopback/link-local/reserved IP literal (in
|
|
15
|
+
* any encoding) or a well-known internal hostname. Returns the parsed URL.
|
|
16
|
+
*
|
|
17
|
+
* @throws {AppError} `OUTBOUND_URL_BLOCKED` on any violation.
|
|
18
|
+
*/
|
|
19
|
+
export declare const assertPublicOutboundUrl: (rawUrl: string, options?: OutboundGuardOptions) => URL;
|
|
20
|
+
/**
|
|
21
|
+
* DNS-aware SSRF guard: the structural checks of {@link assertPublicOutboundUrl}
|
|
22
|
+
* PLUS a resolution pass — every address the hostname resolves to is re-checked,
|
|
23
|
+
* so a public name pointing at a private IP is refused. Still racy against an
|
|
24
|
+
* attacker who rebinds between this lookup and the actual connect, which is why
|
|
25
|
+
* the SaaS profile must also enforce network-layer egress; but it closes the
|
|
26
|
+
* naive `evil.example → 169.254.169.254` vector the structural pass cannot see.
|
|
27
|
+
*
|
|
28
|
+
* @throws {AppError} `OUTBOUND_URL_BLOCKED` on any violation.
|
|
29
|
+
*/
|
|
30
|
+
export declare const assertResolvedPublicUrl: (rawUrl: string, options?: OutboundGuardOptions) => Promise<URL>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF guard for server-side outbound HTTP (03 §2.4 H-4 adjudication).
|
|
3
|
+
*
|
|
4
|
+
* The Request Builder send (`/api/system/http`) is server-side BY DESIGN: the
|
|
5
|
+
* pinned CSP's `connect-src` would block a browser-side request to an arbitrary
|
|
6
|
+
* host, so the send is performed by the server to keep the tool working without
|
|
7
|
+
* widening a single header. But moving the send server-side creates a
|
|
8
|
+
* server-side request forgery surface the CSP argument never addressed — an
|
|
9
|
+
* authenticated caller can now make the SERVER fetch:
|
|
10
|
+
*
|
|
11
|
+
* - `http://169.254.169.254/latest/meta-data/…` → cloud metadata → credentials
|
|
12
|
+
* - `http://localhost:4747/…` / `http://127.0.0.1` → the station and sibling apps
|
|
13
|
+
* - `http://10.x` / `172.16.x` / `192.168.x` → anything on the private network
|
|
14
|
+
* - `http://<internal-rds-or-redis-or-admin>` → internal-only services
|
|
15
|
+
*
|
|
16
|
+
* This module is the boundary that refuses those. It is fail-CLOSED and lives in
|
|
17
|
+
* the framework (not in a leaf `sendHttp` impl) so no implementation can forget
|
|
18
|
+
* it: the mixin runs {@link assertPublicOutboundUrl} on the caller-supplied URL
|
|
19
|
+
* BEFORE any impl sees it.
|
|
20
|
+
*
|
|
21
|
+
* Two layers, because neither alone is sufficient:
|
|
22
|
+
* 1. {@link assertPublicOutboundUrl} — synchronous, structural: scheme +
|
|
23
|
+
* credential + literal-IP-range + dangerous-hostname checks. Catches every
|
|
24
|
+
* IP-literal vector (including the decimal/hex/octal encodings that
|
|
25
|
+
* `new URL()` preserves and `fetch` still resolves) and the well-known
|
|
26
|
+
* internal hostnames. This is what the request path calls on every send.
|
|
27
|
+
* 2. {@link assertResolvedPublicUrl} — async, DNS-aware: additionally resolves
|
|
28
|
+
* the hostname and re-checks every resolved address, which is the only
|
|
29
|
+
* defense against a public name that resolves to a private IP (DNS
|
|
30
|
+
* rebinding). Opt-in for impls that can afford the lookup; the SaaS profile
|
|
31
|
+
* must ALSO enforce egress at the network layer, since even this races a
|
|
32
|
+
* rebinding attacker between resolve and connect.
|
|
33
|
+
*/
|
|
34
|
+
import { isIP } from 'node:net';
|
|
35
|
+
import { lookup } from 'node:dns/promises';
|
|
36
|
+
import { AppError } from '../errors';
|
|
37
|
+
export const OUTBOUND_URL_BLOCKED = 'OUTBOUND_URL_BLOCKED';
|
|
38
|
+
const blocked = (reason, context) => new AppError(`Outbound request refused: ${reason}.`, {
|
|
39
|
+
code: OUTBOUND_URL_BLOCKED,
|
|
40
|
+
status: 400,
|
|
41
|
+
category: 'security',
|
|
42
|
+
// Deliberately terse context — do not echo internal topology back.
|
|
43
|
+
context,
|
|
44
|
+
});
|
|
45
|
+
/** Hostnames and suffixes that always resolve to a non-public / internal target. */
|
|
46
|
+
const BLOCKED_HOST_EXACT = new Set(['localhost', 'metadata.google.internal', 'metadata']);
|
|
47
|
+
const BLOCKED_HOST_SUFFIXES = ['.localhost', '.local', '.internal', '.intranet', '.cluster.local', '.svc'];
|
|
48
|
+
/**
|
|
49
|
+
* Parse a hostname into an IPv4 the way a permissive resolver would, covering
|
|
50
|
+
* the encodings SSRF filters classically miss: dotted-decimal (`127.0.0.1`), a
|
|
51
|
+
* bare 32-bit integer (`2130706433`), and hex (`0x7f000001`). Returns the four
|
|
52
|
+
* octets, or `null` when the host is not an IPv4 literal in any of these forms.
|
|
53
|
+
*/
|
|
54
|
+
const looseIpv4Octets = (host) => {
|
|
55
|
+
if (isIP(host) === 4) {
|
|
56
|
+
const parts = host.split('.').map(Number);
|
|
57
|
+
return [parts[0], parts[1], parts[2], parts[3]];
|
|
58
|
+
}
|
|
59
|
+
// Bare integer or hex integer → a single 32-bit value.
|
|
60
|
+
const asInt = /^0x[0-9a-f]+$/i.test(host)
|
|
61
|
+
? Number.parseInt(host, 16)
|
|
62
|
+
: /^[0-9]+$/.test(host)
|
|
63
|
+
? Number.parseInt(host, 10)
|
|
64
|
+
: Number.NaN;
|
|
65
|
+
if (Number.isInteger(asInt) && asInt >= 0 && asInt <= 0xffffffff)
|
|
66
|
+
return [(asInt >>> 24) & 0xff, (asInt >>> 16) & 0xff, (asInt >>> 8) & 0xff, asInt & 0xff];
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
/** Whether an IPv4 (as octets) is in a private, loopback, link-local or reserved range. */
|
|
70
|
+
const isBlockedIpv4 = ([a, b]) => {
|
|
71
|
+
if (a === 0 || a === 10 || a === 127)
|
|
72
|
+
return true; // this-host, private, loopback
|
|
73
|
+
if (a === 169 && b === 254)
|
|
74
|
+
return true; // link-local — INCLUDES 169.254.169.254 metadata
|
|
75
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
76
|
+
return true; // private
|
|
77
|
+
if (a === 192 && b === 168)
|
|
78
|
+
return true; // private
|
|
79
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
80
|
+
return true; // CGNAT (100.64/10)
|
|
81
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
82
|
+
return true; // benchmarking
|
|
83
|
+
if (a >= 224)
|
|
84
|
+
return true; // multicast (224/4) + reserved (240/4) + 255.255.255.255
|
|
85
|
+
return false;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Extract the embedded IPv4 of an IPv4-mapped IPv6 address. `new URL` normalizes
|
|
89
|
+
* `::ffff:127.0.0.1` to the HEX form `::ffff:7f00:1`, so both the dotted and the
|
|
90
|
+
* two-hextet forms have to be handled.
|
|
91
|
+
*/
|
|
92
|
+
const mappedV4Octets = (head) => {
|
|
93
|
+
const rest = /^::ffff:(.+)$/i.exec(head)?.[1];
|
|
94
|
+
if (!rest)
|
|
95
|
+
return null;
|
|
96
|
+
if (rest.includes('.'))
|
|
97
|
+
return looseIpv4Octets(rest);
|
|
98
|
+
const groups = rest.split(':');
|
|
99
|
+
if (groups.length !== 2)
|
|
100
|
+
return null;
|
|
101
|
+
const hi = Number.parseInt(groups[0], 16);
|
|
102
|
+
const lo = Number.parseInt(groups[1], 16);
|
|
103
|
+
if (!Number.isInteger(hi) || !Number.isInteger(lo))
|
|
104
|
+
return null;
|
|
105
|
+
return [(hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff, lo & 0xff];
|
|
106
|
+
};
|
|
107
|
+
/** Whether an IPv6 literal (already `net.isIP`-classified) is non-public. */
|
|
108
|
+
const isBlockedIpv6 = (raw) => {
|
|
109
|
+
const head = raw.toLowerCase().replace(/^\[|\]$/g, '');
|
|
110
|
+
if (head === '::1' || head === '::')
|
|
111
|
+
return true; // loopback, unspecified
|
|
112
|
+
const mapped = mappedV4Octets(head);
|
|
113
|
+
if (mapped && isBlockedIpv4(mapped))
|
|
114
|
+
return true; // IPv4-mapped (`::ffff:…`)
|
|
115
|
+
// fe80::/10 link-local (fe80–febf) and fec0::/10 deprecated site-local (fec0–feff).
|
|
116
|
+
if (/^fe[89ab]/.test(head) || /^fe[c-f]/.test(head))
|
|
117
|
+
return true;
|
|
118
|
+
if (head.startsWith('fc') || head.startsWith('fd'))
|
|
119
|
+
return true; // fc00::/7 unique-local (incl. fd00:ec2::254 metadata)
|
|
120
|
+
if (head.startsWith('ff'))
|
|
121
|
+
return true; // ff00::/8 multicast
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
const isBlockedHostname = (host) => {
|
|
125
|
+
if (BLOCKED_HOST_EXACT.has(host))
|
|
126
|
+
return true;
|
|
127
|
+
return BLOCKED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Structural SSRF guard: parse `rawUrl`, enforce scheme, reject credentials, and
|
|
131
|
+
* refuse any host that is a private/loopback/link-local/reserved IP literal (in
|
|
132
|
+
* any encoding) or a well-known internal hostname. Returns the parsed URL.
|
|
133
|
+
*
|
|
134
|
+
* @throws {AppError} `OUTBOUND_URL_BLOCKED` on any violation.
|
|
135
|
+
*/
|
|
136
|
+
export const assertPublicOutboundUrl = (rawUrl, options = {}) => {
|
|
137
|
+
const allowSchemes = options.allowSchemes ?? ['http:', 'https:'];
|
|
138
|
+
const allowHosts = new Set((options.allowHosts ?? []).map((h) => h.toLowerCase()));
|
|
139
|
+
let url;
|
|
140
|
+
try {
|
|
141
|
+
url = new URL(rawUrl);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw blocked('not a valid absolute URL', {});
|
|
145
|
+
}
|
|
146
|
+
if (!allowSchemes.includes(url.protocol))
|
|
147
|
+
throw blocked(`scheme '${url.protocol}' is not allowed`, { scheme: url.protocol });
|
|
148
|
+
// Credentials in the URL are a redirect/parsing-confusion vector and never
|
|
149
|
+
// legitimate for a tool send.
|
|
150
|
+
if (url.username !== '' || url.password !== '')
|
|
151
|
+
throw blocked('credentials in URL are not allowed', {});
|
|
152
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
153
|
+
if (allowHosts.has(host))
|
|
154
|
+
return url; // explicit host opt-in bypasses the blocks
|
|
155
|
+
const v4 = looseIpv4Octets(host);
|
|
156
|
+
if (v4) {
|
|
157
|
+
if (isBlockedIpv4(v4))
|
|
158
|
+
throw blocked('target resolves to a private or reserved address', { host });
|
|
159
|
+
return url;
|
|
160
|
+
}
|
|
161
|
+
if (isIP(host) === 6) {
|
|
162
|
+
if (isBlockedIpv6(host))
|
|
163
|
+
throw blocked('target resolves to a private or reserved address', { host });
|
|
164
|
+
return url;
|
|
165
|
+
}
|
|
166
|
+
if (isBlockedHostname(host))
|
|
167
|
+
throw blocked('target host is internal-only', { host });
|
|
168
|
+
return url;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* DNS-aware SSRF guard: the structural checks of {@link assertPublicOutboundUrl}
|
|
172
|
+
* PLUS a resolution pass — every address the hostname resolves to is re-checked,
|
|
173
|
+
* so a public name pointing at a private IP is refused. Still racy against an
|
|
174
|
+
* attacker who rebinds between this lookup and the actual connect, which is why
|
|
175
|
+
* the SaaS profile must also enforce network-layer egress; but it closes the
|
|
176
|
+
* naive `evil.example → 169.254.169.254` vector the structural pass cannot see.
|
|
177
|
+
*
|
|
178
|
+
* @throws {AppError} `OUTBOUND_URL_BLOCKED` on any violation.
|
|
179
|
+
*/
|
|
180
|
+
export const assertResolvedPublicUrl = async (rawUrl, options = {}) => {
|
|
181
|
+
const url = assertPublicOutboundUrl(rawUrl, options);
|
|
182
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
183
|
+
// A literal IP or an explicitly-allowed host needs no resolution recheck.
|
|
184
|
+
if (isIP(host) !== 0 || looseIpv4Octets(host) || (options.allowHosts ?? []).some((h) => h.toLowerCase() === host))
|
|
185
|
+
return url;
|
|
186
|
+
let addresses;
|
|
187
|
+
try {
|
|
188
|
+
addresses = await lookup(host, { all: true });
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw blocked('target hostname did not resolve', { host });
|
|
192
|
+
}
|
|
193
|
+
for (const { address } of addresses) {
|
|
194
|
+
const octets = looseIpv4Octets(address);
|
|
195
|
+
if (octets ? isBlockedIpv4(octets) : isIP(address) === 6 && isBlockedIpv6(address))
|
|
196
|
+
throw blocked('target hostname resolves to a private or reserved address', { host });
|
|
197
|
+
}
|
|
198
|
+
return url;
|
|
199
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live delivery for the notification center — the half that makes a bell badge
|
|
3
|
+
* change without anybody clicking anything.
|
|
4
|
+
*
|
|
5
|
+
* The center already has the seam (`NotificationSink`); what was missing was a
|
|
6
|
+
* transport behind it. This is that transport, as **SSE** rather than a
|
|
7
|
+
* WebSocket, and the reason is the shape of the traffic: notifications are
|
|
8
|
+
* server→client only, they must survive a laptop lid closing, and a browser
|
|
9
|
+
* reconnects an `EventSource` by itself. A WebSocket would mean writing the
|
|
10
|
+
* reconnect loop, the heartbeat and the backoff that `EventSource` already has
|
|
11
|
+
* — and `cursedbelt-server/ws` remains the right answer for anything the client
|
|
12
|
+
* needs to send back up.
|
|
13
|
+
*
|
|
14
|
+
* 🔴 **A hub is a cache of who is listening, never a queue.** A notification is
|
|
15
|
+
* already durable (it is a row) before a sink is called, so a subscriber that
|
|
16
|
+
* is asleep, disconnected or behind a proxy that ate the stream loses nothing:
|
|
17
|
+
* the next list/poll returns the same row. That is why every emit here is
|
|
18
|
+
* fire-and-forget and a failing subscriber is dropped rather than retried — the
|
|
19
|
+
* poll transport in `cursedbelt/react/notifications` is the backstop, and it is
|
|
20
|
+
* not optional.
|
|
21
|
+
*/
|
|
22
|
+
import type { AppNotification, NotificationSink } from "cwip/notifications";
|
|
23
|
+
/** What a subscriber receives. `unread` rides alongside so a bell badge never
|
|
24
|
+
* has to make a second request to learn its own number. */
|
|
25
|
+
export type NotificationHubEvent = {
|
|
26
|
+
kind: "notification";
|
|
27
|
+
notification: AppNotification;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "unread";
|
|
30
|
+
userId: string;
|
|
31
|
+
count: number;
|
|
32
|
+
};
|
|
33
|
+
export type NotificationHubListener = (event: NotificationHubEvent) => void;
|
|
34
|
+
export interface NotificationHubOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Comment-line heartbeat cadence, in ms. Default 25s — under the 30s idle
|
|
37
|
+
* timeout nginx and Cloudflare both default to, which is what silently kills
|
|
38
|
+
* an otherwise healthy SSE stream. 0 disables it (tests).
|
|
39
|
+
*/
|
|
40
|
+
heartbeatMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Max concurrent streams per user. Default 8. A tab is a stream, and a
|
|
43
|
+
* browser left open for a week accumulates them; without a cap one person's
|
|
44
|
+
* tab habit is an unbounded server-side array.
|
|
45
|
+
*/
|
|
46
|
+
maxStreamsPerUser?: number;
|
|
47
|
+
/** Reported when a listener throws. Never rethrown — see the module doc. */
|
|
48
|
+
onError?: (error: unknown, context: string) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface NotificationHub {
|
|
51
|
+
/**
|
|
52
|
+
* Hand this to `createNotificationCenter({ sink })`. Every `notify*` then
|
|
53
|
+
* reaches the connected tabs of exactly the addressed user.
|
|
54
|
+
*/
|
|
55
|
+
readonly sink: NotificationSink;
|
|
56
|
+
/** Register a listener for one user's events. Returns an unsubscribe fn. */
|
|
57
|
+
subscribe(userId: string, listener: NotificationHubListener): () => void;
|
|
58
|
+
/** An SSE `Response` for one user — the thing a route returns. */
|
|
59
|
+
stream(userId: string, options?: {
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
}): Response;
|
|
62
|
+
/** How many live subscribers a user has (all users when omitted). */
|
|
63
|
+
subscriberCount(userId?: string): number;
|
|
64
|
+
/** Drop every subscriber and stop every heartbeat (shutdown, and tests). */
|
|
65
|
+
close(): void;
|
|
66
|
+
}
|
|
67
|
+
export declare function createNotificationHub(options?: NotificationHubOptions): NotificationHub;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const SSE_HEADERS = {
|
|
2
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
3
|
+
"cache-control": "no-store, no-transform",
|
|
4
|
+
connection: "keep-alive",
|
|
5
|
+
// nginx buffers proxied responses by default, which turns a live stream into
|
|
6
|
+
// one delivered when it ends. The fleet's box is behind nginx; this is the
|
|
7
|
+
// header that opts out, and it is inert everywhere else.
|
|
8
|
+
"x-accel-buffering": "no",
|
|
9
|
+
};
|
|
10
|
+
const encodeEvent = (event) => {
|
|
11
|
+
const name = event.kind;
|
|
12
|
+
const data = event.kind === "notification" ? event.notification : { count: event.count };
|
|
13
|
+
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
14
|
+
};
|
|
15
|
+
export function createNotificationHub(options = {}) {
|
|
16
|
+
const heartbeatMs = options.heartbeatMs ?? 25_000;
|
|
17
|
+
const maxStreamsPerUser = options.maxStreamsPerUser ?? 8;
|
|
18
|
+
const onError = options.onError ?? (() => { });
|
|
19
|
+
/** userId → listeners. An empty set is deleted, so the map never grows with
|
|
20
|
+
* the user table — only with who is actually connected right now. */
|
|
21
|
+
const listeners = new Map();
|
|
22
|
+
let closed = false;
|
|
23
|
+
const emit = (userId, event) => {
|
|
24
|
+
const set = listeners.get(userId);
|
|
25
|
+
if (!set)
|
|
26
|
+
return;
|
|
27
|
+
// Snapshot: a listener may unsubscribe itself while being called (a
|
|
28
|
+
// stream that has just been cancelled does exactly that).
|
|
29
|
+
for (const listener of [...set]) {
|
|
30
|
+
try {
|
|
31
|
+
listener(event);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
onError(error, "notificationHub.listener");
|
|
35
|
+
set.delete(listener);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (set.size === 0)
|
|
39
|
+
listeners.delete(userId);
|
|
40
|
+
};
|
|
41
|
+
const subscribe = (userId, listener) => {
|
|
42
|
+
if (closed)
|
|
43
|
+
return () => { };
|
|
44
|
+
let set = listeners.get(userId);
|
|
45
|
+
if (!set) {
|
|
46
|
+
set = new Set();
|
|
47
|
+
listeners.set(userId, set);
|
|
48
|
+
}
|
|
49
|
+
// Oldest-first eviction: the tab that has been open longest is the one
|
|
50
|
+
// least likely to be looked at, and dropping it costs nothing (the poll
|
|
51
|
+
// backstop still runs there).
|
|
52
|
+
while (set.size >= maxStreamsPerUser) {
|
|
53
|
+
const oldest = set.values().next().value;
|
|
54
|
+
if (!oldest)
|
|
55
|
+
break;
|
|
56
|
+
set.delete(oldest);
|
|
57
|
+
try {
|
|
58
|
+
oldest({ kind: "unread", userId, count: -1 });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
onError(error, "notificationHub.evict");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
set.add(listener);
|
|
65
|
+
return () => {
|
|
66
|
+
const current = listeners.get(userId);
|
|
67
|
+
if (!current)
|
|
68
|
+
return;
|
|
69
|
+
current.delete(listener);
|
|
70
|
+
if (current.size === 0)
|
|
71
|
+
listeners.delete(userId);
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
sink: {
|
|
76
|
+
onNotification(notification) {
|
|
77
|
+
emit(notification.userId, { kind: "notification", notification });
|
|
78
|
+
},
|
|
79
|
+
onUnread(userId, count) {
|
|
80
|
+
emit(userId, { kind: "unread", userId, count });
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
subscribe,
|
|
84
|
+
stream(userId, streamOptions = {}) {
|
|
85
|
+
let unsubscribe;
|
|
86
|
+
let heartbeat;
|
|
87
|
+
let onAbort;
|
|
88
|
+
const encoder = new TextEncoder();
|
|
89
|
+
const body = new ReadableStream({
|
|
90
|
+
start(controller) {
|
|
91
|
+
let live = true;
|
|
92
|
+
const send = (chunk) => {
|
|
93
|
+
if (!live)
|
|
94
|
+
return;
|
|
95
|
+
try {
|
|
96
|
+
controller.enqueue(encoder.encode(chunk));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// The peer went away between our check and the write. Tear the
|
|
100
|
+
// subscription down rather than logging once per event forever.
|
|
101
|
+
live = false;
|
|
102
|
+
onError(error, "notificationHub.enqueue");
|
|
103
|
+
teardown();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const teardown = () => {
|
|
107
|
+
live = false;
|
|
108
|
+
unsubscribe?.();
|
|
109
|
+
unsubscribe = undefined;
|
|
110
|
+
if (heartbeat)
|
|
111
|
+
clearInterval(heartbeat);
|
|
112
|
+
heartbeat = undefined;
|
|
113
|
+
if (onAbort)
|
|
114
|
+
streamOptions.signal?.removeEventListener("abort", onAbort);
|
|
115
|
+
onAbort = undefined;
|
|
116
|
+
};
|
|
117
|
+
// A comment line first: it flushes proxy buffers and tells the
|
|
118
|
+
// client the stream is genuinely open before anything happens.
|
|
119
|
+
send(": connected\n\n");
|
|
120
|
+
unsubscribe = subscribe(userId, (event) => {
|
|
121
|
+
// The eviction poke (count -1) is not a real event — it means
|
|
122
|
+
// "this stream lost its slot", so close rather than report it.
|
|
123
|
+
if (event.kind === "unread" && event.count < 0) {
|
|
124
|
+
teardown();
|
|
125
|
+
try {
|
|
126
|
+
controller.close();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* already closed */
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
send(encodeEvent(event));
|
|
134
|
+
});
|
|
135
|
+
if (heartbeatMs > 0) {
|
|
136
|
+
heartbeat = setInterval(() => send(": ping\n\n"), heartbeatMs);
|
|
137
|
+
// Never hold the process open for a heartbeat — a satellite that
|
|
138
|
+
// is shutting down must not wait out someone's open tab.
|
|
139
|
+
heartbeat.unref?.();
|
|
140
|
+
}
|
|
141
|
+
if (streamOptions.signal) {
|
|
142
|
+
onAbort = () => {
|
|
143
|
+
teardown();
|
|
144
|
+
try {
|
|
145
|
+
controller.close();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* already closed */
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
streamOptions.signal.addEventListener("abort", onAbort, { once: true });
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
cancel() {
|
|
155
|
+
unsubscribe?.();
|
|
156
|
+
unsubscribe = undefined;
|
|
157
|
+
if (heartbeat)
|
|
158
|
+
clearInterval(heartbeat);
|
|
159
|
+
heartbeat = undefined;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
return new Response(body, { status: 200, headers: SSE_HEADERS });
|
|
163
|
+
},
|
|
164
|
+
subscriberCount(userId) {
|
|
165
|
+
if (userId !== undefined)
|
|
166
|
+
return listeners.get(userId)?.size ?? 0;
|
|
167
|
+
let total = 0;
|
|
168
|
+
for (const set of listeners.values())
|
|
169
|
+
total += set.size;
|
|
170
|
+
return total;
|
|
171
|
+
},
|
|
172
|
+
close() {
|
|
173
|
+
closed = true;
|
|
174
|
+
for (const [userId, set] of [...listeners]) {
|
|
175
|
+
for (const listener of [...set]) {
|
|
176
|
+
try {
|
|
177
|
+
listener({ kind: "unread", userId, count: -1 });
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
onError(error, "notificationHub.close");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
listeners.clear();
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|