@cosmicdrift/kumiko-framework 0.369.0 → 0.371.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/AGENTS.md +14 -1
- package/agents/examples/apex-surface-auth.md +2 -1
- package/agents/examples/app-config.md +98 -0
- package/agents/examples/auth-mfa.md +72 -0
- package/agents/examples/chat-channels.md +1 -1
- package/agents/examples/compose-stacks.md +188 -0
- package/agents/examples/env-composition.md +278 -0
- package/agents/examples/host-routing.md +181 -0
- package/agents/examples/inbound-mail-triage.md +1 -1
- package/agents/examples/legal-pages.md +52 -3
- package/agents/examples/managed-config.md +1 -0
- package/agents/examples/managed-pages.md +1 -0
- package/agents/examples/prod-boot-options.md +180 -0
- package/agents/examples/relations.md +4 -0
- package/agents/examples/secrets-demo.md +4 -2
- package/agents/examples/security-baseline.md +20 -17
- package/agents/examples.md +6 -1
- package/agents/features.md +14 -2
- package/dist/api/anonymous-cookie.d.ts +1 -1
- package/dist/api/anonymous-live-entities.d.ts +1 -1
- package/dist/api/auth-middleware.d.ts +2 -2
- package/dist/api/auth-routes.d.ts +7 -7
- package/dist/api/extra-route.d.ts +2 -2
- package/dist/api/index.d.ts +37 -37
- package/dist/api/jwt.d.ts +1 -1
- package/dist/api/observability-middleware.d.ts +1 -1
- package/dist/api/readiness.d.ts +1 -1
- package/dist/api/redis-sse-broker.d.ts +2 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-id-middleware.d.ts +3 -3
- package/dist/api/route-registrars.d.ts +4 -4
- package/dist/api/routes.d.ts +1 -1
- package/dist/api/server.d.ts +16 -16
- package/dist/api/sse-broker.d.ts +1 -1
- package/dist/api/sse-route.d.ts +1 -1
- package/dist/api/websocket-route.d.ts +3 -3
- package/dist/auth/index.d.ts +2 -2
- package/dist/compliance/duration-spec.d.ts +2 -2
- package/dist/compliance/index.d.ts +6 -6
- package/dist/compliance/profiles.d.ts +1 -1
- package/dist/config/index.d.ts +44 -0
- package/dist/config/index.js +61 -0
- package/dist/crypto/blind-index.d.ts +1 -1
- package/dist/crypto/event-pii.d.ts +1 -1
- package/dist/crypto/in-memory-kms-adapter.d.ts +1 -1
- package/dist/crypto/index.d.ts +11 -11
- package/dist/crypto/is-self-pii-field.d.ts +1 -1
- package/dist/crypto/kms-wiring.d.ts +2 -2
- package/dist/crypto/owner-transition-rekey.d.ts +1 -1
- package/dist/crypto/pg-kms-adapter.d.ts +1 -1
- package/dist/crypto/pii-field-encryption.d.ts +4 -4
- package/dist/crypto/request-kms-cache.d.ts +1 -1
- package/dist/crypto/subject-resolver.d.ts +3 -3
- package/dist/db/apply-entity-event.d.ts +4 -4
- package/dist/db/assert-exists-in.d.ts +4 -4
- package/dist/db/blind-index-cleanup.d.ts +2 -2
- package/dist/db/collect-table-metas.d.ts +2 -2
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/config-seed.d.ts +5 -5
- package/dist/db/connection.d.ts +1 -6
- package/dist/db/connection.js +15 -4
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/entity-field-encryption.d.ts +3 -3
- package/dist/db/entity-field-encryption.js +3 -1
- package/dist/db/entity-table-from-registry.d.ts +3 -3
- package/dist/db/entity-table-meta.d.ts +3 -3
- package/dist/db/event-store-executor-context.d.ts +9 -9
- package/dist/db/event-store-executor-read.d.ts +2 -2
- package/dist/db/event-store-executor-write.d.ts +2 -2
- package/dist/db/event-store-executor.d.ts +8 -8
- package/dist/db/feature-table-sources.d.ts +1 -1
- package/dist/db/index.d.ts +39 -39
- package/dist/db/located-timestamp.d.ts +1 -1
- package/dist/db/lock-guard.d.ts +2 -2
- package/dist/db/migrate-generator.d.ts +2 -2
- package/dist/db/migrate-runner.d.ts +1 -1
- package/dist/db/money.d.ts +1 -1
- package/dist/db/parent-ref-clause.d.ts +4 -4
- package/dist/db/projection-registry-binding.d.ts +1 -1
- package/dist/db/queries/advisory-lock.d.ts +1 -1
- package/dist/db/queries/backfill-pii.d.ts +2 -2
- package/dist/db/queries/ddl.d.ts +1 -1
- package/dist/db/queries/entity-read.d.ts +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/event-consumer.d.ts +2 -2
- package/dist/db/queries/event-store-admin.d.ts +1 -1
- package/dist/db/queries/event-store-transfer.d.ts +1 -1
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/pii-backfill-state.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/raw-sql.d.ts +1 -1
- package/dist/db/queries/seed-context.d.ts +2 -2
- package/dist/db/queries/shadow-swap.d.ts +3 -3
- package/dist/db/queries/table-ops.d.ts +1 -1
- package/dist/db/queries/test-stack.d.ts +2 -2
- package/dist/db/query.d.ts +3 -3
- package/dist/db/rebuild-marker.d.ts +1 -1
- package/dist/db/reference-data.d.ts +3 -3
- package/dist/db/render-ddl.d.ts +1 -1
- package/dist/db/replay-migration-sql.d.ts +1 -1
- package/dist/db/retired-framework-tables.d.ts +1 -1
- package/dist/db/schema-inspection.d.ts +1 -1
- package/dist/db/table-builder.d.ts +4 -4
- package/dist/db/table-push.d.ts +3 -3
- package/dist/db/tenant-db-runner.d.ts +2 -2
- package/dist/db/tenant-db.d.ts +7 -9
- package/dist/db/tenant-db.js +2 -12
- package/dist/derivatives/derivatives-context.d.ts +3 -3
- package/dist/derivatives/field-variants.d.ts +1 -1
- package/dist/derivatives/index.d.ts +6 -6
- package/dist/engine/access.d.ts +1 -1
- package/dist/engine/active-membership.d.ts +2 -2
- package/dist/engine/boot-validator/access-declarations.d.ts +1 -1
- package/dist/engine/boot-validator/access-roles.d.ts +1 -1
- package/dist/engine/boot-validator/action-wiring.d.ts +1 -1
- package/dist/engine/boot-validator/api-ext.d.ts +1 -1
- package/dist/engine/boot-validator/boot-check.d.ts +1 -1
- package/dist/engine/boot-validator/card-meta-overflow.d.ts +1 -1
- package/dist/engine/boot-validator/config-deps.d.ts +1 -1
- package/dist/engine/boot-validator/detail-screens.d.ts +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -1
- package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/gdpr-storage.d.ts +1 -1
- package/dist/engine/boot-validator/global-tenancy.d.ts +1 -1
- package/dist/engine/boot-validator/handler-field-references.d.ts +1 -1
- package/dist/engine/boot-validator/i18n-keys.d.ts +1 -1
- package/dist/engine/boot-validator/index.d.ts +7 -7
- package/dist/engine/boot-validator/nav.d.ts +1 -1
- package/dist/engine/boot-validator/ownership.d.ts +2 -2
- package/dist/engine/boot-validator/parent-ref.d.ts +1 -1
- package/dist/engine/boot-validator/pii-retention.d.ts +2 -2
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/query-output-columns.d.ts +1 -1
- package/dist/engine/boot-validator/query-refs.d.ts +1 -1
- package/dist/engine/boot-validator/record-owned.d.ts +1 -1
- package/dist/engine/boot-validator/screens.d.ts +2 -2
- package/dist/engine/boot-validator/security-baseline.d.ts +1 -1
- package/dist/engine/boot-validator/selector-owner-panels.d.ts +1 -1
- package/dist/engine/boot-validator/transfer-graph.d.ts +2 -2
- package/dist/engine/boot-validator/url-prefill-fields.d.ts +1 -1
- package/dist/engine/boot-validator/workspaces.d.ts +1 -1
- package/dist/engine/boot-validator.d.ts +1 -1
- package/dist/engine/bridge-stub.d.ts +1 -1
- package/dist/engine/build-app-schema.d.ts +2 -2
- package/dist/engine/build-config-feature-schema.d.ts +5 -5
- package/dist/engine/build-target.d.ts +2 -2
- package/dist/engine/changeset-changes.d.ts +1 -1
- package/dist/engine/codemod/index.d.ts +2 -2
- package/dist/engine/config-helpers.d.ts +1 -1
- package/dist/engine/constants.d.ts +1 -1
- package/dist/engine/create-app.d.ts +2 -2
- package/dist/engine/cross-tenant.d.ts +2 -2
- package/dist/engine/dedupe-features.d.ts +1 -1
- package/dist/engine/define-feature.d.ts +1 -1
- package/dist/engine/define-feature.js +2 -2
- package/dist/engine/define-handler.d.ts +4 -4
- package/dist/engine/define-step.d.ts +1 -1
- package/dist/engine/define-workflow.d.ts +2 -2
- package/dist/engine/effective-features.d.ts +1 -1
- package/dist/engine/embedded-derived.d.ts +1 -1
- package/dist/engine/entity-handlers.d.ts +6 -5
- package/dist/engine/entity-handlers.js +6 -3
- package/dist/engine/event-def.d.ts +15 -0
- package/dist/engine/event-def.js +123 -0
- package/dist/engine/event-helpers.d.ts +1 -1
- package/dist/engine/extension-names.d.ts +8 -8
- package/dist/engine/extension-selector-plugins.d.ts +2 -2
- package/dist/engine/extensions/escape-hatch-usage.d.ts +1 -1
- package/dist/engine/extensions/storage-provider.d.ts +1 -1
- package/dist/engine/extensions/tenant-data.d.ts +3 -3
- package/dist/engine/extensions/tenant-resource.d.ts +2 -2
- package/dist/engine/extensions/user-data.d.ts +2 -2
- package/dist/engine/factories.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/ai-steps.d.ts +2 -2
- package/dist/engine/feature-ast/extractors/events.d.ts +2 -2
- package/dist/engine/feature-ast/extractors/handlers.d.ts +4 -4
- package/dist/engine/feature-ast/extractors/hooks.d.ts +5 -5
- package/dist/engine/feature-ast/extractors/index.d.ts +14 -14
- package/dist/engine/feature-ast/extractors/jobs-routes.d.ts +3 -3
- package/dist/engine/feature-ast/extractors/projections-screens.d.ts +3 -3
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -2
- package/dist/engine/feature-ast/extractors/round2.d.ts +2 -2
- package/dist/engine/feature-ast/extractors/round3.d.ts +4 -4
- package/dist/engine/feature-ast/extractors/round5.d.ts +2 -2
- package/dist/engine/feature-ast/extractors/round6.d.ts +2 -2
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -2
- package/dist/engine/feature-ast/index.d.ts +16 -16
- package/dist/engine/feature-ast/parse.d.ts +2 -2
- package/dist/engine/feature-ast/patch.d.ts +1 -1
- package/dist/engine/feature-ast/patcher.d.ts +15 -15
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -10
- package/dist/engine/feature-ast/patterns.d.ts +15 -15
- package/dist/engine/feature-ast/render.d.ts +1 -1
- package/dist/engine/feature-builder-state.d.ts +5 -5
- package/dist/engine/feature-config-events-jobs.d.ts +3 -3
- package/dist/engine/feature-config-events-jobs.js +8 -130
- package/dist/engine/feature-entity-handlers.d.ts +4 -4
- package/dist/engine/feature-entity-handlers.js +11 -5
- package/dist/engine/feature-manifest.d.ts +1 -1
- package/dist/engine/feature-ui-extensions.d.ts +7 -7
- package/dist/engine/feature-ui-extensions.js +2 -2
- package/dist/engine/field-access.d.ts +1 -1
- package/dist/engine/field-helpers.d.ts +1 -1
- package/dist/engine/handler-helpers.d.ts +1 -1
- package/dist/engine/index.d.ts +89 -88
- package/dist/engine/index.js +1 -0
- package/dist/engine/instruction-fields.d.ts +1 -1
- package/dist/engine/object-form.d.ts +1 -1
- package/dist/engine/object-form.js +5 -6
- package/dist/engine/ownership.d.ts +3 -3
- package/dist/engine/pattern-library/index.d.ts +2 -2
- package/dist/engine/pattern-library/library.d.ts +2 -2
- package/dist/engine/pattern-library/mixed-schemas.d.ts +1 -1
- package/dist/engine/pattern-library/opaque-schemas.d.ts +1 -1
- package/dist/engine/pattern-library/shared-fields.d.ts +1 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -1
- package/dist/engine/pattern-library/types.d.ts +1 -1
- package/dist/engine/personal-data-fields.d.ts +2 -2
- package/dist/engine/pipeline.d.ts +2 -2
- package/dist/engine/project-app-schema-for-roles.d.ts +1 -1
- package/dist/engine/projection-helpers.d.ts +4 -4
- package/dist/engine/read-claim.d.ts +1 -1
- package/dist/engine/registry-facade.d.ts +2 -2
- package/dist/engine/registry-ingest.d.ts +2 -2
- package/dist/engine/registry-state.d.ts +2 -2
- package/dist/engine/registry-validate.d.ts +3 -3
- package/dist/engine/registry.d.ts +1 -1
- package/dist/engine/resolve-config-or-param.d.ts +2 -2
- package/dist/engine/role-assignment.d.ts +1 -1
- package/dist/engine/run-in.d.ts +1 -1
- package/dist/engine/run-pipeline.d.ts +3 -3
- package/dist/engine/schema-builder.d.ts +1 -1
- package/dist/engine/screen-filter-ops.d.ts +1 -1
- package/dist/engine/screen-helpers.d.ts +2 -2
- package/dist/engine/soft-delete-cleanup.d.ts +2 -2
- package/dist/engine/steps/_event-match.d.ts +1 -1
- package/dist/engine/steps/_no-return-guard.d.ts +1 -1
- package/dist/engine/steps/_read-source.d.ts +4 -4
- package/dist/engine/steps/_resolver-utils.d.ts +1 -1
- package/dist/engine/steps/aggregate-append-event.d.ts +1 -1
- package/dist/engine/steps/aggregate-create.d.ts +2 -2
- package/dist/engine/steps/aggregate-update.d.ts +3 -3
- package/dist/engine/steps/branch.d.ts +1 -1
- package/dist/engine/steps/call-feature.d.ts +2 -2
- package/dist/engine/steps/compute.d.ts +1 -1
- package/dist/engine/steps/for-each.d.ts +1 -1
- package/dist/engine/steps/mail-send.d.ts +1 -1
- package/dist/engine/steps/read-find-many.d.ts +2 -2
- package/dist/engine/steps/read-find-one.d.ts +2 -2
- package/dist/engine/steps/retry.d.ts +1 -1
- package/dist/engine/steps/return.d.ts +2 -2
- package/dist/engine/steps/unsafe-projection-delete.d.ts +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.d.ts +1 -1
- package/dist/engine/steps/wait-for-event.d.ts +1 -1
- package/dist/engine/steps/wait.d.ts +1 -1
- package/dist/engine/steps/webhook-send.d.ts +1 -1
- package/dist/engine/strip-disabled-features-from-app-schema.d.ts +4 -4
- package/dist/engine/system-user.d.ts +2 -2
- package/dist/engine/template-variables.d.ts +1 -1
- package/dist/engine/tier-resolver-extension.d.ts +4 -4
- package/dist/engine/transfer-adjacency.d.ts +1 -1
- package/dist/engine/types/index.d.ts +6 -6
- package/dist/engine/validate-projection-allowlist.d.ts +1 -1
- package/dist/engine/validation.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +12 -12
- package/dist/env/dry-run.d.ts +14 -1
- package/dist/env/dry-run.js +50 -0
- package/dist/env/index.d.ts +54 -16
- package/dist/env/index.js +76 -7
- package/dist/errors/classes.d.ts +3 -3
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/index.d.ts +18 -18
- package/dist/errors/member-resolution.d.ts +1 -1
- package/dist/errors/serialize.d.ts +2 -2
- package/dist/errors/subject-erased.d.ts +1 -1
- package/dist/errors/to-kumiko-error.d.ts +1 -1
- package/dist/errors/write-error-info.d.ts +1 -1
- package/dist/errors/zod-bridge.d.ts +1 -1
- package/dist/es-ops/context.d.ts +4 -4
- package/dist/es-ops/index.d.ts +4 -4
- package/dist/es-ops/operations-schema.d.ts +1 -1
- package/dist/es-ops/runner.d.ts +3 -3
- package/dist/es-ops/runner.js +9 -1
- package/dist/es-ops/types.d.ts +2 -2
- package/dist/event-store/admin-api.d.ts +3 -3
- package/dist/event-store/archive.d.ts +2 -2
- package/dist/event-store/event-store.d.ts +2 -2
- package/dist/event-store/events-schema.d.ts +1 -1
- package/dist/event-store/index.d.ts +15 -15
- package/dist/event-store/provenance-append.d.ts +3 -3
- package/dist/event-store/rebuild-dead-letter.d.ts +2 -2
- package/dist/event-store/row-to-stored-event.d.ts +2 -2
- package/dist/event-store/snapshot.d.ts +2 -2
- package/dist/event-store/tenant-db-append.d.ts +2 -2
- package/dist/event-store/transfer.d.ts +3 -3
- package/dist/event-store/upcaster-dead-letter.d.ts +2 -2
- package/dist/event-store/upcaster.d.ts +3 -3
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +2 -2
- package/dist/files/bind-pending-file-refs.d.ts +3 -3
- package/dist/files/create-file-ref-from-bytes.d.ts +4 -4
- package/dist/files/create-file-ref.d.ts +3 -3
- package/dist/files/dispose-removed-file-refs.d.ts +2 -2
- package/dist/files/feature.d.ts +3 -3
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-ref-entity.d.ts +1 -1
- package/dist/files/file-ref-table.d.ts +1 -1
- package/dist/files/file-routes.d.ts +5 -5
- package/dist/files/in-memory-provider.d.ts +1 -1
- package/dist/files/index.d.ts +20 -20
- package/dist/files/load-save-file-refs.d.ts +2 -2
- package/dist/files/local-provider.d.ts +1 -1
- package/dist/files/provider-resolver.d.ts +5 -5
- package/dist/files/purge-expired-file-ref-bytes.d.ts +2 -2
- package/dist/files/record-read-check.d.ts +1 -1
- package/dist/files/record-write-check.d.ts +3 -3
- package/dist/files/storage-tracking.d.ts +2 -2
- package/dist/files/types.d.ts +1 -1
- package/dist/files/zip-stream.d.ts +1 -1
- package/dist/i18n/index.d.ts +4 -4
- package/dist/i18n/mail-registry.d.ts +1 -1
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/settings-hub-keys.d.ts +1 -1
- package/dist/internal/stack-support.d.ts +12 -12
- package/dist/internal/testing.d.ts +4 -4
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/job-runner.d.ts +1 -1
- package/dist/jobs/max-per-tenant.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +2 -2
- package/dist/lifecycle/lifecycle.d.ts +1 -1
- package/dist/lifecycle/signal-handlers.d.ts +1 -1
- package/dist/logging/index.d.ts +4 -4
- package/dist/logging/pino-logger.d.ts +1 -1
- package/dist/logging/utils.d.ts +1 -1
- package/dist/migrations/index.d.ts +4 -4
- package/dist/migrations/kumiko-drift.d.ts +1 -1
- package/dist/migrations/pending-rebuilds.d.ts +5 -5
- package/dist/migrations/pii-event-backfill.d.ts +4 -4
- package/dist/migrations/projection-table-index.d.ts +1 -1
- package/dist/observability/console-provider.d.ts +1 -1
- package/dist/observability/context.d.ts +1 -1
- package/dist/observability/escape-hatch-report.d.ts +3 -3
- package/dist/observability/fallback.d.ts +1 -1
- package/dist/observability/index.d.ts +16 -16
- package/dist/observability/metric-validator.d.ts +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -1
- package/dist/observability/metrics-wiring.d.ts +2 -2
- package/dist/observability/noop-provider.d.ts +1 -1
- package/dist/observability/prometheus-meter.d.ts +1 -1
- package/dist/observability/recording-meter.d.ts +1 -1
- package/dist/observability/recording-tracer.d.ts +1 -1
- package/dist/observability/redis-wrapper.d.ts +1 -1
- package/dist/observability/sensitive-filter.d.ts +1 -1
- package/dist/observability/standard-metrics.d.ts +1 -1
- package/dist/pipeline/active-membership.d.ts +4 -4
- package/dist/pipeline/append-event-core.d.ts +3 -3
- package/dist/pipeline/auth-claims-resolver.d.ts +2 -2
- package/dist/pipeline/cascade-handler.d.ts +3 -3
- package/dist/pipeline/dispatch-batch.d.ts +2 -2
- package/dist/pipeline/dispatch-query.d.ts +4 -4
- package/dist/pipeline/dispatch-shared.d.ts +17 -17
- package/dist/pipeline/dispatch-stream.d.ts +2 -2
- package/dist/pipeline/dispatch-write.d.ts +4 -4
- package/dist/pipeline/dispatcher-utils.d.ts +2 -2
- package/dist/pipeline/dispatcher.d.ts +12 -12
- package/dist/pipeline/event-consumer-state.d.ts +1 -1
- package/dist/pipeline/event-dispatcher-admin.d.ts +1 -1
- package/dist/pipeline/event-dispatcher-delivery.d.ts +7 -7
- package/dist/pipeline/event-dispatcher.d.ts +7 -7
- package/dist/pipeline/event-retention.d.ts +1 -1
- package/dist/pipeline/handler-access-error.d.ts +2 -2
- package/dist/pipeline/index.d.ts +42 -42
- package/dist/pipeline/irreversible-operation-gate.d.ts +1 -1
- package/dist/pipeline/lifecycle-pipeline.d.ts +2 -2
- package/dist/pipeline/member-read-only-transaction.d.ts +2 -2
- package/dist/pipeline/member-reader.d.ts +5 -5
- package/dist/pipeline/msp-rebuild.d.ts +4 -4
- package/dist/pipeline/multi-stream-apply-context.d.ts +3 -3
- package/dist/pipeline/notify-scope.d.ts +3 -3
- package/dist/pipeline/pending-gap-ranges.d.ts +2 -2
- package/dist/pipeline/pre-handler-gates.d.ts +3 -3
- package/dist/pipeline/pre-transaction-reservations.d.ts +4 -4
- package/dist/pipeline/projection-rebuild.d.ts +3 -3
- package/dist/pipeline/projection-state.d.ts +1 -1
- package/dist/pipeline/projections-runner.d.ts +3 -3
- package/dist/pipeline/projections-runner.js +3 -4
- package/dist/pipeline/system-hooks.d.ts +4 -4
- package/dist/pipeline/system-identity-switch.d.ts +3 -3
- package/dist/pipeline/tenant-timezone-cache.d.ts +1 -1
- package/dist/pipeline/write-origin.d.ts +3 -3
- package/dist/pipeline/write-result-masking.d.ts +2 -2
- package/dist/random/index.d.ts +2 -2
- package/dist/rate-limit/bucket.d.ts +1 -1
- package/dist/rate-limit/index.d.ts +3 -3
- package/dist/rate-limit/middleware.d.ts +3 -3
- package/dist/redis/cache-sync-bus.d.ts +2 -2
- package/dist/redis/cache-sync-topics.d.ts +1 -1
- package/dist/redis/index.d.ts +8 -8
- package/dist/redis/pubsub-signal.d.ts +1 -1
- package/dist/schema-cli.d.ts +1 -1
- package/dist/schema-cli.js +21 -2
- package/dist/search/derive-search-adapter-config.d.ts +2 -2
- package/dist/search/in-memory-adapter.d.ts +1 -1
- package/dist/search/index.d.ts +6 -6
- package/dist/search/meilisearch-adapter.d.ts +2 -2
- package/dist/search/purge-subject.d.ts +4 -4
- package/dist/search/reindex-entity.d.ts +3 -3
- package/dist/secrets/dek-cache.d.ts +1 -1
- package/dist/secrets/env-master-key-provider.d.ts +1 -1
- package/dist/secrets/env-master-key-provider.js +26 -11
- package/dist/secrets/envelope-cipher.d.ts +2 -2
- package/dist/secrets/envelope.d.ts +1 -1
- package/dist/secrets/index.d.ts +9 -9
- package/dist/secrets/rotation.d.ts +1 -1
- package/dist/secrets/stored-envelope.d.ts +1 -1
- package/dist/seeding/entity-seed.d.ts +1 -1
- package/dist/seeding/index.d.ts +3 -3
- package/dist/seeding/seed-users.d.ts +1 -1
- package/dist/time/index.d.ts +6 -6
- package/dist/ui-types/app-schema.d.ts +5 -5
- package/dist/ui-types/index.d.ts +23 -23
- package/dist/upgrade-cli.js +13 -3
- package/dist/utils/index.d.ts +9 -9
- package/package.json +19 -66
- package/presets/biome-app.json +80 -0
- package/presets/tsconfig.app.json +19 -0
- package/src/changes.json +84 -2
- package/src/scripts/codemod/README.md +18 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-factory.ts +1 -1
- package/src/scripts/codemod/import-extensions.ts +118 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +2 -2
- package/src/scripts/codemod/migrate-db-raw.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/object-form.ts +1 -1
- package/src/scripts/codemod/s3-imports.ts +1 -1
- package/src/scripts/codemod/tenant-db-options.ts +1 -1
- package/src/scripts/codemod/testing-move.ts +2 -2
- package/dist/db/bun-db-deprecated.d.ts +0 -10
- package/dist/db/bun-db-deprecated.js +0 -5
- package/dist/stack/index.d.ts +0 -10
- package/dist/stack/index.js +0 -11
- package/dist/testing/e2e-generator.d.ts +0 -5
- package/dist/testing/e2e-generator.js +0 -5
- package/dist/testing/handler-context.d.ts +0 -5
- package/dist/testing/handler-context.js +0 -5
- package/dist/testing/index.d.ts +0 -10
- package/dist/testing/index.js +0 -10
- package/dist/testing/real-providers.d.ts +0 -5
- package/dist/testing/real-providers.js +0 -5
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/env-composition. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# env-composition
|
|
4
|
+
|
|
5
|
+
Typed app env from the env blocks bundled features register, plus app-level blocks in extend.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# Env composition
|
|
10
|
+
|
|
11
|
+
Build the app's env schema from the blocks the mounted features register,
|
|
12
|
+
instead of keeping a hand-written copy of every feature's variables.
|
|
13
|
+
|
|
14
|
+
## What it shows
|
|
15
|
+
|
|
16
|
+
- **Feature blocks**: `createSecretsFeature`, `createCryptoShreddingFeature`
|
|
17
|
+
and `createFileProviderS3Feature({ fromEnv: true })` register `secretsEnvSchema`,
|
|
18
|
+
`kmsEnvSchema` and `s3EnvSchema`. `composeEnvSchema` collects them and
|
|
19
|
+
records which feature declared each variable.
|
|
20
|
+
- **App-level blocks in `extend`**: `buildInfoEnvSchema`, `demoAdminEnvSchema`,
|
|
21
|
+
`stripeEnvSchema({ prices })` and `createS3EnvSchema({ prefix })` for a
|
|
22
|
+
second bucket. These belong to no mounted feature, so the app adds them.
|
|
23
|
+
- **Typed result**: `parseEnv(composed.schema, …)` returns the merged type,
|
|
24
|
+
so `parsed.S3_BUCKET` is `string | undefined` and `parsed.APP_PUBLIC_URL` is
|
|
25
|
+
`string` without a cast.
|
|
26
|
+
- **Error report**: a missing required variable is listed with its source
|
|
27
|
+
(`secrets`, `app`, …).
|
|
28
|
+
|
|
29
|
+
## Blocks
|
|
30
|
+
|
|
31
|
+
| Block | Where it lives | Registered by |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `kmsEnvSchema` | `crypto-shredding` | `createCryptoShreddingFeature()` |
|
|
34
|
+
| `smtpEnvSchema` | `channel-email` | `createChannelEmailFeature()` |
|
|
35
|
+
| `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3` | `createFileProviderS3Feature({ fromEnv: true })` |
|
|
36
|
+
| `stripeEnvSchema({ prices })` | `subscription-stripe` | app (`extend`); the feature registers the bridge keys |
|
|
37
|
+
| `mfaTokenSecretsEnvSchema` | `auth-mfa` | `createAuthMfaFeature()` |
|
|
38
|
+
| `buildInfoEnvSchema`, `demoAdminEnvSchema` | `foundation-shared` | app (`extend`) |
|
|
39
|
+
| `prometheusMetricsEnvSchema` | `framework/observability` | app (`extend`) |
|
|
40
|
+
|
|
41
|
+
## When to reach for it
|
|
42
|
+
|
|
43
|
+
Every app that boots with `runProdApp`. Pass the composed schema as
|
|
44
|
+
`envSchema` and read the parsed values instead of `process.env`.
|
|
45
|
+
|
|
46
|
+
## Source files
|
|
47
|
+
|
|
48
|
+
### src/__tests__/dry-run-boot.test.ts
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { describe, expect, test } from "bun:test";
|
|
52
|
+
import { join } from "node:path";
|
|
53
|
+
import { composeAppEnv } from "../env";
|
|
54
|
+
|
|
55
|
+
const serverEntry = join(import.meta.dir, "..", "..", "bin", "server.ts");
|
|
56
|
+
|
|
57
|
+
const completeValues = {
|
|
58
|
+
DATABASE_URL: "postgres://app@127.0.0.1:1/app",
|
|
59
|
+
REDIS_URL: "redis://127.0.0.1:1",
|
|
60
|
+
JWT_SECRET: "j".repeat(32),
|
|
61
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: Buffer.alloc(32, 7).toString("base64"),
|
|
62
|
+
DEMO_ADMIN_EMAIL: "admin@example.com",
|
|
63
|
+
DEMO_SYSADMIN_EMAIL: "root@example.com",
|
|
64
|
+
APP_PUBLIC_URL: "https://app.example.com",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// The child gets only these variables, never the test runner's env, so a
|
|
68
|
+
// DATABASE_URL on the CI host cannot leak into the "empty env" case.
|
|
69
|
+
async function runBootDryRun(variables: Readonly<Record<string, string>>) {
|
|
70
|
+
const child = Bun.spawn([process.execPath, serverEntry], {
|
|
71
|
+
env: { PATH: process.env["PATH"] ?? "", KUMIKO_DRY_RUN_ENV: "boot", ...variables },
|
|
72
|
+
stdout: "pipe",
|
|
73
|
+
stderr: "pipe",
|
|
74
|
+
});
|
|
75
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
76
|
+
new Response(child.stdout).text(),
|
|
77
|
+
new Response(child.stderr).text(),
|
|
78
|
+
child.exited,
|
|
79
|
+
]);
|
|
80
|
+
return { stdout, stderr, exitCode };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requiredVariableNames(): readonly string[] {
|
|
84
|
+
const { schema } = composeAppEnv();
|
|
85
|
+
return Object.entries(schema.shape)
|
|
86
|
+
.filter(([, field]) => !field.safeParse(undefined).success)
|
|
87
|
+
.map(([name]) => name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe("KUMIKO_DRY_RUN_ENV=boot", () => {
|
|
91
|
+
test("exits 0 on an empty env and lists every required variable as missing", async () => {
|
|
92
|
+
const { stdout, exitCode } = await runBootDryRun({});
|
|
93
|
+
|
|
94
|
+
expect(exitCode).toBe(0);
|
|
95
|
+
const required = requiredVariableNames();
|
|
96
|
+
expect(required).toContain("DATABASE_URL");
|
|
97
|
+
for (const name of [...required, "JWT_SECRET"]) {
|
|
98
|
+
expect(stdout).toMatch(new RegExp(`^\\s+${name}\\s+\\S+\\s+required\\s+missing$`, "m"));
|
|
99
|
+
}
|
|
100
|
+
expect(stdout).toContain("Feature wiring was NOT validated");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("exits 0 on an empty env with NODE_ENV=production", async () => {
|
|
104
|
+
const { exitCode } = await runBootDryRun({ NODE_ENV: "production" });
|
|
105
|
+
|
|
106
|
+
expect(exitCode).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("validates the wiring once every required variable is set", async () => {
|
|
110
|
+
const { stdout, exitCode } = await runBootDryRun(completeValues);
|
|
111
|
+
|
|
112
|
+
expect(exitCode).toBe(0);
|
|
113
|
+
expect(stdout).toContain("All variables present.");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("rejects a keyring the master-key provider cannot use", async () => {
|
|
117
|
+
const { exitCode } = await runBootDryRun({
|
|
118
|
+
...completeValues,
|
|
119
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "2",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(exitCode).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("prints no value even when every variable is invalid", async () => {
|
|
126
|
+
const { schema } = composeAppEnv();
|
|
127
|
+
const leaking = Object.fromEntries(
|
|
128
|
+
Object.keys(schema.shape).map((name) => [name, `LEAK_${name}`]),
|
|
129
|
+
);
|
|
130
|
+
const { stdout, stderr, exitCode } = await runBootDryRun(leaking);
|
|
131
|
+
|
|
132
|
+
expect(exitCode).toBe(1);
|
|
133
|
+
expect(`${stdout}${stderr}`).not.toContain("LEAK_");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("exits 1 on an invalid value and never prints it", async () => {
|
|
137
|
+
const endpoint = "http://s3.leak-check.example.com";
|
|
138
|
+
const { stdout, stderr, exitCode } = await runBootDryRun({
|
|
139
|
+
NODE_ENV: "production",
|
|
140
|
+
S3_ENDPOINT: endpoint,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(exitCode).toBe(1);
|
|
144
|
+
expect(stdout).toMatch(/S3_ENDPOINT\s+file-provider-s3-env\s+optional\s+invalid/);
|
|
145
|
+
expect(`${stdout}${stderr}`).not.toContain("leak-check");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### src/__tests__/env.test.ts
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { describe, expect, test } from "bun:test";
|
|
154
|
+
import { randomBytes } from "node:crypto";
|
|
155
|
+
import { KumikoBootError, parseEnv } from "@cosmicdrift/kumiko-framework/env";
|
|
156
|
+
import { composeAppEnv } from "../env";
|
|
157
|
+
|
|
158
|
+
const masterKey = randomBytes(32).toString("base64");
|
|
159
|
+
|
|
160
|
+
const minimalValidValues = {
|
|
161
|
+
DATABASE_URL: "postgres://app@localhost/app",
|
|
162
|
+
REDIS_URL: "redis://localhost:6379",
|
|
163
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: masterKey,
|
|
164
|
+
DEMO_ADMIN_EMAIL: "admin@example.com",
|
|
165
|
+
DEMO_SYSADMIN_EMAIL: "root@example.com",
|
|
166
|
+
APP_PUBLIC_URL: "https://app.example.com",
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
describe("env composition", () => {
|
|
170
|
+
test("collects every block with the feature that declared it", () => {
|
|
171
|
+
const { sources } = composeAppEnv();
|
|
172
|
+
|
|
173
|
+
expect(sources["KUMIKO_SECRETS_MASTER_KEY_V1"]).toBe("secrets");
|
|
174
|
+
expect(sources["PLATFORM_KEK"]).toBe("crypto-shredding");
|
|
175
|
+
expect(sources["S3_BUCKET"]).toBe("file-provider-s3-env");
|
|
176
|
+
expect(sources["RELEASES_S3_BUCKET"]).toBe("app");
|
|
177
|
+
expect(sources["STRIPE_PRICE_PRO"]).toBe("app");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("parses into a typed env", () => {
|
|
181
|
+
const composed = composeAppEnv();
|
|
182
|
+
const parsed = parseEnv(composed.schema, {
|
|
183
|
+
...minimalValidValues,
|
|
184
|
+
S3_BUCKET: "uploads",
|
|
185
|
+
STRIPE_PRICE_PRO: "price_pro",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const bucket: string | undefined = parsed.S3_BUCKET;
|
|
189
|
+
const publicUrl: string = parsed.APP_PUBLIC_URL;
|
|
190
|
+
expect(bucket).toBe("uploads");
|
|
191
|
+
expect(publicUrl).toBe("https://app.example.com");
|
|
192
|
+
expect(parsed.KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION).toBe("1");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("reports every missing required variable with its source", () => {
|
|
196
|
+
const composed = composeAppEnv();
|
|
197
|
+
let report = "";
|
|
198
|
+
try {
|
|
199
|
+
parseEnv(composed.schema, {}, { sources: composed.sources });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (!(err instanceof KumikoBootError)) throw err;
|
|
202
|
+
report = err.format();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expect(report).toContain("DATABASE_URL (framework-core, required, missing)");
|
|
206
|
+
expect(report).toContain("KUMIKO_SECRETS_MASTER_KEY_V1 (secrets, required, missing)");
|
|
207
|
+
expect(report).toContain("DEMO_ADMIN_EMAIL (app, required, missing)");
|
|
208
|
+
expect(report).toContain("APP_PUBLIC_URL (app, required, missing)");
|
|
209
|
+
expect(report).not.toContain("S3_BUCKET");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("rejects a remote http S3 endpoint in production", () => {
|
|
213
|
+
const composed = composeAppEnv();
|
|
214
|
+
const originalNodeEnv = process.env["NODE_ENV"];
|
|
215
|
+
process.env["NODE_ENV"] = "production";
|
|
216
|
+
try {
|
|
217
|
+
expect(() =>
|
|
218
|
+
parseEnv(composed.schema, { ...minimalValidValues, S3_ENDPOINT: "http://s3.example.com" }),
|
|
219
|
+
).toThrow(KumikoBootError);
|
|
220
|
+
} finally {
|
|
221
|
+
if (originalNodeEnv === undefined) delete process.env["NODE_ENV"];
|
|
222
|
+
else process.env["NODE_ENV"] = originalNodeEnv;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### src/env.ts
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// Env Composition Sample
|
|
232
|
+
// Shows: the app env schema is composed from the blocks the mounted features
|
|
233
|
+
// register, plus app-level blocks in `extend`. The parsed env is typed from
|
|
234
|
+
// those blocks, so no app keeps a hand-written twin of a feature's variables.
|
|
235
|
+
|
|
236
|
+
import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
237
|
+
import { createCryptoShreddingFeature } from "@cosmicdrift/kumiko-bundled-features/crypto-shredding";
|
|
238
|
+
import { createFileFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
|
|
239
|
+
import {
|
|
240
|
+
createFileProviderS3Feature,
|
|
241
|
+
createS3EnvSchema,
|
|
242
|
+
} from "@cosmicdrift/kumiko-bundled-features/file-provider-s3";
|
|
243
|
+
import {
|
|
244
|
+
buildInfoEnvSchema,
|
|
245
|
+
demoAdminEnvSchema,
|
|
246
|
+
} from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
|
|
247
|
+
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
248
|
+
import { stripeEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
249
|
+
import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";
|
|
250
|
+
import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
|
|
251
|
+
import * as z from "zod";
|
|
252
|
+
|
|
253
|
+
export function createEnvCompositionFeatures() {
|
|
254
|
+
return [
|
|
255
|
+
createConfigFeature(),
|
|
256
|
+
createSecretsFeature(),
|
|
257
|
+
createCryptoShreddingFeature(),
|
|
258
|
+
createFileFoundationFeature(),
|
|
259
|
+
createFileProviderS3Feature({ fromEnv: true }),
|
|
260
|
+
] as const;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// App-level blocks: none of these belong to a mounted feature, so they go
|
|
264
|
+
// into extend. A second bucket reuses the S3 block under its own prefix.
|
|
265
|
+
const appEnvSchema = z.object({
|
|
266
|
+
...buildInfoEnvSchema.shape,
|
|
267
|
+
...demoAdminEnvSchema.shape,
|
|
268
|
+
...stripeEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
269
|
+
...createS3EnvSchema({ prefix: "RELEASES_S3_" }).shape,
|
|
270
|
+
APP_PUBLIC_URL: z.url().describe("Public origin of the app, used in mail links."),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export function composeAppEnv(
|
|
274
|
+
features: ReturnType<typeof createEnvCompositionFeatures> = createEnvCompositionFeatures(),
|
|
275
|
+
) {
|
|
276
|
+
return composeEnvSchema({ core: frameworkCoreEnvSchema, features, extend: appEnvSchema });
|
|
277
|
+
}
|
|
278
|
+
```
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/host-routing. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# host-routing
|
|
4
|
+
|
|
5
|
+
createHostRoutingFeature + createHostDispatch — tenant per subdomain, per-host HTML and CSP, locale redirect, shared by runProdApp and the dev server.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# Host Routing
|
|
10
|
+
|
|
11
|
+
Tenant per subdomain with one options object for the API and the HTML pages.
|
|
12
|
+
|
|
13
|
+
## What it shows
|
|
14
|
+
|
|
15
|
+
- `createHostRoutingFeature(options)`: the Host header is the only source of the
|
|
16
|
+
anonymous tenant; a forged `X-Tenant` header answers `400 tenant_mismatch`.
|
|
17
|
+
- `createHostDispatch(options)`: HTML file and CSP per host, a 404 page for
|
|
18
|
+
unknown shops, and `/de/...` redirecting to the unprefixed path.
|
|
19
|
+
- The same `hostDispatch` goes into `runProdApp({ hostDispatch })`; the dev
|
|
20
|
+
server takes it through `toDevHostDispatch` from `@cosmicdrift/kumiko-dev-server`.
|
|
21
|
+
|
|
22
|
+
## Source
|
|
23
|
+
|
|
24
|
+
Feature entry point: `src/feature.ts`.
|
|
25
|
+
|
|
26
|
+
## Tests
|
|
27
|
+
|
|
28
|
+
Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd samples/recipes/host-routing
|
|
32
|
+
bun test
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Source files
|
|
36
|
+
|
|
37
|
+
### src/__tests__/feature.integration.test.ts
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
41
|
+
import {
|
|
42
|
+
createAuthFoundationFeature,
|
|
43
|
+
resolveAnonymousAccessFromRegistry,
|
|
44
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
45
|
+
import {
|
|
46
|
+
configValuesTable,
|
|
47
|
+
createConfigFeature,
|
|
48
|
+
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
49
|
+
import {
|
|
50
|
+
createTenantFeature,
|
|
51
|
+
tenantEntity,
|
|
52
|
+
tenantMembershipsTable,
|
|
53
|
+
} from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
54
|
+
import { seedTenant } from "@cosmicdrift/kumiko-bundled-features/tenant/seeding";
|
|
55
|
+
import { createUserFeature, userEntity } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
56
|
+
import { createAnonymousUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
57
|
+
import {
|
|
58
|
+
setupTestStack,
|
|
59
|
+
type TestStack,
|
|
60
|
+
unsafeCreateEntityTable,
|
|
61
|
+
unsafePushTables,
|
|
62
|
+
} from "@cosmicdrift/kumiko-testing/stack";
|
|
63
|
+
import { hostDispatch, hostRoutingFeature, routingOptions, storefrontFeature } from "../feature";
|
|
64
|
+
|
|
65
|
+
const SHOPS = [
|
|
66
|
+
{ id: "00000000-0000-4000-8000-0000000000b1" as TenantId, key: "acme", name: "Acme" },
|
|
67
|
+
{ id: "00000000-0000-4000-8000-0000000000b2" as TenantId, key: "globex", name: "Globex" },
|
|
68
|
+
{ id: "00000000-0000-4000-8000-0000000000b3" as TenantId, key: "initech", name: "Initech" },
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
let stack: TestStack;
|
|
72
|
+
|
|
73
|
+
const systemQuery = (type: string, payload: unknown, tenantId: TenantId) =>
|
|
74
|
+
stack.dispatcher.query(type, payload, createAnonymousUser(tenantId));
|
|
75
|
+
|
|
76
|
+
function whoami(host: string, headers: Record<string, string> = {}) {
|
|
77
|
+
return stack.http.raw(
|
|
78
|
+
"POST",
|
|
79
|
+
"/api/query",
|
|
80
|
+
{ type: "storefront:query:whoami", payload: {} },
|
|
81
|
+
{ Host: host, ...headers },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
beforeAll(async () => {
|
|
86
|
+
stack = await setupTestStack({
|
|
87
|
+
features: [
|
|
88
|
+
createConfigFeature(),
|
|
89
|
+
createUserFeature(),
|
|
90
|
+
createTenantFeature(),
|
|
91
|
+
createAuthFoundationFeature(),
|
|
92
|
+
hostRoutingFeature,
|
|
93
|
+
storefrontFeature,
|
|
94
|
+
],
|
|
95
|
+
// The app opts into anonymous access; the routing feature supplies the
|
|
96
|
+
// resolver and existence check through the registry, as in runProdApp.
|
|
97
|
+
anonymousAccess: {},
|
|
98
|
+
enrichAnonymousAccess: (base, deps) => resolveAnonymousAccessFromRegistry(base, deps),
|
|
99
|
+
});
|
|
100
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
101
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
102
|
+
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
103
|
+
for (const shop of SHOPS) await seedTenant(stack.db, shop);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterAll(async () => stack?.cleanup());
|
|
107
|
+
|
|
108
|
+
describe("host-routing recipe", () => {
|
|
109
|
+
test("three shop hosts land on three tenants", async () => {
|
|
110
|
+
for (const shop of SHOPS) {
|
|
111
|
+
const res = await whoami(`${shop.key}.${routingOptions.baseDomain}`);
|
|
112
|
+
expect(res.status).toBe(200);
|
|
113
|
+
const body = (await res.json()) as { data: { tenantId: string } };
|
|
114
|
+
expect(body.data.tenantId).toBe(shop.id);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a forged X-Tenant header is rejected", async () => {
|
|
119
|
+
const res = await whoami(`acme.${routingOptions.baseDomain}`, { "X-Tenant": SHOPS[1].id });
|
|
120
|
+
expect(res.status).toBe(400);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("the page dispatch serves the shop shell with its CSP, or the 404 page", async () => {
|
|
124
|
+
expect(
|
|
125
|
+
await hostDispatch({ host: "globex.shop.test", path: "/", search: "" }, { systemQuery }),
|
|
126
|
+
).toEqual({ kind: "html", file: "shop.html", csp: routingOptions.tenant.csp });
|
|
127
|
+
expect(
|
|
128
|
+
await hostDispatch({ host: "nope.shop.test", path: "/", search: "" }, { systemQuery }),
|
|
129
|
+
).toEqual({ kind: "not-found", file: "unknown-shop.html" });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("the default-locale prefix redirects, the English prefix gets its own file", async () => {
|
|
133
|
+
expect(
|
|
134
|
+
await hostDispatch({ host: "shop.test", path: "/de/about", search: "?a=1" }, { systemQuery }),
|
|
135
|
+
).toEqual({ kind: "redirect", to: "/about?a=1", status: 301 });
|
|
136
|
+
expect(
|
|
137
|
+
await hostDispatch({ host: "shop.test", path: "/en/about", search: "" }, { systemQuery }),
|
|
138
|
+
).toEqual({ kind: "html", file: "en/index.html" });
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### src/feature.ts
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// Host routing: one options object drives both halves. The feature makes the
|
|
147
|
+
// Host header the only source of the anonymous tenant; the dispatch picks the
|
|
148
|
+
// HTML file per host for runProdApp's `hostDispatch` (and, through
|
|
149
|
+
// toDevHostDispatch, for the dev server).
|
|
150
|
+
|
|
151
|
+
import {
|
|
152
|
+
createHostDispatch,
|
|
153
|
+
createHostRoutingFeature,
|
|
154
|
+
type HostRoutingOptions,
|
|
155
|
+
} from "@cosmicdrift/kumiko-bundled-features/host-routing";
|
|
156
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
157
|
+
import { z } from "zod";
|
|
158
|
+
|
|
159
|
+
export const routingOptions: HostRoutingOptions = {
|
|
160
|
+
tenantBy: "host",
|
|
161
|
+
baseDomain: "shop.test",
|
|
162
|
+
apex: { file: "index.html", localeFiles: { en: "en/index.html" } },
|
|
163
|
+
tenant: { file: "shop.html", csp: "default-src 'self'; frame-ancestors 'none'" },
|
|
164
|
+
unknownTenant: { file: "unknown-shop.html" },
|
|
165
|
+
locales: { default: "de", prefixed: ["en"] },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const hostRoutingFeature = createHostRoutingFeature(routingOptions);
|
|
169
|
+
export const hostDispatch = createHostDispatch(routingOptions);
|
|
170
|
+
|
|
171
|
+
// A public query that reports which tenant the request landed on.
|
|
172
|
+
export const storefrontFeature = defineFeature("storefront", (r) => {
|
|
173
|
+
r.queryHandler({
|
|
174
|
+
name: "whoami",
|
|
175
|
+
schema: z.object({}),
|
|
176
|
+
access: { roles: ["anonymous"] },
|
|
177
|
+
rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
|
|
178
|
+
handler: async (query) => ({ tenantId: query.user.tenantId }),
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
```
|
|
@@ -71,6 +71,7 @@ import { createLegalPagesFeature } from "@cosmicdrift/kumiko-bundled-features/le
|
|
|
71
71
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
72
72
|
|
|
73
73
|
await runProdApp({
|
|
74
|
+
envSchema, // composeEnvSchema({ core: frameworkCoreEnvSchema, features })
|
|
74
75
|
features: [
|
|
75
76
|
createTemplateResolverFeature(),
|
|
76
77
|
createLegalPagesFeature(),
|
|
@@ -151,6 +152,20 @@ await seedTextBlock(db, {
|
|
|
151
152
|
→ Templates for full legally-sound texts: e-recht24.de
|
|
152
153
|
or datenschutz-generator.de by Dr. Schwenke.
|
|
153
154
|
|
|
155
|
+
With `runProdApp` the same blocks can come from a JSON file on every boot.
|
|
156
|
+
`legalContentSeed` updates existing blocks, so the file stays authoritative:
|
|
157
|
+
|
|
158
|
+
```typescript illustration
|
|
159
|
+
import legalContent from "./legal-content.json" with { type: "json" };
|
|
160
|
+
import { legalContentSeed } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
|
|
161
|
+
|
|
162
|
+
runProdApp({ /* ... */ seeds: [legalContentSeed({ blocks: legalContent })] });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The recipe mounts `createLegalPagesFeature({ terms: true, wrapLayout })`, so it
|
|
166
|
+
also serves `/legal/nutzungsbedingungen` and `/legal/terms`, and wraps every
|
|
167
|
+
page in `createLegalPagesLayout({ siteName })`.
|
|
168
|
+
|
|
154
169
|
### 4. Visit the pages
|
|
155
170
|
|
|
156
171
|
After the seed these URLs are immediately live:
|
|
@@ -216,6 +231,7 @@ from SYSTEM_TENANT:
|
|
|
216
231
|
|
|
217
232
|
```ts
|
|
218
233
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
234
|
+
import { legalContentSeed } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
|
|
219
235
|
import {
|
|
220
236
|
createTemplateResolverApi,
|
|
221
237
|
templateResourceEntity,
|
|
@@ -229,7 +245,12 @@ import {
|
|
|
229
245
|
type TestStack,
|
|
230
246
|
unsafeCreateEntityTable,
|
|
231
247
|
} from "@cosmicdrift/kumiko-testing/stack";
|
|
232
|
-
import {
|
|
248
|
+
import {
|
|
249
|
+
LEGAL_ROUTES,
|
|
250
|
+
LEGAL_TERMS_ROUTES,
|
|
251
|
+
legalPagesFeature,
|
|
252
|
+
templateResolverFeature,
|
|
253
|
+
} from "../feature";
|
|
233
254
|
|
|
234
255
|
let stack: TestStack;
|
|
235
256
|
let db: DbConnection;
|
|
@@ -334,6 +355,23 @@ describe("legal-pages sample", () => {
|
|
|
334
355
|
const res = await stack.app.request("/legal/impressum");
|
|
335
356
|
expect(res.headers.get("cache-control")).toBe("public, max-age=60, must-revalidate");
|
|
336
357
|
});
|
|
358
|
+
|
|
359
|
+
test("terms from legalContentSeed render in both languages inside the app layout", async () => {
|
|
360
|
+
await legalContentSeed({
|
|
361
|
+
blocks: [
|
|
362
|
+
{ slug: "terms", locale: "de", title: "Nutzungsbedingungen", content: "## Geltung" },
|
|
363
|
+
{ slug: "terms", locale: "en", title: "Terms of Service", content: "## Scope" },
|
|
364
|
+
],
|
|
365
|
+
})({ db });
|
|
366
|
+
const expected = { de: "<h2>Geltung</h2>", en: "<h2>Scope</h2>" } as const;
|
|
367
|
+
for (const route of LEGAL_TERMS_ROUTES) {
|
|
368
|
+
const res = await stack.app.request(route.path);
|
|
369
|
+
expect(res.status).toBe(200);
|
|
370
|
+
const body = await res.text();
|
|
371
|
+
expect(body).toContain(expected[route.lang]);
|
|
372
|
+
expect(body).toContain("· Sample App</title>");
|
|
373
|
+
}
|
|
374
|
+
});
|
|
337
375
|
});
|
|
338
376
|
```
|
|
339
377
|
|
|
@@ -377,8 +415,10 @@ describe("legal-pages sample", () => {
|
|
|
377
415
|
|
|
378
416
|
import {
|
|
379
417
|
createLegalPagesFeature,
|
|
418
|
+
createLegalPagesLayout,
|
|
380
419
|
LEGAL_REQUIRED_BLOCKS,
|
|
381
420
|
LEGAL_ROUTES,
|
|
421
|
+
LEGAL_TERMS_ROUTES,
|
|
382
422
|
} from "@cosmicdrift/kumiko-bundled-features/legal-pages";
|
|
383
423
|
import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
384
424
|
|
|
@@ -386,8 +426,17 @@ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-featu
|
|
|
386
426
|
// requires sie. r.requires({ features: ["template-resolver"] }) greift automatisch im
|
|
387
427
|
// legal-pages-Feature.
|
|
388
428
|
export const templateResolverFeature = createTemplateResolverFeature();
|
|
389
|
-
|
|
429
|
+
// `terms: true` adds the opt-in terms routes; the layout puts the app's chrome
|
|
430
|
+
// around every legal page.
|
|
431
|
+
export const legalPagesFeature = createLegalPagesFeature({
|
|
432
|
+
terms: true,
|
|
433
|
+
wrapLayout: createLegalPagesLayout({
|
|
434
|
+
siteName: "Sample App",
|
|
435
|
+
footerHtml: (lang) =>
|
|
436
|
+
lang === "de" ? "<footer>Sample App</footer>" : "<footer>Sample App (en)</footer>",
|
|
437
|
+
}),
|
|
438
|
+
});
|
|
390
439
|
|
|
391
440
|
// Re-exports für Tests + andere Demos
|
|
392
|
-
export { LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES };
|
|
441
|
+
export { LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES, LEGAL_TERMS_ROUTES };
|
|
393
442
|
```
|
|
@@ -77,6 +77,7 @@ const masterKeyProvider = createEnvMasterKeyProvider({ env: process.env });
|
|
|
77
77
|
const resolver = createConfigResolver();
|
|
78
78
|
|
|
79
79
|
await runProdApp({
|
|
80
|
+
envSchema, // composeEnvSchema({ core: frameworkCoreEnvSchema, features })
|
|
80
81
|
extraContext: ({ db, registry }) => ({
|
|
81
82
|
configResolver: resolver,
|
|
82
83
|
_configAccessorFactory: createConfigAccessorFactory(registry, resolver),
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
} from "@cosmicdrift/kumiko-bundled-features/managed-pages";
|
|
62
62
|
|
|
63
63
|
await runProdApp({
|
|
64
|
+
envSchema, // composeEnvSchema({ core: frameworkCoreEnvSchema, features })
|
|
64
65
|
features: [
|
|
65
66
|
createManagedPagesFeature({
|
|
66
67
|
// Host → tenantId. Single-tenant returns a constant; multi-tenant maps
|