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,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔴 The per-person wall's isolation properties, as tests rather than as prose.
|
|
3
|
+
*
|
|
4
|
+
* The owner's constraint when this was built (2026-08-25): *"ensure it cannot be used to
|
|
5
|
+
* bypass the password by real users or used to access any real user data"*. Every claim
|
|
6
|
+
* the design makes about that is exercised here, because a wall whose isolation is only
|
|
7
|
+
* argued for in a comment is a wall nobody can prove still holds next month.
|
|
8
|
+
*
|
|
9
|
+
* The four claims:
|
|
10
|
+
* 1. One person's unlock token does not open another person's lock — cookie or header.
|
|
11
|
+
* 2. There is no route from "signed in" to "past the wall" without a password: a new
|
|
12
|
+
* principal is LOCKED, not open, and no env/header/label changes that.
|
|
13
|
+
* 3. `enroll` creates only the password that was never set. It cannot overwrite one,
|
|
14
|
+
* cannot run outside per-person mode, and cannot be raced into overwriting one.
|
|
15
|
+
* 4. Nothing a caller sends chooses WHICH lock they are measured against.
|
|
16
|
+
*/
|
|
17
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
18
|
+
import {
|
|
19
|
+
MASTER_LOCK_HEADER,
|
|
20
|
+
MASTER_LOCK_PATHS,
|
|
21
|
+
type MasterLockKdfParams,
|
|
22
|
+
deriveMasterLockVerifier,
|
|
23
|
+
newMasterLockKdfParams,
|
|
24
|
+
} from "cursedbelt-core/master-lock";
|
|
25
|
+
import { createMasterLockGuard } from "./guard";
|
|
26
|
+
import { MasterLock, type MasterLockStore } from "./masterLock";
|
|
27
|
+
import { MASTER_LOCK_PRINCIPAL_KEY_PREFIX, MasterLockDirectory, masterLockPrincipalKey } from "./principals";
|
|
28
|
+
import { createMemoryMasterLockStore } from "./store";
|
|
29
|
+
|
|
30
|
+
const KDF: MasterLockKdfParams = {
|
|
31
|
+
v: 1,
|
|
32
|
+
alg: "PBKDF2-SHA256",
|
|
33
|
+
iter: 1,
|
|
34
|
+
salt: "AAECAwQFBgcICQoLDA0ODw",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** A key/value surface the directory writes into, plus a window on what it wrote. */
|
|
38
|
+
function memoryKv() {
|
|
39
|
+
const rows = new Map<string, string>();
|
|
40
|
+
return {
|
|
41
|
+
rows,
|
|
42
|
+
get: (key: string) => rows.get(key) ?? null,
|
|
43
|
+
set: (key: string, value: string) => void rows.set(key, value),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const OWNER = "curtwphillips@gmail.com";
|
|
48
|
+
const GUEST = "ameena.phillips@gmail.com";
|
|
49
|
+
|
|
50
|
+
let kv: ReturnType<typeof memoryKv>;
|
|
51
|
+
let directory: MasterLockDirectory;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
kv = memoryKv();
|
|
55
|
+
directory = new MasterLockDirectory({ kv });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** Enroll `principal` with `password` and return the unlock token it was handed. */
|
|
59
|
+
async function enroll(principal: string, password: string): Promise<string> {
|
|
60
|
+
const kdf = newMasterLockKdfParams();
|
|
61
|
+
const result = await directory.for(principal).enroll({
|
|
62
|
+
kdf,
|
|
63
|
+
verifier: await deriveMasterLockVerifier(password, kdf),
|
|
64
|
+
});
|
|
65
|
+
if (!result.ok) throw new Error(`enrollment refused: ${result.reason}`);
|
|
66
|
+
return result.token;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("claim 1 — one person's unlock never opens another's", () => {
|
|
70
|
+
test("the owner's token is refused by the guest's lock, cookie or header", async () => {
|
|
71
|
+
const ownerToken = await enroll(OWNER, "the owner's own password");
|
|
72
|
+
await enroll(GUEST, "a different password entirely");
|
|
73
|
+
// The guest's lock is open on the guest's OWN token, so this is a real comparison
|
|
74
|
+
// and not merely "everything is locked".
|
|
75
|
+
directory.for(GUEST).lock();
|
|
76
|
+
|
|
77
|
+
const withCookie = new Request("https://orch.example/api/board", {
|
|
78
|
+
headers: { cookie: `master_lock=${ownerToken}` },
|
|
79
|
+
});
|
|
80
|
+
const withHeader = new Request("https://orch.example/api/board", {
|
|
81
|
+
headers: { [MASTER_LOCK_HEADER]: ownerToken },
|
|
82
|
+
});
|
|
83
|
+
expect(directory.for(GUEST).presents(withCookie)).toBe(false);
|
|
84
|
+
expect(directory.for(GUEST).presents(withHeader)).toBe(false);
|
|
85
|
+
// …and it is still exactly right for the person it was minted for.
|
|
86
|
+
expect(directory.for(OWNER).presents(withCookie)).toBe(true);
|
|
87
|
+
expect(directory.for(OWNER).presents(withHeader)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("🔴 the header is a TOKEN channel, not a bypass — an invented value opens nothing", async () => {
|
|
91
|
+
await enroll(OWNER, "the owner's own password");
|
|
92
|
+
directory.for(OWNER).lock();
|
|
93
|
+
for (const forged of ["1", "true", "yes", "smoke", "", "x".repeat(32)]) {
|
|
94
|
+
const req = new Request("https://orch.example/api/board", {
|
|
95
|
+
headers: { [MASTER_LOCK_HEADER]: forged },
|
|
96
|
+
});
|
|
97
|
+
expect(directory.for(OWNER).presents(req)).toBe(false);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("locking one person leaves the other open — the unit is the person", async () => {
|
|
102
|
+
await enroll(OWNER, "the owner's own password");
|
|
103
|
+
await enroll(GUEST, "a different password entirely");
|
|
104
|
+
expect(directory.for(OWNER).unlocked).toBe(true);
|
|
105
|
+
expect(directory.for(GUEST).unlocked).toBe(true);
|
|
106
|
+
directory.for(OWNER).lock();
|
|
107
|
+
expect(directory.for(OWNER).unlocked).toBe(false);
|
|
108
|
+
expect(directory.for(GUEST).unlocked).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("rotating one person's password cannot lock the other out of their own", async () => {
|
|
112
|
+
await enroll(OWNER, "the owner's own password");
|
|
113
|
+
const guestPassword = "a different password entirely";
|
|
114
|
+
await enroll(GUEST, guestPassword);
|
|
115
|
+
const kdf = newMasterLockKdfParams();
|
|
116
|
+
const rotated = await directory.for(OWNER).change({
|
|
117
|
+
currentVerifier: await deriveMasterLockVerifier("the owner's own password", directory.for(OWNER).kdf as MasterLockKdfParams),
|
|
118
|
+
kdf,
|
|
119
|
+
verifier: await deriveMasterLockVerifier("something new", kdf),
|
|
120
|
+
});
|
|
121
|
+
expect(rotated.ok).toBe(true);
|
|
122
|
+
// The guest's still opens on the guest's unchanged password.
|
|
123
|
+
directory.for(GUEST).lock();
|
|
124
|
+
const guestKdf = directory.for(GUEST).kdf as MasterLockKdfParams;
|
|
125
|
+
const reopened = await directory
|
|
126
|
+
.for(GUEST)
|
|
127
|
+
.unlock(await deriveMasterLockVerifier(guestPassword, guestKdf));
|
|
128
|
+
expect(reopened.ok).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("🔴 claim 1b — the unlock is a TOKEN, never ambient state", () => {
|
|
133
|
+
/**
|
|
134
|
+
* Measured while building this, and the reason the guard no longer reads `unlocked` in
|
|
135
|
+
* per-person mode: with `lock.unlocked ||` in front of the token check, the owner
|
|
136
|
+
* enrolling in one browser opened the app for every OTHER browser signed in as him —
|
|
137
|
+
* his phone rode his desk's unlock, and an invented `x-master-lock` value was accepted
|
|
138
|
+
* because nothing ever looked at it. Both tests below fail if that `||` comes back.
|
|
139
|
+
*/
|
|
140
|
+
test("a second device signed in as the SAME person still has to type it", async () => {
|
|
141
|
+
await enroll(OWNER, "the owner's own password");
|
|
142
|
+
const guard = createMasterLockGuard({
|
|
143
|
+
appLabel: "Orchestrator",
|
|
144
|
+
lock: () => directory.for(OWNER),
|
|
145
|
+
});
|
|
146
|
+
// The lock IS open — the desk holds a token. This request carries none.
|
|
147
|
+
expect(directory.for(OWNER).unlocked).toBe(true);
|
|
148
|
+
const phone = await guard.handle(
|
|
149
|
+
new Request("https://orch.example/api/board", { headers: { accept: "application/json" } }),
|
|
150
|
+
);
|
|
151
|
+
expect(phone?.status).toBe(401);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("both devices stay open once BOTH have typed it — neither evicts the other", async () => {
|
|
155
|
+
const desk = await enroll(OWNER, "the owner's own password");
|
|
156
|
+
const lock = directory.for(OWNER);
|
|
157
|
+
const phone = await lock.unlock(
|
|
158
|
+
await deriveMasterLockVerifier("the owner's own password", lock.kdf as MasterLockKdfParams),
|
|
159
|
+
);
|
|
160
|
+
expect(phone.ok).toBe(true);
|
|
161
|
+
const token = phone.ok ? phone.token : "";
|
|
162
|
+
// The owner asked to use this from the desk AND a phone; an unlock that replaced
|
|
163
|
+
// the previous one would sign the other out on every sign-in.
|
|
164
|
+
expect(lock.presents(new Request("https://orch.example/", { headers: { cookie: `master_lock=${desk}` } }))).toBe(true);
|
|
165
|
+
expect(lock.presents(new Request("https://orch.example/", { headers: { cookie: `master_lock=${token}` } }))).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("the manual lock closes EVERY device that person has open", async () => {
|
|
169
|
+
const desk = await enroll(OWNER, "the owner's own password");
|
|
170
|
+
const lock = directory.for(OWNER);
|
|
171
|
+
const second = await lock.unlock(
|
|
172
|
+
await deriveMasterLockVerifier("the owner's own password", lock.kdf as MasterLockKdfParams),
|
|
173
|
+
);
|
|
174
|
+
lock.lock();
|
|
175
|
+
expect(lock.presents(new Request("https://orch.example/", { headers: { cookie: `master_lock=${desk}` } }))).toBe(false);
|
|
176
|
+
expect(
|
|
177
|
+
lock.presents(
|
|
178
|
+
new Request("https://orch.example/", {
|
|
179
|
+
headers: { cookie: `master_lock=${second.ok ? second.token : ""}` },
|
|
180
|
+
}),
|
|
181
|
+
),
|
|
182
|
+
).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("claim 2 — signed in is never enough", () => {
|
|
187
|
+
test("🔴 a principal with no record reads LOCKED, not open", () => {
|
|
188
|
+
const fresh = directory.for("somebody-brand-new");
|
|
189
|
+
expect(fresh.configured).toBe(false);
|
|
190
|
+
expect(fresh.unlocked).toBe(false);
|
|
191
|
+
expect(fresh.awaitingEnrollment).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("the app-wide mode keeps its opposite default, so collections is untouched", () => {
|
|
195
|
+
const appWide = new MasterLock({ store: createMemoryMasterLockStore(null) });
|
|
196
|
+
expect(appWide.configured).toBe(false);
|
|
197
|
+
expect(appWide.unlocked).toBe(true);
|
|
198
|
+
expect(appWide.awaitingEnrollment).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("the guard refuses every app path for an un-enrolled principal", async () => {
|
|
202
|
+
const guard = createMasterLockGuard({
|
|
203
|
+
appLabel: "Orchestrator",
|
|
204
|
+
lock: () => directory.for(GUEST),
|
|
205
|
+
});
|
|
206
|
+
const refused = await guard.handle(
|
|
207
|
+
new Request("https://orch.example/api/board", { headers: { accept: "application/json" } }),
|
|
208
|
+
);
|
|
209
|
+
expect(refused?.status).toBe(401);
|
|
210
|
+
const page = await guard.handle(
|
|
211
|
+
new Request("https://orch.example/", { headers: { accept: "text/html" } }),
|
|
212
|
+
);
|
|
213
|
+
// A document gets the choose-a-password page in place — 200, but it is the WALL,
|
|
214
|
+
// not the app: it must carry the locked marker and none of the app's HTML.
|
|
215
|
+
expect(page?.status).toBe(200);
|
|
216
|
+
expect(page?.headers.get("x-master-lock-state")).toBe("locked");
|
|
217
|
+
const html = await page?.text();
|
|
218
|
+
expect(html).toContain('data-mode="enroll"');
|
|
219
|
+
expect(html).toContain("Confirm master password");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("claim 3 — enroll only ever creates the password that was never set", () => {
|
|
224
|
+
test("🔴 it refuses once a record exists — there is no overwrite", async () => {
|
|
225
|
+
await enroll(OWNER, "the owner's own password");
|
|
226
|
+
const kdf = newMasterLockKdfParams();
|
|
227
|
+
const second = await directory.for(OWNER).enroll({
|
|
228
|
+
kdf,
|
|
229
|
+
verifier: await deriveMasterLockVerifier("an attacker's password", kdf),
|
|
230
|
+
});
|
|
231
|
+
expect(second).toEqual({ ok: false, reason: "already-configured" });
|
|
232
|
+
// And the ORIGINAL still opens it.
|
|
233
|
+
directory.for(OWNER).lock();
|
|
234
|
+
const opened = await directory
|
|
235
|
+
.for(OWNER)
|
|
236
|
+
.unlock(await deriveMasterLockVerifier("the owner's own password", directory.for(OWNER).kdf as MasterLockKdfParams));
|
|
237
|
+
expect(opened.ok).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("🔴 a second lock over the SAME row cannot enroll past the first", async () => {
|
|
241
|
+
// Two MasterLock instances over one store is what two processes — or one process
|
|
242
|
+
// that rebuilt its map — actually look like. The re-read in `enroll` is what makes
|
|
243
|
+
// the second lose, and without it the last writer would silently win.
|
|
244
|
+
const store: MasterLockStore = (() => {
|
|
245
|
+
const kvv = memoryKv();
|
|
246
|
+
const key = "shared";
|
|
247
|
+
return { read: () => kvv.get(key), write: (json) => kvv.set(key, json) };
|
|
248
|
+
})();
|
|
249
|
+
const a = new MasterLock({ store, enrollable: true });
|
|
250
|
+
const b = new MasterLock({ store, enrollable: true });
|
|
251
|
+
const first = await a.enroll({ kdf: KDF, verifier: await deriveMasterLockVerifier("first", KDF) });
|
|
252
|
+
expect(first.ok).toBe(true);
|
|
253
|
+
const second = await b.enroll({ kdf: KDF, verifier: await deriveMasterLockVerifier("second", KDF) });
|
|
254
|
+
expect(second).toEqual({ ok: false, reason: "already-configured" });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("🔴 it refuses outside per-person mode, so no app-wide lock can be armed by a POST", async () => {
|
|
258
|
+
const appWide = new MasterLock({ store: createMemoryMasterLockStore(null) });
|
|
259
|
+
const result = await appWide.enroll({
|
|
260
|
+
kdf: KDF,
|
|
261
|
+
verifier: await deriveMasterLockVerifier("anything", KDF),
|
|
262
|
+
});
|
|
263
|
+
expect(result).toEqual({ ok: false, reason: "not-enrollable" });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("a malformed submission is refused rather than stored", async () => {
|
|
267
|
+
const lock = directory.for(OWNER);
|
|
268
|
+
expect(await lock.enroll({ kdf: { v: 9 } as never, verifier: "x" })).toEqual({
|
|
269
|
+
ok: false,
|
|
270
|
+
reason: "invalid",
|
|
271
|
+
});
|
|
272
|
+
expect(await lock.enroll({ kdf: KDF, verifier: "" })).toEqual({ ok: false, reason: "invalid" });
|
|
273
|
+
expect(lock.configured).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("the route's refusal is the SAME refusal as every other — it cannot be used to probe", async () => {
|
|
277
|
+
await enroll(OWNER, "the owner's own password");
|
|
278
|
+
const guard = createMasterLockGuard({ appLabel: "Orchestrator", lock: () => directory.for(OWNER) });
|
|
279
|
+
const kdf = newMasterLockKdfParams();
|
|
280
|
+
const enrolled = await guard.handle(
|
|
281
|
+
new Request(`https://orch.example${MASTER_LOCK_PATHS.enroll}`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "content-type": "application/json" },
|
|
284
|
+
body: JSON.stringify({ kdf, verifier: await deriveMasterLockVerifier("x", kdf) }),
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
expect(enrolled?.status).toBe(401);
|
|
288
|
+
expect(await enrolled?.json()).toEqual({ error: "locked", retryAfterMs: 0 });
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("seed() — a pre-derived record for a machine account", () => {
|
|
293
|
+
/** The shape `MASTER_LOCK_SEED` carries: derived params + argon2(verifier), no password. */
|
|
294
|
+
async function seedJsonFor(password: string): Promise<string> {
|
|
295
|
+
const kdf = newMasterLockKdfParams();
|
|
296
|
+
const verifier = await deriveMasterLockVerifier(password, kdf);
|
|
297
|
+
return JSON.stringify({
|
|
298
|
+
kdf,
|
|
299
|
+
verifierHash: await Bun.password.hash(verifier, { algorithm: "argon2id" }),
|
|
300
|
+
idleMs: 300_000,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
test("arms a principal that has none, and the seeded password then unlocks it", async () => {
|
|
305
|
+
const SMOKE = "smoke@auth.local";
|
|
306
|
+
expect(directory.seed(SMOKE, await seedJsonFor("the smoke's own password"))).toBe(true);
|
|
307
|
+
const lock = directory.for(SMOKE);
|
|
308
|
+
expect(lock.configured).toBe(true);
|
|
309
|
+
// Locked until it is opened — seeding decides that an account HAS a password, never
|
|
310
|
+
// that anybody may skip it.
|
|
311
|
+
expect(lock.unlocked).toBe(false);
|
|
312
|
+
const opened = await lock.unlock(
|
|
313
|
+
await deriveMasterLockVerifier("the smoke's own password", lock.kdf as MasterLockKdfParams),
|
|
314
|
+
);
|
|
315
|
+
expect(opened.ok).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("🔴 a WRONG password still fails against a seeded record", async () => {
|
|
319
|
+
const SMOKE = "smoke@auth.local";
|
|
320
|
+
directory.seed(SMOKE, await seedJsonFor("the smoke's own password"));
|
|
321
|
+
const lock = directory.for(SMOKE);
|
|
322
|
+
const refused = await lock.unlock(
|
|
323
|
+
await deriveMasterLockVerifier("not it", lock.kdf as MasterLockKdfParams),
|
|
324
|
+
);
|
|
325
|
+
expect(refused.ok).toBe(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("🔴 it REFUSES to rotate an existing record out from under its owner", async () => {
|
|
329
|
+
await enroll(OWNER, "the owner's own password");
|
|
330
|
+
expect(directory.seed(OWNER, await seedJsonFor("an attacker's password"))).toBe(false);
|
|
331
|
+
// The original still opens it.
|
|
332
|
+
const lock = directory.for(OWNER);
|
|
333
|
+
lock.lock();
|
|
334
|
+
const opened = await lock.unlock(
|
|
335
|
+
await deriveMasterLockVerifier("the owner's own password", lock.kdf as MasterLockKdfParams),
|
|
336
|
+
);
|
|
337
|
+
expect(opened.ok).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("a malformed seed installs nothing rather than half a record", async () => {
|
|
341
|
+
expect(directory.seed("nobody@example.test", "not json")).toBe(false);
|
|
342
|
+
expect(directory.seed("nobody@example.test", JSON.stringify({ kdf: { v: 9 } }))).toBe(false);
|
|
343
|
+
expect(directory.for("nobody@example.test").configured).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("seeding one principal arms NOBODY else", async () => {
|
|
347
|
+
directory.seed("smoke@auth.local", await seedJsonFor("the smoke's own password"));
|
|
348
|
+
expect(directory.for(OWNER).configured).toBe(false);
|
|
349
|
+
expect(directory.for(GUEST).configured).toBe(false);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("claim 4 — nothing a caller sends chooses whose lock they meet", () => {
|
|
354
|
+
test("🔴 a null principal refuses /__lock/* rather than falling through", async () => {
|
|
355
|
+
const guard = createMasterLockGuard({ appLabel: "Orchestrator", lock: () => null });
|
|
356
|
+
const status = await guard.handle(new Request(`https://orch.example${MASTER_LOCK_PATHS.status}`));
|
|
357
|
+
expect(status?.status).toBe(401);
|
|
358
|
+
// An ordinary path stands aside: the app's own sign-in owns a signed-out caller.
|
|
359
|
+
expect(await guard.handle(new Request("https://orch.example/"))).toBeNull();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("the storage key is a hash, so no id can collide with or escape another's", () => {
|
|
363
|
+
const key = masterLockPrincipalKey("a:b/../c");
|
|
364
|
+
expect(key.startsWith(MASTER_LOCK_PRINCIPAL_KEY_PREFIX)).toBe(true);
|
|
365
|
+
expect(key.slice(MASTER_LOCK_PRINCIPAL_KEY_PREFIX.length)).toMatch(/^[0-9a-f]{64}$/);
|
|
366
|
+
expect(masterLockPrincipalKey(OWNER)).not.toBe(masterLockPrincipalKey(GUEST));
|
|
367
|
+
expect(masterLockPrincipalKey(OWNER)).toBe(masterLockPrincipalKey(OWNER));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("the stored row carries no principal id and no password", async () => {
|
|
371
|
+
await enroll(OWNER, "the owner's own password");
|
|
372
|
+
const [key, value] = [...kv.rows.entries()][0] as [string, string];
|
|
373
|
+
expect(key).toBe(masterLockPrincipalKey(OWNER));
|
|
374
|
+
expect(value).not.toContain(OWNER);
|
|
375
|
+
expect(value).not.toContain("the owner's own password");
|
|
376
|
+
expect(JSON.parse(value).verifierHash.startsWith("$argon2")).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PER-PERSON master locks — one record, one password and one unlock per account.
|
|
3
|
+
*
|
|
4
|
+
* ── What this changes, and what it deliberately does not ────────────────────
|
|
5
|
+
* The engine next door (`masterLock.ts`) was built for an app with exactly one user, and
|
|
6
|
+
* its module note says so at length: *"There is exactly ONE unlock at a time, not one per
|
|
7
|
+
* browser session … the unit is the SITE."* That is still true of `apps/collections` and
|
|
8
|
+
* the binary-server inspector, and this file does not touch them.
|
|
9
|
+
*
|
|
10
|
+
* It stops being true the moment a second person may open the app. The owner, 2026-08-25:
|
|
11
|
+
* *"I would pick one that only I know and use and she could pick one for only she knows
|
|
12
|
+
* just like we did for the vault app."* Two people cannot share one record — the first to
|
|
13
|
+
* rotate it locks the second out of an app they were granted — and they must not share one
|
|
14
|
+
* unlock either, or her phone opens his desk.
|
|
15
|
+
*
|
|
16
|
+
* So the unit here is the PERSON. Each principal gets its own {@link MasterLock}: its own
|
|
17
|
+
* KDF salt, its own `argon2id(verifier)`, its own idle clock, its own guess throttle and
|
|
18
|
+
* its own in-memory unlock. Everything the engine already reasons about holds per person
|
|
19
|
+
* instead of per app, unchanged.
|
|
20
|
+
*
|
|
21
|
+
* ── 🔴 Three properties this file is responsible for ────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* **1. One principal's unlock can never open another's.** Falls out of the construction —
|
|
24
|
+
* a token is compared only against the session of the lock it was minted by, and the lock
|
|
25
|
+
* is selected by the principal on the request's own signed-in session, never by anything
|
|
26
|
+
* the caller sends. There is no code path that takes a token and searches for a matching
|
|
27
|
+
* lock, and there must never be one.
|
|
28
|
+
*
|
|
29
|
+
* **2. A principal id can never reach the storage key.** Ids in this fleet are sometimes
|
|
30
|
+
* uuids and sometimes raw email addresses (`apps/auth` carries ca's originals verbatim),
|
|
31
|
+
* so they are attacker-adjacent strings of unbounded shape. They are HASHED into the key
|
|
32
|
+
* rather than interpolated: a fixed-length hex suffix cannot collide with another key,
|
|
33
|
+
* cannot escape the namespace, and cannot smuggle a `:` that makes one person's row look
|
|
34
|
+
* like another's.
|
|
35
|
+
*
|
|
36
|
+
* **3. There is no seed.** `MASTER_LOCK_SEED` arms an app-wide lock from the vault's own
|
|
37
|
+
* record; per-person there is nothing sensible to seed a *stranger's* password from, and
|
|
38
|
+
* an env var that pre-set someone's password would be exactly the "one credential opens
|
|
39
|
+
* everybody" shape this replaces. A principal with no record is `awaitingEnrollment` —
|
|
40
|
+
* locked, and offered the choose-a-password form.
|
|
41
|
+
*/
|
|
42
|
+
import { createHash } from "node:crypto";
|
|
43
|
+
import { MasterLock, type MasterLockStore, readRecord } from "./masterLock";
|
|
44
|
+
|
|
45
|
+
/** Namespace for every per-person record, so one glance at the table says what they are. */
|
|
46
|
+
export const MASTER_LOCK_PRINCIPAL_KEY_PREFIX = "master_lock:principal:";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The storage key for one principal.
|
|
50
|
+
*
|
|
51
|
+
* SHA-256 hex of the id — see property 2 in the module note. It is not a security
|
|
52
|
+
* boundary (the id is not a secret and the hash is not salted); it is a SHAPE boundary,
|
|
53
|
+
* which is the thing that was actually at risk.
|
|
54
|
+
*/
|
|
55
|
+
export function masterLockPrincipalKey(principalId: string): string {
|
|
56
|
+
const digest = createHash("sha256").update(principalId, "utf8").digest("hex");
|
|
57
|
+
return `${MASTER_LOCK_PRINCIPAL_KEY_PREFIX}${digest}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MasterLockDirectoryOptions {
|
|
61
|
+
/**
|
|
62
|
+
* The app's key/value surface. One row per principal, written under
|
|
63
|
+
* {@link masterLockPrincipalKey} — no migration, same shape as the app-wide record.
|
|
64
|
+
*/
|
|
65
|
+
kv: {
|
|
66
|
+
get(key: string): string | null | undefined;
|
|
67
|
+
set(key: string, value: string): void;
|
|
68
|
+
};
|
|
69
|
+
/** Injected clock. The tests drive it; production omits it. */
|
|
70
|
+
now?: () => number;
|
|
71
|
+
/** Where to log a rotation or a first enrollment. Receives the principal LABEL. */
|
|
72
|
+
log?: (line: string) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Every person's lock, made on first sight and held for the process's life.
|
|
77
|
+
*
|
|
78
|
+
* Locks live in memory alongside their unlock, exactly as the single-app engine's do, so
|
|
79
|
+
* a restart re-locks everybody. That is the correct default for a wall whose whole risk
|
|
80
|
+
* model is "this browser is somewhere it should not be".
|
|
81
|
+
*/
|
|
82
|
+
export class MasterLockDirectory {
|
|
83
|
+
private readonly locks = new Map<string, MasterLock>();
|
|
84
|
+
private readonly options: MasterLockDirectoryOptions;
|
|
85
|
+
|
|
86
|
+
constructor(options: MasterLockDirectoryOptions) {
|
|
87
|
+
this.options = options;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* This principal's lock, created on first sight.
|
|
92
|
+
*
|
|
93
|
+
* 🔴 `principalId` must come from the request's own verified session and never from a
|
|
94
|
+
* header, a query parameter or a body field. The guard's `resolve` is the one place
|
|
95
|
+
* that decides it; see property 1 in the module note.
|
|
96
|
+
*/
|
|
97
|
+
for(principalId: string): MasterLock {
|
|
98
|
+
const existing = this.locks.get(principalId);
|
|
99
|
+
if (existing) return existing;
|
|
100
|
+
const key = masterLockPrincipalKey(principalId);
|
|
101
|
+
const { kv } = this.options;
|
|
102
|
+
const store: MasterLockStore = {
|
|
103
|
+
read: () => kv.get(key) ?? null,
|
|
104
|
+
write: (json) => kv.set(key, json),
|
|
105
|
+
};
|
|
106
|
+
const lock = new MasterLock({
|
|
107
|
+
store,
|
|
108
|
+
enrollable: true,
|
|
109
|
+
...(this.options.now ? { now: this.options.now } : {}),
|
|
110
|
+
...(this.options.log
|
|
111
|
+
? { log: (line: string) => this.options.log?.(`[${principalId}] ${line}`) }
|
|
112
|
+
: {}),
|
|
113
|
+
});
|
|
114
|
+
this.locks.set(principalId, lock);
|
|
115
|
+
return lock;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Has this principal chosen a master password? Reads storage, so it is true of a
|
|
119
|
+
* person who enrolled in a previous boot and has not been seen in this one. */
|
|
120
|
+
isEnrolled(principalId: string): boolean {
|
|
121
|
+
return this.for(principalId).configured;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Install a PRE-DERIVED record for one principal, if and only if it has none.
|
|
126
|
+
*
|
|
127
|
+
* ── What this is for, and what it is not ───────────────────────────────────
|
|
128
|
+
* A machine account — `apps/orch`'s deploy smoke is the only one so far. Its master
|
|
129
|
+
* password lives 0600 on the owner's Mac, so there is no browser to type it in, and
|
|
130
|
+
* the alternative shapes are both worse: letting the smoke `POST /__lock/enroll` makes
|
|
131
|
+
* it WRITE to a live production site (which `scripts/smokeCleanup.test.ts` refuses, on
|
|
132
|
+
* the owner's rule), and giving the smoke a bypass makes the wall unproven.
|
|
133
|
+
*
|
|
134
|
+
* 🔴 It is NOT an opening and cannot become one. `json` is `{kdf, verifierHash}` —
|
|
135
|
+
* already derived, the same shape `MASTER_LOCK_SEED` has carried since 2026-08-15 — so
|
|
136
|
+
* no password passes through this process, and whoever unlocks still has to present a
|
|
137
|
+
* verifier that matches. Seeding an account only decides that it HAS a password, never
|
|
138
|
+
* that anybody may skip it.
|
|
139
|
+
*
|
|
140
|
+
* Refuses when a record already exists, so it can never rotate one out from under the
|
|
141
|
+
* person holding it. Returns whether it installed anything.
|
|
142
|
+
*/
|
|
143
|
+
seed(principalId: string, json: string): boolean {
|
|
144
|
+
const key = masterLockPrincipalKey(principalId);
|
|
145
|
+
const { kv } = this.options;
|
|
146
|
+
if (readRecord(kv.get(key) ?? null)) return false;
|
|
147
|
+
const record = readRecord(json);
|
|
148
|
+
if (!record) return false;
|
|
149
|
+
kv.set(key, JSON.stringify(record));
|
|
150
|
+
// Drop any lock built before the row existed, so `for()` re-reads it rather than
|
|
151
|
+
// serving the cached "unconfigured" one. Nothing can be unlocked yet by definition.
|
|
152
|
+
this.locks.delete(principalId);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Lock every open session — the "I am handing this laptop over" button, fleet-wide.
|
|
157
|
+
* Only ever reaches principals seen since boot, which is all that can be unlocked. */
|
|
158
|
+
lockAll(): void {
|
|
159
|
+
for (const lock of this.locks.values()) lock.lock();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** How many principals hold a live unlock right now. For `/healthz` and the boot log;
|
|
163
|
+
* a COUNT discloses nothing about who. */
|
|
164
|
+
unlockedCount(): number {
|
|
165
|
+
let count = 0;
|
|
166
|
+
for (const lock of this.locks.values()) if (lock.unlocked) count += 1;
|
|
167
|
+
return count;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Drop every in-memory lock — for tests between cases. */
|
|
171
|
+
resetForTest(): void {
|
|
172
|
+
for (const lock of this.locks.values()) lock.resetForTest();
|
|
173
|
+
this.locks.clear();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { deriveMasterLockVerifier } from "cursedbelt-core/master-lock/kdf";
|
|
3
|
+
import { MasterLock, readRecord } from "./masterLock";
|
|
4
|
+
import { createMemoryMasterLockStore } from "./store";
|
|
5
|
+
import { generateStagePassword, mintMasterLockSeed, serializeMasterLockSeed } from "./seed";
|
|
6
|
+
|
|
7
|
+
describe("mintMasterLockSeed", () => {
|
|
8
|
+
// The whole point: a seed minted here must UNLOCK, through the real engine,
|
|
9
|
+
// under the real derivation. Anything less is a hash that looks plausible.
|
|
10
|
+
it("produces a record the lock actually opens with that password", async () => {
|
|
11
|
+
const record = await mintMasterLockSeed("stage-password-1", { idleMs: 60_000 });
|
|
12
|
+
const lock = new MasterLock({
|
|
13
|
+
store: createMemoryMasterLockStore(),
|
|
14
|
+
seedJson: serializeMasterLockSeed(record),
|
|
15
|
+
});
|
|
16
|
+
expect(lock.configured).toBe(true);
|
|
17
|
+
const verifier = await deriveMasterLockVerifier("stage-password-1", record.kdf);
|
|
18
|
+
const result = await lock.unlock(verifier);
|
|
19
|
+
expect(result.ok).toBe(true);
|
|
20
|
+
expect(lock.unlocked).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("does not open with a different password", async () => {
|
|
24
|
+
const record = await mintMasterLockSeed("stage-password-1");
|
|
25
|
+
const lock = new MasterLock({
|
|
26
|
+
store: createMemoryMasterLockStore(),
|
|
27
|
+
seedJson: serializeMasterLockSeed(record),
|
|
28
|
+
});
|
|
29
|
+
const wrong = await deriveMasterLockVerifier("stage-password-2", record.kdf);
|
|
30
|
+
expect((await lock.unlock(wrong)).ok).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("gives every mint its own salt, so two stages are not one lock", async () => {
|
|
34
|
+
const a = await mintMasterLockSeed("stage-password-1");
|
|
35
|
+
const b = await mintMasterLockSeed("stage-password-1");
|
|
36
|
+
expect(a.kdf.salt).not.toBe(b.kdf.salt);
|
|
37
|
+
expect(a.verifierHash).not.toBe(b.verifierHash);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("refuses a password short enough to make the lock decorative", async () => {
|
|
41
|
+
await expect(mintMasterLockSeed("short")).rejects.toThrow(/at least 8/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("serializeMasterLockSeed", () => {
|
|
46
|
+
// 🔴 The argon2 hash contains `$` and the file is sourced by a shell. An
|
|
47
|
+
// unquoted value arrives with `$argon2id` expanded to nothing.
|
|
48
|
+
it("single-quotes the blob, and readRecord strips exactly that pair", async () => {
|
|
49
|
+
const record = await mintMasterLockSeed("stage-password-1");
|
|
50
|
+
const env = serializeMasterLockSeed(record);
|
|
51
|
+
expect(env.startsWith("'")).toBe(true);
|
|
52
|
+
expect(env.endsWith("'")).toBe(true);
|
|
53
|
+
expect(readRecord(env)?.verifierHash).toBe(record.verifierHash);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("generateStagePassword", () => {
|
|
58
|
+
it("avoids the characters that get misread when typed by hand", () => {
|
|
59
|
+
for (let i = 0; i < 40; i += 1) {
|
|
60
|
+
expect(generateStagePassword()).not.toMatch(/[l1O0]/);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("is long enough to seed a lock", async () => {
|
|
65
|
+
const password = generateStagePassword();
|
|
66
|
+
expect(password.length).toBeGreaterThanOrEqual(20);
|
|
67
|
+
await expect(mintMasterLockSeed(password)).resolves.toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
});
|