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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a state TRANSITION into a message, and sending it exactly once.
|
|
3
|
+
*
|
|
4
|
+
* Everything that decides "does the owner hear about this?" is here, and there
|
|
5
|
+
* is very little of it, because the hard part was done in `store.ts`: this
|
|
6
|
+
* module only ever sees `opened`, `escalated` and `resolved`. There is no
|
|
7
|
+
* de-duplication logic in this file and there must never be — a second dedupe,
|
|
8
|
+
* on a timer, is how a system ends up with two disagreeing answers to "have I
|
|
9
|
+
* already said this?".
|
|
10
|
+
*
|
|
11
|
+
* ── The message the owner actually gets ─────────────────────────────────────
|
|
12
|
+
* The owner's standing complaint about agent hand-offs is that they name a
|
|
13
|
+
* symptom, a command and a credential without saying what any of them are. So
|
|
14
|
+
* an incident message is three sentences with fixed jobs, in this order:
|
|
15
|
+
*
|
|
16
|
+
* WHAT BROKE — in words, naming the app. Assume he does not know what
|
|
17
|
+
* your daemon, flag, tab or port is.
|
|
18
|
+
* WHAT IT BLOCKS — what he cannot do until it is fixed. This is the sentence
|
|
19
|
+
* that earns a text message at 2am; if you cannot write it,
|
|
20
|
+
* the incident is a `warning`.
|
|
21
|
+
* WHAT TO DO — a next step. A command carries its own `cd`, because he
|
|
22
|
+
* opens terminals anywhere (`fleet/rules/
|
|
23
|
+
* commands-must-run-from-anywhere.md`).
|
|
24
|
+
*
|
|
25
|
+
* ── 🔴 What is deliberately NOT in a message ────────────────────────────────
|
|
26
|
+
* File names, note titles, vault keys, e-mail addresses, anything a person
|
|
27
|
+
* typed. `subject` is an identifier and `facts` are numbers, both enforced at
|
|
28
|
+
* the store (`assertNoContent`). A text message is the least private surface
|
|
29
|
+
* this fleet has and several of its apps are private by construction.
|
|
30
|
+
*/
|
|
31
|
+
import type { ChannelMessage, ChannelResult, IncidentChannel } from "./channels";
|
|
32
|
+
import type { IncidentStore } from "./store";
|
|
33
|
+
import type { Incident, IncidentTransitions } from "./types";
|
|
34
|
+
|
|
35
|
+
export interface IncidentNotifier {
|
|
36
|
+
/** Send for every transition in `t`. Returns one result per message sent. */
|
|
37
|
+
announce: (t: IncidentTransitions) => Promise<AnnounceResult[]>;
|
|
38
|
+
/** The channels registered on this host, for the console's health row. */
|
|
39
|
+
readonly channels: readonly IncidentChannel[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AnnounceResult {
|
|
43
|
+
fingerprint: string;
|
|
44
|
+
transition: "opened" | "escalated" | "resolved";
|
|
45
|
+
results: ChannelResult[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How long to wait before trying a BLOCKING incident again when no channel
|
|
50
|
+
* managed to deliver it.
|
|
51
|
+
*
|
|
52
|
+
* 🔴 This exists because of a real hole found on the first live sweep: `roms`'
|
|
53
|
+
* overdue backup opened as blocking while the email channel was still
|
|
54
|
+
* misconfigured, both channels failed, and — because `notifiedAt` was stamped —
|
|
55
|
+
* nothing would ever have tried again. The owner would have had a blocking
|
|
56
|
+
* incident on a board he had no reason to open. The dedupe promise is "you are
|
|
57
|
+
* told ONCE", not "we attempt once": an attempt that reached nobody has not told
|
|
58
|
+
* him anything.
|
|
59
|
+
*
|
|
60
|
+
* It retries at most hourly, and it STOPS the moment any channel reports
|
|
61
|
+
* `delivered` — so a permanently broken channel costs one send an hour, and a
|
|
62
|
+
* working one costs exactly one message, which is the guarantee.
|
|
63
|
+
*/
|
|
64
|
+
export const UNDELIVERED_RETRY_MS = 60 * 60 * 1000;
|
|
65
|
+
|
|
66
|
+
/** Did any channel actually get it out? `skipped` is not delivery. */
|
|
67
|
+
const wasDelivered = (notifyResult: string | null): boolean =>
|
|
68
|
+
Boolean(notifyResult?.includes("=delivered"));
|
|
69
|
+
|
|
70
|
+
const factLine = (incident: Incident): string => {
|
|
71
|
+
const facts = Object.entries(incident.facts ?? {});
|
|
72
|
+
if (facts.length === 0) return "";
|
|
73
|
+
return `\n${facts.map(([k, v]) => `${k}: ${v}`).join(" · ")}`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** The message for a newly-open (or newly-blocking) incident. */
|
|
77
|
+
export function composeOpened(incident: Incident): ChannelMessage {
|
|
78
|
+
const tag = incident.severity === "blocking" ? "BLOCKED" : "Heads up";
|
|
79
|
+
return {
|
|
80
|
+
severity: incident.severity,
|
|
81
|
+
subject: `[${tag}] ${incident.title}`,
|
|
82
|
+
text: [
|
|
83
|
+
`What broke: ${incident.whatBroke}`,
|
|
84
|
+
`What it blocks: ${incident.whatItBlocks}`,
|
|
85
|
+
`What to do: ${incident.whatToDo}`,
|
|
86
|
+
`(${incident.source} · ${incident.kind} · ${incident.subject})${factLine(incident)}`,
|
|
87
|
+
].join("\n\n"),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The message for a resolve. It exists so "is it fixed?" is answered without
|
|
93
|
+
* him having to go and look — the same reason the open message exists.
|
|
94
|
+
*/
|
|
95
|
+
export function composeResolved(incident: Incident): ChannelMessage {
|
|
96
|
+
const minutes = Math.max(1, Math.round(((incident.resolvedAt ?? 0) - incident.firstSeen) / 60000));
|
|
97
|
+
return {
|
|
98
|
+
severity: incident.severity,
|
|
99
|
+
subject: `[cleared] ${incident.title}`,
|
|
100
|
+
text: [
|
|
101
|
+
`This is no longer happening. It was open for about ${minutes} minute${minutes === 1 ? "" : "s"}.`,
|
|
102
|
+
`It was: ${incident.whatBroke}`,
|
|
103
|
+
`(${incident.source} · ${incident.kind} · ${incident.subject})`,
|
|
104
|
+
].join("\n\n"),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface CreateNotifierOptions {
|
|
109
|
+
store: IncidentStore;
|
|
110
|
+
channels: readonly IncidentChannel[];
|
|
111
|
+
/**
|
|
112
|
+
* Called with every channel result, so a host can record channel health.
|
|
113
|
+
* `detectChannelHealth` in `detectors.ts` turns a run of failures into its
|
|
114
|
+
* own incident — which is why `ChannelMessage.avoid` exists.
|
|
115
|
+
*/
|
|
116
|
+
onChannelResult?: (fingerprint: string, result: ChannelResult) => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createIncidentNotifier(options: CreateNotifierOptions): IncidentNotifier {
|
|
120
|
+
const { store, channels } = options;
|
|
121
|
+
|
|
122
|
+
const dispatch = async (
|
|
123
|
+
incident: Incident,
|
|
124
|
+
message: ChannelMessage,
|
|
125
|
+
transition: AnnounceResult["transition"],
|
|
126
|
+
): Promise<AnnounceResult> => {
|
|
127
|
+
// An incident that IS a channel failure must not be announced down the
|
|
128
|
+
// channel it is about. Without this the first SES outage produces an
|
|
129
|
+
// endless retry loop against SES.
|
|
130
|
+
const avoid =
|
|
131
|
+
incident.kind === "notification-channel-failing" ? [incident.subject] : undefined;
|
|
132
|
+
const withAvoid: ChannelMessage = avoid ? { ...message, avoid } : message;
|
|
133
|
+
|
|
134
|
+
const results: ChannelResult[] = [];
|
|
135
|
+
for (const channel of channels) {
|
|
136
|
+
// 🔴 The `avoid` guard lives HERE, not in each channel. It was in every
|
|
137
|
+
// channel first and a spy channel in the spec walked straight past it —
|
|
138
|
+
// which is the same hole any channel added later would have had. One
|
|
139
|
+
// enforcement point, and a new channel inherits it by existing.
|
|
140
|
+
if (withAvoid.avoid?.includes(channel.key)) {
|
|
141
|
+
results.push({
|
|
142
|
+
channel: channel.key,
|
|
143
|
+
outcome: "skipped",
|
|
144
|
+
detail: "this message is ABOUT this channel — not sent down it",
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Channels are awaited in series on purpose: there are at most three,
|
|
149
|
+
// they are all fast, and a serial loop means the text channel's
|
|
150
|
+
// delivery-proof read cannot race the email channel's for the same
|
|
151
|
+
// laptop resources.
|
|
152
|
+
const result = await channel.send(withAvoid).catch(
|
|
153
|
+
(err): ChannelResult => ({
|
|
154
|
+
channel: channel.key,
|
|
155
|
+
outcome: "failed",
|
|
156
|
+
detail: err instanceof Error ? err.message.slice(0, 200) : String(err),
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
results.push(result);
|
|
160
|
+
options.onChannelResult?.(incident.fingerprint, result);
|
|
161
|
+
}
|
|
162
|
+
return { fingerprint: incident.fingerprint, transition, results };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const summarise = (results: ChannelResult[]): string =>
|
|
166
|
+
results.map((r) => `${r.channel}=${r.outcome}`).join(" ");
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
channels,
|
|
170
|
+
async announce(t) {
|
|
171
|
+
const out: AnnounceResult[] = [];
|
|
172
|
+
|
|
173
|
+
for (const incident of [...t.opened, ...t.escalated]) {
|
|
174
|
+
const transition = t.opened.includes(incident) ? "opened" : "escalated";
|
|
175
|
+
const result = await dispatch(incident, composeOpened(incident), transition);
|
|
176
|
+
// 🔴 Only stamp `notifiedAt` when there was actually a channel to try.
|
|
177
|
+
// A host with none (a prod deployment that only stores what the Mac
|
|
178
|
+
// pushes it) has not told him anything, and stamping it there is what
|
|
179
|
+
// made the resolve message below fire for an open he never saw.
|
|
180
|
+
if (result.results.length > 0) {
|
|
181
|
+
store.markNotified([incident.fingerprint], summarise(result.results));
|
|
182
|
+
}
|
|
183
|
+
out.push(result);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// A blocking incident nobody could be told about is retried, hourly,
|
|
187
|
+
// until one channel lands it. See `UNDELIVERED_RETRY_MS`.
|
|
188
|
+
for (const incident of t.unchanged) {
|
|
189
|
+
if (incident.severity !== "blocking") continue;
|
|
190
|
+
if (incident.notifiedAt === null) continue;
|
|
191
|
+
if (wasDelivered(incident.notifyResult)) continue;
|
|
192
|
+
if (Date.now() - incident.notifiedAt < UNDELIVERED_RETRY_MS) continue;
|
|
193
|
+
const result = await dispatch(incident, composeOpened(incident), "opened");
|
|
194
|
+
if (result.results.length > 0) {
|
|
195
|
+
store.markNotified([incident.fingerprint], summarise(result.results));
|
|
196
|
+
}
|
|
197
|
+
out.push(result);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const incident of t.resolved) {
|
|
201
|
+
// Never announce a resolve for something whose OPEN was never
|
|
202
|
+
// announced — the owner would get "cleared: X" for an X he has never
|
|
203
|
+
// heard of, which reads as a bug in the alerting rather than as good
|
|
204
|
+
// news. This happens for real: an incident opened, acknowledged and
|
|
205
|
+
// resolved between two sweeps of a host that had no channels.
|
|
206
|
+
if (incident.notifiedAt === null) continue;
|
|
207
|
+
const result = await dispatch(incident, composeResolved(incident), "resolved");
|
|
208
|
+
store.markResolvedNotified([incident.fingerprint]);
|
|
209
|
+
out.push(result);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return out;
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The other side of `POST /incidents/report` — how a service that is NOT the
|
|
3
|
+
* console tells the console what it can see.
|
|
4
|
+
*
|
|
5
|
+
* ── Why a service reports rather than being polled ──────────────────────────
|
|
6
|
+
* The console can measure a service's disk, its port and its HTTP status from
|
|
7
|
+
* outside. It cannot measure the things that actually go wrong inside one: an
|
|
8
|
+
* ingest run where every item failed, a queue whose worker is looping on the
|
|
9
|
+
* same error, a store that is refusing writes for a reason of its own. Those
|
|
10
|
+
* facts exist for a few milliseconds inside the process that has them, and the
|
|
11
|
+
* only place they can be turned into an incident is there.
|
|
12
|
+
*
|
|
13
|
+
* ── 🔴 Reporting is a HEARTBEAT, not an event ───────────────────────────────
|
|
14
|
+
* A reporter sends the complete set of conditions that are true right now — the
|
|
15
|
+
* same contract a local check has — and sends it on a schedule whether or not
|
|
16
|
+
* anything is wrong. That is what lets the console clear an incident when the
|
|
17
|
+
* service recovers. A reporter that only posts on failure can open an incident
|
|
18
|
+
* and can never close one, and the owner ends up with a permanently red board,
|
|
19
|
+
* which is the same uselessness as a permanently green one.
|
|
20
|
+
*
|
|
21
|
+
* The dedupe is entirely the console's: post the same thing every five minutes
|
|
22
|
+
* for a week and he is told once. So a reporter needs no state, no "have I said
|
|
23
|
+
* this?" flag, and no backoff — which is why this file is forty lines and not
|
|
24
|
+
* four hundred.
|
|
25
|
+
*
|
|
26
|
+
* ── It never throws, and never blocks the service ───────────────────────────
|
|
27
|
+
* A service whose real work fails because its ALERTING is unreachable is worse
|
|
28
|
+
* than one with no alerting. Every failure here is logged and swallowed, and the
|
|
29
|
+
* console notices the silence by other means (the service's own health probe).
|
|
30
|
+
*/
|
|
31
|
+
import type { IncidentReport } from "./types";
|
|
32
|
+
|
|
33
|
+
export interface IncidentReporterOptions {
|
|
34
|
+
/** e.g. `http://127.0.0.1:3024/api/station/incidents/report`. */
|
|
35
|
+
url: string;
|
|
36
|
+
/** The shared bearer the console's `reporterToken` compares against. */
|
|
37
|
+
token: string;
|
|
38
|
+
/** This service's name — the `source` of everything it reports. */
|
|
39
|
+
source: string;
|
|
40
|
+
/** A stable key for this reporter, owning the incidents it opens. */
|
|
41
|
+
checkKey: string;
|
|
42
|
+
/**
|
|
43
|
+
* Every `kind` this reporter can produce. Required, not optional: without it
|
|
44
|
+
* the console cannot tell "nothing is wrong" from "nothing was said", so a
|
|
45
|
+
* recovered service would stay red for ever. See `ReconcileOptions`.
|
|
46
|
+
*/
|
|
47
|
+
kinds: readonly string[];
|
|
48
|
+
/** Produce the current set of true conditions. Called on every beat. */
|
|
49
|
+
collect: () => Promise<IncidentReport[]> | IncidentReport[];
|
|
50
|
+
/** Default 5 minutes. The console dedupes, so this is chosen for the network. */
|
|
51
|
+
intervalMs?: number;
|
|
52
|
+
fetchImpl?: typeof fetch;
|
|
53
|
+
log?: (message: string) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface IncidentReporter {
|
|
57
|
+
/** Send one beat now. Resolves `false` on any failure — it never throws. */
|
|
58
|
+
beat: () => Promise<boolean>;
|
|
59
|
+
start: () => void;
|
|
60
|
+
stop: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createIncidentReporter(options: IncidentReporterOptions): IncidentReporter {
|
|
64
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
65
|
+
const log = options.log ?? ((m: string) => console.warn(`[incidents] ${m}`));
|
|
66
|
+
const intervalMs = options.intervalMs ?? 5 * 60_000;
|
|
67
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
68
|
+
|
|
69
|
+
const beat = async (): Promise<boolean> => {
|
|
70
|
+
try {
|
|
71
|
+
const reports = await options.collect();
|
|
72
|
+
const res = await doFetch(options.url, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"content-type": "application/json",
|
|
76
|
+
authorization: `Bearer ${options.token}`,
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
checkKey: options.checkKey,
|
|
80
|
+
source: options.source,
|
|
81
|
+
kinds: options.kinds,
|
|
82
|
+
reports,
|
|
83
|
+
}),
|
|
84
|
+
signal: AbortSignal.timeout(10_000),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
log(`the console answered ${res.status} — ${reports.length} finding(s) not filed`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log(`could not reach the console: ${err instanceof Error ? err.message : String(err)}`);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
beat,
|
|
99
|
+
start() {
|
|
100
|
+
if (timer) return;
|
|
101
|
+
timer = setInterval(() => void beat(), intervalMs);
|
|
102
|
+
// Never hold the process open for the sake of its own alerting.
|
|
103
|
+
timer.unref?.();
|
|
104
|
+
},
|
|
105
|
+
stop() {
|
|
106
|
+
if (timer) clearInterval(timer);
|
|
107
|
+
timer = null;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mountable half — ordinary Hono routes, so any app can expose its incidents
|
|
3
|
+
* without adopting another app's rpc layer (the same reasoning, and the same
|
|
4
|
+
* shape, as `server/notifications/routes.ts`).
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // BEFORE the session gate — the machine doors carry their own bearer:
|
|
8
|
+
* mountIncidentMachineRoutes(app, { store, notifier, reporterToken });
|
|
9
|
+
* // INSIDE it — the owner's own surface:
|
|
10
|
+
* mountIncidentOwnerRoutes(app, { store, notifier, sweep, authorize: isOwner });
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* ── 🔴 Two doors, two MOUNTS, and that is the whole point ───────────────────
|
|
14
|
+
* `authorize` is the OWNER's session — it guards the list, the acks, the manual
|
|
15
|
+
* sweep and the channel test. `reporterToken` is a shared bearer and guards the
|
|
16
|
+
* two routes a DAEMON uses. They answer different questions ("is this the
|
|
17
|
+
* owner's browser?" and "is this one of my own processes?"), so a reporter can
|
|
18
|
+
* never read the incident list and a browser session can never forge an
|
|
19
|
+
* incident.
|
|
20
|
+
*
|
|
21
|
+
* They were one function first, and the console mounted it inside its session
|
|
22
|
+
* gate. Every beat from binary-server and orch came back
|
|
23
|
+
* `{"error":"unauthorized"}` — from the GATE, so nothing in this module logged
|
|
24
|
+
* anything and the console showed a perfectly calm, empty board. A reporter
|
|
25
|
+
* cannot report that it could not reach the reporting door. Two functions make
|
|
26
|
+
* the mount order a thing you have to get right rather than a thing you can
|
|
27
|
+
* forget, and they stop the opposite mistake too: registering the owner's
|
|
28
|
+
* `GET /incidents` outside the gate, where Hono's first-match would shadow the
|
|
29
|
+
* real one for ever.
|
|
30
|
+
*
|
|
31
|
+
* 🔴 `POST /report` is the ONE route that accepts incident text from off-box, so
|
|
32
|
+
* it is also the one that has to be paranoid about it: every field is length-
|
|
33
|
+
* capped and `facts` is re-validated through the store's `assertNoContent`
|
|
34
|
+
* before anything is written. A reporter is trusted to be honest, not to be
|
|
35
|
+
* correct.
|
|
36
|
+
*/
|
|
37
|
+
import type { Hono } from "hono";
|
|
38
|
+
import type { IncidentChannel } from "./channels";
|
|
39
|
+
import type { IncidentNotifier } from "./notifier";
|
|
40
|
+
import { assertNoContent, type IncidentStore } from "./store";
|
|
41
|
+
import type { IncidentReport, IncidentSeverity } from "./types";
|
|
42
|
+
import type { SweepResult } from "./checks";
|
|
43
|
+
|
|
44
|
+
const noStore = { "cache-control": "no-store" } as const;
|
|
45
|
+
|
|
46
|
+
/** Long enough for three real sentences, short enough that nobody pastes a log in. */
|
|
47
|
+
const MAX_FIELD = 600;
|
|
48
|
+
const MAX_REPORTS = 100;
|
|
49
|
+
|
|
50
|
+
export interface IncidentRoutesOptions {
|
|
51
|
+
store: IncidentStore;
|
|
52
|
+
/** Owner-only gate for the read + write surface. Return false to 403. */
|
|
53
|
+
authorize: (c: unknown) => boolean | Promise<boolean>;
|
|
54
|
+
/** Run the checks now. Omitted on a host that runs no detectors (prod). */
|
|
55
|
+
sweep?: () => Promise<SweepResult>;
|
|
56
|
+
/** Used to announce anything a remote report opens, and to list channel health. */
|
|
57
|
+
notifier?: IncidentNotifier;
|
|
58
|
+
/** Shared bearer for `POST /report`. Omitted, the route is not mounted. */
|
|
59
|
+
reporterToken?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Deliver one message as this host. Mounted as `POST /relay-email` and used
|
|
62
|
+
* by the local console, which cannot reach SES — see `channels.ts`.
|
|
63
|
+
*/
|
|
64
|
+
relayEmail?: (msg: {
|
|
65
|
+
subject: string;
|
|
66
|
+
text: string;
|
|
67
|
+
html?: string;
|
|
68
|
+
}) => Promise<{ sent: boolean; reason?: string }>;
|
|
69
|
+
/** The last sweep, for the health route. */
|
|
70
|
+
lastSweep?: () => SweepResult | null;
|
|
71
|
+
/** Send a test message down one channel, so the owner can prove it works. */
|
|
72
|
+
channelsOf?: () => readonly IncidentChannel[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const str = (v: unknown, fallback = ""): string =>
|
|
76
|
+
typeof v === "string" ? v.slice(0, MAX_FIELD) : fallback;
|
|
77
|
+
|
|
78
|
+
/** Parse one posted report defensively. Throws with a usable message. */
|
|
79
|
+
function parseReport(raw: unknown): IncidentReport {
|
|
80
|
+
if (!raw || typeof raw !== "object") throw new Error("each report must be an object");
|
|
81
|
+
const r = raw as Record<string, unknown>;
|
|
82
|
+
const severity: IncidentSeverity = r.severity === "blocking" ? "blocking" : "warning";
|
|
83
|
+
const report: IncidentReport = {
|
|
84
|
+
source: str(r.source),
|
|
85
|
+
kind: str(r.kind),
|
|
86
|
+
subject: str(r.subject),
|
|
87
|
+
severity,
|
|
88
|
+
title: str(r.title),
|
|
89
|
+
whatBroke: str(r.whatBroke),
|
|
90
|
+
whatItBlocks: str(r.whatItBlocks),
|
|
91
|
+
whatToDo: str(r.whatToDo),
|
|
92
|
+
...(r.facts && typeof r.facts === "object"
|
|
93
|
+
? { facts: r.facts as IncidentReport["facts"] }
|
|
94
|
+
: {}),
|
|
95
|
+
};
|
|
96
|
+
for (const field of ["source", "kind", "subject", "title", "whatBroke"] as const) {
|
|
97
|
+
if (!report[field]) throw new Error(`report is missing "${field}"`);
|
|
98
|
+
}
|
|
99
|
+
// The privacy gate, applied to off-box input before it can reach a phone.
|
|
100
|
+
assertNoContent(report);
|
|
101
|
+
return report;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The OWNER's surface: the list, the acks, the health strip, the manual sweep,
|
|
106
|
+
* the channel test. Mount it INSIDE the app's session gate.
|
|
107
|
+
*/
|
|
108
|
+
export function mountIncidentOwnerRoutes(
|
|
109
|
+
app: Hono,
|
|
110
|
+
options: IncidentRoutesOptions,
|
|
111
|
+
basePath = "/incidents",
|
|
112
|
+
): void {
|
|
113
|
+
const { store } = options;
|
|
114
|
+
|
|
115
|
+
const guard = async (c: { json: (b: unknown, s?: number) => Response }): Promise<Response | null> =>
|
|
116
|
+
(await options.authorize(c)) ? null : c.json({ error: "forbidden" }, 403);
|
|
117
|
+
|
|
118
|
+
app.get(`${basePath}`, async (c) => {
|
|
119
|
+
const denied = await guard(c);
|
|
120
|
+
if (denied) return denied;
|
|
121
|
+
const state = c.req.query("state");
|
|
122
|
+
const incidents = store.list({
|
|
123
|
+
state:
|
|
124
|
+
state === "all"
|
|
125
|
+
? undefined
|
|
126
|
+
: state === "resolved"
|
|
127
|
+
? "resolved"
|
|
128
|
+
: state === "open"
|
|
129
|
+
? "open"
|
|
130
|
+
: "active",
|
|
131
|
+
limit: Number(c.req.query("limit") ?? 200),
|
|
132
|
+
});
|
|
133
|
+
return c.json({ incidents }, 200, noStore);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
app.post(`${basePath}/:fingerprint/ack`, async (c) => {
|
|
137
|
+
const denied = await guard(c);
|
|
138
|
+
if (denied) return denied;
|
|
139
|
+
const ok = store.acknowledge(decodeURIComponent(c.req.param("fingerprint")));
|
|
140
|
+
return c.json({ ok }, ok ? 200 : 404, noStore);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
app.get(`${basePath}/health`, async (c) => {
|
|
144
|
+
const denied = await guard(c);
|
|
145
|
+
if (denied) return denied;
|
|
146
|
+
return c.json(
|
|
147
|
+
{
|
|
148
|
+
channels: (options.channelsOf?.() ?? options.notifier?.channels ?? []).map((ch) => ({
|
|
149
|
+
key: ch.key,
|
|
150
|
+
label: ch.label,
|
|
151
|
+
})),
|
|
152
|
+
lastSweep: options.lastSweep?.() ?? null,
|
|
153
|
+
sweepable: Boolean(options.sweep),
|
|
154
|
+
},
|
|
155
|
+
200,
|
|
156
|
+
noStore,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (options.sweep) {
|
|
161
|
+
app.post(`${basePath}/sweep`, async (c) => {
|
|
162
|
+
const denied = await guard(c);
|
|
163
|
+
if (denied) return denied;
|
|
164
|
+
const result = await options.sweep?.();
|
|
165
|
+
if (!result) return c.json({ error: "no detectors on this host" }, 400, noStore);
|
|
166
|
+
const announced = options.notifier ? await options.notifier.announce(result.transitions) : [];
|
|
167
|
+
return c.json({ result, announced }, 200, noStore);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (options.channelsOf) {
|
|
172
|
+
app.post(`${basePath}/test/:channel`, async (c) => {
|
|
173
|
+
const denied = await guard(c);
|
|
174
|
+
if (denied) return denied;
|
|
175
|
+
const key = c.req.param("channel");
|
|
176
|
+
const channel = options.channelsOf?.().find((ch) => ch.key === key);
|
|
177
|
+
if (!channel) return c.json({ error: `no channel "${key}" on this host` }, 404, noStore);
|
|
178
|
+
// Sent at `blocking` because that is the severity whose delivery matters —
|
|
179
|
+
// a test that exercises the in-app path and skips the phone proves nothing.
|
|
180
|
+
const result = await channel.send({
|
|
181
|
+
severity: "blocking",
|
|
182
|
+
subject: "[test] fleet incident alerts",
|
|
183
|
+
text: "This is the console proving the channel works. Nothing is wrong. No action needed.",
|
|
184
|
+
});
|
|
185
|
+
return c.json({ result }, 200, noStore);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The MACHINE doors: `POST /report`, which another service beats its findings
|
|
192
|
+
* into, and `POST /relay-email`, which a host that cannot send mail hands a
|
|
193
|
+
* message to. Both carry a shared bearer.
|
|
194
|
+
*
|
|
195
|
+
* 🔴 **Mount this BEFORE the session gate, and never call it with `authorize`
|
|
196
|
+
* expecting it to matter — it registers no route a browser should reach.**
|
|
197
|
+
* Mounting the two together inside a cookie gate is the trap this split exists
|
|
198
|
+
* to make impossible: every beat from binary-server and orch came back
|
|
199
|
+
* `{"error":"unauthorized"}` from the gate, so no log line in this module said
|
|
200
|
+
* anything was wrong and the console rendered a perfectly calm, empty board.
|
|
201
|
+
* A reporter cannot report the failure to reach the reporting door.
|
|
202
|
+
*
|
|
203
|
+
* Keeping them in one function also risked the opposite mistake — registering
|
|
204
|
+
* the owner's `GET /incidents` outside the gate, where Hono's first-match would
|
|
205
|
+
* have shadowed the real one for ever.
|
|
206
|
+
*/
|
|
207
|
+
export function mountIncidentMachineRoutes(
|
|
208
|
+
app: Hono,
|
|
209
|
+
options: Pick<IncidentRoutesOptions, "store" | "notifier" | "reporterToken" | "relayEmail">,
|
|
210
|
+
basePath = "/incidents",
|
|
211
|
+
): void {
|
|
212
|
+
const { store } = options;
|
|
213
|
+
if (options.reporterToken) {
|
|
214
|
+
app.post(`${basePath}/report`, async (c) => {
|
|
215
|
+
const header = c.req.header("authorization") ?? "";
|
|
216
|
+
const presented = header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
217
|
+
// Length-first compare then constant-time-ish equality; the token is a
|
|
218
|
+
// shared secret between our own daemons, not a user credential.
|
|
219
|
+
if (
|
|
220
|
+
presented.length !== options.reporterToken?.length ||
|
|
221
|
+
presented !== options.reporterToken
|
|
222
|
+
) {
|
|
223
|
+
return c.json({ error: "forbidden" }, 403, noStore);
|
|
224
|
+
}
|
|
225
|
+
const body = (await c.req.json().catch(() => null)) as {
|
|
226
|
+
checkKey?: unknown;
|
|
227
|
+
source?: unknown;
|
|
228
|
+
kinds?: unknown;
|
|
229
|
+
reports?: unknown;
|
|
230
|
+
} | null;
|
|
231
|
+
const checkKey = str(body?.checkKey);
|
|
232
|
+
const source = str(body?.source);
|
|
233
|
+
const rawReports = Array.isArray(body?.reports) ? body.reports : null;
|
|
234
|
+
if (!checkKey || !source || !rawReports) {
|
|
235
|
+
return c.json(
|
|
236
|
+
{ error: "checkKey, source and reports[] are required" },
|
|
237
|
+
400,
|
|
238
|
+
noStore,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (rawReports.length > MAX_REPORTS) {
|
|
242
|
+
return c.json({ error: `at most ${MAX_REPORTS} reports per call` }, 400, noStore);
|
|
243
|
+
}
|
|
244
|
+
let reports: IncidentReport[];
|
|
245
|
+
try {
|
|
246
|
+
reports = rawReports.map(parseReport);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return c.json(
|
|
249
|
+
{ error: err instanceof Error ? err.message : "bad report" },
|
|
250
|
+
400,
|
|
251
|
+
noStore,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
// A reporter declares the kinds it is authoritative for, exactly as a
|
|
255
|
+
// local check does — without them a remote service could never clear its
|
|
256
|
+
// own incidents, and every transient blip would stay red forever.
|
|
257
|
+
const kinds = Array.isArray(body?.kinds)
|
|
258
|
+
? (body.kinds as unknown[]).filter((k): k is string => typeof k === "string")
|
|
259
|
+
: [...new Set(reports.map((r) => r.kind))];
|
|
260
|
+
const transitions = store.reconcile(checkKey, reports, {
|
|
261
|
+
namespaces: kinds.map((k) => `${source}:${k}:`),
|
|
262
|
+
});
|
|
263
|
+
const announced = options.notifier ? await options.notifier.announce(transitions) : [];
|
|
264
|
+
return c.json(
|
|
265
|
+
{
|
|
266
|
+
opened: transitions.opened.length,
|
|
267
|
+
escalated: transitions.escalated.length,
|
|
268
|
+
resolved: transitions.resolved.length,
|
|
269
|
+
unchanged: transitions.unchanged.length,
|
|
270
|
+
announced,
|
|
271
|
+
},
|
|
272
|
+
200,
|
|
273
|
+
noStore,
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (options.relayEmail) {
|
|
279
|
+
app.post(`${basePath}/relay-email`, async (c) => {
|
|
280
|
+
const header = c.req.header("authorization") ?? "";
|
|
281
|
+
const presented = header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
282
|
+
if (
|
|
283
|
+
!options.reporterToken ||
|
|
284
|
+
presented.length !== options.reporterToken.length ||
|
|
285
|
+
presented !== options.reporterToken
|
|
286
|
+
) {
|
|
287
|
+
return c.json({ error: "forbidden" }, 403, noStore);
|
|
288
|
+
}
|
|
289
|
+
const body = (await c.req.json().catch(() => null)) as {
|
|
290
|
+
subject?: unknown;
|
|
291
|
+
text?: unknown;
|
|
292
|
+
html?: unknown;
|
|
293
|
+
} | null;
|
|
294
|
+
const subject = str(body?.subject);
|
|
295
|
+
const text = typeof body?.text === "string" ? body.text.slice(0, 4000) : "";
|
|
296
|
+
if (!subject || !text) return c.json({ error: "subject and text required" }, 400, noStore);
|
|
297
|
+
// 🔴 The REASON crosses the wire, not just the boolean. `{sent:false}`
|
|
298
|
+
// alone made the local console report "the prod console accepted the
|
|
299
|
+
// request but its mailer declined" — true, useless, and indistinguishable
|
|
300
|
+
// between "mail is off on that host" and "SES refused the address". The
|
|
301
|
+
// relay is the only place that knows.
|
|
302
|
+
const outcome = await options.relayEmail?.({
|
|
303
|
+
subject,
|
|
304
|
+
text,
|
|
305
|
+
...(typeof body?.html === "string" ? { html: body.html.slice(0, 20_000) } : {}),
|
|
306
|
+
});
|
|
307
|
+
return c.json(
|
|
308
|
+
{ sent: Boolean(outcome?.sent), ...(outcome?.reason ? { reason: outcome.reason } : {}) },
|
|
309
|
+
200,
|
|
310
|
+
noStore,
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|