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,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lock page — the ONLY thing a locked app serves.
|
|
3
|
+
*
|
|
4
|
+
* ── Why it is authored here as strings ──────────────────────────────────────
|
|
5
|
+
* It has to render before any of the app exists: no bundle, no framework, no design system,
|
|
6
|
+
* no fonts, nothing fetched from anywhere. That is the requirement — *"a user logged in as
|
|
7
|
+
* me cannot see anything about the site, not even text or images, nothing at all"* — and the
|
|
8
|
+
* honest way to meet it is for the locked response to have nothing of the app in it.
|
|
9
|
+
*
|
|
10
|
+
* ── Why the CSS and JS are separate routes, not inline ──────────────────────
|
|
11
|
+
* `apps/vault`'s CSP wall forbids inline script, and any app adopting this lock deserves to
|
|
12
|
+
* keep a wall that strict. Two same-origin files cost one round trip each and let the page
|
|
13
|
+
* ship under `script-src 'self'` with no nonce plumbing.
|
|
14
|
+
*
|
|
15
|
+
* ── 🔴 The derivation in `LOCK_SCRIPT` is a HAND COPY of `deriveMasterLockVerifier` ──────
|
|
16
|
+
* It cannot import it: this file is served to a browser as plain text, outside every build.
|
|
17
|
+
* A copy that drifts derives a verifier that can never match, and the symptom would be "my
|
|
18
|
+
* correct password is refused" — the same shape of bug that cost `apps/collections` a day.
|
|
19
|
+
* So `lockPage.spec.ts` EVALUATES the script's function out of this source and asserts it
|
|
20
|
+
* agrees with the TypeScript one on a fixed vector. Change one, the spec fails until you
|
|
21
|
+
* change the other.
|
|
22
|
+
*/
|
|
23
|
+
import { MASTER_LOCK_PATHS } from "cursedbelt-core/master-lock";
|
|
24
|
+
|
|
25
|
+
/** Everything the page needs to know about the app it is standing in front of. */
|
|
26
|
+
export interface LockPageOptions {
|
|
27
|
+
/** What to call this app — "Collections", "binary-server inspector". */
|
|
28
|
+
appLabel: string;
|
|
29
|
+
/** Shown under the title. Defaults to the sentence that explains the second wall. */
|
|
30
|
+
note?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Render the CHOOSE-a-password form rather than the type-it form.
|
|
33
|
+
*
|
|
34
|
+
* Set by the guard from `lock.awaitingEnrollment`, i.e. per-person mode with no record
|
|
35
|
+
* for this account yet. It is a rendering hint and nothing more — the server refuses
|
|
36
|
+
* `POST /__lock/enroll` on its own record's state, so a page asking to enroll cannot
|
|
37
|
+
* make enrollment legal.
|
|
38
|
+
*/
|
|
39
|
+
enroll?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const escapeHtml = (value: string): string =>
|
|
43
|
+
value.replace(
|
|
44
|
+
/[&<>"']/g,
|
|
45
|
+
(ch) =>
|
|
46
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch] ?? ch,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The document. Deliberately says the app's NAME and nothing else about it — whoever is
|
|
51
|
+
* looking already reached the origin, so the hostname told them that much, and a page that
|
|
52
|
+
* would not say which app it is cannot tell the owner which password to type.
|
|
53
|
+
*/
|
|
54
|
+
export function lockPageHtml(options: LockPageOptions): string {
|
|
55
|
+
const label = escapeHtml(options.appLabel);
|
|
56
|
+
const enroll = options.enroll === true;
|
|
57
|
+
const note = escapeHtml(
|
|
58
|
+
options.note ??
|
|
59
|
+
(enroll
|
|
60
|
+
? "You are signed in, but this site needs a master password of your own. " +
|
|
61
|
+
"Choose one now. Nobody else can see it and nobody else can reset it — " +
|
|
62
|
+
"if you forget it, it cannot be recovered."
|
|
63
|
+
: "This site is locked. Signing in is not enough — enter the master password to continue."),
|
|
64
|
+
);
|
|
65
|
+
return `<!doctype html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="utf-8">
|
|
69
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
70
|
+
<meta name="robots" content="noindex, nofollow">
|
|
71
|
+
<title>Locked · ${label}</title>
|
|
72
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔒</text></svg>">
|
|
73
|
+
<link rel="stylesheet" href="${MASTER_LOCK_PATHS.style}">
|
|
74
|
+
</head>
|
|
75
|
+
<body data-mode="${enroll ? "enroll" : "unlock"}">
|
|
76
|
+
<main class="card" aria-labelledby="ml-title">
|
|
77
|
+
<div class="glyph" aria-hidden="true">${enroll ? "🔑" : "🔒"}</div>
|
|
78
|
+
<h1 id="ml-title">${label}</h1>
|
|
79
|
+
<p class="note">${note}</p>
|
|
80
|
+
<form id="ml-form" autocomplete="off">
|
|
81
|
+
<label class="field">
|
|
82
|
+
<span>${enroll ? "New master password" : "Master password"}</span>
|
|
83
|
+
<input id="ml-input" type="password" name="master-password"
|
|
84
|
+
autocomplete="${enroll ? "new-password" : "current-password"}"
|
|
85
|
+
autofocus required spellcheck="false" enterkeyhint="${enroll ? "next" : "go"}"${
|
|
86
|
+
enroll ? ' minlength="10"' : ""
|
|
87
|
+
}>
|
|
88
|
+
</label>${
|
|
89
|
+
enroll
|
|
90
|
+
? `
|
|
91
|
+
<label class="field">
|
|
92
|
+
<span>Confirm master password</span>
|
|
93
|
+
<input id="ml-confirm" type="password" name="confirm-master-password"
|
|
94
|
+
autocomplete="new-password" required spellcheck="false" enterkeyhint="go"
|
|
95
|
+
minlength="10">
|
|
96
|
+
</label>`
|
|
97
|
+
: ""
|
|
98
|
+
}
|
|
99
|
+
<button id="ml-submit" type="submit">${enroll ? "Set master password" : "Unlock"}</button>
|
|
100
|
+
</form>
|
|
101
|
+
<p id="ml-status" class="status" role="status" aria-live="polite"></p>
|
|
102
|
+
</main>
|
|
103
|
+
<script type="module" src="${MASTER_LOCK_PATHS.script}"></script>
|
|
104
|
+
</body>
|
|
105
|
+
</html>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The stylesheet. Follows the viewer's theme and commits to nothing else. */
|
|
110
|
+
export const LOCK_STYLE = `:root{color-scheme:light dark;
|
|
111
|
+
--ml-bg:#f4f5f7;--ml-card:#ffffff;--ml-text:#14171c;--ml-muted:#69707c;--ml-border:#d9dde3;
|
|
112
|
+
--ml-accent:#1f6feb;--ml-accent-text:#ffffff;--ml-danger:#b42318;
|
|
113
|
+
--ml-shadow:0 18px 48px rgba(16,18,22,.14)}
|
|
114
|
+
@media (prefers-color-scheme:dark){:root{
|
|
115
|
+
--ml-bg:#0d0f13;--ml-card:#171a20;--ml-text:#e7eaef;--ml-muted:#8b93a1;--ml-border:#2b3039;
|
|
116
|
+
--ml-accent:#4f8cf7;--ml-accent-text:#0b0d11;--ml-danger:#f97066;
|
|
117
|
+
--ml-shadow:0 18px 48px rgba(0,0,0,.55)}}
|
|
118
|
+
*{box-sizing:border-box}
|
|
119
|
+
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;
|
|
120
|
+
background:var(--ml-bg);color:var(--ml-text);
|
|
121
|
+
font:15px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
122
|
+
.card{width:100%;max-width:23rem;background:var(--ml-card);border:1px solid var(--ml-border);
|
|
123
|
+
border-radius:16px;padding:28px 26px 24px;box-shadow:var(--ml-shadow);text-align:center}
|
|
124
|
+
.glyph{font-size:30px;line-height:1;margin-bottom:10px}
|
|
125
|
+
h1{margin:0 0 6px;font-size:18px;font-weight:650;letter-spacing:-.01em}
|
|
126
|
+
.note{margin:0 0 20px;font-size:13px;color:var(--ml-muted)}
|
|
127
|
+
.field{display:block;text-align:left;margin-bottom:14px}
|
|
128
|
+
.field span{display:block;font-size:12px;font-weight:600;color:var(--ml-muted);margin-bottom:6px}
|
|
129
|
+
input{width:100%;height:40px;padding:0 12px;border:1px solid var(--ml-border);border-radius:9px;
|
|
130
|
+
background:var(--ml-bg);color:inherit;font:inherit}
|
|
131
|
+
input:focus{outline:2px solid var(--ml-accent);outline-offset:1px;border-color:transparent}
|
|
132
|
+
button{width:100%;height:40px;border:0;border-radius:9px;background:var(--ml-accent);
|
|
133
|
+
color:var(--ml-accent-text);font:inherit;font-weight:600;cursor:pointer}
|
|
134
|
+
button[disabled]{opacity:.6;cursor:progress}
|
|
135
|
+
.status{margin:14px 0 0;min-height:1.2em;font-size:12.5px;color:var(--ml-muted)}
|
|
136
|
+
.status[data-tone="error"]{color:var(--ml-danger)}
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The page's script.
|
|
141
|
+
*
|
|
142
|
+
* Derives in the browser and sends only the verifier — the password itself never crosses
|
|
143
|
+
* the network, which is what makes "saved nowhere" true rather than aspirational.
|
|
144
|
+
*
|
|
145
|
+
* 600,000 PBKDF2 rounds take a beat on a phone, so the button says so; a page that looks
|
|
146
|
+
* frozen gets clicked again, and every extra click is another 600k rounds.
|
|
147
|
+
*/
|
|
148
|
+
export const LOCK_SCRIPT = `const paths = ${JSON.stringify(MASTER_LOCK_PATHS)};
|
|
149
|
+
|
|
150
|
+
const form = document.getElementById("ml-form");
|
|
151
|
+
const input = document.getElementById("ml-input");
|
|
152
|
+
const confirmInput = document.getElementById("ml-confirm");
|
|
153
|
+
const submit = document.getElementById("ml-submit");
|
|
154
|
+
const status = document.getElementById("ml-status");
|
|
155
|
+
|
|
156
|
+
function say(message, tone) {
|
|
157
|
+
status.textContent = message;
|
|
158
|
+
if (tone) status.setAttribute("data-tone", tone);
|
|
159
|
+
else status.removeAttribute("data-tone");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── derivation ───────────────────────────────────────────────────────────────
|
|
163
|
+
// 🔴 Byte-identical to deriveMasterLockVerifier in cursedbelt-core/master-lock. Pinned by
|
|
164
|
+
// lockPage.spec.ts, which evaluates THIS function and compares. Do not "tidy" it apart.
|
|
165
|
+
function unb64u(text) {
|
|
166
|
+
const padded = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
167
|
+
const binary = atob(padded + "=".repeat((4 - (padded.length % 4)) % 4));
|
|
168
|
+
const out = new Uint8Array(binary.length);
|
|
169
|
+
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function b64u(bytes) {
|
|
173
|
+
let binary = "";
|
|
174
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
175
|
+
return btoa(binary).replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=+$/, "");
|
|
176
|
+
}
|
|
177
|
+
async function pbkdf2(material, salt, iterations) {
|
|
178
|
+
const key = await crypto.subtle.importKey("raw", material, "PBKDF2", false, ["deriveBits"]);
|
|
179
|
+
const bits = await crypto.subtle.deriveBits(
|
|
180
|
+
{ name: "PBKDF2", salt: salt, iterations: iterations, hash: "SHA-256" }, key, 256);
|
|
181
|
+
return new Uint8Array(bits);
|
|
182
|
+
}
|
|
183
|
+
async function deriveVerifier(password, kdf) {
|
|
184
|
+
const salt = unb64u(kdf.salt);
|
|
185
|
+
const bytes = new TextEncoder().encode(password);
|
|
186
|
+
const kek = await pbkdf2(bytes, salt, kdf.iter);
|
|
187
|
+
const joined = new Uint8Array(kek.length + bytes.length);
|
|
188
|
+
joined.set(kek, 0);
|
|
189
|
+
joined.set(bytes, kek.length);
|
|
190
|
+
return b64u(await pbkdf2(joined, salt, 1));
|
|
191
|
+
}
|
|
192
|
+
globalThis.__masterLockDerive = deriveVerifier;
|
|
193
|
+
|
|
194
|
+
// ── the page ─────────────────────────────────────────────────────────────────
|
|
195
|
+
// 🔴 Everything below this line touches \`document\` beyond \`getElementById\`, and
|
|
196
|
+
// lockPage.spec.ts cuts the source at the marker above to evaluate the derivation with a
|
|
197
|
+
// stub. Keep page state here, not in the prelude, or that test throws instead of running.
|
|
198
|
+
//
|
|
199
|
+
// Rendered by the server from THIS principal's own record. The server re-decides it on
|
|
200
|
+
// every POST, so this only chooses which form to submit — it cannot grant anything.
|
|
201
|
+
const enrolling = document.body.dataset.mode === "enroll";
|
|
202
|
+
|
|
203
|
+
/** The shortest password worth calling one. Matched by the input's own minlength, and
|
|
204
|
+
* checked here too because a pasted value can beat the constraint on some engines. */
|
|
205
|
+
const MIN_LENGTH = 10;
|
|
206
|
+
|
|
207
|
+
let kdf = null;
|
|
208
|
+
let busy = false;
|
|
209
|
+
|
|
210
|
+
async function loadStatus() {
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetch(paths.status, { credentials: "same-origin" });
|
|
213
|
+
if (!res.ok) return;
|
|
214
|
+
const body = await res.json();
|
|
215
|
+
kdf = body.kdf;
|
|
216
|
+
if (body.enrollable) { /* the choose-a-password form is already rendered */ }
|
|
217
|
+
else if (!body.configured) say("No master password is set for this app.", "error");
|
|
218
|
+
else if (body.retryAfterMs > 0) countdown(body.retryAfterMs);
|
|
219
|
+
else if (!body.locked) location.reload();
|
|
220
|
+
} catch { /* offline: the submit below reports it */ }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let timer = null;
|
|
224
|
+
function countdown(ms) {
|
|
225
|
+
if (timer) clearInterval(timer);
|
|
226
|
+
let left = Math.ceil(ms / 1000);
|
|
227
|
+
const tick = () => {
|
|
228
|
+
if (left <= 0) { clearInterval(timer); timer = null; say(""); submit.disabled = false; return; }
|
|
229
|
+
say("Too many attempts. Try again in " + left + "s.", "error");
|
|
230
|
+
left -= 1;
|
|
231
|
+
};
|
|
232
|
+
submit.disabled = true;
|
|
233
|
+
tick();
|
|
234
|
+
timer = setInterval(tick, 1000);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Fresh params for a password nobody has chosen before. Byte-identical to
|
|
238
|
+
* newMasterLockKdfParams — pinned by lockPage.spec.ts alongside the derivation. */
|
|
239
|
+
function newKdf() {
|
|
240
|
+
const salt = new Uint8Array(16);
|
|
241
|
+
crypto.getRandomValues(salt);
|
|
242
|
+
return { v: 1, alg: "PBKDF2-SHA256", iter: 600000, salt: b64u(salt) };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function doEnroll(password) {
|
|
246
|
+
const params = newKdf();
|
|
247
|
+
const verifier = await deriveVerifier(password, params);
|
|
248
|
+
const res = await fetch(paths.enroll, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
credentials: "same-origin",
|
|
251
|
+
headers: { "content-type": "application/json" },
|
|
252
|
+
body: JSON.stringify({ kdf: params, verifier: verifier }),
|
|
253
|
+
});
|
|
254
|
+
if (res.ok) { say("Master password set."); location.reload(); return true; }
|
|
255
|
+
// The server collapses every enrollment refusal into one, so this cannot say more
|
|
256
|
+
// than "reload and see" without inventing a distinction the server refused to draw.
|
|
257
|
+
say("This site could not accept that right now. Reload and try again.", "error");
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function doUnlock(password) {
|
|
262
|
+
const verifier = await deriveVerifier(password, kdf);
|
|
263
|
+
const res = await fetch(paths.unlock, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
credentials: "same-origin",
|
|
266
|
+
headers: { "content-type": "application/json" },
|
|
267
|
+
body: JSON.stringify({ verifier: verifier }),
|
|
268
|
+
});
|
|
269
|
+
if (res.ok) { say("Unlocked."); location.reload(); return true; }
|
|
270
|
+
const body = await res.json().catch(() => ({}));
|
|
271
|
+
input.value = "";
|
|
272
|
+
input.focus();
|
|
273
|
+
if (body && body.retryAfterMs > 0) { countdown(body.retryAfterMs); return false; }
|
|
274
|
+
say("That is not the master password.", "error");
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
form.addEventListener("submit", async (event) => {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
if (busy || submit.disabled) return;
|
|
281
|
+
const password = input.value;
|
|
282
|
+
if (!password) return;
|
|
283
|
+
if (enrolling) {
|
|
284
|
+
if (password.length < MIN_LENGTH) {
|
|
285
|
+
say("Use at least " + MIN_LENGTH + " characters.", "error");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (confirmInput && confirmInput.value !== password) {
|
|
289
|
+
say("Those two do not match.", "error");
|
|
290
|
+
confirmInput.value = "";
|
|
291
|
+
confirmInput.focus();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
} else if (!kdf) {
|
|
295
|
+
await loadStatus();
|
|
296
|
+
if (!kdf) { say("Cannot reach this site right now.", "error"); return; }
|
|
297
|
+
}
|
|
298
|
+
busy = true;
|
|
299
|
+
submit.disabled = true;
|
|
300
|
+
say(enrolling ? "Setting…" : "Checking…");
|
|
301
|
+
try {
|
|
302
|
+
const done = enrolling ? await doEnroll(password) : await doUnlock(password);
|
|
303
|
+
if (!done) submit.disabled = false;
|
|
304
|
+
} catch {
|
|
305
|
+
say("Cannot reach this site right now.", "error");
|
|
306
|
+
submit.disabled = false;
|
|
307
|
+
} finally {
|
|
308
|
+
busy = false;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
loadStatus();
|
|
313
|
+
`;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine: seeding, the idle clock, the throttle, rotation.
|
|
3
|
+
*
|
|
4
|
+
* Real argon2id runs on every unlock here (that is the thing being tested), so this leans on
|
|
5
|
+
* the repo's per-test budget rather than trying to be fast.
|
|
6
|
+
*/
|
|
7
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_IDLE_MS,
|
|
10
|
+
MAX_IDLE_MS,
|
|
11
|
+
MIN_IDLE_MS,
|
|
12
|
+
type MasterLockKdfParams,
|
|
13
|
+
deriveMasterLockVerifier,
|
|
14
|
+
newMasterLockKdfParams,
|
|
15
|
+
} from "cursedbelt-core/master-lock";
|
|
16
|
+
import { MasterLock, readRecord } from "./masterLock";
|
|
17
|
+
import { createMemoryMasterLockStore } from "./store";
|
|
18
|
+
|
|
19
|
+
const KDF: MasterLockKdfParams = {
|
|
20
|
+
v: 1,
|
|
21
|
+
alg: "PBKDF2-SHA256",
|
|
22
|
+
iter: 1, // 600k rounds x N tests is minutes; the ROUND COUNT is pinned in kdf.spec.ts
|
|
23
|
+
salt: "AAECAwQFBgcICQoLDA0ODw",
|
|
24
|
+
};
|
|
25
|
+
const PASSWORD = "the owner's vault master password";
|
|
26
|
+
|
|
27
|
+
let VERIFIER = "";
|
|
28
|
+
let SEED = "";
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
if (VERIFIER) return;
|
|
32
|
+
VERIFIER = await deriveMasterLockVerifier(PASSWORD, KDF);
|
|
33
|
+
SEED = JSON.stringify({
|
|
34
|
+
kdf: KDF,
|
|
35
|
+
verifierHash: await Bun.password.hash(VERIFIER, { algorithm: "argon2id" }),
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** A lock with a fake clock the test drives. */
|
|
40
|
+
function build(options: { seed?: string | null; stored?: string | null; now?: () => number } = {}) {
|
|
41
|
+
const store = createMemoryMasterLockStore(options.stored ?? null);
|
|
42
|
+
const locked: boolean[] = [];
|
|
43
|
+
const lock = new MasterLock({
|
|
44
|
+
store,
|
|
45
|
+
seedJson: options.seed === undefined ? SEED : options.seed,
|
|
46
|
+
...(options.now ? { now: options.now } : {}),
|
|
47
|
+
onLockedChange: (value) => locked.push(value),
|
|
48
|
+
});
|
|
49
|
+
return { lock, store, locked };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const req = (token?: string): Request =>
|
|
53
|
+
new Request("http://x/", token ? { headers: { cookie: `master_lock=${token}` } } : undefined);
|
|
54
|
+
|
|
55
|
+
describe("seeding", () => {
|
|
56
|
+
test("adopts MASTER_LOCK_SEED when the store is empty, and persists it", () => {
|
|
57
|
+
const { lock, store } = build();
|
|
58
|
+
expect(lock.configured).toBe(true);
|
|
59
|
+
expect(store.read()).toBeTruthy();
|
|
60
|
+
expect(readRecord(store.read())?.kdf.salt).toBe(KDF.salt);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a STORED record wins over the seed — a rotation is permanent", async () => {
|
|
64
|
+
const otherKdf = newMasterLockKdfParams();
|
|
65
|
+
const stored = JSON.stringify({
|
|
66
|
+
kdf: otherKdf,
|
|
67
|
+
verifierHash: await Bun.password.hash("whatever", { algorithm: "argon2id" }),
|
|
68
|
+
idleMs: 90_000,
|
|
69
|
+
});
|
|
70
|
+
const { lock } = build({ stored });
|
|
71
|
+
expect(lock.kdf?.salt).toBe(otherKdf.salt);
|
|
72
|
+
expect(lock.idleMs).toBe(90_000);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("an unusable seed leaves the lock unconfigured rather than half-armed", () => {
|
|
76
|
+
const { lock } = build({ seed: '{"kdf":{"v":1},"verifierHash":"nope"}' });
|
|
77
|
+
expect(lock.configured).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("🔴 an UNCONFIGURED app reads as unlocked — a lock nobody can open is not protection", () => {
|
|
81
|
+
const { lock } = build({ seed: null });
|
|
82
|
+
expect(lock.configured).toBe(false);
|
|
83
|
+
expect(lock.unlocked).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a seed whose hash arrived shell-quoted is repaired, not refused", async () => {
|
|
87
|
+
const hash = await Bun.password.hash(VERIFIER, { algorithm: "argon2id" });
|
|
88
|
+
const { lock } = build({ seed: JSON.stringify({ kdf: KDF, verifierHash: `'${hash}'` }) });
|
|
89
|
+
expect(lock.configured).toBe(true);
|
|
90
|
+
expect((await lock.unlock(VERIFIER)).ok).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("🔴 a seed whose WHOLE BLOB arrived shell-quoted is repaired too", async () => {
|
|
94
|
+
// The value is written single-quoted into a file a shell sources, because an argon2id
|
|
95
|
+
// hash contains `$`. Every reader in the fleet strips that pair — and one of them was
|
|
96
|
+
// wrong once, for a day, in production. Both shapes have to work.
|
|
97
|
+
const { lock } = build({ seed: `'${SEED}'` });
|
|
98
|
+
expect(lock.configured).toBe(true);
|
|
99
|
+
expect((await lock.unlock(VERIFIER)).ok).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("unlock", () => {
|
|
104
|
+
test("the right verifier opens it; the wrong one does not", async () => {
|
|
105
|
+
const { lock, locked } = build();
|
|
106
|
+
expect(lock.unlocked).toBe(false);
|
|
107
|
+
expect((await lock.unlock("not it")).ok).toBe(false);
|
|
108
|
+
expect(lock.unlocked).toBe(false);
|
|
109
|
+
|
|
110
|
+
const result = await lock.unlock(VERIFIER);
|
|
111
|
+
expect(result.ok).toBe(true);
|
|
112
|
+
expect(lock.unlocked).toBe(true);
|
|
113
|
+
expect(locked).toEqual([false]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("the minted token is what `presents` recognises, and nothing else is", async () => {
|
|
117
|
+
const { lock } = build();
|
|
118
|
+
const result = await lock.unlock(VERIFIER);
|
|
119
|
+
if (!result.ok) throw new Error("expected an unlock");
|
|
120
|
+
expect(lock.presents(req(result.token))).toBe(true);
|
|
121
|
+
expect(lock.presents(req("a-different-token-of-the-same-length"))).toBe(false);
|
|
122
|
+
expect(lock.presents(req())).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a hash this build cannot parse answers 'no' without counting a failure", async () => {
|
|
126
|
+
const { lock } = build({
|
|
127
|
+
seed: JSON.stringify({ kdf: KDF, verifierHash: "$argon2id$this-is-not-a-hash" }),
|
|
128
|
+
});
|
|
129
|
+
for (let i = 0; i < 8; i += 1) expect((await lock.unlock(VERIFIER)).ok).toBe(false);
|
|
130
|
+
// Never throttled: the owner is not guessing, the machine is broken.
|
|
131
|
+
expect(lock.status().retryAfterMs).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("the idle clock", () => {
|
|
136
|
+
test("🔴 an ordinary request does NOT slide it — only interaction does", async () => {
|
|
137
|
+
let now = 1_000_000;
|
|
138
|
+
const { lock } = build({ now: () => now });
|
|
139
|
+
const result = await lock.unlock(VERIFIER);
|
|
140
|
+
if (!result.ok) throw new Error("expected an unlock");
|
|
141
|
+
const token = result.token;
|
|
142
|
+
|
|
143
|
+
// Nine minutes of a page polling every thirty seconds, with a real read of the lock
|
|
144
|
+
// state on every tick — i.e. exactly what react-query does to an idle tab. A clock
|
|
145
|
+
// slid by traffic would still be open at the end; the assertion is that it is not.
|
|
146
|
+
const start = now;
|
|
147
|
+
let sawLocked = false;
|
|
148
|
+
for (let i = 0; i < 18; i += 1) {
|
|
149
|
+
now += 30_000;
|
|
150
|
+
const open = lock.presents(req(token));
|
|
151
|
+
expect(open).toBe(now - start <= DEFAULT_IDLE_MS);
|
|
152
|
+
sawLocked ||= !open;
|
|
153
|
+
}
|
|
154
|
+
expect(sawLocked).toBe(true);
|
|
155
|
+
expect(lock.unlocked).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("touch() slides it, so an active session never lapses", async () => {
|
|
159
|
+
let now = 1_000_000;
|
|
160
|
+
const { lock } = build({ now: () => now });
|
|
161
|
+
const result = await lock.unlock(VERIFIER);
|
|
162
|
+
if (!result.ok) throw new Error("expected an unlock");
|
|
163
|
+
const token = result.token;
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i < 20; i += 1) {
|
|
166
|
+
now += DEFAULT_IDLE_MS - 1_000;
|
|
167
|
+
expect(lock.touch(req(token))).toBe(true);
|
|
168
|
+
}
|
|
169
|
+
expect(lock.unlocked).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("🔴 touch() never REVIVES a lapsed unlock", async () => {
|
|
173
|
+
let now = 1_000_000;
|
|
174
|
+
const { lock } = build({ now: () => now });
|
|
175
|
+
const result = await lock.unlock(VERIFIER);
|
|
176
|
+
if (!result.ok) throw new Error("expected an unlock");
|
|
177
|
+
now += DEFAULT_IDLE_MS + 1;
|
|
178
|
+
expect(lock.touch(req(result.token))).toBe(false);
|
|
179
|
+
expect(lock.unlocked).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("lapsing fires onLockedChange, so a route swap can follow it", async () => {
|
|
183
|
+
let now = 1_000_000;
|
|
184
|
+
const { lock, locked } = build({ now: () => now });
|
|
185
|
+
await lock.unlock(VERIFIER);
|
|
186
|
+
now += DEFAULT_IDLE_MS + 1;
|
|
187
|
+
expect(lock.unlocked).toBe(false);
|
|
188
|
+
expect(locked).toEqual([false, true]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("the manual lock closes it at once", async () => {
|
|
192
|
+
const { lock, locked } = build();
|
|
193
|
+
await lock.unlock(VERIFIER);
|
|
194
|
+
lock.lock();
|
|
195
|
+
expect(lock.unlocked).toBe(false);
|
|
196
|
+
expect(locked).toEqual([false, true]);
|
|
197
|
+
// Idempotent — a second lock is not a second event.
|
|
198
|
+
lock.lock();
|
|
199
|
+
expect(locked).toEqual([false, true]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("setIdleMs clamps and persists", () => {
|
|
203
|
+
const { lock, store } = build();
|
|
204
|
+
expect(lock.setIdleMs(60_000)).toBe(60_000);
|
|
205
|
+
expect(readRecord(store.read())?.idleMs).toBe(60_000);
|
|
206
|
+
expect(lock.setIdleMs(1)).toBe(MIN_IDLE_MS);
|
|
207
|
+
expect(lock.setIdleMs(999 * 60 * 60_000)).toBe(MAX_IDLE_MS);
|
|
208
|
+
expect(lock.setIdleMs(Number.NaN)).toBe(DEFAULT_IDLE_MS);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 🔴 The two tests below declare their own budget, and it is not padding.
|
|
214
|
+
*
|
|
215
|
+
* Every `unlock()` runs a real argon2id verification — that IS the thing under test — and
|
|
216
|
+
* these two are the only ones in the file that do it in bulk: 9 attempts and 17 attempts.
|
|
217
|
+
* Measured 2026-09-13 on this machine: 4.5s and 7.2s against bun's DEFAULT per-test budget
|
|
218
|
+
* of 5000ms, so the second failed on every run and the first was one hiccup behind it. The
|
|
219
|
+
* file's header says it "leans on the repo's per-test budget"; this repo's `bunfig.toml`
|
|
220
|
+
* declares none, so that budget is 5s and the claim was simply false.
|
|
221
|
+
*
|
|
222
|
+
* Stated per test rather than raised in `bunfig.toml`, deliberately: a repo-wide 20s would
|
|
223
|
+
* buy these two nothing the line below does not, and would cost all 5,200 other tests the
|
|
224
|
+
* fast failure that tells you something is HUNG rather than slow.
|
|
225
|
+
*/
|
|
226
|
+
const KDF_BULK_MS = 20_000;
|
|
227
|
+
|
|
228
|
+
describe("the guess throttle", () => {
|
|
229
|
+
test("five failures are free, the sixth costs a wait, and a success clears it", async () => {
|
|
230
|
+
let now = 1_000_000;
|
|
231
|
+
const { lock } = build({ now: () => now });
|
|
232
|
+
for (let i = 0; i < 5; i += 1) {
|
|
233
|
+
expect((await lock.unlock("wrong")).ok).toBe(false);
|
|
234
|
+
expect(lock.status().retryAfterMs).toBe(0);
|
|
235
|
+
}
|
|
236
|
+
const sixth = await lock.unlock("wrong");
|
|
237
|
+
expect(sixth.ok).toBe(false);
|
|
238
|
+
expect(sixth.ok === false && sixth.retryAfterMs).toBe(30_000);
|
|
239
|
+
|
|
240
|
+
// 🔴 Inside the window even the CORRECT verifier is refused — otherwise the throttle
|
|
241
|
+
// would be an oracle that says "that one was right".
|
|
242
|
+
const early = await lock.unlock(VERIFIER);
|
|
243
|
+
expect(early.ok).toBe(false);
|
|
244
|
+
expect(lock.unlocked).toBe(false);
|
|
245
|
+
|
|
246
|
+
now += 30_001;
|
|
247
|
+
expect((await lock.unlock(VERIFIER)).ok).toBe(true);
|
|
248
|
+
expect(lock.status().retryAfterMs).toBe(0);
|
|
249
|
+
}, KDF_BULK_MS);
|
|
250
|
+
|
|
251
|
+
test("a windowed refusal does not advance the counter", async () => {
|
|
252
|
+
let now = 1_000_000;
|
|
253
|
+
const { lock } = build({ now: () => now });
|
|
254
|
+
for (let i = 0; i < 6; i += 1) await lock.unlock("wrong");
|
|
255
|
+
for (let i = 0; i < 10; i += 1) await lock.unlock("wrong");
|
|
256
|
+
now += 30_001;
|
|
257
|
+
// Still the 6th failure's delay, not the 16th's — the hammering bought nothing and
|
|
258
|
+
// cost nothing.
|
|
259
|
+
const next = await lock.unlock("wrong");
|
|
260
|
+
expect(next.ok === false && next.retryAfterMs).toBe(60_000);
|
|
261
|
+
}, KDF_BULK_MS);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("rotation", () => {
|
|
265
|
+
test("needs the current password, rewrites the record, and locks the site", async () => {
|
|
266
|
+
const { lock, store, locked } = build();
|
|
267
|
+
await lock.unlock(VERIFIER);
|
|
268
|
+
|
|
269
|
+
const nextKdf = newMasterLockKdfParams();
|
|
270
|
+
const nextVerifier = await deriveMasterLockVerifier("a different one", nextKdf);
|
|
271
|
+
|
|
272
|
+
const refused = await lock.change({
|
|
273
|
+
currentVerifier: "wrong",
|
|
274
|
+
kdf: nextKdf,
|
|
275
|
+
verifier: nextVerifier,
|
|
276
|
+
});
|
|
277
|
+
expect(refused).toEqual({ ok: false, reason: "wrong" });
|
|
278
|
+
expect(lock.unlocked).toBe(true);
|
|
279
|
+
|
|
280
|
+
const done = await lock.change({
|
|
281
|
+
currentVerifier: VERIFIER,
|
|
282
|
+
kdf: nextKdf,
|
|
283
|
+
verifier: nextVerifier,
|
|
284
|
+
});
|
|
285
|
+
expect(done).toEqual({ ok: true });
|
|
286
|
+
// 🔴 Every open page dies with the old password.
|
|
287
|
+
expect(lock.unlocked).toBe(false);
|
|
288
|
+
expect(locked.at(-1)).toBe(true);
|
|
289
|
+
|
|
290
|
+
// The OLD one no longer opens it; the new one does.
|
|
291
|
+
expect((await lock.unlock(VERIFIER)).ok).toBe(false);
|
|
292
|
+
expect((await lock.unlock(nextVerifier)).ok).toBe(true);
|
|
293
|
+
expect(readRecord(store.read())?.kdf.salt).toBe(nextKdf.salt);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("refuses params it could not derive under, before touching the record", async () => {
|
|
297
|
+
const { lock, store } = build();
|
|
298
|
+
const before = store.read();
|
|
299
|
+
const result = await lock.change({
|
|
300
|
+
currentVerifier: VERIFIER,
|
|
301
|
+
kdf: { v: 2 } as unknown as MasterLockKdfParams,
|
|
302
|
+
verifier: "x",
|
|
303
|
+
});
|
|
304
|
+
expect(result).toEqual({ ok: false, reason: "invalid" });
|
|
305
|
+
expect(store.read()).toBe(before);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("keeps this app's idle timeout across a rotation", async () => {
|
|
309
|
+
const { lock, store } = build();
|
|
310
|
+
lock.setIdleMs(120_000);
|
|
311
|
+
await lock.unlock(VERIFIER);
|
|
312
|
+
const nextKdf = newMasterLockKdfParams();
|
|
313
|
+
await lock.change({
|
|
314
|
+
currentVerifier: VERIFIER,
|
|
315
|
+
kdf: nextKdf,
|
|
316
|
+
verifier: await deriveMasterLockVerifier("next", nextKdf),
|
|
317
|
+
});
|
|
318
|
+
expect(readRecord(store.read())?.idleMs).toBe(120_000);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("readRecord", () => {
|
|
323
|
+
test("rejects everything that is not a usable record", () => {
|
|
324
|
+
expect(readRecord(null)).toBeNull();
|
|
325
|
+
expect(readRecord("")).toBeNull();
|
|
326
|
+
expect(readRecord("not json")).toBeNull();
|
|
327
|
+
expect(readRecord("[]")).toBeNull();
|
|
328
|
+
expect(readRecord(JSON.stringify({ kdf: KDF }))).toBeNull();
|
|
329
|
+
expect(readRecord(JSON.stringify({ kdf: KDF, verifierHash: "plaintext" }))).toBeNull();
|
|
330
|
+
expect(readRecord(JSON.stringify({ verifierHash: "$argon2id$x" }))).toBeNull();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("defaults a missing or absurd idleMs rather than inheriting it", () => {
|
|
334
|
+
const base = { kdf: KDF, verifierHash: "$argon2id$v=19$m=1,t=1,p=1$c2FsdA$aGFzaA" };
|
|
335
|
+
expect(readRecord(JSON.stringify(base))?.idleMs).toBe(DEFAULT_IDLE_MS);
|
|
336
|
+
expect(readRecord(JSON.stringify({ ...base, idleMs: -5 }))?.idleMs).toBe(MIN_IDLE_MS);
|
|
337
|
+
});
|
|
338
|
+
});
|