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,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire and store types every consumer of this sync engine shares. The op
|
|
3
|
+
* envelope is the notes shape — `(origin, originSeq)` gives exactly-once application
|
|
4
|
+
* with no coordination — and the report is vault's cursor discipline (the reader may
|
|
5
|
+
* advance exactly to `cursor`, never further) merged with notes' `seen` (a duplicate
|
|
6
|
+
* is not an error, it is proof the dedupe ledger works).
|
|
7
|
+
*
|
|
8
|
+
* ── 🔴 "sync-kit" is a PAST NAME. The engine is `cursedbelt-server/sync` ────────────
|
|
9
|
+
* The retired generation published this as `@satellites/sync-kit`. It came across
|
|
10
|
+
* whole into `apps/vault/src/kit/sync/` when that app graduated, and came here —
|
|
11
|
+
* `cursedbelt-server/sync` — on 2026-09-15, before `apps/station` could graduate and fork
|
|
12
|
+
* it a second time. That is the move this header used to call an open question in
|
|
13
|
+
* `tasks/`; it is done, and the app-local copies are what is temporary now.
|
|
14
|
+
*
|
|
15
|
+
* `@satellites/sync-kit` is NOT installable and never was — it was never published
|
|
16
|
+
* to npm — and its design plan (`plans/01-sync-kit-adoption-design.md`) belonged to
|
|
17
|
+
* the retired monorepo and did not come across. Comments in consuming apps still say
|
|
18
|
+
* "the sync-kit adoption": that is the NAME OF AN EVENT, the 2026-08 change that
|
|
19
|
+
* moved vault from its own `oplog` table onto this engine's `sync_ops`, and it is
|
|
20
|
+
* worth keeping because the dual-compat code only makes sense in its light.
|
|
21
|
+
*/
|
|
22
|
+
/** One durable mutation in the shared op log. `payload` is app-defined per `kind`. */
|
|
23
|
+
export interface SyncOp {
|
|
24
|
+
/** This instance's monotonic sequence for the op (assigned by the local log). */
|
|
25
|
+
seq: number;
|
|
26
|
+
/** The instance id that BORN the op (stable per instance, never per device). */
|
|
27
|
+
origin: string;
|
|
28
|
+
/** The origin's own monotonic counter — `(origin, originSeq)` is the identity. */
|
|
29
|
+
originSeq: number;
|
|
30
|
+
/** Wall-clock ms when the op was born. LWW inputs, display, nothing structural. */
|
|
31
|
+
ts: number;
|
|
32
|
+
/** App-defined kind, e.g. `doc.set`. The version-negotiation unit. */
|
|
33
|
+
kind: string;
|
|
34
|
+
payload: unknown;
|
|
35
|
+
}
|
|
36
|
+
/** What applying one batch did. `cursor` is the last seq FULLY applied — a stopped
|
|
37
|
+
* batch leaves it just before the failure so the next run resumes there. */
|
|
38
|
+
export interface ApplyReport {
|
|
39
|
+
/** Ops that changed local state. */
|
|
40
|
+
applied: number;
|
|
41
|
+
/** Ops already held (dedupe hit) — counted, never re-applied. */
|
|
42
|
+
seen: number;
|
|
43
|
+
/** Concurrent edits reconciled (however the app's applier chose to). */
|
|
44
|
+
conflicts: number;
|
|
45
|
+
cursor: number;
|
|
46
|
+
/** Non-null when the batch stopped early (version skew, deferred ordering…). */
|
|
47
|
+
error: string | null;
|
|
48
|
+
}
|
|
49
|
+
/** The peer info handshake. `opKinds` is the compatibility advertisement — a dialer
|
|
50
|
+
* stops BEFORE the first op a peer cannot take (notes' version-skew lesson: sending
|
|
51
|
+
* it wedges the push with an error that reads like corruption). */
|
|
52
|
+
export interface PeerInfo {
|
|
53
|
+
instanceId: string;
|
|
54
|
+
head: number;
|
|
55
|
+
opKinds: string[];
|
|
56
|
+
}
|
|
57
|
+
/** The transport seam. Pure interface so tests drive the engine against a REAL second
|
|
58
|
+
* store mounted in a Hono app — no HTTP mocks (the notes/vault discipline). */
|
|
59
|
+
export interface RemoteApi {
|
|
60
|
+
info(): Promise<PeerInfo>;
|
|
61
|
+
pull(after: number, excludeOrigin: string): Promise<{
|
|
62
|
+
ops: SyncOp[];
|
|
63
|
+
cursor: number;
|
|
64
|
+
head: number;
|
|
65
|
+
}>;
|
|
66
|
+
push(payload: {
|
|
67
|
+
ops: SyncOp[];
|
|
68
|
+
peerHas: number;
|
|
69
|
+
}): Promise<ApplyReport>;
|
|
70
|
+
/** The receiver's "someone pressed Sync now here" stamp; null when unsupported. */
|
|
71
|
+
requestedAt?(): Promise<number | null>;
|
|
72
|
+
}
|
|
73
|
+
/** Context the applier gets per batch — vault's concurrency bound. */
|
|
74
|
+
export interface ApplyContext {
|
|
75
|
+
/** The peer's stable instance id. */
|
|
76
|
+
peerId: string;
|
|
77
|
+
/** Max LOCAL-born seq the peer already holds of OURS — an entity whose newest
|
|
78
|
+
* local op is beyond this AND diverges is a true concurrent edit. */
|
|
79
|
+
peerReceivedUpTo: number;
|
|
80
|
+
}
|
|
81
|
+
/** The app's merge logic for ONE op that passed the dedupe ledger. Return value:
|
|
82
|
+
* - `"applied"` — state changed
|
|
83
|
+
* - `"ignored"` — stale/no-op (still recorded in the ledger, cursor advances)
|
|
84
|
+
* - `"conflict"` — applied with a materialized conflict artifact
|
|
85
|
+
* Throw {@link StopBatch} to stop BEFORE this op (cursor stays short of it). */
|
|
86
|
+
export type ApplyOne = (op: SyncOp, ctx: ApplyContext) => "applied" | "ignored" | "conflict";
|
|
87
|
+
/** Stop the current batch before the offending op — deferred ordering, a payload this
|
|
88
|
+
* build cannot hold yet, anything where "retry after the world changes" is right. */
|
|
89
|
+
export declare class StopBatch extends Error {
|
|
90
|
+
constructor(reason: string);
|
|
91
|
+
}
|
|
92
|
+
export interface SyncSummary {
|
|
93
|
+
remoteId: string;
|
|
94
|
+
pushed: number;
|
|
95
|
+
pulled: number;
|
|
96
|
+
conflicts: number;
|
|
97
|
+
/** Ops the export policy held back (counted, never sent). */
|
|
98
|
+
held: number;
|
|
99
|
+
/** Set when the remote runs an older schema and cannot take what comes next. */
|
|
100
|
+
blockedBy?: string;
|
|
101
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire and store types every consumer of this sync engine shares. The op
|
|
3
|
+
* envelope is the notes shape — `(origin, originSeq)` gives exactly-once application
|
|
4
|
+
* with no coordination — and the report is vault's cursor discipline (the reader may
|
|
5
|
+
* advance exactly to `cursor`, never further) merged with notes' `seen` (a duplicate
|
|
6
|
+
* is not an error, it is proof the dedupe ledger works).
|
|
7
|
+
*
|
|
8
|
+
* ── 🔴 "sync-kit" is a PAST NAME. The engine is `cursedbelt-server/sync` ────────────
|
|
9
|
+
* The retired generation published this as `@satellites/sync-kit`. It came across
|
|
10
|
+
* whole into `apps/vault/src/kit/sync/` when that app graduated, and came here —
|
|
11
|
+
* `cursedbelt-server/sync` — on 2026-09-15, before `apps/station` could graduate and fork
|
|
12
|
+
* it a second time. That is the move this header used to call an open question in
|
|
13
|
+
* `tasks/`; it is done, and the app-local copies are what is temporary now.
|
|
14
|
+
*
|
|
15
|
+
* `@satellites/sync-kit` is NOT installable and never was — it was never published
|
|
16
|
+
* to npm — and its design plan (`plans/01-sync-kit-adoption-design.md`) belonged to
|
|
17
|
+
* the retired monorepo and did not come across. Comments in consuming apps still say
|
|
18
|
+
* "the sync-kit adoption": that is the NAME OF AN EVENT, the 2026-08 change that
|
|
19
|
+
* moved vault from its own `oplog` table onto this engine's `sync_ops`, and it is
|
|
20
|
+
* worth keeping because the dual-compat code only makes sense in its light.
|
|
21
|
+
*/
|
|
22
|
+
/** Stop the current batch before the offending op — deferred ordering, a payload this
|
|
23
|
+
* build cannot hold yet, anything where "retry after the world changes" is right. */
|
|
24
|
+
export class StopBatch extends Error {
|
|
25
|
+
constructor(reason) {
|
|
26
|
+
super(reason);
|
|
27
|
+
this.name = "StopBatch";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EventBus } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The push-only subset ru uses: broadcast-to-all, no auth, no inbound routing. ru
|
|
4
|
+
* mounts one bus and pushes from its task queue / caSync / Forge workers
|
|
5
|
+
* (`bus.broadcast('taskq-update', { … })`). Same Bun-native upgrade as the hub, minus
|
|
6
|
+
* the channel registry, presence, and heartbeat.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const bus = createEventBus();
|
|
10
|
+
* app.get('/events', bus.upgradeHandler);
|
|
11
|
+
* serveBun(app, { port, websocket: bus.handlers });
|
|
12
|
+
* bus.broadcast('taskq-update', { running: 3 });
|
|
13
|
+
*/
|
|
14
|
+
export declare function createEventBus(): EventBus;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The push-only subset ru uses: broadcast-to-all, no auth, no inbound routing. ru
|
|
3
|
+
* mounts one bus and pushes from its task queue / caSync / Forge workers
|
|
4
|
+
* (`bus.broadcast('taskq-update', { … })`). Same Bun-native upgrade as the hub, minus
|
|
5
|
+
* the channel registry, presence, and heartbeat.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const bus = createEventBus();
|
|
9
|
+
* app.get('/events', bus.upgradeHandler);
|
|
10
|
+
* serveBun(app, { port, websocket: bus.handlers });
|
|
11
|
+
* bus.broadcast('taskq-update', { running: 3 });
|
|
12
|
+
*/
|
|
13
|
+
export function createEventBus() {
|
|
14
|
+
const sockets = new Set();
|
|
15
|
+
const upgradeHandler = async (c) => {
|
|
16
|
+
const server = c.env;
|
|
17
|
+
const upgraded = server.upgrade(c.req.raw, { data: {} });
|
|
18
|
+
if (!upgraded)
|
|
19
|
+
return c.text('Upgrade required', 426);
|
|
20
|
+
return c.body(null, 204);
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
upgradeHandler,
|
|
24
|
+
handlers: {
|
|
25
|
+
open(ws) {
|
|
26
|
+
sockets.add(ws);
|
|
27
|
+
},
|
|
28
|
+
// Push-only: inbound frames are intentionally ignored. (Bun requires a `message`
|
|
29
|
+
// handler, so this is a documented no-op rather than an omitted key.)
|
|
30
|
+
message() { },
|
|
31
|
+
close(ws) {
|
|
32
|
+
sockets.delete(ws);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
broadcast(event, data) {
|
|
36
|
+
const payload = JSON.stringify({ event, data });
|
|
37
|
+
for (const ws of sockets)
|
|
38
|
+
ws.send(payload);
|
|
39
|
+
},
|
|
40
|
+
clientCount: () => sockets.size,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WsChannel, WsClient, WsHub, WsHubOpts } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The heartbeat sweep, factored out as a pure function so it's testable without faking
|
|
4
|
+
* timers: ping every live client, close (don't delete — let the `close` handler fire
|
|
5
|
+
* `onDisconnect` + remove) any unheard-from past `staleMs`. The hub's `setInterval`
|
|
6
|
+
* calls this with `Date.now()`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sweepStaleClients(clients: Iterable<WsClient>, now: number, staleMs: number): void;
|
|
9
|
+
/**
|
|
10
|
+
* Build a typed WebSocket hub on Bun's native upgrade. The hub owns the transport —
|
|
11
|
+
* connection registry, channel routing, heartbeat/stale-cull, and presence-as-a-view;
|
|
12
|
+
* the registered {@link WsChannel}s own the business logic. Mount {@link WsHub.upgradeHandler}
|
|
13
|
+
* on the Hono upgrade route(s) and hand {@link WsHub.handlers} to `serveBun`'s `websocket`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const hub = createWsHub({ channels: wsChannels, authenticate });
|
|
17
|
+
* app.get('/ws/:channel', hub.upgradeHandler);
|
|
18
|
+
* serveBun(app, { port, websocket: hub.handlers });
|
|
19
|
+
*/
|
|
20
|
+
export declare function createWsHub<T extends Record<string, WsChannel<any>>>(opts: WsHubOpts<T>): WsHub;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ApiError, toErrorEnvelope } from '../errors';
|
|
2
|
+
import { computePresence } from './presence';
|
|
3
|
+
import { joinRoom, ROOM_META_KEY } from './room';
|
|
4
|
+
const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
5
|
+
const DEFAULT_STALE_MS = 120_000;
|
|
6
|
+
/** Bun's `idleTimeout` ceiling (seconds). */
|
|
7
|
+
const BUN_IDLE_TIMEOUT_MAX_S = 960;
|
|
8
|
+
/** Serialize the wire envelope once per fan-out. */
|
|
9
|
+
const encode = (event, data) => JSON.stringify({ event, data });
|
|
10
|
+
/**
|
|
11
|
+
* The heartbeat sweep, factored out as a pure function so it's testable without faking
|
|
12
|
+
* timers: ping every live client, close (don't delete — let the `close` handler fire
|
|
13
|
+
* `onDisconnect` + remove) any unheard-from past `staleMs`. The hub's `setInterval`
|
|
14
|
+
* calls this with `Date.now()`.
|
|
15
|
+
*/
|
|
16
|
+
export function sweepStaleClients(clients, now, staleMs) {
|
|
17
|
+
for (const client of clients) {
|
|
18
|
+
if (now - client.lastPingAt > staleMs) {
|
|
19
|
+
client.ws.close(1001, 'stale');
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
client.ws.ping();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a typed WebSocket hub on Bun's native upgrade. The hub owns the transport —
|
|
28
|
+
* connection registry, channel routing, heartbeat/stale-cull, and presence-as-a-view;
|
|
29
|
+
* the registered {@link WsChannel}s own the business logic. Mount {@link WsHub.upgradeHandler}
|
|
30
|
+
* on the Hono upgrade route(s) and hand {@link WsHub.handlers} to `serveBun`'s `websocket`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const hub = createWsHub({ channels: wsChannels, authenticate });
|
|
34
|
+
* app.get('/ws/:channel', hub.upgradeHandler);
|
|
35
|
+
* serveBun(app, { port, websocket: hub.handlers });
|
|
36
|
+
*/
|
|
37
|
+
export function createWsHub(opts) {
|
|
38
|
+
const clients = new Map();
|
|
39
|
+
const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
40
|
+
const heartbeatMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
|
|
41
|
+
const channelOf = (name) => opts.channels[name];
|
|
42
|
+
const clientFromWs = (ws) => clients.get(ws.data.clientId);
|
|
43
|
+
// Our heartbeat is authoritative (it also drives presence), so disable Bun's auto-ping
|
|
44
|
+
// and let its idle timeout backstop just beyond our stale window.
|
|
45
|
+
const idleTimeout = Math.min(Math.ceil(staleMs / 1000), BUN_IDLE_TIMEOUT_MAX_S);
|
|
46
|
+
const heartbeat = setInterval(() => {
|
|
47
|
+
sweepStaleClients(clients.values(), Date.now(), staleMs);
|
|
48
|
+
}, heartbeatMs);
|
|
49
|
+
// Don't keep the process alive for the heartbeat alone (server tests, graceful exit).
|
|
50
|
+
heartbeat.unref?.();
|
|
51
|
+
const upgradeHandler = async (c) => {
|
|
52
|
+
const channel = c.req.param('channel') ?? c.req.query('channel') ?? 'default';
|
|
53
|
+
const room = c.req.query('room') ?? undefined;
|
|
54
|
+
const handshake = { headers: c.req.raw.headers, url: c.req.url, channel, room };
|
|
55
|
+
let user;
|
|
56
|
+
try {
|
|
57
|
+
user = await opts.authenticate(handshake);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
user = null;
|
|
61
|
+
}
|
|
62
|
+
if (!user)
|
|
63
|
+
return c.json(toErrorEnvelope(new ApiError(401, 'Unauthorized')), 401);
|
|
64
|
+
const data = { clientId: crypto.randomUUID(), user, channel, room };
|
|
65
|
+
const server = c.env;
|
|
66
|
+
const upgraded = server.upgrade(c.req.raw, { data });
|
|
67
|
+
if (!upgraded)
|
|
68
|
+
return c.text('Upgrade required', 426);
|
|
69
|
+
// Bun has taken over the socket and sent the 101; this Response is discarded by
|
|
70
|
+
// Bun.serve. (A real 101 can't be constructed — the Fetch Response ctor forbids 1xx.)
|
|
71
|
+
return c.body(null, 204);
|
|
72
|
+
};
|
|
73
|
+
const hub = {
|
|
74
|
+
upgradeHandler,
|
|
75
|
+
handlers: {
|
|
76
|
+
idleTimeout,
|
|
77
|
+
sendPings: false,
|
|
78
|
+
open(ws) {
|
|
79
|
+
const { clientId, user, channel, room } = ws.data;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const client = {
|
|
82
|
+
id: clientId,
|
|
83
|
+
user,
|
|
84
|
+
ws,
|
|
85
|
+
channel,
|
|
86
|
+
connectedAt: now,
|
|
87
|
+
lastPingAt: now,
|
|
88
|
+
meta: {},
|
|
89
|
+
};
|
|
90
|
+
if (room)
|
|
91
|
+
joinRoom(client, room);
|
|
92
|
+
clients.set(clientId, client);
|
|
93
|
+
channelOf(channel)?.onConnect?.(client, hub);
|
|
94
|
+
},
|
|
95
|
+
message(ws, raw) {
|
|
96
|
+
const client = clientFromWs(ws);
|
|
97
|
+
if (!client)
|
|
98
|
+
return;
|
|
99
|
+
client.lastPingAt = Date.now();
|
|
100
|
+
let msg;
|
|
101
|
+
try {
|
|
102
|
+
msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return; // ignore malformed frames rather than crash the socket
|
|
106
|
+
}
|
|
107
|
+
channelOf(client.channel)?.onMessage(client, msg, hub);
|
|
108
|
+
},
|
|
109
|
+
// A pong (reply to our heartbeat ping) proves liveness without an app message.
|
|
110
|
+
pong(ws) {
|
|
111
|
+
const client = clientFromWs(ws);
|
|
112
|
+
if (client)
|
|
113
|
+
client.lastPingAt = Date.now();
|
|
114
|
+
},
|
|
115
|
+
close(ws) {
|
|
116
|
+
const client = clientFromWs(ws);
|
|
117
|
+
if (!client)
|
|
118
|
+
return;
|
|
119
|
+
channelOf(client.channel)?.onDisconnect?.(client, hub);
|
|
120
|
+
clients.delete(client.id);
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
broadcast(channel, event, data) {
|
|
124
|
+
const payload = encode(event, data);
|
|
125
|
+
for (const client of clients.values()) {
|
|
126
|
+
if (client.channel === channel)
|
|
127
|
+
client.ws.send(payload);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
broadcastToRoom(channel, roomId, event, data) {
|
|
131
|
+
const payload = encode(event, data);
|
|
132
|
+
for (const client of clients.values()) {
|
|
133
|
+
if (client.channel === channel && client.meta[ROOM_META_KEY] === roomId) {
|
|
134
|
+
client.ws.send(payload);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
send(clientId, event, data) {
|
|
139
|
+
clients.get(clientId)?.ws.send(encode(event, data));
|
|
140
|
+
},
|
|
141
|
+
getClients(channel) {
|
|
142
|
+
const all = [...clients.values()];
|
|
143
|
+
return channel ? all.filter((client) => client.channel === channel) : all;
|
|
144
|
+
},
|
|
145
|
+
getClient: (id) => clients.get(id),
|
|
146
|
+
getPresence(channel) {
|
|
147
|
+
return computePresence(clients.values(), channel, channelOf(channel)?.presence ?? false);
|
|
148
|
+
},
|
|
149
|
+
close() {
|
|
150
|
+
clearInterval(heartbeat);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
return hub;
|
|
154
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createEventBus } from './eventBus';
|
|
2
|
+
export { createWsHub } from './hub';
|
|
3
|
+
export { computePresence } from './presence';
|
|
4
|
+
export { joinRoom, leaveRoom, ROOM_META_KEY, roomOf } from './room';
|
|
5
|
+
export type { EventBus, EventBusData, PresenceEntry, PresenceState, WsChannel, WsClient, WsData, WsHandshake, WsHub, WsHubOpts, WsMessage, } from './types';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// cursedbelt-server WS hub — Bun-native upgrade, typed channel registry, heartbeat,
|
|
2
|
+
// presence-as-a-view, and the push-only event-bus subset. Pairs with the partysocket
|
|
3
|
+
// client in `cursedbelt/client`.
|
|
4
|
+
export { createEventBus } from './eventBus';
|
|
5
|
+
export { createWsHub } from './hub';
|
|
6
|
+
export { computePresence } from './presence';
|
|
7
|
+
export { joinRoom, leaveRoom, ROOM_META_KEY, roomOf } from './room';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PresenceState, WsClient } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Presence is a *view* over the live client map, not a tracked structure — there is no
|
|
4
|
+
* `PresenceTracker` class to keep in sync. The hub's `getPresence(channel)` delegates
|
|
5
|
+
* here; a channel that wants to push presence does
|
|
6
|
+
* `hub.broadcast('chat', 'presence', hub.getPresence('chat'))` from its lifecycle hooks.
|
|
7
|
+
*/
|
|
8
|
+
/** The presence snapshot for `channel`: one `{ userId, connectedAt }` row per live
|
|
9
|
+
* client on that channel, or empty when the channel doesn't opt into presence.
|
|
10
|
+
* Pure (no timers, no state) so it's trivially testable. */
|
|
11
|
+
export declare function computePresence(clients: Iterable<WsClient>, channel: string, presenceEnabled: boolean): PresenceState;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence is a *view* over the live client map, not a tracked structure — there is no
|
|
3
|
+
* `PresenceTracker` class to keep in sync. The hub's `getPresence(channel)` delegates
|
|
4
|
+
* here; a channel that wants to push presence does
|
|
5
|
+
* `hub.broadcast('chat', 'presence', hub.getPresence('chat'))` from its lifecycle hooks.
|
|
6
|
+
*/
|
|
7
|
+
/** The presence snapshot for `channel`: one `{ userId, connectedAt }` row per live
|
|
8
|
+
* client on that channel, or empty when the channel doesn't opt into presence.
|
|
9
|
+
* Pure (no timers, no state) so it's trivially testable. */
|
|
10
|
+
export function computePresence(clients, channel, presenceEnabled) {
|
|
11
|
+
if (!presenceEnabled)
|
|
12
|
+
return [];
|
|
13
|
+
const rows = [];
|
|
14
|
+
for (const client of clients) {
|
|
15
|
+
if (client.channel === channel) {
|
|
16
|
+
rows.push({ userId: client.user.id, connectedAt: client.connectedAt });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return rows;
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WsClient } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Rooms are not a separate stateful abstraction — they are a typed convention over
|
|
4
|
+
* {@link WsClient.meta}. A channel puts a client in a room with {@link joinRoom}; the
|
|
5
|
+
* hub's `broadcastToRoom` matches on the same key. This replaces the old `wsRoom.ts` /
|
|
6
|
+
* `reconnectableRoom.ts` classes: the hub owns connections + fan-out, the channel owns
|
|
7
|
+
* membership + any history buffer.
|
|
8
|
+
*/
|
|
9
|
+
/** The {@link WsClient.meta} key a client's current room id lives under. The hub's
|
|
10
|
+
* `broadcastToRoom` reads the same key — keep them in lockstep. */
|
|
11
|
+
export declare const ROOM_META_KEY = "roomId";
|
|
12
|
+
/** Put `client` in `roomId` (replaces any current room). */
|
|
13
|
+
export declare function joinRoom(client: WsClient, roomId: string): void;
|
|
14
|
+
/** Remove `client` from its room (a subsequent room broadcast won't reach it). */
|
|
15
|
+
export declare function leaveRoom(client: WsClient): void;
|
|
16
|
+
/** The client's current room id, or undefined if it isn't in one. */
|
|
17
|
+
export declare function roomOf(client: WsClient): string | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rooms are not a separate stateful abstraction — they are a typed convention over
|
|
3
|
+
* {@link WsClient.meta}. A channel puts a client in a room with {@link joinRoom}; the
|
|
4
|
+
* hub's `broadcastToRoom` matches on the same key. This replaces the old `wsRoom.ts` /
|
|
5
|
+
* `reconnectableRoom.ts` classes: the hub owns connections + fan-out, the channel owns
|
|
6
|
+
* membership + any history buffer.
|
|
7
|
+
*/
|
|
8
|
+
/** The {@link WsClient.meta} key a client's current room id lives under. The hub's
|
|
9
|
+
* `broadcastToRoom` reads the same key — keep them in lockstep. */
|
|
10
|
+
export const ROOM_META_KEY = 'roomId';
|
|
11
|
+
/** Put `client` in `roomId` (replaces any current room). */
|
|
12
|
+
export function joinRoom(client, roomId) {
|
|
13
|
+
client.meta[ROOM_META_KEY] = roomId;
|
|
14
|
+
}
|
|
15
|
+
/** Remove `client` from its room (a subsequent room broadcast won't reach it). */
|
|
16
|
+
export function leaveRoom(client) {
|
|
17
|
+
delete client.meta[ROOM_META_KEY];
|
|
18
|
+
}
|
|
19
|
+
/** The client's current room id, or undefined if it isn't in one. */
|
|
20
|
+
export function roomOf(client) {
|
|
21
|
+
const room = client.meta[ROOM_META_KEY];
|
|
22
|
+
return typeof room === 'string' ? room : undefined;
|
|
23
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ServerWebSocket, WebSocketHandler } from 'bun';
|
|
2
|
+
import type { MiddlewareHandler } from 'hono';
|
|
3
|
+
import type { AuthUser } from '../auth/types';
|
|
4
|
+
/**
|
|
5
|
+
* Shared types for the cursedbelt WebSocket hub.
|
|
6
|
+
*
|
|
7
|
+
* cursedbelt owns the *transport* — connection lifecycle, channel routing,
|
|
8
|
+
* heartbeat/stale-cull, presence-as-a-view, and an injected `authenticate`
|
|
9
|
+
* callback. The *channels* (what each message means, the per-feature game/chat
|
|
10
|
+
* state) stay app-business; an app registers its channels into the hub and
|
|
11
|
+
* consumes the broadcast helpers. ru's push-only variant is {@link EventBus}.
|
|
12
|
+
*
|
|
13
|
+
* Built on Bun's native WS upgrade (`Server.upgrade` + `Bun.serve { websocket }`),
|
|
14
|
+
* surfaced through Hono via {@link WsHub.upgradeHandler}. No `ws`-lib, no adapter.
|
|
15
|
+
*/
|
|
16
|
+
/** The JSON envelope every frame uses, both directions: `{ event, data }`. The hub
|
|
17
|
+
* emits this from {@link WsHub.broadcast}/`send`; channels receive the parsed inbound
|
|
18
|
+
* envelope as their `onMessage` payload. `T` narrows `data` for a typed channel. */
|
|
19
|
+
export interface WsMessage<T = unknown> {
|
|
20
|
+
event: string;
|
|
21
|
+
data: T;
|
|
22
|
+
}
|
|
23
|
+
/** What the hub stashes on the Bun `ServerWebSocket.data` at upgrade time so `open`
|
|
24
|
+
* can register the client without a second handshake round-trip (see the gotcha in
|
|
25
|
+
* the task spec). `room` is the optional `?room=` query param — a named room within
|
|
26
|
+
* the channel, applied to {@link WsClient.meta} on connect. */
|
|
27
|
+
export interface WsData {
|
|
28
|
+
clientId: string;
|
|
29
|
+
user: AuthUser;
|
|
30
|
+
channel: string;
|
|
31
|
+
room?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The minimal view of the WS handshake handed to {@link WsHubOpts.authenticate}. The
|
|
34
|
+
* app reads its auth cookie off `headers` and verifies it (cursedbelt never reads the
|
|
35
|
+
* cookie itself — auth *model* is app business). */
|
|
36
|
+
export interface WsHandshake {
|
|
37
|
+
headers: Headers;
|
|
38
|
+
url: string;
|
|
39
|
+
channel: string;
|
|
40
|
+
room?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A live connection. `meta` is the per-channel scratch bag (room membership via the
|
|
43
|
+
* `room.ts` helpers, game seat, typing flag, …) — the hub never interprets it beyond
|
|
44
|
+
* {@link WsHub.broadcastToRoom}'s room match. `lastPingAt` is "last time we heard from
|
|
45
|
+
* this client" (an inbound message OR a pong); the heartbeat culls on it. */
|
|
46
|
+
export interface WsClient {
|
|
47
|
+
id: string;
|
|
48
|
+
user: AuthUser;
|
|
49
|
+
ws: ServerWebSocket<WsData>;
|
|
50
|
+
channel: string;
|
|
51
|
+
connectedAt: number;
|
|
52
|
+
lastPingAt: number;
|
|
53
|
+
meta: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
/** A business channel registered into the hub. The hub routes inbound frames to
|
|
56
|
+
* `onMessage` by {@link WsClient.channel} and fires the lifecycle hooks; everything
|
|
57
|
+
* the handler *does* (broadcast shape, game state, history) is app business. */
|
|
58
|
+
export interface WsChannel<TMsg = WsMessage> {
|
|
59
|
+
name: string;
|
|
60
|
+
/** Fired after the client is registered (room meta already applied). */
|
|
61
|
+
onConnect?(client: WsClient, hub: WsHub): void;
|
|
62
|
+
/** Fired once per inbound frame (the parsed `{ event, data }` envelope). */
|
|
63
|
+
onMessage(client: WsClient, msg: TMsg, hub: WsHub): void;
|
|
64
|
+
/** Fired when the socket closes (explicit, stale-culled, or peer-dropped). */
|
|
65
|
+
onDisconnect?(client: WsClient, hub: WsHub): void;
|
|
66
|
+
/** Whether this channel contributes to {@link WsHub.getPresence}. Default: false. */
|
|
67
|
+
presence?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** A presence row — the public, business-safe view of a connected client. */
|
|
70
|
+
export interface PresenceEntry {
|
|
71
|
+
userId: string;
|
|
72
|
+
connectedAt: number;
|
|
73
|
+
}
|
|
74
|
+
/** The presence snapshot for a channel: one row per live, presence-enabled client. */
|
|
75
|
+
export type PresenceState = PresenceEntry[];
|
|
76
|
+
export interface WsHubOpts<TChannelMap extends Record<string, WsChannel<any>>> {
|
|
77
|
+
/** The business channel registry, keyed by channel name (the `?channel=`/`:channel`
|
|
78
|
+
* the client connects to). */
|
|
79
|
+
channels: TChannelMap;
|
|
80
|
+
/** Cookie→user verification for the handshake. Return `null` to reject the upgrade
|
|
81
|
+
* with a 401 (an HTTP 401, not a WS close). */
|
|
82
|
+
authenticate(handshake: WsHandshake): Promise<AuthUser | null>;
|
|
83
|
+
/** How often the heartbeat pings live clients + sweeps stale ones. Default 30_000. */
|
|
84
|
+
heartbeatIntervalMs?: number;
|
|
85
|
+
/** A client unheard-from for longer than this is culled. Default 120_000 (2 min). */
|
|
86
|
+
staleMs?: number;
|
|
87
|
+
}
|
|
88
|
+
export interface WsHub {
|
|
89
|
+
/** The Bun `websocket` config — pass straight to `serveBun(app, { websocket: hub.handlers })`.
|
|
90
|
+
* `message` is required by Bun; `open`/`close`/`pong` drive lifecycle + liveness. */
|
|
91
|
+
handlers: WebSocketHandler<WsData>;
|
|
92
|
+
/** Mount on the Hono upgrade route(s): `app.get('/ws/:channel', hub.upgradeHandler)`.
|
|
93
|
+
* Authenticates, then calls `Server.upgrade` (Bun-native). */
|
|
94
|
+
upgradeHandler: MiddlewareHandler;
|
|
95
|
+
/** Send `{ event, data }` to every client on `channel`. */
|
|
96
|
+
broadcast(channel: string, event: string, data: unknown): void;
|
|
97
|
+
/** Send `{ event, data }` to every client on `channel` whose room (meta) is `roomId`. */
|
|
98
|
+
broadcastToRoom(channel: string, roomId: string, event: string, data: unknown): void;
|
|
99
|
+
/** Send `{ event, data }` to one client by id (no-op if gone). */
|
|
100
|
+
send(clientId: string, event: string, data: unknown): void;
|
|
101
|
+
/** Live clients, optionally filtered to one channel. */
|
|
102
|
+
getClients(channel?: string): WsClient[];
|
|
103
|
+
/** One client by id, or undefined. */
|
|
104
|
+
getClient(clientId: string): WsClient | undefined;
|
|
105
|
+
/** The presence snapshot for a presence-enabled channel (empty otherwise). */
|
|
106
|
+
getPresence(channel: string): PresenceState;
|
|
107
|
+
/** Stop the heartbeat timer (call on shutdown / in tests to avoid a leaked interval). */
|
|
108
|
+
close(): void;
|
|
109
|
+
}
|
|
110
|
+
/** ru's push-only subset: broadcast-to-all, no auth, no inbound routing. One bus,
|
|
111
|
+
* mounted once; ru's task queue / caSync / Forge workers call `broadcast`. */
|
|
112
|
+
export interface EventBus {
|
|
113
|
+
/** Bun `websocket` config. `message` is a documented no-op (push-only). */
|
|
114
|
+
handlers: WebSocketHandler<EventBusData>;
|
|
115
|
+
upgradeHandler: MiddlewareHandler;
|
|
116
|
+
/** Push `{ event, data }` to every connected client. */
|
|
117
|
+
broadcast(event: string, data: unknown): void;
|
|
118
|
+
/** Number of connected clients. */
|
|
119
|
+
clientCount(): number;
|
|
120
|
+
}
|
|
121
|
+
/** The event bus carries no per-socket data (no auth, no channel). */
|
|
122
|
+
export type EventBusData = Record<never, never>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|