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,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault item at-rest crypto — standard Web Crypto (HKDF-SHA256 + AES-256-GCM),
|
|
3
|
+
* replacing the old hand-rolled `secretBox`/`deriveKey` scheme. Auditable, native on
|
|
4
|
+
* modern Bun, zero dependencies. These primitives live in cursedbelt (not ca's
|
|
5
|
+
* business layer) because ru gains a vault too.
|
|
6
|
+
*
|
|
7
|
+
* Keying is SERVER-SECRET–derived (per user), NOT master-password–derived: the
|
|
8
|
+
* master password is a separate argon2id gate (a `Bun.password` hash — see
|
|
9
|
+
* {@link ./password}), so resetting it never requires re-encrypting vault data. The
|
|
10
|
+
* envelope carries a version byte so a future re-key can proceed row-by-row without
|
|
11
|
+
* a flag-day migration.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const VERSION = 1;
|
|
15
|
+
const IV_BYTES = 12; // 96-bit nonce — the AES-GCM standard
|
|
16
|
+
const enc = new TextEncoder();
|
|
17
|
+
const dec = new TextDecoder();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Derive a per-user AES-256-GCM vault key from the server secret via HKDF. The
|
|
21
|
+
* per-user `salt` domain-separates users; `info` domain-separates this use of the
|
|
22
|
+
* secret. The returned key is non-extractable (encrypt/decrypt only).
|
|
23
|
+
*/
|
|
24
|
+
export async function deriveVaultKey(serverSecret: string, userId: string): Promise<CryptoKey> {
|
|
25
|
+
const baseKey = await crypto.subtle.importKey(
|
|
26
|
+
'raw',
|
|
27
|
+
enc.encode(serverSecret),
|
|
28
|
+
{ name: 'HKDF' },
|
|
29
|
+
false,
|
|
30
|
+
['deriveKey'],
|
|
31
|
+
);
|
|
32
|
+
return crypto.subtle.deriveKey(
|
|
33
|
+
{
|
|
34
|
+
name: 'HKDF',
|
|
35
|
+
hash: 'SHA-256',
|
|
36
|
+
salt: enc.encode(`vault:${userId}`),
|
|
37
|
+
info: enc.encode('vault-item-key'),
|
|
38
|
+
},
|
|
39
|
+
baseKey,
|
|
40
|
+
{ name: 'AES-GCM', length: 256 },
|
|
41
|
+
false,
|
|
42
|
+
['encrypt', 'decrypt'],
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Encrypt vault item plaintext. Returns a base64 envelope:
|
|
48
|
+
* `version(1B) || iv(12B) || ciphertext(+GCM tag)`
|
|
49
|
+
* A fresh random IV per call. Store the string verbatim in `vault_items.data`.
|
|
50
|
+
*/
|
|
51
|
+
export async function encryptVaultItem(key: CryptoKey, plaintext: string): Promise<string> {
|
|
52
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
53
|
+
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plaintext));
|
|
54
|
+
const envelope = new Uint8Array(1 + IV_BYTES + ct.byteLength);
|
|
55
|
+
envelope[0] = VERSION;
|
|
56
|
+
envelope.set(iv, 1);
|
|
57
|
+
envelope.set(new Uint8Array(ct), 1 + IV_BYTES);
|
|
58
|
+
return Buffer.from(envelope).toString('base64');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Decrypt an envelope produced by {@link encryptVaultItem}. Throws on an unknown
|
|
62
|
+
* version, a truncated envelope, or a failed GCM auth (tamper/wrong key). */
|
|
63
|
+
export async function decryptVaultItem(key: CryptoKey, envelope: string): Promise<string> {
|
|
64
|
+
const bytes = new Uint8Array(Buffer.from(envelope, 'base64'));
|
|
65
|
+
if (bytes.length < 1 + IV_BYTES + 16) throw new Error('vault: envelope too short');
|
|
66
|
+
if (bytes[0] !== VERSION) throw new Error(`vault: unsupported envelope version ${bytes[0]}`);
|
|
67
|
+
const iv = bytes.subarray(1, 1 + IV_BYTES);
|
|
68
|
+
const ct = bytes.subarray(1 + IV_BYTES);
|
|
69
|
+
const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
|
|
70
|
+
return dec.decode(plain);
|
|
71
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CtgrDecoded, CtgrMethod } from 'cursedbelt-core/ctgr/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The cursedbelt Hono environment. `createApp()` returns a `Hono<CursedbeltEnv>`
|
|
5
|
+
* so every middleware and handler shares one typed `c.get`/`c.set` surface.
|
|
6
|
+
*
|
|
7
|
+
* Only *plumbing* variables live here — request identity that the cross-cutting
|
|
8
|
+
* middleware owns. Business variables (roles, session, sharing) are added by the
|
|
9
|
+
* app's own auth/feature middleware via its own env extension (task 26+).
|
|
10
|
+
*/
|
|
11
|
+
export interface CursedbeltVariables {
|
|
12
|
+
/** Per-request correlation id. Set by {@link correlationId} on every request. */
|
|
13
|
+
correlationId: string;
|
|
14
|
+
/**
|
|
15
|
+
* Authenticated user id. cursedbelt never sets this — the app's auth middleware
|
|
16
|
+
* (task 26) populates it so the request logger can attribute metrics/events to a
|
|
17
|
+
* user. Optional because anonymous requests have none.
|
|
18
|
+
*/
|
|
19
|
+
userId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The decoded original request on a `/ctgr*` GET twin. Set by the ctgr `unGETify`
|
|
22
|
+
* middleware; the downstream handler / zod validator reads the body from here.
|
|
23
|
+
*/
|
|
24
|
+
ctgrDecoded?: CtgrDecoded;
|
|
25
|
+
/** The decoded original HTTP method of a ctgr-tunneled request. */
|
|
26
|
+
ctgrMethod?: CtgrMethod;
|
|
27
|
+
/**
|
|
28
|
+
* The parsed/coerced/stripped body a `zValidator(...)` route accepted. A convenience
|
|
29
|
+
* mirror of the typed `c.req.valid('json')` slot for handlers that prefer a getter.
|
|
30
|
+
* Set only on a validated route; `unknown` because the type is the route's schema.
|
|
31
|
+
*/
|
|
32
|
+
validatedBody?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CursedbeltEnv {
|
|
36
|
+
Variables: CursedbeltVariables;
|
|
37
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { toBase64 } from 'cursedbelt-core/ctgr';
|
|
4
|
+
import { ChunkStore } from 'cursedbelt-core/ctgr/chunk-store';
|
|
5
|
+
import { encodeRequest } from 'cursedbelt-core/ctgr';
|
|
6
|
+
import type { CursedbeltEnv } from './context';
|
|
7
|
+
import { buildRoutes, CTGR_ROUTE_PATTERN, getCtgrDecoded, unGETify } from './ctgr';
|
|
8
|
+
|
|
9
|
+
const ORIGIN = 'http://localhost';
|
|
10
|
+
const fetchChunk = (app: Hono<CursedbeltEnv>, url: string, init?: RequestInit) =>
|
|
11
|
+
app.fetch(new Request(ORIGIN + url, init));
|
|
12
|
+
|
|
13
|
+
// Mirror the legacy client encoder so we can exercise the v0 fallback path.
|
|
14
|
+
const encodeV0 = (jsonString: string): string => {
|
|
15
|
+
const encode = (s: string) => toBase64(new TextEncoder().encode(s));
|
|
16
|
+
const reverse = (s: string) => s.split('').reverse().join('');
|
|
17
|
+
const swap = (s: string) => {
|
|
18
|
+
if (s.length < 2) return s;
|
|
19
|
+
const arr = s.split('');
|
|
20
|
+
arr[0] = s[s.length - 1];
|
|
21
|
+
arr[s.length - 1] = s[0];
|
|
22
|
+
return arr.join('');
|
|
23
|
+
};
|
|
24
|
+
return swap(encode(swap(reverse(encode(jsonString)))));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function makeApp() {
|
|
28
|
+
const app = new Hono<CursedbeltEnv>();
|
|
29
|
+
const store = new ChunkStore();
|
|
30
|
+
app.use(CTGR_ROUTE_PATTERN, unGETify(store));
|
|
31
|
+
|
|
32
|
+
const register = buildRoutes(app);
|
|
33
|
+
// An echo handler shared by the real verb route AND its GET twin: it reads the
|
|
34
|
+
// body from the ctgr-decoded context when tunneled, else from the request.
|
|
35
|
+
register('post', '/notes', async (c) => {
|
|
36
|
+
const decoded = getCtgrDecoded(c);
|
|
37
|
+
const body = decoded ? decoded.body : await c.req.json();
|
|
38
|
+
return c.json({ ok: true, rawMethod: c.req.method, decodedMethod: decoded?.method, body });
|
|
39
|
+
});
|
|
40
|
+
register('delete', '/notes/:id', (c) => {
|
|
41
|
+
const decoded = getCtgrDecoded(c);
|
|
42
|
+
return c.json({
|
|
43
|
+
ok: true,
|
|
44
|
+
id: c.req.param('id'),
|
|
45
|
+
method: decoded?.method,
|
|
46
|
+
body: decoded?.body,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Binary download stays a PLAIN GET — never registered via buildRoutes, so no
|
|
51
|
+
// `/ctgrde/...` twin exists to mangle its bytes through base64url.
|
|
52
|
+
app.get('/files/:id/download', (c) => c.text(`binary:${c.req.param('id')}`));
|
|
53
|
+
|
|
54
|
+
return app;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('unGETify + buildRoutes (Hono integration)', () => {
|
|
58
|
+
test('a single-chunk POST twin runs the handler with the decoded body', async () => {
|
|
59
|
+
const app = makeApp();
|
|
60
|
+
const enc = await encodeRequest('POST', '/notes', { body: 'hi' });
|
|
61
|
+
const res = await fetchChunk(app, enc.chunks[0].url);
|
|
62
|
+
expect(res.status).toBe(200);
|
|
63
|
+
expect(await res.json()).toEqual({
|
|
64
|
+
ok: true,
|
|
65
|
+
rawMethod: 'GET', // the transport is a GET — what the getOnly 403 keys off
|
|
66
|
+
decodedMethod: 'POST', // the decoded intent — what the handler/validator sees
|
|
67
|
+
body: { body: 'hi' },
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('the real verb route still works (no tunnel)', async () => {
|
|
72
|
+
const app = makeApp();
|
|
73
|
+
const res = await app.fetch(
|
|
74
|
+
new Request(`${ORIGIN}/notes`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'content-type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ body: 'direct' }),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
expect(res.status).toBe(200);
|
|
81
|
+
const json = (await res.json()) as { rawMethod: string; body: unknown };
|
|
82
|
+
expect(json.rawMethod).toBe('POST');
|
|
83
|
+
expect(json.body).toEqual({ body: 'direct' });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a multi-chunk payload: intermediate chunks 202, the last drives the handler', async () => {
|
|
87
|
+
const app = makeApp();
|
|
88
|
+
// gzip disabled so the payload genuinely spans multiple chunks/requests.
|
|
89
|
+
const big = { body: 'q'.repeat(5000) };
|
|
90
|
+
const enc = await encodeRequest('POST', '/notes', big, {
|
|
91
|
+
chunkSizeBytes: 400,
|
|
92
|
+
gzipThresholdBytes: 10_000_000,
|
|
93
|
+
});
|
|
94
|
+
expect(enc.chunks.length).toBeGreaterThan(1);
|
|
95
|
+
for (const chunk of enc.chunks.slice(0, -1)) {
|
|
96
|
+
const res = await fetchChunk(app, chunk.url);
|
|
97
|
+
expect(res.status).toBe(202);
|
|
98
|
+
}
|
|
99
|
+
const final = await fetchChunk(app, enc.chunks.at(-1)!.url);
|
|
100
|
+
expect(final.status).toBe(200);
|
|
101
|
+
expect((await final.json()).body).toEqual(big);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('a body-less DELETE twin decodes to a null body', async () => {
|
|
105
|
+
const app = makeApp();
|
|
106
|
+
const enc = await encodeRequest('DELETE', '/notes/7', undefined);
|
|
107
|
+
const res = await fetchChunk(app, enc.chunks[0].url);
|
|
108
|
+
expect(res.status).toBe(200);
|
|
109
|
+
expect(await res.json()).toMatchObject({ ok: true, id: '7', method: 'DELETE', body: null });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('a legacy v0 request (no _cv) decodes via the compat path', async () => {
|
|
113
|
+
const app = makeApp();
|
|
114
|
+
const p = encodeV0(JSON.stringify({ body: 'legacy' }));
|
|
115
|
+
const res = await fetchChunk(app, `/ctgrpo/notes?p=${encodeURIComponent(p)}`);
|
|
116
|
+
expect(res.status).toBe(200);
|
|
117
|
+
expect((await res.json()).body).toEqual({ body: 'legacy' });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a corrupt tunneled payload returns a 400 error envelope', async () => {
|
|
121
|
+
const app = makeApp();
|
|
122
|
+
const enc = await encodeRequest('POST', '/notes', { body: 'hi' });
|
|
123
|
+
const tampered = enc.chunks[0].url.replace(/_sha=[0-9a-f]+/, '_sha=deadbeef');
|
|
124
|
+
const res = await fetchChunk(app, tampered);
|
|
125
|
+
expect(res.status).toBe(400);
|
|
126
|
+
expect((await res.json()).error.message).toMatch(/sha256/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('binary downloads stay a plain GET with NO ctgr twin', async () => {
|
|
130
|
+
const app = makeApp();
|
|
131
|
+
const direct = await fetchChunk(app, '/files/42/download');
|
|
132
|
+
expect(direct.status).toBe(200);
|
|
133
|
+
expect(await direct.text()).toBe('binary:42');
|
|
134
|
+
// No `/ctgrde/files/:id/download` route was registered → 404.
|
|
135
|
+
const twin = await fetchChunk(app, '/ctgrde/files/42/download');
|
|
136
|
+
expect(twin.status).toBe(404);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ctgr Hono transport — the server half of the GET-tunnel: the `unGETify` middleware
|
|
3
|
+
* that decodes a `/ctgr<XX>` twin's query params back into the original request, and
|
|
4
|
+
* the `buildRoutes` registrar that wires the real verb route and its GET twin from one
|
|
5
|
+
* call. The codec re-exports from `cursedbelt-server`; the browser side is `cursedbelt/client`.
|
|
6
|
+
*
|
|
7
|
+
* Protection is unchanged and lives elsewhere by design: the getOnly-claim 403 keys
|
|
8
|
+
* off the RAW request method (always GET on a twin) in the auth middleware, and the
|
|
9
|
+
* CSRF double-submit guard fires on every `/ctgr*` path. unGETify ONLY decodes.
|
|
10
|
+
*/
|
|
11
|
+
import type { Context, Hono, MiddlewareHandler } from 'hono';
|
|
12
|
+
import type { H } from 'hono/types';
|
|
13
|
+
import type { ChunkStore } from 'cursedbelt-core/ctgr/chunk-store';
|
|
14
|
+
import {
|
|
15
|
+
ABBREV_TO_METHOD,
|
|
16
|
+
type CtgrAbbrev,
|
|
17
|
+
type CtgrDecoded,
|
|
18
|
+
type CtgrMethod,
|
|
19
|
+
METHOD_TO_ABBREV,
|
|
20
|
+
} from 'cursedbelt-core/ctgr/types';
|
|
21
|
+
import { V0ChunkStore } from 'cursedbelt-core/ctgr/v0compat';
|
|
22
|
+
import type { CursedbeltEnv } from './context';
|
|
23
|
+
import { ApiError, toErrorEnvelope } from './errors';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The path pattern that matches EVERY ctgr GET twin (`/ctgr<XX>...`) and nothing
|
|
27
|
+
* else — mount the decode + CSRF + getOnly middleware on this. NOTE: Hono's router
|
|
28
|
+
* does not treat `/ctgr*` as a prefix wildcard (a bare-suffix `*` only matches a
|
|
29
|
+
* single following segment), so a named param with a `ctgr.+` regex is the correct,
|
|
30
|
+
* portable matcher (`.` spans `/`, so it covers multi-segment paths like
|
|
31
|
+
* `/ctgrpa/api/v1/lists/123`). Reuse this everywhere a `/ctgr*` mount is meant.
|
|
32
|
+
*/
|
|
33
|
+
export const CTGR_ROUTE_PATTERN = '/:ctgrTwin{ctgr.+}';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mount on {@link CTGR_ROUTE_PATTERN} (before the twin routes). A single chunk decodes
|
|
37
|
+
* inline; a multi-chunk payload accumulates in the injected {@link ChunkStore}
|
|
38
|
+
* (intermediate chunk → 202). The decoded request + method are stashed on the context for the
|
|
39
|
+
* downstream handler / validator (`c.get('ctgrDecoded')`). Legacy `_cv`-absent
|
|
40
|
+
* requests fall back to the v0 decoder for the rollout window.
|
|
41
|
+
*/
|
|
42
|
+
export function unGETify(store: ChunkStore): MiddlewareHandler<CursedbeltEnv> {
|
|
43
|
+
const legacy = new V0ChunkStore();
|
|
44
|
+
return async (c, next) => {
|
|
45
|
+
const params = Object.fromEntries(new URL(c.req.url).searchParams);
|
|
46
|
+
const pathMethod = ABBREV_TO_METHOD[c.req.path.slice(5, 7) as CtgrAbbrev];
|
|
47
|
+
if (!pathMethod) {
|
|
48
|
+
return c.json(toErrorEnvelope(new ApiError(400, 'ctgr: unknown method twin', { code: 'unknown_method' })), 400);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result =
|
|
52
|
+
params._cv === undefined ? legacy.add(params, pathMethod) : await store.add(params);
|
|
53
|
+
|
|
54
|
+
if (result === false) return c.json({ acknowledged: true }, 202);
|
|
55
|
+
if (result instanceof Error)
|
|
56
|
+
return c.json(toErrorEnvelope(new ApiError(400, result.message, { code: 'ctgr_decode_failed' })), 400);
|
|
57
|
+
|
|
58
|
+
c.set('ctgrDecoded', result);
|
|
59
|
+
c.set('ctgrMethod', result.method);
|
|
60
|
+
await next();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Read the ctgr-decoded request a `/ctgr*` twin stashed, if this was a tunneled call. */
|
|
65
|
+
export function getCtgrDecoded(c: Context<CursedbeltEnv>): CtgrDecoded | undefined {
|
|
66
|
+
return c.get('ctgrDecoded');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The internal dispatch shape of `app.on` — Hono's overload soup can't resolve a
|
|
70
|
+
* runtime-length handler spread, so we assert the (method, path, ...handlers) shape
|
|
71
|
+
* it accepts at runtime while keeping the public {@link buildRoutes} API type-safe. */
|
|
72
|
+
type RouteDispatch = (
|
|
73
|
+
method: string,
|
|
74
|
+
path: string,
|
|
75
|
+
...handlers: MiddlewareHandler<CursedbeltEnv>[]
|
|
76
|
+
) => unknown;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Twin registrar: one call registers the real verb route AND its `/ctgr<XX>` GET twin
|
|
80
|
+
* (same handlers; the global `/ctgr*` {@link unGETify} middleware decodes the body
|
|
81
|
+
* first). Binary/download routes MUST NOT go through this — register those as a plain
|
|
82
|
+
* `app.get(path, handler)` so their bytes never ride the base64url tunnel and get
|
|
83
|
+
* mangled.
|
|
84
|
+
*/
|
|
85
|
+
export function buildRoutes(app: Hono<CursedbeltEnv>) {
|
|
86
|
+
const on = app.on.bind(app) as RouteDispatch;
|
|
87
|
+
return function register(
|
|
88
|
+
method: CtgrMethod | Lowercase<CtgrMethod>,
|
|
89
|
+
path: string,
|
|
90
|
+
/*
|
|
91
|
+
* `H`, not `MiddlewareHandler`. These are ROUTE handlers: Hono's
|
|
92
|
+
* `MiddlewareHandler` must return `Promise<Response | void>`, so declaring it
|
|
93
|
+
* here forced every caller to be `async` for no reason — a plain
|
|
94
|
+
* `(c) => c.json(…)` did not typecheck against a registrar whose whole job is
|
|
95
|
+
* registering exactly that. `H` is Hono's own union of both kinds and is what
|
|
96
|
+
* `app.on` accepts.
|
|
97
|
+
*/
|
|
98
|
+
...handlers: H<CursedbeltEnv>[]
|
|
99
|
+
): void {
|
|
100
|
+
const upper = method.toUpperCase() as CtgrMethod;
|
|
101
|
+
const abbrev = METHOD_TO_ABBREV[upper];
|
|
102
|
+
if (!abbrev) throw new Error(`buildRoutes: "${method}" is not a ctgr-tunnelable method`);
|
|
103
|
+
|
|
104
|
+
// The real verb route + its GET twin, from one call.
|
|
105
|
+
on(upper, path, ...handlers);
|
|
106
|
+
on('GET', `/ctgr${abbrev}${path}`, ...handlers);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { createKysely, type PlumbingDb } from './kysely';
|
|
4
|
+
|
|
5
|
+
const AUTH_SESSIONS_DDL = `CREATE TABLE auth_sessions (
|
|
6
|
+
session_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
7
|
+
last_seen_at TEXT NOT NULL, refresh_expires_at TEXT NOT NULL, access_expires_at TEXT,
|
|
8
|
+
get_only INTEGER NOT NULL DEFAULT 0, disabled INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
disabled_at TEXT, disabled_by TEXT, user_agent TEXT, ip TEXT
|
|
10
|
+
)`;
|
|
11
|
+
|
|
12
|
+
describe('createKysely (plumbing tables)', () => {
|
|
13
|
+
let db: Database;
|
|
14
|
+
let ky: PlumbingDb;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
db = new Database(':memory:');
|
|
18
|
+
db.run(AUTH_SESSIONS_DDL);
|
|
19
|
+
db.run(
|
|
20
|
+
'CREATE TABLE app_config (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL, updated_by TEXT)',
|
|
21
|
+
);
|
|
22
|
+
ky = createKysely(db);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await ky.destroy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('typed selectAll on an empty plumbing table returns []', async () => {
|
|
30
|
+
const rows = await ky.selectFrom('auth_sessions').selectAll().execute();
|
|
31
|
+
expect(rows).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('typed insert + select round-trips a row', async () => {
|
|
35
|
+
await ky
|
|
36
|
+
.insertInto('app_config')
|
|
37
|
+
.values({ key: 'media_backend', value: 'disk', updated_at: 't', updated_by: null })
|
|
38
|
+
.execute();
|
|
39
|
+
|
|
40
|
+
const row = await ky
|
|
41
|
+
.selectFrom('app_config')
|
|
42
|
+
.select(['key', 'value'])
|
|
43
|
+
.where('key', '=', 'media_backend')
|
|
44
|
+
.executeTakeFirst();
|
|
45
|
+
|
|
46
|
+
expect(row).toEqual({ key: 'media_backend', value: 'disk' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('reads through the SAME connection (raw write is visible to Kysely)', async () => {
|
|
50
|
+
db.run(
|
|
51
|
+
`INSERT INTO auth_sessions (session_id, user_id, created_at, last_seen_at, refresh_expires_at)
|
|
52
|
+
VALUES ('s1', 'u1', 't', 't', 't')`,
|
|
53
|
+
);
|
|
54
|
+
const row = await ky
|
|
55
|
+
.selectFrom('auth_sessions')
|
|
56
|
+
.selectAll()
|
|
57
|
+
.where('session_id', '=', 's1')
|
|
58
|
+
.executeTakeFirst();
|
|
59
|
+
expect(row?.user_id).toBe('u1');
|
|
60
|
+
expect(row?.get_only).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import { Kysely } from 'kysely';
|
|
3
|
+
import { BunSqliteDialect } from 'kysely-bun-sqlite';
|
|
4
|
+
import type { PlumbingSchema } from './plumbingTypes';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A Kysely query builder typed to the {@link PlumbingSchema} plumbing tables, on the
|
|
8
|
+
* `bun:sqlite` dialect. Use it for type-checked reads/writes of sessions, config,
|
|
9
|
+
* logs, metrics, and jobs. Business / `data JSON` tables stay on raw `db.query()` —
|
|
10
|
+
* do NOT add them here (that would couple the Kysely schema to every feature table).
|
|
11
|
+
*
|
|
12
|
+
* The dialect wraps the SAME `Database` handle the rest of the app uses, so there is
|
|
13
|
+
* one connection, one WAL, one transaction space.
|
|
14
|
+
*/
|
|
15
|
+
export function createKysely(db: Database): Kysely<PlumbingSchema> {
|
|
16
|
+
return new Kysely<PlumbingSchema>({
|
|
17
|
+
dialect: new BunSqliteDialect({ database: db }),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PlumbingDb = Kysely<PlumbingSchema>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript row interfaces for the *plumbing* tables — the structural, non-business
|
|
3
|
+
* tables cursedbelt owns: sessions, config, logs, metrics, jobs, the migration
|
|
4
|
+
* sentinel. These get Kysely-typed access (see `./kysely`) so the toolkit's own
|
|
5
|
+
* queries are type-checked.
|
|
6
|
+
*
|
|
7
|
+
* Business / feature tables (notes, lists, shares, …) deliberately stay OUT of this
|
|
8
|
+
* schema: they keep raw `db.query()` + flexible `data JSON`, so the Kysely schema is
|
|
9
|
+
* not coupled to every feature table. Shapes mirror the canonical DDL (task 22,
|
|
10
|
+
* `0001_initial`). INTEGER booleans are kept as `number` (SQLite has no real bool).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface AuthSessionRow {
|
|
14
|
+
session_id: string;
|
|
15
|
+
user_id: string;
|
|
16
|
+
created_at: string;
|
|
17
|
+
last_seen_at: string;
|
|
18
|
+
refresh_expires_at: string;
|
|
19
|
+
access_expires_at: string | null;
|
|
20
|
+
/** 0 | 1 — the `/f` GET-only restriction, enforced + audited per session. */
|
|
21
|
+
get_only: number;
|
|
22
|
+
/** 0 | 1 */
|
|
23
|
+
disabled: number;
|
|
24
|
+
disabled_at: string | null;
|
|
25
|
+
disabled_by: string | null;
|
|
26
|
+
user_agent: string | null;
|
|
27
|
+
ip: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AppConfigRow {
|
|
31
|
+
key: string;
|
|
32
|
+
value: string;
|
|
33
|
+
updated_at: string;
|
|
34
|
+
updated_by: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface EventLogRow {
|
|
38
|
+
id: string;
|
|
39
|
+
correlation_id: string | null;
|
|
40
|
+
event_name: string;
|
|
41
|
+
/** info | warn | error (free text, defaults to 'info'). */
|
|
42
|
+
type: string;
|
|
43
|
+
message: string | null;
|
|
44
|
+
/** JSON blob of structured detail. */
|
|
45
|
+
details: string | null;
|
|
46
|
+
duration_ms: number | null;
|
|
47
|
+
user_id: string | null;
|
|
48
|
+
created_at: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RequestMetricRow {
|
|
52
|
+
id: string;
|
|
53
|
+
/** Epoch ms. */
|
|
54
|
+
ts: number;
|
|
55
|
+
method: string | null;
|
|
56
|
+
route: string | null;
|
|
57
|
+
status: number | null;
|
|
58
|
+
duration_ms: number | null;
|
|
59
|
+
bytes_out: number | null;
|
|
60
|
+
user_id: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SystemSampleRow {
|
|
64
|
+
id: string;
|
|
65
|
+
/** Epoch ms. */
|
|
66
|
+
ts: number;
|
|
67
|
+
rss: number | null;
|
|
68
|
+
heap_used: number | null;
|
|
69
|
+
heap_total: number | null;
|
|
70
|
+
os_used: number | null;
|
|
71
|
+
os_total: number | null;
|
|
72
|
+
uptime_s: number | null;
|
|
73
|
+
event_loop_lag_ms: number | null;
|
|
74
|
+
ws_clients: number | null;
|
|
75
|
+
req_count: number | null;
|
|
76
|
+
err_count: number | null;
|
|
77
|
+
avg_duration_ms: number | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EmailFailureRow {
|
|
81
|
+
id: string;
|
|
82
|
+
created_at: string;
|
|
83
|
+
recipient: string;
|
|
84
|
+
subject: string;
|
|
85
|
+
/** Defaults to 'send_error'. */
|
|
86
|
+
reason: string;
|
|
87
|
+
error_name: string | null;
|
|
88
|
+
error_code: string | null;
|
|
89
|
+
error_message: string | null;
|
|
90
|
+
details: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface JobStateRow {
|
|
94
|
+
name: string;
|
|
95
|
+
last_run_at: string | null;
|
|
96
|
+
/** JSON blob. */
|
|
97
|
+
meta: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface JobRunRow {
|
|
101
|
+
id: string;
|
|
102
|
+
job_name: string;
|
|
103
|
+
started_at: string;
|
|
104
|
+
ended_at: string | null;
|
|
105
|
+
/** running | ok | error (defaults to 'running'). */
|
|
106
|
+
status: string;
|
|
107
|
+
error: string | null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface MigrationRow {
|
|
111
|
+
name: string;
|
|
112
|
+
applied_at: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The Kysely database schema for cursedbelt plumbing tables. */
|
|
116
|
+
export interface PlumbingSchema {
|
|
117
|
+
auth_sessions: AuthSessionRow;
|
|
118
|
+
app_config: AppConfigRow;
|
|
119
|
+
event_logs: EventLogRow;
|
|
120
|
+
request_metrics: RequestMetricRow;
|
|
121
|
+
system_samples: SystemSampleRow;
|
|
122
|
+
email_failures: EmailFailureRow;
|
|
123
|
+
job_state: JobStateRow;
|
|
124
|
+
job_runs: JobRunRow;
|
|
125
|
+
_migrations: MigrationRow;
|
|
126
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking binary-server "do you already have this?" — the client half.
|
|
3
|
+
*
|
|
4
|
+
* Owner's ask, 2026-08-13: *"make strategy to always check for duplicate binaries in same app
|
|
5
|
+
* for same user, automatically skip if it's literally a duplicate file"* … *"if it's an
|
|
6
|
+
* automated upload with ingestions or an agent guiding it and I need to make a decision put it
|
|
7
|
+
* in binary-server UI for me to see and handle it."*
|
|
8
|
+
*
|
|
9
|
+
* ── 🔴 Asked BEFORE the bytes move, and that ordering is the whole point ───────────────
|
|
10
|
+
* The obvious design is upload-then-detect: store it, notice the collision, delete one. That
|
|
11
|
+
* moves every duplicate twice, and on a media dropzone it does so for multi-gigabyte video the
|
|
12
|
+
* store already holds. The scraper has the file, so it has the digest; one small POST answers
|
|
13
|
+
* the question before a single byte is sent.
|
|
14
|
+
*
|
|
15
|
+
* ── 🔴 It lives in cursedbelt, not in satellite-kit ────────────────────────────────────
|
|
16
|
+
* binary-server itself consumes `cursedbelt` and does NOT consume `satellite-kit`, and every
|
|
17
|
+
* ingesting app consumes both. This is the one shelf both sides can see. It is also the reason
|
|
18
|
+
* the request/response shape is stated here as types rather than being re-declared by each
|
|
19
|
+
* caller — a dropzone that got `hold` backwards would upload exactly the files the owner asked
|
|
20
|
+
* it to park.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { createReadStream } from "node:fs";
|
|
25
|
+
|
|
26
|
+
export interface DuplicateQuestion {
|
|
27
|
+
/** binary-server's loopback origin. Never the public tunnel — see the note below. */
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
/** The shared internal secret; this route is internal-only and 403s without it. */
|
|
30
|
+
internalSecret: string;
|
|
31
|
+
/** The binary-server tenant, e.g. `music`. */
|
|
32
|
+
app: string;
|
|
33
|
+
/** Scope the comparison to one user. Null on the owner-only apps, which is normal. */
|
|
34
|
+
ownerId?: string | null;
|
|
35
|
+
/** sha256 of the bytes about to be uploaded, hex. {@link hashFile} produces it. */
|
|
36
|
+
checksum: string;
|
|
37
|
+
size: number;
|
|
38
|
+
/** The name the object would carry — what the "same name" half of the rule compares. */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Where the file is right now, so the owner's later decision can act on it. */
|
|
41
|
+
heldPath?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DuplicateVerdict {
|
|
45
|
+
/** Present when the store already holds these exact bytes. Skip the upload entirely. */
|
|
46
|
+
exact: { key: string; title: string | null; size: number } | null;
|
|
47
|
+
/** Name-alikes with different bytes — a question, never an instruction. */
|
|
48
|
+
likely: { key: string; title: string | null; size: number; reason: string }[];
|
|
49
|
+
/** True when the caller must NOT upload and must park the file for the owner. */
|
|
50
|
+
hold: boolean;
|
|
51
|
+
/** The review-queue row binary-server filed, when it filed one. */
|
|
52
|
+
candidateId: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Streaming sha256 of a file on disk.
|
|
57
|
+
*
|
|
58
|
+
* Streamed rather than buffered because a music library is thousands of files and some of them
|
|
59
|
+
* are 60 MB; reading each one whole to hash it is how an ingest becomes a memory problem.
|
|
60
|
+
*/
|
|
61
|
+
export function hashFile(path: string): Promise<string> {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const hash = createHash("sha256");
|
|
64
|
+
const stream = createReadStream(path);
|
|
65
|
+
stream.on("error", reject);
|
|
66
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
67
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ask binary-server about one incoming file.
|
|
73
|
+
*
|
|
74
|
+
* 🔴 **Fails OPEN, deliberately.** If binary-server cannot be reached, or answers something
|
|
75
|
+
* unexpected, the verdict is "no duplicate, do not hold" and the ingest proceeds. The
|
|
76
|
+
* alternative is that a dropzone stops taking the owner's files because a de-duplication
|
|
77
|
+
* nicety is down — trading a real capability for a housekeeping one. A duplicate that slips
|
|
78
|
+
* through costs disk and is found later by the scan button, which is exactly what that button
|
|
79
|
+
* is for.
|
|
80
|
+
*
|
|
81
|
+
* 🔴 **`baseUrl` must be the LOOPBACK origin.** The route is absent from binary-server's
|
|
82
|
+
* public path allowlist and 404s when the request arrives bearing the public tunnel Host —
|
|
83
|
+
* because it reports the titles and sizes of stored objects, and listing is what the tunnel
|
|
84
|
+
* must never publish. A caller that passes the public URL gets a 404 and, through the
|
|
85
|
+
* fail-open above, silently loses de-duplication entirely. notes shipped this exact bug once
|
|
86
|
+
* with `/api/transcode`.
|
|
87
|
+
*/
|
|
88
|
+
export async function askBinaryServerAboutDuplicate(
|
|
89
|
+
q: DuplicateQuestion,
|
|
90
|
+
): Promise<DuplicateVerdict> {
|
|
91
|
+
const none: DuplicateVerdict = { exact: null, likely: [], hold: false, candidateId: null };
|
|
92
|
+
if (!q.internalSecret) return none;
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`${q.baseUrl.replace(/\/+$/, "")}/internal/ops/duplicates/check`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
"content-type": "application/json",
|
|
98
|
+
"x-internal-secret": q.internalSecret,
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
app: q.app,
|
|
102
|
+
ownerId: q.ownerId ?? null,
|
|
103
|
+
checksum: q.checksum,
|
|
104
|
+
size: q.size,
|
|
105
|
+
name: q.name,
|
|
106
|
+
origin: "automated",
|
|
107
|
+
heldPath: q.heldPath ?? null,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) return none;
|
|
111
|
+
const body = (await res.json()) as Partial<DuplicateVerdict>;
|
|
112
|
+
return {
|
|
113
|
+
exact: body.exact ?? null,
|
|
114
|
+
likely: body.likely ?? [],
|
|
115
|
+
hold: body.hold === true,
|
|
116
|
+
candidateId: body.candidateId ?? null,
|
|
117
|
+
};
|
|
118
|
+
} catch {
|
|
119
|
+
return none;
|
|
120
|
+
}
|
|
121
|
+
}
|