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,205 @@
|
|
|
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
|
+
|
|
38
|
+
export const OUTBOUND_URL_BLOCKED = 'OUTBOUND_URL_BLOCKED';
|
|
39
|
+
|
|
40
|
+
export interface OutboundGuardOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Exact hostnames (case-insensitive) explicitly permitted even if they would
|
|
43
|
+
* otherwise be blocked — for a deliberately-configured internal integration.
|
|
44
|
+
* Empty by default: nothing internal is reachable unless a host opts in.
|
|
45
|
+
*/
|
|
46
|
+
allowHosts?: readonly string[];
|
|
47
|
+
/** Permitted URL schemes. Default `['http:', 'https:']`. */
|
|
48
|
+
allowSchemes?: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const blocked = (reason: string, context: Record<string, unknown>): AppError =>
|
|
52
|
+
new AppError(`Outbound request refused: ${reason}.`, {
|
|
53
|
+
code: OUTBOUND_URL_BLOCKED,
|
|
54
|
+
status: 400,
|
|
55
|
+
category: 'security',
|
|
56
|
+
// Deliberately terse context — do not echo internal topology back.
|
|
57
|
+
context,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** Hostnames and suffixes that always resolve to a non-public / internal target. */
|
|
61
|
+
const BLOCKED_HOST_EXACT = new Set(['localhost', 'metadata.google.internal', 'metadata']);
|
|
62
|
+
const BLOCKED_HOST_SUFFIXES = ['.localhost', '.local', '.internal', '.intranet', '.cluster.local', '.svc'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse a hostname into an IPv4 the way a permissive resolver would, covering
|
|
66
|
+
* the encodings SSRF filters classically miss: dotted-decimal (`127.0.0.1`), a
|
|
67
|
+
* bare 32-bit integer (`2130706433`), and hex (`0x7f000001`). Returns the four
|
|
68
|
+
* octets, or `null` when the host is not an IPv4 literal in any of these forms.
|
|
69
|
+
*/
|
|
70
|
+
const looseIpv4Octets = (host: string): [number, number, number, number] | null => {
|
|
71
|
+
if (isIP(host) === 4) {
|
|
72
|
+
const parts = host.split('.').map(Number);
|
|
73
|
+
return [parts[0]!, parts[1]!, parts[2]!, parts[3]!];
|
|
74
|
+
}
|
|
75
|
+
// Bare integer or hex integer → a single 32-bit value.
|
|
76
|
+
const asInt = /^0x[0-9a-f]+$/i.test(host)
|
|
77
|
+
? Number.parseInt(host, 16)
|
|
78
|
+
: /^[0-9]+$/.test(host)
|
|
79
|
+
? Number.parseInt(host, 10)
|
|
80
|
+
: Number.NaN;
|
|
81
|
+
if (Number.isInteger(asInt) && asInt >= 0 && asInt <= 0xffffffff)
|
|
82
|
+
return [(asInt >>> 24) & 0xff, (asInt >>> 16) & 0xff, (asInt >>> 8) & 0xff, asInt & 0xff];
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Whether an IPv4 (as octets) is in a private, loopback, link-local or reserved range. */
|
|
87
|
+
const isBlockedIpv4 = ([a, b]: [number, number, number, number]): boolean => {
|
|
88
|
+
if (a === 0 || a === 10 || a === 127) return true; // this-host, private, loopback
|
|
89
|
+
if (a === 169 && b === 254) return true; // link-local — INCLUDES 169.254.169.254 metadata
|
|
90
|
+
if (a === 172 && b >= 16 && b <= 31) return true; // private
|
|
91
|
+
if (a === 192 && b === 168) return true; // private
|
|
92
|
+
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT (100.64/10)
|
|
93
|
+
if (a === 198 && (b === 18 || b === 19)) return true; // benchmarking
|
|
94
|
+
if (a >= 224) return true; // multicast (224/4) + reserved (240/4) + 255.255.255.255
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract the embedded IPv4 of an IPv4-mapped IPv6 address. `new URL` normalizes
|
|
100
|
+
* `::ffff:127.0.0.1` to the HEX form `::ffff:7f00:1`, so both the dotted and the
|
|
101
|
+
* two-hextet forms have to be handled.
|
|
102
|
+
*/
|
|
103
|
+
const mappedV4Octets = (head: string): [number, number, number, number] | null => {
|
|
104
|
+
const rest = /^::ffff:(.+)$/i.exec(head)?.[1];
|
|
105
|
+
if (!rest) return null;
|
|
106
|
+
if (rest.includes('.')) return looseIpv4Octets(rest);
|
|
107
|
+
const groups = rest.split(':');
|
|
108
|
+
if (groups.length !== 2) return null;
|
|
109
|
+
const hi = Number.parseInt(groups[0]!, 16);
|
|
110
|
+
const lo = Number.parseInt(groups[1]!, 16);
|
|
111
|
+
if (!Number.isInteger(hi) || !Number.isInteger(lo)) return null;
|
|
112
|
+
return [(hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff, lo & 0xff];
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Whether an IPv6 literal (already `net.isIP`-classified) is non-public. */
|
|
116
|
+
const isBlockedIpv6 = (raw: string): boolean => {
|
|
117
|
+
const head = raw.toLowerCase().replace(/^\[|\]$/g, '');
|
|
118
|
+
if (head === '::1' || head === '::') return true; // loopback, unspecified
|
|
119
|
+
const mapped = mappedV4Octets(head);
|
|
120
|
+
if (mapped && isBlockedIpv4(mapped)) return true; // IPv4-mapped (`::ffff:…`)
|
|
121
|
+
// fe80::/10 link-local (fe80–febf) and fec0::/10 deprecated site-local (fec0–feff).
|
|
122
|
+
if (/^fe[89ab]/.test(head) || /^fe[c-f]/.test(head)) return true;
|
|
123
|
+
if (head.startsWith('fc') || head.startsWith('fd')) return true; // fc00::/7 unique-local (incl. fd00:ec2::254 metadata)
|
|
124
|
+
if (head.startsWith('ff')) return true; // ff00::/8 multicast
|
|
125
|
+
return false;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const isBlockedHostname = (host: string): boolean => {
|
|
129
|
+
if (BLOCKED_HOST_EXACT.has(host)) return true;
|
|
130
|
+
return BLOCKED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Structural SSRF guard: parse `rawUrl`, enforce scheme, reject credentials, and
|
|
135
|
+
* refuse any host that is a private/loopback/link-local/reserved IP literal (in
|
|
136
|
+
* any encoding) or a well-known internal hostname. Returns the parsed URL.
|
|
137
|
+
*
|
|
138
|
+
* @throws {AppError} `OUTBOUND_URL_BLOCKED` on any violation.
|
|
139
|
+
*/
|
|
140
|
+
export const assertPublicOutboundUrl = (rawUrl: string, options: OutboundGuardOptions = {}): URL => {
|
|
141
|
+
const allowSchemes = options.allowSchemes ?? ['http:', 'https:'];
|
|
142
|
+
const allowHosts = new Set((options.allowHosts ?? []).map((h) => h.toLowerCase()));
|
|
143
|
+
|
|
144
|
+
let url: URL;
|
|
145
|
+
try {
|
|
146
|
+
url = new URL(rawUrl);
|
|
147
|
+
} catch {
|
|
148
|
+
throw blocked('not a valid absolute URL', {});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!allowSchemes.includes(url.protocol))
|
|
152
|
+
throw blocked(`scheme '${url.protocol}' is not allowed`, { scheme: url.protocol });
|
|
153
|
+
|
|
154
|
+
// Credentials in the URL are a redirect/parsing-confusion vector and never
|
|
155
|
+
// legitimate for a tool send.
|
|
156
|
+
if (url.username !== '' || url.password !== '') throw blocked('credentials in URL are not allowed', {});
|
|
157
|
+
|
|
158
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
159
|
+
if (allowHosts.has(host)) return url; // explicit host opt-in bypasses the blocks
|
|
160
|
+
|
|
161
|
+
const v4 = looseIpv4Octets(host);
|
|
162
|
+
if (v4) {
|
|
163
|
+
if (isBlockedIpv4(v4)) throw blocked('target resolves to a private or reserved address', { host });
|
|
164
|
+
return url;
|
|
165
|
+
}
|
|
166
|
+
if (isIP(host) === 6) {
|
|
167
|
+
if (isBlockedIpv6(host)) throw blocked('target resolves to a private or reserved address', { host });
|
|
168
|
+
return url;
|
|
169
|
+
}
|
|
170
|
+
if (isBlockedHostname(host)) throw blocked('target host is internal-only', { host });
|
|
171
|
+
|
|
172
|
+
return url;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* DNS-aware SSRF guard: the structural checks of {@link assertPublicOutboundUrl}
|
|
177
|
+
* PLUS a resolution pass — every address the hostname resolves to is re-checked,
|
|
178
|
+
* so a public name pointing at a private IP is refused. Still racy against an
|
|
179
|
+
* attacker who rebinds between this lookup and the actual connect, which is why
|
|
180
|
+
* the SaaS profile must also enforce network-layer egress; but it closes the
|
|
181
|
+
* naive `evil.example → 169.254.169.254` vector the structural pass cannot see.
|
|
182
|
+
*
|
|
183
|
+
* @throws {AppError} `OUTBOUND_URL_BLOCKED` on any violation.
|
|
184
|
+
*/
|
|
185
|
+
export const assertResolvedPublicUrl = async (rawUrl: string, options: OutboundGuardOptions = {}): Promise<URL> => {
|
|
186
|
+
const url = assertPublicOutboundUrl(rawUrl, options);
|
|
187
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
188
|
+
|
|
189
|
+
// A literal IP or an explicitly-allowed host needs no resolution recheck.
|
|
190
|
+
if (isIP(host) !== 0 || looseIpv4Octets(host) || (options.allowHosts ?? []).some((h) => h.toLowerCase() === host))
|
|
191
|
+
return url;
|
|
192
|
+
|
|
193
|
+
let addresses: { address: string; family: number }[];
|
|
194
|
+
try {
|
|
195
|
+
addresses = await lookup(host, { all: true });
|
|
196
|
+
} catch {
|
|
197
|
+
throw blocked('target hostname did not resolve', { host });
|
|
198
|
+
}
|
|
199
|
+
for (const { address } of addresses) {
|
|
200
|
+
const octets = looseIpv4Octets(address);
|
|
201
|
+
if (octets ? isBlockedIpv4(octets) : isIP(address) === 6 && isBlockedIpv6(address))
|
|
202
|
+
throw blocked('target hostname resolves to a private or reserved address', { host });
|
|
203
|
+
}
|
|
204
|
+
return url;
|
|
205
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
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
|
+
|
|
24
|
+
/** What a subscriber receives. `unread` rides alongside so a bell badge never
|
|
25
|
+
* has to make a second request to learn its own number. */
|
|
26
|
+
export type NotificationHubEvent =
|
|
27
|
+
| { kind: "notification"; notification: AppNotification }
|
|
28
|
+
| { kind: "unread"; userId: string; count: number };
|
|
29
|
+
|
|
30
|
+
export type NotificationHubListener = (event: NotificationHubEvent) => void;
|
|
31
|
+
|
|
32
|
+
export interface NotificationHubOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Comment-line heartbeat cadence, in ms. Default 25s — under the 30s idle
|
|
35
|
+
* timeout nginx and Cloudflare both default to, which is what silently kills
|
|
36
|
+
* an otherwise healthy SSE stream. 0 disables it (tests).
|
|
37
|
+
*/
|
|
38
|
+
heartbeatMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Max concurrent streams per user. Default 8. A tab is a stream, and a
|
|
41
|
+
* browser left open for a week accumulates them; without a cap one person's
|
|
42
|
+
* tab habit is an unbounded server-side array.
|
|
43
|
+
*/
|
|
44
|
+
maxStreamsPerUser?: number;
|
|
45
|
+
/** Reported when a listener throws. Never rethrown — see the module doc. */
|
|
46
|
+
onError?: (error: unknown, context: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface NotificationHub {
|
|
50
|
+
/**
|
|
51
|
+
* Hand this to `createNotificationCenter({ sink })`. Every `notify*` then
|
|
52
|
+
* reaches the connected tabs of exactly the addressed user.
|
|
53
|
+
*/
|
|
54
|
+
readonly sink: NotificationSink;
|
|
55
|
+
/** Register a listener for one user's events. Returns an unsubscribe fn. */
|
|
56
|
+
subscribe(userId: string, listener: NotificationHubListener): () => void;
|
|
57
|
+
/** An SSE `Response` for one user — the thing a route returns. */
|
|
58
|
+
stream(userId: string, options?: { signal?: AbortSignal }): Response;
|
|
59
|
+
/** How many live subscribers a user has (all users when omitted). */
|
|
60
|
+
subscriberCount(userId?: string): number;
|
|
61
|
+
/** Drop every subscriber and stop every heartbeat (shutdown, and tests). */
|
|
62
|
+
close(): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const SSE_HEADERS: Record<string, string> = {
|
|
66
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
67
|
+
"cache-control": "no-store, no-transform",
|
|
68
|
+
connection: "keep-alive",
|
|
69
|
+
// nginx buffers proxied responses by default, which turns a live stream into
|
|
70
|
+
// one delivered when it ends. The fleet's box is behind nginx; this is the
|
|
71
|
+
// header that opts out, and it is inert everywhere else.
|
|
72
|
+
"x-accel-buffering": "no",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const encodeEvent = (event: NotificationHubEvent): string => {
|
|
76
|
+
const name = event.kind;
|
|
77
|
+
const data = event.kind === "notification" ? event.notification : { count: event.count };
|
|
78
|
+
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function createNotificationHub(options: NotificationHubOptions = {}): NotificationHub {
|
|
82
|
+
const heartbeatMs = options.heartbeatMs ?? 25_000;
|
|
83
|
+
const maxStreamsPerUser = options.maxStreamsPerUser ?? 8;
|
|
84
|
+
const onError = options.onError ?? (() => {});
|
|
85
|
+
/** userId → listeners. An empty set is deleted, so the map never grows with
|
|
86
|
+
* the user table — only with who is actually connected right now. */
|
|
87
|
+
const listeners = new Map<string, Set<NotificationHubListener>>();
|
|
88
|
+
let closed = false;
|
|
89
|
+
|
|
90
|
+
const emit = (userId: string, event: NotificationHubEvent): void => {
|
|
91
|
+
const set = listeners.get(userId);
|
|
92
|
+
if (!set) return;
|
|
93
|
+
// Snapshot: a listener may unsubscribe itself while being called (a
|
|
94
|
+
// stream that has just been cancelled does exactly that).
|
|
95
|
+
for (const listener of [...set]) {
|
|
96
|
+
try {
|
|
97
|
+
listener(event);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
onError(error, "notificationHub.listener");
|
|
100
|
+
set.delete(listener);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (set.size === 0) listeners.delete(userId);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const subscribe: NotificationHub["subscribe"] = (userId, listener) => {
|
|
107
|
+
if (closed) return () => {};
|
|
108
|
+
let set = listeners.get(userId);
|
|
109
|
+
if (!set) {
|
|
110
|
+
set = new Set();
|
|
111
|
+
listeners.set(userId, set);
|
|
112
|
+
}
|
|
113
|
+
// Oldest-first eviction: the tab that has been open longest is the one
|
|
114
|
+
// least likely to be looked at, and dropping it costs nothing (the poll
|
|
115
|
+
// backstop still runs there).
|
|
116
|
+
while (set.size >= maxStreamsPerUser) {
|
|
117
|
+
const oldest = set.values().next().value;
|
|
118
|
+
if (!oldest) break;
|
|
119
|
+
set.delete(oldest);
|
|
120
|
+
try {
|
|
121
|
+
oldest({ kind: "unread", userId, count: -1 });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
onError(error, "notificationHub.evict");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
set.add(listener);
|
|
127
|
+
return () => {
|
|
128
|
+
const current = listeners.get(userId);
|
|
129
|
+
if (!current) return;
|
|
130
|
+
current.delete(listener);
|
|
131
|
+
if (current.size === 0) listeners.delete(userId);
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
sink: {
|
|
137
|
+
onNotification(notification) {
|
|
138
|
+
emit(notification.userId, { kind: "notification", notification });
|
|
139
|
+
},
|
|
140
|
+
onUnread(userId, count) {
|
|
141
|
+
emit(userId, { kind: "unread", userId, count });
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
subscribe,
|
|
146
|
+
|
|
147
|
+
stream(userId, streamOptions = {}) {
|
|
148
|
+
let unsubscribe: (() => void) | undefined;
|
|
149
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
150
|
+
let onAbort: (() => void) | undefined;
|
|
151
|
+
const encoder = new TextEncoder();
|
|
152
|
+
|
|
153
|
+
const body = new ReadableStream<Uint8Array>({
|
|
154
|
+
start(controller) {
|
|
155
|
+
let live = true;
|
|
156
|
+
const send = (chunk: string) => {
|
|
157
|
+
if (!live) return;
|
|
158
|
+
try {
|
|
159
|
+
controller.enqueue(encoder.encode(chunk));
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// The peer went away between our check and the write. Tear the
|
|
162
|
+
// subscription down rather than logging once per event forever.
|
|
163
|
+
live = false;
|
|
164
|
+
onError(error, "notificationHub.enqueue");
|
|
165
|
+
teardown();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const teardown = () => {
|
|
169
|
+
live = false;
|
|
170
|
+
unsubscribe?.();
|
|
171
|
+
unsubscribe = undefined;
|
|
172
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
173
|
+
heartbeat = undefined;
|
|
174
|
+
if (onAbort) streamOptions.signal?.removeEventListener("abort", onAbort);
|
|
175
|
+
onAbort = undefined;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// A comment line first: it flushes proxy buffers and tells the
|
|
179
|
+
// client the stream is genuinely open before anything happens.
|
|
180
|
+
send(": connected\n\n");
|
|
181
|
+
unsubscribe = subscribe(userId, (event) => {
|
|
182
|
+
// The eviction poke (count -1) is not a real event — it means
|
|
183
|
+
// "this stream lost its slot", so close rather than report it.
|
|
184
|
+
if (event.kind === "unread" && event.count < 0) {
|
|
185
|
+
teardown();
|
|
186
|
+
try {
|
|
187
|
+
controller.close();
|
|
188
|
+
} catch {
|
|
189
|
+
/* already closed */
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
send(encodeEvent(event));
|
|
194
|
+
});
|
|
195
|
+
if (heartbeatMs > 0) {
|
|
196
|
+
heartbeat = setInterval(() => send(": ping\n\n"), heartbeatMs);
|
|
197
|
+
// Never hold the process open for a heartbeat — a satellite that
|
|
198
|
+
// is shutting down must not wait out someone's open tab.
|
|
199
|
+
(heartbeat as unknown as { unref?: () => void }).unref?.();
|
|
200
|
+
}
|
|
201
|
+
if (streamOptions.signal) {
|
|
202
|
+
onAbort = () => {
|
|
203
|
+
teardown();
|
|
204
|
+
try {
|
|
205
|
+
controller.close();
|
|
206
|
+
} catch {
|
|
207
|
+
/* already closed */
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
streamOptions.signal.addEventListener("abort", onAbort, { once: true });
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
cancel() {
|
|
214
|
+
unsubscribe?.();
|
|
215
|
+
unsubscribe = undefined;
|
|
216
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
217
|
+
heartbeat = undefined;
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return new Response(body, { status: 200, headers: SSE_HEADERS });
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
subscriberCount(userId) {
|
|
225
|
+
if (userId !== undefined) return listeners.get(userId)?.size ?? 0;
|
|
226
|
+
let total = 0;
|
|
227
|
+
for (const set of listeners.values()) total += set.size;
|
|
228
|
+
return total;
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
close() {
|
|
232
|
+
closed = true;
|
|
233
|
+
for (const [userId, set] of [...listeners]) {
|
|
234
|
+
for (const listener of [...set]) {
|
|
235
|
+
try {
|
|
236
|
+
listener({ kind: "unread", userId, count: -1 });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
onError(error, "notificationHub.close");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
listeners.clear();
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-server/notifications` — the SERVER half of the fleet's one
|
|
3
|
+
* notification primitive.
|
|
4
|
+
*
|
|
5
|
+
* ── What was already here, and what was missing ─────────────────────────────
|
|
6
|
+
* The dispatch engine has lived in `cwip/notifications` for months (a
|
|
7
|
+
* persistent per-user row, `groupKey` coalescing, unread counts, deep links, a
|
|
8
|
+
* live-delivery sink) and the whole client has lived in
|
|
9
|
+
* `cursedbelt/react/notifications` (store, bell, feed, inbox, poll transport).
|
|
10
|
+
* What did NOT exist was a way to MOUNT any of it: the only mounted surface in
|
|
11
|
+
* the fleet was `orch-ui/services/notifications`, written against
|
|
12
|
+
* `@satellites/satellite-rpc` inside a package only the orchestrator consumes.
|
|
13
|
+
* So a satellite wanting a bell had two options, and both were wrong — adopt
|
|
14
|
+
* orch's rpc layer, or write the routes again.
|
|
15
|
+
*
|
|
16
|
+
* This module is the third: ordinary Hono routes, an SSE hub behind the sink
|
|
17
|
+
* the center already had, and a migration for apps using cursedbelt's runner.
|
|
18
|
+
*
|
|
19
|
+
* const notifications = createNotificationService({ db });
|
|
20
|
+
* notifications.mount(app, { authorize: (c) => userOf(c)?.id ?? null });
|
|
21
|
+
*
|
|
22
|
+
* and on the client:
|
|
23
|
+
*
|
|
24
|
+
* const { store } = createNotificationsBinding(); // cursedbelt/react/notifications
|
|
25
|
+
* <NotificationBell store={store} />
|
|
26
|
+
*
|
|
27
|
+
* ── The three properties worth knowing ──────────────────────────────────────
|
|
28
|
+
* 1. **A notification is a row before it is an event.** Delivery is a cache of
|
|
29
|
+
* who is listening; the poll transport is the backstop and is not optional.
|
|
30
|
+
* Nothing is lost by a closed laptop.
|
|
31
|
+
* 2. **Coalescing is a database constraint, not a debounce.** The partial
|
|
32
|
+
* unique index on `(user_id, group_key) WHERE group_key IS NOT NULL AND
|
|
33
|
+
* done = 0` is what makes "twenty of the same thing" one row with a count,
|
|
34
|
+
* even under concurrent producers.
|
|
35
|
+
* 3. **The routes address the session's user and nothing else.** There is no
|
|
36
|
+
* route that notifies somebody else; producing is server-side code calling
|
|
37
|
+
* `center.notify*`.
|
|
38
|
+
*/
|
|
39
|
+
export { createNotificationHub } from "./hub";
|
|
40
|
+
export type {
|
|
41
|
+
NotificationHub,
|
|
42
|
+
NotificationHubEvent,
|
|
43
|
+
NotificationHubListener,
|
|
44
|
+
NotificationHubOptions,
|
|
45
|
+
} from "./hub";
|
|
46
|
+
export { NOTIFICATION_MIGRATIONS, NOTIFICATIONS_TABLE } from "./migrations";
|
|
47
|
+
export { mountNotificationRoutes } from "./routes";
|
|
48
|
+
export type { NotificationRouteCenter, NotificationRoutesOptions } from "./routes";
|
|
49
|
+
export { createNotificationService } from "./service";
|
|
50
|
+
export type {
|
|
51
|
+
CreateNotificationServiceOptions,
|
|
52
|
+
NotificationService,
|
|
53
|
+
NotificationServiceMountOptions,
|
|
54
|
+
} from "./service";
|
|
55
|
+
// Re-exported so a server consumer needs ONE import for the whole contract —
|
|
56
|
+
// the same courtesy `server/sharing` extends over `cursedbelt-core/sharing`. The
|
|
57
|
+
// engine remains cwip's; this is a door, not a copy.
|
|
58
|
+
export {
|
|
59
|
+
createNotificationCenter,
|
|
60
|
+
createMemoryNotificationStore,
|
|
61
|
+
createSqliteNotificationStore,
|
|
62
|
+
migrateNotificationsSchema,
|
|
63
|
+
} from "cwip/notifications";
|
|
64
|
+
export type {
|
|
65
|
+
AppNotification,
|
|
66
|
+
ListNotificationsQuery,
|
|
67
|
+
NotificationAudience,
|
|
68
|
+
NotificationCenter,
|
|
69
|
+
NotificationCenterOptions,
|
|
70
|
+
NotificationDraft,
|
|
71
|
+
NotificationFilter,
|
|
72
|
+
NotificationPage,
|
|
73
|
+
NotificationSeverity,
|
|
74
|
+
NotificationSink,
|
|
75
|
+
NotificationStore,
|
|
76
|
+
Notifier,
|
|
77
|
+
} from "cwip/notifications";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The notification table, as a migration an app runs inside its OWN database.
|
|
3
|
+
*
|
|
4
|
+
* Same reasoning as `../sharing/migrations.ts`: the SCHEMA is shared, the DATA
|
|
5
|
+
* is local. A notification is about one app's rows and is read on the same page
|
|
6
|
+
* that reads them, so a central notification service would put a network hop in
|
|
7
|
+
* front of a badge count — and the first thing an app would do is cache it.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 The DDL itself is NOT written here. `cwip/notifications` already owns it
|
|
10
|
+
* (`migrateNotificationsSchema`, which the sqlite store calls on construction),
|
|
11
|
+
* and a second copy in this file is a schema that agrees with the store until
|
|
12
|
+
* the day it doesn't. This module exists to give apps that use cursedbelt's
|
|
13
|
+
* migration RUNNER an ordered, once-only entry for it — the DDL is idempotent,
|
|
14
|
+
* so both routes converge on one table.
|
|
15
|
+
*/
|
|
16
|
+
import type { Database } from "bun:sqlite";
|
|
17
|
+
import { migrateNotificationsSchema, type NotificationDb } from "cwip/notifications";
|
|
18
|
+
import type { Migration } from "../migration/types";
|
|
19
|
+
|
|
20
|
+
export const NOTIFICATIONS_TABLE = "notifications";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ordered migrations for the notification table.
|
|
24
|
+
*
|
|
25
|
+
* applyRecommendedPragmas(db);
|
|
26
|
+
* runMigrations(db, [...APP_MIGRATIONS, ...NOTIFICATION_MIGRATIONS]);
|
|
27
|
+
*
|
|
28
|
+
* An app that instead constructs `createSqliteNotificationStore(db)` gets the
|
|
29
|
+
* same table without this — the store migrates on construction. Running both is
|
|
30
|
+
* harmless and is the normal state of an app that adopted the store first.
|
|
31
|
+
*/
|
|
32
|
+
export const NOTIFICATION_MIGRATIONS: readonly Migration[] = [
|
|
33
|
+
{
|
|
34
|
+
name: "0001_notifications",
|
|
35
|
+
up(db: Database): void {
|
|
36
|
+
migrateNotificationsSchema(db as unknown as NotificationDb, NOTIFICATIONS_TABLE);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|