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,111 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runMigrations } from '../../migration/runner';
|
|
7
|
+
import { createDiskStore } from '../diskStore';
|
|
8
|
+
import type { MediaCtx } from '../types';
|
|
9
|
+
import { catalogueUploadKey, createSqliteFileCatalogue } from './catalogue';
|
|
10
|
+
import { createChunkedUploadService } from './chunkedUpload';
|
|
11
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
12
|
+
import { createStorageOpsLedger } from './storageOps';
|
|
13
|
+
|
|
14
|
+
const CTX: MediaCtx = { userId: 'u1', tenantId: 'u1' };
|
|
15
|
+
|
|
16
|
+
describe('chunked upload', () => {
|
|
17
|
+
let db: Database;
|
|
18
|
+
let root: string;
|
|
19
|
+
let clock = 0;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
db = new Database(':memory:');
|
|
23
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
24
|
+
root = await mkdtemp(path.join(tmpdir(), 'fc-chunk-'));
|
|
25
|
+
clock = 0;
|
|
26
|
+
});
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
db.close();
|
|
29
|
+
await rm(root, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const build = () => {
|
|
33
|
+
const store = createDiskStore(root);
|
|
34
|
+
const catalogue = createSqliteFileCatalogue({
|
|
35
|
+
db,
|
|
36
|
+
store,
|
|
37
|
+
workDir: path.join(root, 'w'),
|
|
38
|
+
owner: (c) => c.userId,
|
|
39
|
+
now: () => ++clock,
|
|
40
|
+
});
|
|
41
|
+
const ledger = createStorageOpsLedger(db, () => ++clock);
|
|
42
|
+
const minted: Array<{ key: string; sid: string; ci: number; tc: number }> = [];
|
|
43
|
+
const svc = createChunkedUploadService({
|
|
44
|
+
catalogue,
|
|
45
|
+
ledger,
|
|
46
|
+
backend: 'binary',
|
|
47
|
+
chunkSize: 8 * 1024 * 1024,
|
|
48
|
+
owner: (c) => c.userId,
|
|
49
|
+
now: () => ++clock,
|
|
50
|
+
mintChunkUpload: async (a) => {
|
|
51
|
+
minted.push(a);
|
|
52
|
+
return `https://bs.example/upload-chunk/app/${a.key}?sid=${a.sid}&ci=${a.ci}&tc=${a.tc}`;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
return { catalogue, ledger, svc, minted };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
test('init reserves a file, records a pending upload op, and mints one URL per chunk', async () => {
|
|
59
|
+
const { ledger, svc, minted } = build();
|
|
60
|
+
// 20 MiB at an 8 MiB chunk size ⇒ 3 chunks.
|
|
61
|
+
const init = await svc.init(CTX, {
|
|
62
|
+
name: 'big.png',
|
|
63
|
+
mime: 'image/png',
|
|
64
|
+
size: 20 * 1024 * 1024,
|
|
65
|
+
});
|
|
66
|
+
expect(init.chunkCount).toBe(3);
|
|
67
|
+
expect(init.chunkUploadUrls).toHaveLength(3);
|
|
68
|
+
expect(minted.map((m) => m.ci)).toEqual([0, 1, 2]);
|
|
69
|
+
expect(minted.every((m) => m.tc === 3 && m.sid === init.sid)).toBe(true);
|
|
70
|
+
// The mint targets exactly the key the catalogue will look for on complete.
|
|
71
|
+
expect(init.key).toBe(catalogueUploadKey('u1', init.fileId, 'image/png'));
|
|
72
|
+
|
|
73
|
+
// A durable pending 'upload' op keyed by the session id.
|
|
74
|
+
const op = ledger.get(init.sid);
|
|
75
|
+
expect(op?.status).toBe('pending');
|
|
76
|
+
expect(op?.action).toBe('upload');
|
|
77
|
+
expect(op?.key).toBe(init.key);
|
|
78
|
+
expect(svc.pending()).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('complete registers the catalogue row over the assembled bytes + marks the op done', async () => {
|
|
82
|
+
const { catalogue, ledger, svc } = build();
|
|
83
|
+
const init = await svc.init(CTX, {
|
|
84
|
+
name: 'photo.png',
|
|
85
|
+
mime: 'image/png',
|
|
86
|
+
size: 12 * 1024 * 1024,
|
|
87
|
+
tags: ['big'],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const rec = await svc.complete(init.sid, { size: 12 * 1024 * 1024 });
|
|
91
|
+
expect(rec?.id).toBe(init.fileId);
|
|
92
|
+
expect(rec?.name).toBe('photo.png');
|
|
93
|
+
expect(rec?.tags).toEqual(['big']);
|
|
94
|
+
// Now discoverable through the normal catalogue (no re-upload happened).
|
|
95
|
+
expect((await catalogue.get(CTX, init.fileId))?.name).toBe('photo.png');
|
|
96
|
+
expect(ledger.get(init.sid)?.status).toBe('done');
|
|
97
|
+
expect(svc.pending()).toBe(0);
|
|
98
|
+
|
|
99
|
+
// A second complete for the same (now-cleared) session is a no-op.
|
|
100
|
+
expect(await svc.complete(init.sid)).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('abort marks the op failed and drops the session (no catalogue row)', async () => {
|
|
104
|
+
const { catalogue, ledger, svc } = build();
|
|
105
|
+
const init = await svc.init(CTX, { name: 'x.png', mime: 'image/png', size: 1 });
|
|
106
|
+
svc.abort(init.sid);
|
|
107
|
+
expect(ledger.get(init.sid)?.status).toBe('failed');
|
|
108
|
+
expect(svc.pending()).toBe(0);
|
|
109
|
+
expect(await catalogue.get(CTX, init.fileId)).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { DEFAULT_UPLOAD_CHUNK_SIZE } from 'cursedbelt-core/media/upload-limits';
|
|
2
|
+
import type { MediaCtx } from '../types';
|
|
3
|
+
import { catalogueUploadKey } from './catalogue';
|
|
4
|
+
import type { StorageOpsLedger } from './storageOps';
|
|
5
|
+
import type { FileCatalogue, FileRecord } from './types';
|
|
6
|
+
|
|
7
|
+
// ── Chunked upload (large files, direct-to-backend) ──────────────────────────────
|
|
8
|
+
// For multi-GB uploads we don't proxy bytes through the app origin. Instead: the client asks the
|
|
9
|
+
// app to START an upload (init) → the app mints per-chunk PUT URLs that go DIRECT to the byte
|
|
10
|
+
// backend (binary-server), records a durable `upload` ledger op, and hands the URLs back. The
|
|
11
|
+
// client PUTs each chunk to the backend, which assembles them at the target key once the last
|
|
12
|
+
// arrives. Then the client (or a backend webhook) calls COMPLETE, and the app registers the
|
|
13
|
+
// catalogue row over the already-stored bytes (no re-upload) + kicks processing.
|
|
14
|
+
//
|
|
15
|
+
// This module is backend-AGNOSTIC: the app injects `mintChunkUpload` (the binary-server chunk URL
|
|
16
|
+
// + signed sid/ci/tc token). On disk-only deploys, an app simply doesn't wire chunked uploads and
|
|
17
|
+
// keeps the streamed `PUT /upload`. Sessions are in-memory (the transient name/mime/size); the
|
|
18
|
+
// ledger row is the DURABLE record of intent, so an abandoned upload is observable/reconcilable.
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CHUNK_SIZE = DEFAULT_UPLOAD_CHUNK_SIZE;
|
|
21
|
+
|
|
22
|
+
export type ChunkedInit = {
|
|
23
|
+
fileId: string;
|
|
24
|
+
/** Chunk session id — also the ledger op id; echoed to the backend as sid so its assemble event
|
|
25
|
+
* matches our storage_operations row. */
|
|
26
|
+
sid: string;
|
|
27
|
+
/** The storage key the assembled bytes land at (fsSourceKey for video, else fileBinaryKey). */
|
|
28
|
+
key: string;
|
|
29
|
+
chunkSize: number;
|
|
30
|
+
chunkCount: number;
|
|
31
|
+
/** One direct-to-backend PUT URL per chunk, in index order. */
|
|
32
|
+
chunkUploadUrls: string[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type Session = {
|
|
36
|
+
fileId: string;
|
|
37
|
+
owner: string;
|
|
38
|
+
ctx: MediaCtx;
|
|
39
|
+
name: string;
|
|
40
|
+
mime: string;
|
|
41
|
+
size: number;
|
|
42
|
+
key: string;
|
|
43
|
+
chunkCount: number;
|
|
44
|
+
tags?: string[];
|
|
45
|
+
createdAt: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface ChunkedUploadService {
|
|
49
|
+
/** Begin a chunked upload: reserve an id, record a pending `upload` op, and mint the per-chunk
|
|
50
|
+
* direct-to-backend PUT URLs. */
|
|
51
|
+
init(
|
|
52
|
+
ctx: MediaCtx,
|
|
53
|
+
req: { name: string; mime: string; size: number; tags?: string[] },
|
|
54
|
+
): Promise<ChunkedInit>;
|
|
55
|
+
/** Finish it: mark the op done and register the catalogue row over the assembled bytes. Returns
|
|
56
|
+
* the new record, or null if the session is unknown (already completed / server restarted). */
|
|
57
|
+
complete(sid: string, confirmed?: { size?: number }): Promise<FileRecord | null>;
|
|
58
|
+
/** Give up on a session (client cancelled / a chunk failed) — mark the ledger op failed. */
|
|
59
|
+
abort(sid: string): void;
|
|
60
|
+
/** Open session count (observability / tests). */
|
|
61
|
+
pending(): number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Mint the direct-to-backend PUT URL for one chunk. The app supplies this so the module stays
|
|
66
|
+
* backend-agnostic. For binary-server: `${base}/upload-chunk/${app}/${key}?token=` with a token
|
|
67
|
+
* carrying { k, uid, sid, ci, tc }.
|
|
68
|
+
*/
|
|
69
|
+
export type MintChunkUpload = (args: {
|
|
70
|
+
key: string;
|
|
71
|
+
sid: string;
|
|
72
|
+
ci: number;
|
|
73
|
+
tc: number;
|
|
74
|
+
owner: string;
|
|
75
|
+
}) => Promise<string>;
|
|
76
|
+
|
|
77
|
+
export function createChunkedUploadService(cfg: {
|
|
78
|
+
catalogue: FileCatalogue;
|
|
79
|
+
ledger: StorageOpsLedger;
|
|
80
|
+
/** Byte backend label recorded on the ledger op (e.g. 'binary'). */
|
|
81
|
+
backend: string;
|
|
82
|
+
mintChunkUpload: MintChunkUpload;
|
|
83
|
+
chunkSize?: number;
|
|
84
|
+
owner?: (ctx: MediaCtx) => string;
|
|
85
|
+
now?: () => number;
|
|
86
|
+
}): ChunkedUploadService {
|
|
87
|
+
const ownerOf = cfg.owner ?? ((c: MediaCtx) => c.tenantId);
|
|
88
|
+
const now = cfg.now ?? (() => Date.now());
|
|
89
|
+
const chunkSize = cfg.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
90
|
+
const sessions = new Map<string, Session>();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
async init(ctx, req) {
|
|
94
|
+
const owner = ownerOf(ctx);
|
|
95
|
+
const fileId = crypto.randomUUID();
|
|
96
|
+
const sid = crypto.randomUUID();
|
|
97
|
+
const key = catalogueUploadKey(owner, fileId, req.mime);
|
|
98
|
+
const chunkCount = Math.max(1, Math.ceil((req.size || 1) / chunkSize));
|
|
99
|
+
// Durable record of intent — its op_id (sid) is what the backend echoes on assemble.
|
|
100
|
+
cfg.ledger.record({ owner, fileId, action: 'upload', key, backend: cfg.backend, opId: sid });
|
|
101
|
+
sessions.set(sid, {
|
|
102
|
+
fileId,
|
|
103
|
+
owner,
|
|
104
|
+
ctx,
|
|
105
|
+
name: req.name,
|
|
106
|
+
mime: req.mime,
|
|
107
|
+
size: req.size,
|
|
108
|
+
key,
|
|
109
|
+
chunkCount,
|
|
110
|
+
tags: req.tags,
|
|
111
|
+
createdAt: now(),
|
|
112
|
+
});
|
|
113
|
+
const chunkUploadUrls = await Promise.all(
|
|
114
|
+
Array.from({ length: chunkCount }, (_, ci) =>
|
|
115
|
+
cfg.mintChunkUpload({ key, sid, ci, tc: chunkCount, owner }),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
return { fileId, sid, key, chunkSize, chunkCount, chunkUploadUrls };
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async complete(sid, confirmed) {
|
|
122
|
+
const s = sessions.get(sid);
|
|
123
|
+
if (!s) return null;
|
|
124
|
+
cfg.ledger.markDone(sid);
|
|
125
|
+
const rec = await cfg.catalogue.registerUploaded(s.ctx, {
|
|
126
|
+
id: s.fileId,
|
|
127
|
+
name: s.name,
|
|
128
|
+
mime: s.mime,
|
|
129
|
+
size: confirmed?.size ?? s.size,
|
|
130
|
+
tags: s.tags,
|
|
131
|
+
});
|
|
132
|
+
sessions.delete(sid);
|
|
133
|
+
return rec;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
abort(sid) {
|
|
137
|
+
if (!sessions.has(sid)) return;
|
|
138
|
+
cfg.ledger.markFailed(sid, 'aborted');
|
|
139
|
+
sessions.delete(sid);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
pending() {
|
|
143
|
+
return sessions.size;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runMigrations } from '../../migration/runner';
|
|
7
|
+
import { createDiskStore } from '../diskStore';
|
|
8
|
+
import type { MediaCtx } from '../types';
|
|
9
|
+
import { createSqliteFileCatalogue } from './catalogue';
|
|
10
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
11
|
+
import type { FileCatalogue } from './types';
|
|
12
|
+
|
|
13
|
+
const CTX: MediaCtx = { userId: 'u1', tenantId: 'owner1' };
|
|
14
|
+
const CTX2: MediaCtx = { userId: 'u2', tenantId: 'owner2' };
|
|
15
|
+
const stream = (s: string) =>
|
|
16
|
+
new Response(new TextEncoder().encode(s)).body as ReadableStream<Uint8Array>;
|
|
17
|
+
|
|
18
|
+
describe('FileCatalogue — Collections', () => {
|
|
19
|
+
let db: Database;
|
|
20
|
+
let root: string;
|
|
21
|
+
let cat: FileCatalogue;
|
|
22
|
+
let clock = 2000;
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
db = new Database(':memory:');
|
|
26
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
27
|
+
root = await mkdtemp(path.join(tmpdir(), 'fc-coll-'));
|
|
28
|
+
cat = createSqliteFileCatalogue({
|
|
29
|
+
db,
|
|
30
|
+
store: createDiskStore(root),
|
|
31
|
+
workDir: path.join(root, 'work'),
|
|
32
|
+
now: () => ++clock,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
db.close();
|
|
37
|
+
await rm(root, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const ingest = (ctx: MediaCtx, name: string) =>
|
|
41
|
+
cat.ingest(ctx, { name, mime: 'image/png', body: stream(`bytes-${name}`) });
|
|
42
|
+
|
|
43
|
+
// ── CRUD ─────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
test('createCollection and listCollections round-trip', async () => {
|
|
46
|
+
const coll = await cat.createCollection(CTX, { name: 'Summer 2025' });
|
|
47
|
+
expect(coll.name).toBe('Summer 2025');
|
|
48
|
+
expect(coll.kind).toBe('custom');
|
|
49
|
+
expect(coll.visibility).toBe('normal');
|
|
50
|
+
expect(coll.fileCount).toBe(0);
|
|
51
|
+
|
|
52
|
+
const list = await cat.listCollections(CTX);
|
|
53
|
+
expect(list.some((c) => c.id === coll.id)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('createCollection trims name and rejects empty', async () => {
|
|
57
|
+
const c = await cat.createCollection(CTX, { name: ' Trip ' });
|
|
58
|
+
expect(c.name).toBe('Trip');
|
|
59
|
+
|
|
60
|
+
await expect(cat.createCollection(CTX, { name: ' ' })).rejects.toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('updateCollection renames and changes visibility', async () => {
|
|
64
|
+
const coll = await cat.createCollection(CTX, { name: 'Before' });
|
|
65
|
+
const updated = await cat.updateCollection(CTX, coll.id, {
|
|
66
|
+
name: 'After',
|
|
67
|
+
visibility: 'unlock',
|
|
68
|
+
});
|
|
69
|
+
expect(updated?.name).toBe('After');
|
|
70
|
+
expect(updated?.visibility).toBe('unlock');
|
|
71
|
+
// updateCollection with no such id returns null
|
|
72
|
+
expect(await cat.updateCollection(CTX, 'nonexistent', { name: 'X' })).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('removeCollection deletes collection but not files', async () => {
|
|
76
|
+
const coll = await cat.createCollection(CTX, { name: 'ToDelete' });
|
|
77
|
+
const file = await ingest(CTX, 'willsurvive.png');
|
|
78
|
+
await cat.addToCollection(CTX, coll.id, [file.id]);
|
|
79
|
+
|
|
80
|
+
await cat.removeCollection(CTX, coll.id);
|
|
81
|
+
|
|
82
|
+
// Collection is gone
|
|
83
|
+
const list = await cat.listCollections(CTX);
|
|
84
|
+
expect(list.some((c) => c.id === coll.id)).toBe(false);
|
|
85
|
+
// File still exists
|
|
86
|
+
expect(await cat.get(CTX, file.id)).not.toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('removeCollection silently ignores nonexistent id', async () => {
|
|
90
|
+
await expect(cat.removeCollection(CTX, 'ghost-id')).resolves.toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Membership ───────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
test('addToCollection / removeFromCollection are idempotent', async () => {
|
|
96
|
+
const coll = await cat.createCollection(CTX, { name: 'Membership' });
|
|
97
|
+
const f1 = await ingest(CTX, 'f1.png');
|
|
98
|
+
const f2 = await ingest(CTX, 'f2.png');
|
|
99
|
+
|
|
100
|
+
// Add twice — should be idempotent
|
|
101
|
+
await cat.addToCollection(CTX, coll.id, [f1.id, f2.id]);
|
|
102
|
+
await cat.addToCollection(CTX, coll.id, [f1.id]);
|
|
103
|
+
|
|
104
|
+
const page = await cat.list(CTX, { collectionId: coll.id });
|
|
105
|
+
const ids = page.items.map((f) => f.id).sort();
|
|
106
|
+
expect(ids).toEqual([f1.id, f2.id].sort());
|
|
107
|
+
|
|
108
|
+
// Remove f1; f2 remains
|
|
109
|
+
await cat.removeFromCollection(CTX, coll.id, [f1.id]);
|
|
110
|
+
const after = await cat.list(CTX, { collectionId: coll.id });
|
|
111
|
+
expect(after.items.map((f) => f.id)).toEqual([f2.id]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('addToCollection ignores unknown or other-owner file ids', async () => {
|
|
115
|
+
const coll = await cat.createCollection(CTX, { name: 'OwnerGuard' });
|
|
116
|
+
const other = await ingest(CTX2, 'other-owner.png');
|
|
117
|
+
|
|
118
|
+
// Should not throw and should not add the foreign file
|
|
119
|
+
await cat.addToCollection(CTX, coll.id, [other.id, 'completely-unknown']);
|
|
120
|
+
const page = await cat.list(CTX, { collectionId: coll.id });
|
|
121
|
+
expect(page.items).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── collectionIds on FileRecord ───────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
test('FileRecord.collectionIds reflects current memberships', async () => {
|
|
127
|
+
const c1 = await cat.createCollection(CTX, { name: 'C1' });
|
|
128
|
+
const c2 = await cat.createCollection(CTX, { name: 'C2' });
|
|
129
|
+
const file = await ingest(CTX, 'multi.png');
|
|
130
|
+
|
|
131
|
+
await cat.addToCollection(CTX, c1.id, [file.id]);
|
|
132
|
+
await cat.addToCollection(CTX, c2.id, [file.id]);
|
|
133
|
+
|
|
134
|
+
const rec = await cat.get(CTX, file.id);
|
|
135
|
+
expect(rec?.collectionIds.sort()).toEqual([c1.id, c2.id].sort());
|
|
136
|
+
expect(rec?.favorite).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── Favorites ────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
test('setFavorite lazily creates Favorites collection and toggles membership', async () => {
|
|
142
|
+
const file = await ingest(CTX, 'favfile.png');
|
|
143
|
+
|
|
144
|
+
// Not favorited initially
|
|
145
|
+
expect((await cat.get(CTX, file.id))?.favorite).toBe(false);
|
|
146
|
+
|
|
147
|
+
// Favorite it — Favorites collection should be created
|
|
148
|
+
await cat.setFavorite(CTX, file.id, true);
|
|
149
|
+
const rec = await cat.get(CTX, file.id);
|
|
150
|
+
expect(rec?.favorite).toBe(true);
|
|
151
|
+
|
|
152
|
+
const collections = await cat.listCollections(CTX);
|
|
153
|
+
const favColl = collections.find((c) => c.kind === 'favorites');
|
|
154
|
+
expect(favColl).toBeDefined();
|
|
155
|
+
expect(favColl?.name).toBe('Favorites');
|
|
156
|
+
expect(favColl?.fileCount).toBe(1);
|
|
157
|
+
|
|
158
|
+
// Unfavorite
|
|
159
|
+
await cat.setFavorite(CTX, file.id, false);
|
|
160
|
+
expect((await cat.get(CTX, file.id))?.favorite).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('setFavorite is idempotent (toggle twice stays in correct state)', async () => {
|
|
164
|
+
const file = await ingest(CTX, 'idem.png');
|
|
165
|
+
await cat.setFavorite(CTX, file.id, true);
|
|
166
|
+
await cat.setFavorite(CTX, file.id, true); // idempotent
|
|
167
|
+
expect((await cat.get(CTX, file.id))?.favorite).toBe(true);
|
|
168
|
+
await cat.setFavorite(CTX, file.id, false);
|
|
169
|
+
await cat.setFavorite(CTX, file.id, false); // idempotent
|
|
170
|
+
expect((await cat.get(CTX, file.id))?.favorite).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('Favorites collection is a singleton per owner', async () => {
|
|
174
|
+
const f1 = await ingest(CTX, 'sing1.png');
|
|
175
|
+
const f2 = await ingest(CTX, 'sing2.png');
|
|
176
|
+
await cat.setFavorite(CTX, f1.id, true);
|
|
177
|
+
await cat.setFavorite(CTX, f2.id, true);
|
|
178
|
+
|
|
179
|
+
const collections = await cat.listCollections(CTX);
|
|
180
|
+
expect(collections.filter((c) => c.kind === 'favorites')).toHaveLength(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('favorite=true filter on list returns only favorited files', async () => {
|
|
184
|
+
// Create a clean ctx3 so we start fresh
|
|
185
|
+
const CTX3: MediaCtx = { userId: 'u3', tenantId: 'owner3' };
|
|
186
|
+
const a = await ingest(CTX3, 'favA.png');
|
|
187
|
+
const b = await ingest(CTX3, 'favB.png');
|
|
188
|
+
await ingest(CTX3, 'notFav.png');
|
|
189
|
+
|
|
190
|
+
await cat.setFavorite(CTX3, a.id, true);
|
|
191
|
+
await cat.setFavorite(CTX3, b.id, true);
|
|
192
|
+
|
|
193
|
+
const page = await cat.list(CTX3, { favorite: true });
|
|
194
|
+
const ids = page.items.map((f) => f.id).sort();
|
|
195
|
+
expect(ids).toEqual([a.id, b.id].sort());
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('favorite=true with no Favorites collection returns empty page', async () => {
|
|
199
|
+
const CTX4: MediaCtx = { userId: 'u4', tenantId: 'owner4' };
|
|
200
|
+
await ingest(CTX4, 'nofav.png');
|
|
201
|
+
const page = await cat.list(CTX4, { favorite: true });
|
|
202
|
+
expect(page.items).toHaveLength(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('Favorites collection cannot be removed', async () => {
|
|
206
|
+
const f = await ingest(CTX, 'keepfav.png');
|
|
207
|
+
await cat.setFavorite(CTX, f.id, true);
|
|
208
|
+
const colls = await cat.listCollections(CTX);
|
|
209
|
+
const favColl = colls.find((c) => c.kind === 'favorites')!;
|
|
210
|
+
await expect(cat.removeCollection(CTX, favColl.id)).rejects.toThrow('Favorites');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ── Unlock-gated visibility ───────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
test('unlock-gated collection excluded from listCollections by default', async () => {
|
|
216
|
+
const coll = await cat.createCollection(CTX, { name: 'Secret', visibility: 'unlock' });
|
|
217
|
+
|
|
218
|
+
const defaultList = await cat.listCollections(CTX);
|
|
219
|
+
expect(defaultList.some((c) => c.id === coll.id)).toBe(false);
|
|
220
|
+
|
|
221
|
+
const fullList = await cat.listCollections(CTX, { includeUnlockable: true });
|
|
222
|
+
expect(fullList.some((c) => c.id === coll.id)).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('list with unlock-gated collectionId returns empty unless includeUnlockable', async () => {
|
|
226
|
+
const coll = await cat.createCollection(CTX, { name: 'Hidden', visibility: 'unlock' });
|
|
227
|
+
const file = await ingest(CTX, 'hidden.png');
|
|
228
|
+
await cat.addToCollection(CTX, coll.id, [file.id]);
|
|
229
|
+
|
|
230
|
+
const gated = await cat.list(CTX, { collectionId: coll.id });
|
|
231
|
+
expect(gated.items).toHaveLength(0);
|
|
232
|
+
|
|
233
|
+
const open = await cat.list(CTX, { collectionId: coll.id, includeUnlockable: true });
|
|
234
|
+
expect(open.items.some((f) => f.id === file.id)).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── Owner isolation ───────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
test('owner isolation: collections and memberships are tenant-scoped', async () => {
|
|
240
|
+
const collA = await cat.createCollection(CTX, { name: 'AOnly' });
|
|
241
|
+
const collB = await cat.createCollection(CTX2, { name: 'BOnly' });
|
|
242
|
+
|
|
243
|
+
const fileA = await ingest(CTX, 'a-file.png');
|
|
244
|
+
const fileB = await ingest(CTX2, 'b-file.png');
|
|
245
|
+
await cat.addToCollection(CTX, collA.id, [fileA.id]);
|
|
246
|
+
await cat.addToCollection(CTX2, collB.id, [fileB.id]);
|
|
247
|
+
|
|
248
|
+
// CTX can only see its own collections
|
|
249
|
+
const listA = await cat.listCollections(CTX);
|
|
250
|
+
expect(listA.some((c) => c.id === collB.id)).toBe(false);
|
|
251
|
+
|
|
252
|
+
// CTX cannot see files from collB
|
|
253
|
+
const pageA = await cat.list(CTX, { collectionId: collB.id });
|
|
254
|
+
expect(pageA.items).toHaveLength(0);
|
|
255
|
+
|
|
256
|
+
// CTX2 cannot see files from collA
|
|
257
|
+
const pageB = await cat.list(CTX2, { collectionId: collA.id });
|
|
258
|
+
expect(pageB.items).toHaveLength(0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ── Cascade on file remove ────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
test('removing a file removes its collection memberships', async () => {
|
|
264
|
+
const coll = await cat.createCollection(CTX, { name: 'CascadeTest' });
|
|
265
|
+
const file = await ingest(CTX, 'cascade.png');
|
|
266
|
+
await cat.addToCollection(CTX, coll.id, [file.id]);
|
|
267
|
+
|
|
268
|
+
// Confirm it's in there
|
|
269
|
+
const before = await cat.list(CTX, { collectionId: coll.id });
|
|
270
|
+
expect(before.items.some((f) => f.id === file.id)).toBe(true);
|
|
271
|
+
|
|
272
|
+
await cat.remove(CTX, file.id);
|
|
273
|
+
|
|
274
|
+
const after = await cat.list(CTX, { collectionId: coll.id });
|
|
275
|
+
expect(after.items.some((f) => f.id === file.id)).toBe(false);
|
|
276
|
+
// Collection itself still exists
|
|
277
|
+
const colls = await cat.listCollections(CTX);
|
|
278
|
+
expect(colls.some((c) => c.id === coll.id)).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('removing a collection removes memberships but not files', async () => {
|
|
282
|
+
const coll = await cat.createCollection(CTX, { name: 'CollCascade' });
|
|
283
|
+
const file = await ingest(CTX, 'survives.png');
|
|
284
|
+
await cat.addToCollection(CTX, coll.id, [file.id]);
|
|
285
|
+
|
|
286
|
+
await cat.removeCollection(CTX, coll.id);
|
|
287
|
+
|
|
288
|
+
// File survives
|
|
289
|
+
expect(await cat.get(CTX, file.id)).not.toBeNull();
|
|
290
|
+
// collectionIds no longer includes the removed collection
|
|
291
|
+
const rec = await cat.get(CTX, file.id);
|
|
292
|
+
expect(rec?.collectionIds.includes(coll.id)).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import { runMigrations } from '../../migration/runner';
|
|
8
|
+
import { createDiskStore } from '../diskStore';
|
|
9
|
+
import { createStreamTokenService } from '../streamToken';
|
|
10
|
+
import type { MediaCtx, MediaStore } from '../types';
|
|
11
|
+
import { createSqliteFileCatalogue, fileBinaryKey } from './catalogue';
|
|
12
|
+
import { mountFileRoutes } from './fileRoutes';
|
|
13
|
+
import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
|
|
14
|
+
import type { FileCatalogue, PrivateFileCatalogue } from './types';
|
|
15
|
+
|
|
16
|
+
// PUT /_files/replace/:id — the editor's "overwrite original" byte edge.
|
|
17
|
+
|
|
18
|
+
const CTX: MediaCtx = { userId: 'u1', tenantId: 't1' };
|
|
19
|
+
const streamToken = createStreamTokenService({ secret: 'stream' });
|
|
20
|
+
const stream = (s: string) =>
|
|
21
|
+
new Response(new TextEncoder().encode(s)).body as ReadableStream<Uint8Array>;
|
|
22
|
+
|
|
23
|
+
describe('fileRoutes — replace', () => {
|
|
24
|
+
let db: Database;
|
|
25
|
+
let root: string;
|
|
26
|
+
let store: MediaStore;
|
|
27
|
+
let cat: FileCatalogue & PrivateFileCatalogue;
|
|
28
|
+
let authed = true;
|
|
29
|
+
const app = new Hono();
|
|
30
|
+
let fileId: string;
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
db = new Database(':memory:');
|
|
34
|
+
runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
|
|
35
|
+
root = await mkdtemp(path.join(tmpdir(), 'fr-replace-'));
|
|
36
|
+
store = createDiskStore(root);
|
|
37
|
+
cat = createSqliteFileCatalogue({
|
|
38
|
+
db,
|
|
39
|
+
store,
|
|
40
|
+
workDir: path.join(root, 'w'),
|
|
41
|
+
owner: (c) => c.userId,
|
|
42
|
+
});
|
|
43
|
+
mountFileRoutes(app, {
|
|
44
|
+
store,
|
|
45
|
+
token: streamToken,
|
|
46
|
+
catalogue: cat,
|
|
47
|
+
authorizeUpload: () => (authed ? CTX : null),
|
|
48
|
+
});
|
|
49
|
+
fileId = (await cat.ingest(CTX, { name: 'doc.txt', mime: 'text/plain', body: stream('old') }))
|
|
50
|
+
.id;
|
|
51
|
+
});
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
db.close();
|
|
54
|
+
await rm(root, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('replaces bytes under the same id', async () => {
|
|
58
|
+
const res = await app.request(`/_files/replace/${fileId}`, {
|
|
59
|
+
method: 'PUT',
|
|
60
|
+
headers: { 'content-type': 'text/plain', 'x-filename': 'doc-v2.txt' },
|
|
61
|
+
body: 'brand new bytes',
|
|
62
|
+
});
|
|
63
|
+
expect(res.status).toBe(200);
|
|
64
|
+
const { file } = (await res.json()) as { file: { id: string; name: string; size: number } };
|
|
65
|
+
expect(file.id).toBe(fileId);
|
|
66
|
+
expect(file.name).toBe('doc-v2.txt');
|
|
67
|
+
const bytes = await store.get(fileBinaryKey(CTX.userId, fileId));
|
|
68
|
+
expect(new TextDecoder().decode(bytes as Uint8Array)).toBe('brand new bytes');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('unknown id → 404; unauthorized → 401', async () => {
|
|
72
|
+
const missing = await app.request('/_files/replace/nope', {
|
|
73
|
+
method: 'PUT',
|
|
74
|
+
headers: { 'content-type': 'text/plain' },
|
|
75
|
+
body: 'x',
|
|
76
|
+
});
|
|
77
|
+
expect(missing.status).toBe(404);
|
|
78
|
+
|
|
79
|
+
authed = false;
|
|
80
|
+
const denied = await app.request(`/_files/replace/${fileId}`, {
|
|
81
|
+
method: 'PUT',
|
|
82
|
+
headers: { 'content-type': 'text/plain' },
|
|
83
|
+
body: 'x',
|
|
84
|
+
});
|
|
85
|
+
expect(denied.status).toBe(401);
|
|
86
|
+
authed = true;
|
|
87
|
+
});
|
|
88
|
+
});
|