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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain-HTTP surface for the notification center.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists (2026-08-11) ────────────────────────────────────────────
|
|
5
|
+
* The engine (`cwip/notifications`), the store and the whole client
|
|
6
|
+
* (`cursedbelt/react/notifications`) were already fleet-wide. The only mounted
|
|
7
|
+
* SERVER surface was `orch-ui/services/notifications`, written against
|
|
8
|
+
* `@satellites/satellite-rpc` and living inside a package only the orchestrator
|
|
9
|
+
* consumes — so every other satellite's route to a notification was "adopt
|
|
10
|
+
* orch's rpc layer" or "write it again". This is that surface as ordinary
|
|
11
|
+
* routes on an ordinary Hono app, one mount call, no rpc.
|
|
12
|
+
*
|
|
13
|
+
* The route table mirrors the rpc one 1:1 on purpose: the two are the same
|
|
14
|
+
* contract in two dialects, and a client written against either sees the same
|
|
15
|
+
* fields. `mutations` are POST/DELETE and `queries` are GET, so the GET-only
|
|
16
|
+
* enforcement middleware some satellites run stays meaningful.
|
|
17
|
+
*
|
|
18
|
+
* GET {base} → { items, hasMore } ?filter=&limit=&offset=
|
|
19
|
+
* GET {base}/unread-count → { count }
|
|
20
|
+
* GET {base}/stream → SSE (live delivery; needs a hub)
|
|
21
|
+
* GET {base}/:id → { notification }
|
|
22
|
+
* POST {base}/read-all → { changed }
|
|
23
|
+
* POST {base}/done-all → { changed }
|
|
24
|
+
* POST {base}/:id/read → { changed } body { read?: boolean }
|
|
25
|
+
* POST {base}/:id/done → { changed } body { done?: boolean }
|
|
26
|
+
* DELETE {base}/:id → { changed }
|
|
27
|
+
*
|
|
28
|
+
* 🔴 **Every handler addresses `authorize(c)`'s user and never a body field.**
|
|
29
|
+
* The center is user-scoped at the store level, but a route that took a
|
|
30
|
+
* `userId` parameter would make that scoping decorative — so there is no such
|
|
31
|
+
* parameter anywhere below, and there is deliberately no route that notifies
|
|
32
|
+
* somebody else. Producing notifications is server-side business logic calling
|
|
33
|
+
* `center.notify*` directly; a browser can only ever read and acknowledge its
|
|
34
|
+
* OWN inbox.
|
|
35
|
+
*/
|
|
36
|
+
import type { Context, Hono } from "hono";
|
|
37
|
+
import type { NotificationCenter, NotificationFilter } from "cwip/notifications";
|
|
38
|
+
import type { NotificationHub } from "./hub";
|
|
39
|
+
|
|
40
|
+
/** The slice of the center these routes use — `createNotificationCenter(...)`
|
|
41
|
+
* satisfies it, and so does any app-specific facade over one. */
|
|
42
|
+
export type NotificationRouteCenter = Pick<
|
|
43
|
+
NotificationCenter,
|
|
44
|
+
"list" | "get" | "unreadCount" | "markRead" | "markAllRead" | "setDone" | "doneAll" | "remove"
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
export interface NotificationRoutesOptions {
|
|
48
|
+
center: NotificationRouteCenter;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the caller to a user id, or null to refuse with 401. This is the
|
|
51
|
+
* app's session model and the ONLY thing that decides whose inbox is served.
|
|
52
|
+
*/
|
|
53
|
+
authorize: (c: Context) => string | null | Promise<string | null>;
|
|
54
|
+
/** Live delivery. Omit it and `{base}/stream` is not mounted at all — a
|
|
55
|
+
* route that answers 200 with a stream that never emits is worse than a
|
|
56
|
+
* 404 the client can fall back from. */
|
|
57
|
+
hub?: NotificationHub;
|
|
58
|
+
/** Path prefix. Default `/api/notifications`. */
|
|
59
|
+
basePath?: string;
|
|
60
|
+
/** Largest page a caller may ask for. Default 200. */
|
|
61
|
+
maxLimit?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const NO_STORE = { "cache-control": "no-store" } as const;
|
|
65
|
+
|
|
66
|
+
const FILTERS: readonly NotificationFilter[] = ["unread", "all", "done"];
|
|
67
|
+
|
|
68
|
+
const parseFilter = (raw: string | undefined): NotificationFilter | undefined =>
|
|
69
|
+
FILTERS.find((f) => f === raw);
|
|
70
|
+
|
|
71
|
+
/** Query integers are untrusted text: a NaN or a negative offset must become
|
|
72
|
+
* the default, never reach SQL, and never be an error the caller has to handle. */
|
|
73
|
+
const parseInt_ = (raw: string | undefined, min: number, max: number): number | undefined => {
|
|
74
|
+
if (raw === undefined || raw === "") return undefined;
|
|
75
|
+
const value = Number.parseInt(raw, 10);
|
|
76
|
+
if (!Number.isFinite(value)) return undefined;
|
|
77
|
+
return Math.min(max, Math.max(min, value));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** A body that is absent, malformed, or oversized is `{}` — every field these
|
|
81
|
+
* routes read is optional with a documented default, so there is nothing a
|
|
82
|
+
* caller could usefully be told about a bad one. */
|
|
83
|
+
const readFlag = async (c: Context, key: "read" | "done"): Promise<boolean> => {
|
|
84
|
+
try {
|
|
85
|
+
const raw = await c.req.text();
|
|
86
|
+
if (!raw || raw.length > 1024) return true;
|
|
87
|
+
const body = JSON.parse(raw) as Record<string, unknown> | null;
|
|
88
|
+
const value = body?.[key];
|
|
89
|
+
return typeof value === "boolean" ? value : true;
|
|
90
|
+
} catch {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function mountNotificationRoutes(app: Hono, options: NotificationRoutesOptions): void {
|
|
96
|
+
const base = options.basePath ?? "/api/notifications";
|
|
97
|
+
const maxLimit = options.maxLimit ?? 200;
|
|
98
|
+
const { center, hub } = options;
|
|
99
|
+
|
|
100
|
+
/** Every handler starts here. Returns the user id, or the 401 to return. */
|
|
101
|
+
const whoOr401 = async (c: Context): Promise<string | Response> => {
|
|
102
|
+
const userId = await options.authorize(c);
|
|
103
|
+
if (!userId) return c.json({ error: "unauthorized" }, 401, NO_STORE);
|
|
104
|
+
return userId;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
app.get(base, async (c) => {
|
|
108
|
+
const who = await whoOr401(c);
|
|
109
|
+
if (typeof who !== "string") return who;
|
|
110
|
+
const page = await center.list({
|
|
111
|
+
userId: who,
|
|
112
|
+
filter: parseFilter(c.req.query("filter")),
|
|
113
|
+
limit: parseInt_(c.req.query("limit"), 1, maxLimit),
|
|
114
|
+
offset: parseInt_(c.req.query("offset"), 0, Number.MAX_SAFE_INTEGER),
|
|
115
|
+
});
|
|
116
|
+
return c.json(page, 200, NO_STORE);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.get(`${base}/unread-count`, async (c) => {
|
|
120
|
+
const who = await whoOr401(c);
|
|
121
|
+
if (typeof who !== "string") return who;
|
|
122
|
+
return c.json({ count: await center.unreadCount(who) }, 200, NO_STORE);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Mounted only when there is something to stream — see the option's doc.
|
|
126
|
+
if (hub) {
|
|
127
|
+
app.get(`${base}/stream`, async (c) => {
|
|
128
|
+
const who = await whoOr401(c);
|
|
129
|
+
if (typeof who !== "string") return who;
|
|
130
|
+
// The request's own abort signal is what ends the stream when the tab
|
|
131
|
+
// closes; without it the subscription outlives the socket.
|
|
132
|
+
return hub.stream(who, { signal: c.req.raw.signal });
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
app.post(`${base}/read-all`, async (c) => {
|
|
137
|
+
const who = await whoOr401(c);
|
|
138
|
+
if (typeof who !== "string") return who;
|
|
139
|
+
return c.json({ changed: await center.markAllRead(who) }, 200, NO_STORE);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
app.post(`${base}/done-all`, async (c) => {
|
|
143
|
+
const who = await whoOr401(c);
|
|
144
|
+
if (typeof who !== "string") return who;
|
|
145
|
+
return c.json({ changed: await center.doneAll(who) }, 200, NO_STORE);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
app.post(`${base}/:id/read`, async (c) => {
|
|
149
|
+
const who = await whoOr401(c);
|
|
150
|
+
if (typeof who !== "string") return who;
|
|
151
|
+
const read = await readFlag(c, "read");
|
|
152
|
+
return c.json({ changed: await center.markRead(who, c.req.param("id"), read) }, 200, NO_STORE);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
app.post(`${base}/:id/done`, async (c) => {
|
|
156
|
+
const who = await whoOr401(c);
|
|
157
|
+
if (typeof who !== "string") return who;
|
|
158
|
+
const done = await readFlag(c, "done");
|
|
159
|
+
return c.json({ changed: await center.setDone(who, c.req.param("id"), done) }, 200, NO_STORE);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.delete(`${base}/:id`, async (c) => {
|
|
163
|
+
const who = await whoOr401(c);
|
|
164
|
+
if (typeof who !== "string") return who;
|
|
165
|
+
return c.json({ changed: await center.remove(who, c.req.param("id")) }, 200, NO_STORE);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// LAST: `/:id` would otherwise swallow `/unread-count` and `/stream`, and
|
|
169
|
+
// the failure is a 404-shaped `{ notification: null }` rather than an error.
|
|
170
|
+
app.get(`${base}/:id`, async (c) => {
|
|
171
|
+
const who = await whoOr401(c);
|
|
172
|
+
if (typeof who !== "string") return who;
|
|
173
|
+
const notification = await center.get(who, c.req.param("id"));
|
|
174
|
+
if (!notification) return c.json({ error: "not found" }, 404, NO_STORE);
|
|
175
|
+
return c.json({ notification }, 200, NO_STORE);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server half end to end: a real SQLite database, a real Hono app, real
|
|
3
|
+
* HTTP requests through `app.fetch`.
|
|
4
|
+
*
|
|
5
|
+
* Every assertion here is about a property a consuming app depends on — the
|
|
6
|
+
* user scoping, the coalescing, the acknowledgment verbs, and the two that
|
|
7
|
+
* would be silent if they broke: that `/unread-count` is not swallowed by
|
|
8
|
+
* `/:id`, and that a stream ends when its request is aborted.
|
|
9
|
+
*/
|
|
10
|
+
import { Database } from "bun:sqlite";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import { Hono } from "hono";
|
|
13
|
+
import { createNotificationHub } from "./hub";
|
|
14
|
+
import { NOTIFICATION_MIGRATIONS, NOTIFICATIONS_TABLE } from "./migrations";
|
|
15
|
+
import { createNotificationService, type NotificationService } from "./service";
|
|
16
|
+
|
|
17
|
+
let db: Database;
|
|
18
|
+
let service: NotificationService;
|
|
19
|
+
let app: Hono;
|
|
20
|
+
/** Whose session the next request carries. `null` = signed out. */
|
|
21
|
+
let who: string | null;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
db = new Database(":memory:");
|
|
25
|
+
who = "u1";
|
|
26
|
+
service = createNotificationService({ db, live: { heartbeatMs: 0 }, onError: () => {} });
|
|
27
|
+
app = new Hono();
|
|
28
|
+
service.mount(app, { authorize: () => who });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
service.close();
|
|
33
|
+
db.close();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const get = (path: string) => app.fetch(new Request(`http://t${path}`));
|
|
37
|
+
const post = (path: string, body?: unknown) =>
|
|
38
|
+
app.fetch(
|
|
39
|
+
new Request(`http://t${path}`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
const del = (path: string) => app.fetch(new Request(`http://t${path}`, { method: "DELETE" }));
|
|
45
|
+
|
|
46
|
+
describe("the inbox surface", () => {
|
|
47
|
+
test("a notification is listed, counted, and carries its deep link", async () => {
|
|
48
|
+
await service.center.notify("u1", {
|
|
49
|
+
type: "auth.login",
|
|
50
|
+
title: "New sign-in",
|
|
51
|
+
body: "from 10.0.0.1",
|
|
52
|
+
link: "/#/account",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const page = await (await get("/api/notifications")).json();
|
|
57
|
+
expect(page.items).toHaveLength(1);
|
|
58
|
+
expect(page.items[0]).toMatchObject({
|
|
59
|
+
type: "auth.login",
|
|
60
|
+
title: "New sign-in",
|
|
61
|
+
link: "/#/account",
|
|
62
|
+
severity: "warning",
|
|
63
|
+
read: false,
|
|
64
|
+
count: 1,
|
|
65
|
+
});
|
|
66
|
+
expect(page.hasMore).toBe(false);
|
|
67
|
+
expect(await (await get("/api/notifications/unread-count")).json()).toEqual({ count: 1 });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("🔴 one user never sees another's inbox", async () => {
|
|
71
|
+
await service.center.notify("u2", { type: "t", title: "not yours" });
|
|
72
|
+
|
|
73
|
+
expect((await (await get("/api/notifications")).json()).items).toEqual([]);
|
|
74
|
+
expect(await (await get("/api/notifications/unread-count")).json()).toEqual({ count: 0 });
|
|
75
|
+
|
|
76
|
+
// …and the id being guessable changes nothing.
|
|
77
|
+
const theirs = await service.center.notify("u2", { type: "t", title: "theirs" });
|
|
78
|
+
expect((await get(`/api/notifications/${theirs?.id}`)).status).toBe(404);
|
|
79
|
+
expect(await (await post(`/api/notifications/${theirs?.id}/read`)).json()).toEqual({
|
|
80
|
+
changed: false,
|
|
81
|
+
});
|
|
82
|
+
expect(await service.center.unreadCount("u2")).toBe(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("🔴 signed out is 401 on every route, and no route leaks a body", async () => {
|
|
86
|
+
who = null;
|
|
87
|
+
for (const response of [
|
|
88
|
+
await get("/api/notifications"),
|
|
89
|
+
await get("/api/notifications/unread-count"),
|
|
90
|
+
await get("/api/notifications/anything"),
|
|
91
|
+
await get("/api/notifications/stream"),
|
|
92
|
+
await post("/api/notifications/read-all"),
|
|
93
|
+
await post("/api/notifications/done-all"),
|
|
94
|
+
await post("/api/notifications/x/read"),
|
|
95
|
+
await post("/api/notifications/x/done"),
|
|
96
|
+
await del("/api/notifications/x"),
|
|
97
|
+
]) {
|
|
98
|
+
expect(response.status).toBe(401);
|
|
99
|
+
expect(await response.json()).toEqual({ error: "unauthorized" });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("🔴 /unread-count and /stream are routes, not ids", async () => {
|
|
104
|
+
// Registration order is the whole property: `/:id` is mounted last, so a
|
|
105
|
+
// static segment can never be read as a notification id.
|
|
106
|
+
expect((await get("/api/notifications/unread-count")).status).toBe(200);
|
|
107
|
+
expect((await get("/api/notifications/stream")).headers.get("content-type")).toContain(
|
|
108
|
+
"text/event-stream",
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("repeat events with a groupKey coalesce into one counted row", async () => {
|
|
113
|
+
for (let i = 0; i < 5; i++) {
|
|
114
|
+
await service.center.notify("u1", {
|
|
115
|
+
type: "auth.login",
|
|
116
|
+
title: `sign-in ${i}`,
|
|
117
|
+
groupKey: "auth.login",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const page = await (await get("/api/notifications")).json();
|
|
121
|
+
expect(page.items).toHaveLength(1);
|
|
122
|
+
expect(page.items[0].count).toBe(5);
|
|
123
|
+
// The newest event wins the row's copy — the count says how many.
|
|
124
|
+
expect(page.items[0].title).toBe("sign-in 4");
|
|
125
|
+
expect(await (await get("/api/notifications/unread-count")).json()).toEqual({ count: 1 });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("acknowledging: read, read-all, done, done-all, remove", async () => {
|
|
129
|
+
const a = await service.center.notify("u1", { type: "t", title: "a" });
|
|
130
|
+
const b = await service.center.notify("u1", { type: "t", title: "b" });
|
|
131
|
+
|
|
132
|
+
expect(await (await post(`/api/notifications/${a?.id}/read`)).json()).toEqual({
|
|
133
|
+
changed: true,
|
|
134
|
+
});
|
|
135
|
+
expect(await (await get("/api/notifications/unread-count")).json()).toEqual({ count: 1 });
|
|
136
|
+
|
|
137
|
+
// …and the flag is honored, not assumed.
|
|
138
|
+
expect(
|
|
139
|
+
await (await post(`/api/notifications/${a?.id}/read`, { read: false })).json(),
|
|
140
|
+
).toEqual({ changed: true });
|
|
141
|
+
expect(await (await get("/api/notifications/unread-count")).json()).toEqual({ count: 2 });
|
|
142
|
+
|
|
143
|
+
expect(await (await post("/api/notifications/read-all")).json()).toEqual({ changed: 2 });
|
|
144
|
+
|
|
145
|
+
expect(await (await post(`/api/notifications/${b?.id}/done`)).json()).toEqual({
|
|
146
|
+
changed: true,
|
|
147
|
+
});
|
|
148
|
+
let page = await (await get("/api/notifications")).json();
|
|
149
|
+
expect(page.items.map((n: { id: string }) => n.id)).toEqual([a?.id]);
|
|
150
|
+
page = await (await get("/api/notifications?filter=done")).json();
|
|
151
|
+
expect(page.items.map((n: { id: string }) => n.id)).toEqual([b?.id]);
|
|
152
|
+
|
|
153
|
+
expect(await (await del(`/api/notifications/${a?.id}`)).json()).toEqual({ changed: true });
|
|
154
|
+
expect(await (await post("/api/notifications/done-all")).json()).toEqual({ changed: 0 });
|
|
155
|
+
expect((await (await get("/api/notifications")).json()).items).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a malformed or oversized body falls back to the documented default", async () => {
|
|
159
|
+
const n = await service.center.notify("u1", { type: "t", title: "a" });
|
|
160
|
+
const response = await app.fetch(
|
|
161
|
+
new Request(`http://t/api/notifications/${n?.id}/read`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: "{not json",
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
// Default is `read: true` — a bad body must not become a 500 or a no-op.
|
|
167
|
+
expect(await response.json()).toEqual({ changed: true });
|
|
168
|
+
expect(await service.center.unreadCount("u1")).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("limit and offset are clamped, and junk becomes the default", async () => {
|
|
172
|
+
for (let i = 0; i < 4; i++) await service.center.notify("u1", { type: "t", title: `n${i}` });
|
|
173
|
+
|
|
174
|
+
expect((await (await get("/api/notifications?limit=2")).json()).hasMore).toBe(true);
|
|
175
|
+
expect((await (await get("/api/notifications?limit=2&offset=2")).json()).items).toHaveLength(2);
|
|
176
|
+
// max 200 by default; a bigger ask is clamped rather than refused
|
|
177
|
+
expect((await (await get("/api/notifications?limit=99999")).json()).items).toHaveLength(4);
|
|
178
|
+
// …and unparseable values must not reach SQL
|
|
179
|
+
expect((await (await get("/api/notifications?limit=abc&offset=-9")).json()).items).toHaveLength(
|
|
180
|
+
4,
|
|
181
|
+
);
|
|
182
|
+
expect((await (await get("/api/notifications?filter=nonsense")).json()).items).toHaveLength(4);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("live delivery", () => {
|
|
187
|
+
test("the sink reaches exactly the addressed user's subscribers", async () => {
|
|
188
|
+
const mine: string[] = [];
|
|
189
|
+
const theirs: string[] = [];
|
|
190
|
+
const hub = service.hub;
|
|
191
|
+
if (!hub) throw new Error("expected a hub");
|
|
192
|
+
hub.subscribe("u1", (e) => mine.push(e.kind));
|
|
193
|
+
hub.subscribe("u2", (e) => theirs.push(e.kind));
|
|
194
|
+
|
|
195
|
+
await service.center.notify("u1", { type: "t", title: "a" });
|
|
196
|
+
|
|
197
|
+
expect(mine).toEqual(["notification", "unread"]);
|
|
198
|
+
expect(theirs).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("🔴 an aborted request unsubscribes — a closed tab is not a leak", async () => {
|
|
202
|
+
const hub = service.hub;
|
|
203
|
+
if (!hub) throw new Error("expected a hub");
|
|
204
|
+
const controller = new AbortController();
|
|
205
|
+
const response = await app.fetch(
|
|
206
|
+
new Request("http://t/api/notifications/stream", { signal: controller.signal }),
|
|
207
|
+
);
|
|
208
|
+
const reader = response.body?.getReader();
|
|
209
|
+
// The opening comment line proves the stream is genuinely open.
|
|
210
|
+
expect(new TextDecoder().decode((await reader?.read())?.value)).toBe(": connected\n\n");
|
|
211
|
+
expect(hub.subscriberCount("u1")).toBe(1);
|
|
212
|
+
|
|
213
|
+
controller.abort();
|
|
214
|
+
await Promise.resolve();
|
|
215
|
+
expect(hub.subscriberCount("u1")).toBe(0);
|
|
216
|
+
await reader?.cancel().catch(() => {});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("a notification arrives on an open stream as an SSE event", async () => {
|
|
220
|
+
const response = await get("/api/notifications/stream");
|
|
221
|
+
const reader = response.body?.getReader();
|
|
222
|
+
await reader?.read(); // ": connected"
|
|
223
|
+
|
|
224
|
+
await service.center.notify("u1", { type: "t", title: "hello", link: "/#/x" });
|
|
225
|
+
|
|
226
|
+
const frame = new TextDecoder().decode((await reader?.read())?.value);
|
|
227
|
+
expect(frame.startsWith("event: notification\ndata: ")).toBe(true);
|
|
228
|
+
expect(JSON.parse(frame.slice("event: notification\ndata: ".length))).toMatchObject({
|
|
229
|
+
title: "hello",
|
|
230
|
+
link: "/#/x",
|
|
231
|
+
userId: "u1",
|
|
232
|
+
});
|
|
233
|
+
await reader?.cancel().catch(() => {});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("live: false mounts no stream route at all", async () => {
|
|
237
|
+
const plain = createNotificationService({ db, live: false });
|
|
238
|
+
const plainApp = new Hono();
|
|
239
|
+
plain.mount(plainApp, { authorize: () => "u1" });
|
|
240
|
+
const response = await plainApp.fetch(new Request("http://t/api/notifications/stream"));
|
|
241
|
+
// Falls through to `/:id` → a notification that does not exist.
|
|
242
|
+
expect(response.status).toBe(404);
|
|
243
|
+
expect(plain.hub).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("a user's streams are capped, oldest first", () => {
|
|
247
|
+
const hub = createNotificationHub({ heartbeatMs: 0, maxStreamsPerUser: 2 });
|
|
248
|
+
const seen: number[] = [];
|
|
249
|
+
hub.subscribe("u1", () => seen.push(1));
|
|
250
|
+
hub.subscribe("u1", () => seen.push(2));
|
|
251
|
+
hub.subscribe("u1", () => seen.push(3));
|
|
252
|
+
expect(hub.subscriberCount("u1")).toBe(2);
|
|
253
|
+
// The evicted subscriber is the one that was there first.
|
|
254
|
+
expect(seen).toEqual([1]);
|
|
255
|
+
hub.close();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("a throwing subscriber is dropped, and the next one still hears it", () => {
|
|
259
|
+
const hub = createNotificationHub({ heartbeatMs: 0, onError: () => {} });
|
|
260
|
+
const heard: string[] = [];
|
|
261
|
+
hub.subscribe("u1", () => {
|
|
262
|
+
throw new Error("boom");
|
|
263
|
+
});
|
|
264
|
+
hub.subscribe("u1", () => heard.push("ok"));
|
|
265
|
+
hub.sink.onUnread?.("u1", 3);
|
|
266
|
+
expect(heard).toEqual(["ok"]);
|
|
267
|
+
expect(hub.subscriberCount("u1")).toBe(1);
|
|
268
|
+
hub.close();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("the table", () => {
|
|
273
|
+
test("the migration and the store converge on one table, in either order", () => {
|
|
274
|
+
const fresh = new Database(":memory:");
|
|
275
|
+
for (const migration of NOTIFICATION_MIGRATIONS) migration.up(fresh);
|
|
276
|
+
// Constructing the store over an already-migrated database must not throw
|
|
277
|
+
// — an app that adopted the store first and the runner second is normal.
|
|
278
|
+
const second = createNotificationService({ db: fresh, live: false });
|
|
279
|
+
const columns = (
|
|
280
|
+
fresh.query(`PRAGMA table_info(${NOTIFICATIONS_TABLE})`).all() as { name: string }[]
|
|
281
|
+
).map((c) => c.name);
|
|
282
|
+
expect(columns).toContain("group_key");
|
|
283
|
+
expect(columns).toContain("severity");
|
|
284
|
+
second.close();
|
|
285
|
+
fresh.close();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("🔴 coalescing is enforced by the index, not only by the read", () => {
|
|
289
|
+
// Two open rows in one group would make `count` a lie the moment two
|
|
290
|
+
// producers raced. The partial unique index is what forbids it.
|
|
291
|
+
const fresh = new Database(":memory:");
|
|
292
|
+
const svc = createNotificationService({ db: fresh, live: false });
|
|
293
|
+
const insert = () =>
|
|
294
|
+
fresh.run(
|
|
295
|
+
`INSERT INTO ${NOTIFICATIONS_TABLE}
|
|
296
|
+
(id, user_id, created_at, updated_at, type, severity, title, group_key, count, read, done)
|
|
297
|
+
VALUES (?, 'u1', 0, 0, 't', 'info', 'x', 'g', 1, 0, 0)`,
|
|
298
|
+
[crypto.randomUUID()],
|
|
299
|
+
);
|
|
300
|
+
insert();
|
|
301
|
+
expect(insert).toThrow();
|
|
302
|
+
svc.close();
|
|
303
|
+
fresh.close();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One call that gives an app the whole server half: a migrated table, the
|
|
3
|
+
* dispatch center, live delivery, and the routes.
|
|
4
|
+
*
|
|
5
|
+
* const notifications = createNotificationService({ db });
|
|
6
|
+
* notifications.mount(app, { authorize: (c) => userOf(c)?.id ?? null });
|
|
7
|
+
* // …anywhere in the app's business logic:
|
|
8
|
+
* await notifications.center.notify(userId, { type: "auth.login", title: "New sign-in" });
|
|
9
|
+
*
|
|
10
|
+
* Everything it composes is separately usable — an app that already has a
|
|
11
|
+
* center, or that wants the routes on a store of its own, calls
|
|
12
|
+
* `mountNotificationRoutes` directly. This exists so the common case is one
|
|
13
|
+
* import and no wiring to get wrong.
|
|
14
|
+
*/
|
|
15
|
+
import type { Database } from "bun:sqlite";
|
|
16
|
+
import {
|
|
17
|
+
createNotificationCenter,
|
|
18
|
+
createSqliteNotificationStore,
|
|
19
|
+
type NotificationCenter,
|
|
20
|
+
type NotificationDb,
|
|
21
|
+
type NotificationStore,
|
|
22
|
+
} from "cwip/notifications";
|
|
23
|
+
import type { Context, Hono } from "hono";
|
|
24
|
+
import { createNotificationHub, type NotificationHub } from "./hub";
|
|
25
|
+
import { mountNotificationRoutes } from "./routes";
|
|
26
|
+
|
|
27
|
+
export interface CreateNotificationServiceOptions {
|
|
28
|
+
/** The app's own database — the table lands beside its rows. */
|
|
29
|
+
db: Database;
|
|
30
|
+
/** Table name override (identifier-validated by the store). */
|
|
31
|
+
table?: string;
|
|
32
|
+
/** Resolve a role audience (`notifyAudience({ role })`) to user ids. */
|
|
33
|
+
resolveRole?: (role: string) => Promise<string[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Live delivery. `false` builds no hub — the client's poll transport is the
|
|
36
|
+
* whole delivery story then, which is a perfectly good one for an app whose
|
|
37
|
+
* notifications are not time-critical.
|
|
38
|
+
*/
|
|
39
|
+
live?: boolean | { heartbeatMs?: number; maxStreamsPerUser?: number };
|
|
40
|
+
/** Producer-path failures and sink failures land here. Defaults to stderr. */
|
|
41
|
+
onError?: (error: unknown, context: string) => void;
|
|
42
|
+
/** Test seams, forwarded to the center. */
|
|
43
|
+
now?: () => number;
|
|
44
|
+
newId?: () => string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface NotificationServiceMountOptions {
|
|
48
|
+
/** Resolve the caller to a user id, or null to refuse. */
|
|
49
|
+
authorize: (c: Context) => string | null | Promise<string | null>;
|
|
50
|
+
/** Path prefix. Default `/api/notifications`. */
|
|
51
|
+
basePath?: string;
|
|
52
|
+
/** Largest page a caller may ask for. Default 200. */
|
|
53
|
+
maxLimit?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface NotificationService {
|
|
57
|
+
readonly center: NotificationCenter;
|
|
58
|
+
readonly store: NotificationStore;
|
|
59
|
+
/** Null when `live: false` — there is then nothing to stream. */
|
|
60
|
+
readonly hub: NotificationHub | null;
|
|
61
|
+
mount(app: Hono, options: NotificationServiceMountOptions): void;
|
|
62
|
+
/** Drop live subscribers (shutdown, tests). Safe to call with no hub. */
|
|
63
|
+
close(): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createNotificationService(
|
|
67
|
+
options: CreateNotificationServiceOptions,
|
|
68
|
+
): NotificationService {
|
|
69
|
+
const onError =
|
|
70
|
+
options.onError ??
|
|
71
|
+
((error: unknown, context: string) =>
|
|
72
|
+
console.error(`[notifications] ${context}:`, error instanceof Error ? error.message : error));
|
|
73
|
+
|
|
74
|
+
const store = createSqliteNotificationStore(options.db as unknown as NotificationDb, {
|
|
75
|
+
table: options.table,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const hub =
|
|
79
|
+
options.live === false
|
|
80
|
+
? null
|
|
81
|
+
: createNotificationHub({
|
|
82
|
+
...(typeof options.live === "object" ? options.live : {}),
|
|
83
|
+
onError,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const center = createNotificationCenter({
|
|
87
|
+
store,
|
|
88
|
+
sink: hub?.sink,
|
|
89
|
+
resolveRole: options.resolveRole,
|
|
90
|
+
now: options.now,
|
|
91
|
+
newId: options.newId,
|
|
92
|
+
onError,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
center,
|
|
97
|
+
store,
|
|
98
|
+
hub,
|
|
99
|
+
mount(app, mountOptions) {
|
|
100
|
+
mountNotificationRoutes(app, {
|
|
101
|
+
center,
|
|
102
|
+
hub: hub ?? undefined,
|
|
103
|
+
authorize: mountOptions.authorize,
|
|
104
|
+
basePath: mountOptions.basePath,
|
|
105
|
+
maxLimit: mountOptions.maxLimit,
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
close() {
|
|
109
|
+
hub?.close();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { APP_DATA_HOME_ENV, resolveAppDataHome, XDG_DATA_HOME_ENV } from "./appDataHome";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The published-library defect this file is the gate for: a resolver that knows
|
|
8
|
+
* the name of a directory in one person's home. Every assertion below is either
|
|
9
|
+
* "the caller decides" or "the OS decides"; there is no third source.
|
|
10
|
+
*/
|
|
11
|
+
describe("the app-data home", () => {
|
|
12
|
+
it("takes an explicit argument over everything else", () => {
|
|
13
|
+
expect(
|
|
14
|
+
resolveAppDataHome({
|
|
15
|
+
override: "/srv/data",
|
|
16
|
+
env: { [APP_DATA_HOME_ENV]: "/env/data", [XDG_DATA_HOME_ENV]: "/xdg", HOME: "/home/x" },
|
|
17
|
+
}),
|
|
18
|
+
).toBe("/srv/data");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("reads APP_DATA_HOME — the one env var — before any convention", () => {
|
|
22
|
+
expect(
|
|
23
|
+
resolveAppDataHome({
|
|
24
|
+
env: { [APP_DATA_HOME_ENV]: "/env/data", [XDG_DATA_HOME_ENV]: "/xdg", HOME: "/home/x" },
|
|
25
|
+
}),
|
|
26
|
+
).toBe("/env/data");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("honors XDG_DATA_HOME on every platform, macOS included", () => {
|
|
30
|
+
for (const platform of ["darwin", "linux"]) {
|
|
31
|
+
expect(resolveAppDataHome({ env: { [XDG_DATA_HOME_ENV]: "/xdg", HOME: "/home/x" }, platform })).toBe("/xdg");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("falls back to the macOS convention, not to anybody's dotfolder", () => {
|
|
36
|
+
const resolved = resolveAppDataHome({ env: { HOME: "/home/x" }, platform: "darwin" });
|
|
37
|
+
expect(resolved).toBe(join("/home/x", "Library", "Application Support"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to the XDG default path elsewhere", () => {
|
|
41
|
+
expect(resolveAppDataHome({ env: { HOME: "/home/x" }, platform: "linux" })).toBe(
|
|
42
|
+
join("/home/x", ".local", "share"),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Whitespace is how an env var gets "set" by a shell script that meant to unset
|
|
47
|
+
// it; treating it as configuration points every app at the filesystem root.
|
|
48
|
+
it("treats a blank setting as no setting", () => {
|
|
49
|
+
expect(
|
|
50
|
+
resolveAppDataHome({
|
|
51
|
+
override: " ",
|
|
52
|
+
env: { [APP_DATA_HOME_ENV]: " ", [XDG_DATA_HOME_ENV]: "", HOME: "/home/x" },
|
|
53
|
+
platform: "linux",
|
|
54
|
+
}),
|
|
55
|
+
).toBe(join("/home/x", ".local", "share"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("falls back to the process's own home when the env names none", () => {
|
|
59
|
+
expect(resolveAppDataHome({ env: {}, platform: "linux" })).toBe(join(homedir(), ".local", "share"));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 🔴 The assertion this module exists for. It is deliberately a property of the
|
|
64
|
+
* OUTPUT rather than of the source text, so it still holds if somebody
|
|
65
|
+
* reintroduces the old folder through a variable, a `join`, or a constant.
|
|
66
|
+
*/
|
|
67
|
+
it("never resolves a previous generation's private directory", () => {
|
|
68
|
+
const envs: Array<Record<string, string | undefined>> = [
|
|
69
|
+
{},
|
|
70
|
+
{ HOME: "/home/x" },
|
|
71
|
+
{ HOME: "/home/x", [APP_DATA_HOME_ENV]: "/srv/data" },
|
|
72
|
+
{ HOME: "/home/x", [XDG_DATA_HOME_ENV]: "/xdg" },
|
|
73
|
+
// The previous generation's own env var must no longer steer anything: the
|
|
74
|
+
// value below is deliberately unlike any convention, so a resolver that
|
|
75
|
+
// still read this var could not produce a passing answer by accident.
|
|
76
|
+
{ HOME: "/home/x", APP_MANAGED_HOME: "/nowhere/inherited" },
|
|
77
|
+
];
|
|
78
|
+
for (const env of envs) {
|
|
79
|
+
for (const platform of ["darwin", "linux"]) {
|
|
80
|
+
const resolved = resolveAppDataHome({ env, platform });
|
|
81
|
+
expect(resolved).not.toContain("app-managed");
|
|
82
|
+
expect(resolved).not.toContain("code-retiring");
|
|
83
|
+
expect(resolved).not.toContain("Master-Docs");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|