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,37 @@
|
|
|
1
|
+
import { compileOriginMatcher, parseOriginList, siteOriginRules } from 'cwip/http';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
3
|
+
/** Build the Hono middleware from {@link CorsOpts}. Compiles the matcher once. */
|
|
4
|
+
export const corsAllowlist = (opts) => {
|
|
5
|
+
const rules = [
|
|
6
|
+
...(opts.siteDomain ? siteOriginRules(opts.siteDomain) : []),
|
|
7
|
+
...(opts.origins ?? []),
|
|
8
|
+
];
|
|
9
|
+
const isAllowed = compileOriginMatcher(rules);
|
|
10
|
+
return cors({
|
|
11
|
+
origin: (origin) => {
|
|
12
|
+
if (isAllowed(origin)) {
|
|
13
|
+
return origin;
|
|
14
|
+
}
|
|
15
|
+
if (origin) {
|
|
16
|
+
opts.onReject?.(origin);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
credentials: opts.credentials ?? true,
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve {@link CorsOpts} from the conventional env vars, `undefined` when
|
|
25
|
+
* neither is set (same-origin deploy — no CORS middleware needed):
|
|
26
|
+
* - `CORS_SITE_DOMAIN=example.com` — the systemic knob: apex + every subdomain.
|
|
27
|
+
* - `CORS_ORIGINS=https://a.com,*.b.com` — exact/wildcard extras (or the whole
|
|
28
|
+
* list, for apps that predate `CORS_SITE_DOMAIN`).
|
|
29
|
+
*/
|
|
30
|
+
export const corsOptsFromEnv = (env = process.env) => {
|
|
31
|
+
const siteDomain = env.CORS_SITE_DOMAIN?.trim() || undefined;
|
|
32
|
+
const origins = parseOriginList(env.CORS_ORIGINS);
|
|
33
|
+
if (!siteDomain && origins.length === 0) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return { siteDomain, origins };
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ErrorHandler } from 'hono';
|
|
2
|
+
import type { CursedbeltEnv } from '../context';
|
|
3
|
+
/**
|
|
4
|
+
* Hono `onError` handler. Converts ANY thrown value into the ONE canonical rich
|
|
5
|
+
* envelope — `{ error: { name, message, code?, status?, category?, context?,
|
|
6
|
+
* isOperational, timestamp }, correlationId? }` (the shared `toErrorEnvelope` from
|
|
7
|
+
* `cwip/error`, re-exported via cursedbelt-core/errors) — plus the right HTTP status,
|
|
8
|
+
* and echoes the correlation id header so a client can quote it in a bug report.
|
|
9
|
+
* This is the Hono twin of cwip's express `errorHandler`; both funnel through the
|
|
10
|
+
* same formatter, so their JSON is byte-identical for the same thrown error.
|
|
11
|
+
*
|
|
12
|
+
* - `ApiError` / any `AppError` → its `status` (else 500) + full summary.
|
|
13
|
+
* - `HTTPException` → normalized to an `ApiError` (its status + message).
|
|
14
|
+
* - anything else → 500 + a generic message; the real error is
|
|
15
|
+
* logged with the correlation id (never leaked).
|
|
16
|
+
*/
|
|
17
|
+
export declare const errorEnvelope: ErrorHandler<CursedbeltEnv>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { HTTPException } from 'hono/http-exception';
|
|
2
|
+
import { ApiError, isAppError, toErrorEnvelope } from '../errors';
|
|
3
|
+
/**
|
|
4
|
+
* Hono `onError` handler. Converts ANY thrown value into the ONE canonical rich
|
|
5
|
+
* envelope — `{ error: { name, message, code?, status?, category?, context?,
|
|
6
|
+
* isOperational, timestamp }, correlationId? }` (the shared `toErrorEnvelope` from
|
|
7
|
+
* `cwip/error`, re-exported via cursedbelt-core/errors) — plus the right HTTP status,
|
|
8
|
+
* and echoes the correlation id header so a client can quote it in a bug report.
|
|
9
|
+
* This is the Hono twin of cwip's express `errorHandler`; both funnel through the
|
|
10
|
+
* same formatter, so their JSON is byte-identical for the same thrown error.
|
|
11
|
+
*
|
|
12
|
+
* - `ApiError` / any `AppError` → its `status` (else 500) + full summary.
|
|
13
|
+
* - `HTTPException` → normalized to an `ApiError` (its status + message).
|
|
14
|
+
* - anything else → 500 + a generic message; the real error is
|
|
15
|
+
* logged with the correlation id (never leaked).
|
|
16
|
+
*/
|
|
17
|
+
export const errorEnvelope = (err, c) => {
|
|
18
|
+
const cid = c.get('correlationId');
|
|
19
|
+
if (cid)
|
|
20
|
+
c.header('x-correlation-id', cid);
|
|
21
|
+
// Hono's own thrown errors carry a status + safe message — normalize to an
|
|
22
|
+
// `ApiError` so they flow through the single rich-envelope path.
|
|
23
|
+
const normalized = err instanceof HTTPException ? new ApiError(err.status, err.message || 'Error') : err;
|
|
24
|
+
// Unexpected (non-`AppError`) errors: log the real thing (with the correlation
|
|
25
|
+
// id for cross-reference). The envelope stays sanitized — `toErrorEnvelope`
|
|
26
|
+
// returns a generic 500 message for non-`AppError`s, so internals never leak.
|
|
27
|
+
if (!isAppError(normalized)) {
|
|
28
|
+
console.error(`[error]${cid ? ` [${cid}]` : ''}`, err);
|
|
29
|
+
}
|
|
30
|
+
const envelope = toErrorEnvelope(normalized, { correlationId: cid });
|
|
31
|
+
return c.json(envelope, (envelope.error.status ?? 500));
|
|
32
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { correlationId } from './correlationId';
|
|
2
|
+
export { type CorsOpts, corsAllowlist, corsOptsFromEnv } from './corsAllowlist';
|
|
3
|
+
export { errorEnvelope } from './errorEnvelope';
|
|
4
|
+
export { crossOriginRefusal } from './localOrigin';
|
|
5
|
+
export { clientIp, createRateLimiter, type RateLimitOpts } from './rateLimit';
|
|
6
|
+
export { type RequestLoggerOpts, requestLogger } from './requestLogger';
|
|
7
|
+
export { type CspDirectives, type SecurityHeadersOpts, securityHeaders, } from './securityHeaders';
|
|
8
|
+
export { toValidationIssues, VALIDATION_ERROR_CODE, type ValidationIssue, validationEnvelope, } from '../errors';
|
|
9
|
+
export { type ZValidatorTarget, zValidator } from './zValidator';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The cross-cutting cursedbelt server middleware set.
|
|
2
|
+
export { correlationId } from './correlationId';
|
|
3
|
+
export { corsAllowlist, corsOptsFromEnv } from './corsAllowlist';
|
|
4
|
+
export { errorEnvelope } from './errorEnvelope';
|
|
5
|
+
export { crossOriginRefusal } from './localOrigin';
|
|
6
|
+
export { clientIp, createRateLimiter } from './rateLimit';
|
|
7
|
+
export { requestLogger } from './requestLogger';
|
|
8
|
+
export { securityHeaders, } from './securityHeaders';
|
|
9
|
+
export { toValidationIssues, VALIDATION_ERROR_CODE, validationEnvelope, } from '../errors';
|
|
10
|
+
export { zValidator } from './zValidator';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The second lock on a LOOPBACK-ONLY HTTP daemon — the one binding to 127.0.0.1
|
|
3
|
+
* does not provide.
|
|
4
|
+
*
|
|
5
|
+
* ── The gap, measured 2026-08-06 against a live daemon ──────────────────────
|
|
6
|
+
* A machine-local daemon's whole trust model is "it only listens on 127.0.0.1",
|
|
7
|
+
* and for the NETWORK that is a real wall. For the owner's BROWSER it is not,
|
|
8
|
+
* for two reasons that have nothing to do with binding:
|
|
9
|
+
*
|
|
10
|
+
* 1. **CSRF.** Any page on any website can `POST http://127.0.0.1:<port>/...`.
|
|
11
|
+
* A cross-origin POST normally trips a CORS preflight the server would fail,
|
|
12
|
+
* but a preflight is only required for a NON-simple request — and
|
|
13
|
+
* `<form enctype="text/plain">` is simple, so it fires with no preflight at
|
|
14
|
+
* all. A body parser that calls `req.json()` parses it regardless of
|
|
15
|
+
* Content-Type, and the classic `text/plain` form trick smuggles valid JSON
|
|
16
|
+
* straight through. Verified by hand against the station daemon on :4747: a
|
|
17
|
+
* POST carrying `Content-Type: text/plain`, `Origin: https://evil.example.com`
|
|
18
|
+
* and `Sec-Fetch-Site: cross-site` was accepted, parsed, and reached field
|
|
19
|
+
* validation. The attacker cannot READ the reply (no CORS headers, so it
|
|
20
|
+
* stays opaque) — but destruction does not need a reply.
|
|
21
|
+
*
|
|
22
|
+
* 2. **DNS rebinding.** With no Host check, an attacker domain whose DNS flips
|
|
23
|
+
* to 127.0.0.1 becomes same-origin in the browser's eyes — which lifts the
|
|
24
|
+
* opaque-response limit above and turns every GET into an exfiltration
|
|
25
|
+
* channel.
|
|
26
|
+
*
|
|
27
|
+
* ── What this refuses, and why it costs nothing ─────────────────────────────
|
|
28
|
+
* A refusal needs a browser-only signal to be present and WRONG. Real local
|
|
29
|
+
* clients — curl, a bun `fetch` in an agent, a server-side proxy — send no
|
|
30
|
+
* `Origin` and no `Sec-Fetch-Site`, so they are untouched. A page served from
|
|
31
|
+
* the daemon itself is same-origin. A loopback SPA on ANOTHER port is
|
|
32
|
+
* `same-site` with a loopback `Origin`, and is allowed on purpose: the wall
|
|
33
|
+
* being defended is "not the local machine", not "not another port".
|
|
34
|
+
*
|
|
35
|
+
* This deliberately does NOT add a login. Loopback + owner-only remains the
|
|
36
|
+
* design for the daemons that use it; this only closes the hole where
|
|
37
|
+
* "loopback" silently included "every website the owner visits".
|
|
38
|
+
*
|
|
39
|
+
* ── Placement ───────────────────────────────────────────────────────────────
|
|
40
|
+
* Landed in station's own tree on 2026-08-06 and lifted here the same day, when
|
|
41
|
+
* orch (:3021) and cloud (:3022) turned out to need the identical guard —
|
|
42
|
+
* STANDARDS.md §1: three copies is one too many, and a request-header security
|
|
43
|
+
* check is not app-shaped. It sits beside `corsAllowlist` and `rateLimit`
|
|
44
|
+
* because it is the same kind of thing: a pure, header-only verdict about
|
|
45
|
+
* whether a request may be served.
|
|
46
|
+
*/
|
|
47
|
+
export interface LocalOriginOpts {
|
|
48
|
+
/**
|
|
49
|
+
* Names the surface in the refusal text ("the machine API", "the orchestrator
|
|
50
|
+
* engine"). The owner reads these in a log line with no other context about
|
|
51
|
+
* which daemon spoke.
|
|
52
|
+
*/
|
|
53
|
+
label?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Extra EXACT hostnames that also mean this machine — a local host-header
|
|
56
|
+
* proxy's names (`cloud.localhost`). Matched whole, never as a suffix.
|
|
57
|
+
*/
|
|
58
|
+
allowHosts?: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Why this request must be refused, or `null` to let it through.
|
|
62
|
+
*
|
|
63
|
+
* Pure and header-only so it is exhaustively testable without a socket. The
|
|
64
|
+
* caller decides the response; every current caller answers `403` with the
|
|
65
|
+
* reason as the body, and logs it — a silent refusal on a local daemon is
|
|
66
|
+
* indistinguishable from the daemon being down.
|
|
67
|
+
*
|
|
68
|
+
* @param opts see {@link LocalOriginOpts}. A bare string is accepted as the
|
|
69
|
+
* label, since that is the only thing most callers need.
|
|
70
|
+
*/
|
|
71
|
+
export declare function crossOriginRefusal(req: Request, opts?: LocalOriginOpts | string): string | null;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The second lock on a LOOPBACK-ONLY HTTP daemon — the one binding to 127.0.0.1
|
|
3
|
+
* does not provide.
|
|
4
|
+
*
|
|
5
|
+
* ── The gap, measured 2026-08-06 against a live daemon ──────────────────────
|
|
6
|
+
* A machine-local daemon's whole trust model is "it only listens on 127.0.0.1",
|
|
7
|
+
* and for the NETWORK that is a real wall. For the owner's BROWSER it is not,
|
|
8
|
+
* for two reasons that have nothing to do with binding:
|
|
9
|
+
*
|
|
10
|
+
* 1. **CSRF.** Any page on any website can `POST http://127.0.0.1:<port>/...`.
|
|
11
|
+
* A cross-origin POST normally trips a CORS preflight the server would fail,
|
|
12
|
+
* but a preflight is only required for a NON-simple request — and
|
|
13
|
+
* `<form enctype="text/plain">` is simple, so it fires with no preflight at
|
|
14
|
+
* all. A body parser that calls `req.json()` parses it regardless of
|
|
15
|
+
* Content-Type, and the classic `text/plain` form trick smuggles valid JSON
|
|
16
|
+
* straight through. Verified by hand against the station daemon on :4747: a
|
|
17
|
+
* POST carrying `Content-Type: text/plain`, `Origin: https://evil.example.com`
|
|
18
|
+
* and `Sec-Fetch-Site: cross-site` was accepted, parsed, and reached field
|
|
19
|
+
* validation. The attacker cannot READ the reply (no CORS headers, so it
|
|
20
|
+
* stays opaque) — but destruction does not need a reply.
|
|
21
|
+
*
|
|
22
|
+
* 2. **DNS rebinding.** With no Host check, an attacker domain whose DNS flips
|
|
23
|
+
* to 127.0.0.1 becomes same-origin in the browser's eyes — which lifts the
|
|
24
|
+
* opaque-response limit above and turns every GET into an exfiltration
|
|
25
|
+
* channel.
|
|
26
|
+
*
|
|
27
|
+
* ── What this refuses, and why it costs nothing ─────────────────────────────
|
|
28
|
+
* A refusal needs a browser-only signal to be present and WRONG. Real local
|
|
29
|
+
* clients — curl, a bun `fetch` in an agent, a server-side proxy — send no
|
|
30
|
+
* `Origin` and no `Sec-Fetch-Site`, so they are untouched. A page served from
|
|
31
|
+
* the daemon itself is same-origin. A loopback SPA on ANOTHER port is
|
|
32
|
+
* `same-site` with a loopback `Origin`, and is allowed on purpose: the wall
|
|
33
|
+
* being defended is "not the local machine", not "not another port".
|
|
34
|
+
*
|
|
35
|
+
* This deliberately does NOT add a login. Loopback + owner-only remains the
|
|
36
|
+
* design for the daemons that use it; this only closes the hole where
|
|
37
|
+
* "loopback" silently included "every website the owner visits".
|
|
38
|
+
*
|
|
39
|
+
* ── Placement ───────────────────────────────────────────────────────────────
|
|
40
|
+
* Landed in station's own tree on 2026-08-06 and lifted here the same day, when
|
|
41
|
+
* orch (:3021) and cloud (:3022) turned out to need the identical guard —
|
|
42
|
+
* STANDARDS.md §1: three copies is one too many, and a request-header security
|
|
43
|
+
* check is not app-shaped. It sits beside `corsAllowlist` and `rateLimit`
|
|
44
|
+
* because it is the same kind of thing: a pure, header-only verdict about
|
|
45
|
+
* whether a request may be served.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Hosts that mean THIS machine. IPv6 arrives bracketed in a Host header.
|
|
49
|
+
*
|
|
50
|
+
* `extra` is for a daemon reached through a local host-header proxy under a
|
|
51
|
+
* name that is not itself a loopback literal — the dev gateway serves
|
|
52
|
+
* `cloud.localhost`, and note that the SUFFIX is deliberately not enough on its
|
|
53
|
+
* own (`localhost.evil.example.com` must stay refused), so each such name is
|
|
54
|
+
* listed exactly rather than pattern-matched.
|
|
55
|
+
*/
|
|
56
|
+
function isLoopbackHost(hostname, extra) {
|
|
57
|
+
const h = hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
58
|
+
return (h === 'localhost' ||
|
|
59
|
+
h === '::1' ||
|
|
60
|
+
h === '0:0:0:0:0:0:0:1' ||
|
|
61
|
+
// 127.0.0.0/8 — all of it is loopback, not just 127.0.0.1.
|
|
62
|
+
/^127(\.\d{1,3}){3}$/.test(h) ||
|
|
63
|
+
extra.has(h));
|
|
64
|
+
}
|
|
65
|
+
/** The hostname of a Host header value (`127.0.0.1:4747`, `[::1]:4747`). */
|
|
66
|
+
function hostnameOf(hostHeader) {
|
|
67
|
+
const trimmed = hostHeader.trim();
|
|
68
|
+
if (trimmed.startsWith('['))
|
|
69
|
+
return trimmed.slice(0, trimmed.indexOf(']') + 1);
|
|
70
|
+
const colon = trimmed.lastIndexOf(':');
|
|
71
|
+
return colon === -1 ? trimmed : trimmed.slice(0, colon);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Why this request must be refused, or `null` to let it through.
|
|
75
|
+
*
|
|
76
|
+
* Pure and header-only so it is exhaustively testable without a socket. The
|
|
77
|
+
* caller decides the response; every current caller answers `403` with the
|
|
78
|
+
* reason as the body, and logs it — a silent refusal on a local daemon is
|
|
79
|
+
* indistinguishable from the daemon being down.
|
|
80
|
+
*
|
|
81
|
+
* @param opts see {@link LocalOriginOpts}. A bare string is accepted as the
|
|
82
|
+
* label, since that is the only thing most callers need.
|
|
83
|
+
*/
|
|
84
|
+
export function crossOriginRefusal(req, opts = {}) {
|
|
85
|
+
const { label = 'this daemon', allowHosts = [] } = typeof opts === 'string' ? { label: opts } : opts;
|
|
86
|
+
const extra = new Set(allowHosts.map((h) => h.toLowerCase()));
|
|
87
|
+
// ── DNS rebinding: the Host must name this machine ──────────────────────
|
|
88
|
+
// Checked first: a rebound Host is the case where the OTHER signals start
|
|
89
|
+
// looking legitimately same-origin, so it has to fail before they are read.
|
|
90
|
+
const host = req.headers.get('host');
|
|
91
|
+
if (host && !isLoopbackHost(hostnameOf(host), extra)) {
|
|
92
|
+
return `Host "${host}" is not a loopback name — ${label} answers only to 127.0.0.1/localhost`;
|
|
93
|
+
}
|
|
94
|
+
// ── CSRF: a browser told us where this came from; believe it ─────────────
|
|
95
|
+
const origin = req.headers.get('origin');
|
|
96
|
+
if (origin && origin !== 'null') {
|
|
97
|
+
let originHost;
|
|
98
|
+
try {
|
|
99
|
+
originHost = new URL(origin).hostname;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return `unparseable Origin "${origin}"`;
|
|
103
|
+
}
|
|
104
|
+
if (!isLoopbackHost(originHost, extra)) {
|
|
105
|
+
return `cross-origin request from "${origin}" — ${label} is local-only`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// `same-site` is deliberately allowed: a loopback SPA on another port is
|
|
109
|
+
// cross-ORIGIN but same-SITE, and it is a legitimate local caller. Anything
|
|
110
|
+
// genuinely external reports `cross-site` and never reaches here anyway,
|
|
111
|
+
// because its Origin failed the loopback test above — this is the belt to
|
|
112
|
+
// that braces, for a browser that sends Sec-Fetch-Site but omits Origin
|
|
113
|
+
// (which is what a cross-site GET navigation does).
|
|
114
|
+
const site = req.headers.get('sec-fetch-site');
|
|
115
|
+
if (site && !['same-origin', 'same-site', 'none'].includes(site.toLowerCase())) {
|
|
116
|
+
return `Sec-Fetch-Site: ${site} — ${label} does not serve other sites`;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Context, MiddlewareHandler } from 'hono';
|
|
2
|
+
/** How much of the forwarded-header chain, if any, this server may believe. */
|
|
3
|
+
export interface ClientIpOptions {
|
|
4
|
+
/**
|
|
5
|
+
* How many TRUSTED reverse proxies sit in front of this server.
|
|
6
|
+
*
|
|
7
|
+
* `0` (the default) means none: use the socket peer and ignore forwarded
|
|
8
|
+
* headers entirely. Set it to the real number of hops you control (nginx in
|
|
9
|
+
* front of Bun ⇒ `1`).
|
|
10
|
+
*/
|
|
11
|
+
trustedProxyHops?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The client IP for rate-limiting / logging.
|
|
15
|
+
*
|
|
16
|
+
* `X-Forwarded-For` is written by whoever is calling — a client can send any
|
|
17
|
+
* value it likes. Reading it unconditionally means an attacker gets a NEW
|
|
18
|
+
* rate-limit bucket on every request simply by varying the header, which
|
|
19
|
+
* silently removes the protection the limiter exists to provide. So forwarded
|
|
20
|
+
* headers are IGNORED unless you declare how many proxies you actually trust.
|
|
21
|
+
*
|
|
22
|
+
* When you do, the chain is read RIGHT-to-LEFT. A proxy appends the peer it saw
|
|
23
|
+
* to any existing header (nginx's `$proxy_add_x_forwarded_for`), so with one
|
|
24
|
+
* trusted hop the last entry is the address YOUR proxy observed, while
|
|
25
|
+
* everything to its left is caller-supplied and unverifiable. Taking the first
|
|
26
|
+
* entry — the previous behavior — is taking exactly the part an attacker
|
|
27
|
+
* controls.
|
|
28
|
+
*
|
|
29
|
+
* Falls back to `'unknown'` when no connection info is available (e.g.
|
|
30
|
+
* `app.fetch(new Request(...))` in tests).
|
|
31
|
+
*/
|
|
32
|
+
export declare function clientIp(c: Context, options?: ClientIpOptions): string;
|
|
33
|
+
export interface RateLimitOpts {
|
|
34
|
+
/** Max requests allowed per window per key. */
|
|
35
|
+
limit: number;
|
|
36
|
+
/** Sliding window length in ms. */
|
|
37
|
+
windowMs: number;
|
|
38
|
+
/** 429 body message. */
|
|
39
|
+
message?: string;
|
|
40
|
+
/** Key function (default: client IP). Use to throttle per authenticated user. */
|
|
41
|
+
keyFn?: (c: Context) => string;
|
|
42
|
+
/**
|
|
43
|
+
* How many trusted reverse proxies sit in front of this server, for the
|
|
44
|
+
* default IP key. Default `0` — forwarded headers are ignored, because a
|
|
45
|
+
* caller can forge them to mint a fresh bucket per request. See
|
|
46
|
+
* {@link ClientIpOptions}.
|
|
47
|
+
*/
|
|
48
|
+
trustedProxyHops?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A sliding-window, in-memory rate limiter as Hono middleware. Keeps a timestamp log
|
|
52
|
+
* per key, prunes entries older than the window on access, and a cheap periodic
|
|
53
|
+
* sweep drops idle keys (no dangling timers). On limit: 429 + `Retry-After` + the
|
|
54
|
+
* canonical `{ error }` envelope. Used by auth routes (task 26) + vault unlock.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createRateLimiter(opts: RateLimitOpts): MiddlewareHandler;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getConnInfo } from 'hono/bun';
|
|
2
|
+
import { ApiError, toErrorEnvelope } from '../errors';
|
|
3
|
+
/**
|
|
4
|
+
* The client IP for rate-limiting / logging.
|
|
5
|
+
*
|
|
6
|
+
* `X-Forwarded-For` is written by whoever is calling — a client can send any
|
|
7
|
+
* value it likes. Reading it unconditionally means an attacker gets a NEW
|
|
8
|
+
* rate-limit bucket on every request simply by varying the header, which
|
|
9
|
+
* silently removes the protection the limiter exists to provide. So forwarded
|
|
10
|
+
* headers are IGNORED unless you declare how many proxies you actually trust.
|
|
11
|
+
*
|
|
12
|
+
* When you do, the chain is read RIGHT-to-LEFT. A proxy appends the peer it saw
|
|
13
|
+
* to any existing header (nginx's `$proxy_add_x_forwarded_for`), so with one
|
|
14
|
+
* trusted hop the last entry is the address YOUR proxy observed, while
|
|
15
|
+
* everything to its left is caller-supplied and unverifiable. Taking the first
|
|
16
|
+
* entry — the previous behavior — is taking exactly the part an attacker
|
|
17
|
+
* controls.
|
|
18
|
+
*
|
|
19
|
+
* Falls back to `'unknown'` when no connection info is available (e.g.
|
|
20
|
+
* `app.fetch(new Request(...))` in tests).
|
|
21
|
+
*/
|
|
22
|
+
export function clientIp(c, options = {}) {
|
|
23
|
+
const hops = Math.max(0, Math.trunc(options.trustedProxyHops ?? 0));
|
|
24
|
+
if (hops > 0) {
|
|
25
|
+
const chain = (c.req.header('x-forwarded-for') ?? '')
|
|
26
|
+
.split(',')
|
|
27
|
+
.map((part) => part.trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
// The entry your own trusted hop observed. If the chain is shorter than the
|
|
30
|
+
// configured hop count the client sent fewer entries than expected — fall
|
|
31
|
+
// through to the socket rather than believing the leftmost value.
|
|
32
|
+
const trusted = chain[chain.length - hops];
|
|
33
|
+
if (trusted)
|
|
34
|
+
return trusted;
|
|
35
|
+
// `x-real-ip` is single-valued and set by the same trusted proxy.
|
|
36
|
+
const xri = c.req.header('x-real-ip');
|
|
37
|
+
if (xri?.trim())
|
|
38
|
+
return xri.trim();
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return getConnInfo(c).remote.address ?? 'unknown';
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return 'unknown';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A sliding-window, in-memory rate limiter as Hono middleware. Keeps a timestamp log
|
|
49
|
+
* per key, prunes entries older than the window on access, and a cheap periodic
|
|
50
|
+
* sweep drops idle keys (no dangling timers). On limit: 429 + `Retry-After` + the
|
|
51
|
+
* canonical `{ error }` envelope. Used by auth routes (task 26) + vault unlock.
|
|
52
|
+
*/
|
|
53
|
+
export function createRateLimiter(opts) {
|
|
54
|
+
const { limit, windowMs, message = 'Too many requests. Please try again later.', keyFn, trustedProxyHops = 0, } = opts;
|
|
55
|
+
const hits = new Map();
|
|
56
|
+
let lastSweep = Date.now();
|
|
57
|
+
const sweep = (now) => {
|
|
58
|
+
const cutoff = now - windowMs;
|
|
59
|
+
for (const [key, times] of hits) {
|
|
60
|
+
const live = times.filter((t) => t > cutoff);
|
|
61
|
+
if (live.length === 0)
|
|
62
|
+
hits.delete(key);
|
|
63
|
+
else
|
|
64
|
+
hits.set(key, live);
|
|
65
|
+
}
|
|
66
|
+
lastSweep = now;
|
|
67
|
+
};
|
|
68
|
+
return async (c, next) => {
|
|
69
|
+
const key = keyFn ? keyFn(c) : clientIp(c, { trustedProxyHops });
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const cutoff = now - windowMs;
|
|
72
|
+
// Periodically evict idle keys so the map can't grow unbounded.
|
|
73
|
+
if (now - lastSweep > windowMs)
|
|
74
|
+
sweep(now);
|
|
75
|
+
const times = (hits.get(key) ?? []).filter((t) => t > cutoff);
|
|
76
|
+
if (times.length >= limit) {
|
|
77
|
+
hits.set(key, times);
|
|
78
|
+
const retryMs = (times[0] ?? now) + windowMs - now;
|
|
79
|
+
c.header('Retry-After', String(Math.max(1, Math.ceil(retryMs / 1000))));
|
|
80
|
+
return c.json(toErrorEnvelope(new ApiError(429, message, { code: 'rate_limited' })), 429);
|
|
81
|
+
}
|
|
82
|
+
times.push(now);
|
|
83
|
+
hits.set(key, times);
|
|
84
|
+
await next();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type { MiddlewareHandler } from 'hono';
|
|
3
|
+
import type { CursedbeltEnv } from '../context';
|
|
4
|
+
import { type MetricsBuffer } from '../metrics/metricsBuffer';
|
|
5
|
+
/**
|
|
6
|
+
* Structured request/response logging. After each request it records one slim
|
|
7
|
+
* `request_metrics` row (via the batched {@link MetricsBuffer}, off the hot path)
|
|
8
|
+
* and, for errors (status ≥ 400) or slow requests, one `event_logs` row tagged with
|
|
9
|
+
* the correlation id. Attributes rows to `c.get('userId')` when the app's auth
|
|
10
|
+
* middleware has set it.
|
|
11
|
+
*
|
|
12
|
+
* It records metrics even when a downstream handler THROWS: the error is caught,
|
|
13
|
+
* the would-be status derived, the row written, then the error re-thrown so the
|
|
14
|
+
* error-envelope `onError` still produces the response.
|
|
15
|
+
*/
|
|
16
|
+
export interface RequestLoggerOpts {
|
|
17
|
+
/**
|
|
18
|
+
* The metrics buffer to write through. Provide one (and own its lifecycle via
|
|
19
|
+
* `buffer.stop()`) to share it / flush deterministically; omit to create one from
|
|
20
|
+
* `db`. Its flush timer is `unref()`'d, so an internally-created buffer is fine for
|
|
21
|
+
* long-running servers.
|
|
22
|
+
*/
|
|
23
|
+
buffer?: MetricsBuffer;
|
|
24
|
+
/** Duration (ms) at/above which a 2xx request also logs an `event_logs` row. Default: 2000. */
|
|
25
|
+
slowMs?: number;
|
|
26
|
+
/** Also write `event_logs` rows for status ≥ 400. Default: true. */
|
|
27
|
+
logErrors?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function requestLogger(db: Database, opts?: RequestLoggerOpts): MiddlewareHandler<CursedbeltEnv>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { HTTPException } from 'hono/http-exception';
|
|
2
|
+
import { isApiError } from '../errors';
|
|
3
|
+
import { createMetricsBuffer } from '../metrics/metricsBuffer';
|
|
4
|
+
import { normalizeRoute } from '../metrics/normalizeRoute';
|
|
5
|
+
function statusOfThrown(err) {
|
|
6
|
+
if (isApiError(err))
|
|
7
|
+
return err.status;
|
|
8
|
+
if (err instanceof HTTPException)
|
|
9
|
+
return err.status;
|
|
10
|
+
return 500;
|
|
11
|
+
}
|
|
12
|
+
export function requestLogger(db, opts = {}) {
|
|
13
|
+
const buffer = opts.buffer ?? createMetricsBuffer({ db });
|
|
14
|
+
const slowMs = opts.slowMs ?? 2000;
|
|
15
|
+
const logErrors = opts.logErrors ?? true;
|
|
16
|
+
const insertEvent = db.prepare(`INSERT INTO event_logs
|
|
17
|
+
(id, correlation_id, event_name, type, message, details, duration_ms, user_id, created_at)
|
|
18
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
19
|
+
return async (c, next) => {
|
|
20
|
+
const start = performance.now();
|
|
21
|
+
let thrown;
|
|
22
|
+
let didThrow = false;
|
|
23
|
+
try {
|
|
24
|
+
await next();
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
thrown = err;
|
|
28
|
+
didThrow = true;
|
|
29
|
+
}
|
|
30
|
+
const durationMs = performance.now() - start;
|
|
31
|
+
const { method, route } = normalizeRoute(c);
|
|
32
|
+
const status = didThrow ? statusOfThrown(thrown) : c.res.status;
|
|
33
|
+
const lenHeader = didThrow ? null : c.res.headers.get('content-length');
|
|
34
|
+
const bytesOut = lenHeader ? Number(lenHeader) : null;
|
|
35
|
+
const userId = c.get('userId') ?? null;
|
|
36
|
+
buffer.record({ ts: Date.now(), method, route, status, durationMs, bytesOut, userId });
|
|
37
|
+
const isError = status >= 400;
|
|
38
|
+
const isSlow = durationMs >= slowMs;
|
|
39
|
+
if ((logErrors && isError) || isSlow) {
|
|
40
|
+
const type = status >= 500 ? 'error' : isError ? 'warn' : 'info';
|
|
41
|
+
insertEvent.run(crypto.randomUUID(), c.get('correlationId') ?? null, isError ? 'http_error' : 'http_slow', type, `${method} ${route} → ${status}`, JSON.stringify({ method, route, status, durationMs: Math.round(durationMs), bytesOut }), Math.round(durationMs), userId, new Date().toISOString());
|
|
42
|
+
}
|
|
43
|
+
// Re-throw so the error-envelope onError still builds the response.
|
|
44
|
+
if (didThrow)
|
|
45
|
+
throw thrown;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import { secureHeaders } from 'hono/secure-headers';
|
|
3
|
+
/**
|
|
4
|
+
* Security headers, built on Hono's `secureHeaders` (helmet-equivalent) rather than
|
|
5
|
+
* a hand-rolled header pass — robust + maintained, per the "prefer configured
|
|
6
|
+
* libraries" mandate. cursedbelt supplies sensible cross-app defaults (strict CSP,
|
|
7
|
+
* `X-Frame-Options: DENY`, COOP, HSTS in production, …); the consuming app extends
|
|
8
|
+
* just the directives it needs (CDN hosts, embed frames) via {@link SecurityHeadersOpts}.
|
|
9
|
+
*/
|
|
10
|
+
type SecureHeadersOptions = NonNullable<Parameters<typeof secureHeaders>[0]>;
|
|
11
|
+
export type CspDirectives = NonNullable<SecureHeadersOptions['contentSecurityPolicy']>;
|
|
12
|
+
export interface SecurityHeadersOpts {
|
|
13
|
+
/**
|
|
14
|
+
* Extend/override the default CSP directives. Shallow-merged per directive, so a
|
|
15
|
+
* supplied directive REPLACES that default's value (pass the full list). Set to
|
|
16
|
+
* `false` to omit the CSP header entirely.
|
|
17
|
+
*/
|
|
18
|
+
csp?: CspDirectives | false;
|
|
19
|
+
/**
|
|
20
|
+
* `Strict-Transport-Security` value. Default: enabled in production
|
|
21
|
+
* (`NODE_ENV==='production'`) as `max-age=31536000; includeSubDomains`, off
|
|
22
|
+
* otherwise. Pass a string to force a value, or `false` to disable.
|
|
23
|
+
*/
|
|
24
|
+
hsts?: boolean | string;
|
|
25
|
+
/** Escape hatch: any other `secureHeaders` option, merged last (wins). */
|
|
26
|
+
overrides?: Partial<SecureHeadersOptions>;
|
|
27
|
+
}
|
|
28
|
+
export declare function securityHeaders(opts?: SecurityHeadersOpts): MiddlewareHandler;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { secureHeaders } from 'hono/secure-headers';
|
|
2
|
+
// A strict baseline that works for a same-origin SPA + JSON API. Apps widen the
|
|
3
|
+
// few directives they need (script/img/connect hosts, frame embeds) via `opts.csp`.
|
|
4
|
+
const DEFAULT_CSP = {
|
|
5
|
+
defaultSrc: ["'self'"],
|
|
6
|
+
baseUri: ["'self'"],
|
|
7
|
+
objectSrc: ["'none'"],
|
|
8
|
+
frameAncestors: ["'none'"],
|
|
9
|
+
formAction: ["'self'"],
|
|
10
|
+
scriptSrc: ["'self'"],
|
|
11
|
+
scriptSrcAttr: ["'none'"],
|
|
12
|
+
// Inline styles are needed for Tailwind / chart / drag transforms; inline SCRIPTS
|
|
13
|
+
// stay blocked (the real XSS barrier).
|
|
14
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
15
|
+
imgSrc: ["'self'", 'data:', 'blob:'],
|
|
16
|
+
fontSrc: ["'self'", 'data:'],
|
|
17
|
+
connectSrc: ["'self'"],
|
|
18
|
+
upgradeInsecureRequests: [],
|
|
19
|
+
};
|
|
20
|
+
export function securityHeaders(opts = {}) {
|
|
21
|
+
// A runtime escape hatch matching ca's ops convention: CSP_DISABLED=true drops CSP.
|
|
22
|
+
const cspOff = opts.csp === false || process.env.CSP_DISABLED === 'true';
|
|
23
|
+
const csp = cspOff ? undefined : { ...DEFAULT_CSP, ...(opts.csp ?? {}) };
|
|
24
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
25
|
+
const hsts = opts.hsts ?? (isProd ? 'max-age=31536000; includeSubDomains' : false);
|
|
26
|
+
return secureHeaders({
|
|
27
|
+
contentSecurityPolicy: csp,
|
|
28
|
+
xContentTypeOptions: 'nosniff',
|
|
29
|
+
// DENY (not SAMEORIGIN) to agree with CSP `frame-ancestors 'none'`.
|
|
30
|
+
xFrameOptions: 'DENY',
|
|
31
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
32
|
+
crossOriginOpenerPolicy: 'same-origin',
|
|
33
|
+
// We serve cross-origin media/fonts, so keep CORP permissive.
|
|
34
|
+
crossOriginResourcePolicy: 'cross-origin',
|
|
35
|
+
// COEP breaks third-party embeds (YouTube, CDNs) — off by default.
|
|
36
|
+
crossOriginEmbedderPolicy: false,
|
|
37
|
+
originAgentCluster: true,
|
|
38
|
+
xDnsPrefetchControl: 'off',
|
|
39
|
+
xPermittedCrossDomainPolicies: 'none',
|
|
40
|
+
strictTransportSecurity: hsts,
|
|
41
|
+
removePoweredBy: true,
|
|
42
|
+
...opts.overrides,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Env, MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { ZodType, z } from 'zod';
|
|
3
|
+
import type { CursedbeltEnv } from '../context';
|
|
4
|
+
/** The request locations a schema can validate. Mirrors Hono's body/non-body split. */
|
|
5
|
+
export type ZValidatorTarget = 'json' | 'form' | 'query' | 'param';
|
|
6
|
+
/**
|
|
7
|
+
* Validate a request `target` against a zod `schema`. Mount per route ahead of the
|
|
8
|
+
* handler:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* register('post', '/notes', zValidator(CreateNoteSchema), (c) => {
|
|
12
|
+
* const body = c.req.valid('json'); // typed z.infer<typeof CreateNoteSchema>
|
|
13
|
+
* // …
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* On failure responds `400` with the canonical envelope (field errors under
|
|
18
|
+
* `context.issues`); never throws, so it produces the same body whether or not the
|
|
19
|
+
* app mounts the {@link errorEnvelope} `onError` handler. On success the parsed value
|
|
20
|
+
* is available as both `c.req.valid(target)` and `c.get('validatedBody')`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function zValidator<Schema extends ZodType, Target extends ZValidatorTarget = 'json', E extends Env = CursedbeltEnv, P extends string = string>(schema: Schema, target?: Target): MiddlewareHandler<E, P, {
|
|
23
|
+
in: {
|
|
24
|
+
[K in Target]: z.input<Schema>;
|
|
25
|
+
};
|
|
26
|
+
out: {
|
|
27
|
+
[K in Target]: z.output<Schema>;
|
|
28
|
+
};
|
|
29
|
+
}>;
|