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,265 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createDecipheriv } from 'node:crypto';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import { isFfmpegAvailable } from '../media-bun';
|
|
8
|
+
import { createDiskStore } from '../storage/diskStore';
|
|
9
|
+
import { generateUserSalt, sealMeta } from './crypto';
|
|
10
|
+
import { createMemoryPrivateIndex, type PrivateItem } from './index-store';
|
|
11
|
+
import { mountPrivateRoutes } from './mountPrivateRoutes';
|
|
12
|
+
import { processPrivateVideo } from './pipeline';
|
|
13
|
+
import { createUnlockService } from './unlock';
|
|
14
|
+
|
|
15
|
+
const unlock = createUnlockService({ serverSecret: 'srv', unlockSecret: 'unlock' });
|
|
16
|
+
|
|
17
|
+
describe('unlock service', () => {
|
|
18
|
+
test('round-trips a deniable set key through a token', async () => {
|
|
19
|
+
const { token, mode } = await unlock.unlock({ userId: 'u1', userSalt: 's', password: 'pw' });
|
|
20
|
+
expect(mode).toBe('deniable');
|
|
21
|
+
const grant = await unlock.verify(token);
|
|
22
|
+
expect(grant?.userId).toBe('u1');
|
|
23
|
+
expect(grant?.mode).toBe('deniable');
|
|
24
|
+
// The recovered key matches a fresh derivation from the same password.
|
|
25
|
+
expect(
|
|
26
|
+
grant?.setKey.equals(unlock.deriveSetKey({ userId: 'u1', userSalt: 's', password: 'pw' })),
|
|
27
|
+
).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
test('no password ⇒ standard (recoverable) mode', async () => {
|
|
30
|
+
const { mode } = await unlock.unlock({ userId: 'u1', userSalt: 's' });
|
|
31
|
+
expect(mode).toBe('standard');
|
|
32
|
+
});
|
|
33
|
+
test('a token from a different secret does not verify', async () => {
|
|
34
|
+
const other = createUnlockService({ serverSecret: 'srv', unlockSecret: 'DIFFERENT' });
|
|
35
|
+
const { token } = await other.unlock({ userId: 'u1', userSalt: 's', password: 'pw' });
|
|
36
|
+
expect(await unlock.verify(token)).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Index + reveal need no ffmpeg — sealed metadata under two passwords, then the "try a password"
|
|
41
|
+
// endpoint reveals only the matching set; the index endpoint leaks only size + createdAt.
|
|
42
|
+
describe('index + reveal routes', () => {
|
|
43
|
+
const index = createMemoryPrivateIndex();
|
|
44
|
+
const salt = generateUserSalt();
|
|
45
|
+
const app = new Hono();
|
|
46
|
+
mountPrivateRoutes(app, {
|
|
47
|
+
store: createDiskStore('/tmp/unused-pm'),
|
|
48
|
+
index,
|
|
49
|
+
unlock,
|
|
50
|
+
authorize: async () => ({ userId: 'u1', userSalt: salt }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
beforeAll(async () => {
|
|
54
|
+
const keyA = unlock.deriveSetKey({ userId: 'u1', userSalt: salt, password: 'alpha' });
|
|
55
|
+
const keyB = unlock.deriveSetKey({ userId: 'u1', userSalt: salt, password: 'bravo' });
|
|
56
|
+
const mk = (id: string, key: Buffer, title: string): PrivateItem => ({
|
|
57
|
+
id,
|
|
58
|
+
owner: 'u1',
|
|
59
|
+
sizeBytes: 1234,
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
status: 'ready',
|
|
62
|
+
keyMode: 'deniable',
|
|
63
|
+
wrappedContentKey: 'x',
|
|
64
|
+
sealedMeta: sealMeta(key, { title, notes: 'secret' }),
|
|
65
|
+
});
|
|
66
|
+
await index.add(mk('a1', keyA, 'Alpha One'));
|
|
67
|
+
await index.add(mk('a2', keyA, 'Alpha Two'));
|
|
68
|
+
await index.add(mk('b1', keyB, 'Bravo One'));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('index exposes only size/createdAt/status — no title/notes/mode', async () => {
|
|
72
|
+
const res = await app.request('/_private/index');
|
|
73
|
+
const body = (await res.json()) as { items: Array<Record<string, unknown>> };
|
|
74
|
+
expect(body.items.length).toBe(3);
|
|
75
|
+
for (const row of body.items) {
|
|
76
|
+
expect(row.sizeBytes).toBe(1234);
|
|
77
|
+
expect(row).not.toHaveProperty('title');
|
|
78
|
+
expect(row).not.toHaveProperty('sealedMeta');
|
|
79
|
+
expect(row).not.toHaveProperty('keyMode');
|
|
80
|
+
expect(row).not.toHaveProperty('wrappedContentKey');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('reveal unlocks only the matching password set', async () => {
|
|
85
|
+
const res = await app.request('/_private/reveal', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'content-type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ password: 'alpha' }),
|
|
89
|
+
});
|
|
90
|
+
const body = (await res.json()) as { items: Array<{ id: string; meta: { title: string } }> };
|
|
91
|
+
expect(body.items.map((i) => i.id).sort()).toEqual(['a1', 'a2']);
|
|
92
|
+
expect(body.items.find((i) => i.id === 'a1')?.meta.title).toBe('Alpha One');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a wrong password reveals nothing (no info leak)', async () => {
|
|
96
|
+
const res = await app.request('/_private/reveal', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'content-type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ password: 'zzz' }),
|
|
100
|
+
});
|
|
101
|
+
expect(((await res.json()) as { items: unknown[] }).items).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('reveal-unlocked: an unlock token reveals the same set as its password', async () => {
|
|
105
|
+
const { token } = await unlock.unlock({ userId: 'u1', userSalt: salt, password: 'alpha' });
|
|
106
|
+
const res = await app.request('/_private/reveal-unlocked', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'content-type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ token }),
|
|
110
|
+
});
|
|
111
|
+
expect(res.status).toBe(200);
|
|
112
|
+
const body = (await res.json()) as { items: Array<{ id: string; meta: { title: string } }> };
|
|
113
|
+
expect(body.items.map((i) => i.id).sort()).toEqual(['a1', 'a2']);
|
|
114
|
+
expect(body.items.find((i) => i.id === 'a1')?.meta.title).toBe('Alpha One');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('reveal-unlocked: a token for a DIFFERENT set reveals only that set', async () => {
|
|
118
|
+
const { token } = await unlock.unlock({ userId: 'u1', userSalt: salt, password: 'bravo' });
|
|
119
|
+
const res = await app.request('/_private/reveal-unlocked', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'content-type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({ token }),
|
|
123
|
+
});
|
|
124
|
+
const body = (await res.json()) as { items: Array<{ id: string }> };
|
|
125
|
+
expect(body.items.map((i) => i.id)).toEqual(['b1']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('reveal-unlocked: missing / invalid / other-user tokens are rejected', async () => {
|
|
129
|
+
const missing = await app.request('/_private/reveal-unlocked', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'content-type': 'application/json' },
|
|
132
|
+
body: JSON.stringify({}),
|
|
133
|
+
});
|
|
134
|
+
expect(missing.status).toBe(400);
|
|
135
|
+
|
|
136
|
+
const bogus = await app.request('/_private/reveal-unlocked', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'content-type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({ token: 'not-a-token' }),
|
|
140
|
+
});
|
|
141
|
+
expect(bogus.status).toBe(401);
|
|
142
|
+
|
|
143
|
+
// A valid token minted for ANOTHER user must not open this session's items.
|
|
144
|
+
const { token: foreign } = await unlock.unlock({
|
|
145
|
+
userId: 'u2',
|
|
146
|
+
userSalt: salt,
|
|
147
|
+
password: 'alpha',
|
|
148
|
+
});
|
|
149
|
+
const cross = await app.request('/_private/reveal-unlocked', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'content-type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ token: foreign }),
|
|
153
|
+
});
|
|
154
|
+
expect(cross.status).toBe(401);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Full encrypted playback path — needs ffmpeg (no-ops without it).
|
|
159
|
+
describe('encrypted playback routes', () => {
|
|
160
|
+
let root: string;
|
|
161
|
+
let work: string;
|
|
162
|
+
let hasFfmpeg = false;
|
|
163
|
+
|
|
164
|
+
beforeAll(async () => {
|
|
165
|
+
hasFfmpeg = await isFfmpegAvailable();
|
|
166
|
+
root = await mkdtemp(path.join(tmpdir(), 'pm-r-store-'));
|
|
167
|
+
work = await mkdtemp(path.join(tmpdir(), 'pm-r-work-'));
|
|
168
|
+
});
|
|
169
|
+
afterAll(async () => {
|
|
170
|
+
await rm(root, { recursive: true, force: true });
|
|
171
|
+
await rm(work, { recursive: true, force: true });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('unlock → content key (gated) decrypts a served encrypted segment', async () => {
|
|
175
|
+
if (!hasFfmpeg) {
|
|
176
|
+
console.warn('[routes.spec] ffmpeg absent — skipping encrypted playback route test');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const store = createDiskStore(root);
|
|
180
|
+
const index = createMemoryPrivateIndex();
|
|
181
|
+
const salt = generateUserSalt();
|
|
182
|
+
const setKey = unlock.deriveSetKey({ userId: 'u1', userSalt: salt, password: 'pw' });
|
|
183
|
+
|
|
184
|
+
// Produce an encrypted item + register it in the index.
|
|
185
|
+
const clip = path.join(work, 'src.mp4');
|
|
186
|
+
const gen = Bun.spawn(
|
|
187
|
+
[
|
|
188
|
+
'ffmpeg',
|
|
189
|
+
'-y',
|
|
190
|
+
'-f',
|
|
191
|
+
'lavfi',
|
|
192
|
+
'-i',
|
|
193
|
+
'testsrc=size=854x480:rate=15:duration=3',
|
|
194
|
+
'-pix_fmt',
|
|
195
|
+
'yuv420p',
|
|
196
|
+
'-c:v',
|
|
197
|
+
'libx264',
|
|
198
|
+
'-preset',
|
|
199
|
+
'ultrafast',
|
|
200
|
+
'-t',
|
|
201
|
+
'3',
|
|
202
|
+
clip,
|
|
203
|
+
],
|
|
204
|
+
{ stdout: 'ignore', stderr: 'ignore' },
|
|
205
|
+
);
|
|
206
|
+
await gen.exited;
|
|
207
|
+
const res = await processPrivateVideo({
|
|
208
|
+
store,
|
|
209
|
+
workDir: work,
|
|
210
|
+
owner: 'u1',
|
|
211
|
+
id: 'v1',
|
|
212
|
+
sourcePath: clip,
|
|
213
|
+
setKey,
|
|
214
|
+
});
|
|
215
|
+
expect(res.status).toBe('ready');
|
|
216
|
+
await index.add({
|
|
217
|
+
id: 'v1',
|
|
218
|
+
owner: 'u1',
|
|
219
|
+
sizeBytes: 1000,
|
|
220
|
+
createdAt: Date.now(),
|
|
221
|
+
status: 'ready',
|
|
222
|
+
keyMode: 'deniable',
|
|
223
|
+
wrappedContentKey: res.wrappedContentKey as string,
|
|
224
|
+
sealedMeta: sealMeta(setKey, { title: 'Private Clip' }),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const app = new Hono();
|
|
228
|
+
mountPrivateRoutes(app, {
|
|
229
|
+
store,
|
|
230
|
+
index,
|
|
231
|
+
unlock,
|
|
232
|
+
authorize: async () => ({ userId: 'u1', userSalt: salt }),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Unlock with the password → token.
|
|
236
|
+
const unlockRes = await app.request('/_private/unlock', {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: { 'content-type': 'application/json' },
|
|
239
|
+
body: JSON.stringify({ password: 'pw' }),
|
|
240
|
+
});
|
|
241
|
+
const token = ((await unlockRes.json()) as { token: string }).token;
|
|
242
|
+
|
|
243
|
+
// The gated key route returns the 16-byte content key.
|
|
244
|
+
const keyRes = await app.request(`/_private/u/${token}/hls/gallery/v1/480/enc.key`);
|
|
245
|
+
expect(keyRes.status).toBe(200);
|
|
246
|
+
expect(keyRes.headers.get('cache-control')).toBe('no-store');
|
|
247
|
+
const contentKey = new Uint8Array(await keyRes.arrayBuffer());
|
|
248
|
+
expect(contentKey.length).toBe(16);
|
|
249
|
+
|
|
250
|
+
// The segment route serves the ENCRYPTED bytes; decrypting with the key → valid MPEG-TS.
|
|
251
|
+
const segRes = await app.request(`/_private/u/${token}/hls/gallery/v1/480/seg_00000.ts`);
|
|
252
|
+
expect([200, 206]).toContain(segRes.status);
|
|
253
|
+
const segBytes = new Uint8Array(await segRes.arrayBuffer());
|
|
254
|
+
// Recover the IV from the variant playlist (#EXT-X-KEY … IV=0x…).
|
|
255
|
+
const plRes = await app.request(`/_private/u/${token}/hls/gallery/v1/480/playlist.m3u8`);
|
|
256
|
+
const playlist = await plRes.text();
|
|
257
|
+
const iv = Buffer.from(/IV=0x([0-9a-fA-F]+)/.exec(playlist)?.[1] ?? '', 'hex');
|
|
258
|
+
const d = createDecipheriv('aes-128-cbc', Buffer.from(contentKey), iv);
|
|
259
|
+
const plain = Buffer.concat([d.update(Buffer.from(segBytes)), d.final()]);
|
|
260
|
+
expect(plain[0]).toBe(0x47);
|
|
261
|
+
|
|
262
|
+
// A bogus token is rejected.
|
|
263
|
+
expect((await app.request('/_private/u/bogus/hls/gallery/v1/480/enc.key')).status).toBe(401);
|
|
264
|
+
}, 30_000);
|
|
265
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { decryptSecret, deriveKey, encryptSecret } from 'cwip/node';
|
|
2
|
+
// 🔴 `../auth/jwt`, NOT `../auth` — same reason as `storage/streamToken.ts`, and this is the
|
|
3
|
+
// SECOND edge of the pair: `cursedbelt-server/storage` reaches private-media, so the barrel
|
|
4
|
+
// import here put `otplib` (via `auth/index` → `totp.ts`) back into the storage graph after
|
|
5
|
+
// streamToken.ts had been fixed. Found by bundling the graph and reading the externals, not by
|
|
6
|
+
// grep — a two-hop edge is invisible to the obvious search.
|
|
7
|
+
import { signToken, verifyPurposeToken } from '../auth/jwt';
|
|
8
|
+
import { derivePasswordSetKey, deriveServerSetKey, type KeyMode } from './crypto';
|
|
9
|
+
|
|
10
|
+
// ── Unlock service ──────────────────────────────────────────────────────────────
|
|
11
|
+
// Turns a private-mode unlock into a short-lived token that CARRIES the set key so later
|
|
12
|
+
// requests (the HLS content-key route, reveal) can unwrap without re-deriving. Two modes:
|
|
13
|
+
// • deniable — password given → setKey = scrypt(password, userSalt); the server holds no
|
|
14
|
+
// copy, so the token is the ONLY way back to that set key until it expires.
|
|
15
|
+
// • standard — no password → setKey = scrypt(serverSecret, userId); recoverable, session-gated.
|
|
16
|
+
// The set key is SEALED inside the token (AES-GCM under a server seal key), so the client can't
|
|
17
|
+
// read it out; only this server can. The JWT is signed via cursedbelt's one jose integration
|
|
18
|
+
// under the existing 'pm-unlock' purpose, so it can't be replayed as an access token.
|
|
19
|
+
|
|
20
|
+
const PURPOSE = 'pm-unlock';
|
|
21
|
+
|
|
22
|
+
export type UnlockGrant = { userId: string; setKey: Buffer; mode: KeyMode };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What a supplied password MEANS at the unlock/reveal/private-upload surfaces:
|
|
26
|
+
* • derive (default) — the password derives the set key (deniable sets; the
|
|
27
|
+
* historical behavior).
|
|
28
|
+
* • gate — the password is an ACCESS CHECK against the app's own accounts
|
|
29
|
+
* (verifyPassword, e.g. the login store's argon2id verify) and the set key
|
|
30
|
+
* stays the recoverable server-derived one. Password changes never orphan
|
|
31
|
+
* content, and a password is REQUIRED to unlock (no silent session unlock).
|
|
32
|
+
*/
|
|
33
|
+
export type UnlockPasswordPolicy =
|
|
34
|
+
| { mode: 'derive' }
|
|
35
|
+
| {
|
|
36
|
+
mode: 'gate';
|
|
37
|
+
verifyPassword: (
|
|
38
|
+
who: { userId: string; userSalt: string },
|
|
39
|
+
password: string,
|
|
40
|
+
) => Promise<boolean>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface UnlockService {
|
|
44
|
+
/** Mint an unlock token. Pass `password` for a deniable set; omit for the standard server-key
|
|
45
|
+
* set (the caller must already have authenticated the user). */
|
|
46
|
+
unlock(args: { userId: string; userSalt: string; password?: string }): Promise<{
|
|
47
|
+
token: string;
|
|
48
|
+
mode: KeyMode;
|
|
49
|
+
}>;
|
|
50
|
+
/** Verify a token and recover the sealed set key, or null if invalid/expired/tampered. */
|
|
51
|
+
verify(token: string): Promise<UnlockGrant | null>;
|
|
52
|
+
/** Derive a set key without minting a token (for the reveal endpoint's per-item trials). */
|
|
53
|
+
deriveSetKey(args: { userId: string; userSalt: string; password?: string }): Buffer;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createUnlockService(opts: {
|
|
57
|
+
/** Derives the recoverable (standard) set key. */
|
|
58
|
+
serverSecret: string;
|
|
59
|
+
/** Signs the unlock JWT AND (domain-separated) seals the set key inside it. */
|
|
60
|
+
unlockSecret: string;
|
|
61
|
+
/** Token lifetime — long enough to survive a paused-and-resumed private video. Default 6h. */
|
|
62
|
+
ttlSeconds?: number;
|
|
63
|
+
}): UnlockService {
|
|
64
|
+
const ttl = opts.ttlSeconds ?? 6 * 60 * 60;
|
|
65
|
+
const sealKey = deriveKey(opts.unlockSecret, 'pm-unlock-seal');
|
|
66
|
+
|
|
67
|
+
const deriveSetKey = ({
|
|
68
|
+
userId,
|
|
69
|
+
userSalt,
|
|
70
|
+
password,
|
|
71
|
+
}: {
|
|
72
|
+
userId: string;
|
|
73
|
+
userSalt: string;
|
|
74
|
+
password?: string;
|
|
75
|
+
}): Buffer =>
|
|
76
|
+
password
|
|
77
|
+
? derivePasswordSetKey(password, userSalt)
|
|
78
|
+
: deriveServerSetKey(opts.serverSecret, userId);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
deriveSetKey,
|
|
82
|
+
async unlock(args) {
|
|
83
|
+
const mode: KeyMode = args.password ? 'deniable' : 'standard';
|
|
84
|
+
const setKey = deriveSetKey(args);
|
|
85
|
+
const token = await signToken(
|
|
86
|
+
{
|
|
87
|
+
userId: args.userId,
|
|
88
|
+
sessionId: 'pm',
|
|
89
|
+
roles: [],
|
|
90
|
+
purpose: PURPOSE,
|
|
91
|
+
sk: encryptSecret(setKey.toString('base64'), sealKey),
|
|
92
|
+
m: mode,
|
|
93
|
+
},
|
|
94
|
+
opts.unlockSecret,
|
|
95
|
+
ttl,
|
|
96
|
+
);
|
|
97
|
+
return { token, mode };
|
|
98
|
+
},
|
|
99
|
+
async verify(token) {
|
|
100
|
+
try {
|
|
101
|
+
const p = await verifyPurposeToken(token, opts.unlockSecret, PURPOSE);
|
|
102
|
+
const sealed = (p as { sk?: unknown }).sk;
|
|
103
|
+
const mode = (p as { m?: unknown }).m;
|
|
104
|
+
if (typeof sealed !== 'string' || (mode !== 'standard' && mode !== 'deniable')) return null;
|
|
105
|
+
const setKey = Buffer.from(decryptSecret(sealed, sealKey), 'base64');
|
|
106
|
+
return { userId: p.userId, setKey, mode };
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { Database } from 'bun:sqlite';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { mountResumeRoutes } from './routes';
|
|
5
|
+
import { createSqliteResumeStore, MAX_LOCATION_LENGTH } from './store';
|
|
6
|
+
|
|
7
|
+
const storeOf = () => createSqliteResumeStore(new Database(':memory:'));
|
|
8
|
+
|
|
9
|
+
describe('the store', () => {
|
|
10
|
+
test('remembers one place per user per key, and the last write wins', () => {
|
|
11
|
+
const store = storeOf();
|
|
12
|
+
expect(store.read('u1')).toBe(null);
|
|
13
|
+
store.write('u1', '#/?folder=Main%201&track=75');
|
|
14
|
+
store.write('u1', '#/queue', 'other');
|
|
15
|
+
store.write('u2', '#/albums');
|
|
16
|
+
expect(store.read('u1')?.location).toBe('#/?folder=Main%201&track=75');
|
|
17
|
+
expect(store.read('u1', 'other')?.location).toBe('#/queue');
|
|
18
|
+
expect(store.read('u2')?.location).toBe('#/albums');
|
|
19
|
+
store.write('u1', '#/albums/7');
|
|
20
|
+
expect(store.read('u1')?.location).toBe('#/albums/7');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('clear drops one place, or every place for that user', () => {
|
|
24
|
+
const store = storeOf();
|
|
25
|
+
store.write('u1', '#/a');
|
|
26
|
+
store.write('u1', '#/b', 'other');
|
|
27
|
+
store.clear('u1', 'other');
|
|
28
|
+
expect(store.read('u1')?.location).toBe('#/a');
|
|
29
|
+
store.clear('u1');
|
|
30
|
+
expect(store.read('u1')).toBe(null);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('an absurd location is truncated rather than stored whole', () => {
|
|
34
|
+
const store = storeOf();
|
|
35
|
+
store.write('u1', `#/${'x'.repeat(MAX_LOCATION_LENGTH * 2)}`);
|
|
36
|
+
expect(store.read('u1')!.location.length).toBe(MAX_LOCATION_LENGTH);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('a table name that is not an identifier is refused', () => {
|
|
40
|
+
expect(() => createSqliteResumeStore(new Database(':memory:'), { table: 'a; drop table b' })).toThrow();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('the routes', () => {
|
|
45
|
+
const appWith = (user: string | null) => {
|
|
46
|
+
const store = storeOf();
|
|
47
|
+
const app = new Hono();
|
|
48
|
+
mountResumeRoutes(app, { store, authorize: () => user });
|
|
49
|
+
return { app, store };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
test('round-trips a place and never lets a cache hold it', async () => {
|
|
53
|
+
const { app } = appWith('u1');
|
|
54
|
+
const put = await app.request('/api/resume?key=library', {
|
|
55
|
+
method: 'PUT',
|
|
56
|
+
headers: { 'content-type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ location: '#/?folder=Main+1&track=75' }),
|
|
58
|
+
});
|
|
59
|
+
expect(put.status).toBe(200);
|
|
60
|
+
const get = await app.request('/api/resume?key=library');
|
|
61
|
+
expect(get.headers.get('cache-control')).toBe('no-store, private');
|
|
62
|
+
expect((await get.json()).location).toBe('#/?folder=Main+1&track=75');
|
|
63
|
+
|
|
64
|
+
const cleared = await app.request('/api/resume?key=library', { method: 'DELETE' });
|
|
65
|
+
expect(cleared.status).toBe(200);
|
|
66
|
+
expect((await (await app.request('/api/resume?key=library')).json()).location).toBe(null);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('nothing stored is a 200 with a null location, not a 404', async () => {
|
|
70
|
+
const { app } = appWith('u1');
|
|
71
|
+
const response = await app.request('/api/resume');
|
|
72
|
+
expect(response.status).toBe(200);
|
|
73
|
+
expect(await response.json()).toEqual({ location: null, updatedAt: null });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a signed-out caller gets 401 and writes nothing', async () => {
|
|
77
|
+
const { app, store } = appWith(null);
|
|
78
|
+
expect((await app.request('/api/resume')).status).toBe(401);
|
|
79
|
+
const put = await app.request('/api/resume', {
|
|
80
|
+
method: 'PUT',
|
|
81
|
+
headers: { 'content-type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ location: '#/x' }),
|
|
83
|
+
});
|
|
84
|
+
expect(put.status).toBe(401);
|
|
85
|
+
expect(store.read('u1')).toBe(null);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a junk key or a junk body is refused', async () => {
|
|
89
|
+
const { app } = appWith('u1');
|
|
90
|
+
expect((await app.request('/api/resume?key=../etc')).status).toBe(400);
|
|
91
|
+
const empty = await app.request('/api/resume', {
|
|
92
|
+
method: 'PUT',
|
|
93
|
+
headers: { 'content-type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({}),
|
|
95
|
+
});
|
|
96
|
+
expect(empty.status).toBe(400);
|
|
97
|
+
const huge = await app.request('/api/resume', {
|
|
98
|
+
method: 'PUT',
|
|
99
|
+
headers: { 'content-type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({ location: `#/${'x'.repeat(MAX_LOCATION_LENGTH + 1)}` }),
|
|
101
|
+
});
|
|
102
|
+
expect(huge.status).toBe(413);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('one user cannot read another user’s place', async () => {
|
|
106
|
+
const store = storeOf();
|
|
107
|
+
const app = new Hono();
|
|
108
|
+
let who = 'u1';
|
|
109
|
+
mountResumeRoutes(app, { store, authorize: () => who });
|
|
110
|
+
await app.request('/api/resume', {
|
|
111
|
+
method: 'PUT',
|
|
112
|
+
headers: { 'content-type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ location: '#/private' }),
|
|
114
|
+
});
|
|
115
|
+
who = 'u2';
|
|
116
|
+
expect((await (await app.request('/api/resume')).json()).location).toBe(null);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three routes behind `useResume`:
|
|
3
|
+
*
|
|
4
|
+
* GET {base}?key=default → { location, updatedAt } | { location: null }
|
|
5
|
+
* PUT {base}?key=default → { ok: true } body { location }
|
|
6
|
+
* DELETE {base}?key=default → { ok: true }
|
|
7
|
+
*
|
|
8
|
+
* ── Caching, decided here rather than left to a proxy ───────────────────────
|
|
9
|
+
* Every response is `no-store, private`. The body is one person's last screen —
|
|
10
|
+
* it is per-user, it changes on every navigation, and a shared cache holding it
|
|
11
|
+
* would hand one reader another reader's place. This is the ONE endpoint the
|
|
12
|
+
* deep-link work adds; the deep links themselves are same-document hash URLs and
|
|
13
|
+
* change nothing about how the app shell is cached.
|
|
14
|
+
*
|
|
15
|
+
* Authorization is the app's, passed in: every satellite already knows how to
|
|
16
|
+
* turn a cookie into a user id, and a resume row is exactly as sensitive as the
|
|
17
|
+
* screen it names.
|
|
18
|
+
*/
|
|
19
|
+
import type { Context, Hono } from 'hono';
|
|
20
|
+
import { MAX_LOCATION_LENGTH, type ResumeStore } from './store';
|
|
21
|
+
|
|
22
|
+
export interface ResumeRoutesOptions {
|
|
23
|
+
store: ResumeStore;
|
|
24
|
+
/** Resolve the caller to a user id, or null to refuse. */
|
|
25
|
+
authorize(c: Context): string | null | Promise<string | null>;
|
|
26
|
+
/** Default `/api/resume`. */
|
|
27
|
+
basePath?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NO_STORE = { 'cache-control': 'no-store, private' } as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* What a `?key=` may look like. 🔴 EXPORTED because a client that builds a key
|
|
34
|
+
* out of app data (`useTableViewMode`'s `table-view.<id>`) has no other way to
|
|
35
|
+
* know what this route will accept — and the failure is SILENT: the client
|
|
36
|
+
* swallows every error into "nothing stored", so a rejected key looks exactly
|
|
37
|
+
* like a preference that was never saved. `useTableViewMode.spec.tsx` asserts
|
|
38
|
+
* the keys that hook produces against this regex, so changing either end turns
|
|
39
|
+
* a test red instead of quietly un-remembering everybody's view.
|
|
40
|
+
*
|
|
41
|
+
* Note there is no `:` in the class, which is the separator a key builder
|
|
42
|
+
* reaches for first.
|
|
43
|
+
*/
|
|
44
|
+
export const RESUME_KEY_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
45
|
+
|
|
46
|
+
const keyOf = (c: Context): string | null => {
|
|
47
|
+
const raw = c.req.query('key') ?? 'default';
|
|
48
|
+
return RESUME_KEY_PATTERN.test(raw) ? raw : null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function mountResumeRoutes(app: Hono, options: ResumeRoutesOptions): void {
|
|
52
|
+
const { store, authorize } = options;
|
|
53
|
+
const base = options.basePath ?? '/api/resume';
|
|
54
|
+
|
|
55
|
+
const withUser = (handler: (c: Context, userId: string, key: string) => Response | Promise<Response>) =>
|
|
56
|
+
async (c: Context): Promise<Response> => {
|
|
57
|
+
const userId = await authorize(c);
|
|
58
|
+
if (!userId) return c.json({ error: 'unauthorized' }, 401, NO_STORE);
|
|
59
|
+
const key = keyOf(c);
|
|
60
|
+
if (!key) return c.json({ error: 'invalid key' }, 400, NO_STORE);
|
|
61
|
+
return handler(c, userId, key);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
app.get(
|
|
65
|
+
base,
|
|
66
|
+
withUser((c, userId, key) => {
|
|
67
|
+
const record = store.read(userId, key);
|
|
68
|
+
return c.json(record ?? { location: null, updatedAt: null }, 200, NO_STORE);
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
app.put(
|
|
73
|
+
base,
|
|
74
|
+
withUser(async (c, userId, key) => {
|
|
75
|
+
const body = (await c.req.json().catch(() => null)) as { location?: unknown } | null;
|
|
76
|
+
const location = typeof body?.location === 'string' ? body.location.trim() : '';
|
|
77
|
+
if (!location) return c.json({ error: 'location required' }, 400, NO_STORE);
|
|
78
|
+
if (location.length > MAX_LOCATION_LENGTH)
|
|
79
|
+
return c.json({ error: 'location too long' }, 413, NO_STORE);
|
|
80
|
+
store.write(userId, location, key);
|
|
81
|
+
return c.json({ ok: true }, 200, NO_STORE);
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
app.delete(
|
|
86
|
+
base,
|
|
87
|
+
withUser((c, userId, key) => {
|
|
88
|
+
store.clear(userId, key);
|
|
89
|
+
return c.json({ ok: true }, 200, NO_STORE);
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where "the last place you were" is kept — one small table, in the app's own
|
|
3
|
+
* database, keyed by the app's own user id.
|
|
4
|
+
*
|
|
5
|
+
* ── Why server-side at all ──────────────────────────────────────────────────
|
|
6
|
+
* `localStorage` would be one line and would answer the wrong question. The
|
|
7
|
+
* owner's report is about coming BACK to an app, and he does that from another
|
|
8
|
+
* device — the 2026-08-13 music sign-in from a second laptop is the same shape.
|
|
9
|
+
* A place remembered only in the browser that saw it is not remembered.
|
|
10
|
+
*
|
|
11
|
+
* ── Why the app's database and not a fleet service ──────────────────────────
|
|
12
|
+
* Same reasoning as `../notifications/migrations.ts`: the SCHEMA is shared, the
|
|
13
|
+
* DATA is local. A resume row is one string per user per app; a central service
|
|
14
|
+
* would put a network hop in front of the first paint of every app in the fleet,
|
|
15
|
+
* and the first thing each would do is cache it.
|
|
16
|
+
*
|
|
17
|
+
* The DDL is idempotent, so calling this twice — or beside a migration runner —
|
|
18
|
+
* converges on one table.
|
|
19
|
+
*/
|
|
20
|
+
import type { Database } from 'bun:sqlite';
|
|
21
|
+
|
|
22
|
+
/** Longer than any real place, short enough that nobody can park a payload here. */
|
|
23
|
+
export const MAX_LOCATION_LENGTH = 2048;
|
|
24
|
+
|
|
25
|
+
export interface ResumeRecord {
|
|
26
|
+
location: string;
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ResumeStore {
|
|
31
|
+
read(userId: string, key?: string): ResumeRecord | null;
|
|
32
|
+
write(userId: string, location: string, key?: string): void;
|
|
33
|
+
/** No key clears every remembered place for that user. */
|
|
34
|
+
clear(userId: string, key?: string): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ResumeStoreOptions {
|
|
38
|
+
table?: string;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
43
|
+
const DEFAULT_KEY = 'default';
|
|
44
|
+
|
|
45
|
+
export function createSqliteResumeStore(db: Database, options: ResumeStoreOptions = {}): ResumeStore {
|
|
46
|
+
const table = options.table ?? 'cb_resume';
|
|
47
|
+
if (!IDENTIFIER.test(table)) throw new Error(`invalid resume table name: ${table}`);
|
|
48
|
+
const now = options.now ?? Date.now;
|
|
49
|
+
|
|
50
|
+
db.run(`CREATE TABLE IF NOT EXISTS ${table} (
|
|
51
|
+
user_id TEXT NOT NULL,
|
|
52
|
+
place_key TEXT NOT NULL,
|
|
53
|
+
location TEXT NOT NULL,
|
|
54
|
+
updated_at INTEGER NOT NULL,
|
|
55
|
+
PRIMARY KEY (user_id, place_key)
|
|
56
|
+
)`);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
read(userId, key = DEFAULT_KEY) {
|
|
60
|
+
const row = db
|
|
61
|
+
.query<{ location: string; updated_at: number }, [string, string]>(
|
|
62
|
+
`SELECT location, updated_at FROM ${table} WHERE user_id = ? AND place_key = ?`,
|
|
63
|
+
)
|
|
64
|
+
.get(userId, key);
|
|
65
|
+
return row ? { location: row.location, updatedAt: row.updated_at } : null;
|
|
66
|
+
},
|
|
67
|
+
write(userId, location, key = DEFAULT_KEY) {
|
|
68
|
+
// Truncated rather than refused: a place that is somehow too long should
|
|
69
|
+
// cost the reader nothing, and the next navigation overwrites it anyway.
|
|
70
|
+
const value = location.slice(0, MAX_LOCATION_LENGTH);
|
|
71
|
+
db.run(
|
|
72
|
+
`INSERT INTO ${table} (user_id, place_key, location, updated_at) VALUES (?, ?, ?, ?)
|
|
73
|
+
ON CONFLICT(user_id, place_key) DO UPDATE SET location = excluded.location, updated_at = excluded.updated_at`,
|
|
74
|
+
[userId, key, value, now()],
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
clear(userId, key) {
|
|
78
|
+
if (key === undefined) db.run(`DELETE FROM ${table} WHERE user_id = ?`, [userId]);
|
|
79
|
+
else db.run(`DELETE FROM ${table} WHERE user_id = ? AND place_key = ?`, [userId, key]);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|