@cosmicdrift/kumiko-framework 1.0.0 → 2.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/README.md +9 -38
- package/package.json +21 -2
- package/src/__tests__/anonymous-access.integration.test.ts +185 -0
- package/src/__tests__/consumer-cli.integration.test.ts +172 -0
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
- package/src/__tests__/schema-cli.integration.test.ts +50 -0
- package/src/__tests__/store-table.integration.test.ts +93 -0
- package/src/api/__tests__/api.test.ts +373 -0
- package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
- package/src/api/__tests__/batch.integration.test.ts +101 -11
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +200 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
- package/src/api/__tests__/pat-scope.test.ts +36 -0
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
- package/src/api/__tests__/request-id-middleware.test.ts +51 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/__tests__/sse-broker.test.ts +57 -0
- package/src/api/__tests__/sse-route.test.ts +4 -0
- package/src/api/api-constants.ts +11 -0
- package/src/api/auth-middleware.ts +248 -40
- package/src/api/auth-routes.ts +576 -95
- package/src/api/index.ts +13 -4
- package/src/api/jwt.ts +170 -11
- package/src/api/pat-scope.ts +14 -0
- package/src/api/pii-leak-guard.ts +47 -0
- package/src/api/request-context.ts +3 -0
- package/src/api/request-id-middleware.ts +2 -0
- package/src/api/routes.ts +169 -2
- package/src/api/server.ts +112 -16
- package/src/api/sse-broker.ts +39 -0
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/__tests__/write-brand.test.ts +21 -0
- package/src/bun-db/connection.ts +3 -3
- package/src/bun-db/index.ts +2 -0
- package/src/bun-db/query.ts +105 -32
- package/src/consumer-cli.ts +134 -0
- package/src/crypto/__tests__/blind-index.test.ts +130 -0
- package/src/crypto/__tests__/event-pii.test.ts +161 -0
- package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
- package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
- package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
- package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
- package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
- package/src/crypto/blind-index.ts +118 -0
- package/src/crypto/event-pii.ts +70 -0
- package/src/crypto/in-memory-kms-adapter.ts +50 -0
- package/src/crypto/index.ts +67 -0
- package/src/crypto/kms-adapter.ts +2 -0
- package/src/crypto/pg-kms-adapter.ts +295 -0
- package/src/crypto/pii-field-encryption.ts +248 -0
- package/src/crypto/request-kms-cache.ts +38 -0
- package/src/crypto/subject-resolver.ts +91 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
- package/src/db/__tests__/blind-index.integration.test.ts +248 -0
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/collect-table-metas.test.ts +10 -10
- package/src/db/__tests__/config-seed.integration.test.ts +13 -5
- package/src/db/__tests__/dialect-instant.test.ts +1 -4
- package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
- package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
- package/src/db/__tests__/feature-table-sources.test.ts +34 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
- package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
- package/src/db/__tests__/located-timestamp.test.ts +19 -0
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/migrate-runner.test.ts +61 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
- package/src/db/__tests__/rebuild-marker.test.ts +13 -3
- package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
- package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
- package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
- package/src/db/api.ts +2 -2
- package/src/db/apply-entity-event.ts +18 -14
- package/src/db/blind-index-cleanup.ts +55 -0
- package/src/db/bun-provider.ts +2 -2
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/config-seed.ts +9 -9
- package/src/db/connection.ts +7 -12
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +20 -26
- package/src/db/entity-field-encryption.ts +81 -24
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +63 -90
- package/src/db/event-store-executor-context.ts +404 -0
- package/src/db/event-store-executor-read.ts +275 -0
- package/src/db/event-store-executor-write.ts +644 -0
- package/src/db/event-store-executor.ts +28 -1155
- package/src/db/feature-table-sources.ts +6 -5
- package/src/db/index.ts +20 -3
- package/src/db/located-timestamp.ts +4 -0
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/migrate-runner.ts +107 -11
- package/src/db/pg-error.ts +9 -1
- package/src/db/postgres-provider.ts +2 -2
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
- package/src/db/queries/backfill-pii.ts +277 -0
- package/src/db/queries/ddl.ts +45 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/queries/event-store.ts +126 -19
- package/src/db/queries/projection-rebuild.ts +22 -8
- package/src/db/queries/shadow-swap.ts +183 -0
- package/src/db/queries/test-stack.ts +4 -30
- package/src/db/query-api.ts +1 -0
- package/src/db/query.ts +1 -0
- package/src/db/rebuild-marker.ts +18 -2
- package/src/db/reference-data.ts +2 -3
- package/src/db/replay-migration-sql.ts +257 -0
- package/src/db/schema-inspection.ts +1 -1
- package/src/db/table-builder.ts +102 -71
- package/src/db/tenant-db.ts +15 -53
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
- package/src/engine/__tests__/boot-validator.test.ts +219 -15
- package/src/engine/__tests__/build-app-schema.test.ts +82 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
- package/src/engine/__tests__/build-target.test.ts +3 -11
- package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
- package/src/engine/__tests__/config-helpers.test.ts +16 -0
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +163 -1
- package/src/engine/__tests__/entity-handlers.test.ts +80 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/feature-manifest.test.ts +31 -1
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/nav.test.ts +86 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/__tests__/registry.test.ts +144 -0
- package/src/engine/__tests__/schema-builder.test.ts +18 -0
- package/src/engine/__tests__/screen.test.ts +147 -1
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
- package/src/engine/__tests__/store-table.test.ts +229 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
- package/src/engine/boot-validator/action-wiring.ts +149 -0
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/config-deps.ts +40 -4
- package/src/engine/boot-validator/entity-handler.ts +20 -21
- package/src/engine/boot-validator/entity-list-screens.ts +88 -0
- package/src/engine/boot-validator/gdpr-storage.ts +0 -20
- package/src/engine/boot-validator/i18n-keys.ts +58 -4
- package/src/engine/boot-validator/index.ts +33 -12
- package/src/engine/boot-validator/nav.ts +125 -0
- package/src/engine/boot-validator/pii-retention.ts +95 -10
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/build-app-schema.ts +16 -0
- package/src/engine/build-config-feature-schema.ts +44 -7
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/config-helpers.ts +15 -0
- package/src/engine/constants.ts +32 -6
- package/src/engine/create-app.ts +11 -0
- package/src/engine/define-feature.ts +95 -947
- package/src/engine/define-handler.ts +28 -94
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/effective-features.ts +12 -2
- package/src/engine/entity-handlers.ts +76 -11
- package/src/engine/extensions/tenant-data.ts +19 -0
- package/src/engine/extensions/user-data.ts +29 -2
- package/src/engine/factories.ts +8 -49
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
- package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
- package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +234 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +34 -31
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/index.ts +2 -4
- package/src/engine/feature-ast/parse.ts +130 -23
- package/src/engine/feature-ast/patch.ts +39 -50
- package/src/engine/feature-ast/patcher.ts +37 -38
- package/src/engine/feature-ast/patterns.ts +50 -62
- package/src/engine/feature-ast/render.ts +67 -44
- package/src/engine/feature-builder-state.ts +168 -0
- package/src/engine/feature-config-events-jobs.ts +403 -0
- package/src/engine/feature-entity-handlers.ts +208 -0
- package/src/engine/feature-manifest.ts +2 -1
- package/src/engine/feature-ui-extensions.ts +500 -0
- package/src/engine/field-access.ts +13 -2
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +16 -0
- package/src/engine/index.ts +23 -7
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/object-form.ts +27 -0
- package/src/engine/ownership.ts +26 -79
- package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
- package/src/engine/pattern-library/library.ts +44 -1152
- package/src/engine/pattern-library/mixed-schemas.ts +450 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +75 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/registry-facade.ts +362 -0
- package/src/engine/registry-ingest.ts +478 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +642 -0
- package/src/engine/registry.ts +78 -1658
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/schema-builder.ts +1 -0
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/soft-delete-cleanup.ts +6 -2
- package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
- package/src/engine/steps/_duration-utils.ts +2 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -482
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -972
- package/src/engine/types/fields.ts +2 -675
- package/src/engine/types/handlers.ts +2 -774
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -54
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +86 -40
- package/src/engine/types/nav.ts +2 -63
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -138
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -574
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -132
- package/src/engine/types/workspace.ts +2 -49
- package/src/engine/validate-projection-allowlist.ts +6 -6
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
- package/src/entrypoint/index.ts +49 -6
- package/src/errors/classes.ts +22 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/write-error-info.ts +10 -22
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
- package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
- package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/event-store/admin-api.ts +11 -4
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +64 -78
- package/src/event-store/events-schema.ts +11 -13
- package/src/event-store/index.ts +18 -3
- package/src/event-store/rebuild-dead-letter.ts +111 -0
- package/src/event-store/snapshot.ts +63 -40
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/build-storage-key.test.ts +28 -0
- package/src/files/__tests__/file-ref-entity.test.ts +8 -0
- package/src/files/__tests__/files.integration.test.ts +24 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/__tests__/local-provider.test.ts +31 -0
- package/src/files/__tests__/provider-resolver.test.ts +70 -0
- package/src/files/__tests__/write-stream.test.ts +13 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/file-ref-entity.ts +2 -2
- package/src/files/file-routes.ts +13 -0
- package/src/files/index.ts +1 -1
- package/src/files/local-provider.ts +27 -8
- package/src/files/provider-resolver.ts +31 -20
- package/src/files/types.ts +13 -55
- package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
- package/src/i18n/required-surface-keys.ts +120 -1
- package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
- package/src/jobs/job-runner.ts +82 -12
- package/src/logging/types.ts +1 -7
- package/src/migrations/pending-rebuilds.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +4 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +2 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +64 -2
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -64
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
- package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +826 -0
- package/src/pipeline/dispatch-stream.ts +90 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +44 -1372
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +30 -2
- package/src/pipeline/event-dispatcher-admin.ts +293 -0
- package/src/pipeline/event-dispatcher-delivery.ts +305 -0
- package/src/pipeline/event-dispatcher.ts +85 -542
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/msp-rebuild.ts +42 -3
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/projection-rebuild.ts +105 -3
- package/src/pipeline/system-hooks.ts +144 -1
- package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
- package/src/rate-limit/resolver.ts +16 -32
- package/src/schema-cli.ts +76 -16
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
- package/src/search/index.ts +6 -0
- package/src/search/meilisearch-adapter.ts +13 -12
- package/src/search/reindex-entity.ts +177 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/contains-secret.test.ts +34 -0
- package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/dek-cache.ts +26 -5
- package/src/secrets/envelope-cipher.ts +53 -0
- package/src/secrets/envelope.ts +5 -3
- package/src/secrets/index.ts +13 -1
- package/src/secrets/stored-envelope.ts +46 -0
- package/src/secrets/types.ts +2 -162
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
- package/src/stack/db.ts +2 -1
- package/src/stack/push-entity-projection-tables.ts +4 -3
- package/src/stack/redis.ts +8 -0
- package/src/stack/request-helper.ts +38 -2
- package/src/stack/table-helpers.ts +6 -4
- package/src/stack/test-stack.ts +249 -131
- package/src/testing/__tests__/late-bound.test.ts +32 -0
- package/src/testing/__tests__/wait-for.test.ts +59 -0
- package/src/testing/boot-validator-fixture.ts +121 -0
- package/src/testing/e2e-generator.ts +8 -0
- package/src/testing/file-provider-contract.ts +104 -0
- package/src/testing/handler-context.ts +3 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/late-bound.ts +5 -3
- package/src/testing/mutable-master-key-provider.ts +29 -3
- package/src/testing/wait-for.ts +3 -0
- package/src/testing/without-ambient-temporal.ts +14 -0
- package/src/time/__tests__/tz-dateline.test.ts +8 -10
- package/src/time/geo-tz.ts +1 -32
- package/src/time/index.ts +1 -0
- package/src/time/legacy-date.ts +18 -0
- package/src/time/polyfill.ts +21 -38
- package/src/time/tz-context.ts +44 -85
- package/src/ui-types/app-schema.ts +12 -0
- package/src/ui-types/index.ts +20 -7
- package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
- package/src/utils/safe-json.ts +13 -1
- package/src/__tests__/raw-table.integration.test.ts +0 -116
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/__tests__/encryption.test.ts +0 -39
- package/src/db/encryption.ts +0 -39
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/__tests__/raw-table.test.ts +0 -150
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
- package/src/engine/feature-ast/extractors/round4.ts +0 -1366
|
@@ -16,6 +16,12 @@ export const AUTH_COOKIE_NAME = "kumiko_auth";
|
|
|
16
16
|
export const CSRF_COOKIE_NAME = "kumiko_csrf";
|
|
17
17
|
export const CSRF_HEADER_NAME = "X-CSRF-Token";
|
|
18
18
|
|
|
19
|
+
// Prefix that marks a bearer token as a long-lived Personal Access Token
|
|
20
|
+
// rather than a JWT session. The personal-access-tokens feature mints tokens
|
|
21
|
+
// with this prefix and declares it as its tokenVerifier shape (kind:
|
|
22
|
+
// "prefix") — kept here so both sides import the same literal.
|
|
23
|
+
export const PAT_TOKEN_PREFIX = "kpat_";
|
|
24
|
+
|
|
19
25
|
// Which wire the current request authenticated over. Downstream
|
|
20
26
|
// csrf-middleware reads this: cookie-auth gets a CSRF-token check, bearer
|
|
21
27
|
// does not (headers aren't set cross-origin by browsers, so there is no
|
|
@@ -41,21 +47,39 @@ export type AuthSessionChecker = (
|
|
|
41
47
|
expectedUserId: string,
|
|
42
48
|
) => Promise<AuthSessionStatus>;
|
|
43
49
|
|
|
50
|
+
// Resolves a raw bearer token into a SessionUser, or null when no registered
|
|
51
|
+
// provider claims it (or the claiming provider rejects it as unknown/revoked/
|
|
52
|
+
// expired). Wired generically from the auth-foundation `tokenVerifier`
|
|
53
|
+
// extension-point registry — the middleware has no PAT/JWT-specific
|
|
54
|
+
// knowledge, it just consults whatever the app wired in and short-circuits
|
|
55
|
+
// the JWT path on a hit.
|
|
56
|
+
export type TokenVerifier = (rawToken: string) => Promise<SessionUser | null>;
|
|
57
|
+
|
|
58
|
+
export type TenantLifecycleStatusResolver = (
|
|
59
|
+
tenantId: TenantId,
|
|
60
|
+
) => Promise<{ readonly status: string } | null>;
|
|
61
|
+
|
|
44
62
|
export type AuthMiddlewareOptions = {
|
|
45
63
|
// Called after JWT-verify when the token carries a sid. If the checker
|
|
46
64
|
// reports anything other than "live", the request is rejected with 401.
|
|
47
65
|
// Omit to run in stateless-JWT mode (any valid JWT is accepted).
|
|
48
66
|
readonly sessionChecker?: AuthSessionChecker;
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
67
|
+
// Called for bearer tokens, BEFORE jwt.verify. On a hit the middleware
|
|
68
|
+
// sets the returned SessionUser and skips the JWT path entirely; on a
|
|
69
|
+
// miss (null) falls through to jwt.verify. Wired from the auth-foundation
|
|
70
|
+
// `tokenVerifier` extension-point registry — generic across providers
|
|
71
|
+
// (PAT, future JWT-provider, ...), the middleware itself has no
|
|
72
|
+
// provider-specific knowledge. Omit to disable non-JWT bearer auth.
|
|
73
|
+
readonly tokenVerifier?: TokenVerifier;
|
|
54
74
|
// Opt-in: when set, requests without a JWT are treated as anonymous
|
|
55
75
|
// callers instead of being rejected with 401. The middleware synthesises
|
|
56
76
|
// a SessionUser with id="anonymous" and roles=["anonymous"], scoped to a
|
|
57
77
|
// tenantId resolved through the chain documented on AnonymousAccessConfig.
|
|
58
|
-
readonly anonymousAccess?:
|
|
78
|
+
readonly anonymousAccess?: AnonymousAccessResolved;
|
|
79
|
+
// Consulted after tenantId is resolved (JWT/PAT/anonymous). Returns 410
|
|
80
|
+
// when the tenant is in teardown (destroyRequested/destroying/destroyed).
|
|
81
|
+
// cancel-destruction is exempt while status=destroyRequested.
|
|
82
|
+
readonly resolveTenantLifecycleStatus?: TenantLifecycleStatusResolver;
|
|
59
83
|
};
|
|
60
84
|
|
|
61
85
|
// Resolves the tenant for an unauthenticated request. Returns null when no
|
|
@@ -72,23 +96,22 @@ export type TenantResolver = (c: Context) => Promise<TenantId | null> | TenantId
|
|
|
72
96
|
// deployments where a caller could otherwise probe arbitrary ids.
|
|
73
97
|
export type TenantExists = (tenantId: TenantId) => Promise<boolean> | boolean;
|
|
74
98
|
|
|
99
|
+
// App-facing anonymous-access config (#1374). tenantResolver / tenantExists
|
|
100
|
+
// / resolverTrust come from auth-foundation providers (EXT_TENANT_RESOLVER /
|
|
101
|
+
// EXT_TENANT_EXISTENCE), resolved at boot into AnonymousAccessResolved.
|
|
102
|
+
//
|
|
103
|
+
// Single-tenant shortcut: when defaultTenantId is set, the server runs in
|
|
104
|
+
// **locked** mode (client must agree or get tenant_mismatch).
|
|
75
105
|
export type AnonymousAccessConfig = {
|
|
76
|
-
// Custom resolver (e.g. subdomain parser). Consulted only when neither
|
|
77
|
-
// the X-Tenant header nor the kumiko_tenant cookie are present.
|
|
78
|
-
readonly tenantResolver?: TenantResolver;
|
|
79
|
-
// Single-tenant shortcut. When set, the server runs in **locked** mode:
|
|
80
|
-
// - no client-supplied tenant: defaultTenantId is used.
|
|
81
|
-
// - client supplies a matching tenant (header/cookie/resolver): allowed.
|
|
82
|
-
// - client supplies a non-matching tenant: 400 tenant_mismatch (the
|
|
83
|
-
// server is single-tenant; rejecting protects against confused clients
|
|
84
|
-
// who think they're talking to a different deployment).
|
|
85
|
-
// The framework does NOT verify defaultTenantId against the DB at boot;
|
|
86
|
-
// the caller is responsible (see sample for the pattern).
|
|
87
106
|
readonly defaultTenantId?: TenantId;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Runtime config consumed by authMiddleware after boot merges registry
|
|
110
|
+
// providers onto the app-facing AnonymousAccessConfig.
|
|
111
|
+
export type AnonymousAccessResolved = AnonymousAccessConfig & {
|
|
112
|
+
readonly tenantResolver?: TenantResolver;
|
|
91
113
|
readonly tenantExists?: TenantExists;
|
|
114
|
+
readonly resolverTrust?: "authoritative" | "fallback-only";
|
|
92
115
|
};
|
|
93
116
|
|
|
94
117
|
// Where the candidate tenant came from. Drives the validation policy:
|
|
@@ -109,14 +132,15 @@ type MiddlewareRejectCode =
|
|
|
109
132
|
| "tenant_required"
|
|
110
133
|
| "tenant_not_found"
|
|
111
134
|
| "tenant_mismatch"
|
|
112
|
-
| "invalid_tenant_format"
|
|
135
|
+
| "invalid_tenant_format"
|
|
136
|
+
| "tenant_unavailable";
|
|
113
137
|
|
|
114
138
|
// @wrapper-known error-helper
|
|
115
139
|
function middlewareReject(
|
|
116
140
|
c: Context,
|
|
117
141
|
opts: {
|
|
118
142
|
code: MiddlewareRejectCode;
|
|
119
|
-
status: 400 | 401 | 403 | 404;
|
|
143
|
+
status: 400 | 401 | 403 | 404 | 410;
|
|
120
144
|
message: string;
|
|
121
145
|
i18nKey: string;
|
|
122
146
|
details?: Record<string, unknown>;
|
|
@@ -169,7 +193,18 @@ function extractToken(
|
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions = {}) {
|
|
172
|
-
const { sessionChecker,
|
|
196
|
+
const { sessionChecker, anonymousAccess, tokenVerifier, resolveTenantLifecycleStatus } = options;
|
|
197
|
+
|
|
198
|
+
// Fail loud at boot, not silently at request time: a tenantResolver
|
|
199
|
+
// without a declared resolverTrust is an ambiguous trust decision no
|
|
200
|
+
// sane default can make on the app's behalf (see AnonymousAccessConfig).
|
|
201
|
+
if (anonymousAccess?.tenantResolver && anonymousAccess.resolverTrust === undefined) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"authMiddleware: anonymousAccess.tenantResolver is set without resolverTrust — " +
|
|
204
|
+
'declare "authoritative" (resolver wins over client header/cookie) or ' +
|
|
205
|
+
'"fallback-only" (client wins, resolver is a last resort).',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
173
208
|
|
|
174
209
|
return async (c: Context, next: Next) => {
|
|
175
210
|
const extracted = extractToken(c);
|
|
@@ -201,7 +236,7 @@ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions =
|
|
|
201
236
|
i18nKey: "auth.errors.missingToken",
|
|
202
237
|
});
|
|
203
238
|
}
|
|
204
|
-
return await handleAnonymous(c, anonymousAccess, next);
|
|
239
|
+
return await handleAnonymous(c, anonymousAccess, next, resolveTenantLifecycleStatus);
|
|
205
240
|
}
|
|
206
241
|
return middlewareReject(c, {
|
|
207
242
|
code: "missing_token",
|
|
@@ -212,6 +247,19 @@ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions =
|
|
|
212
247
|
}
|
|
213
248
|
const { token, transport } = extracted;
|
|
214
249
|
|
|
250
|
+
// Generic bearer-verifier path: try the wired tokenVerifier (PAT, future
|
|
251
|
+
// JWT-provider, ...) BEFORE jwt.verify. A hit short-circuits the JWT path
|
|
252
|
+
// entirely; a miss (null — no provider's shape matched, or the matching
|
|
253
|
+
// provider rejected the token) falls through to the normal JWT flow below,
|
|
254
|
+
// which rejects it uniformly as invalid_token. Cookie transport is never
|
|
255
|
+
// routed here (the browser holds the JWT).
|
|
256
|
+
if (tokenVerifier && transport === "bearer") {
|
|
257
|
+
const verifiedUser = await tokenVerifier(token);
|
|
258
|
+
if (verifiedUser) {
|
|
259
|
+
return await handleVerifiedBearerUser(c, verifiedUser, next, resolveTenantLifecycleStatus);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
215
263
|
let payload: Awaited<ReturnType<JwtHelper["verify"]>>;
|
|
216
264
|
try {
|
|
217
265
|
payload = await jwt.verify(token);
|
|
@@ -225,15 +273,16 @@ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions =
|
|
|
225
273
|
}
|
|
226
274
|
|
|
227
275
|
// Session liveness check — only when both a checker is wired AND the
|
|
228
|
-
// token carries a sid.
|
|
229
|
-
//
|
|
276
|
+
// token carries a sid.
|
|
277
|
+
// A checker wired without a sid on the token means the token predates
|
|
278
|
+
// session tracking (or the JWT was forged) — reject.
|
|
230
279
|
if (sessionChecker) {
|
|
231
280
|
if (payload.jti) {
|
|
232
281
|
const status = await sessionChecker(payload.jti, payload.sub);
|
|
233
282
|
if (status !== "live") {
|
|
234
283
|
return sessionInvalid(c, status);
|
|
235
284
|
}
|
|
236
|
-
} else
|
|
285
|
+
} else {
|
|
237
286
|
return sessionInvalid(c, "no_sid");
|
|
238
287
|
}
|
|
239
288
|
}
|
|
@@ -261,6 +310,12 @@ export function authMiddleware(jwt: JwtHelper, options: AuthMiddlewareOptions =
|
|
|
261
310
|
...(payload.claims ? { claims: payload.claims } : {}),
|
|
262
311
|
...(payload.jti ? { sid: payload.jti } : {}),
|
|
263
312
|
};
|
|
313
|
+
const lifecycleReject = await rejectIfTenantTeardown(
|
|
314
|
+
c,
|
|
315
|
+
payload.tenantId,
|
|
316
|
+
resolveTenantLifecycleStatus,
|
|
317
|
+
);
|
|
318
|
+
if (lifecycleReject) return lifecycleReject;
|
|
264
319
|
c.set(USER_KEY, user);
|
|
265
320
|
c.set(AUTH_TRANSPORT_KEY, transport);
|
|
266
321
|
await next();
|
|
@@ -277,6 +332,42 @@ export function getAuthTransport(c: Context): AuthTransport | undefined {
|
|
|
277
332
|
return c.get(AUTH_TRANSPORT_KEY) as AuthTransport | undefined;
|
|
278
333
|
}
|
|
279
334
|
|
|
335
|
+
// Verified-bearer request flow. `user` was already resolved by the wired
|
|
336
|
+
// tokenVerifier (PAT today, future JWT-provider). Apply the same
|
|
337
|
+
// X-Tenant-mismatch guard as the JWT path before continuing. Structured like
|
|
338
|
+
// handleAnonymous so authMiddleware stays flat.
|
|
339
|
+
async function handleVerifiedBearerUser(
|
|
340
|
+
c: Context,
|
|
341
|
+
user: SessionUser,
|
|
342
|
+
next: Next,
|
|
343
|
+
resolveTenantLifecycleStatus?: TenantLifecycleStatusResolver,
|
|
344
|
+
): Promise<Response | undefined> {
|
|
345
|
+
// The token carries its own tenant; an X-Tenant header pointing elsewhere is
|
|
346
|
+
// a confused client — reject loudly, same stance as the JWT path.
|
|
347
|
+
const headerTenant = c.req.header(TENANT_HEADER_NAME);
|
|
348
|
+
if (headerTenant !== undefined && headerTenant !== user.tenantId) {
|
|
349
|
+
return middlewareReject(c, {
|
|
350
|
+
code: "tenant_mismatch",
|
|
351
|
+
status: 400,
|
|
352
|
+
message: "token tenantId and X-Tenant header disagree",
|
|
353
|
+
i18nKey: "auth.errors.tenantMismatch",
|
|
354
|
+
details: { tokenTenantId: user.tenantId, headerTenantId: headerTenant },
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
c.set(USER_KEY, user);
|
|
358
|
+
c.set(AUTH_TRANSPORT_KEY, "bearer");
|
|
359
|
+
const lifecycleReject = await rejectIfTenantTeardown(
|
|
360
|
+
c,
|
|
361
|
+
user.tenantId,
|
|
362
|
+
resolveTenantLifecycleStatus,
|
|
363
|
+
);
|
|
364
|
+
if (lifecycleReject) return lifecycleReject;
|
|
365
|
+
await next();
|
|
366
|
+
// skip: verified-bearer path completed — next() ran; explicit return keeps
|
|
367
|
+
// the Response|undefined union honest (same as handleAnonymous).
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
280
371
|
// Anonymous request flow. Steps:
|
|
281
372
|
// 1. Read raw client-supplied tenant from X-Tenant header / cookie.
|
|
282
373
|
// 2. Validate format (UUID-shape) — junk strings get 400 right here.
|
|
@@ -291,8 +382,9 @@ export function getAuthTransport(c: Context): AuthTransport | undefined {
|
|
|
291
382
|
// no CSRF vector to defend against).
|
|
292
383
|
async function handleAnonymous(
|
|
293
384
|
c: Context,
|
|
294
|
-
config:
|
|
385
|
+
config: AnonymousAccessResolved,
|
|
295
386
|
next: Next,
|
|
387
|
+
resolveTenantLifecycleStatus?: TenantLifecycleStatusResolver,
|
|
296
388
|
): Promise<Response | undefined> {
|
|
297
389
|
// Step 1+2: parse client-supplied tenant. Reject malformed values before
|
|
298
390
|
// they touch any downstream consumer (DB, cache, audit row).
|
|
@@ -330,6 +422,12 @@ async function handleAnonymous(
|
|
|
330
422
|
}
|
|
331
423
|
|
|
332
424
|
// Step 5: synthesise + continue.
|
|
425
|
+
const lifecycleReject = await rejectIfTenantTeardown(
|
|
426
|
+
c,
|
|
427
|
+
resolved.tenantId,
|
|
428
|
+
resolveTenantLifecycleStatus,
|
|
429
|
+
);
|
|
430
|
+
if (lifecycleReject) return lifecycleReject;
|
|
333
431
|
c.set(USER_KEY, createAnonymousUser(resolved.tenantId));
|
|
334
432
|
await next();
|
|
335
433
|
// skip: anonymous path completed — Hono middleware contract returns void
|
|
@@ -370,38 +468,93 @@ type ResolveError = { error: RejectArgs };
|
|
|
370
468
|
// clients that think they're talking to a different installation.
|
|
371
469
|
async function resolveTenant(
|
|
372
470
|
c: Context,
|
|
373
|
-
config:
|
|
471
|
+
config: AnonymousAccessResolved,
|
|
374
472
|
clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
|
|
375
473
|
): Promise<ResolvedTenant | ResolveError> {
|
|
376
474
|
if (config.defaultTenantId !== undefined) {
|
|
377
|
-
|
|
475
|
+
return resolveAgainstDefault(config.defaultTenantId, clientTenant);
|
|
476
|
+
}
|
|
477
|
+
if (config.tenantResolver && config.resolverTrust === "authoritative") {
|
|
478
|
+
return await resolveWithAuthoritativeResolver(c, config.tenantResolver, clientTenant);
|
|
479
|
+
}
|
|
480
|
+
return await resolveWithClientPrecedence(c, config.tenantResolver, clientTenant);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Locked single-tenant mode: the client either agrees with the default or
|
|
484
|
+
// gets tenant_mismatch — defending the deployment from confused clients
|
|
485
|
+
// that think they're talking to a different installation.
|
|
486
|
+
function resolveAgainstDefault(
|
|
487
|
+
defaultTenantId: TenantId,
|
|
488
|
+
clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
|
|
489
|
+
): ResolvedTenant | ResolveError {
|
|
490
|
+
if (clientTenant && clientTenant.id !== defaultTenantId) {
|
|
491
|
+
return {
|
|
492
|
+
error: {
|
|
493
|
+
code: "tenant_mismatch",
|
|
494
|
+
status: 400,
|
|
495
|
+
message: `${clientTenant.source} tenant disagrees with server default`,
|
|
496
|
+
i18nKey: "auth.errors.tenantMismatch",
|
|
497
|
+
details: { clientTenantId: clientTenant.id, defaultTenantId },
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return { tenantId: defaultTenantId, source: "default" };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// resolverTrust: "authoritative" — the resolver is trusted over the client.
|
|
505
|
+
// A client-supplied tenant that disagrees is rejected, never used to
|
|
506
|
+
// override the resolver's answer. Falls back to the client tenant only
|
|
507
|
+
// when the resolver itself has no opinion (unrecognised host).
|
|
508
|
+
async function resolveWithAuthoritativeResolver(
|
|
509
|
+
c: Context,
|
|
510
|
+
tenantResolver: TenantResolver,
|
|
511
|
+
clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
|
|
512
|
+
): Promise<ResolvedTenant | ResolveError> {
|
|
513
|
+
const resolved = await tenantResolver(c);
|
|
514
|
+
if (resolved !== null && resolved !== undefined) {
|
|
515
|
+
if (clientTenant && clientTenant.id !== resolved) {
|
|
378
516
|
return {
|
|
379
517
|
error: {
|
|
380
518
|
code: "tenant_mismatch",
|
|
381
519
|
status: 400,
|
|
382
|
-
message: `${clientTenant.source} tenant disagrees with
|
|
520
|
+
message: `${clientTenant.source} tenant disagrees with resolved tenant`,
|
|
383
521
|
i18nKey: "auth.errors.tenantMismatch",
|
|
384
|
-
details: {
|
|
385
|
-
clientTenantId: clientTenant.id,
|
|
386
|
-
defaultTenantId: config.defaultTenantId,
|
|
387
|
-
},
|
|
522
|
+
details: { clientTenantId: clientTenant.id, resolvedTenantId: resolved },
|
|
388
523
|
},
|
|
389
524
|
};
|
|
390
525
|
}
|
|
391
|
-
return { tenantId:
|
|
526
|
+
return { tenantId: resolved, source: "resolver" };
|
|
392
527
|
}
|
|
528
|
+
// Resolver had no opinion (unrecognised host) — do NOT fall back to the
|
|
529
|
+
// client-supplied tenant here. That would let an unrecognised-host
|
|
530
|
+
// request pick its own tenant via X-Tenant, the exact override this mode
|
|
531
|
+
// exists to prevent; it's just reached through "unknown host" instead of
|
|
532
|
+
// "known host, disagreeing header". Authoritative means the resolver's
|
|
533
|
+
// silence is final, not a delegation back to the client.
|
|
534
|
+
return tenantRequiredError();
|
|
535
|
+
}
|
|
393
536
|
|
|
537
|
+
// resolverTrust: "fallback-only" (or no resolver at all) — client tenant
|
|
538
|
+
// wins outright; the resolver only runs as a last resort when neither
|
|
539
|
+
// header nor cookie supplied a value.
|
|
540
|
+
async function resolveWithClientPrecedence(
|
|
541
|
+
c: Context,
|
|
542
|
+
tenantResolver: TenantResolver | undefined,
|
|
543
|
+
clientTenant: { id: TenantId; source: "header" | "cookie" } | null,
|
|
544
|
+
): Promise<ResolvedTenant | ResolveError> {
|
|
394
545
|
if (clientTenant) {
|
|
395
546
|
return { tenantId: clientTenant.id, source: clientTenant.source };
|
|
396
547
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const resolved = await config.tenantResolver(c);
|
|
548
|
+
if (tenantResolver) {
|
|
549
|
+
const resolved = await tenantResolver(c);
|
|
400
550
|
if (resolved !== null && resolved !== undefined) {
|
|
401
551
|
return { tenantId: resolved, source: "resolver" };
|
|
402
552
|
}
|
|
403
553
|
}
|
|
554
|
+
return tenantRequiredError();
|
|
555
|
+
}
|
|
404
556
|
|
|
557
|
+
function tenantRequiredError(): ResolveError {
|
|
405
558
|
return {
|
|
406
559
|
error: {
|
|
407
560
|
code: "tenant_required",
|
|
@@ -415,8 +568,63 @@ async function resolveTenant(
|
|
|
415
568
|
|
|
416
569
|
type RejectArgs = {
|
|
417
570
|
code: MiddlewareRejectCode;
|
|
418
|
-
status: 400 | 401 | 403 | 404;
|
|
571
|
+
status: 400 | 401 | 403 | 404 | 410;
|
|
419
572
|
message: string;
|
|
420
573
|
i18nKey: string;
|
|
421
574
|
details?: Record<string, unknown>;
|
|
422
575
|
};
|
|
576
|
+
|
|
577
|
+
const TENANT_LIFECYCLE_BLOCKED = new Set([
|
|
578
|
+
"destroyRequested",
|
|
579
|
+
"destroying",
|
|
580
|
+
"destroyFailed",
|
|
581
|
+
"destroyed",
|
|
582
|
+
]);
|
|
583
|
+
const TENANT_LIFECYCLE_CANCEL_QN = "tenant-lifecycle:write:cancel-destruction";
|
|
584
|
+
|
|
585
|
+
async function requestsCancelDestruction(c: Context): Promise<boolean> {
|
|
586
|
+
if (c.req.method !== "POST") return false;
|
|
587
|
+
try {
|
|
588
|
+
const path = c.req.path;
|
|
589
|
+
const body = (await c.req.raw.clone().json()) as {
|
|
590
|
+
type?: string;
|
|
591
|
+
commands?: Array<{ type?: string }>;
|
|
592
|
+
};
|
|
593
|
+
if (path === "/api/write") {
|
|
594
|
+
return body.type === TENANT_LIFECYCLE_CANCEL_QN;
|
|
595
|
+
}
|
|
596
|
+
if (path === "/api/batch") {
|
|
597
|
+
// every(), not some(): a batch mixing the cancel command with other
|
|
598
|
+
// writes must NOT wave the whole batch through the teardown gate —
|
|
599
|
+
// only a batch consisting solely of cancel-destruction is exempt.
|
|
600
|
+
return (
|
|
601
|
+
Array.isArray(body.commands) &&
|
|
602
|
+
body.commands.length > 0 &&
|
|
603
|
+
body.commands.every((command) => command.type === TENANT_LIFECYCLE_CANCEL_QN)
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
} catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function rejectIfTenantTeardown(
|
|
613
|
+
c: Context,
|
|
614
|
+
tenantId: TenantId,
|
|
615
|
+
resolveTenantLifecycleStatus: TenantLifecycleStatusResolver | undefined,
|
|
616
|
+
): Promise<Response | undefined> {
|
|
617
|
+
if (!resolveTenantLifecycleStatus) return undefined;
|
|
618
|
+
const lifecycle = await resolveTenantLifecycleStatus(tenantId);
|
|
619
|
+
if (!lifecycle || !TENANT_LIFECYCLE_BLOCKED.has(lifecycle.status)) return undefined;
|
|
620
|
+
if (lifecycle.status === "destroyRequested" && (await requestsCancelDestruction(c))) {
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
return middlewareReject(c, {
|
|
624
|
+
code: "tenant_unavailable",
|
|
625
|
+
status: 410,
|
|
626
|
+
message: `tenant "${tenantId}" is unavailable (${lifecycle.status})`,
|
|
627
|
+
i18nKey: "auth.errors.tenantUnavailable",
|
|
628
|
+
details: { tenantId, status: lifecycle.status },
|
|
629
|
+
});
|
|
630
|
+
}
|