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,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dedupe guarantee, proved rather than asserted.
|
|
3
|
+
*
|
|
4
|
+
* The owner's requirement was one sentence — *"it needs to avoid sending me the
|
|
5
|
+
* same problem over and over in duplicated notifications if it's polling for
|
|
6
|
+
* problems"* — and it is the requirement that decides whether the feature is
|
|
7
|
+
* used or muted. So the first test here is literally the poller, run twice
|
|
8
|
+
* against an unchanged failure, counting messages.
|
|
9
|
+
*
|
|
10
|
+
* The rest are the ways a naive version lies: a crashed detector announcing that
|
|
11
|
+
* an outage is over, a resolve for something the owner was never told about, a
|
|
12
|
+
* private file name reaching a text message, and a broken channel being told
|
|
13
|
+
* about itself down itself.
|
|
14
|
+
*/
|
|
15
|
+
import { Database } from "bun:sqlite";
|
|
16
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
17
|
+
import type { ChannelMessage, ChannelResult, IncidentChannel } from "./channels";
|
|
18
|
+
import { createTextChannel } from "./channels";
|
|
19
|
+
import { type IncidentCheck, runIncidentSweep } from "./checks";
|
|
20
|
+
import { createIncidentNotifier, UNDELIVERED_RETRY_MS } from "./notifier";
|
|
21
|
+
import { createIncidentReporter } from "./reporter";
|
|
22
|
+
import { createIncidentStore, type IncidentStore } from "./store";
|
|
23
|
+
import type { IncidentReport } from "./types";
|
|
24
|
+
|
|
25
|
+
const stuckIngest = (failed = 42): IncidentReport => ({
|
|
26
|
+
source: "station",
|
|
27
|
+
kind: "ingest-run-all-failed",
|
|
28
|
+
subject: "collections",
|
|
29
|
+
severity: "blocking",
|
|
30
|
+
title: "collections: every queued upload is failing",
|
|
31
|
+
whatBroke: "The scheduled ingest ran and every one of its items failed.",
|
|
32
|
+
whatItBlocks: "Nothing new reaches the collections app, and the queue keeps growing.",
|
|
33
|
+
whatToDo: "cd ~/code/local/binary-server && bun run storage-drift",
|
|
34
|
+
facts: { failed, pending: failed },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** A channel that records what it was asked to send, and always succeeds. */
|
|
38
|
+
function spyChannel(key = "text"): IncidentChannel & { sent: ChannelMessage[] } {
|
|
39
|
+
const sent: ChannelMessage[] = [];
|
|
40
|
+
return {
|
|
41
|
+
key,
|
|
42
|
+
label: `spy:${key}`,
|
|
43
|
+
sent,
|
|
44
|
+
async send(message): Promise<ChannelResult> {
|
|
45
|
+
sent.push(message);
|
|
46
|
+
return { channel: key, outcome: "delivered", detail: "spy" };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const checkOf = (run: IncidentCheck["run"]): IncidentCheck => ({
|
|
52
|
+
key: "ingest",
|
|
53
|
+
source: "station",
|
|
54
|
+
kinds: ["ingest-run-all-failed"],
|
|
55
|
+
label: "Ingest runs",
|
|
56
|
+
run,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("incident dedupe", () => {
|
|
60
|
+
let db: Database;
|
|
61
|
+
let store: IncidentStore;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
db = new Database(":memory:");
|
|
65
|
+
store = createIncidentStore(db);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a poller that sees the same failure twice notifies ONCE", async () => {
|
|
69
|
+
const channel = spyChannel();
|
|
70
|
+
const notifier = createIncidentNotifier({ store, channels: [channel] });
|
|
71
|
+
const check = checkOf(() => [stuckIngest()]);
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < 2; i++) {
|
|
74
|
+
const sweep = await runIncidentSweep(store, [check]);
|
|
75
|
+
await notifier.announce(sweep.transitions);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
expect(channel.sent).toHaveLength(1);
|
|
79
|
+
expect(channel.sent[0]?.subject).toContain("collections");
|
|
80
|
+
const incident = store.list({ state: "active" })[0];
|
|
81
|
+
expect(incident?.seenCount).toBe(2);
|
|
82
|
+
expect(incident?.episodes).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("ten more sweeps still notify nothing, even as the numbers move", async () => {
|
|
86
|
+
const channel = spyChannel();
|
|
87
|
+
const notifier = createIncidentNotifier({ store, channels: [channel] });
|
|
88
|
+
let failed = 42;
|
|
89
|
+
const check = checkOf(() => [stuckIngest(failed)]);
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < 12; i++) {
|
|
92
|
+
failed += 1;
|
|
93
|
+
const sweep = await runIncidentSweep(store, [check]);
|
|
94
|
+
await notifier.announce(sweep.transitions);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
expect(channel.sent).toHaveLength(1);
|
|
98
|
+
// The refreshed facts are what the console shows, so they must move.
|
|
99
|
+
expect(store.list({ state: "active" })[0]?.facts?.failed).toBe(54);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a fix notifies once more, and a relapse is a new episode", async () => {
|
|
103
|
+
const channel = spyChannel();
|
|
104
|
+
const notifier = createIncidentNotifier({ store, channels: [channel] });
|
|
105
|
+
let broken = true;
|
|
106
|
+
const check = checkOf(() => (broken ? [stuckIngest()] : []));
|
|
107
|
+
|
|
108
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
109
|
+
broken = false;
|
|
110
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
111
|
+
broken = true;
|
|
112
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
113
|
+
|
|
114
|
+
expect(channel.sent.map((m) => m.subject)).toEqual([
|
|
115
|
+
"[BLOCKED] collections: every queued upload is failing",
|
|
116
|
+
"[cleared] collections: every queued upload is failing",
|
|
117
|
+
"[BLOCKED] collections: every queued upload is failing",
|
|
118
|
+
]);
|
|
119
|
+
expect(store.list({ state: "active" })[0]?.episodes).toBe(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a warning that becomes blocking is announced again — the channels changed", async () => {
|
|
123
|
+
const channel = spyChannel();
|
|
124
|
+
const notifier = createIncidentNotifier({ store, channels: [channel] });
|
|
125
|
+
let severity: "warning" | "blocking" = "warning";
|
|
126
|
+
const check = checkOf(() => [{ ...stuckIngest(), severity }]);
|
|
127
|
+
|
|
128
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
129
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
130
|
+
severity = "blocking";
|
|
131
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
132
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
133
|
+
|
|
134
|
+
expect(channel.sent.map((m) => m.severity)).toEqual(["warning", "blocking"]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("a broken detector never says everything is fine", () => {
|
|
139
|
+
let store: IncidentStore;
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
store = createIncidentStore(new Database(":memory:"));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a check that THROWS leaves its open incidents open, and sends nothing", async () => {
|
|
146
|
+
const channel = spyChannel();
|
|
147
|
+
const notifier = createIncidentNotifier({ store, channels: [channel] });
|
|
148
|
+
let mode: "broken" | "throw" = "broken";
|
|
149
|
+
const check = checkOf(() => {
|
|
150
|
+
if (mode === "throw") throw new Error("ENOENT: the queue directory is gone");
|
|
151
|
+
return [stuckIngest()];
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
155
|
+
mode = "throw";
|
|
156
|
+
const second = await runIncidentSweep(store, [check]);
|
|
157
|
+
await notifier.announce(second.transitions);
|
|
158
|
+
|
|
159
|
+
expect(second.outcomes[0]?.status).toBe("failed");
|
|
160
|
+
expect(second.outcomes[0]?.error).toContain("ENOENT");
|
|
161
|
+
expect(second.transitions.resolved).toHaveLength(0);
|
|
162
|
+
expect(store.list({ state: "active" })).toHaveLength(1);
|
|
163
|
+
// One message, from the first sweep. No "cleared".
|
|
164
|
+
expect(channel.sent).toHaveLength(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a check clears only the kinds it DECLARES", async () => {
|
|
168
|
+
// A check that opened an incident of a kind it no longer declares cannot
|
|
169
|
+
// resolve it — better a stale red the console shows than a false all-clear.
|
|
170
|
+
store.reconcile("ingest", [stuckIngest()], {
|
|
171
|
+
namespaces: ["station:ingest-run-all-failed:"],
|
|
172
|
+
});
|
|
173
|
+
const narrowed: IncidentCheck = { ...checkOf(() => []), kinds: ["some-other-kind"] };
|
|
174
|
+
const sweep = await runIncidentSweep(store, [narrowed]);
|
|
175
|
+
expect(sweep.transitions.resolved).toHaveLength(0);
|
|
176
|
+
expect(store.list({ state: "active" })).toHaveLength(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("one check cannot resolve another check's incidents", async () => {
|
|
180
|
+
store.reconcile("ingest", [stuckIngest()], {
|
|
181
|
+
namespaces: ["station:ingest-run-all-failed:"],
|
|
182
|
+
});
|
|
183
|
+
const impostor: IncidentCheck = {
|
|
184
|
+
key: "disk",
|
|
185
|
+
source: "station",
|
|
186
|
+
kinds: ["ingest-run-all-failed"],
|
|
187
|
+
label: "Disk",
|
|
188
|
+
run: () => [],
|
|
189
|
+
};
|
|
190
|
+
await runIncidentSweep(store, [impostor]);
|
|
191
|
+
expect(store.list({ state: "active" })).toHaveLength(1);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("what a message may not contain or travel down", () => {
|
|
196
|
+
let store: IncidentStore;
|
|
197
|
+
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
store = createIncidentStore(new Database(":memory:"));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("a string in `facts` is refused at the write — it is how a file name reaches a phone", () => {
|
|
203
|
+
expect(() =>
|
|
204
|
+
store.reconcile("ingest", [
|
|
205
|
+
{
|
|
206
|
+
...stuckIngest(),
|
|
207
|
+
// biome-ignore lint/suspicious/noExplicitAny: the point of the test is the wrong type
|
|
208
|
+
facts: { failed: 42, firstFile: "Mom's 60th birthday.mov" as any },
|
|
209
|
+
},
|
|
210
|
+
]),
|
|
211
|
+
).toThrow(/facts take numbers/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("an incident about a dead channel is not sent down that channel", async () => {
|
|
215
|
+
const text = spyChannel("text");
|
|
216
|
+
const notifier = createIncidentNotifier({ store, channels: [text] });
|
|
217
|
+
const t = store.reconcile("channel-health", [
|
|
218
|
+
{
|
|
219
|
+
source: "station",
|
|
220
|
+
kind: "notification-channel-failing",
|
|
221
|
+
subject: "text",
|
|
222
|
+
severity: "blocking",
|
|
223
|
+
title: "The text-message channel is not delivering",
|
|
224
|
+
whatBroke: "The last blocking alert could not be proven to have left this Mac.",
|
|
225
|
+
whatItBlocks: "You would not be texted about the next outage.",
|
|
226
|
+
whatToDo: "Open the console → Incidents → Send a test text.",
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
await notifier.announce(t);
|
|
230
|
+
expect(text.sent).toHaveLength(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("a resolve for an incident the owner was never told about is not sent", async () => {
|
|
234
|
+
// No channels on the first pass: the open is recorded but never announced.
|
|
235
|
+
const silent = createIncidentNotifier({ store, channels: [] });
|
|
236
|
+
const check = checkOf(() => [stuckIngest()]);
|
|
237
|
+
await silent.announce((await runIncidentSweep(store, [check])).transitions);
|
|
238
|
+
|
|
239
|
+
const channel = spyChannel();
|
|
240
|
+
const loud = createIncidentNotifier({ store, channels: [channel] });
|
|
241
|
+
const cleared = checkOf(() => []);
|
|
242
|
+
await loud.announce((await runIncidentSweep(store, [cleared])).transitions);
|
|
243
|
+
|
|
244
|
+
expect(channel.sent).toHaveLength(0);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("the text channel deals in evidence", () => {
|
|
249
|
+
const message: ChannelMessage = {
|
|
250
|
+
severity: "blocking",
|
|
251
|
+
subject: "[BLOCKED] test",
|
|
252
|
+
text: "body",
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
test("osascript exiting 0 is NOT delivery — an unprovable send is `unproven`", async () => {
|
|
256
|
+
const channel = createTextChannel({
|
|
257
|
+
handle: "+15550000000",
|
|
258
|
+
send: async () => ({ ok: true, detail: "accepted" }),
|
|
259
|
+
proveSent: async () => null, // chat.db unreadable
|
|
260
|
+
});
|
|
261
|
+
const result = await channel.send(message);
|
|
262
|
+
expect(result.outcome).toBe("unproven");
|
|
263
|
+
expect(result.detail).toContain("Full Disk Access");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("a sent row in chat.db is delivery", async () => {
|
|
267
|
+
const channel = createTextChannel({
|
|
268
|
+
handle: "+15550000000",
|
|
269
|
+
send: async () => ({ ok: true, detail: "accepted" }),
|
|
270
|
+
proveSent: async () => true,
|
|
271
|
+
});
|
|
272
|
+
expect((await channel.send(message)).outcome).toBe("delivered");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("no proof row inside the window is `unproven`, not `delivered`", async () => {
|
|
276
|
+
const channel = createTextChannel({
|
|
277
|
+
handle: "+15550000000",
|
|
278
|
+
send: async () => ({ ok: true, detail: "accepted" }),
|
|
279
|
+
proveSent: async () => false,
|
|
280
|
+
proofTimeoutMs: 20,
|
|
281
|
+
});
|
|
282
|
+
const result = await channel.send(message);
|
|
283
|
+
expect(result.outcome).toBe("unproven");
|
|
284
|
+
expect(result.detail).toContain("no sent row appeared");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("a warning never reaches the phone", async () => {
|
|
288
|
+
const channel = createTextChannel({
|
|
289
|
+
handle: "+15550000000",
|
|
290
|
+
send: async () => {
|
|
291
|
+
throw new Error("must not be called");
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
expect((await channel.send({ ...message, severity: "warning" })).outcome).toBe("skipped");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("acknowledgement", () => {
|
|
299
|
+
test("an acknowledged incident stays listed, stays deduped, and still clears", async () => {
|
|
300
|
+
const store = createIncidentStore(new Database(":memory:"));
|
|
301
|
+
const channel = spyChannel();
|
|
302
|
+
const notifier = createIncidentNotifier({ store, channels: [channel] });
|
|
303
|
+
let broken = true;
|
|
304
|
+
const check = checkOf(() => (broken ? [stuckIngest()] : []));
|
|
305
|
+
|
|
306
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
307
|
+
const fp = store.list({ state: "active" })[0]?.fingerprint ?? "";
|
|
308
|
+
expect(store.acknowledge(fp)).toBe(true);
|
|
309
|
+
|
|
310
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
311
|
+
expect(channel.sent).toHaveLength(1);
|
|
312
|
+
expect(store.get(fp)?.state).toBe("acknowledged");
|
|
313
|
+
|
|
314
|
+
broken = false;
|
|
315
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
316
|
+
expect(store.get(fp)?.state).toBe("resolved");
|
|
317
|
+
expect(channel.sent).toHaveLength(2);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("a remote reporter", () => {
|
|
322
|
+
test("posts the complete set every beat, so the console can CLEAR as well as open", async () => {
|
|
323
|
+
const posted: Array<Record<string, unknown>> = [];
|
|
324
|
+
let broken = true;
|
|
325
|
+
const reporter = createIncidentReporter({
|
|
326
|
+
url: "http://console.invalid/report",
|
|
327
|
+
token: "t",
|
|
328
|
+
source: "binary-server",
|
|
329
|
+
checkKey: "bs:self",
|
|
330
|
+
kinds: ["disk-below-reserve"],
|
|
331
|
+
collect: () =>
|
|
332
|
+
broken
|
|
333
|
+
? [
|
|
334
|
+
{
|
|
335
|
+
source: "binary-server",
|
|
336
|
+
kind: "disk-below-reserve",
|
|
337
|
+
subject: "binary-server",
|
|
338
|
+
severity: "blocking" as const,
|
|
339
|
+
title: "t",
|
|
340
|
+
whatBroke: "b",
|
|
341
|
+
whatItBlocks: "c",
|
|
342
|
+
whatToDo: "d",
|
|
343
|
+
},
|
|
344
|
+
]
|
|
345
|
+
: [],
|
|
346
|
+
fetchImpl: (async (_url: string, init: RequestInit) => {
|
|
347
|
+
posted.push(JSON.parse(String(init.body)) as Record<string, unknown>);
|
|
348
|
+
return new Response("{}", { status: 200 });
|
|
349
|
+
}) as unknown as typeof fetch,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(await reporter.beat()).toBe(true);
|
|
353
|
+
broken = false;
|
|
354
|
+
expect(await reporter.beat()).toBe(true);
|
|
355
|
+
|
|
356
|
+
// The healthy beat still fires and still declares its kinds — that empty
|
|
357
|
+
// `reports` with a non-empty `kinds` is the ONLY thing that can resolve the
|
|
358
|
+
// incident the first beat opened.
|
|
359
|
+
expect((posted[1]?.reports as unknown[])).toHaveLength(0);
|
|
360
|
+
expect(posted[1]?.kinds).toEqual(["disk-below-reserve"]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("an unreachable console is false, never a throw — alerting must not break the service", async () => {
|
|
364
|
+
const reporter = createIncidentReporter({
|
|
365
|
+
url: "http://console.invalid/report",
|
|
366
|
+
token: "t",
|
|
367
|
+
source: "binary-server",
|
|
368
|
+
checkKey: "bs:self",
|
|
369
|
+
kinds: ["disk-below-reserve"],
|
|
370
|
+
collect: () => [],
|
|
371
|
+
log: () => {},
|
|
372
|
+
fetchImpl: (async () => {
|
|
373
|
+
throw new Error("ECONNREFUSED");
|
|
374
|
+
}) as unknown as typeof fetch,
|
|
375
|
+
});
|
|
376
|
+
expect(await reporter.beat()).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("the text channel names the macOS decision, not the error code", () => {
|
|
381
|
+
test("a -1712 TIMEOUT from a daemon is reported as the consent problem it is", async () => {
|
|
382
|
+
// The real symptom, measured 2026-08-21: the console's launchd daemon got
|
|
383
|
+
// `AppleEvent timed out. (-1712)` — not a refusal — while TCC.db held a
|
|
384
|
+
// fresh DENY for it. An agent reading "-1712" learns nothing; the message
|
|
385
|
+
// has to name the toggle.
|
|
386
|
+
const channel = createTextChannel({
|
|
387
|
+
handle: "+15550000000",
|
|
388
|
+
send: async () => ({
|
|
389
|
+
ok: false,
|
|
390
|
+
detail:
|
|
391
|
+
"macOS has Automation access to Messages DENIED for this console's daemon. Turn it back on in System Settings ▸ Privacy & Security ▸ Automation (the entry is 'bun'), then press Send a test.",
|
|
392
|
+
}),
|
|
393
|
+
});
|
|
394
|
+
const result = await channel.send({
|
|
395
|
+
severity: "blocking",
|
|
396
|
+
subject: "s",
|
|
397
|
+
text: "t",
|
|
398
|
+
});
|
|
399
|
+
expect(result.outcome).toBe("failed");
|
|
400
|
+
expect(result.detail).toContain("Privacy & Security");
|
|
401
|
+
expect(result.detail).not.toContain("-1712");
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("an alert nobody could be told about is retried", () => {
|
|
406
|
+
test("a blocking incident whose channels ALL failed tries again an hour later — once", async () => {
|
|
407
|
+
const store = createIncidentStore(new Database(":memory:"));
|
|
408
|
+
let deliver = false;
|
|
409
|
+
const sent: string[] = [];
|
|
410
|
+
const flaky: IncidentChannel = {
|
|
411
|
+
key: "text",
|
|
412
|
+
label: "flaky",
|
|
413
|
+
async send(message) {
|
|
414
|
+
sent.push(message.subject);
|
|
415
|
+
return deliver
|
|
416
|
+
? { channel: "text", outcome: "delivered", detail: "ok" }
|
|
417
|
+
: { channel: "text", outcome: "failed", detail: "no" };
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
const notifier = createIncidentNotifier({ store, channels: [flaky] });
|
|
421
|
+
const check = checkOf(() => [stuckIngest()]);
|
|
422
|
+
|
|
423
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
424
|
+
expect(sent).toHaveLength(1);
|
|
425
|
+
|
|
426
|
+
// Within the hour: nothing. This is still the dedupe promise.
|
|
427
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
428
|
+
expect(sent).toHaveLength(1);
|
|
429
|
+
|
|
430
|
+
// Age the failed attempt past the window.
|
|
431
|
+
const fp = store.list({ state: "active" })[0]?.fingerprint ?? "";
|
|
432
|
+
store.markNotified([fp], "text=failed", Date.now() - UNDELIVERED_RETRY_MS - 1000);
|
|
433
|
+
|
|
434
|
+
deliver = true;
|
|
435
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
436
|
+
expect(sent).toHaveLength(2);
|
|
437
|
+
|
|
438
|
+
// It landed, so it is never sent again however long it stays open.
|
|
439
|
+
store.markNotified([fp], "text=delivered", Date.now() - UNDELIVERED_RETRY_MS - 1000);
|
|
440
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
441
|
+
expect(sent).toHaveLength(2);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("a WARNING that failed is not retried — it was never entitled to a channel", async () => {
|
|
445
|
+
const store = createIncidentStore(new Database(":memory:"));
|
|
446
|
+
const sent: string[] = [];
|
|
447
|
+
const channel: IncidentChannel = {
|
|
448
|
+
key: "in-app",
|
|
449
|
+
label: "bell",
|
|
450
|
+
async send(m) {
|
|
451
|
+
sent.push(m.subject);
|
|
452
|
+
return { channel: "in-app", outcome: "failed", detail: "no" };
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
const notifier = createIncidentNotifier({ store, channels: [channel] });
|
|
456
|
+
const check = checkOf(() => [{ ...stuckIngest(), severity: "warning" as const }]);
|
|
457
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
458
|
+
const fp = store.list({ state: "active" })[0]?.fingerprint ?? "";
|
|
459
|
+
store.markNotified([fp], "in-app=failed", Date.now() - UNDELIVERED_RETRY_MS - 1000);
|
|
460
|
+
await notifier.announce((await runIncidentSweep(store, [check])).transitions);
|
|
461
|
+
expect(sent).toHaveLength(1);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-server/incidents` — the fleet's ONE owner-alert primitive: a
|
|
3
|
+
* condition that is true for a while, told to the owner exactly once, and told
|
|
4
|
+
* to him again when it clears.
|
|
5
|
+
*
|
|
6
|
+
* ── Why it exists (the owner, 2026-08-13) ───────────────────────────────────
|
|
7
|
+
* *"I found out from an agent my collection uploads were stuck … there are
|
|
8
|
+
* probably all kinds of possible problems I miss over time, such as every orch
|
|
9
|
+
* task failing to deploy and live sites falling over 10 commits behind."*
|
|
10
|
+
* The incident behind it: 42 videos / 35 GB sat in the collections dropzone and
|
|
11
|
+
* every scheduled ingest failed all 42 with EPIPE against binary-server, while
|
|
12
|
+
* binary-server logged that it was under its 12 GB disk reserve — and the
|
|
13
|
+
* dropzone deletes originals once they reach the catalog. Nothing told him. Two
|
|
14
|
+
* separate deterministic checks would each have caught it on the first run.
|
|
15
|
+
*
|
|
16
|
+
* ── Why it is HERE and not in `satellite-kit` ───────────────────────────────
|
|
17
|
+
* The queued task suggested satellite-kit ("beside the metrics buffer"), and
|
|
18
|
+
* that would have been wrong for one measurable reason: **binary-server is one
|
|
19
|
+
* of the three apps the owner named, it is a different repo, and it depends on
|
|
20
|
+
* `cursedbelt` and `cwip` — not on satellite-kit.** Putting the engine in the
|
|
21
|
+
* kit would have meant bs either growing its own copy or growing a dependency on
|
|
22
|
+
* the satellites' internal kit, and the first of those is the drift this whole
|
|
23
|
+
* queue exists to end. The fleet-shaped DETECTORS (the deploy ledger, launchd,
|
|
24
|
+
* backups) are satellite-shaped and do live in the kit; the engine is here.
|
|
25
|
+
*
|
|
26
|
+
* ── The four pieces ─────────────────────────────────────────────────────────
|
|
27
|
+
* - {@link createIncidentStore} — one row per condition, and `reconcile`, which
|
|
28
|
+
* is the entire dedupe guarantee. Nothing else in the system decides whether
|
|
29
|
+
* something is new.
|
|
30
|
+
* - {@link runIncidentSweep} — runs checks; a check that THROWS resolves
|
|
31
|
+
* nothing, so a broken detector can never announce that an outage is over.
|
|
32
|
+
* - channels — in-app (the fleet's existing notification centre), email, and a
|
|
33
|
+
* text message from this Mac that reads `chat.db` back to prove it left.
|
|
34
|
+
* - {@link createIncidentNotifier} — composes the three-sentence message and
|
|
35
|
+
* sends it on OPEN, on ESCALATE and on RESOLVE. Never on an observation.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const store = createIncidentStore(db);
|
|
39
|
+
* const notifier = createIncidentNotifier({ store, channels });
|
|
40
|
+
* const result = await runIncidentSweep(store, checks);
|
|
41
|
+
* await notifier.announce(result.transitions); // usually sends nothing
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export {
|
|
45
|
+
type ChannelMessage,
|
|
46
|
+
type ChannelOutcome,
|
|
47
|
+
type ChannelResult,
|
|
48
|
+
createEmailChannel,
|
|
49
|
+
createInAppChannel,
|
|
50
|
+
createRelayEmailChannel,
|
|
51
|
+
createTextChannel,
|
|
52
|
+
type EmailChannelOptions,
|
|
53
|
+
type IncidentChannel,
|
|
54
|
+
type SendEmail,
|
|
55
|
+
type TextChannelOptions,
|
|
56
|
+
} from "./channels";
|
|
57
|
+
export {
|
|
58
|
+
type CheckOutcome,
|
|
59
|
+
type IncidentCheck,
|
|
60
|
+
runIncidentSweep,
|
|
61
|
+
type SweepResult,
|
|
62
|
+
} from "./checks";
|
|
63
|
+
export { INCIDENT_MIGRATIONS, INCIDENTS_TABLE } from "./migrations";
|
|
64
|
+
export {
|
|
65
|
+
type AnnounceResult,
|
|
66
|
+
composeOpened,
|
|
67
|
+
composeResolved,
|
|
68
|
+
type CreateNotifierOptions,
|
|
69
|
+
createIncidentNotifier,
|
|
70
|
+
type IncidentNotifier,
|
|
71
|
+
} from "./notifier";
|
|
72
|
+
export {
|
|
73
|
+
createIncidentReporter,
|
|
74
|
+
type IncidentReporter,
|
|
75
|
+
type IncidentReporterOptions,
|
|
76
|
+
} from "./reporter";
|
|
77
|
+
export {
|
|
78
|
+
type IncidentRoutesOptions,
|
|
79
|
+
mountIncidentMachineRoutes,
|
|
80
|
+
mountIncidentOwnerRoutes,
|
|
81
|
+
} from "./routes";
|
|
82
|
+
export {
|
|
83
|
+
assertNoContent,
|
|
84
|
+
createIncidentStore,
|
|
85
|
+
type IncidentStore,
|
|
86
|
+
type ReconcileOptions,
|
|
87
|
+
} from "./store";
|
|
88
|
+
export {
|
|
89
|
+
fingerprint,
|
|
90
|
+
type Incident,
|
|
91
|
+
type IncidentFact,
|
|
92
|
+
type IncidentReport,
|
|
93
|
+
type IncidentSeverity,
|
|
94
|
+
type IncidentState,
|
|
95
|
+
type IncidentTransitions,
|
|
96
|
+
} from "./types";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { Migration } from "../migration/types";
|
|
3
|
+
|
|
4
|
+
/** The one table this primitive owns. */
|
|
5
|
+
export const INCIDENTS_TABLE = "incidents";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ONE row per CONDITION, keyed by its fingerprint — not one row per observation.
|
|
9
|
+
*
|
|
10
|
+
* That is the whole dedupe design expressed as a schema: a poller that notices
|
|
11
|
+
* the same broken thing 288 times a day cannot produce 288 rows, because there
|
|
12
|
+
* is nowhere to put them. `seen_count` and `last_seen` absorb the repetition,
|
|
13
|
+
* and the notification columns record that the owner was told ONCE.
|
|
14
|
+
*
|
|
15
|
+
* 🔴 **Never retention-managed.** Every other telemetry table in this package
|
|
16
|
+
* is capped or aged out; this one is not, and must not be declared as a
|
|
17
|
+
* retention target. An incident is the record of something having gone wrong on
|
|
18
|
+
* a fleet whose owner's stated problem is *"I missed this for a long time"* —
|
|
19
|
+
* the row that answers "has this happened before?" six months later is the
|
|
20
|
+
* expensive one to have thrown away. The table is bounded by reality instead:
|
|
21
|
+
* a fingerprint is stable, so a recurring problem re-uses its row forever.
|
|
22
|
+
*/
|
|
23
|
+
export const INCIDENT_MIGRATIONS: readonly Migration[] = [
|
|
24
|
+
{
|
|
25
|
+
name: "0001_incidents",
|
|
26
|
+
up(db: Database): void {
|
|
27
|
+
db.run(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS ${INCIDENTS_TABLE} (
|
|
29
|
+
fingerprint TEXT PRIMARY KEY,
|
|
30
|
+
source TEXT NOT NULL,
|
|
31
|
+
check_key TEXT NOT NULL,
|
|
32
|
+
kind TEXT NOT NULL,
|
|
33
|
+
subject TEXT NOT NULL,
|
|
34
|
+
severity TEXT NOT NULL
|
|
35
|
+
CHECK (severity IN ('blocking', 'warning')),
|
|
36
|
+
state TEXT NOT NULL
|
|
37
|
+
CHECK (state IN ('open', 'acknowledged', 'resolved')),
|
|
38
|
+
title TEXT NOT NULL,
|
|
39
|
+
what_broke TEXT NOT NULL,
|
|
40
|
+
what_it_blocks TEXT NOT NULL,
|
|
41
|
+
what_to_do TEXT NOT NULL,
|
|
42
|
+
facts_json TEXT,
|
|
43
|
+
first_seen INTEGER NOT NULL,
|
|
44
|
+
last_seen INTEGER NOT NULL,
|
|
45
|
+
seen_count INTEGER NOT NULL DEFAULT 1,
|
|
46
|
+
episodes INTEGER NOT NULL DEFAULT 1,
|
|
47
|
+
acknowledged_at INTEGER,
|
|
48
|
+
resolved_at INTEGER,
|
|
49
|
+
notified_at INTEGER,
|
|
50
|
+
notify_result TEXT,
|
|
51
|
+
resolved_notified_at INTEGER
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
// The read every screen starts from: what is wrong right now.
|
|
55
|
+
db.run(
|
|
56
|
+
`CREATE INDEX IF NOT EXISTS idx_incidents_state
|
|
57
|
+
ON ${INCIDENTS_TABLE} (state, severity, last_seen DESC)`,
|
|
58
|
+
);
|
|
59
|
+
// The read `reconcile` does on every sweep, for every check.
|
|
60
|
+
db.run(
|
|
61
|
+
`CREATE INDEX IF NOT EXISTS idx_incidents_check
|
|
62
|
+
ON ${INCIDENTS_TABLE} (check_key, state)`,
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
];
|