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,236 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { resolveAppDataHome } from "../paths/appDataHome";
|
|
6
|
+
import type { RetentionDeclaration } from "./policy";
|
|
7
|
+
import {
|
|
8
|
+
publishRetention,
|
|
9
|
+
readRetentionOverrides,
|
|
10
|
+
readRetentionRegistry,
|
|
11
|
+
resolveRetentionHome,
|
|
12
|
+
setRetentionHomeForTests,
|
|
13
|
+
writeRetentionOverride,
|
|
14
|
+
} from "./store";
|
|
15
|
+
|
|
16
|
+
const decl = (app: string, id: string): RetentionDeclaration => ({
|
|
17
|
+
app,
|
|
18
|
+
id,
|
|
19
|
+
label: id,
|
|
20
|
+
category: "run-log",
|
|
21
|
+
unit: "rows",
|
|
22
|
+
location: `${app}.sqlite · ${id}`,
|
|
23
|
+
consequence: "past run outcomes",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let home: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
home = mkdtempSync(join(tmpdir(), "retention-home-"));
|
|
30
|
+
setRetentionHomeForTests(home);
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
setRetentionHomeForTests(null);
|
|
34
|
+
rmSync(home, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("publications — one file per app", () => {
|
|
38
|
+
test("an app publishes and can be read back", () => {
|
|
39
|
+
expect(
|
|
40
|
+
publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "events") }] }),
|
|
41
|
+
).toBe(true);
|
|
42
|
+
const registry = readRetentionRegistry(home);
|
|
43
|
+
expect(registry).toHaveLength(1);
|
|
44
|
+
expect(registry[0]?.app).toBe("orch");
|
|
45
|
+
expect(registry[0]?.resources[0]?.declaration.id).toBe("events");
|
|
46
|
+
expect(registry[0]?.publishedAt).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("two apps publishing concurrently cannot lose each other's records", () => {
|
|
50
|
+
// The reason this is one file per app: a shared registry that concurrent
|
|
51
|
+
// writers read-modify-write loses records, which is the exact bug the agent
|
|
52
|
+
// coordination board is one-file-per-record to avoid.
|
|
53
|
+
publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "events") }] });
|
|
54
|
+
publishRetention({ app: "notes", resources: [{ declaration: decl("notes", "ingest-runs") }] });
|
|
55
|
+
expect(
|
|
56
|
+
readRetentionRegistry(home)
|
|
57
|
+
.map((p) => p.app)
|
|
58
|
+
.sort(),
|
|
59
|
+
).toEqual(["notes", "orch"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("republishing replaces only that app's file", () => {
|
|
63
|
+
publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "events") }] });
|
|
64
|
+
publishRetention({ app: "notes", resources: [{ declaration: decl("notes", "ingest-runs") }] });
|
|
65
|
+
publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "job-runs") }] });
|
|
66
|
+
const orch = readRetentionRegistry(home).find((p) => p.app === "orch");
|
|
67
|
+
expect(orch?.resources.map((r) => r.declaration.id)).toEqual(["job-runs"]);
|
|
68
|
+
expect(readRetentionRegistry(home)).toHaveLength(2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("an app key that is not a safe file name is refused", () => {
|
|
72
|
+
expect(publishRetention({ app: "../escape", resources: [] })).toBe(false);
|
|
73
|
+
expect(readRetentionRegistry(home)).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a corrupt publication is skipped, never fatal to the whole view", () => {
|
|
77
|
+
publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "events") }] });
|
|
78
|
+
writeFileSync(join(home, "apps", "broken.json"), "{ this is not json");
|
|
79
|
+
expect(readRetentionRegistry(home).map((p) => p.app)).toEqual(["orch"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("publications carry the last sweep so the UI can show it while the app is down", () => {
|
|
83
|
+
publishRetention({
|
|
84
|
+
app: "orch",
|
|
85
|
+
instance: "local",
|
|
86
|
+
resources: [
|
|
87
|
+
{
|
|
88
|
+
declaration: decl("orch", "events"),
|
|
89
|
+
measurement: { items: 3000, bytes: 4096 },
|
|
90
|
+
lastSweep: { at: 123, aged: 12, overflow: 0 },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
const row = readRetentionRegistry(home)[0]?.resources[0];
|
|
95
|
+
expect(row?.measurement).toEqual({ items: 3000, bytes: 4096 });
|
|
96
|
+
expect(row?.lastSweep).toEqual({ at: 123, aged: 12, overflow: 0 });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("overrides — the single-writer file", () => {
|
|
101
|
+
test("a write round-trips and stamps who and when", () => {
|
|
102
|
+
writeRetentionOverride("orch", "events", { days: 30 }, { home, updatedBy: "curt", now: 5 });
|
|
103
|
+
const file = readRetentionOverrides(home);
|
|
104
|
+
expect(file.overrides["orch:events"]).toMatchObject({
|
|
105
|
+
days: 30,
|
|
106
|
+
updatedBy: "curt",
|
|
107
|
+
updatedAt: 5,
|
|
108
|
+
});
|
|
109
|
+
expect(file.updatedAt).toBe(5);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("editing one resource never loses another's setting", () => {
|
|
113
|
+
writeRetentionOverride("orch", "events", { days: 30 }, { home });
|
|
114
|
+
writeRetentionOverride("notes", "ingest-runs", { days: 3 }, { home });
|
|
115
|
+
const file = readRetentionOverrides(home);
|
|
116
|
+
expect(file.overrides["orch:events"]?.days).toBe(30);
|
|
117
|
+
expect(file.overrides["notes:ingest-runs"]?.days).toBe(3);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("null CLEARS a field so the declaration's default becomes authoritative again", () => {
|
|
121
|
+
// Storing a value that merely equals today's default would freeze the
|
|
122
|
+
// resource at that number if the default ever moved.
|
|
123
|
+
writeRetentionOverride("orch", "events", { days: 30, maxItems: 500 }, { home });
|
|
124
|
+
writeRetentionOverride("orch", "events", { days: null }, { home });
|
|
125
|
+
expect(readRetentionOverrides(home).overrides["orch:events"]).toMatchObject({ maxItems: 500 });
|
|
126
|
+
expect(readRetentionOverrides(home).overrides["orch:events"]?.days).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a null patch removes the whole override — the UI's Reset", () => {
|
|
130
|
+
writeRetentionOverride("orch", "events", { days: 30 }, { home });
|
|
131
|
+
writeRetentionOverride("orch", "events", null, { home });
|
|
132
|
+
expect(readRetentionOverrides(home).overrides["orch:events"]).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("an override left with nothing meaningful in it is dropped, not stored empty", () => {
|
|
136
|
+
writeRetentionOverride("orch", "events", { days: 30 }, { home });
|
|
137
|
+
writeRetentionOverride("orch", "events", { days: null }, { home });
|
|
138
|
+
expect(readRetentionOverrides(home).overrides["orch:events"]).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("garbage values are ignored rather than written through to a sweep", () => {
|
|
142
|
+
writeRetentionOverride("orch", "events", { days: "thirty", maxItems: Number.NaN }, { home });
|
|
143
|
+
expect(readRetentionOverrides(home).overrides["orch:events"]).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("enabled:false is storable and distinct from an absent value", () => {
|
|
147
|
+
writeRetentionOverride("orch", "events", { enabled: false }, { home });
|
|
148
|
+
expect(readRetentionOverrides(home).overrides["orch:events"]?.enabled).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a corrupt overrides file reads as no overrides — never a boot failure", () => {
|
|
152
|
+
writeFileSync(join(home, "overrides.json"), "}{");
|
|
153
|
+
expect(readRetentionOverrides(home).overrides).toEqual({});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("the file is written whole and valid — a reader never sees half of it", () => {
|
|
157
|
+
writeRetentionOverride("orch", "events", { days: 30, note: "keeping a month" }, { home });
|
|
158
|
+
const raw = readFileSync(join(home, "overrides.json"), "utf8");
|
|
159
|
+
expect(() => JSON.parse(raw)).not.toThrow();
|
|
160
|
+
expect(raw.endsWith("\n")).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("resolveRetentionHome", () => {
|
|
165
|
+
afterEach(() => setRetentionHomeForTests(home));
|
|
166
|
+
|
|
167
|
+
test("RETENTION_HOME wins outright", () => {
|
|
168
|
+
setRetentionHomeForTests(null);
|
|
169
|
+
const explicit = join(home, "explicit");
|
|
170
|
+
expect(resolveRetentionHome({ RETENTION_HOME: explicit })).toBe(explicit);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("APP_DATA_HOME — the one env var — is honored before any convention", () => {
|
|
174
|
+
setRetentionHomeForTests(null);
|
|
175
|
+
expect(resolveRetentionHome({ APP_DATA_HOME: home })).toBe(join(home, "retention"));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a configured home is used even before it exists — naming it is the statement", () => {
|
|
179
|
+
setRetentionHomeForTests(null);
|
|
180
|
+
const fresh = join(home, "not-yet");
|
|
181
|
+
expect(resolveRetentionHome({ APP_DATA_HOME: fresh, APP_DATA_DIR: join(home, "data") })).toBe(
|
|
182
|
+
join(fresh, "retention"),
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("the shared OS-convention home wins over APP_DATA_DIR so the station sees ONE fleet", () => {
|
|
187
|
+
// An app's own APP_DATA_DIR is private to it; publishing there instead of the
|
|
188
|
+
// shared home would hide it from every other app's view of the fleet.
|
|
189
|
+
setRetentionHomeForTests(null);
|
|
190
|
+
const conventional = resolveAppDataHome({ env: { HOME: home } });
|
|
191
|
+
mkdirSync(conventional, { recursive: true });
|
|
192
|
+
expect(resolveRetentionHome({ HOME: home, APP_DATA_DIR: join(home, "data") })).toBe(
|
|
193
|
+
join(conventional, "retention"),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a deployed box — no shared data home — falls through to APP_DATA_DIR", () => {
|
|
198
|
+
// prod-ec2 has no shared data home, and under ProtectSystem=strict only the
|
|
199
|
+
// data dir is writable. The app still sweeps; its fleet is that box's.
|
|
200
|
+
setRetentionHomeForTests(null);
|
|
201
|
+
const dataDir = join(home, "data");
|
|
202
|
+
expect(
|
|
203
|
+
resolveRetentionHome({ HOME: join(home, "nonexistent-home"), APP_DATA_DIR: dataDir }),
|
|
204
|
+
).toBe(join(dataDir, "retention"));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("an unwritable candidate is probed and skipped, never assumed", () => {
|
|
208
|
+
setRetentionHomeForTests(null);
|
|
209
|
+
const dataDir = join(home, "data2");
|
|
210
|
+
expect(
|
|
211
|
+
resolveRetentionHome({
|
|
212
|
+
APP_DATA_HOME: "/proc/definitely-not-writable",
|
|
213
|
+
HOME: join(home, "nonexistent-home"),
|
|
214
|
+
APP_DATA_DIR: dataDir,
|
|
215
|
+
}),
|
|
216
|
+
).toBe(join(dataDir, "retention"));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 🔴 The published defect, asserted on the OUTPUT rather than on the source: up
|
|
221
|
+
* to 2026-09-13 this resolved a hardcoded dotfolder in the owner's home, so
|
|
222
|
+
* every installer of `cursedbelt` got a library reaching into a directory that
|
|
223
|
+
* had nothing to do with them.
|
|
224
|
+
*/
|
|
225
|
+
test("resolves nothing from a previous generation, whatever the env says", () => {
|
|
226
|
+
for (const env of [
|
|
227
|
+
{ HOME: join(home, "nonexistent-home") },
|
|
228
|
+
{ HOME: join(home, "nonexistent-home"), APP_MANAGED_HOME: "/nowhere/inherited" },
|
|
229
|
+
]) {
|
|
230
|
+
setRetentionHomeForTests(null);
|
|
231
|
+
const resolved = resolveRetentionHome(env);
|
|
232
|
+
expect(resolved).not.toContain("app-managed");
|
|
233
|
+
expect(resolved).not.toContain("/nowhere/inherited");
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,338 @@
|
|
|
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 {
|
|
48
|
+
existsSync,
|
|
49
|
+
mkdirSync,
|
|
50
|
+
readdirSync,
|
|
51
|
+
readFileSync,
|
|
52
|
+
renameSync,
|
|
53
|
+
rmSync,
|
|
54
|
+
writeFileSync,
|
|
55
|
+
} from "node:fs";
|
|
56
|
+
import { join } from "node:path";
|
|
57
|
+
import { APP_DATA_HOME_ENV, resolveAppDataHome } from "../paths/appDataHome";
|
|
58
|
+
import type { RetentionDeclaration, RetentionOverride } from "./policy";
|
|
59
|
+
import { retentionKey } from "./policy";
|
|
60
|
+
|
|
61
|
+
/** What one sweep of one resource did. Published so the UI can show it. */
|
|
62
|
+
export interface RetentionSweepReport {
|
|
63
|
+
at: number;
|
|
64
|
+
/** Items removed because they aged past the cutoff. */
|
|
65
|
+
aged: number;
|
|
66
|
+
/** Items removed by the item-count backstop, liveness ignored. */
|
|
67
|
+
overflow: number;
|
|
68
|
+
/**
|
|
69
|
+
* Items the sweep deliberately left alone because they are still live — a
|
|
70
|
+
* running worker's transcript, an unsettled task's trail. Reported so a
|
|
71
|
+
* resource that stays large has an explanation rather than looking like a
|
|
72
|
+
* broken sweep.
|
|
73
|
+
*/
|
|
74
|
+
protectedItems?: number;
|
|
75
|
+
bytesFreed?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Items written to the archive before they were deleted — the `archived-history`
|
|
78
|
+
* category's receipt. Reported so the station can show "moved" rather than
|
|
79
|
+
* "removed": for an archiving resource those two words are the whole difference,
|
|
80
|
+
* and a UI that said the second would be describing data loss that did not happen.
|
|
81
|
+
*/
|
|
82
|
+
archived?: number;
|
|
83
|
+
/** Bytes those archived items occupy in the archive. */
|
|
84
|
+
archivedBytes?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Why this sweep deliberately removed nothing, when it otherwise would have.
|
|
87
|
+
* An archiving resource whose archive is not being collected REFUSES to trim,
|
|
88
|
+
* and a resource that quietly stops working is indistinguishable from one with
|
|
89
|
+
* nothing to do — so the reason is carried out to the UI rather than logged and
|
|
90
|
+
* lost.
|
|
91
|
+
*/
|
|
92
|
+
blocked?: string;
|
|
93
|
+
error?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A live measurement of a resource, taken at sweep time. */
|
|
97
|
+
export interface RetentionMeasurement {
|
|
98
|
+
items: number;
|
|
99
|
+
bytes?: number;
|
|
100
|
+
/** Timestamp of the oldest item, when the target can report it cheaply. */
|
|
101
|
+
oldestAt?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One resource as published by its owning app. */
|
|
105
|
+
export interface PublishedResource {
|
|
106
|
+
declaration: RetentionDeclaration;
|
|
107
|
+
measurement?: RetentionMeasurement;
|
|
108
|
+
lastSweep?: RetentionSweepReport;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One app's publication file. */
|
|
112
|
+
export interface RetentionPublication {
|
|
113
|
+
app: string;
|
|
114
|
+
version: 1;
|
|
115
|
+
publishedAt: number;
|
|
116
|
+
/** The instance that published — `local`, `prod`, `dev`; purely informational. */
|
|
117
|
+
instance?: string;
|
|
118
|
+
/** True when the process that published it has sweeping disabled entirely. */
|
|
119
|
+
sweepDisabled?: boolean;
|
|
120
|
+
resources: PublishedResource[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface RetentionOverridesFile {
|
|
124
|
+
version: 1;
|
|
125
|
+
updatedAt: number;
|
|
126
|
+
/** Keyed by `<app>:<id>` — `retentionKey()`. */
|
|
127
|
+
overrides: Record<string, RetentionOverride>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A FUNCTION, not a shared constant. `writeRetentionOverride` mutates what
|
|
132
|
+
* `readRetentionOverrides` hands it, so a module-level empty object would be
|
|
133
|
+
* aliased into every caller: the first write on a machine with no overrides file
|
|
134
|
+
* would edit the constant itself, and a later read of an unrelated home would
|
|
135
|
+
* return those overrides. Caught by `store.test.ts`.
|
|
136
|
+
*/
|
|
137
|
+
const emptyOverrides = (): RetentionOverridesFile => ({ version: 1, updatedAt: 0, overrides: {} });
|
|
138
|
+
|
|
139
|
+
let cachedHome: string | null = null;
|
|
140
|
+
|
|
141
|
+
/** Can we create and write inside this directory? Probed, never assumed. */
|
|
142
|
+
function isUsable(dir: string): boolean {
|
|
143
|
+
try {
|
|
144
|
+
mkdirSync(dir, { recursive: true });
|
|
145
|
+
const probe = join(dir, ".write-probe");
|
|
146
|
+
writeFileSync(probe, "");
|
|
147
|
+
rmSync(probe, { force: true });
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the retention home. See the module note for the ordering rationale.
|
|
156
|
+
* Cached per process — it is consulted on every sweep and cannot change under a
|
|
157
|
+
* running app.
|
|
158
|
+
*/
|
|
159
|
+
export function resolveRetentionHome(
|
|
160
|
+
env: Record<string, string | undefined> = process.env,
|
|
161
|
+
): string {
|
|
162
|
+
if (cachedHome) return cachedHome;
|
|
163
|
+
const candidates: string[] = [];
|
|
164
|
+
if (env.RETENTION_HOME) candidates.push(env.RETENTION_HOME);
|
|
165
|
+
// An explicitly CONFIGURED home is used whether or not it exists yet — the
|
|
166
|
+
// operator naming it is the statement that it is the right place.
|
|
167
|
+
const configured = env[APP_DATA_HOME_ENV]?.trim();
|
|
168
|
+
if (configured) candidates.push(join(configured, "retention"));
|
|
169
|
+
const conventional = resolveAppDataHome({ env });
|
|
170
|
+
// The OS convention is only PREFERRED when it is already there: on a deployed
|
|
171
|
+
// box creating it would either fail (read-only namespace) or strand a
|
|
172
|
+
// publication somewhere no station instance looks.
|
|
173
|
+
if (!configured && existsSync(conventional)) candidates.push(join(conventional, "retention"));
|
|
174
|
+
if (env.APP_DATA_DIR) candidates.push(join(env.APP_DATA_DIR, "retention"));
|
|
175
|
+
candidates.push(join(conventional, "retention"));
|
|
176
|
+
|
|
177
|
+
for (const candidate of candidates) {
|
|
178
|
+
if (isUsable(candidate)) {
|
|
179
|
+
cachedHome = candidate;
|
|
180
|
+
return candidate;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Nothing is writable — return the conventional path anyway. Every writer here
|
|
184
|
+
// is best-effort and will log its failure; retention must not stop an app from
|
|
185
|
+
// booting.
|
|
186
|
+
cachedHome = join(conventional, "retention");
|
|
187
|
+
return cachedHome;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Test seam — point the home somewhere else for this process. */
|
|
191
|
+
export function setRetentionHomeForTests(home: string | null): void {
|
|
192
|
+
cachedHome = home;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const appsDir = (home: string): string => join(home, "apps");
|
|
196
|
+
const overridesPath = (home: string): string => join(home, "overrides.json");
|
|
197
|
+
|
|
198
|
+
/** A file name cannot be derived from an app key without pinning the charset. */
|
|
199
|
+
const SAFE_APP = /^[a-z0-9][a-z0-9-]*$/;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Write whole, atomically: a reader must never see half a file. `rename` inside
|
|
203
|
+
* one directory is atomic on every filesystem this fleet runs on, and the temp
|
|
204
|
+
* name carries the pid so two writers cannot share a scratch path.
|
|
205
|
+
*/
|
|
206
|
+
function writeJsonAtomic(path: string, value: unknown): void {
|
|
207
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
208
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
209
|
+
renameSync(tmp, path);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readJson<T>(path: string, fallback: T): T {
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
215
|
+
} catch {
|
|
216
|
+
// Absent is the normal first-run case; corrupt is a truncated write from a
|
|
217
|
+
// killed process. Both mean "no settings yet" — never a boot failure.
|
|
218
|
+
return fallback;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Publish one app's resources. Best-effort by contract: a retention publication
|
|
224
|
+
* that cannot be written is a missing row in a dashboard, and must never take
|
|
225
|
+
* down the app that owns the data.
|
|
226
|
+
*/
|
|
227
|
+
export function publishRetention(
|
|
228
|
+
publication: Omit<RetentionPublication, "version" | "publishedAt">,
|
|
229
|
+
options: { home?: string; log?: (line: string) => void } = {},
|
|
230
|
+
): boolean {
|
|
231
|
+
const home = options.home ?? resolveRetentionHome();
|
|
232
|
+
if (!SAFE_APP.test(publication.app)) return false;
|
|
233
|
+
try {
|
|
234
|
+
mkdirSync(appsDir(home), { recursive: true });
|
|
235
|
+
writeJsonAtomic(join(appsDir(home), `${publication.app}.json`), {
|
|
236
|
+
version: 1,
|
|
237
|
+
publishedAt: Date.now(),
|
|
238
|
+
...publication,
|
|
239
|
+
} satisfies RetentionPublication);
|
|
240
|
+
return true;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
options.log?.(
|
|
243
|
+
`⚠️ retention: could not publish ${publication.app} — ${error instanceof Error ? error.message : String(error)}`,
|
|
244
|
+
);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Every app's publication, newest-published first. */
|
|
250
|
+
export function readRetentionRegistry(home = resolveRetentionHome()): RetentionPublication[] {
|
|
251
|
+
const dir = appsDir(home);
|
|
252
|
+
let names: string[];
|
|
253
|
+
try {
|
|
254
|
+
names = readdirSync(dir).filter((n) => n.endsWith(".json"));
|
|
255
|
+
} catch {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const publications: RetentionPublication[] = [];
|
|
259
|
+
for (const name of names) {
|
|
260
|
+
const parsed = readJson<RetentionPublication | null>(join(dir, name), null);
|
|
261
|
+
if (parsed && Array.isArray(parsed.resources) && typeof parsed.app === "string") {
|
|
262
|
+
publications.push(parsed);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return publications.sort((a, b) => (b.publishedAt ?? 0) - (a.publishedAt ?? 0));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function readRetentionOverrides(home = resolveRetentionHome()): RetentionOverridesFile {
|
|
269
|
+
const file = readJson<RetentionOverridesFile>(overridesPath(home), emptyOverrides());
|
|
270
|
+
if (!file || typeof file !== "object" || typeof file.overrides !== "object" || !file.overrides) {
|
|
271
|
+
return emptyOverrides();
|
|
272
|
+
}
|
|
273
|
+
return { version: 1, updatedAt: file.updatedAt ?? 0, overrides: file.overrides };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Apply one override. Read-modify-write, so an unrelated resource's setting is
|
|
278
|
+
* never lost to a concurrent edit of this one; the station is the only writer,
|
|
279
|
+
* which is what keeps that honest.
|
|
280
|
+
*
|
|
281
|
+
* A patch field set to `undefined` is left alone; set to `null` it is CLEARED,
|
|
282
|
+
* which is how the UI's "reset to default" works — deleting the key is the only
|
|
283
|
+
* way to make the declaration's default authoritative again, since a stored
|
|
284
|
+
* value that merely equals the default would freeze the resource at today's
|
|
285
|
+
* number if the default ever moves.
|
|
286
|
+
*/
|
|
287
|
+
export function writeRetentionOverride(
|
|
288
|
+
app: string,
|
|
289
|
+
id: string,
|
|
290
|
+
patch: Partial<Record<"days" | "maxItems" | "maxBytes" | "enabled" | "note", unknown>> | null,
|
|
291
|
+
options: { home?: string; updatedBy?: string; now?: number } = {},
|
|
292
|
+
): RetentionOverridesFile {
|
|
293
|
+
const home = options.home ?? resolveRetentionHome();
|
|
294
|
+
const now = options.now ?? Date.now();
|
|
295
|
+
const file = readRetentionOverrides(home);
|
|
296
|
+
const key = retentionKey(app, id);
|
|
297
|
+
|
|
298
|
+
if (patch === null) {
|
|
299
|
+
delete file.overrides[key];
|
|
300
|
+
} else {
|
|
301
|
+
const current: RetentionOverride = { ...(file.overrides[key] ?? {}) };
|
|
302
|
+
for (const field of ["days", "maxItems", "maxBytes"] as const) {
|
|
303
|
+
if (field in patch) {
|
|
304
|
+
const value = patch[field];
|
|
305
|
+
if (value === null) delete current[field];
|
|
306
|
+
else if (typeof value === "number" && Number.isFinite(value)) current[field] = value;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if ("enabled" in patch) {
|
|
310
|
+
const value = patch.enabled;
|
|
311
|
+
if (value === null) delete current.enabled;
|
|
312
|
+
else if (typeof value === "boolean") current.enabled = value;
|
|
313
|
+
}
|
|
314
|
+
if ("note" in patch) {
|
|
315
|
+
const value = patch.note;
|
|
316
|
+
if (value === null || value === "") delete current.note;
|
|
317
|
+
else if (typeof value === "string") current.note = value.slice(0, 500);
|
|
318
|
+
}
|
|
319
|
+
current.updatedAt = now;
|
|
320
|
+
if (options.updatedBy) current.updatedBy = options.updatedBy;
|
|
321
|
+
// An override with nothing left in it is not an override — drop the key so
|
|
322
|
+
// the resource reads as "default" rather than as a customized row that
|
|
323
|
+
// happens to change nothing.
|
|
324
|
+
const meaningful =
|
|
325
|
+
current.days !== undefined ||
|
|
326
|
+
current.maxItems !== undefined ||
|
|
327
|
+
current.maxBytes !== undefined ||
|
|
328
|
+
current.enabled !== undefined ||
|
|
329
|
+
current.note !== undefined;
|
|
330
|
+
if (meaningful) file.overrides[key] = current;
|
|
331
|
+
else delete file.overrides[key];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
file.updatedAt = now;
|
|
335
|
+
mkdirSync(home, { recursive: true });
|
|
336
|
+
writeJsonAtomic(overridesPath(home), file);
|
|
337
|
+
return file;
|
|
338
|
+
}
|