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,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-server/notifications` — the SERVER half of the fleet's one
|
|
3
|
+
* notification primitive.
|
|
4
|
+
*
|
|
5
|
+
* ── What was already here, and what was missing ─────────────────────────────
|
|
6
|
+
* The dispatch engine has lived in `cwip/notifications` for months (a
|
|
7
|
+
* persistent per-user row, `groupKey` coalescing, unread counts, deep links, a
|
|
8
|
+
* live-delivery sink) and the whole client has lived in
|
|
9
|
+
* `cursedbelt/react/notifications` (store, bell, feed, inbox, poll transport).
|
|
10
|
+
* What did NOT exist was a way to MOUNT any of it: the only mounted surface in
|
|
11
|
+
* the fleet was `orch-ui/services/notifications`, written against
|
|
12
|
+
* `@satellites/satellite-rpc` inside a package only the orchestrator consumes.
|
|
13
|
+
* So a satellite wanting a bell had two options, and both were wrong — adopt
|
|
14
|
+
* orch's rpc layer, or write the routes again.
|
|
15
|
+
*
|
|
16
|
+
* This module is the third: ordinary Hono routes, an SSE hub behind the sink
|
|
17
|
+
* the center already had, and a migration for apps using cursedbelt's runner.
|
|
18
|
+
*
|
|
19
|
+
* const notifications = createNotificationService({ db });
|
|
20
|
+
* notifications.mount(app, { authorize: (c) => userOf(c)?.id ?? null });
|
|
21
|
+
*
|
|
22
|
+
* and on the client:
|
|
23
|
+
*
|
|
24
|
+
* const { store } = createNotificationsBinding(); // cursedbelt/react/notifications
|
|
25
|
+
* <NotificationBell store={store} />
|
|
26
|
+
*
|
|
27
|
+
* ── The three properties worth knowing ──────────────────────────────────────
|
|
28
|
+
* 1. **A notification is a row before it is an event.** Delivery is a cache of
|
|
29
|
+
* who is listening; the poll transport is the backstop and is not optional.
|
|
30
|
+
* Nothing is lost by a closed laptop.
|
|
31
|
+
* 2. **Coalescing is a database constraint, not a debounce.** The partial
|
|
32
|
+
* unique index on `(user_id, group_key) WHERE group_key IS NOT NULL AND
|
|
33
|
+
* done = 0` is what makes "twenty of the same thing" one row with a count,
|
|
34
|
+
* even under concurrent producers.
|
|
35
|
+
* 3. **The routes address the session's user and nothing else.** There is no
|
|
36
|
+
* route that notifies somebody else; producing is server-side code calling
|
|
37
|
+
* `center.notify*`.
|
|
38
|
+
*/
|
|
39
|
+
export { createNotificationHub } from "./hub";
|
|
40
|
+
export type { NotificationHub, NotificationHubEvent, NotificationHubListener, NotificationHubOptions, } from "./hub";
|
|
41
|
+
export { NOTIFICATION_MIGRATIONS, NOTIFICATIONS_TABLE } from "./migrations";
|
|
42
|
+
export { mountNotificationRoutes } from "./routes";
|
|
43
|
+
export type { NotificationRouteCenter, NotificationRoutesOptions } from "./routes";
|
|
44
|
+
export { createNotificationService } from "./service";
|
|
45
|
+
export type { CreateNotificationServiceOptions, NotificationService, NotificationServiceMountOptions, } from "./service";
|
|
46
|
+
export { createNotificationCenter, createMemoryNotificationStore, createSqliteNotificationStore, migrateNotificationsSchema, } from "cwip/notifications";
|
|
47
|
+
export type { AppNotification, ListNotificationsQuery, NotificationAudience, NotificationCenter, NotificationCenterOptions, NotificationDraft, NotificationFilter, NotificationPage, NotificationSeverity, NotificationSink, NotificationStore, Notifier, } from "cwip/notifications";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-server/notifications` — the SERVER half of the fleet's one
|
|
3
|
+
* notification primitive.
|
|
4
|
+
*
|
|
5
|
+
* ── What was already here, and what was missing ─────────────────────────────
|
|
6
|
+
* The dispatch engine has lived in `cwip/notifications` for months (a
|
|
7
|
+
* persistent per-user row, `groupKey` coalescing, unread counts, deep links, a
|
|
8
|
+
* live-delivery sink) and the whole client has lived in
|
|
9
|
+
* `cursedbelt/react/notifications` (store, bell, feed, inbox, poll transport).
|
|
10
|
+
* What did NOT exist was a way to MOUNT any of it: the only mounted surface in
|
|
11
|
+
* the fleet was `orch-ui/services/notifications`, written against
|
|
12
|
+
* `@satellites/satellite-rpc` inside a package only the orchestrator consumes.
|
|
13
|
+
* So a satellite wanting a bell had two options, and both were wrong — adopt
|
|
14
|
+
* orch's rpc layer, or write the routes again.
|
|
15
|
+
*
|
|
16
|
+
* This module is the third: ordinary Hono routes, an SSE hub behind the sink
|
|
17
|
+
* the center already had, and a migration for apps using cursedbelt's runner.
|
|
18
|
+
*
|
|
19
|
+
* const notifications = createNotificationService({ db });
|
|
20
|
+
* notifications.mount(app, { authorize: (c) => userOf(c)?.id ?? null });
|
|
21
|
+
*
|
|
22
|
+
* and on the client:
|
|
23
|
+
*
|
|
24
|
+
* const { store } = createNotificationsBinding(); // cursedbelt/react/notifications
|
|
25
|
+
* <NotificationBell store={store} />
|
|
26
|
+
*
|
|
27
|
+
* ── The three properties worth knowing ──────────────────────────────────────
|
|
28
|
+
* 1. **A notification is a row before it is an event.** Delivery is a cache of
|
|
29
|
+
* who is listening; the poll transport is the backstop and is not optional.
|
|
30
|
+
* Nothing is lost by a closed laptop.
|
|
31
|
+
* 2. **Coalescing is a database constraint, not a debounce.** The partial
|
|
32
|
+
* unique index on `(user_id, group_key) WHERE group_key IS NOT NULL AND
|
|
33
|
+
* done = 0` is what makes "twenty of the same thing" one row with a count,
|
|
34
|
+
* even under concurrent producers.
|
|
35
|
+
* 3. **The routes address the session's user and nothing else.** There is no
|
|
36
|
+
* route that notifies somebody else; producing is server-side code calling
|
|
37
|
+
* `center.notify*`.
|
|
38
|
+
*/
|
|
39
|
+
export { createNotificationHub } from "./hub";
|
|
40
|
+
export { NOTIFICATION_MIGRATIONS, NOTIFICATIONS_TABLE } from "./migrations";
|
|
41
|
+
export { mountNotificationRoutes } from "./routes";
|
|
42
|
+
export { createNotificationService } from "./service";
|
|
43
|
+
// Re-exported so a server consumer needs ONE import for the whole contract —
|
|
44
|
+
// the same courtesy `server/sharing` extends over `cursedbelt-core/sharing`. The
|
|
45
|
+
// engine remains cwip's; this is a door, not a copy.
|
|
46
|
+
export { createNotificationCenter, createMemoryNotificationStore, createSqliteNotificationStore, migrateNotificationsSchema, } from "cwip/notifications";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Migration } from "../migration/types";
|
|
2
|
+
export declare const NOTIFICATIONS_TABLE = "notifications";
|
|
3
|
+
/**
|
|
4
|
+
* Ordered migrations for the notification table.
|
|
5
|
+
*
|
|
6
|
+
* applyRecommendedPragmas(db);
|
|
7
|
+
* runMigrations(db, [...APP_MIGRATIONS, ...NOTIFICATION_MIGRATIONS]);
|
|
8
|
+
*
|
|
9
|
+
* An app that instead constructs `createSqliteNotificationStore(db)` gets the
|
|
10
|
+
* same table without this — the store migrates on construction. Running both is
|
|
11
|
+
* harmless and is the normal state of an app that adopted the store first.
|
|
12
|
+
*/
|
|
13
|
+
export declare const NOTIFICATION_MIGRATIONS: readonly Migration[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { migrateNotificationsSchema } from "cwip/notifications";
|
|
2
|
+
export const NOTIFICATIONS_TABLE = "notifications";
|
|
3
|
+
/**
|
|
4
|
+
* Ordered migrations for the notification table.
|
|
5
|
+
*
|
|
6
|
+
* applyRecommendedPragmas(db);
|
|
7
|
+
* runMigrations(db, [...APP_MIGRATIONS, ...NOTIFICATION_MIGRATIONS]);
|
|
8
|
+
*
|
|
9
|
+
* An app that instead constructs `createSqliteNotificationStore(db)` gets the
|
|
10
|
+
* same table without this — the store migrates on construction. Running both is
|
|
11
|
+
* harmless and is the normal state of an app that adopted the store first.
|
|
12
|
+
*/
|
|
13
|
+
export const NOTIFICATION_MIGRATIONS = [
|
|
14
|
+
{
|
|
15
|
+
name: "0001_notifications",
|
|
16
|
+
up(db) {
|
|
17
|
+
migrateNotificationsSchema(db, NOTIFICATIONS_TABLE);
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain-HTTP surface for the notification center.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists (2026-08-11) ────────────────────────────────────────────
|
|
5
|
+
* The engine (`cwip/notifications`), the store and the whole client
|
|
6
|
+
* (`cursedbelt/react/notifications`) were already fleet-wide. The only mounted
|
|
7
|
+
* SERVER surface was `orch-ui/services/notifications`, written against
|
|
8
|
+
* `@satellites/satellite-rpc` and living inside a package only the orchestrator
|
|
9
|
+
* consumes — so every other satellite's route to a notification was "adopt
|
|
10
|
+
* orch's rpc layer" or "write it again". This is that surface as ordinary
|
|
11
|
+
* routes on an ordinary Hono app, one mount call, no rpc.
|
|
12
|
+
*
|
|
13
|
+
* The route table mirrors the rpc one 1:1 on purpose: the two are the same
|
|
14
|
+
* contract in two dialects, and a client written against either sees the same
|
|
15
|
+
* fields. `mutations` are POST/DELETE and `queries` are GET, so the GET-only
|
|
16
|
+
* enforcement middleware some satellites run stays meaningful.
|
|
17
|
+
*
|
|
18
|
+
* GET {base} → { items, hasMore } ?filter=&limit=&offset=
|
|
19
|
+
* GET {base}/unread-count → { count }
|
|
20
|
+
* GET {base}/stream → SSE (live delivery; needs a hub)
|
|
21
|
+
* GET {base}/:id → { notification }
|
|
22
|
+
* POST {base}/read-all → { changed }
|
|
23
|
+
* POST {base}/done-all → { changed }
|
|
24
|
+
* POST {base}/:id/read → { changed } body { read?: boolean }
|
|
25
|
+
* POST {base}/:id/done → { changed } body { done?: boolean }
|
|
26
|
+
* DELETE {base}/:id → { changed }
|
|
27
|
+
*
|
|
28
|
+
* 🔴 **Every handler addresses `authorize(c)`'s user and never a body field.**
|
|
29
|
+
* The center is user-scoped at the store level, but a route that took a
|
|
30
|
+
* `userId` parameter would make that scoping decorative — so there is no such
|
|
31
|
+
* parameter anywhere below, and there is deliberately no route that notifies
|
|
32
|
+
* somebody else. Producing notifications is server-side business logic calling
|
|
33
|
+
* `center.notify*` directly; a browser can only ever read and acknowledge its
|
|
34
|
+
* OWN inbox.
|
|
35
|
+
*/
|
|
36
|
+
import type { Context, Hono } from "hono";
|
|
37
|
+
import type { NotificationCenter } from "cwip/notifications";
|
|
38
|
+
import type { NotificationHub } from "./hub";
|
|
39
|
+
/** The slice of the center these routes use — `createNotificationCenter(...)`
|
|
40
|
+
* satisfies it, and so does any app-specific facade over one. */
|
|
41
|
+
export type NotificationRouteCenter = Pick<NotificationCenter, "list" | "get" | "unreadCount" | "markRead" | "markAllRead" | "setDone" | "doneAll" | "remove">;
|
|
42
|
+
export interface NotificationRoutesOptions {
|
|
43
|
+
center: NotificationRouteCenter;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the caller to a user id, or null to refuse with 401. This is the
|
|
46
|
+
* app's session model and the ONLY thing that decides whose inbox is served.
|
|
47
|
+
*/
|
|
48
|
+
authorize: (c: Context) => string | null | Promise<string | null>;
|
|
49
|
+
/** Live delivery. Omit it and `{base}/stream` is not mounted at all — a
|
|
50
|
+
* route that answers 200 with a stream that never emits is worse than a
|
|
51
|
+
* 404 the client can fall back from. */
|
|
52
|
+
hub?: NotificationHub;
|
|
53
|
+
/** Path prefix. Default `/api/notifications`. */
|
|
54
|
+
basePath?: string;
|
|
55
|
+
/** Largest page a caller may ask for. Default 200. */
|
|
56
|
+
maxLimit?: number;
|
|
57
|
+
}
|
|
58
|
+
export declare function mountNotificationRoutes(app: Hono, options: NotificationRoutesOptions): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
2
|
+
const FILTERS = ["unread", "all", "done"];
|
|
3
|
+
const parseFilter = (raw) => FILTERS.find((f) => f === raw);
|
|
4
|
+
/** Query integers are untrusted text: a NaN or a negative offset must become
|
|
5
|
+
* the default, never reach SQL, and never be an error the caller has to handle. */
|
|
6
|
+
const parseInt_ = (raw, min, max) => {
|
|
7
|
+
if (raw === undefined || raw === "")
|
|
8
|
+
return undefined;
|
|
9
|
+
const value = Number.parseInt(raw, 10);
|
|
10
|
+
if (!Number.isFinite(value))
|
|
11
|
+
return undefined;
|
|
12
|
+
return Math.min(max, Math.max(min, value));
|
|
13
|
+
};
|
|
14
|
+
/** A body that is absent, malformed, or oversized is `{}` — every field these
|
|
15
|
+
* routes read is optional with a documented default, so there is nothing a
|
|
16
|
+
* caller could usefully be told about a bad one. */
|
|
17
|
+
const readFlag = async (c, key) => {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await c.req.text();
|
|
20
|
+
if (!raw || raw.length > 1024)
|
|
21
|
+
return true;
|
|
22
|
+
const body = JSON.parse(raw);
|
|
23
|
+
const value = body?.[key];
|
|
24
|
+
return typeof value === "boolean" ? value : true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export function mountNotificationRoutes(app, options) {
|
|
31
|
+
const base = options.basePath ?? "/api/notifications";
|
|
32
|
+
const maxLimit = options.maxLimit ?? 200;
|
|
33
|
+
const { center, hub } = options;
|
|
34
|
+
/** Every handler starts here. Returns the user id, or the 401 to return. */
|
|
35
|
+
const whoOr401 = async (c) => {
|
|
36
|
+
const userId = await options.authorize(c);
|
|
37
|
+
if (!userId)
|
|
38
|
+
return c.json({ error: "unauthorized" }, 401, NO_STORE);
|
|
39
|
+
return userId;
|
|
40
|
+
};
|
|
41
|
+
app.get(base, async (c) => {
|
|
42
|
+
const who = await whoOr401(c);
|
|
43
|
+
if (typeof who !== "string")
|
|
44
|
+
return who;
|
|
45
|
+
const page = await center.list({
|
|
46
|
+
userId: who,
|
|
47
|
+
filter: parseFilter(c.req.query("filter")),
|
|
48
|
+
limit: parseInt_(c.req.query("limit"), 1, maxLimit),
|
|
49
|
+
offset: parseInt_(c.req.query("offset"), 0, Number.MAX_SAFE_INTEGER),
|
|
50
|
+
});
|
|
51
|
+
return c.json(page, 200, NO_STORE);
|
|
52
|
+
});
|
|
53
|
+
app.get(`${base}/unread-count`, async (c) => {
|
|
54
|
+
const who = await whoOr401(c);
|
|
55
|
+
if (typeof who !== "string")
|
|
56
|
+
return who;
|
|
57
|
+
return c.json({ count: await center.unreadCount(who) }, 200, NO_STORE);
|
|
58
|
+
});
|
|
59
|
+
// Mounted only when there is something to stream — see the option's doc.
|
|
60
|
+
if (hub) {
|
|
61
|
+
app.get(`${base}/stream`, async (c) => {
|
|
62
|
+
const who = await whoOr401(c);
|
|
63
|
+
if (typeof who !== "string")
|
|
64
|
+
return who;
|
|
65
|
+
// The request's own abort signal is what ends the stream when the tab
|
|
66
|
+
// closes; without it the subscription outlives the socket.
|
|
67
|
+
return hub.stream(who, { signal: c.req.raw.signal });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
app.post(`${base}/read-all`, async (c) => {
|
|
71
|
+
const who = await whoOr401(c);
|
|
72
|
+
if (typeof who !== "string")
|
|
73
|
+
return who;
|
|
74
|
+
return c.json({ changed: await center.markAllRead(who) }, 200, NO_STORE);
|
|
75
|
+
});
|
|
76
|
+
app.post(`${base}/done-all`, async (c) => {
|
|
77
|
+
const who = await whoOr401(c);
|
|
78
|
+
if (typeof who !== "string")
|
|
79
|
+
return who;
|
|
80
|
+
return c.json({ changed: await center.doneAll(who) }, 200, NO_STORE);
|
|
81
|
+
});
|
|
82
|
+
app.post(`${base}/:id/read`, async (c) => {
|
|
83
|
+
const who = await whoOr401(c);
|
|
84
|
+
if (typeof who !== "string")
|
|
85
|
+
return who;
|
|
86
|
+
const read = await readFlag(c, "read");
|
|
87
|
+
return c.json({ changed: await center.markRead(who, c.req.param("id"), read) }, 200, NO_STORE);
|
|
88
|
+
});
|
|
89
|
+
app.post(`${base}/:id/done`, async (c) => {
|
|
90
|
+
const who = await whoOr401(c);
|
|
91
|
+
if (typeof who !== "string")
|
|
92
|
+
return who;
|
|
93
|
+
const done = await readFlag(c, "done");
|
|
94
|
+
return c.json({ changed: await center.setDone(who, c.req.param("id"), done) }, 200, NO_STORE);
|
|
95
|
+
});
|
|
96
|
+
app.delete(`${base}/:id`, async (c) => {
|
|
97
|
+
const who = await whoOr401(c);
|
|
98
|
+
if (typeof who !== "string")
|
|
99
|
+
return who;
|
|
100
|
+
return c.json({ changed: await center.remove(who, c.req.param("id")) }, 200, NO_STORE);
|
|
101
|
+
});
|
|
102
|
+
// LAST: `/:id` would otherwise swallow `/unread-count` and `/stream`, and
|
|
103
|
+
// the failure is a 404-shaped `{ notification: null }` rather than an error.
|
|
104
|
+
app.get(`${base}/:id`, async (c) => {
|
|
105
|
+
const who = await whoOr401(c);
|
|
106
|
+
if (typeof who !== "string")
|
|
107
|
+
return who;
|
|
108
|
+
const notification = await center.get(who, c.req.param("id"));
|
|
109
|
+
if (!notification)
|
|
110
|
+
return c.json({ error: "not found" }, 404, NO_STORE);
|
|
111
|
+
return c.json({ notification }, 200, NO_STORE);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One call that gives an app the whole server half: a migrated table, the
|
|
3
|
+
* dispatch center, live delivery, and the routes.
|
|
4
|
+
*
|
|
5
|
+
* const notifications = createNotificationService({ db });
|
|
6
|
+
* notifications.mount(app, { authorize: (c) => userOf(c)?.id ?? null });
|
|
7
|
+
* // …anywhere in the app's business logic:
|
|
8
|
+
* await notifications.center.notify(userId, { type: "auth.login", title: "New sign-in" });
|
|
9
|
+
*
|
|
10
|
+
* Everything it composes is separately usable — an app that already has a
|
|
11
|
+
* center, or that wants the routes on a store of its own, calls
|
|
12
|
+
* `mountNotificationRoutes` directly. This exists so the common case is one
|
|
13
|
+
* import and no wiring to get wrong.
|
|
14
|
+
*/
|
|
15
|
+
import type { Database } from "bun:sqlite";
|
|
16
|
+
import { type NotificationCenter, type NotificationStore } from "cwip/notifications";
|
|
17
|
+
import type { Context, Hono } from "hono";
|
|
18
|
+
import { type NotificationHub } from "./hub";
|
|
19
|
+
export interface CreateNotificationServiceOptions {
|
|
20
|
+
/** The app's own database — the table lands beside its rows. */
|
|
21
|
+
db: Database;
|
|
22
|
+
/** Table name override (identifier-validated by the store). */
|
|
23
|
+
table?: string;
|
|
24
|
+
/** Resolve a role audience (`notifyAudience({ role })`) to user ids. */
|
|
25
|
+
resolveRole?: (role: string) => Promise<string[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Live delivery. `false` builds no hub — the client's poll transport is the
|
|
28
|
+
* whole delivery story then, which is a perfectly good one for an app whose
|
|
29
|
+
* notifications are not time-critical.
|
|
30
|
+
*/
|
|
31
|
+
live?: boolean | {
|
|
32
|
+
heartbeatMs?: number;
|
|
33
|
+
maxStreamsPerUser?: number;
|
|
34
|
+
};
|
|
35
|
+
/** Producer-path failures and sink failures land here. Defaults to stderr. */
|
|
36
|
+
onError?: (error: unknown, context: string) => void;
|
|
37
|
+
/** Test seams, forwarded to the center. */
|
|
38
|
+
now?: () => number;
|
|
39
|
+
newId?: () => string;
|
|
40
|
+
}
|
|
41
|
+
export interface NotificationServiceMountOptions {
|
|
42
|
+
/** Resolve the caller to a user id, or null to refuse. */
|
|
43
|
+
authorize: (c: Context) => string | null | Promise<string | null>;
|
|
44
|
+
/** Path prefix. Default `/api/notifications`. */
|
|
45
|
+
basePath?: string;
|
|
46
|
+
/** Largest page a caller may ask for. Default 200. */
|
|
47
|
+
maxLimit?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface NotificationService {
|
|
50
|
+
readonly center: NotificationCenter;
|
|
51
|
+
readonly store: NotificationStore;
|
|
52
|
+
/** Null when `live: false` — there is then nothing to stream. */
|
|
53
|
+
readonly hub: NotificationHub | null;
|
|
54
|
+
mount(app: Hono, options: NotificationServiceMountOptions): void;
|
|
55
|
+
/** Drop live subscribers (shutdown, tests). Safe to call with no hub. */
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
export declare function createNotificationService(options: CreateNotificationServiceOptions): NotificationService;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createNotificationCenter, createSqliteNotificationStore, } from "cwip/notifications";
|
|
2
|
+
import { createNotificationHub } from "./hub";
|
|
3
|
+
import { mountNotificationRoutes } from "./routes";
|
|
4
|
+
export function createNotificationService(options) {
|
|
5
|
+
const onError = options.onError ??
|
|
6
|
+
((error, context) => console.error(`[notifications] ${context}:`, error instanceof Error ? error.message : error));
|
|
7
|
+
const store = createSqliteNotificationStore(options.db, {
|
|
8
|
+
table: options.table,
|
|
9
|
+
});
|
|
10
|
+
const hub = options.live === false
|
|
11
|
+
? null
|
|
12
|
+
: createNotificationHub({
|
|
13
|
+
...(typeof options.live === "object" ? options.live : {}),
|
|
14
|
+
onError,
|
|
15
|
+
});
|
|
16
|
+
const center = createNotificationCenter({
|
|
17
|
+
store,
|
|
18
|
+
sink: hub?.sink,
|
|
19
|
+
resolveRole: options.resolveRole,
|
|
20
|
+
now: options.now,
|
|
21
|
+
newId: options.newId,
|
|
22
|
+
onError,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
center,
|
|
26
|
+
store,
|
|
27
|
+
hub,
|
|
28
|
+
mount(app, mountOptions) {
|
|
29
|
+
mountNotificationRoutes(app, {
|
|
30
|
+
center,
|
|
31
|
+
hub: hub ?? undefined,
|
|
32
|
+
authorize: mountOptions.authorize,
|
|
33
|
+
basePath: mountOptions.basePath,
|
|
34
|
+
maxLimit: mountOptions.maxLimit,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
close() {
|
|
38
|
+
hub?.close();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The one env var. A machine with a shared data root sets this and nothing else. */
|
|
2
|
+
export declare const APP_DATA_HOME_ENV = "APP_DATA_HOME";
|
|
3
|
+
/** The cross-desktop convention this honors ahead of the platform default. */
|
|
4
|
+
export declare const XDG_DATA_HOME_ENV = "XDG_DATA_HOME";
|
|
5
|
+
export interface ResolveAppDataHomeOptions {
|
|
6
|
+
/** An explicit answer from the caller. Wins outright — this is the "argument" half. */
|
|
7
|
+
readonly override?: string;
|
|
8
|
+
/** Environment to read. Injected by tests; defaults to the process's own. */
|
|
9
|
+
readonly env?: Record<string, string | undefined>;
|
|
10
|
+
/**
|
|
11
|
+
* Platform to resolve for. Injected by tests so both branches are proved on one
|
|
12
|
+
* machine; defaults to the running platform.
|
|
13
|
+
*/
|
|
14
|
+
readonly platform?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The machine's app-data home. See the module note for the ordering rationale.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveAppDataHome(options?: ResolveAppDataHomeOptions): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a machine keeps an app's data — as CONFIGURATION, never as a convention
|
|
3
|
+
* baked into the library.
|
|
4
|
+
*
|
|
5
|
+
* ## What this replaces, and why it is a defect rather than untidiness
|
|
6
|
+
*
|
|
7
|
+
* Until 2026-09-13 three live modules in this package joined the ambient home
|
|
8
|
+
* directory to a hardcoded dotfolder belonging to the previous generation: the
|
|
9
|
+
* retention store, the archive, and `resolveAppDataRoot`. This package is
|
|
10
|
+
* PUBLISHED. Anybody who installed it got a library that went looking for a
|
|
11
|
+
* directory in their home that has nothing to do with them, and — worse — wrote
|
|
12
|
+
* into it if it happened to exist.
|
|
13
|
+
*
|
|
14
|
+
* A library may not know the name of a directory in somebody's home. It may know
|
|
15
|
+
* two things: what the caller told it, and what the operating system says the
|
|
16
|
+
* convention is. That is the whole ladder below.
|
|
17
|
+
*
|
|
18
|
+
* ## The ladder
|
|
19
|
+
*
|
|
20
|
+
* 1. an explicit `override` — a `--data-root` flag, a host's own resolution;
|
|
21
|
+
* 2. `$APP_DATA_HOME` — the ONE env var, for a machine that keeps every app's
|
|
22
|
+
* data under one root so a fleet-wide tool can see all of it at once;
|
|
23
|
+
* 3. `$XDG_DATA_HOME` — the user already told the OS where data goes;
|
|
24
|
+
* 4. the platform default: `~/Library/Application Support` on macOS,
|
|
25
|
+
* `~/.local/share` everywhere else.
|
|
26
|
+
*
|
|
27
|
+
* `$XDG_DATA_HOME` is honored on macOS too. It is not the platform's own
|
|
28
|
+
* convention there, but a user who sets it has stated a preference, and a
|
|
29
|
+
* preference outranks a default on any platform.
|
|
30
|
+
*
|
|
31
|
+
* Nothing here touches the filesystem: it is a pure mapping from environment to
|
|
32
|
+
* path, so a caller can reason about the answer before creating anything, and a
|
|
33
|
+
* test can assert both platforms on one machine.
|
|
34
|
+
*/
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
/** The one env var. A machine with a shared data root sets this and nothing else. */
|
|
38
|
+
export const APP_DATA_HOME_ENV = "APP_DATA_HOME";
|
|
39
|
+
/** The cross-desktop convention this honors ahead of the platform default. */
|
|
40
|
+
export const XDG_DATA_HOME_ENV = "XDG_DATA_HOME";
|
|
41
|
+
/** Trimmed, or undefined — an env var set to whitespace is not a configuration. */
|
|
42
|
+
function configured(value) {
|
|
43
|
+
const trimmed = value?.trim();
|
|
44
|
+
return trimmed ? trimmed : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The machine's app-data home. See the module note for the ordering rationale.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveAppDataHome(options = {}) {
|
|
50
|
+
const env = options.env ?? process.env;
|
|
51
|
+
const override = configured(options.override);
|
|
52
|
+
if (override)
|
|
53
|
+
return override;
|
|
54
|
+
const appDataHome = configured(env[APP_DATA_HOME_ENV]);
|
|
55
|
+
if (appDataHome)
|
|
56
|
+
return appDataHome;
|
|
57
|
+
const xdg = configured(env[XDG_DATA_HOME_ENV]);
|
|
58
|
+
if (xdg)
|
|
59
|
+
return xdg;
|
|
60
|
+
// `env.HOME` rather than `homedir()` alone, so a caller passing an env object
|
|
61
|
+
// gets a resolution it can actually control — every test below depends on it,
|
|
62
|
+
// and so does any host that boots an app under a different account.
|
|
63
|
+
const home = configured(env.HOME) ?? homedir();
|
|
64
|
+
const platform = options.platform ?? process.platform;
|
|
65
|
+
return platform === "darwin"
|
|
66
|
+
? join(home, "Library", "Application Support")
|
|
67
|
+
: join(home, ".local", "share");
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type KeyMode = 'standard' | 'deniable';
|
|
2
|
+
/** A random per-user salt (non-secret, stored plaintext, stable once created). Domain-separates
|
|
3
|
+
* a user's password-derived keys so the same password yields different keys across users. */
|
|
4
|
+
export declare const generateUserSalt: () => string;
|
|
5
|
+
/** Zero-knowledge set key: scrypt(password, per-user salt). The server never stores this (or a
|
|
6
|
+
* hash of it); it exists only transiently after an unlock. Different passwords → disjoint sets. */
|
|
7
|
+
export declare const derivePasswordSetKey: (password: string, userSalt: string) => Buffer;
|
|
8
|
+
/** Recoverable set key: scrypt(serverSecret, userId). The server can always derive it, so
|
|
9
|
+
* `standard`-mode media is decryptable once private is unlocked (session-gated) — protects a
|
|
10
|
+
* stolen disk / Finder / backups, but a compromised running server can decrypt. */
|
|
11
|
+
export declare const deriveServerSetKey: (serverSecret: string, userId: string) => Buffer;
|
|
12
|
+
export declare const sealMeta: (setKey: Buffer, meta: unknown) => string;
|
|
13
|
+
/** Open a sealed envelope, or null if this key doesn't own it (wrong password / mode key).
|
|
14
|
+
* The null return IS the membership signal — no stored set identifier is needed. */
|
|
15
|
+
export declare function openMeta<T = unknown>(setKey: Buffer, sealed: string): T | null;
|
|
16
|
+
export type ContentKey = {
|
|
17
|
+
key: Buffer;
|
|
18
|
+
iv: Buffer;
|
|
19
|
+
};
|
|
20
|
+
export declare const generateContentKey: () => ContentKey;
|
|
21
|
+
export declare const wrapContentKey: (setKey: Buffer, content: ContentKey) => string;
|
|
22
|
+
/** Unwrap a content key, or null if the set key can't open it (wrong password / mode key). */
|
|
23
|
+
export declare function unwrapContentKey(setKey: Buffer, wrapped: string): ContentKey | null;
|
|
24
|
+
/** Pipe a byte stream through the item's AES-128-CTR keystream. Symmetric: pass ciphertext to
|
|
25
|
+
* decrypt, plaintext to encrypt. */
|
|
26
|
+
/** Big-endian increment of a 16-byte CTR IV by `blocks` — the counter arithmetic that lets a
|
|
27
|
+
* decryptor start mid-stream (genapp-100: Range/seek on private blobs). */
|
|
28
|
+
export declare function incrementIv(iv: Buffer, blocks: number): Buffer;
|
|
29
|
+
/**
|
|
30
|
+
* Decrypt a CTR ciphertext stream that starts at the BLOCK-ALIGNED offset covering plaintext
|
|
31
|
+
* byte `startOffset`, dropping the intra-block lead-in — the seek half of {@link cryptBlobStream}.
|
|
32
|
+
* CTR is length-preserving and positionally keyed, so byte N of plaintext only needs ciphertext
|
|
33
|
+
* from block ⌊N/16⌋ onward with the IV counter advanced by that many blocks (genapp-100: this is
|
|
34
|
+
* what makes big private videos scrub on a remote-store host with no local derivatives).
|
|
35
|
+
*/
|
|
36
|
+
export declare function cryptBlobRangeStream(content: ContentKey, stream: ReadableStream<Uint8Array>, startOffset: number): ReadableStream<Uint8Array>;
|
|
37
|
+
export declare function cryptBlobStream(content: ContentKey, stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createCipheriv, randomBytes } from 'node:crypto';
|
|
2
|
+
import { decryptSecret, deriveKey, encryptSecret } from 'cwip/node';
|
|
3
|
+
/** A random per-user salt (non-secret, stored plaintext, stable once created). Domain-separates
|
|
4
|
+
* a user's password-derived keys so the same password yields different keys across users. */
|
|
5
|
+
export const generateUserSalt = () => randomBytes(16).toString('base64url');
|
|
6
|
+
/** Zero-knowledge set key: scrypt(password, per-user salt). The server never stores this (or a
|
|
7
|
+
* hash of it); it exists only transiently after an unlock. Different passwords → disjoint sets. */
|
|
8
|
+
export const derivePasswordSetKey = (password, userSalt) => deriveKey(password, `pmedia:${userSalt}`);
|
|
9
|
+
/** Recoverable set key: scrypt(serverSecret, userId). The server can always derive it, so
|
|
10
|
+
* `standard`-mode media is decryptable once private is unlocked (session-gated) — protects a
|
|
11
|
+
* stolen disk / Finder / backups, but a compromised running server can decrypt. */
|
|
12
|
+
export const deriveServerSetKey = (serverSecret, userId) => deriveKey(serverSecret, `pmedia-srv:${userId}`);
|
|
13
|
+
// ── Sealed metadata envelope (title / thumbnail key / notes / tags) ──────────────
|
|
14
|
+
// Encrypted under the set key; NEVER stored in the plaintext index. A successful decrypt is
|
|
15
|
+
// also the deniable MEMBERSHIP TEST — trying a password reveals only the items it opens.
|
|
16
|
+
export const sealMeta = (setKey, meta) => encryptSecret(JSON.stringify(meta), setKey);
|
|
17
|
+
/** Open a sealed envelope, or null if this key doesn't own it (wrong password / mode key).
|
|
18
|
+
* The null return IS the membership signal — no stored set identifier is needed. */
|
|
19
|
+
export function openMeta(setKey, sealed) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(decryptSecret(sealed, setKey));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const generateContentKey = () => ({
|
|
28
|
+
key: randomBytes(16), // AES-128
|
|
29
|
+
iv: randomBytes(16),
|
|
30
|
+
});
|
|
31
|
+
export const wrapContentKey = (setKey, content) => encryptSecret(`${content.key.toString('base64')}.${content.iv.toString('base64')}`, setKey);
|
|
32
|
+
/** Unwrap a content key, or null if the set key can't open it (wrong password / mode key). */
|
|
33
|
+
export function unwrapContentKey(setKey, wrapped) {
|
|
34
|
+
try {
|
|
35
|
+
const [k, iv] = decryptSecret(wrapped, setKey).split('.');
|
|
36
|
+
if (!k || !iv)
|
|
37
|
+
return null;
|
|
38
|
+
return { key: Buffer.from(k, 'base64'), iv: Buffer.from(iv, 'base64') };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ── Whole-file blob cipher (non-video private files: photos, docs) ────────────────
|
|
45
|
+
// AES-128-CTR under the item's content key — the same key/IV material the HLS path wraps, so
|
|
46
|
+
// wrap/unwrap is shared. CTR is length-preserving and streaming (no padding, no buffering), and
|
|
47
|
+
// encrypt/decrypt are the same operation. Tamper-evidence comes from the GCM-sealed descriptor +
|
|
48
|
+
// the gated serve route (parity with the encrypted-HLS segments, which are CBC-without-MAC too).
|
|
49
|
+
/** Pipe a byte stream through the item's AES-128-CTR keystream. Symmetric: pass ciphertext to
|
|
50
|
+
* decrypt, plaintext to encrypt. */
|
|
51
|
+
/** Big-endian increment of a 16-byte CTR IV by `blocks` — the counter arithmetic that lets a
|
|
52
|
+
* decryptor start mid-stream (genapp-100: Range/seek on private blobs). */
|
|
53
|
+
export function incrementIv(iv, blocks) {
|
|
54
|
+
const out = Buffer.from(iv);
|
|
55
|
+
let carry = BigInt(blocks);
|
|
56
|
+
for (let i = out.length - 1; i >= 0 && carry > 0n; i--) {
|
|
57
|
+
const sum = BigInt(out[i]) + (carry & 0xffn);
|
|
58
|
+
out[i] = Number(sum & 0xffn);
|
|
59
|
+
carry = (carry >> 8n) + (sum >> 8n);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Decrypt a CTR ciphertext stream that starts at the BLOCK-ALIGNED offset covering plaintext
|
|
65
|
+
* byte `startOffset`, dropping the intra-block lead-in — the seek half of {@link cryptBlobStream}.
|
|
66
|
+
* CTR is length-preserving and positionally keyed, so byte N of plaintext only needs ciphertext
|
|
67
|
+
* from block ⌊N/16⌋ onward with the IV counter advanced by that many blocks (genapp-100: this is
|
|
68
|
+
* what makes big private videos scrub on a remote-store host with no local derivatives).
|
|
69
|
+
*/
|
|
70
|
+
export function cryptBlobRangeStream(content, stream, startOffset) {
|
|
71
|
+
const BLOCK = 16;
|
|
72
|
+
const blocks = Math.floor(startOffset / BLOCK);
|
|
73
|
+
let drop = startOffset % BLOCK;
|
|
74
|
+
const cipher = createCipheriv('aes-128-ctr', content.key, incrementIv(content.iv, blocks));
|
|
75
|
+
return stream.pipeThrough(new TransformStream({
|
|
76
|
+
transform(chunk, controller) {
|
|
77
|
+
let out = cipher.update(chunk);
|
|
78
|
+
if (drop > 0) {
|
|
79
|
+
const skip = Math.min(drop, out.byteLength);
|
|
80
|
+
out = out.subarray(skip);
|
|
81
|
+
drop -= skip;
|
|
82
|
+
}
|
|
83
|
+
if (out.byteLength > 0) {
|
|
84
|
+
controller.enqueue(new Uint8Array(out.buffer, out.byteOffset, out.byteLength));
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
flush(controller) {
|
|
88
|
+
const out = cipher.final(); // empty for CTR — kept for cipher-contract completeness
|
|
89
|
+
if (out.byteLength > 0) {
|
|
90
|
+
controller.enqueue(new Uint8Array(out.buffer, out.byteOffset, out.byteLength));
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
export function cryptBlobStream(content, stream) {
|
|
96
|
+
const cipher = createCipheriv('aes-128-ctr', content.key, content.iv);
|
|
97
|
+
return stream.pipeThrough(new TransformStream({
|
|
98
|
+
transform(chunk, controller) {
|
|
99
|
+
const out = cipher.update(chunk);
|
|
100
|
+
if (out.byteLength > 0) {
|
|
101
|
+
controller.enqueue(new Uint8Array(out.buffer, out.byteOffset, out.byteLength));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
flush(controller) {
|
|
105
|
+
const out = cipher.final(); // empty for CTR — kept for cipher-contract completeness
|
|
106
|
+
if (out.byteLength > 0) {
|
|
107
|
+
controller.enqueue(new Uint8Array(out.buffer, out.byteOffset, out.byteLength));
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
}
|