@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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Sofortiges Blind-Index-Nulling nach einem Subject-Erase (#818).
|
|
2
|
+
//
|
|
3
|
+
// Nach kms.eraseKey ist der Ciphertext unlesbar, aber die deterministische
|
|
4
|
+
// bidx-Spalte bliebe bis zum nächsten Write/Rebuild matchbar — ein
|
|
5
|
+
// Linkage-Fenster ("hat irgendeine Row den Wert X"). Dieser Sweep schließt
|
|
6
|
+
// es sofort: der Ciphertext nennt sein Subject inline
|
|
7
|
+
// (kumiko-pii:v1:<subjectKey>:...), also findet ein LIKE-Prefix-Match exakt
|
|
8
|
+
// die Rows des erased Subjects — pro lookupable-Feld ein UPDATE.
|
|
9
|
+
//
|
|
10
|
+
// Rows, die der Forget-Lauf ohnehin via Executor löscht/anonymisiert,
|
|
11
|
+
// bekommen ihren bidx dort automatisch neu berechnet; dieser Sweep deckt
|
|
12
|
+
// die liegen bleibenden Rows ab (fremde Entities mit userOwned-Feldern).
|
|
13
|
+
|
|
14
|
+
import { collectLookupableFields } from "../crypto/blind-index";
|
|
15
|
+
import type { FeatureDefinition } from "../engine/types";
|
|
16
|
+
import { toSnakeCase } from "../utils/case";
|
|
17
|
+
import type { DbRunner } from "./connection";
|
|
18
|
+
import { resolveTableName } from "./entity-table-meta";
|
|
19
|
+
import { executeRawQuery } from "./queries/raw-sql";
|
|
20
|
+
|
|
21
|
+
function quoteIdent(name: string): string {
|
|
22
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function escapeLikePattern(value: string): string {
|
|
26
|
+
return value.replace(/[\\%_]/g, (m) => `\\${m}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function nullBlindIndexesForSubject(
|
|
30
|
+
db: DbRunner,
|
|
31
|
+
features: ReadonlyMap<string, FeatureDefinition>,
|
|
32
|
+
subjectKey: string,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
// "v%" matches any format version (v1 no-AAD, v2 AAD-bound, #1263) — the
|
|
35
|
+
// subject key placement is stable across versions.
|
|
36
|
+
const likePattern = `kumiko-pii:v%:${escapeLikePattern(subjectKey)}:%`;
|
|
37
|
+
for (const feature of features.values()) {
|
|
38
|
+
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
39
|
+
const lookupable = collectLookupableFields(entity);
|
|
40
|
+
if (lookupable.length === 0) continue;
|
|
41
|
+
// Kein featureName-Prefix — der Dispatcher baut Entity-Tables ohne
|
|
42
|
+
// (buildEntityTable ohne featureName-Option), der Sweep muss dieselben
|
|
43
|
+
// Namen treffen.
|
|
44
|
+
const tableName = resolveTableName(entityName, entity, undefined);
|
|
45
|
+
for (const fieldName of lookupable) {
|
|
46
|
+
const snake = toSnakeCase(fieldName);
|
|
47
|
+
await executeRawQuery(
|
|
48
|
+
db,
|
|
49
|
+
`UPDATE ${quoteIdent(tableName)} SET ${quoteIdent(`${snake}_bidx`)} = NULL WHERE ${quoteIdent(snake)} LIKE $1`,
|
|
50
|
+
[likePattern],
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/db/bun-provider.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
// Bun.SQL hat kein LISTEN — postgres-js-Peer für event-dispatcher.
|
|
9
9
|
|
|
10
10
|
import postgres from "postgres";
|
|
11
|
-
import type {
|
|
11
|
+
import type { DbConnectionOptions, DbPoolHandle } from "./api";
|
|
12
12
|
|
|
13
|
-
export function createBunConnection(url: string, options: DbConnectionOptions = {}):
|
|
13
|
+
export function createBunConnection(url: string, options: DbConnectionOptions = {}): DbPoolHandle {
|
|
14
14
|
const bunOpts: { max?: number; idleTimeout?: number; connectionTimeout?: number } = {};
|
|
15
15
|
if (options.maxConnections !== undefined) bunOpts.max = options.maxConnections;
|
|
16
16
|
if (options.idleTimeoutSeconds !== undefined) bunOpts.idleTimeout = options.idleTimeoutSeconds;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// collectTableMetas — kanonische ENTITY_METAS-Quelle für `kumiko schema
|
|
2
2
|
// generate`. Erfasst dieselben Tabellen-Quellen wie der setupTestStack-
|
|
3
|
-
// auto-push (entities,
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// crashte (#255).
|
|
3
|
+
// auto-push (entities, storeTables, projections, multiStreamProjections) —
|
|
4
|
+
// die frühere Template-Variante sammelte nur entities + storeTables, wodurch
|
|
5
|
+
// projection-only-Tabellen (z.B. billing-foundation read_subscriptions) nie
|
|
6
|
+
// in Migrations landeten und der erste Prod-Write crashte (#255).
|
|
8
7
|
|
|
9
8
|
import type { FeatureDefinition } from "../engine/types";
|
|
10
9
|
import { compareByCodepoint } from "../utils";
|
|
@@ -60,11 +59,11 @@ export function collectTableMetas(
|
|
|
60
59
|
metas.push(meta);
|
|
61
60
|
byName.set(meta.tableName, { meta, origin: `entity "${name}" (${feature.name})` });
|
|
62
61
|
}
|
|
63
|
-
for (const entry of Object.values(feature.
|
|
62
|
+
for (const entry of Object.values(feature.storeTables)) {
|
|
64
63
|
metas.push(entry.meta);
|
|
65
64
|
byName.set(entry.meta.tableName, {
|
|
66
65
|
meta: entry.meta,
|
|
67
|
-
origin: `
|
|
66
|
+
origin: `storeTable "${entry.name}" (${feature.name})`,
|
|
68
67
|
});
|
|
69
68
|
}
|
|
70
69
|
}
|
package/src/db/config-seed.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { v5 as uuidv5 } from "uuid";
|
|
|
2
2
|
import type { EntityDefinition } from "../engine";
|
|
3
3
|
import { createSystemUser, SYSTEM_TENANT_ID } from "../engine";
|
|
4
4
|
import type { ConfigSeedDef, Registry } from "../engine/types";
|
|
5
|
+
import type { EnvelopeCipher } from "../secrets/envelope-cipher";
|
|
5
6
|
import type { DbConnection } from "./connection";
|
|
6
|
-
import type { EncryptionProvider } from "./encryption";
|
|
7
7
|
import { createEventStoreExecutor } from "./event-store-executor";
|
|
8
8
|
import type { EntityTable } from "./table-builder";
|
|
9
9
|
import { createTenantDb } from "./tenant-db";
|
|
@@ -30,7 +30,7 @@ export async function seedConfigValues<E extends EntityDefinition>(
|
|
|
30
30
|
entity: E,
|
|
31
31
|
registry: Registry,
|
|
32
32
|
db: DbConnection,
|
|
33
|
-
|
|
33
|
+
cipher?: EnvelopeCipher,
|
|
34
34
|
): Promise<{ created: number; skipped: number }> {
|
|
35
35
|
let created = 0;
|
|
36
36
|
let skipped = 0;
|
|
@@ -48,12 +48,12 @@ export async function seedConfigValues<E extends EntityDefinition>(
|
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Encrypted keys without
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
if (keyDef.encrypted && !
|
|
51
|
+
// Encrypted keys without a cipher would silently write plaintext to a
|
|
52
|
+
// column the resolver later tries to decrypt — fail loud at boot, not
|
|
53
|
+
// on first read in prod.
|
|
54
|
+
if (keyDef.encrypted && !cipher) {
|
|
55
55
|
throw new Error(
|
|
56
|
-
`seedConfigValues: key "${seed.key}" is encrypted but no
|
|
56
|
+
`seedConfigValues: key "${seed.key}" is encrypted but no EnvelopeCipher was supplied.`,
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -77,8 +77,8 @@ export async function seedConfigValues<E extends EntityDefinition>(
|
|
|
77
77
|
const aggregateId = uuidv5(idSource, CONFIG_SEED_NS);
|
|
78
78
|
|
|
79
79
|
let value = JSON.stringify(seed.value);
|
|
80
|
-
if (keyDef.encrypted &&
|
|
81
|
-
value =
|
|
80
|
+
if (keyDef.encrypted && cipher) {
|
|
81
|
+
value = await cipher.encrypt(value, { tenantId });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const payload: Record<string, unknown> = {
|
package/src/db/connection.ts
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
// DB-Connection-Types: provider-agnostic via db/api.ts.
|
|
2
2
|
// createConnection delegiert an postgres-provider (default) oder bun-provider (DB_PROVIDER=bun).
|
|
3
|
+
import type { DbConnection, PgClient } from "@cosmicdrift/kumiko-types/db-connection";
|
|
3
4
|
import postgres from "postgres";
|
|
4
5
|
import { readPositiveIntEnv } from "../utils/env-parse";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
export type
|
|
11
|
-
|
|
12
|
-
export type DbTx = postgres.TransactionSql<any> | any;
|
|
13
|
-
export type DbRunner = DbConnection | DbTx;
|
|
14
|
-
export type DbRow = Record<string, unknown>;
|
|
15
|
-
export type PgClient = ReturnType<typeof postgres>;
|
|
16
|
-
|
|
17
|
-
export type PgListenClient = ReturnType<typeof postgres>;
|
|
7
|
+
// Raw client types (postgres-js | Bun.SQL) — the name used across query/
|
|
8
|
+
// event-store/pipeline call sites. The structural pool handle from ./api is
|
|
9
|
+
// `DbPoolHandle` (createConnection's return type) to avoid colliding with this.
|
|
10
|
+
export type * from "@cosmicdrift/kumiko-types/db-connection";
|
|
11
|
+
export type { DbConnectionOptions, DbPoolHandle } from "./api";
|
|
12
|
+
export { createConnection } from "./api";
|
|
18
13
|
|
|
19
14
|
// Legacy: postgres-js only. Neue Aufrufer: createConnection() aus api.ts.
|
|
20
15
|
// guard:dup-ok — andere Layer als createPgConnection (gibt DbConnection zurück, nicht postgres-Instanz)
|
package/src/db/cursor.ts
CHANGED
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export type CursorQueryOptions = {
|
|
4
|
-
tenantId: TenantId;
|
|
5
|
-
cursor?: string;
|
|
6
|
-
limit?: number;
|
|
7
|
-
filterIds?: readonly EntityId[];
|
|
8
|
-
sort?: string;
|
|
9
|
-
sortDirection?: "asc" | "desc";
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type CursorResult<T> = {
|
|
13
|
-
rows: T[];
|
|
14
|
-
nextCursor: string | null;
|
|
15
|
-
/** Optional total row count — nur present wenn der Caller `totalCount: true`
|
|
16
|
-
* in der Query setzt. */
|
|
17
|
-
total?: number;
|
|
18
|
-
};
|
|
1
|
+
export type { CursorQueryOptions, CursorResult } from "@cosmicdrift/kumiko-types/cursor-types";
|
|
19
2
|
|
|
20
3
|
// String-basiert damit UUIDs (Default seit Sprint F) + Integer-Auto-Increment
|
|
21
4
|
// durch denselben Cursor-Pfad laufen. UUIDv7 erfüllt die lex-Monotonie
|
package/src/db/dialect.ts
CHANGED
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
//
|
|
13
13
|
// The framework no longer imports drizzle-orm at runtime — schema-files
|
|
14
14
|
// use only this module.
|
|
15
|
-
|
|
15
|
+
//
|
|
16
|
+
// Static import, not the ambient global: Bun doesn't expose Temporal on
|
|
17
|
+
// globalThis, so instantToDriver crashed on timestamptz writes (#1480).
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type ColumnHandle,
|
|
21
|
+
KUMIKO_COLUMNS_SYMBOL,
|
|
22
|
+
KUMIKO_META_SYMBOL,
|
|
23
|
+
KUMIKO_NAME_SYMBOL,
|
|
24
|
+
type SchemaTable,
|
|
25
|
+
} from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
26
|
+
import { Temporal } from "temporal-polyfill";
|
|
16
27
|
import type {
|
|
17
28
|
ColumnMeta,
|
|
18
29
|
CompositePrimaryKeyMeta,
|
|
@@ -21,6 +32,8 @@ import type {
|
|
|
21
32
|
PgType,
|
|
22
33
|
} from "./entity-table-meta";
|
|
23
34
|
|
|
35
|
+
export type { ColumnHandle, SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
36
|
+
|
|
24
37
|
// Public type aliases — historical compat for callers that used to import
|
|
25
38
|
// these from drizzle-orm/pg-core. SelectQuery is no longer a meaningful
|
|
26
39
|
// shape (no chain builder); TableColumns is the new SchemaTable union.
|
|
@@ -29,31 +42,6 @@ export type TableColumns<_T = any> = SchemaTable;
|
|
|
29
42
|
// biome-ignore lint/suspicious/noExplicitAny: legacy type — chain API is gone
|
|
30
43
|
export type SelectQuery = any;
|
|
31
44
|
|
|
32
|
-
// Column handle exposed on the SchemaTable. The `name` is the SQL column
|
|
33
|
-
// name (snake_case); legacy code accesses `table.fieldName.name` to
|
|
34
|
-
// produce raw SQL.
|
|
35
|
-
export type ColumnHandle = {
|
|
36
|
-
readonly name: string;
|
|
37
|
-
readonly pgType: PgType;
|
|
38
|
-
readonly getSQLType: () => string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
42
|
-
const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
|
|
43
|
-
// Shadow-proof handle on the EntityTableMeta. The column handles below are
|
|
44
|
-
// spread as enumerable props, so an entity field named `source`/`columns`/
|
|
45
|
-
// `tableName`/… would overwrite the matching meta key. extractTableInfo reads
|
|
46
|
-
// the canonical meta from this symbol instead of the (shadowable) props.
|
|
47
|
-
const KUMIKO_META_SYMBOL = Symbol.for("kumiko:schema:Meta");
|
|
48
|
-
|
|
49
|
-
// SchemaTable — opaque shape with both EntityTableMeta + Symbol-based
|
|
50
|
-
// introspection. Returned by `table(...)`.
|
|
51
|
-
export type SchemaTable = EntityTableMeta & {
|
|
52
|
-
readonly [KUMIKO_NAME_SYMBOL]: string;
|
|
53
|
-
readonly [KUMIKO_COLUMNS_SYMBOL]: Record<string, ColumnHandle>;
|
|
54
|
-
readonly [field: string]: unknown;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
45
|
function isNumericPgType(t: PgType): t is `numeric(${number},${number})` {
|
|
58
46
|
return t.startsWith("numeric(");
|
|
59
47
|
}
|
|
@@ -72,6 +60,8 @@ export function pgTypeToSqlType(pgType: PgType): string {
|
|
|
72
60
|
return "boolean";
|
|
73
61
|
case "integer":
|
|
74
62
|
return "integer";
|
|
63
|
+
case "double precision":
|
|
64
|
+
return "double precision";
|
|
75
65
|
case "bigint":
|
|
76
66
|
return "bigint";
|
|
77
67
|
case "serial":
|
|
@@ -234,6 +224,10 @@ export function integer(name: string): ColumnBuilder<number> {
|
|
|
234
224
|
return buildColumn(name, "integer") as ColumnBuilder<number>;
|
|
235
225
|
}
|
|
236
226
|
|
|
227
|
+
export function doublePrecision(name: string): ColumnBuilder<number> {
|
|
228
|
+
return buildColumn(name, "double precision") as ColumnBuilder<number>;
|
|
229
|
+
}
|
|
230
|
+
|
|
237
231
|
export function serial(name: string): ColumnBuilder<number> {
|
|
238
232
|
return buildColumn(name, "serial") as ColumnBuilder<number>;
|
|
239
233
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Entity-field encryption (`encrypted: true` on text/longText fields),
|
|
2
|
+
// backed by the same envelope cipher as encrypted config values: JSON
|
|
3
|
+
// StoredEnvelope in the TEXT column, kekVersion per value, legacy
|
|
4
|
+
// single-key ciphertexts (pre-envelope ENCRYPTION_KEY era) readable via
|
|
5
|
+
// the cipher's legacy fallback until re-encrypted.
|
|
6
|
+
|
|
7
|
+
import type { EntityDefinition, TenantId } from "../engine/types";
|
|
8
|
+
import { createEnvMasterKeyProvider } from "../secrets/env-master-key-provider";
|
|
9
|
+
import type { EnvelopeCipher } from "../secrets/envelope-cipher";
|
|
10
|
+
import type { KeyScope } from "../secrets/types";
|
|
4
11
|
|
|
5
12
|
export function collectEncryptedFieldNames(entity: EntityDefinition): ReadonlySet<string> {
|
|
6
13
|
const names = new Set<string>();
|
|
@@ -12,70 +19,120 @@ export function collectEncryptedFieldNames(entity: EntityDefinition): ReadonlySe
|
|
|
12
19
|
return names;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
// BYOK hook: thread the row's tenant to the cipher when the row carries one
|
|
23
|
+
// (system-scope rows may not).
|
|
24
|
+
function scopeOf(row: Record<string, unknown>): KeyScope | undefined {
|
|
25
|
+
const tenantId = row["tenantId"];
|
|
26
|
+
return typeof tenantId === "string" ? { tenantId: tenantId as TenantId } : undefined; // @cast-boundary db-row
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function encryptFieldValue(
|
|
16
30
|
fieldName: string,
|
|
17
31
|
value: unknown,
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
cipher: EnvelopeCipher,
|
|
33
|
+
scope: KeyScope | undefined,
|
|
34
|
+
): Promise<string> {
|
|
20
35
|
if (value === null || value === undefined) {
|
|
21
36
|
throw new Error(`encrypted field "${fieldName}" cannot be null or undefined`);
|
|
22
37
|
}
|
|
23
38
|
if (typeof value !== "string") {
|
|
24
39
|
throw new Error(`encrypted field "${fieldName}" must be a string, got ${typeof value}`);
|
|
25
40
|
}
|
|
26
|
-
return
|
|
41
|
+
return cipher.encrypt(value, scope);
|
|
27
42
|
}
|
|
28
43
|
|
|
29
|
-
export function encryptEntityFieldValues(
|
|
44
|
+
export async function encryptEntityFieldValues(
|
|
30
45
|
row: Record<string, unknown>,
|
|
31
46
|
encryptedFields: ReadonlySet<string>,
|
|
32
|
-
|
|
47
|
+
cipher: EnvelopeCipher,
|
|
33
48
|
opts?: { onlyKeys?: Iterable<string> },
|
|
34
|
-
): Record<string, unknown
|
|
49
|
+
): Promise<Record<string, unknown>> {
|
|
35
50
|
if (encryptedFields.size === 0) return row;
|
|
36
51
|
const only = opts?.onlyKeys ? new Set(opts.onlyKeys) : null;
|
|
37
52
|
const out = { ...row };
|
|
53
|
+
const scope = scopeOf(row);
|
|
38
54
|
for (const name of encryptedFields) {
|
|
39
55
|
if (only && !only.has(name)) continue;
|
|
40
56
|
if (!(name in out)) continue;
|
|
41
57
|
const value = out[name];
|
|
42
58
|
if (value === null || value === undefined) continue;
|
|
43
|
-
out[name] = encryptFieldValue(name, value,
|
|
59
|
+
out[name] = await encryptFieldValue(name, value, cipher, scope);
|
|
44
60
|
}
|
|
45
61
|
return out;
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
export function decryptEntityFieldValues(
|
|
64
|
+
export async function decryptEntityFieldValues(
|
|
49
65
|
row: Record<string, unknown>,
|
|
50
66
|
encryptedFields: ReadonlySet<string>,
|
|
51
|
-
|
|
52
|
-
): Record<string, unknown
|
|
67
|
+
cipher: EnvelopeCipher,
|
|
68
|
+
): Promise<Record<string, unknown>> {
|
|
53
69
|
if (encryptedFields.size === 0) return row;
|
|
54
70
|
const out = { ...row };
|
|
71
|
+
const scope = scopeOf(row);
|
|
55
72
|
for (const name of encryptedFields) {
|
|
56
73
|
const value = out[name];
|
|
57
74
|
if (value === null || value === undefined) continue;
|
|
58
75
|
if (typeof value !== "string") continue;
|
|
59
|
-
out[name] =
|
|
76
|
+
out[name] = await cipher.decrypt(value, scope);
|
|
60
77
|
}
|
|
61
78
|
return out;
|
|
62
79
|
}
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
// Boot-injected app-wide cipher. run{Prod,Dev}App (and test setups that
|
|
82
|
+
// exercise encrypted fields) call configureEntityFieldEncryption once with
|
|
83
|
+
// the same cipher instance the config feature uses; executors resolve it
|
|
84
|
+
// lazily so entities without encrypted fields never touch it.
|
|
85
|
+
let injectedCipher: EnvelopeCipher | undefined;
|
|
65
86
|
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
export function configureEntityFieldEncryption(cipher: EnvelopeCipher | undefined): void {
|
|
88
|
+
injectedCipher = cipher;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveEntityFieldEncryption(): EnvelopeCipher {
|
|
92
|
+
if (!injectedCipher) {
|
|
70
93
|
throw new Error(
|
|
71
|
-
"
|
|
94
|
+
"entity-field encryption is not configured — encrypted entity fields need a master key " +
|
|
95
|
+
"(KUMIKO_SECRETS_MASTER_KEY_V<n>); run{Prod,Dev}App wire the cipher automatically, " +
|
|
96
|
+
"custom boots call configureEntityFieldEncryption(cipher).",
|
|
72
97
|
);
|
|
73
98
|
}
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
return injectedCipher;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Non-throwing probe for callers that degrade gracefully (DSGVO export
|
|
103
|
+
// replaces encrypted fields with an explicit marker instead of failing
|
|
104
|
+
// the whole export when no cipher is wired).
|
|
105
|
+
export function configuredEntityFieldEncryption(): EnvelopeCipher | undefined {
|
|
106
|
+
return injectedCipher;
|
|
76
107
|
}
|
|
77
108
|
|
|
78
109
|
/** @internal test-only */
|
|
79
110
|
export function resetEntityFieldEncryptionCacheForTests(): void {
|
|
80
|
-
|
|
111
|
+
injectedCipher = undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Boot-time availability probe for validateBoot: either a cipher was
|
|
115
|
+
// injected, or the env keyring must be constructible — eager construction
|
|
116
|
+
// surfaces missing AND malformed keys (wrong length, bad base64) at boot
|
|
117
|
+
// instead of on the first encrypted read in prod. validateBoot runs before
|
|
118
|
+
// configureEntityFieldEncryption, so the env probe is the common path.
|
|
119
|
+
export function validateEntityFieldEncryptionAvailable(): void {
|
|
120
|
+
// skip: an injected cipher (test seam / custom KMS) satisfies availability
|
|
121
|
+
if (injectedCipher) return;
|
|
122
|
+
try {
|
|
123
|
+
createEnvMasterKeyProvider({
|
|
124
|
+
env: {
|
|
125
|
+
...process.env,
|
|
126
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION:
|
|
127
|
+
process.env["KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION"] ?? "1",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
132
|
+
throw new Error(
|
|
133
|
+
`encrypted entity fields in use but no usable master key (${reason}) — set ` +
|
|
134
|
+
"KUMIKO_SECRETS_MASTER_KEY_V1 (32 bytes, base64) or inject a cipher via " +
|
|
135
|
+
"configureEntityFieldEncryption().",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
81
138
|
}
|
|
@@ -18,81 +18,26 @@
|
|
|
18
18
|
// auf Sondercases beschränken (child-projection-tables ohne tenant,
|
|
19
19
|
// append-only-logs mit serial PK, aggregate-ID ohne DEFAULT, …).
|
|
20
20
|
|
|
21
|
+
import { collectPiiSubjectFields } from "../crypto";
|
|
22
|
+
import type { EntityDefinition, EntityIndexDef, FieldDefinition } from "../engine/types";
|
|
21
23
|
import type {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
BuildEntityTableMetaOptions,
|
|
25
|
+
ColumnMeta,
|
|
26
|
+
EntityTableMeta,
|
|
27
|
+
IndexMeta,
|
|
28
|
+
UnmanagedTableInput,
|
|
29
|
+
} from "./entity-table-meta-types";
|
|
27
30
|
import { READ_MODEL_PREFIX, toSnakeCase, toTableName } from "./table-builder";
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| "bigint"
|
|
39
|
-
| "serial"
|
|
40
|
-
| "bigserial"
|
|
41
|
-
| "jsonb"
|
|
42
|
-
| "timestamptz"
|
|
43
|
-
| "timestamptz(3)"
|
|
44
|
-
// Exact decimal — precision/scale are encoded in the type string so the
|
|
45
|
-
// DDL renderer and read-coercion need no side-channel metadata.
|
|
46
|
-
| `numeric(${number},${number})`;
|
|
47
|
-
|
|
48
|
-
export type ColumnMeta = {
|
|
49
|
-
readonly name: string; // snake_case PG column name
|
|
50
|
-
readonly pgType: PgType;
|
|
51
|
-
readonly notNull: boolean;
|
|
52
|
-
// Raw SQL-default-expression (e.g. `now()`, `gen_random_uuid()`,
|
|
53
|
-
// `'[]'::jsonb`). undefined = no DEFAULT clause.
|
|
54
|
-
readonly defaultSql?: string;
|
|
55
|
-
readonly primaryKey?: boolean;
|
|
56
|
-
readonly identity?: boolean;
|
|
57
|
-
// bigint/bigserial only: JS round-trip mode. `number` = createBigIntField /
|
|
58
|
-
// drizzle mode:"number" (safe ≤2^53). `bigint` = money cents, raw unmanaged.
|
|
59
|
-
readonly bigintJsMode?: "number" | "bigint";
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export type IndexMeta = {
|
|
63
|
-
readonly name: string;
|
|
64
|
-
readonly columns: readonly string[]; // snake_case PG column names
|
|
65
|
-
readonly unique?: boolean;
|
|
66
|
-
// Raw SQL-where-expression for partial indexes. Caller is responsible
|
|
67
|
-
// for safety — emitted verbatim.
|
|
68
|
-
readonly whereSql?: string;
|
|
69
|
-
// Set wenn die EntityDefinition ein partial-Index hat (def.where als
|
|
70
|
-
// drizzle SQL-AST), der vom Generator nicht zuverlässig renderbar ist.
|
|
71
|
-
// Renderer emittiert das Statement dann AUSKOMMENTIERT mit Warn-Hint —
|
|
72
|
-
// App-Author muss das WHERE manuell im generierten SQL hinzufügen.
|
|
73
|
-
readonly needsManualWhere?: boolean;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export type CompositePrimaryKeyMeta = {
|
|
77
|
-
readonly name: string;
|
|
78
|
-
readonly columns: readonly string[];
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export type EntityTableMeta = {
|
|
82
|
-
readonly tableName: string;
|
|
83
|
-
readonly columns: readonly ColumnMeta[];
|
|
84
|
-
readonly indexes: readonly IndexMeta[];
|
|
85
|
-
// For tables with composite PK (no single id column, e.g. snapshots
|
|
86
|
-
// keyed by aggregate_id+version). When set, no column should have
|
|
87
|
-
// primaryKey:true; the constraint is emitted at table-level.
|
|
88
|
-
readonly compositePrimaryKey?: CompositePrimaryKeyMeta;
|
|
89
|
-
// Source-hint für Diagnose/Tests — nicht funktional verwendet.
|
|
90
|
-
// "managed" = aus EntityDefinition (mit base-columns + audit-trail).
|
|
91
|
-
// "unmanaged" = via defineUnmanagedTable — kein Standard-Audit, App
|
|
92
|
-
// trägt Verantwortung. Migration-Generator + Tooling können den
|
|
93
|
-
// discriminator nutzen um Warnungen zu rendern ("X tables are unmanaged").
|
|
94
|
-
readonly source: "managed" | "unmanaged";
|
|
95
|
-
};
|
|
32
|
+
export type {
|
|
33
|
+
BuildEntityTableMetaOptions,
|
|
34
|
+
ColumnMeta,
|
|
35
|
+
CompositePrimaryKeyMeta,
|
|
36
|
+
EntityTableMeta,
|
|
37
|
+
IndexMeta,
|
|
38
|
+
PgType,
|
|
39
|
+
UnmanagedTableInput,
|
|
40
|
+
} from "./entity-table-meta-types";
|
|
96
41
|
|
|
97
42
|
// Standard base-columns für event-sourced Read-Model-Tabellen. Spiegelt
|
|
98
43
|
// `buildBaseColumns()` aus table-builder.ts (drizzle-Variante).
|
|
@@ -153,7 +98,7 @@ function fieldToColumnMeta(
|
|
|
153
98
|
case "text":
|
|
154
99
|
case "longText": {
|
|
155
100
|
const def = fieldDefaultLiteral(field);
|
|
156
|
-
|
|
101
|
+
const cols: ColumnMeta[] = [
|
|
157
102
|
{
|
|
158
103
|
name: snake,
|
|
159
104
|
pgType: "text",
|
|
@@ -161,6 +106,13 @@ function fieldToColumnMeta(
|
|
|
161
106
|
...(def !== undefined && { defaultSql: def }),
|
|
162
107
|
},
|
|
163
108
|
];
|
|
109
|
+
// lookupable → HMAC-Blind-Index-Pendant. Nullable ist Pflicht:
|
|
110
|
+
// managedChangeRequiresRecreate würde eine NOT-NULL-Spalte ohne
|
|
111
|
+
// Default auf Bestandstabellen als DROP+Rebuild diffen.
|
|
112
|
+
if (field.type === "text" && field.lookupable === true) {
|
|
113
|
+
cols.push({ name: `${snake}_bidx`, pgType: "text", notNull: false });
|
|
114
|
+
}
|
|
115
|
+
return cols;
|
|
164
116
|
}
|
|
165
117
|
case "boolean": {
|
|
166
118
|
const def = fieldDefaultLiteral(field);
|
|
@@ -192,7 +144,7 @@ function fieldToColumnMeta(
|
|
|
192
144
|
return [
|
|
193
145
|
{
|
|
194
146
|
name: snake,
|
|
195
|
-
pgType: "integer",
|
|
147
|
+
pgType: field.integer === true ? "integer" : "double precision",
|
|
196
148
|
notNull: field.required === true,
|
|
197
149
|
...(def !== undefined && { defaultSql: def }),
|
|
198
150
|
},
|
|
@@ -280,11 +232,6 @@ export function resolveTableName(
|
|
|
280
232
|
return `${featureName}_${baseName}`;
|
|
281
233
|
}
|
|
282
234
|
|
|
283
|
-
export type BuildEntityTableMetaOptions = {
|
|
284
|
-
readonly featureName?: string;
|
|
285
|
-
readonly relations?: EntityRelations;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
235
|
export function buildEntityTableMeta(
|
|
289
236
|
entityName: string,
|
|
290
237
|
entity: EntityDefinition,
|
|
@@ -305,10 +252,17 @@ export function buildEntityTableMeta(
|
|
|
305
252
|
for (const c of baseCols) colByName.set(c.name, c);
|
|
306
253
|
|
|
307
254
|
const fieldNameToSnake = new Map<string, string>();
|
|
255
|
+
const bidxSnakeByFieldSnake = new Map<string, string>();
|
|
308
256
|
for (const [name, field] of Object.entries(entity.fields)) {
|
|
309
257
|
const fieldCols = fieldToColumnMeta(name, field, entity);
|
|
310
258
|
for (const c of fieldCols) colByName.set(c.name, c);
|
|
311
|
-
|
|
259
|
+
// Multi-column fields map to their primary column when its name IS the
|
|
260
|
+
// field's snake (text+bidx, money+currency) — matches the toSnakeCase
|
|
261
|
+
// fallback below, so explicit indexes keep resolving.
|
|
262
|
+
const primary = fieldCols[0];
|
|
263
|
+
if (primary && primary.name === toSnakeCase(name)) fieldNameToSnake.set(name, primary.name);
|
|
264
|
+
const bidxCol = fieldCols.find((c) => c.name.endsWith("_bidx"));
|
|
265
|
+
if (primary && bidxCol) bidxSnakeByFieldSnake.set(primary.name, bidxCol.name);
|
|
312
266
|
}
|
|
313
267
|
|
|
314
268
|
// Preserve base-col order, then any new user-col-names in fields-order.
|
|
@@ -347,6 +301,12 @@ export function buildEntityTableMeta(
|
|
|
347
301
|
indexes.push({ name: `${tableName}_${snake}_idx`, columns: [snake] });
|
|
348
302
|
}
|
|
349
303
|
|
|
304
|
+
// lookupable-Felder: Index auf der bidx-Spalte — der OR-Rewrite der
|
|
305
|
+
// Query-Compiler trifft sie bei jedem Equality-Lookup.
|
|
306
|
+
for (const bidxSnake of bidxSnakeByFieldSnake.values()) {
|
|
307
|
+
indexes.push({ name: `${tableName}_${bidxSnake}_idx`, columns: [bidxSnake] });
|
|
308
|
+
}
|
|
309
|
+
|
|
350
310
|
// Explizit deklarierte indexes (EntityIndexDef). `def.where` ist ein
|
|
351
311
|
// SqlExpression (`sql\`…\`` aus @cosmicdrift/kumiko-framework/db) —
|
|
352
312
|
// renderbar via `.text`. Unbekannte where-Shapes bleiben needsManualWhere.
|
|
@@ -364,13 +324,33 @@ export function buildEntityTableMeta(
|
|
|
364
324
|
...(whereSql !== undefined && { whereSql }),
|
|
365
325
|
...(def.where !== undefined && whereSql === undefined && { needsManualWhere: true }),
|
|
366
326
|
});
|
|
327
|
+
// Unique-Index über lookupable-Spalten: partielles bidx-Pendant, damit
|
|
328
|
+
// Uniqueness auch für verschlüsselte Rows greift. Das Original bleibt
|
|
329
|
+
// für Klartext-Alt-Rows; partial (IS NOT NULL) weil bidx bei erased/
|
|
330
|
+
// key-losen Rows NULL ist.
|
|
331
|
+
if (def.unique === true && def.where === undefined) {
|
|
332
|
+
const bidxCols = cols.map((c) => bidxSnakeByFieldSnake.get(c) ?? c);
|
|
333
|
+
if (bidxCols.some((c, i) => c !== cols[i])) {
|
|
334
|
+
const notNullParts = bidxCols
|
|
335
|
+
.filter((c, i) => c !== cols[i])
|
|
336
|
+
.map((c) => `"${c}" IS NOT NULL`);
|
|
337
|
+
indexes.push({
|
|
338
|
+
name: `${indexName}_bidx`,
|
|
339
|
+
columns: bidxCols,
|
|
340
|
+
unique: true,
|
|
341
|
+
whereSql: notNullParts.join(" AND "),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
367
345
|
}
|
|
368
346
|
|
|
347
|
+
const piiSubjectFields = collectPiiSubjectFields(entity);
|
|
369
348
|
return {
|
|
370
349
|
tableName,
|
|
371
350
|
columns,
|
|
372
351
|
indexes,
|
|
373
|
-
source: "managed",
|
|
352
|
+
source: options?.source ?? "managed",
|
|
353
|
+
...(piiSubjectFields.length > 0 && { piiSubjectFields }),
|
|
374
354
|
};
|
|
375
355
|
}
|
|
376
356
|
|
|
@@ -385,15 +365,8 @@ export function buildEntityTableMeta(
|
|
|
385
365
|
// jede neue defineUnmanagedTable-Stelle prüfen.
|
|
386
366
|
//
|
|
387
367
|
// Heutige use-cases im framework:
|
|
388
|
-
// - `
|
|
389
|
-
// - `
|
|
390
|
-
export type UnmanagedTableInput = {
|
|
391
|
-
readonly tableName: string;
|
|
392
|
-
readonly columns: readonly ColumnMeta[];
|
|
393
|
-
readonly indexes?: readonly IndexMeta[];
|
|
394
|
-
readonly compositePrimaryKey?: CompositePrimaryKeyMeta;
|
|
395
|
-
};
|
|
396
|
-
|
|
368
|
+
// - `store_delivery_attempts` — id kommt aus dem Aggregate-Stream
|
|
369
|
+
// - `store_job_run_logs` — child-table, serial PK, kein tenant-scope
|
|
397
370
|
function sqlExpressionText(where: unknown): string | undefined {
|
|
398
371
|
if (
|
|
399
372
|
typeof where === "object" &&
|