@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,248 @@
|
|
|
1
|
+
// Blind-Index end-to-end (#818): create/update schreiben die bidx-Spalte
|
|
2
|
+
// (nie ins Event-Log), Equality-Lookups matchen über den OR-Rewrite sowohl
|
|
3
|
+
// verschlüsselte als auch Klartext-Alt-Rows, Rebuild recomputet bidx aus
|
|
4
|
+
// dem Ciphertext (erased → NULL), und der Forget-Sweep nullt sofort.
|
|
5
|
+
|
|
6
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import {
|
|
8
|
+
computeBlindIndex,
|
|
9
|
+
configureBlindIndexKey,
|
|
10
|
+
configurePiiSubjectKms,
|
|
11
|
+
decodeBlindIndexKey,
|
|
12
|
+
InMemoryKmsAdapter,
|
|
13
|
+
isPiiCiphertext,
|
|
14
|
+
resetBlindIndexKeyForTests,
|
|
15
|
+
resetPiiSubjectKmsForTests,
|
|
16
|
+
subjectIdToKey,
|
|
17
|
+
} from "../../crypto";
|
|
18
|
+
import { defineFeature } from "../../engine/define-feature";
|
|
19
|
+
import { createEntity, createTextField } from "../../engine/factories";
|
|
20
|
+
import { createRegistry } from "../../engine/registry";
|
|
21
|
+
import { createEventsTable } from "../../event-store";
|
|
22
|
+
import { rebuildProjection } from "../../pipeline";
|
|
23
|
+
import { createProjectionStateTable } from "../../pipeline/projection-state";
|
|
24
|
+
import { createTestDb, type TestDb, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
25
|
+
import { nullBlindIndexesForSubject } from "../blind-index-cleanup";
|
|
26
|
+
import { createEventStoreExecutor } from "../event-store-executor";
|
|
27
|
+
import { asRawClient, fetchOne } from "../query";
|
|
28
|
+
import { buildEntityTable } from "../table-builder";
|
|
29
|
+
import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
30
|
+
|
|
31
|
+
const TEST_KEY_B64 = Buffer.alloc(32, 7).toString("base64");
|
|
32
|
+
const TEST_KEY = decodeBlindIndexKey(TEST_KEY_B64);
|
|
33
|
+
|
|
34
|
+
const personEntity = createEntity({
|
|
35
|
+
table: "read_bidx_persons",
|
|
36
|
+
fields: {
|
|
37
|
+
email: createTextField({ required: true, pii: true, lookupable: true }),
|
|
38
|
+
firstName: createTextField(),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const personFeature = defineFeature("bidxtest", (r) => {
|
|
42
|
+
r.entity("person", personEntity);
|
|
43
|
+
});
|
|
44
|
+
const personTable = buildEntityTable("person", personEntity);
|
|
45
|
+
|
|
46
|
+
let testDb: TestDb;
|
|
47
|
+
let tdb: TenantDb;
|
|
48
|
+
let kms: InMemoryKmsAdapter;
|
|
49
|
+
const adminUser = TestUsers.admin;
|
|
50
|
+
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
testDb = await createTestDb();
|
|
53
|
+
await unsafeCreateEntityTable(testDb.db, personEntity, "person");
|
|
54
|
+
await createEventsTable(testDb.db);
|
|
55
|
+
await createProjectionStateTable(testDb.db);
|
|
56
|
+
tdb = createTenantDb(testDb.db, adminUser.tenantId);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
await testDb.cleanup();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
await asRawClient(testDb.db).unsafe(
|
|
65
|
+
`TRUNCATE kumiko_events, read_bidx_persons, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
66
|
+
);
|
|
67
|
+
kms = new InMemoryKmsAdapter();
|
|
68
|
+
configurePiiSubjectKms(kms);
|
|
69
|
+
configureBlindIndexKey(TEST_KEY_B64);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
resetPiiSubjectKmsForTests();
|
|
74
|
+
resetBlindIndexKeyForTests();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const crud = createEventStoreExecutor(personTable, personEntity, { entityName: "person" });
|
|
78
|
+
|
|
79
|
+
async function rawRow(id: string): Promise<Record<string, unknown>> {
|
|
80
|
+
const rows = await asRawClient(testDb.db).unsafe<Record<string, unknown>>(
|
|
81
|
+
`SELECT * FROM read_bidx_persons WHERE id = $1`,
|
|
82
|
+
[id],
|
|
83
|
+
);
|
|
84
|
+
const row = rows[0];
|
|
85
|
+
if (!row) throw new Error(`no row for ${id}`);
|
|
86
|
+
return row;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("blind-index write path", () => {
|
|
90
|
+
test("create: bidx in the row, ciphertext in the email column, NOTHING in the event", async () => {
|
|
91
|
+
const created = await crud.create({ email: "marc@example.com" }, adminUser, tdb);
|
|
92
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
93
|
+
|
|
94
|
+
const row = await rawRow(String(created.data.id));
|
|
95
|
+
expect(row["email_bidx"]).toBe(computeBlindIndex(TEST_KEY, "marc@example.com"));
|
|
96
|
+
expect(isPiiCiphertext(row["email"])).toBe(true);
|
|
97
|
+
|
|
98
|
+
// Executor-Response: plaintext email, kein bidx-Leak.
|
|
99
|
+
expect(created.data.data["email"]).toBe("marc@example.com");
|
|
100
|
+
expect("emailBidx" in created.data.data).toBe(false);
|
|
101
|
+
|
|
102
|
+
const events = await asRawClient(testDb.db).unsafe<{ payload: unknown }>(
|
|
103
|
+
`SELECT payload FROM kumiko_events WHERE aggregate_id = $1`,
|
|
104
|
+
[String(created.data.id)],
|
|
105
|
+
);
|
|
106
|
+
const payloadText = JSON.stringify(events.map((e) => e.payload));
|
|
107
|
+
expect(payloadText).not.toContain("bidx");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("update recomputes bidx for the changed field", async () => {
|
|
111
|
+
const created = await crud.create({ email: "old@example.com" }, adminUser, tdb);
|
|
112
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
113
|
+
await crud.update(
|
|
114
|
+
{ id: created.data.id, version: 1, changes: { email: "new@example.com" } },
|
|
115
|
+
adminUser,
|
|
116
|
+
tdb,
|
|
117
|
+
);
|
|
118
|
+
const row = await rawRow(String(created.data.id));
|
|
119
|
+
expect(row["email_bidx"]).toBe(computeBlindIndex(TEST_KEY, "new@example.com"));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("blind-index lookup (OR-rewrite)", () => {
|
|
124
|
+
test("fetchOne by plaintext email finds the encrypted row", async () => {
|
|
125
|
+
const created = await crud.create({ email: "marc@example.com" }, adminUser, tdb);
|
|
126
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
127
|
+
|
|
128
|
+
const hit = await fetchOne<Record<string, unknown>>(tdb, personTable, {
|
|
129
|
+
email: "marc@example.com",
|
|
130
|
+
});
|
|
131
|
+
expect(hit).toBeDefined();
|
|
132
|
+
expect(hit?.["id"]).toBe(created.data.id);
|
|
133
|
+
// Read-Strip: bidx nie am Caller.
|
|
134
|
+
expect(hit && "emailBidx" in hit).toBe(false);
|
|
135
|
+
expect(hit && "email_bidx" in hit).toBe(false);
|
|
136
|
+
|
|
137
|
+
expect(await fetchOne(tdb, personTable, { email: "other@example.com" })).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("plaintext legacy row (written pre-rollout) still matches the same query", async () => {
|
|
141
|
+
// Rollout-Zustand VOR KMS+Key: Klartext-Row, bidx NULL.
|
|
142
|
+
resetPiiSubjectKmsForTests();
|
|
143
|
+
resetBlindIndexKeyForTests();
|
|
144
|
+
const legacy = await crud.create({ email: "legacy@example.com" }, adminUser, tdb);
|
|
145
|
+
if (!legacy.isSuccess) throw new Error("create failed");
|
|
146
|
+
const legacyRow = await rawRow(String(legacy.data.id));
|
|
147
|
+
expect(legacyRow["email"]).toBe("legacy@example.com");
|
|
148
|
+
expect(legacyRow["email_bidx"]).toBeNull();
|
|
149
|
+
|
|
150
|
+
// Rollout an: dieselbe Query matcht über den Plaintext-Arm.
|
|
151
|
+
configurePiiSubjectKms(kms);
|
|
152
|
+
configureBlindIndexKey(TEST_KEY_B64);
|
|
153
|
+
const hit = await fetchOne<Record<string, unknown>>(tdb, personTable, {
|
|
154
|
+
email: "legacy@example.com",
|
|
155
|
+
});
|
|
156
|
+
expect(hit?.["id"]).toBe(legacy.data.id);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("list filter eq matches the encrypted row and strips bidx", async () => {
|
|
160
|
+
const created = await crud.create(
|
|
161
|
+
{ email: "marc@example.com", firstName: "Marc" },
|
|
162
|
+
adminUser,
|
|
163
|
+
tdb,
|
|
164
|
+
);
|
|
165
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
166
|
+
|
|
167
|
+
const page = await crud.list(
|
|
168
|
+
{ filter: { field: "email", op: "eq", value: "marc@example.com" } },
|
|
169
|
+
adminUser,
|
|
170
|
+
tdb,
|
|
171
|
+
);
|
|
172
|
+
expect(page.rows).toHaveLength(1);
|
|
173
|
+
expect(page.rows[0]?.["email"]).toBe("marc@example.com");
|
|
174
|
+
expect(page.rows[0] && "emailBidx" in page.rows[0]).toBe(false);
|
|
175
|
+
|
|
176
|
+
const miss = await crud.list(
|
|
177
|
+
{ filter: { field: "email", op: "eq", value: "nobody@example.com" } },
|
|
178
|
+
adminUser,
|
|
179
|
+
tdb,
|
|
180
|
+
);
|
|
181
|
+
expect(miss.rows).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("blind-index rebuild + forget", () => {
|
|
186
|
+
const implicitName = "bidxtest:projection:person-entity";
|
|
187
|
+
|
|
188
|
+
test("rebuild recomputes bidx from ciphertext (identical to live)", async () => {
|
|
189
|
+
const created = await crud.create({ email: "marc@example.com" }, adminUser, tdb);
|
|
190
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
191
|
+
const liveBidx = (await rawRow(String(created.data.id)))["email_bidx"];
|
|
192
|
+
|
|
193
|
+
const registry = createRegistry([personFeature]);
|
|
194
|
+
expect(registry.getAllProjections().has(implicitName)).toBe(true);
|
|
195
|
+
await rebuildProjection(implicitName, { db: testDb.db, registry });
|
|
196
|
+
|
|
197
|
+
const rebuilt = await rawRow(String(created.data.id));
|
|
198
|
+
expect(rebuilt["email_bidx"]).toBe(liveBidx);
|
|
199
|
+
expect(rebuilt["email_bidx"]).toBe(computeBlindIndex(TEST_KEY, "marc@example.com"));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("erased subject → rebuild sets bidx NULL, lookup stops matching", async () => {
|
|
203
|
+
const created = await crud.create({ email: "marc@example.com" }, adminUser, tdb);
|
|
204
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
205
|
+
|
|
206
|
+
await kms.eraseKey({ kind: "user", userId: String(created.data.id) });
|
|
207
|
+
|
|
208
|
+
const registry = createRegistry([personFeature]);
|
|
209
|
+
await rebuildProjection(implicitName, { db: testDb.db, registry });
|
|
210
|
+
|
|
211
|
+
const rebuilt = await rawRow(String(created.data.id));
|
|
212
|
+
expect(rebuilt["email_bidx"]).toBeNull();
|
|
213
|
+
expect(await fetchOne(tdb, personTable, { email: "marc@example.com" })).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("erased subject → column drift excludes bidx (#916, not counted)", async () => {
|
|
217
|
+
// Pre-swap, live still holds the OLD bidx hash while the shadow already
|
|
218
|
+
// computed NULL for the erased subject — a real live-vs-shadow divergence,
|
|
219
|
+
// but the exact legitimate class countColumnDrift is designed to ignore.
|
|
220
|
+
const created = await crud.create({ email: "marc@example.com" }, adminUser, tdb);
|
|
221
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
222
|
+
|
|
223
|
+
await kms.eraseKey({ kind: "user", userId: String(created.data.id) });
|
|
224
|
+
|
|
225
|
+
const registry = createRegistry([personFeature]);
|
|
226
|
+
const result = await rebuildProjection(implicitName, { db: testDb.db, registry });
|
|
227
|
+
|
|
228
|
+
expect(result.columnDriftCount).toBe(0);
|
|
229
|
+
expect((await rawRow(String(created.data.id)))["email_bidx"]).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("nullBlindIndexesForSubject nulls bidx immediately (no rebuild needed)", async () => {
|
|
233
|
+
const created = await crud.create({ email: "marc@example.com" }, adminUser, tdb);
|
|
234
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
235
|
+
expect((await rawRow(String(created.data.id)))["email_bidx"]).not.toBeNull();
|
|
236
|
+
|
|
237
|
+
await kms.eraseKey({ kind: "user", userId: String(created.data.id) });
|
|
238
|
+
const registry = createRegistry([personFeature]);
|
|
239
|
+
await nullBlindIndexesForSubject(
|
|
240
|
+
testDb.db,
|
|
241
|
+
registry.features,
|
|
242
|
+
subjectIdToKey({ kind: "user", userId: String(created.data.id) }),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect((await rawRow(String(created.data.id)))["email_bidx"]).toBeNull();
|
|
246
|
+
expect(await fetchOne(tdb, personTable, { email: "marc@example.com" })).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildFilterWhere } from "../event-store-executor-context";
|
|
3
|
+
|
|
4
|
+
describe("buildFilterWhere", () => {
|
|
5
|
+
test("eq: returns a direct field-equality WhereObject", () => {
|
|
6
|
+
expect(buildFilterWhere("status", "eq", "active")).toEqual({ status: "active" });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("ne: wraps the value in a { ne } clause", () => {
|
|
10
|
+
expect(buildFilterWhere("status", "ne", "active")).toEqual({ status: { ne: "active" } });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("lt: wraps the value in a { lt } clause", () => {
|
|
14
|
+
expect(buildFilterWhere("createdAt", "lt", 100)).toEqual({ createdAt: { lt: 100 } });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("gt: wraps the value in a { gt } clause", () => {
|
|
18
|
+
expect(buildFilterWhere("createdAt", "gt", 100)).toEqual({ createdAt: { gt: 100 } });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("in: non-empty array → direct array WhereObject", () => {
|
|
22
|
+
expect(buildFilterWhere("status", "in", ["active", "pending"])).toEqual({
|
|
23
|
+
status: ["active", "pending"],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("in: empty array → null (no-match short-circuit)", () => {
|
|
28
|
+
expect(buildFilterWhere("status", "in", [])).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("in: non-array value → null (no-match short-circuit)", () => {
|
|
32
|
+
expect(buildFilterWhere("status", "in", "not-an-array")).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -20,7 +20,7 @@ const counterTable = table("read_unit_counters", {
|
|
|
20
20
|
}) as unknown as SchemaTable;
|
|
21
21
|
|
|
22
22
|
describe("collectTableMetas (#255)", () => {
|
|
23
|
-
test("collects entity, projection, MSP and
|
|
23
|
+
test("collects entity, projection, MSP and storeTable tables — same sources as the test-stack auto-push", () => {
|
|
24
24
|
const feature = defineFeature("test", (r) => {
|
|
25
25
|
r.entity("unit", exampleEntity());
|
|
26
26
|
r.projection({
|
|
@@ -39,16 +39,16 @@ describe("collectTableMetas (#255)", () => {
|
|
|
39
39
|
});
|
|
40
40
|
// side-effect-only MSP — no table, must be skipped without throwing
|
|
41
41
|
r.multiStreamProjection({ name: "unit-notify", apply: { "unit.created": async () => {} } });
|
|
42
|
-
r.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
id: uuid
|
|
46
|
-
})
|
|
42
|
+
r.storeTable(
|
|
43
|
+
defineUnmanagedTable({
|
|
44
|
+
tableName: "unit_cache",
|
|
45
|
+
columns: [{ name: "id", pgType: "uuid", notNull: true, primaryKey: true }],
|
|
46
|
+
}),
|
|
47
47
|
{ reason: "test fixture" },
|
|
48
48
|
);
|
|
49
|
-
r.
|
|
49
|
+
r.storeTable(
|
|
50
50
|
defineUnmanagedTable({
|
|
51
|
-
tableName: "
|
|
51
|
+
tableName: "store_unit_log",
|
|
52
52
|
columns: [{ name: "id", pgType: "serial", notNull: true, primaryKey: true }],
|
|
53
53
|
}),
|
|
54
54
|
{ reason: "test fixture" },
|
|
@@ -59,8 +59,8 @@ describe("collectTableMetas (#255)", () => {
|
|
|
59
59
|
expect(names).toContain("read_units"); // entity
|
|
60
60
|
expect(names).toContain("read_unit_counters"); // r.projection
|
|
61
61
|
expect(names).toContain("read_unit_audit"); // r.multiStreamProjection
|
|
62
|
-
expect(names).toContain("unit_cache"); // r.
|
|
63
|
-
expect(names).toContain("
|
|
62
|
+
expect(names).toContain("unit_cache"); // r.storeTable
|
|
63
|
+
expect(names).toContain("store_unit_log"); // r.storeTable (second registration)
|
|
64
64
|
expect(names).toHaveLength(5);
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
SYSTEM_TENANT_ID,
|
|
11
11
|
} from "../../engine";
|
|
12
12
|
import type { ConfigSeedDef, Registry } from "../../engine/types";
|
|
13
|
+
import { createEnvMasterKeyProvider } from "../../secrets/env-master-key-provider";
|
|
14
|
+
import { createEnvelopeCipher } from "../../secrets/envelope-cipher";
|
|
13
15
|
import { unsafeCreateEntityTable } from "../../stack";
|
|
14
16
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
15
17
|
import { seedConfigValues } from "../config-seed";
|
|
16
|
-
import { createEncryptionProvider } from "../encryption";
|
|
17
18
|
import { buildEntityTable } from "../table-builder";
|
|
18
19
|
|
|
19
20
|
// --- Test Entity ---
|
|
@@ -51,8 +52,15 @@ const mockRegistry = {
|
|
|
51
52
|
} as unknown as Registry;
|
|
52
53
|
|
|
53
54
|
// --- Helpers ---
|
|
54
|
-
const encryption =
|
|
55
|
-
|
|
55
|
+
const encryption = createEnvelopeCipher(
|
|
56
|
+
createEnvMasterKeyProvider({
|
|
57
|
+
env: {
|
|
58
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "1",
|
|
59
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: Buffer.from("0123456789abcdef0123456789abcdef").toString(
|
|
60
|
+
"base64",
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
56
64
|
);
|
|
57
65
|
|
|
58
66
|
let testDb: BunTestDb;
|
|
@@ -214,7 +222,7 @@ describe("seedConfigValues", () => {
|
|
|
214
222
|
|
|
215
223
|
await expect(
|
|
216
224
|
seedConfigValues(seeds, configTable, configEntity, mockRegistry, testDb.db),
|
|
217
|
-
).rejects.toThrow(/encrypted but no
|
|
225
|
+
).rejects.toThrow(/encrypted but no EnvelopeCipher/);
|
|
218
226
|
});
|
|
219
227
|
|
|
220
228
|
test("encrypted seed with provider stores ciphertext, not plaintext", async () => {
|
|
@@ -240,7 +248,7 @@ describe("seedConfigValues", () => {
|
|
|
240
248
|
// resolver later runs `decrypt → JSON.parse` to get the primitive
|
|
241
249
|
// back; we replay the same round-trip here.
|
|
242
250
|
expect(row!.value).not.toContain("sk_live_secret_token");
|
|
243
|
-
expect(JSON.parse(encryption.decrypt(row!.value))).toBe("sk_live_secret_token");
|
|
251
|
+
expect(JSON.parse(await encryption.decrypt(row!.value))).toBe("sk_live_secret_token");
|
|
244
252
|
});
|
|
245
253
|
|
|
246
254
|
test("race-safe parallel boot — two concurrent calls result in 1 created + 1 skipped", async () => {
|
|
@@ -11,11 +11,8 @@
|
|
|
11
11
|
// die invalid-Probe (echte Garbage muss weiterhin throwen — kein silent
|
|
12
12
|
// swallowing).
|
|
13
13
|
|
|
14
|
-
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
15
|
-
|
|
16
|
-
await ensureTemporalPolyfill();
|
|
17
|
-
|
|
18
14
|
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import { Temporal } from "temporal-polyfill";
|
|
19
16
|
import { instantToDriver as toDriver } from "../dialect";
|
|
20
17
|
|
|
21
18
|
describe("instant() customType — toDriver", () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { createEntity, createTextField } from "../../engine";
|
|
3
|
-
import {
|
|
3
|
+
import { createTestEnvelopeCipher } from "../../testing";
|
|
4
4
|
import {
|
|
5
5
|
collectEncryptedFieldNames,
|
|
6
6
|
decryptEntityFieldValues,
|
|
@@ -18,25 +18,25 @@ describe("entity-field-encryption", () => {
|
|
|
18
18
|
},
|
|
19
19
|
});
|
|
20
20
|
const encryptedFields = collectEncryptedFieldNames(entity);
|
|
21
|
-
const encryption =
|
|
21
|
+
const encryption = createTestEnvelopeCipher(TEST_KEY);
|
|
22
22
|
|
|
23
23
|
test("collectEncryptedFieldNames finds encrypted text fields only", () => {
|
|
24
24
|
expect([...encryptedFields]).toEqual(["secretNote"]);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
test("encrypt on write / decrypt on read round-trip", () => {
|
|
27
|
+
test("encrypt on write / decrypt on read round-trip", async () => {
|
|
28
28
|
const plain = { email: "a@b.de", secretNote: "top secret" };
|
|
29
|
-
const stored = encryptEntityFieldValues(plain, encryptedFields, encryption);
|
|
29
|
+
const stored = await encryptEntityFieldValues(plain, encryptedFields, encryption);
|
|
30
30
|
expect(stored["email"]).toBe(plain.email);
|
|
31
31
|
expect(stored["secretNote"]).not.toBe("top secret");
|
|
32
32
|
|
|
33
|
-
const read = decryptEntityFieldValues(stored, encryptedFields, encryption);
|
|
33
|
+
const read = await decryptEntityFieldValues(stored, encryptedFields, encryption);
|
|
34
34
|
expect(read).toEqual(plain);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
test("onlyKeys limits encryption to changed fields", () => {
|
|
37
|
+
test("onlyKeys limits encryption to changed fields", async () => {
|
|
38
38
|
const row = { email: "a@b.de", secretNote: "note" };
|
|
39
|
-
const stored = encryptEntityFieldValues(row, encryptedFields, encryption, {
|
|
39
|
+
const stored = await encryptEntityFieldValues(row, encryptedFields, encryption, {
|
|
40
40
|
onlyKeys: ["secretNote"],
|
|
41
41
|
});
|
|
42
42
|
expect(stored["email"]).toBe("a@b.de");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// #1210: buildEntityTableMeta() hardcoded source: "managed", so unmanaged
|
|
2
|
+
// direct-write stores (store_user_sessions, store_api_tokens, mail sync/seen
|
|
3
|
+
// cursors) were misclassified as rebuildable event-sourced projections — a
|
|
4
|
+
// destructive column change would DROP+rebuild-from-events tables that have
|
|
5
|
+
// no events, wiping live data. The options.source escape hatch must produce
|
|
6
|
+
// a byte-identical column/piiSubjectFields shape (only source differs), so
|
|
7
|
+
// the migration diff for an existing table stays empty when flipping it.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { createEntity, createTextField } from "../../engine";
|
|
11
|
+
import { buildEntityTableMeta } from "../entity-table-meta";
|
|
12
|
+
import { diffSnapshots, snapshotFromMetas } from "../migrate-generator";
|
|
13
|
+
|
|
14
|
+
const entity = createEntity({
|
|
15
|
+
table: "source-probe",
|
|
16
|
+
fields: {
|
|
17
|
+
userId: createTextField({ required: true }),
|
|
18
|
+
ip: createTextField({ userOwned: { ownerField: "userId" } }),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("buildEntityTableMeta — options.source (#1210)", () => {
|
|
23
|
+
test("defaults to managed when omitted", () => {
|
|
24
|
+
expect(buildEntityTableMeta("source-probe", entity).source).toBe("managed");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("options.source: 'unmanaged' changes only source, columns + piiSubjectFields stay identical", () => {
|
|
28
|
+
const managed = buildEntityTableMeta("source-probe", entity);
|
|
29
|
+
const unmanaged = buildEntityTableMeta("source-probe", entity, { source: "unmanaged" });
|
|
30
|
+
|
|
31
|
+
expect(managed.source).toBe("managed");
|
|
32
|
+
expect(unmanaged.source).toBe("unmanaged");
|
|
33
|
+
expect(unmanaged.columns).toEqual(managed.columns);
|
|
34
|
+
expect(unmanaged.indexes).toEqual(managed.indexes);
|
|
35
|
+
expect(unmanaged.piiSubjectFields).toEqual(managed.piiSubjectFields);
|
|
36
|
+
expect(unmanaged.piiSubjectFields).toEqual(["ip"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("flipping an existing table's meta to unmanaged produces an empty migration diff", () => {
|
|
40
|
+
const prevSnapshot = snapshotFromMetas([buildEntityTableMeta("source-probe", entity)]);
|
|
41
|
+
const nextSnapshot = snapshotFromMetas([
|
|
42
|
+
buildEntityTableMeta("source-probe", entity, { source: "unmanaged" }),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const diff = diffSnapshots(prevSnapshot, nextSnapshot);
|
|
46
|
+
expect(diff.newTables).toEqual([]);
|
|
47
|
+
expect(diff.droppedTables).toEqual([]);
|
|
48
|
+
expect(diff.changedTables).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
decryptPiiFieldValues,
|
|
4
|
+
InMemoryKmsAdapter,
|
|
5
|
+
isPiiCiphertext,
|
|
6
|
+
type KmsContext,
|
|
7
|
+
} from "../../crypto";
|
|
8
|
+
import { createEntity, createTextField } from "../../engine";
|
|
9
|
+
import { createTestUser, testUserId } from "../../stack/test-users";
|
|
10
|
+
import { createTestEnvelopeCipher } from "../../testing";
|
|
11
|
+
import { buildExecutorContext, type Table } from "../event-store-executor-context";
|
|
12
|
+
|
|
13
|
+
const TEST_KEY = Buffer.from("a]bJm#kP9xQ2@wN!vL$hR5yT8eU0iO3f").toString("base64");
|
|
14
|
+
|
|
15
|
+
describe("event-store-executor-context — encryptForStorage/decryptForRead layering", () => {
|
|
16
|
+
const entity = createEntity({
|
|
17
|
+
table: "pii_roundtrip_test",
|
|
18
|
+
fields: {
|
|
19
|
+
userId: createTextField({ required: true }),
|
|
20
|
+
// Both markers at once — the auth-mfa.totpSecret/recoveryCodes shape
|
|
21
|
+
// that first surfaced the ordering bug (pii-subject-encryption
|
|
22
|
+
// integration test).
|
|
23
|
+
secretNote: createTextField({ encrypted: true, userOwned: { ownerField: "userId" } }),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const encryption = createTestEnvelopeCipher(TEST_KEY);
|
|
27
|
+
const kms = new InMemoryKmsAdapter();
|
|
28
|
+
const kmsCtx: KmsContext = { requestId: "test" };
|
|
29
|
+
const context = buildExecutorContext({} as Table, entity, {
|
|
30
|
+
entityName: "piiRoundtripTest",
|
|
31
|
+
encryption,
|
|
32
|
+
kms,
|
|
33
|
+
});
|
|
34
|
+
const user = createTestUser({ id: testUserId(1) });
|
|
35
|
+
|
|
36
|
+
test("round-trips through both layers", async () => {
|
|
37
|
+
const row = { userId: user.id, secretNote: "the actual secret" };
|
|
38
|
+
const stored = await context.encryptForStorage(row, user);
|
|
39
|
+
expect(stored["secretNote"]).not.toBe("the actual secret");
|
|
40
|
+
|
|
41
|
+
const read = await context.decryptForRead(stored);
|
|
42
|
+
expect(read["secretNote"]).toBe("the actual secret");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("stores PII(envelope(plaintext)) — PII is the outer layer, not envelope", async () => {
|
|
46
|
+
const row = { userId: user.id, secretNote: "the actual secret" };
|
|
47
|
+
const stored = await context.encryptForStorage(row, user);
|
|
48
|
+
const storedNote = stored["secretNote"];
|
|
49
|
+
|
|
50
|
+
// Outer layer is PII ciphertext.
|
|
51
|
+
expect(isPiiCiphertext(storedNote)).toBe(true);
|
|
52
|
+
|
|
53
|
+
// Peeling only the outer (PII) layer must leave an envelope-ciphertext
|
|
54
|
+
// string underneath, not the plaintext directly — proves encryptForStorage
|
|
55
|
+
// wrapped the envelope-ciphertext with PII, not the raw plaintext.
|
|
56
|
+
const piiPeeled = await decryptPiiFieldValues(stored, ["secretNote"], kms, kmsCtx);
|
|
57
|
+
const innerValue = piiPeeled["secretNote"];
|
|
58
|
+
expect(innerValue).not.toBe("the actual secret");
|
|
59
|
+
expect(isPiiCiphertext(innerValue)).toBe(false);
|
|
60
|
+
await expect(encryption.decrypt(innerValue as string)).resolves.toBe("the actual secret");
|
|
61
|
+
|
|
62
|
+
// decryptForRead must peel PII first: feeding the stored value straight
|
|
63
|
+
// to the envelope cipher (skipping the PII unwrap) has to fail — this is
|
|
64
|
+
// the ordering bug the roundtrip guards against.
|
|
65
|
+
await expect(encryption.decrypt(storedNote as string)).rejects.toThrow();
|
|
66
|
+
});
|
|
67
|
+
});
|