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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fleet's retention SOURCE OF TRUTH on disk — where declarations are
|
|
3
|
+
* published and where the owner's overrides are read from.
|
|
4
|
+
*
|
|
5
|
+
* ── Why a file and not an API ───────────────────────────────────────────────
|
|
6
|
+
* The station has to show, and let the owner change, the policy for apps that
|
|
7
|
+
* are not running. Most satellites are started on demand; `orch` is a daemon but
|
|
8
|
+
* `notes`, `roms`, `vault` and the rest are usually down, and the whole point of
|
|
9
|
+
* the UI is to answer "what is keeping my disk full" without booting anything.
|
|
10
|
+
* An HTTP endpoint per app could not answer that, would need an auth model per
|
|
11
|
+
* app, and would make a stopped app look policy-less rather than idle.
|
|
12
|
+
*
|
|
13
|
+
* So an app PUBLISHES its declarations (a description of what it owns, plus what
|
|
14
|
+
* it last measured and swept) and READS overrides. The station reads every
|
|
15
|
+
* publication and is the only writer of overrides. Nothing has to be up for the
|
|
16
|
+
* owner to see the fleet, and no app parses another app's database.
|
|
17
|
+
*
|
|
18
|
+
* ── One file per app, deliberately ──────────────────────────────────────────
|
|
19
|
+
* `apps/<app>.json`, never one shared registry file. Several satellites boot at
|
|
20
|
+
* once (the station's start-all, a `bun run dev` fan-out, launchd at login), and
|
|
21
|
+
* a shared file that concurrent writers read-modify-write loses records — the
|
|
22
|
+
* exact bug the agent-coordination board is one-file-per-record to avoid. Each
|
|
23
|
+
* app owns exactly one path and can only ever clobber itself.
|
|
24
|
+
*
|
|
25
|
+
* `overrides.json` IS a single file, because it has a single writer (the
|
|
26
|
+
* station) and because the owner's settings for the whole fleet are one thing to
|
|
27
|
+
* back up, copy to a box, or read at a glance.
|
|
28
|
+
*
|
|
29
|
+
* ── Where it lives ──────────────────────────────────────────────────────────
|
|
30
|
+
* `<app-data-home>/retention` — one root shared by every local app, so the
|
|
31
|
+
* station sees one fleet. The home is CONFIGURATION ({@link resolveAppDataHome}):
|
|
32
|
+
* `$APP_DATA_HOME` when a machine keeps every app's data under one root, else the
|
|
33
|
+
* OS's own convention (`$XDG_DATA_HOME`, `~/Library/Application Support`).
|
|
34
|
+
*
|
|
35
|
+
* 🔴 It was `~/<a private dotfolder>` until 2026-09-13, hardcoded, in a PUBLISHED
|
|
36
|
+
* package — so anyone who installed cursedbelt got a library hunting for one
|
|
37
|
+
* person's directory layout. A library may know what its caller told it and what
|
|
38
|
+
* the OS says; it may not know the name of a folder in somebody's home.
|
|
39
|
+
*
|
|
40
|
+
* A deployed box under `ProtectSystem=strict` has only its own data dir writable,
|
|
41
|
+
* so it falls back to `$APP_DATA_DIR/retention`: the app still sweeps on the same
|
|
42
|
+
* code and the same defaults, and a station instance on that box reads that box's
|
|
43
|
+
* fleet. The resolution probes WRITABILITY rather than assuming, because the
|
|
44
|
+
* failure it prevents — an app whose every publish silently EROFS-es — is
|
|
45
|
+
* invisible until someone asks why the UI is empty.
|
|
46
|
+
*/
|
|
47
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
import { APP_DATA_HOME_ENV, resolveAppDataHome } from "../paths/appDataHome";
|
|
50
|
+
import { retentionKey } from "./policy";
|
|
51
|
+
/**
|
|
52
|
+
* A FUNCTION, not a shared constant. `writeRetentionOverride` mutates what
|
|
53
|
+
* `readRetentionOverrides` hands it, so a module-level empty object would be
|
|
54
|
+
* aliased into every caller: the first write on a machine with no overrides file
|
|
55
|
+
* would edit the constant itself, and a later read of an unrelated home would
|
|
56
|
+
* return those overrides. Caught by `store.test.ts`.
|
|
57
|
+
*/
|
|
58
|
+
const emptyOverrides = () => ({ version: 1, updatedAt: 0, overrides: {} });
|
|
59
|
+
let cachedHome = null;
|
|
60
|
+
/** Can we create and write inside this directory? Probed, never assumed. */
|
|
61
|
+
function isUsable(dir) {
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
const probe = join(dir, ".write-probe");
|
|
65
|
+
writeFileSync(probe, "");
|
|
66
|
+
rmSync(probe, { force: true });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the retention home. See the module note for the ordering rationale.
|
|
75
|
+
* Cached per process — it is consulted on every sweep and cannot change under a
|
|
76
|
+
* running app.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveRetentionHome(env = process.env) {
|
|
79
|
+
if (cachedHome)
|
|
80
|
+
return cachedHome;
|
|
81
|
+
const candidates = [];
|
|
82
|
+
if (env.RETENTION_HOME)
|
|
83
|
+
candidates.push(env.RETENTION_HOME);
|
|
84
|
+
// An explicitly CONFIGURED home is used whether or not it exists yet — the
|
|
85
|
+
// operator naming it is the statement that it is the right place.
|
|
86
|
+
const configured = env[APP_DATA_HOME_ENV]?.trim();
|
|
87
|
+
if (configured)
|
|
88
|
+
candidates.push(join(configured, "retention"));
|
|
89
|
+
const conventional = resolveAppDataHome({ env });
|
|
90
|
+
// The OS convention is only PREFERRED when it is already there: on a deployed
|
|
91
|
+
// box creating it would either fail (read-only namespace) or strand a
|
|
92
|
+
// publication somewhere no station instance looks.
|
|
93
|
+
if (!configured && existsSync(conventional))
|
|
94
|
+
candidates.push(join(conventional, "retention"));
|
|
95
|
+
if (env.APP_DATA_DIR)
|
|
96
|
+
candidates.push(join(env.APP_DATA_DIR, "retention"));
|
|
97
|
+
candidates.push(join(conventional, "retention"));
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
if (isUsable(candidate)) {
|
|
100
|
+
cachedHome = candidate;
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Nothing is writable — return the conventional path anyway. Every writer here
|
|
105
|
+
// is best-effort and will log its failure; retention must not stop an app from
|
|
106
|
+
// booting.
|
|
107
|
+
cachedHome = join(conventional, "retention");
|
|
108
|
+
return cachedHome;
|
|
109
|
+
}
|
|
110
|
+
/** Test seam — point the home somewhere else for this process. */
|
|
111
|
+
export function setRetentionHomeForTests(home) {
|
|
112
|
+
cachedHome = home;
|
|
113
|
+
}
|
|
114
|
+
const appsDir = (home) => join(home, "apps");
|
|
115
|
+
const overridesPath = (home) => join(home, "overrides.json");
|
|
116
|
+
/** A file name cannot be derived from an app key without pinning the charset. */
|
|
117
|
+
const SAFE_APP = /^[a-z0-9][a-z0-9-]*$/;
|
|
118
|
+
/**
|
|
119
|
+
* Write whole, atomically: a reader must never see half a file. `rename` inside
|
|
120
|
+
* one directory is atomic on every filesystem this fleet runs on, and the temp
|
|
121
|
+
* name carries the pid so two writers cannot share a scratch path.
|
|
122
|
+
*/
|
|
123
|
+
function writeJsonAtomic(path, value) {
|
|
124
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
125
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
126
|
+
renameSync(tmp, path);
|
|
127
|
+
}
|
|
128
|
+
function readJson(path, fallback) {
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Absent is the normal first-run case; corrupt is a truncated write from a
|
|
134
|
+
// killed process. Both mean "no settings yet" — never a boot failure.
|
|
135
|
+
return fallback;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Publish one app's resources. Best-effort by contract: a retention publication
|
|
140
|
+
* that cannot be written is a missing row in a dashboard, and must never take
|
|
141
|
+
* down the app that owns the data.
|
|
142
|
+
*/
|
|
143
|
+
export function publishRetention(publication, options = {}) {
|
|
144
|
+
const home = options.home ?? resolveRetentionHome();
|
|
145
|
+
if (!SAFE_APP.test(publication.app))
|
|
146
|
+
return false;
|
|
147
|
+
try {
|
|
148
|
+
mkdirSync(appsDir(home), { recursive: true });
|
|
149
|
+
writeJsonAtomic(join(appsDir(home), `${publication.app}.json`), {
|
|
150
|
+
version: 1,
|
|
151
|
+
publishedAt: Date.now(),
|
|
152
|
+
...publication,
|
|
153
|
+
});
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
options.log?.(`⚠️ retention: could not publish ${publication.app} — ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Every app's publication, newest-published first. */
|
|
162
|
+
export function readRetentionRegistry(home = resolveRetentionHome()) {
|
|
163
|
+
const dir = appsDir(home);
|
|
164
|
+
let names;
|
|
165
|
+
try {
|
|
166
|
+
names = readdirSync(dir).filter((n) => n.endsWith(".json"));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
const publications = [];
|
|
172
|
+
for (const name of names) {
|
|
173
|
+
const parsed = readJson(join(dir, name), null);
|
|
174
|
+
if (parsed && Array.isArray(parsed.resources) && typeof parsed.app === "string") {
|
|
175
|
+
publications.push(parsed);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return publications.sort((a, b) => (b.publishedAt ?? 0) - (a.publishedAt ?? 0));
|
|
179
|
+
}
|
|
180
|
+
export function readRetentionOverrides(home = resolveRetentionHome()) {
|
|
181
|
+
const file = readJson(overridesPath(home), emptyOverrides());
|
|
182
|
+
if (!file || typeof file !== "object" || typeof file.overrides !== "object" || !file.overrides) {
|
|
183
|
+
return emptyOverrides();
|
|
184
|
+
}
|
|
185
|
+
return { version: 1, updatedAt: file.updatedAt ?? 0, overrides: file.overrides };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Apply one override. Read-modify-write, so an unrelated resource's setting is
|
|
189
|
+
* never lost to a concurrent edit of this one; the station is the only writer,
|
|
190
|
+
* which is what keeps that honest.
|
|
191
|
+
*
|
|
192
|
+
* A patch field set to `undefined` is left alone; set to `null` it is CLEARED,
|
|
193
|
+
* which is how the UI's "reset to default" works — deleting the key is the only
|
|
194
|
+
* way to make the declaration's default authoritative again, since a stored
|
|
195
|
+
* value that merely equals the default would freeze the resource at today's
|
|
196
|
+
* number if the default ever moves.
|
|
197
|
+
*/
|
|
198
|
+
export function writeRetentionOverride(app, id, patch, options = {}) {
|
|
199
|
+
const home = options.home ?? resolveRetentionHome();
|
|
200
|
+
const now = options.now ?? Date.now();
|
|
201
|
+
const file = readRetentionOverrides(home);
|
|
202
|
+
const key = retentionKey(app, id);
|
|
203
|
+
if (patch === null) {
|
|
204
|
+
delete file.overrides[key];
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const current = { ...(file.overrides[key] ?? {}) };
|
|
208
|
+
for (const field of ["days", "maxItems", "maxBytes"]) {
|
|
209
|
+
if (field in patch) {
|
|
210
|
+
const value = patch[field];
|
|
211
|
+
if (value === null)
|
|
212
|
+
delete current[field];
|
|
213
|
+
else if (typeof value === "number" && Number.isFinite(value))
|
|
214
|
+
current[field] = value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if ("enabled" in patch) {
|
|
218
|
+
const value = patch.enabled;
|
|
219
|
+
if (value === null)
|
|
220
|
+
delete current.enabled;
|
|
221
|
+
else if (typeof value === "boolean")
|
|
222
|
+
current.enabled = value;
|
|
223
|
+
}
|
|
224
|
+
if ("note" in patch) {
|
|
225
|
+
const value = patch.note;
|
|
226
|
+
if (value === null || value === "")
|
|
227
|
+
delete current.note;
|
|
228
|
+
else if (typeof value === "string")
|
|
229
|
+
current.note = value.slice(0, 500);
|
|
230
|
+
}
|
|
231
|
+
current.updatedAt = now;
|
|
232
|
+
if (options.updatedBy)
|
|
233
|
+
current.updatedBy = options.updatedBy;
|
|
234
|
+
// An override with nothing left in it is not an override — drop the key so
|
|
235
|
+
// the resource reads as "default" rather than as a customized row that
|
|
236
|
+
// happens to change nothing.
|
|
237
|
+
const meaningful = current.days !== undefined ||
|
|
238
|
+
current.maxItems !== undefined ||
|
|
239
|
+
current.maxBytes !== undefined ||
|
|
240
|
+
current.enabled !== undefined ||
|
|
241
|
+
current.note !== undefined;
|
|
242
|
+
if (meaningful)
|
|
243
|
+
file.overrides[key] = current;
|
|
244
|
+
else
|
|
245
|
+
delete file.overrides[key];
|
|
246
|
+
}
|
|
247
|
+
file.updatedAt = now;
|
|
248
|
+
mkdirSync(home, { recursive: true });
|
|
249
|
+
writeJsonAtomic(overridesPath(home), file);
|
|
250
|
+
return file;
|
|
251
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { EffectiveRetentionPolicy } from "./policy";
|
|
2
|
+
import type { RetentionSweepReport } from "./store";
|
|
3
|
+
import type { RetentionTarget } from "./target";
|
|
4
|
+
/** Default cadence. Diagnostic data is not urgent; four sweeps a day is plenty. */
|
|
5
|
+
export declare const DEFAULT_SWEEP_INTERVAL_MS: number;
|
|
6
|
+
/** How often to notice an overrides edit. */
|
|
7
|
+
export declare const OVERRIDES_PULSE_MS = 60000;
|
|
8
|
+
export interface ResourceOutcome {
|
|
9
|
+
key: string;
|
|
10
|
+
policy: EffectiveRetentionPolicy;
|
|
11
|
+
report: RetentionSweepReport;
|
|
12
|
+
}
|
|
13
|
+
export interface RetentionRunReport {
|
|
14
|
+
at: number;
|
|
15
|
+
resources: ResourceOutcome[];
|
|
16
|
+
/** Totals across every resource, for one honest log line. */
|
|
17
|
+
totals: {
|
|
18
|
+
aged: number;
|
|
19
|
+
overflow: number;
|
|
20
|
+
bytesFreed: number;
|
|
21
|
+
errors: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface RetentionSweeper {
|
|
25
|
+
start(): void;
|
|
26
|
+
stop(): void;
|
|
27
|
+
/** A supervisor's liveness poke — rebuild a timer lost to sleep/hibernate. */
|
|
28
|
+
ensureAlive(): void;
|
|
29
|
+
/** Run one sweep synchronously. Never throws. */
|
|
30
|
+
sweepNow(now?: number): RetentionRunReport;
|
|
31
|
+
/** Measure and republish without deleting anything — what the UI reads. */
|
|
32
|
+
publish(): void;
|
|
33
|
+
/** What each resource WOULD remove right now, under the current policy. */
|
|
34
|
+
preview(now?: number): Array<{
|
|
35
|
+
key: string;
|
|
36
|
+
policy: EffectiveRetentionPolicy;
|
|
37
|
+
aged: number;
|
|
38
|
+
overflow: number;
|
|
39
|
+
}>;
|
|
40
|
+
status(): {
|
|
41
|
+
app: string;
|
|
42
|
+
home: string;
|
|
43
|
+
disabled: boolean;
|
|
44
|
+
lastRun: RetentionRunReport | null;
|
|
45
|
+
nextRunAt: number | null;
|
|
46
|
+
lastError: string | null;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface CreateRetentionSweeperOptions {
|
|
50
|
+
app: string;
|
|
51
|
+
targets: RetentionTarget[];
|
|
52
|
+
intervalMs?: number;
|
|
53
|
+
bootDelayMs?: number;
|
|
54
|
+
/** How often to notice an overrides edit. Injectable so tests need not wait a minute. */
|
|
55
|
+
pulseMs?: number;
|
|
56
|
+
/** `local` / `prod` / `dev` — informational, shown in the UI. */
|
|
57
|
+
instance?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Kill switch. `RETENTION_DISABLED=1` in the app's environment stops every
|
|
60
|
+
* sweep while still publishing measurements, so the UI shows the sizes AND
|
|
61
|
+
* says plainly that nothing is being pruned. A silent kill switch would be
|
|
62
|
+
* indistinguishable from a broken timer.
|
|
63
|
+
*/
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
home?: string;
|
|
66
|
+
log?: (line: string) => void;
|
|
67
|
+
now?: () => number;
|
|
68
|
+
}
|
|
69
|
+
export declare function createRetentionSweeper(options: CreateRetentionSweeperOptions): RetentionSweeper;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sweep RUNTIME — one timer per app that applies the fleet policy to every
|
|
3
|
+
* resource that app declared, publishes what it found, and never takes the app
|
|
4
|
+
* down when it fails.
|
|
5
|
+
*
|
|
6
|
+
* Generalized from `apps/orch/src/server/eventRetention.ts`, which had all of
|
|
7
|
+
* this already for exactly one table: the boot delay, the liveness kick, the
|
|
8
|
+
* never-throw sweep, the status readout. Those were not incidental — each is a
|
|
9
|
+
* failure that had already happened once — so they are kept verbatim in spirit
|
|
10
|
+
* and made to serve every resource in the fleet instead of one.
|
|
11
|
+
*
|
|
12
|
+
* ── Overrides take effect within a minute, not within an interval ───────────
|
|
13
|
+
* A sweep runs every 6 hours by default. If the owner's change in the station UI
|
|
14
|
+
* only applied on that cadence, the control would appear to do nothing for most
|
|
15
|
+
* of a day — the "button that visibly does nothing" failure this fleet has
|
|
16
|
+
* already paid for elsewhere. So a light PULSE timer stats `overrides.json` once
|
|
17
|
+
* a minute and sweeps early when it has changed since the last applied read.
|
|
18
|
+
* One `stat` a minute is free; a UI whose settings look inert is not.
|
|
19
|
+
*/
|
|
20
|
+
import { statSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { assertRetentionDeclaration, resolveRetentionPolicy, retentionKey } from "./policy";
|
|
23
|
+
import { publishRetention, readRetentionOverrides, resolveRetentionHome } from "./store";
|
|
24
|
+
/** Default cadence. Diagnostic data is not urgent; four sweeps a day is plenty. */
|
|
25
|
+
export const DEFAULT_SWEEP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
26
|
+
/** How often to notice an overrides edit. */
|
|
27
|
+
export const OVERRIDES_PULSE_MS = 60_000;
|
|
28
|
+
const formatBytes = (bytes) => {
|
|
29
|
+
if (bytes < 1024)
|
|
30
|
+
return `${bytes} B`;
|
|
31
|
+
const units = ["KB", "MB", "GB"];
|
|
32
|
+
let value = bytes / 1024;
|
|
33
|
+
let unit = 0;
|
|
34
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
35
|
+
value /= 1024;
|
|
36
|
+
unit++;
|
|
37
|
+
}
|
|
38
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
39
|
+
};
|
|
40
|
+
export function createRetentionSweeper(options) {
|
|
41
|
+
const { app, targets } = options;
|
|
42
|
+
const intervalMs = options.intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
|
|
43
|
+
const bootDelayMs = options.bootDelayMs ?? 60_000;
|
|
44
|
+
const home = options.home ?? resolveRetentionHome();
|
|
45
|
+
const log = options.log ?? ((line) => console.log(`[${app}] ${line}`));
|
|
46
|
+
const now = options.now ?? (() => Date.now());
|
|
47
|
+
const disabled = options.disabled ?? process.env.RETENTION_DISABLED === "1";
|
|
48
|
+
// Fail at construction, where the stack names the app — not at the first sweep
|
|
49
|
+
// six hours later, in a daemon nobody is watching. A duplicate key is the
|
|
50
|
+
// nastier of the two: two resources sharing one override would let the owner
|
|
51
|
+
// change a number and silently govern the wrong thing.
|
|
52
|
+
const seenKeys = new Set();
|
|
53
|
+
for (const target of targets) {
|
|
54
|
+
assertRetentionDeclaration(target.declaration);
|
|
55
|
+
const key = retentionKey(target.declaration.app, target.declaration.id);
|
|
56
|
+
if (seenKeys.has(key)) {
|
|
57
|
+
throw new Error(`retention: duplicate resource key "${key}" — ids must be unique within an app`);
|
|
58
|
+
}
|
|
59
|
+
seenKeys.add(key);
|
|
60
|
+
}
|
|
61
|
+
let timer = null;
|
|
62
|
+
let pulse = null;
|
|
63
|
+
let stopped = true;
|
|
64
|
+
let nextRunAt = null;
|
|
65
|
+
let lastRun = null;
|
|
66
|
+
let lastError = null;
|
|
67
|
+
let lastBeatAt = now();
|
|
68
|
+
let appliedOverridesAt = 0;
|
|
69
|
+
const overridesFile = join(home, "overrides.json");
|
|
70
|
+
function policiesNow() {
|
|
71
|
+
const file = readRetentionOverrides(home);
|
|
72
|
+
appliedOverridesAt = Math.max(appliedOverridesAt, file.updatedAt ?? 0);
|
|
73
|
+
const resolved = new Map();
|
|
74
|
+
for (const target of targets) {
|
|
75
|
+
const declaration = target.declaration;
|
|
76
|
+
const policy = resolveRetentionPolicy(declaration, file.overrides[`${declaration.app}:${declaration.id}`]);
|
|
77
|
+
resolved.set(policy.key, policy);
|
|
78
|
+
}
|
|
79
|
+
return resolved;
|
|
80
|
+
}
|
|
81
|
+
/** Measure every resource and write the publication the station UI reads. */
|
|
82
|
+
function publish(lastReports) {
|
|
83
|
+
const resources = [];
|
|
84
|
+
for (const target of targets) {
|
|
85
|
+
const key = `${target.declaration.app}:${target.declaration.id}`;
|
|
86
|
+
let measurement;
|
|
87
|
+
try {
|
|
88
|
+
measurement = target.measure();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
log(`⚠️ retention: could not measure ${key} — ${error instanceof Error ? error.message : String(error)}`);
|
|
92
|
+
}
|
|
93
|
+
const previous = lastRun?.resources.find((r) => r.key === key)?.report;
|
|
94
|
+
const lastSweep = lastReports?.get(key) ?? previous;
|
|
95
|
+
resources.push({
|
|
96
|
+
declaration: target.declaration,
|
|
97
|
+
...(measurement ? { measurement } : {}),
|
|
98
|
+
...(lastSweep ? { lastSweep } : {}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
publishRetention({
|
|
102
|
+
app,
|
|
103
|
+
...(options.instance ? { instance: options.instance } : {}),
|
|
104
|
+
...(disabled ? { sweepDisabled: true } : {}),
|
|
105
|
+
resources,
|
|
106
|
+
}, { home, log });
|
|
107
|
+
}
|
|
108
|
+
function sweepNow(at = now()) {
|
|
109
|
+
lastBeatAt = at;
|
|
110
|
+
const outcomes = [];
|
|
111
|
+
const totals = { aged: 0, overflow: 0, bytesFreed: 0, errors: 0 };
|
|
112
|
+
const reports = new Map();
|
|
113
|
+
let policies;
|
|
114
|
+
try {
|
|
115
|
+
policies = policiesNow();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// A malformed declaration must not wedge the timer — report and skip.
|
|
119
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
120
|
+
log(`⚠️ retention: cannot resolve policies — ${lastError}`);
|
|
121
|
+
const report = { at, resources: [], totals: { ...totals, errors: 1 } };
|
|
122
|
+
lastRun = report;
|
|
123
|
+
return report;
|
|
124
|
+
}
|
|
125
|
+
for (const target of targets) {
|
|
126
|
+
const key = `${target.declaration.app}:${target.declaration.id}`;
|
|
127
|
+
const policy = policies.get(key);
|
|
128
|
+
if (!policy)
|
|
129
|
+
continue;
|
|
130
|
+
let report;
|
|
131
|
+
if (disabled) {
|
|
132
|
+
report = { at, aged: 0, overflow: 0 };
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
try {
|
|
136
|
+
report = target.sweep(policy, at);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
// A busy/locked database is a transient the next sweep clears; one
|
|
140
|
+
// broken resource must never stop the others.
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
report = { at, aged: 0, overflow: 0, error: message };
|
|
143
|
+
totals.errors++;
|
|
144
|
+
log(`⚠️ retention: ${key} sweep failed — ${message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
totals.aged += report.aged;
|
|
148
|
+
totals.overflow += report.overflow;
|
|
149
|
+
totals.bytesFreed += report.bytesFreed ?? 0;
|
|
150
|
+
reports.set(key, report);
|
|
151
|
+
outcomes.push({ key, policy, report });
|
|
152
|
+
// An archiving resource that refuses to trim looks EXACTLY like one with
|
|
153
|
+
// nothing to do — same zeros, same silence — so the refusal has to be the
|
|
154
|
+
// noisy case. It means the archive's collector has stopped, which is a
|
|
155
|
+
// disk problem on its way to becoming a data problem.
|
|
156
|
+
if (report.blocked) {
|
|
157
|
+
log(`⚠️ retention: ${key} did NOT trim — ${report.blocked}`);
|
|
158
|
+
}
|
|
159
|
+
if (report.overflow > 0) {
|
|
160
|
+
log(`⚠️ retention: ${key} OVERFLOW — ${report.overflow} oldest item(s) dropped to hold it at ` +
|
|
161
|
+
`${policy.maxItems}. These went regardless of the age rule: something is writing far ` +
|
|
162
|
+
"faster than expected. A bound that declines to bind is not a bound.");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const report = { at, resources: outcomes, totals };
|
|
166
|
+
lastRun = report;
|
|
167
|
+
if (totals.errors === 0)
|
|
168
|
+
lastError = null;
|
|
169
|
+
// Silence is the healthy steady state — log only a sweep that changed something.
|
|
170
|
+
if (totals.aged > 0 || totals.overflow > 0) {
|
|
171
|
+
// "pruned" and "archived" are not the same event and must not read as one:
|
|
172
|
+
// the first is data the owner agreed to lose, the second is data that moved.
|
|
173
|
+
const archived = outcomes.reduce((sum, o) => sum + (o.report.archived ?? 0), 0);
|
|
174
|
+
log(`retention: pruned ${totals.aged + totals.overflow - archived} item(s)` +
|
|
175
|
+
(archived > 0 ? `, archived ${archived}` : "") +
|
|
176
|
+
(totals.bytesFreed > 0 ? `, ${formatBytes(totals.bytesFreed)} reclaimed` : "") +
|
|
177
|
+
` across ${outcomes.filter((o) => o.report.aged + o.report.overflow > 0).length} resource(s)`);
|
|
178
|
+
}
|
|
179
|
+
publish(reports);
|
|
180
|
+
return report;
|
|
181
|
+
}
|
|
182
|
+
function schedule(delayMs = intervalMs) {
|
|
183
|
+
if (stopped)
|
|
184
|
+
return;
|
|
185
|
+
if (timer)
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
nextRunAt = now() + delayMs;
|
|
188
|
+
timer = setTimeout(() => {
|
|
189
|
+
timer = null;
|
|
190
|
+
sweepNow();
|
|
191
|
+
schedule();
|
|
192
|
+
}, delayMs);
|
|
193
|
+
// Unref'd for the same reason the pulse is, and it was the one that mattered:
|
|
194
|
+
// a 6-hour `setTimeout` is a 6-hour reason for a process not to exit. Every
|
|
195
|
+
// caller that owns a drain calls `stop()`, but `apps/vault` deliberately has
|
|
196
|
+
// no signal handler (launchd restarts it), and without this a SIGTERM there
|
|
197
|
+
// would hang until the timer fired or the supervisor lost patience and
|
|
198
|
+
// SIGKILLed — which is exactly how a WAL gets left half-closed. Housekeeping
|
|
199
|
+
// must never be the last thing holding a process open.
|
|
200
|
+
timer.unref?.();
|
|
201
|
+
}
|
|
202
|
+
/** Notice an overrides edit made by the station and act on it promptly. */
|
|
203
|
+
function checkOverrides() {
|
|
204
|
+
if (stopped)
|
|
205
|
+
return;
|
|
206
|
+
try {
|
|
207
|
+
const mtime = statSync(overridesFile).mtimeMs;
|
|
208
|
+
if (mtime > appliedOverridesAt) {
|
|
209
|
+
appliedOverridesAt = mtime;
|
|
210
|
+
log("retention: policy changed — sweeping now");
|
|
211
|
+
sweepNow();
|
|
212
|
+
schedule();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// No overrides file yet is the normal case on a fresh machine.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
start() {
|
|
221
|
+
if (!stopped)
|
|
222
|
+
return;
|
|
223
|
+
stopped = false;
|
|
224
|
+
lastBeatAt = now();
|
|
225
|
+
// Publish immediately so the UI has the fleet's sizes without waiting for
|
|
226
|
+
// the first sweep — a dashboard that is blank for a minute after boot
|
|
227
|
+
// reads as broken.
|
|
228
|
+
try {
|
|
229
|
+
publish();
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
log(`⚠️ retention: initial publish failed — ${error instanceof Error ? error.message : String(error)}`);
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
appliedOverridesAt = statSync(overridesFile).mtimeMs;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
appliedOverridesAt = 0;
|
|
239
|
+
}
|
|
240
|
+
schedule(Math.min(bootDelayMs, intervalMs));
|
|
241
|
+
pulse = setInterval(checkOverrides, options.pulseMs ?? OVERRIDES_PULSE_MS);
|
|
242
|
+
// Never hold the process open for housekeeping.
|
|
243
|
+
pulse.unref?.();
|
|
244
|
+
},
|
|
245
|
+
stop() {
|
|
246
|
+
stopped = true;
|
|
247
|
+
if (timer)
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
if (pulse)
|
|
250
|
+
clearInterval(pulse);
|
|
251
|
+
timer = null;
|
|
252
|
+
pulse = null;
|
|
253
|
+
nextRunAt = null;
|
|
254
|
+
},
|
|
255
|
+
ensureAlive() {
|
|
256
|
+
if (stopped)
|
|
257
|
+
return;
|
|
258
|
+
// ~3 missed intervals, floored so a fast test cadence is not declared
|
|
259
|
+
// wedged the moment it is a beat late — the watchdog's shape.
|
|
260
|
+
const staleAfterMs = Math.max(intervalMs * 3, 90_000);
|
|
261
|
+
if (now() - lastBeatAt <= staleAfterMs)
|
|
262
|
+
return;
|
|
263
|
+
log("⚠️ retention: LIVENESS KICK — sweep timer silent (system sleep/hibernate?) — sweeping now");
|
|
264
|
+
sweepNow();
|
|
265
|
+
schedule();
|
|
266
|
+
},
|
|
267
|
+
sweepNow,
|
|
268
|
+
publish: () => publish(),
|
|
269
|
+
preview(at = now()) {
|
|
270
|
+
const policies = policiesNow();
|
|
271
|
+
const rows = [];
|
|
272
|
+
for (const target of targets) {
|
|
273
|
+
const key = `${target.declaration.app}:${target.declaration.id}`;
|
|
274
|
+
const policy = policies.get(key);
|
|
275
|
+
if (!policy)
|
|
276
|
+
continue;
|
|
277
|
+
try {
|
|
278
|
+
const { aged, overflow } = target.preview(policy, at);
|
|
279
|
+
rows.push({ key, policy, aged, overflow });
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
rows.push({ key, policy, aged: 0, overflow: 0 });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return rows;
|
|
286
|
+
},
|
|
287
|
+
status: () => ({ app, home, disabled, lastRun, nextRunAt, lastError }),
|
|
288
|
+
};
|
|
289
|
+
}
|