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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ── The NAMES of every video derivative binary-server writes ────────────────────────────
|
|
3
|
+
*
|
|
4
|
+
* A derivative's key is COMPUTED from its source's, never bookkept. A consumer finds one by
|
|
5
|
+
* deriving the name and asking `/meta` whether it exists yet, so there is no webhook, no job
|
|
6
|
+
* id, no schema column and nothing to reconcile: the absence of the object IS the "not ready
|
|
7
|
+
* yet" state, and it answers itself.
|
|
8
|
+
*
|
|
9
|
+
* ── 🔴 Why these live in cursedbelt ─────────────────────────────────────────────────────
|
|
10
|
+
* Owner, 2026-08-14: *"Make it in cb because many apps may move to cloudflare and share the
|
|
11
|
+
* logic."* This contract is the seam between three repos that must never disagree —
|
|
12
|
+
* binary-server WRITES these names, `@satellites/satellite-kit` re-exports them for the
|
|
13
|
+
* satellite apps, and cursedbelt's own `binaryServerStore` mints the tokens that authorize
|
|
14
|
+
* them. A second hand-typed `${key}.web.mp4` anywhere in that triangle is a silent
|
|
15
|
+
* no-preview the day binary-server renames anything, and that has already happened once:
|
|
16
|
+
* `apps/collections` had the contract and `apps/family` shipped video without it, so family
|
|
17
|
+
* had no faststart stream, no scrub previews and no processing state at all.
|
|
18
|
+
*
|
|
19
|
+
* 🔴 What is shared here is the STRATEGY, never the media. Nothing in this module can resolve
|
|
20
|
+
* a stored object — every function is a pure string transform over a key the caller already
|
|
21
|
+
* holds. Each app is its own binary-server tenant with its own signing key, so family cannot
|
|
22
|
+
* reach a collections object through anything here, and that must stay true.
|
|
23
|
+
*
|
|
24
|
+
* ── 🔴 This is NOT the `fs*` keyspace in `./types.ts`, and they must not be merged ───────
|
|
25
|
+
* `fsHlsMasterKey(owner, id)` → `hls/<owner>/<id>/master.m3u8` and its siblings address the
|
|
26
|
+
* FileCatalogue: a caller that holds an owner and an item id, whose derivatives live in their
|
|
27
|
+
* own top-level namespaces. This module addresses a caller that holds a binary-server KEY and
|
|
28
|
+
* derives sibling names from it, which is what makes "ask /meta for a name you computed" work
|
|
29
|
+
* with no catalogue at all.
|
|
30
|
+
*
|
|
31
|
+
* Two consumers, two shapes, both correct:
|
|
32
|
+
* · `fs*` — cursedbelt's own FileCatalogue / the `library` transcode strategy.
|
|
33
|
+
* · this module — binary-server's derivative convention, used by the satellite apps.
|
|
34
|
+
*
|
|
35
|
+
* Collapsing either into the other renames objects that already exist in the store, and a
|
|
36
|
+
* renamed derivative is indistinguishable from an unbuilt one (absence is the "not ready"
|
|
37
|
+
* state) — so the visible result is every video in the fleet losing its previews at once,
|
|
38
|
+
* silently, with the rebuild costing hours of ffmpeg.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The browser-playable remux: the same pictures with the index at the front.
|
|
43
|
+
*
|
|
44
|
+
* A phone MP4/MOV stores its `moov` index at the END, and a browser cannot decode frame one
|
|
45
|
+
* until it has that index — so it downloads the whole file first. Measured on the owner's
|
|
46
|
+
* 90,339,606-byte upload (2026-08-13): the index began at byte 90,305,514, 99.96% of the way
|
|
47
|
+
* in. That is the "videos stay on a spinner for minutes" report, and no amount of Range
|
|
48
|
+
* support, caching or edge tuning fixes it. A `+faststart` remux is a STREAM COPY for
|
|
49
|
+
* h264+aac sources, which is what phones produce: no re-encode, nothing lost.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* ── 🔴 THE ENCODER GENERATION IN THE NAME (2026-09-09) ──────────────────────────────────
|
|
53
|
+
*
|
|
54
|
+
* binary-server rebuilds every video derivative when its encoder improves, and a rebuild
|
|
55
|
+
* used to write **the same filename with different bytes**. That silently cancels out an
|
|
56
|
+
* `immutable` cache lifetime — a browser told `immutable` never re-checks, for a year, and
|
|
57
|
+
* **that cannot be revoked for a copy already on somebody's phone**. So the owner's decision
|
|
58
|
+
* (A, 2026-09-09) is that new bytes get a new address: `clip.web.mp4` → `clip.g2.web.mp4`.
|
|
59
|
+
*
|
|
60
|
+
* 🔴 **Generation 1 is UNTAGGED and always will be.** Every derivative in the store today is
|
|
61
|
+
* at the untagged name; tagging generation 1 would orphan all of them and re-encode the
|
|
62
|
+
* whole library for byte-identical output. Untagged is not a legacy case — it IS generation
|
|
63
|
+
* one.
|
|
64
|
+
*
|
|
65
|
+
* 🔴 **This constant must match binary-server's `VIDEO_ENCODER_GENERATION`**
|
|
66
|
+
* (`src/media/encoderGeneration.ts`). They are in different repos and neither can import the
|
|
67
|
+
* other, so binary-server carries a test that goes RED the moment somebody bumps its number,
|
|
68
|
+
* naming this file. If you are here because of that test: bump this in the same commit, and
|
|
69
|
+
* remember that `notes`, `collections` and `family` all reach these names through
|
|
70
|
+
* `@satellites/satellite-kit/video-derivatives`. Getting it wrong is not an error anywhere —
|
|
71
|
+
* it is a 404 that reads as "no preview available", on every video.
|
|
72
|
+
*/
|
|
73
|
+
export const VIDEO_KEY_GENERATION = 1;
|
|
74
|
+
|
|
75
|
+
/** The generation tag a derivative's name carries. Empty for generation 1 — see above. */
|
|
76
|
+
export const videoGenerationTag = (generation: number = VIDEO_KEY_GENERATION): string =>
|
|
77
|
+
generation <= 1 ? "" : `.g${generation}`;
|
|
78
|
+
|
|
79
|
+
export const webPlayableKeyFor = (
|
|
80
|
+
key: string,
|
|
81
|
+
generation: number = VIDEO_KEY_GENERATION,
|
|
82
|
+
): string => `${key}${videoGenerationTag(generation)}.web.mp4`;
|
|
83
|
+
|
|
84
|
+
/** The scrub-preview sprite sheet — tiled frames from across the clip, as one JPEG. */
|
|
85
|
+
export const spriteKeyFor = (key: string): string => `${key}.sprite.jpg`;
|
|
86
|
+
|
|
87
|
+
/** The WebVTT mapping a playhead position onto a tile of {@link spriteKeyFor}. */
|
|
88
|
+
export const spriteVttKeyFor = (key: string): string => `${key}.sprite.vtt`;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The single still a gallery draws on a tile.
|
|
92
|
+
*
|
|
93
|
+
* Deliberately the SAME name a still photograph's thumbnail gets, so a consumer has one rule
|
|
94
|
+
* — *the tile for anything is `<key>.thumb.jpg`, and its absence means not ready* — rather
|
|
95
|
+
* than one rule per media kind. The sprite sheet is not a substitute: it is a GRID, so drawing
|
|
96
|
+
* it on a tile shows a contact sheet, and using one cell means every consumer carries the
|
|
97
|
+
* VTT's geometry in order to crop it.
|
|
98
|
+
*/
|
|
99
|
+
export const posterKeyFor = (key: string): string => `${key}.thumb.jpg`;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* ── The HLS tree: `<key>.hls/` ──────────────────────────────────────────────────────────
|
|
103
|
+
*
|
|
104
|
+
* 🔴 ONE container, and the trailing name is load-bearing. binary-server's `storage.ts`
|
|
105
|
+
* co-locates a container's whole subtree under a single shard and `removeContainer()` wipes it
|
|
106
|
+
* atomically — so a ladder that is one container is deleted, migrated and reconciled as one
|
|
107
|
+
* thing. Segments scattered under sibling keys would be none of those, and orphaning half a
|
|
108
|
+
* ladder is worse than having none: the master would keep advertising rungs whose segments
|
|
109
|
+
* 404, which a player retries rather than falls back from.
|
|
110
|
+
*
|
|
111
|
+
* 🔴 Why HLS at all, now that Range works. The Range bug (an edge Worker answering 200 + the
|
|
112
|
+
* whole 209 MB to a ranged read) was fixed 2026-08-14 and is NOT what this is for. A single
|
|
113
|
+
* progressive MP4, however well served, cannot ADAPT: a weak cell connection stalls on a
|
|
114
|
+
* 23 Mbps master instead of dropping to a lower rung. Each segment is also a small object that
|
|
115
|
+
* caches WHOLE at the edge, which sidesteps both the 512 MiB single-entry ceiling and the
|
|
116
|
+
* segment-assembly machinery that exists to work around it for big films.
|
|
117
|
+
*/
|
|
118
|
+
export const hlsPrefixFor = (
|
|
119
|
+
key: string,
|
|
120
|
+
generation: number = VIDEO_KEY_GENERATION,
|
|
121
|
+
): string => `${key}${videoGenerationTag(generation)}.hls/`;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The playlist a player is pointed at — the one object whose presence means "the ladder is
|
|
125
|
+
* ready". It is written LAST, after every rung, so a consumer that finds it can trust that
|
|
126
|
+
* what it names exists. Absence is normal and means "play the faststart remux".
|
|
127
|
+
*/
|
|
128
|
+
export const hlsMasterKeyFor = (
|
|
129
|
+
key: string,
|
|
130
|
+
generation: number = VIDEO_KEY_GENERATION,
|
|
131
|
+
): string => `${hlsPrefixFor(key, generation)}master.m3u8`;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Every derivative suffix, for the "is this key itself a derivative" question.
|
|
135
|
+
*
|
|
136
|
+
* 🔴 Load-bearing in three directions, each of which has already cost something:
|
|
137
|
+
* 1. A derivative must never become the source of another, or a sweep builds
|
|
138
|
+
* `clip.web.mp4.web.mp4` and then `…web.mp4.web.mp4.web.mp4`, forever.
|
|
139
|
+
* 2. A sweep must not count a derivative as work still owed, or the backlog is permanently
|
|
140
|
+
* non-empty and ffmpeg re-runs on every pass.
|
|
141
|
+
* 3. A duplicate scan must not propose a file against its own derivative. They share a title
|
|
142
|
+
* by construction, so every video would pair with its own remux and a review queue would
|
|
143
|
+
* contain nothing else.
|
|
144
|
+
*/
|
|
145
|
+
export const DERIVATIVE_SUFFIXES = [
|
|
146
|
+
".web.mp4",
|
|
147
|
+
".sprite.jpg",
|
|
148
|
+
".sprite.vtt",
|
|
149
|
+
".thumb.jpg",
|
|
150
|
+
".web.jpg",
|
|
151
|
+
] as const;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Is this key something binary-server DERIVED rather than something a person uploaded?
|
|
155
|
+
*
|
|
156
|
+
* The HLS tree is matched by its container segment rather than a suffix, because every object
|
|
157
|
+
* inside it (`master.m3u8`, `720/index.m3u8`, `720/000.ts`) is a derivative and none of them
|
|
158
|
+
* ends in a name from the list above.
|
|
159
|
+
*/
|
|
160
|
+
const DERIVATIVE_RE = new RegExp(
|
|
161
|
+
// 🔴 The optional `.g<N>` is the encoder generation a video derivative's name carries
|
|
162
|
+
// (see VIDEO_KEY_GENERATION above). Without it, `clip.g2.web.mp4` reads as a SOURCE — and
|
|
163
|
+
// then a sweep derives `clip.g2.web.mp4.web.mp4` from it, for ever. Built from the suffix
|
|
164
|
+
// list rather than written out, so the two cannot drift apart.
|
|
165
|
+
`(?:\\.g\\d+)?(?:${DERIVATIVE_SUFFIXES.map((x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
export const isDerivativeKey = (key: string): boolean =>
|
|
169
|
+
DERIVATIVE_RE.test(key) ||
|
|
170
|
+
key.includes(".hls/") ||
|
|
171
|
+
/-thumb\.(jpg|webp|png)$/.test(key) ||
|
|
172
|
+
/\.shot\.\d+\.jpg$/.test(key);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { isFfmpegAvailable } from '../media-bun';
|
|
6
|
+
import { createDiskStore } from './diskStore';
|
|
7
|
+
import { fsHlsMasterKey, fsPosterKey, fsSourceKey } from './types';
|
|
8
|
+
import { processVideo } from './videoProcessor';
|
|
9
|
+
|
|
10
|
+
const haveFfmpeg = await isFfmpegAvailable();
|
|
11
|
+
|
|
12
|
+
/** Synthesize a short test clip with ffmpeg's lavfi sources. */
|
|
13
|
+
async function makeSource(dest: string, duration = 2): Promise<void> {
|
|
14
|
+
const proc = Bun.spawn(
|
|
15
|
+
[
|
|
16
|
+
'ffmpeg',
|
|
17
|
+
'-hide_banner',
|
|
18
|
+
'-y',
|
|
19
|
+
'-f',
|
|
20
|
+
'lavfi',
|
|
21
|
+
'-i',
|
|
22
|
+
`testsrc=duration=${duration}:size=320x240:rate=15`,
|
|
23
|
+
'-pix_fmt',
|
|
24
|
+
'yuv420p',
|
|
25
|
+
'-c:v',
|
|
26
|
+
'libx264',
|
|
27
|
+
dest,
|
|
28
|
+
],
|
|
29
|
+
{ stdout: 'ignore', stderr: 'ignore' },
|
|
30
|
+
);
|
|
31
|
+
await proc.exited;
|
|
32
|
+
if (proc.exitCode !== 0) throw new Error('failed to synthesize test clip');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// posterOnly is the memory-constrained-host path: a poster + duration, but NO heavy HLS/re-encode.
|
|
36
|
+
// SKIPPED on a host without ffmpeg/ffprobe — it synthesizes and decodes real media. Runs by default wherever ffmpeg is installed.
|
|
37
|
+
describe.skipIf(!haveFfmpeg)('processVideo posterOnly', () => {
|
|
38
|
+
test('extracts a poster + duration, ready, no HLS', async () => {
|
|
39
|
+
const root = await mkdtemp(path.join(tmpdir(), 'cb-poster-only-'));
|
|
40
|
+
try {
|
|
41
|
+
const store = createDiskStore(root);
|
|
42
|
+
const owner = 'u1';
|
|
43
|
+
const id = 'v1';
|
|
44
|
+
const srcLocal = path.join(root, 'in.mp4');
|
|
45
|
+
await makeSource(srcLocal);
|
|
46
|
+
await store.put(
|
|
47
|
+
fsSourceKey(owner, id),
|
|
48
|
+
new Uint8Array(await Bun.file(srcLocal).arrayBuffer()),
|
|
49
|
+
'video/mp4',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const res = await processVideo({
|
|
53
|
+
store,
|
|
54
|
+
workDir: path.join(root, 'work'),
|
|
55
|
+
owner,
|
|
56
|
+
id,
|
|
57
|
+
sourceKey: fsSourceKey(owner, id),
|
|
58
|
+
posterOnly: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(res.status).toBe('ready');
|
|
62
|
+
expect(res.hasPoster).toBe(true);
|
|
63
|
+
expect(res.hasHls).toBe(false);
|
|
64
|
+
expect(res.hasSprite).toBe(false);
|
|
65
|
+
expect(res.durationSeconds).toBeGreaterThan(1);
|
|
66
|
+
// Poster is uploaded; the HLS master never is.
|
|
67
|
+
expect(await store.stat(fsPosterKey(owner, id))).not.toBeNull();
|
|
68
|
+
expect(await store.stat(fsHlsMasterKey(owner, id))).toBeNull();
|
|
69
|
+
} finally {
|
|
70
|
+
await rm(root, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Regression: a sub-1s clip (the 2026-07-04 prod `0:00` .mov) — the fixed `-ss 1` seek landed
|
|
75
|
+
// past EOF and produced no poster. extractPoster must clamp the seek / fall back to frame 0.
|
|
76
|
+
test('still extracts a poster for a very short (<1s) clip', async () => {
|
|
77
|
+
const root = await mkdtemp(path.join(tmpdir(), 'cb-poster-short-'));
|
|
78
|
+
try {
|
|
79
|
+
const store = createDiskStore(root);
|
|
80
|
+
const owner = 'u1';
|
|
81
|
+
const id = 'short1';
|
|
82
|
+
const srcLocal = path.join(root, 'in.mp4');
|
|
83
|
+
await makeSource(srcLocal, 0.5);
|
|
84
|
+
await store.put(
|
|
85
|
+
fsSourceKey(owner, id),
|
|
86
|
+
new Uint8Array(await Bun.file(srcLocal).arrayBuffer()),
|
|
87
|
+
'video/mp4',
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const res = await processVideo({
|
|
91
|
+
store,
|
|
92
|
+
workDir: path.join(root, 'work'),
|
|
93
|
+
owner,
|
|
94
|
+
id,
|
|
95
|
+
sourceKey: fsSourceKey(owner, id),
|
|
96
|
+
posterOnly: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(res.status).toBe('ready');
|
|
100
|
+
expect(res.hasPoster).toBe(true);
|
|
101
|
+
expect(await store.stat(fsPosterKey(owner, id))).not.toBeNull();
|
|
102
|
+
} finally {
|
|
103
|
+
await rm(root, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { mkdir, readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { ProcessingStatus } from 'cursedbelt-core/media';
|
|
4
|
+
import {
|
|
5
|
+
buildSpriteVtt,
|
|
6
|
+
canStreamCopy,
|
|
7
|
+
faststartArgs,
|
|
8
|
+
hlsLadderArgs,
|
|
9
|
+
isFfmpegAvailable,
|
|
10
|
+
LIBRARY_CRF,
|
|
11
|
+
planSprite,
|
|
12
|
+
posterArgs,
|
|
13
|
+
probeVideoStream,
|
|
14
|
+
resolveLadder,
|
|
15
|
+
runFfmpegProgress,
|
|
16
|
+
spriteArgs,
|
|
17
|
+
} from '../media-bun';
|
|
18
|
+
import {
|
|
19
|
+
fsHlsFileKey,
|
|
20
|
+
fsHlsMasterKey,
|
|
21
|
+
fsMp4Key,
|
|
22
|
+
fsPosterKey,
|
|
23
|
+
fsSpriteKey,
|
|
24
|
+
fsVttKey,
|
|
25
|
+
type MediaStore,
|
|
26
|
+
} from './types';
|
|
27
|
+
|
|
28
|
+
// Ingest→derivatives orchestrator: the store-aware glue on top of `server/media` (media-bun),
|
|
29
|
+
// which owns the argv/codec work but nothing about *where bytes live*. Transcodes a raw upload
|
|
30
|
+
// into web-safe derivatives (faststart MP4 fallback + HLS ladder + poster + sprite/VTT), uploads
|
|
31
|
+
// each into the MediaStore, and reports progress. Best-effort: if ffmpeg is unavailable it marks
|
|
32
|
+
// the video ready and lets it play raw (progressive Range).
|
|
33
|
+
|
|
34
|
+
export type ProcessProgress = (status: ProcessingStatus, pct: number) => void;
|
|
35
|
+
|
|
36
|
+
export type ProcessResult = {
|
|
37
|
+
status: ProcessingStatus;
|
|
38
|
+
durationSeconds: number;
|
|
39
|
+
hasHls: boolean;
|
|
40
|
+
hasSprite: boolean;
|
|
41
|
+
hasPoster: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ProcessOptions = {
|
|
45
|
+
store: MediaStore;
|
|
46
|
+
/** Scratch directory for ffmpeg inputs/outputs (cleaned up afterwards). */
|
|
47
|
+
workDir: string;
|
|
48
|
+
owner: string;
|
|
49
|
+
id: string;
|
|
50
|
+
/** Storage key of the already-persisted raw source. */
|
|
51
|
+
sourceKey: string;
|
|
52
|
+
/** HLS rung heights to attempt (clamped to the source height; never upscaled). */
|
|
53
|
+
ladderHeights?: number[];
|
|
54
|
+
/**
|
|
55
|
+
* Poster-only mode for memory-constrained hosts: probe duration + extract a single-frame
|
|
56
|
+
* poster (cheap, ~tens of MB peak) and skip the heavy re-encode/HLS/sprite work that OOM'd
|
|
57
|
+
* the 1GB prod box (2026-07-04). The raw source still plays progressively; this only buys a
|
|
58
|
+
* grid thumbnail + duration. Best-effort — a failed poster still lands the row `ready`.
|
|
59
|
+
*/
|
|
60
|
+
posterOnly?: boolean;
|
|
61
|
+
onProgress?: ProcessProgress;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Stream a produced local file into the store under `key` (no full heap copy).
|
|
65
|
+
async function uploadFile(store: MediaStore, localPath: string, key: string, mime: string) {
|
|
66
|
+
const file = Bun.file(localPath);
|
|
67
|
+
if (!(await file.exists())) return;
|
|
68
|
+
await store.putStream(key, file.stream(), mime, { contentLength: file.size });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Recursively upload an HLS output tree (master.m3u8 + <height>/playlist.m3u8 + segments).
|
|
72
|
+
async function uploadHlsTree(store: MediaStore, dir: string, owner: string, id: string) {
|
|
73
|
+
const walk = async (rel: string): Promise<void> => {
|
|
74
|
+
const entries = await readdir(path.join(dir, rel), { withFileTypes: true });
|
|
75
|
+
for (const e of entries) {
|
|
76
|
+
const childRel = rel ? path.join(rel, e.name) : e.name;
|
|
77
|
+
if (e.isDirectory()) {
|
|
78
|
+
await walk(childRel);
|
|
79
|
+
} else {
|
|
80
|
+
const mime = e.name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t';
|
|
81
|
+
await uploadFile(store, path.join(dir, childRel), fsHlsFileKey(owner, id, childRel), mime);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
await walk('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract a single JPEG poster, robust to very short clips: seek ~1s in to skip black intros, but
|
|
89
|
+
// clamp the seek inside a short clip and fall back to frame 0 if the seek lands past EOF (which
|
|
90
|
+
// otherwise decodes no frame and silently produces no poster — the 2026-07-04 `0:00` .mov case).
|
|
91
|
+
async function extractPoster(
|
|
92
|
+
inPath: string,
|
|
93
|
+
outPath: string,
|
|
94
|
+
durationSeconds: number,
|
|
95
|
+
): Promise<boolean> {
|
|
96
|
+
let stderrTail = '';
|
|
97
|
+
const onError = (tail: string) => {
|
|
98
|
+
stderrTail = tail;
|
|
99
|
+
};
|
|
100
|
+
const at = durationSeconds > 1.2 ? 1 : 0;
|
|
101
|
+
if (await runFfmpegProgress(posterArgs(inPath, outPath, at), 0, undefined, onError)) return true;
|
|
102
|
+
if (at !== 0 && (await runFfmpegProgress(posterArgs(inPath, outPath, 0), 0, undefined, onError)))
|
|
103
|
+
return true;
|
|
104
|
+
// Real poster failures AND the malformed/short-clip fixtures test suites deliberately
|
|
105
|
+
// feed ffmpeg land here — stderr alone can't tell them apart, so the message is tagged
|
|
106
|
+
// `[expected-in-tests]` (grep-filterable) instead of silenced, so a real prod
|
|
107
|
+
// regression still surfaces.
|
|
108
|
+
console.warn(
|
|
109
|
+
`[videoProcessor][expected-in-tests] poster extraction failed (duration=${durationSeconds}s): ${stderrTail.slice(-400)}`,
|
|
110
|
+
);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function processVideo(opts: ProcessOptions): Promise<ProcessResult> {
|
|
115
|
+
const { store, workDir, owner, id, sourceKey, onProgress } = opts;
|
|
116
|
+
const report = (s: ProcessingStatus, p: number) => onProgress?.(s, p);
|
|
117
|
+
const idle: ProcessResult = {
|
|
118
|
+
status: 'ready',
|
|
119
|
+
durationSeconds: 0,
|
|
120
|
+
hasHls: false,
|
|
121
|
+
hasSprite: false,
|
|
122
|
+
hasPoster: false,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (!(await isFfmpegAvailable())) {
|
|
126
|
+
report('ready', 100);
|
|
127
|
+
return idle;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const jobDir = path.join(workDir, owner, id);
|
|
131
|
+
await mkdir(jobDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
report('processing', 1);
|
|
135
|
+
|
|
136
|
+
// 1. Materialise the source locally for ffmpeg (use the backend's own path if local).
|
|
137
|
+
let srcPath = (await store.localPath?.(sourceKey)) ?? null;
|
|
138
|
+
if (!srcPath) {
|
|
139
|
+
const bytes = await store.get(sourceKey);
|
|
140
|
+
if (!bytes) throw new Error(`source missing: ${sourceKey}`);
|
|
141
|
+
srcPath = path.join(jobDir, 'source.bin');
|
|
142
|
+
await Bun.write(srcPath, bytes);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const probe = await probeVideoStream(srcPath);
|
|
146
|
+
const durationSeconds = probe.duration;
|
|
147
|
+
|
|
148
|
+
// Poster-only mode: extract one frame + report ready. No re-encode, no HLS, no sprite —
|
|
149
|
+
// the memory-heavy steps that OOM'd the 1GB host stay off; the raw source plays as-is.
|
|
150
|
+
if (opts.posterOnly) {
|
|
151
|
+
report('processing', 50);
|
|
152
|
+
const posterPath = path.join(jobDir, 'poster.jpg');
|
|
153
|
+
const okPoster = await extractPoster(srcPath, posterPath, durationSeconds);
|
|
154
|
+
if (okPoster) await uploadFile(store, posterPath, fsPosterKey(owner, id), 'image/jpeg');
|
|
155
|
+
report('ready', 100);
|
|
156
|
+
return {
|
|
157
|
+
status: 'ready',
|
|
158
|
+
durationSeconds,
|
|
159
|
+
hasHls: false,
|
|
160
|
+
hasSprite: false,
|
|
161
|
+
hasPoster: okPoster,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 2. Faststart MP4 — the progressive Range fallback (streamUrl). Stream-copy when the
|
|
166
|
+
// source is already web-safe, else re-encode to the shared web-safe ladder.
|
|
167
|
+
const mp4Path = path.join(jobDir, 'out.mp4');
|
|
168
|
+
const okMp4 = await runFfmpegProgress(
|
|
169
|
+
faststartArgs(srcPath, mp4Path, { copy: canStreamCopy(probe) }),
|
|
170
|
+
durationSeconds,
|
|
171
|
+
(pct) => report('processing', Math.round(pct * 0.4)),
|
|
172
|
+
);
|
|
173
|
+
if (okMp4) await uploadFile(store, mp4Path, fsMp4Key(owner, id), 'video/mp4');
|
|
174
|
+
// ffmpeg operates on the faststart output when it exists, else the raw source.
|
|
175
|
+
const encodeInput = okMp4 ? mp4Path : srcPath;
|
|
176
|
+
|
|
177
|
+
// 3. HLS ladder — the adaptive path (hlsUrl).
|
|
178
|
+
const heights = resolveLadder(opts.ladderHeights ?? [480, 720, 1080], probe.height);
|
|
179
|
+
const hlsDir = path.join(jobDir, 'hls');
|
|
180
|
+
await mkdir(hlsDir, { recursive: true });
|
|
181
|
+
const okHls = await runFfmpegProgress(
|
|
182
|
+
hlsLadderArgs(encodeInput, hlsDir, {
|
|
183
|
+
heights,
|
|
184
|
+
crf: Number(LIBRARY_CRF),
|
|
185
|
+
hasAudio: probe.hasAudio,
|
|
186
|
+
}),
|
|
187
|
+
durationSeconds,
|
|
188
|
+
(pct) => report('processing', 40 + Math.round(pct * 0.5)),
|
|
189
|
+
);
|
|
190
|
+
if (okHls) await uploadHlsTree(store, hlsDir, owner, id);
|
|
191
|
+
|
|
192
|
+
// 4. Poster (frame ~1s in).
|
|
193
|
+
report('processing', 92);
|
|
194
|
+
const posterPath = path.join(jobDir, 'poster.jpg');
|
|
195
|
+
const okPoster = await extractPoster(encodeInput, posterPath, durationSeconds);
|
|
196
|
+
if (okPoster) await uploadFile(store, posterPath, fsPosterKey(owner, id), 'image/jpeg');
|
|
197
|
+
|
|
198
|
+
// 5. Sprite sheet + WebVTT scrub track.
|
|
199
|
+
report('processing', 96);
|
|
200
|
+
let hasSprite = false;
|
|
201
|
+
if (durationSeconds > 0) {
|
|
202
|
+
const plan = planSprite(durationSeconds, 5, probe.width, probe.height);
|
|
203
|
+
const spritePath = path.join(jobDir, 'sprites.jpg');
|
|
204
|
+
if (await runFfmpegProgress(spriteArgs(encodeInput, spritePath, plan), 0)) {
|
|
205
|
+
await uploadFile(store, spritePath, fsSpriteKey(owner, id), 'image/jpeg');
|
|
206
|
+
await store.put(
|
|
207
|
+
fsVttKey(owner, id),
|
|
208
|
+
new TextEncoder().encode(buildSpriteVtt(plan, 'sprites.jpg')),
|
|
209
|
+
'text/vtt',
|
|
210
|
+
);
|
|
211
|
+
hasSprite = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
report('ready', 100);
|
|
216
|
+
return {
|
|
217
|
+
status: 'ready',
|
|
218
|
+
durationSeconds,
|
|
219
|
+
hasHls: okHls && (await store.stat(fsHlsMasterKey(owner, id))) !== null,
|
|
220
|
+
hasSprite,
|
|
221
|
+
hasPoster: okPoster,
|
|
222
|
+
};
|
|
223
|
+
} catch {
|
|
224
|
+
report('failed', 0);
|
|
225
|
+
return { ...idle, status: 'failed' };
|
|
226
|
+
} finally {
|
|
227
|
+
await rm(jobDir, { recursive: true, force: true }).catch(() => {});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createSyncAlarm } from "./alarm";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 🔴 `tmpdir()`, not vault's `scratchDir()` — the seam moved when this engine did.
|
|
9
|
+
*
|
|
10
|
+
* In `apps/vault` this suite imported `scripts/testScratch`, an opt-in helper that
|
|
11
|
+
* hands out a directory under `$FORGE_STATE` and sweeps it. cursedbelt solves the
|
|
12
|
+
* same problem one level lower: `forceTestEnv.ts` is a bunfig PRELOAD that
|
|
13
|
+
* redirects `$TMPDIR` itself for every test process in the repo (rationale and the
|
|
14
|
+
* 4,950× measurement behind it are in `scripts/testTmpRoot.ts`). So a plain
|
|
15
|
+
* `mkdtempSync(join(tmpdir(), …))` here already lands in the small, swept root —
|
|
16
|
+
* and reaching back for the app's helper would be a cross-repo import that only
|
|
17
|
+
* resolves while the copy in vault still exists.
|
|
18
|
+
*/
|
|
19
|
+
const dirs: string[] = [];
|
|
20
|
+
const scratchDir = (prefix: string): string => {
|
|
21
|
+
const dir = mkdtempSync(join(tmpdir(), `cb-${prefix}-`));
|
|
22
|
+
dirs.push(dir);
|
|
23
|
+
return dir;
|
|
24
|
+
};
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function makeAlarm(overrides: { threshold?: number; reRaiseMs?: number } = {}) {
|
|
30
|
+
const dir = scratchDir("sync-alarm");
|
|
31
|
+
const raised: string[] = [];
|
|
32
|
+
let clock = 1_000_000;
|
|
33
|
+
const alarm = createSyncAlarm({
|
|
34
|
+
statePath: join(dir, "alarm.json"),
|
|
35
|
+
threshold: overrides.threshold ?? 3,
|
|
36
|
+
reRaiseMs: overrides.reRaiseMs ?? 60_000,
|
|
37
|
+
notify: (_title, body) => raised.push(body),
|
|
38
|
+
now: () => clock,
|
|
39
|
+
appName: "station",
|
|
40
|
+
});
|
|
41
|
+
return { alarm, raised, tick: (ms: number) => (clock += ms) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("sync alarm", () => {
|
|
45
|
+
test("raises after the threshold of paused rounds, not before", () => {
|
|
46
|
+
const { alarm, raised } = makeAlarm();
|
|
47
|
+
alarm.record("paused", "doc.set");
|
|
48
|
+
alarm.record("paused", "doc.set");
|
|
49
|
+
expect(raised).toHaveLength(0);
|
|
50
|
+
alarm.record("paused", "doc.set");
|
|
51
|
+
expect(raised).toHaveLength(1);
|
|
52
|
+
expect(raised[0]).toContain("doc.set");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("re-raises only after the re-raise window", () => {
|
|
56
|
+
const { alarm, raised, tick } = makeAlarm();
|
|
57
|
+
for (let i = 0; i < 5; i++) alarm.record("paused", "k");
|
|
58
|
+
expect(raised).toHaveLength(1);
|
|
59
|
+
tick(59_000);
|
|
60
|
+
alarm.record("paused", "k");
|
|
61
|
+
expect(raised).toHaveLength(1);
|
|
62
|
+
tick(2_000);
|
|
63
|
+
alarm.record("paused", "k");
|
|
64
|
+
expect(raised).toHaveLength(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("ok resets the counter; unreachable neither counts nor resets", () => {
|
|
68
|
+
const { alarm, raised } = makeAlarm();
|
|
69
|
+
alarm.record("paused", "k");
|
|
70
|
+
alarm.record("paused", "k");
|
|
71
|
+
// The nightly window: unreachable rounds must not walk the counter to the
|
|
72
|
+
// threshold, and must not clear a genuine pause streak either.
|
|
73
|
+
alarm.record("unreachable");
|
|
74
|
+
alarm.record("paused", "k");
|
|
75
|
+
expect(raised).toHaveLength(1);
|
|
76
|
+
alarm.record("ok");
|
|
77
|
+
alarm.record("paused", "k");
|
|
78
|
+
alarm.record("paused", "k");
|
|
79
|
+
expect(raised).toHaveLength(1); // streak restarted from zero
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("auth refusals raise with the re-link message", () => {
|
|
83
|
+
const { alarm, raised } = makeAlarm({ threshold: 1 });
|
|
84
|
+
alarm.record("auth");
|
|
85
|
+
expect(raised[0]).toContain("Re-link");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("state survives a process restart (same state file)", () => {
|
|
89
|
+
const dir = scratchDir("sync-alarm");
|
|
90
|
+
const statePath = join(dir, "alarm.json");
|
|
91
|
+
const raised: string[] = [];
|
|
92
|
+
const make = () =>
|
|
93
|
+
createSyncAlarm({
|
|
94
|
+
statePath,
|
|
95
|
+
threshold: 3,
|
|
96
|
+
notify: (_t, b) => raised.push(b),
|
|
97
|
+
now: () => 5_000,
|
|
98
|
+
});
|
|
99
|
+
make().record("paused", "k");
|
|
100
|
+
make().record("paused", "k");
|
|
101
|
+
make().record("paused", "k");
|
|
102
|
+
expect(raised).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 🔴 The outcome that was missing, and the eight days it cost.
|
|
108
|
+
*
|
|
109
|
+
* The console's subdomain cutover left the Mac's sync dialer aimed at a host
|
|
110
|
+
* that had become a 301. It followed the redirect into a Cloudflare Access login
|
|
111
|
+
* page and threw a JSON parse error every round — neither `paused` (nothing was
|
|
112
|
+
* version-skewed), nor `auth` (no token was refused), nor `unreachable` (the
|
|
113
|
+
* peer answered). So the alarm recorded NOTHING while every mirrored report on
|
|
114
|
+
* prod froze, each still rendering a confident age.
|
|
115
|
+
*/
|
|
116
|
+
describe("a sustained hard ERROR raises, exactly like a sustained pause", () => {
|
|
117
|
+
test("consecutive errors cross the threshold and the alarm names the real error", () => {
|
|
118
|
+
const { alarm, raised } = makeAlarm({ threshold: 3 });
|
|
119
|
+
alarm.record("error", "sync/info: 200 answered text/html, not JSON");
|
|
120
|
+
alarm.record("error", "sync/info: 200 answered text/html, not JSON");
|
|
121
|
+
expect(raised).toHaveLength(0);
|
|
122
|
+
alarm.record("error", "sync/info: 200 answered text/html, not JSON");
|
|
123
|
+
expect(raised).toHaveLength(1);
|
|
124
|
+
// The message carries the cause, because "failed (ignored)" being
|
|
125
|
+
// uninformative is the whole reason this outcome exists.
|
|
126
|
+
expect(raised[0]).toContain("text/html");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("one good round clears the counter", () => {
|
|
130
|
+
const { alarm, raised } = makeAlarm({ threshold: 3 });
|
|
131
|
+
alarm.record("error", "boom");
|
|
132
|
+
alarm.record("error", "boom");
|
|
133
|
+
alarm.record("ok");
|
|
134
|
+
alarm.record("error", "boom");
|
|
135
|
+
alarm.record("error", "boom");
|
|
136
|
+
expect(raised).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("an UNREACHABLE peer between errors neither counts nor resets", () => {
|
|
140
|
+
// The nightly EC2 sleep sits in the middle of a real outage all the time.
|
|
141
|
+
const { alarm, raised } = makeAlarm({ threshold: 3 });
|
|
142
|
+
alarm.record("error", "boom");
|
|
143
|
+
alarm.record("unreachable");
|
|
144
|
+
alarm.record("error", "boom");
|
|
145
|
+
alarm.record("unreachable");
|
|
146
|
+
alarm.record("error", "boom");
|
|
147
|
+
expect(raised).toHaveLength(1);
|
|
148
|
+
});
|
|
149
|
+
});
|