@cosmicdrift/kumiko-framework 0.1.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/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type { Context, Next } from "hono";
|
|
3
|
+
import { getCookie } from "hono/cookie";
|
|
4
|
+
import { CSRF_COOKIE_NAME, CSRF_HEADER_NAME, getAuthTransport } from "./auth-middleware";
|
|
5
|
+
|
|
6
|
+
// Methods that can mutate server state. GET/HEAD/OPTIONS are safe under
|
|
7
|
+
// CORS + SameSite-cookie semantics and skip the CSRF check entirely.
|
|
8
|
+
const STATE_CHANGING_METHODS: ReadonlySet<string> = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
9
|
+
|
|
10
|
+
// Constant-time byte compare. `a !== b` short-circuits at the first
|
|
11
|
+
// differing byte and leaks the common prefix length to anyone who can
|
|
12
|
+
// time requests — in principle exploitable against sufficiently small
|
|
13
|
+
// tokens. CSRF tokens are UUIDs so the practical risk is low, but this
|
|
14
|
+
// is the standard production pattern for any secret-vs-secret compare.
|
|
15
|
+
// Length-check first because timingSafeEqual throws on size mismatch;
|
|
16
|
+
// the length itself isn't a secret (the UUID format is known).
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
function tokensMatch(a: string, b: string): boolean {
|
|
19
|
+
if (a.length !== b.length) return false;
|
|
20
|
+
return timingSafeEqual(encoder.encode(a), encoder.encode(b));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Double-submit CSRF guard. Runs AFTER authMiddleware — reads the
|
|
24
|
+
// authTransport flag set there. Only enforces on cookie-authenticated,
|
|
25
|
+
// state-changing requests. Bearer-auth requests skip the check because
|
|
26
|
+
// browsers cannot set the Authorization header on cross-origin requests
|
|
27
|
+
// (same-origin policy), so there is no CSRF vector to defend against.
|
|
28
|
+
//
|
|
29
|
+
// Mechanic: the framework sets two cookies on login — `kumiko_auth`
|
|
30
|
+
// (HttpOnly, carries the JWT) and `kumiko_csrf` (JS-readable, carries a
|
|
31
|
+
// token). The web client reads `kumiko_csrf` from document.cookie and
|
|
32
|
+
// echoes the value in an `X-CSRF-Token` header on every state-changing
|
|
33
|
+
// request. An attacker on bad.com cannot read the cookie (same-origin)
|
|
34
|
+
// and therefore cannot forge the header, so any cross-site POST from the
|
|
35
|
+
// attacker's page will fail the match even if the browser sent the
|
|
36
|
+
// cookies along (which SameSite=Lax already prevents for all methods
|
|
37
|
+
// other than top-level GETs — CSRF-middleware is belt-and-braces).
|
|
38
|
+
//
|
|
39
|
+
// Token rotation: issued at login + switch-tenant only, tied to the same
|
|
40
|
+
// lifetime as the auth-cookie. No per-request rotation — that's the
|
|
41
|
+
// Synchronizer Token pattern, needed only when token leakage via URL
|
|
42
|
+
// logs or referrers is on the threat model. We keep cookies out of URLs.
|
|
43
|
+
export function csrfMiddleware() {
|
|
44
|
+
return async (c: Context, next: Next) => {
|
|
45
|
+
// Not authenticated (public route) or bearer-only — no CSRF vector.
|
|
46
|
+
const transport = getAuthTransport(c);
|
|
47
|
+
if (transport !== "cookie") return next();
|
|
48
|
+
|
|
49
|
+
// Safe method — no CSRF check. SameSite=Lax blocks cross-site
|
|
50
|
+
// navigation-GETs from sending cookies, which is the only plausible
|
|
51
|
+
// CSRF-via-GET vector.
|
|
52
|
+
if (!STATE_CHANGING_METHODS.has(c.req.method)) return next();
|
|
53
|
+
|
|
54
|
+
const cookieToken = getCookie(c, CSRF_COOKIE_NAME);
|
|
55
|
+
const headerToken = c.req.header(CSRF_HEADER_NAME);
|
|
56
|
+
|
|
57
|
+
// Both must exist and match byte-for-byte. A missing cookie means the
|
|
58
|
+
// token was never issued (stale session or cross-origin attempt);
|
|
59
|
+
// a missing header means the client didn't attach it (attacker's
|
|
60
|
+
// cross-origin form submission can't read the cookie to forge one).
|
|
61
|
+
if (!cookieToken || !headerToken || !tokensMatch(cookieToken, headerToken)) {
|
|
62
|
+
return c.json(
|
|
63
|
+
{
|
|
64
|
+
error: {
|
|
65
|
+
code: "csrf_token_mismatch",
|
|
66
|
+
httpStatus: 403,
|
|
67
|
+
message: "csrf token missing or mismatch",
|
|
68
|
+
i18nKey: "auth.errors.csrfTokenMismatch",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
403,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return next();
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type { SetTenantCookieOptions } from "./anonymous-cookie";
|
|
2
|
+
export { deleteTenantCookie, setTenantCookie } from "./anonymous-cookie";
|
|
3
|
+
export type {
|
|
4
|
+
AnonymousAccessConfig,
|
|
5
|
+
AuthMiddlewareOptions,
|
|
6
|
+
AuthSessionChecker,
|
|
7
|
+
AuthSessionStatus,
|
|
8
|
+
TenantExists,
|
|
9
|
+
TenantResolver,
|
|
10
|
+
} from "./auth-middleware";
|
|
11
|
+
export { authMiddleware, getUser } from "./auth-middleware";
|
|
12
|
+
export type {
|
|
13
|
+
AuthRoutesConfig,
|
|
14
|
+
LoginRateLimiter,
|
|
15
|
+
SessionChecker,
|
|
16
|
+
SessionCreator,
|
|
17
|
+
SessionMetadata,
|
|
18
|
+
SessionRevoker,
|
|
19
|
+
} from "./auth-routes";
|
|
20
|
+
export { createAuthRoutes, createInMemoryLoginRateLimiter } from "./auth-routes";
|
|
21
|
+
export type { JwtHelper, JwtPayload } from "./jwt";
|
|
22
|
+
export { createJwtHelper } from "./jwt";
|
|
23
|
+
export { type RequestContextData, requestContext } from "./request-context";
|
|
24
|
+
export { requestIdMiddleware } from "./request-id-middleware";
|
|
25
|
+
export { createApiRoutes } from "./routes";
|
|
26
|
+
export type { KumikoServer, ServerOptions } from "./server";
|
|
27
|
+
export { buildServer } from "./server";
|
|
28
|
+
export type { SseBroker, SseClient, SseEvent } from "./sse-broker";
|
|
29
|
+
export { createSseBroker } from "./sse-broker";
|
|
30
|
+
export { createSseRoute, SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route";
|
|
31
|
+
export { generateToken } from "./tokens";
|
package/src/api/jwt.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as jose from "jose";
|
|
2
|
+
import type { DbRow } from "../db/connection";
|
|
3
|
+
import type { SessionUser, TenantId } from "../engine/types";
|
|
4
|
+
|
|
5
|
+
export type JwtPayload = {
|
|
6
|
+
// JWT `sub` is a string per RFC 7519. Matches SessionUser.id — a UUID-string
|
|
7
|
+
// under the ES migration. `sign()` already stringifies via String(user.id);
|
|
8
|
+
// `verify()` just passes it through.
|
|
9
|
+
sub: string;
|
|
10
|
+
tenantId: TenantId;
|
|
11
|
+
roles: string[];
|
|
12
|
+
// Optional — present when a feature has registered auth claims via the
|
|
13
|
+
// `r.authClaims()` hook system. Absent for stateless-JWT deployments
|
|
14
|
+
// without auth-claims wiring.
|
|
15
|
+
claims?: Record<string, unknown>;
|
|
16
|
+
// Optional session-ID, carried in the standard `jti` JWT claim.
|
|
17
|
+
// Present when the app wires a `sessionCreator` callback (see sessions
|
|
18
|
+
// feature). Absent → stateless-JWT mode, no revocation possible.
|
|
19
|
+
jti?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type JwtHelper = {
|
|
23
|
+
sign(user: SessionUser): Promise<string>;
|
|
24
|
+
verify(token: string): Promise<JwtPayload>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function createJwtHelper(secret: string, issuer = "kumiko"): JwtHelper {
|
|
28
|
+
const encodedSecret = new TextEncoder().encode(secret);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
async sign(user) {
|
|
32
|
+
const body: Omit<JwtPayload, "sub" | "jti"> = {
|
|
33
|
+
tenantId: user.tenantId,
|
|
34
|
+
roles: [...user.roles],
|
|
35
|
+
};
|
|
36
|
+
if (user.claims) body.claims = { ...user.claims };
|
|
37
|
+
|
|
38
|
+
const builder = new jose.SignJWT(body)
|
|
39
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
40
|
+
.setSubject(String(user.id))
|
|
41
|
+
.setIssuer(issuer)
|
|
42
|
+
.setIssuedAt()
|
|
43
|
+
.setExpirationTime("24h");
|
|
44
|
+
if (user.sid) builder.setJti(user.sid);
|
|
45
|
+
|
|
46
|
+
return builder.sign(encodedSecret);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async verify(token) {
|
|
50
|
+
const { payload } = await jose.jwtVerify(token, encodedSecret, { issuer });
|
|
51
|
+
const result: JwtPayload = {
|
|
52
|
+
sub: String(payload.sub),
|
|
53
|
+
tenantId: payload["tenantId"] as string,
|
|
54
|
+
roles: payload["roles"] as string[],
|
|
55
|
+
};
|
|
56
|
+
const claims = payload["claims"];
|
|
57
|
+
if (claims && typeof claims === "object") {
|
|
58
|
+
result.claims = claims as DbRow;
|
|
59
|
+
}
|
|
60
|
+
if (typeof payload.jti === "string") {
|
|
61
|
+
result.jti = payload.jti;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Context, Next } from "hono";
|
|
2
|
+
import {
|
|
3
|
+
emitHttpRequest,
|
|
4
|
+
type Meter,
|
|
5
|
+
observabilityContext,
|
|
6
|
+
redactQueryString,
|
|
7
|
+
type SensitiveFilterConfig,
|
|
8
|
+
type Tracer,
|
|
9
|
+
} from "../observability";
|
|
10
|
+
import { getUser } from "./auth-middleware";
|
|
11
|
+
import { requestContext } from "./request-context";
|
|
12
|
+
|
|
13
|
+
// Wraps each incoming /api/* request in an `http.request` span. Must be
|
|
14
|
+
// installed AFTER requestIdMiddleware so the active request-id is available
|
|
15
|
+
// as a span attribute. Installed BEFORE auth so auth verification shows up
|
|
16
|
+
// as a child span later when auth-middleware itself is instrumented (v2).
|
|
17
|
+
|
|
18
|
+
export type ObservabilityMiddlewareOptions = {
|
|
19
|
+
readonly tracer: Tracer;
|
|
20
|
+
readonly meter: Meter;
|
|
21
|
+
readonly sensitiveConfig: SensitiveFilterConfig;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function observabilityMiddleware(opts: ObservabilityMiddlewareOptions) {
|
|
25
|
+
const { tracer, meter, sensitiveConfig } = opts;
|
|
26
|
+
|
|
27
|
+
return async (c: Context, next: Next) => {
|
|
28
|
+
const method = c.req.method;
|
|
29
|
+
const path = c.req.path;
|
|
30
|
+
const target = redactQueryString(c.req.url.replace(/^https?:\/\/[^/]+/, ""), sensitiveConfig);
|
|
31
|
+
|
|
32
|
+
// Start the root span for this request. kind=server marks it as an
|
|
33
|
+
// incoming server-side span in OTel terms.
|
|
34
|
+
const span = tracer.startSpan("http.request", {
|
|
35
|
+
kind: "server",
|
|
36
|
+
attributes: {
|
|
37
|
+
"http.method": method,
|
|
38
|
+
"http.route": path,
|
|
39
|
+
"http.target": target,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const reqCtx = requestContext.get();
|
|
44
|
+
if (reqCtx?.requestId) {
|
|
45
|
+
span.setAttribute("kumiko.request_id", reqCtx.requestId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const startTime = performance.now();
|
|
49
|
+
try {
|
|
50
|
+
await observabilityContext.run({ activeSpan: span }, () => next());
|
|
51
|
+
|
|
52
|
+
// Auth middleware runs inside `next()` and sets the user on the
|
|
53
|
+
// Hono context if the token was valid. Enrich the span after the
|
|
54
|
+
// fact so public paths (health, login) don't emit empty user attrs.
|
|
55
|
+
try {
|
|
56
|
+
const user = getUser(c);
|
|
57
|
+
if (user) {
|
|
58
|
+
span.setAttribute("kumiko.user_id", user.id);
|
|
59
|
+
span.setAttribute("kumiko.tenant_id", user.tenantId);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// getUser throws if called before auth ran — public paths, fine.
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
span.setAttribute("http.status_code", c.res.status);
|
|
66
|
+
if (c.res.status >= 500) {
|
|
67
|
+
span.setStatus("error", `HTTP ${c.res.status}`);
|
|
68
|
+
} else {
|
|
69
|
+
span.setStatus("ok");
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof Error) {
|
|
73
|
+
span.recordException(error);
|
|
74
|
+
span.setStatus("error", error.message);
|
|
75
|
+
} else {
|
|
76
|
+
span.setStatus("error", String(error));
|
|
77
|
+
}
|
|
78
|
+
span.setAttribute("http.status_code", 500);
|
|
79
|
+
throw error;
|
|
80
|
+
} finally {
|
|
81
|
+
const durationSec = (performance.now() - startTime) / 1000;
|
|
82
|
+
// c.res may be undefined on very early throws (before any route handler);
|
|
83
|
+
// fall back to 500 for the metric so the counter is always incremented.
|
|
84
|
+
const status = c.res?.status ?? 500;
|
|
85
|
+
emitHttpRequest(meter, { route: path, method, status }, durationSec);
|
|
86
|
+
span.end();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Readiness probe: runs a set of checks in parallel with a per-check timeout
|
|
2
|
+
// and aggregates into a single result. Used by /health/ready when the caller
|
|
3
|
+
// wires DB / Redis / Dispatcher — any of those down drops the probe to 503
|
|
4
|
+
// so load balancers stop routing new traffic even while `lifecycle.state()`
|
|
5
|
+
// is still "ready".
|
|
6
|
+
//
|
|
7
|
+
// Design:
|
|
8
|
+
// - Every check produces a `ReadinessCheckResult` — no thrown errors leak.
|
|
9
|
+
// - Timeout is enforced per-check, not per-probe, so a single hung dependency
|
|
10
|
+
// can't starve siblings of their budget.
|
|
11
|
+
// - Checks run in parallel — the probe is called on every kubelet/ALB poll,
|
|
12
|
+
// so total latency must stay ≈ slowest check, not sum.
|
|
13
|
+
|
|
14
|
+
import { sql } from "drizzle-orm";
|
|
15
|
+
import type Redis from "ioredis";
|
|
16
|
+
import type { DbConnection } from "../db/connection";
|
|
17
|
+
import { getAllConsumerProgress } from "../pipeline/event-dispatcher";
|
|
18
|
+
|
|
19
|
+
export type ReadinessCheck = {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly run: () => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ReadinessCheckResult = {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly ok: boolean;
|
|
27
|
+
readonly latencyMs: number;
|
|
28
|
+
readonly error?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ReadinessResult = {
|
|
32
|
+
readonly ok: boolean;
|
|
33
|
+
readonly checks: readonly ReadinessCheckResult[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ReadinessProbeOptions = {
|
|
37
|
+
readonly timeoutMs?: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const DEFAULT_TIMEOUT_MS = 2_000;
|
|
41
|
+
|
|
42
|
+
export function createReadinessProbe(
|
|
43
|
+
checks: readonly ReadinessCheck[],
|
|
44
|
+
opts: ReadinessProbeOptions = {},
|
|
45
|
+
): () => Promise<ReadinessResult> {
|
|
46
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
47
|
+
|
|
48
|
+
return async () => {
|
|
49
|
+
const results = await Promise.all(checks.map((check) => runOne(check, timeoutMs)));
|
|
50
|
+
return {
|
|
51
|
+
ok: results.every((r) => r.ok),
|
|
52
|
+
checks: results,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runOne(check: ReadinessCheck, timeoutMs: number): Promise<ReadinessCheckResult> {
|
|
58
|
+
const start = performance.now();
|
|
59
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
60
|
+
try {
|
|
61
|
+
await Promise.race([
|
|
62
|
+
check.run(),
|
|
63
|
+
new Promise<never>((_, reject) => {
|
|
64
|
+
timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
65
|
+
// Don't keep the event loop alive on a hung probe during shutdown.
|
|
66
|
+
timer.unref?.();
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
return {
|
|
70
|
+
name: check.name,
|
|
71
|
+
ok: true,
|
|
72
|
+
latencyMs: Math.round(performance.now() - start),
|
|
73
|
+
};
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return {
|
|
77
|
+
name: check.name,
|
|
78
|
+
ok: false,
|
|
79
|
+
latencyMs: Math.round(performance.now() - start),
|
|
80
|
+
error: message,
|
|
81
|
+
};
|
|
82
|
+
} finally {
|
|
83
|
+
if (timer) clearTimeout(timer);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Standard checks --------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
export function dbPingCheck(db: DbConnection): ReadinessCheck {
|
|
90
|
+
return {
|
|
91
|
+
name: "db",
|
|
92
|
+
run: async () => {
|
|
93
|
+
await db.execute(sql`SELECT 1`);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function redisPingCheck(redis: Redis): ReadinessCheck {
|
|
99
|
+
return {
|
|
100
|
+
name: "redis",
|
|
101
|
+
run: async () => {
|
|
102
|
+
const reply = await redis.ping();
|
|
103
|
+
if (reply !== "PONG") throw new Error(`unexpected PING reply: ${reply}`);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Lag-Check für den Async-Event-Dispatcher. Liest HWM + Consumer-Cursor aus
|
|
109
|
+
// der DB — kein extra Redis/Runtime-State. Fail-Schwelle ist in EventIds
|
|
110
|
+
// (bigint), weil das die natürliche Einheit ist (events sind monoton id'd).
|
|
111
|
+
// Ein Tuning-Beispiel: maxLagEvents = 1_000 bedeutet "wenn die Projection
|
|
112
|
+
// mehr als 1k Events hinter HWM ist, stoppe neue Traffic-Zuweisung".
|
|
113
|
+
export function dispatcherLagCheck(
|
|
114
|
+
db: DbConnection,
|
|
115
|
+
consumerNames: readonly string[],
|
|
116
|
+
maxLagEvents: bigint,
|
|
117
|
+
): ReadinessCheck {
|
|
118
|
+
return {
|
|
119
|
+
name: "dispatcher_lag",
|
|
120
|
+
run: async () => {
|
|
121
|
+
// skip: no registered consumers means no dispatcher active — lag check has
|
|
122
|
+
// nothing to measure. Not a failure mode, just a no-op.
|
|
123
|
+
if (consumerNames.length === 0) return;
|
|
124
|
+
const progress = await getAllConsumerProgress(db, consumerNames);
|
|
125
|
+
for (const p of progress) {
|
|
126
|
+
if (p.lag > maxLagEvents) {
|
|
127
|
+
throw new Error(`consumer "${p.name}" lag=${p.lag} exceeds threshold ${maxLagEvents}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { generateId } from "../utils";
|
|
3
|
+
|
|
4
|
+
// Request-scoped propagation. Populated by the HTTP middleware and by the
|
|
5
|
+
// event-dispatcher when it runs an MSP-apply, so ctx.appendEvent downstream
|
|
6
|
+
// automatically stamps the right provenance on every event it writes.
|
|
7
|
+
//
|
|
8
|
+
// requestId — unique per HTTP request (or Job run). Log correlation.
|
|
9
|
+
// correlationId — the end-to-end business operation id; propagates across
|
|
10
|
+
// service boundaries and MSP causation chains. Comes from
|
|
11
|
+
// the `x-correlation-id` header if set, otherwise mirrors
|
|
12
|
+
// requestId (clients that don't set the header pay no
|
|
13
|
+
// penalty — a single HTTP call == one correlation).
|
|
14
|
+
// causationId — the events.id that triggered THIS execution. Null for
|
|
15
|
+
// root HTTP commands; set when an MSP-apply is running
|
|
16
|
+
// (event-dispatcher wraps the handler call). Together
|
|
17
|
+
// with correlationId, forms a causal DAG across streams.
|
|
18
|
+
// signal — AbortSignal from the underlying HTTP request. Aborts
|
|
19
|
+
// when the client disconnects (mobile back-press, tab
|
|
20
|
+
// close). Long-running framework code (event streaming,
|
|
21
|
+
// projection rebuild) checks signal.aborted at chunk
|
|
22
|
+
// boundaries; short queries don't pay the overhead.
|
|
23
|
+
// Undefined for non-HTTP entry-points (jobs, MSP-applies).
|
|
24
|
+
export type RequestContextData = {
|
|
25
|
+
readonly requestId: string;
|
|
26
|
+
readonly correlationId: string;
|
|
27
|
+
readonly causationId?: string;
|
|
28
|
+
readonly signal?: AbortSignal;
|
|
29
|
+
// Client IP for per-IP rate limiting (L1, L2, L3 with per: "ip*").
|
|
30
|
+
// Populated by requestIdMiddleware from x-forwarded-for or the
|
|
31
|
+
// socket address. Undefined for non-HTTP entry points (jobs, MSP).
|
|
32
|
+
readonly ip?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const storage = new AsyncLocalStorage<RequestContextData>();
|
|
36
|
+
|
|
37
|
+
export const requestContext = {
|
|
38
|
+
run<T>(data: RequestContextData, fn: () => T): T {
|
|
39
|
+
return storage.run(data, fn);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
get(): RequestContextData | undefined {
|
|
43
|
+
return storage.getStore();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
generateId(): string {
|
|
47
|
+
return generateId();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Context, Next } from "hono";
|
|
2
|
+
import { requestContext } from "./request-context";
|
|
3
|
+
|
|
4
|
+
const REQUEST_ID_HEADER = "X-Request-ID";
|
|
5
|
+
const CORRELATION_ID_HEADER = "X-Correlation-ID";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Assigns a requestId + correlationId to every request and wraps execution
|
|
9
|
+
* in AsyncLocalStorage. Runs BEFORE auth — both ids are available even for
|
|
10
|
+
* 401 responses.
|
|
11
|
+
*
|
|
12
|
+
* correlationId defaults to the requestId if the client didn't set
|
|
13
|
+
* `x-correlation-id` — clients that don't care about cross-service tracing
|
|
14
|
+
* still get sensible single-request correlation for free.
|
|
15
|
+
*/
|
|
16
|
+
export function requestIdMiddleware() {
|
|
17
|
+
return async (c: Context, next: Next) => {
|
|
18
|
+
const requestId = c.req.header(REQUEST_ID_HEADER) ?? requestContext.generateId();
|
|
19
|
+
const correlationId = c.req.header(CORRELATION_ID_HEADER) ?? requestId;
|
|
20
|
+
c.header(REQUEST_ID_HEADER, requestId);
|
|
21
|
+
c.header(CORRELATION_ID_HEADER, correlationId);
|
|
22
|
+
c.set("requestId", requestId);
|
|
23
|
+
|
|
24
|
+
// Hono exposes the underlying Fetch Request — its `signal` aborts
|
|
25
|
+
// when the client disconnects (mobile back-press, tab close). We
|
|
26
|
+
// propagate it through requestContext so framework internals can
|
|
27
|
+
// honour cancellation at long-running checkpoints. Older Hono /
|
|
28
|
+
// adapter combos may not populate `c.req.raw.signal`; conditional
|
|
29
|
+
// spread keeps `signal: undefined` out of the stored record so
|
|
30
|
+
// downstream `signal?` checks behave as if no signal exists.
|
|
31
|
+
const signal = c.req.raw?.signal;
|
|
32
|
+
// Client IP for per-IP rate limiting. Trust `x-forwarded-for` when
|
|
33
|
+
// present (proxy/CDN) — first hop is the originating client. Adapter-
|
|
34
|
+
// specific socket-address fallback (bun, node) is not standardized
|
|
35
|
+
// in Hono; deployments behind a proxy should always set xff. Without
|
|
36
|
+
// either we leave `ip` undefined and skip ip-bucketed checks rather
|
|
37
|
+
// than fabricate one.
|
|
38
|
+
const xff = c.req.header("x-forwarded-for");
|
|
39
|
+
const ip = xff?.split(",")[0]?.trim();
|
|
40
|
+
await requestContext.run(
|
|
41
|
+
{
|
|
42
|
+
requestId,
|
|
43
|
+
correlationId,
|
|
44
|
+
...(signal ? { signal } : {}),
|
|
45
|
+
...(ip && ip.length > 0 ? { ip } : {}),
|
|
46
|
+
},
|
|
47
|
+
() => next(),
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
}
|