@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,53 @@
|
|
|
1
|
+
// String-in/string-out envelope encryption for TEXT-column stores (config
|
|
2
|
+
// values, entity fields). The stored string is JSON of StoredEnvelope; the
|
|
3
|
+
// kekVersion inside makes every value rotatable via the MasterKeyProvider
|
|
4
|
+
// keyring.
|
|
5
|
+
|
|
6
|
+
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-types/envelope-cipher-types";
|
|
7
|
+
import { InternalError } from "../errors/classes";
|
|
8
|
+
import type { DekCache } from "./dek-cache";
|
|
9
|
+
import { createDekCache, withDekCache } from "./dek-cache";
|
|
10
|
+
import { decryptValue, encryptValue } from "./envelope";
|
|
11
|
+
import { decodeStoredEnvelope, encodeStoredEnvelope, isStoredEnvelope } from "./stored-envelope";
|
|
12
|
+
import type { MasterKeyProvider } from "./types";
|
|
13
|
+
|
|
14
|
+
export type { EnvelopeCipher } from "@cosmicdrift/kumiko-types/envelope-cipher-types";
|
|
15
|
+
|
|
16
|
+
export type EnvelopeCipherOptions = {
|
|
17
|
+
// Shared DEK cache — pass the app-wide instance so config/entity reads
|
|
18
|
+
// amortise KEK unwraps together with ctx.secrets.
|
|
19
|
+
readonly dekCache?: DekCache;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function createEnvelopeCipher(
|
|
23
|
+
provider: MasterKeyProvider,
|
|
24
|
+
opts: EnvelopeCipherOptions = {},
|
|
25
|
+
): EnvelopeCipher {
|
|
26
|
+
const cached = withDekCache(provider, opts.dekCache ?? createDekCache());
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
async encrypt(plaintext, scope) {
|
|
30
|
+
const envelope = await encryptValue(plaintext, provider, scope);
|
|
31
|
+
return JSON.stringify(encodeStoredEnvelope(envelope));
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async decrypt(stored, scope) {
|
|
35
|
+
let parsed: unknown;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(stored);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new InternalError({
|
|
40
|
+
message: "[envelope-cipher] stored value is not valid JSON",
|
|
41
|
+
i18nKey: "secrets.errors.envelope_malformed",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!isStoredEnvelope(parsed)) {
|
|
45
|
+
throw new InternalError({
|
|
46
|
+
message: "[envelope-cipher] stored JSON is not a StoredEnvelope",
|
|
47
|
+
i18nKey: "secrets.errors.envelope_malformed",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return decryptValue(decodeStoredEnvelope(parsed), cached, scope);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/secrets/envelope.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// carries everything needed to decrypt later.
|
|
4
4
|
|
|
5
5
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
6
|
-
import type { Envelope, MasterKeyProvider } from "./types";
|
|
6
|
+
import type { Envelope, KeyScope, MasterKeyProvider } from "./types";
|
|
7
7
|
|
|
8
8
|
const ALGORITHM = "aes-256-gcm";
|
|
9
9
|
const DEK_LENGTH = 32; // AES-256
|
|
@@ -14,6 +14,7 @@ const IV_LENGTH = 12; // GCM standard nonce length
|
|
|
14
14
|
export async function encryptValue(
|
|
15
15
|
plaintext: string,
|
|
16
16
|
provider: MasterKeyProvider,
|
|
17
|
+
scope?: KeyScope,
|
|
17
18
|
): Promise<Envelope> {
|
|
18
19
|
// Fresh DEK per value. Reusing DEKs across rows would break forward
|
|
19
20
|
// secrecy (one compromised ciphertext-IV pair leaks information about
|
|
@@ -26,7 +27,7 @@ export async function encryptValue(
|
|
|
26
27
|
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
27
28
|
const authTag = cipher.getAuthTag();
|
|
28
29
|
|
|
29
|
-
const { encryptedDek, kekVersion } = await provider.wrapDek(dek);
|
|
30
|
+
const { encryptedDek, kekVersion } = await provider.wrapDek(dek, scope);
|
|
30
31
|
return { ciphertext, iv, authTag, encryptedDek, kekVersion };
|
|
31
32
|
} finally {
|
|
32
33
|
// Zero the DEK on best effort regardless of success — if provider.wrapDek
|
|
@@ -42,8 +43,9 @@ export async function encryptValue(
|
|
|
42
43
|
export async function decryptValue(
|
|
43
44
|
envelope: Envelope,
|
|
44
45
|
provider: MasterKeyProvider,
|
|
46
|
+
scope?: KeyScope,
|
|
45
47
|
): Promise<string> {
|
|
46
|
-
const dek = await provider.unwrapDek(envelope.encryptedDek, envelope.kekVersion);
|
|
48
|
+
const dek = await provider.unwrapDek(envelope.encryptedDek, envelope.kekVersion, scope);
|
|
47
49
|
try {
|
|
48
50
|
const decipher = createDecipheriv(ALGORITHM, dek, envelope.iv);
|
|
49
51
|
decipher.setAuthTag(envelope.authTag);
|
package/src/secrets/index.ts
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
export { createDekCache, type DekCache, type DekCacheOptions } from "./dek-cache";
|
|
1
|
+
export { createDekCache, type DekCache, type DekCacheOptions, withDekCache } from "./dek-cache";
|
|
2
2
|
export {
|
|
3
3
|
createEnvMasterKeyProvider,
|
|
4
4
|
type EnvMasterKeyProviderOptions,
|
|
5
5
|
type Keyring,
|
|
6
6
|
} from "./env-master-key-provider";
|
|
7
7
|
export { decryptValue, encryptValue } from "./envelope";
|
|
8
|
+
export {
|
|
9
|
+
createEnvelopeCipher,
|
|
10
|
+
type EnvelopeCipher,
|
|
11
|
+
type EnvelopeCipherOptions,
|
|
12
|
+
} from "./envelope-cipher";
|
|
8
13
|
export { assertNoSecretLeak } from "./leak-guard";
|
|
9
14
|
export { rewrapDek } from "./rotation";
|
|
15
|
+
export {
|
|
16
|
+
decodeStoredEnvelope,
|
|
17
|
+
encodeStoredEnvelope,
|
|
18
|
+
isStoredEnvelope,
|
|
19
|
+
type StoredEnvelope,
|
|
20
|
+
} from "./stored-envelope";
|
|
10
21
|
export {
|
|
11
22
|
type ContainsSecret,
|
|
12
23
|
createSecret,
|
|
13
24
|
type Envelope,
|
|
14
25
|
isSecret,
|
|
26
|
+
type KeyScope,
|
|
15
27
|
type MasterKeyProvider,
|
|
16
28
|
type Secret,
|
|
17
29
|
type SecretAuditContext,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Canonical at-rest form of an Envelope: every Buffer as base64, ready for
|
|
2
|
+
// jsonb columns (secrets feature) or JSON-stringified TEXT columns
|
|
3
|
+
// (EnvelopeCipher for config values / entity fields). One wire shape for
|
|
4
|
+
// every envelope-encrypted store in the framework.
|
|
5
|
+
|
|
6
|
+
import type { Envelope } from "./types";
|
|
7
|
+
|
|
8
|
+
export type StoredEnvelope = {
|
|
9
|
+
readonly ciphertext: string; // base64
|
|
10
|
+
readonly iv: string; // base64
|
|
11
|
+
readonly authTag: string; // base64
|
|
12
|
+
readonly encryptedDek: string; // base64
|
|
13
|
+
readonly kekVersion: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function encodeStoredEnvelope(envelope: Envelope): StoredEnvelope {
|
|
17
|
+
return {
|
|
18
|
+
ciphertext: envelope.ciphertext.toString("base64"),
|
|
19
|
+
iv: envelope.iv.toString("base64"),
|
|
20
|
+
authTag: envelope.authTag.toString("base64"),
|
|
21
|
+
encryptedDek: envelope.encryptedDek.toString("base64"),
|
|
22
|
+
kekVersion: envelope.kekVersion,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function decodeStoredEnvelope(stored: StoredEnvelope): Envelope {
|
|
27
|
+
return {
|
|
28
|
+
ciphertext: Buffer.from(stored.ciphertext, "base64"),
|
|
29
|
+
iv: Buffer.from(stored.iv, "base64"),
|
|
30
|
+
authTag: Buffer.from(stored.authTag, "base64"),
|
|
31
|
+
encryptedDek: Buffer.from(stored.encryptedDek, "base64"),
|
|
32
|
+
kekVersion: stored.kekVersion,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isStoredEnvelope(value: unknown): value is StoredEnvelope {
|
|
37
|
+
if (!value || typeof value !== "object") return false;
|
|
38
|
+
const v = value as Record<string, unknown>; // @cast-boundary parse-boundary after typeof check
|
|
39
|
+
return (
|
|
40
|
+
typeof v["ciphertext"] === "string" &&
|
|
41
|
+
typeof v["iv"] === "string" &&
|
|
42
|
+
typeof v["authTag"] === "string" &&
|
|
43
|
+
typeof v["encryptedDek"] === "string" &&
|
|
44
|
+
typeof v["kekVersion"] === "number"
|
|
45
|
+
);
|
|
46
|
+
}
|
package/src/secrets/types.ts
CHANGED
|
@@ -1,162 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// encryptedDek, never touch the ciphertext.
|
|
4
|
-
|
|
5
|
-
import type { TenantId } from "../engine";
|
|
6
|
-
|
|
7
|
-
// Plaintext-secret wrapper (branded). Carries the actual string internally
|
|
8
|
-
// but the nominal typing stops it from landing in an HTTP response by
|
|
9
|
-
// accident — a response-serializer guard + the reveal() cost make the leak
|
|
10
|
-
// intentional. Framework code that sees `Secret<string>` knows the caller
|
|
11
|
-
// has already gone through the audited ctx.secrets.get path.
|
|
12
|
-
//
|
|
13
|
-
// The brand is a real (non-registered) Symbol so it exists at runtime for
|
|
14
|
-
// isSecret() without clashing with user-land symbols of the same name.
|
|
15
|
-
const SecretBrand: unique symbol = Symbol("kumiko.secret");
|
|
16
|
-
|
|
17
|
-
export type Secret<T = string> = {
|
|
18
|
-
readonly [SecretBrand]: true;
|
|
19
|
-
readonly reveal: () => T;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Implementation helper — bundled-features uses this to wrap a plaintext after
|
|
23
|
-
// decryption. Kept in the framework so both sides share one canonical brand.
|
|
24
|
-
export function createSecret<T>(value: T): Secret<T> {
|
|
25
|
-
return {
|
|
26
|
-
[SecretBrand]: true as const,
|
|
27
|
-
reveal: () => value,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// True for any object carrying the Secret brand. Used by the response guard
|
|
32
|
-
// to reject leaks before serialization.
|
|
33
|
-
export function isSecret(v: unknown): v is Secret<unknown> {
|
|
34
|
-
return typeof v === "object" && v !== null && SecretBrand in v;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// --- Compile-time response guard (R6) --------------------------------------
|
|
38
|
-
//
|
|
39
|
-
// ContainsSecret<T> is `true` only when a Secret<> is DEFINITELY present
|
|
40
|
-
// somewhere in T. The handler-registration guard (defineWriteHandler/
|
|
41
|
-
// defineQueryHandler) turns a `true` into a compile error — the static twin of
|
|
42
|
-
// assertNoSecretLeak's runtime walk.
|
|
43
|
-
//
|
|
44
|
-
// Biased to `false`: anything it cannot inspect — a bare generic type param (a
|
|
45
|
-
// handler generic over its response), `unknown`/`any`, `never` — resolves to
|
|
46
|
-
// `false` = allowed, with the runtime guard as the backstop. The alternative
|
|
47
|
-
// (default-to-leak) false-flags every legitimate generic-over-response handler.
|
|
48
|
-
//
|
|
49
|
-
// Branch order is load-bearing: never/unknown/any first (uninspectable), then
|
|
50
|
-
// Secret, then primitives (covers branded primitives like TenantId without
|
|
51
|
-
// enumerating them), then the SafeLeaf allowlist (opaque class instances that
|
|
52
|
-
// blind `{ [K in keyof T] }` recursion would mangle — the type-level mirror of
|
|
53
|
-
// leak-guard.ts skipping non-plain objects), then arrays, then a "does any
|
|
54
|
-
// field contain a secret" fold over plain objects.
|
|
55
|
-
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
|
|
56
|
-
|
|
57
|
-
// Opaque built-in leaves a response legitimately carries; never recurse into
|
|
58
|
-
// them. Extend when the bundled-features tsc sweep surfaces a real leaf type.
|
|
59
|
-
type SafeLeaf =
|
|
60
|
-
| Date
|
|
61
|
-
| RegExp
|
|
62
|
-
| Temporal.Instant
|
|
63
|
-
| Temporal.ZonedDateTime
|
|
64
|
-
| Temporal.PlainDate
|
|
65
|
-
| Temporal.PlainDateTime
|
|
66
|
-
| Temporal.PlainTime
|
|
67
|
-
| Temporal.PlainYearMonth
|
|
68
|
-
| Temporal.PlainMonthDay
|
|
69
|
-
| Temporal.Duration;
|
|
70
|
-
|
|
71
|
-
export type ContainsSecret<T> = [T] extends [never]
|
|
72
|
-
? false
|
|
73
|
-
: unknown extends T
|
|
74
|
-
? false
|
|
75
|
-
: T extends Secret<unknown>
|
|
76
|
-
? true
|
|
77
|
-
: T extends Primitive
|
|
78
|
-
? false
|
|
79
|
-
: T extends SafeLeaf
|
|
80
|
-
? false
|
|
81
|
-
: T extends readonly (infer U)[]
|
|
82
|
-
? ContainsSecret<U>
|
|
83
|
-
: T extends object
|
|
84
|
-
? true extends { [K in keyof T]-?: ContainsSecret<T[K]> }[keyof T]
|
|
85
|
-
? true
|
|
86
|
-
: false
|
|
87
|
-
: false;
|
|
88
|
-
|
|
89
|
-
// Per-read audit context. Populated by requireSecretsContext() wrapper so
|
|
90
|
-
// handlers don't need to pass userId/handlerName manually on every call.
|
|
91
|
-
// Undefined for framework-internal reads (rotation job, tests) — the audit
|
|
92
|
-
// table stays a "who touched this credential" log, not a crash-report sink.
|
|
93
|
-
export type SecretAuditContext = {
|
|
94
|
-
readonly userId: string;
|
|
95
|
-
readonly handlerName: string;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// Feature code can pass either the raw qualified-name string or a typed
|
|
99
|
-
// handle returned by r.secret. The handle form is safer — renaming the
|
|
100
|
-
// r.secret call updates all references through the import graph.
|
|
101
|
-
export type SecretKeyRef = string | { readonly name: string };
|
|
102
|
-
|
|
103
|
-
// The ctx.secrets contract. Concrete implementation lives in bundled-features
|
|
104
|
-
// (createSecretsContext) where the DB and MasterKeyProvider are known. This
|
|
105
|
-
// lean interface is what the framework's HandlerContext carries so engine
|
|
106
|
-
// code can talk about it without pulling in bundled-features.
|
|
107
|
-
export interface SecretsContext {
|
|
108
|
-
get(
|
|
109
|
-
tenantId: TenantId,
|
|
110
|
-
key: SecretKeyRef,
|
|
111
|
-
auditCtx?: SecretAuditContext,
|
|
112
|
-
): Promise<Secret<string> | undefined>;
|
|
113
|
-
// Metadata-only existence probe: no decryption, no read-audit event.
|
|
114
|
-
// For readiness checks — use get() when the value itself is needed.
|
|
115
|
-
has(tenantId: TenantId, key: SecretKeyRef): Promise<boolean>;
|
|
116
|
-
set(
|
|
117
|
-
tenantId: TenantId,
|
|
118
|
-
key: SecretKeyRef,
|
|
119
|
-
value: string,
|
|
120
|
-
opts?: { redact?: (plaintext: string) => string; hint?: string; updatedBy?: string },
|
|
121
|
-
): Promise<void>;
|
|
122
|
-
delete(tenantId: TenantId, key: SecretKeyRef, opts?: { deletedBy?: string }): Promise<boolean>;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export type Envelope = {
|
|
126
|
-
// AES-256-GCM ciphertext of the plaintext, keyed with a DEK.
|
|
127
|
-
readonly ciphertext: Buffer;
|
|
128
|
-
// GCM nonce (12 bytes). Generated fresh per encryption.
|
|
129
|
-
readonly iv: Buffer;
|
|
130
|
-
// GCM auth tag (16 bytes). Guarantees the ciphertext wasn't tampered.
|
|
131
|
-
readonly authTag: Buffer;
|
|
132
|
-
// DEK wrapped with the current KEK. Decryption needs provider.unwrapDek
|
|
133
|
-
// with the kekVersion to recover the DEK.
|
|
134
|
-
readonly encryptedDek: Buffer;
|
|
135
|
-
// Which KEK version was used to wrap the DEK. On rotation, rows with old
|
|
136
|
-
// versions still decrypt — the provider keeps a keyring of historical KEKs.
|
|
137
|
-
readonly kekVersion: number;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// The contract a KEK backend must fulfil. The framework sees only this
|
|
141
|
-
// interface; concrete implementations live in separate packages
|
|
142
|
-
// (@cosmicdrift/kumiko-secrets-vault, @cosmicdrift/kumiko-secrets-aws-kms, ...). The default is
|
|
143
|
-
// EnvMasterKeyProvider which reads keys from environment variables.
|
|
144
|
-
export interface MasterKeyProvider {
|
|
145
|
-
// Wrap a fresh DEK with the current KEK. Returns the wrapped bytes + the
|
|
146
|
-
// KEK version used — the version ends up in the Envelope so decryption
|
|
147
|
-
// later knows which KEK to ask for.
|
|
148
|
-
wrapDek(dek: Buffer): Promise<{ encryptedDek: Buffer; kekVersion: number }>;
|
|
149
|
-
|
|
150
|
-
// Unwrap a previously-wrapped DEK. During rotation the provider must
|
|
151
|
-
// accept older kekVersion values (2-version window minimum), otherwise
|
|
152
|
-
// old rows become unreadable.
|
|
153
|
-
unwrapDek(encryptedDek: Buffer, kekVersion: number): Promise<Buffer>;
|
|
154
|
-
|
|
155
|
-
// Which KEK version new wraps use. Rotation flips this to a new value
|
|
156
|
-
// and older-version reads continue to work until rows are re-wrapped.
|
|
157
|
-
currentVersion(): number;
|
|
158
|
-
|
|
159
|
-
// Health check: can the provider talk to its backend? Used by
|
|
160
|
-
// /health/ready. Cheap probe, no KEK material read.
|
|
161
|
-
isAvailable(): Promise<boolean>;
|
|
162
|
-
}
|
|
1
|
+
// Legacy path — re-exported for callers still importing this module directly.
|
|
2
|
+
export * from "@cosmicdrift/kumiko-types/secrets-types";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { SseEvent } from "../../api/sse-broker";
|
|
3
|
+
import type { SaveContext } from "../../engine/types";
|
|
4
|
+
import { createEventCollector } from "../event-collector";
|
|
5
|
+
|
|
6
|
+
const probeSseEvent: SseEvent = { type: "probe", data: {} };
|
|
7
|
+
const probeSaveContext: SaveContext = {
|
|
8
|
+
kind: "save",
|
|
9
|
+
id: "e1",
|
|
10
|
+
data: {},
|
|
11
|
+
changes: {},
|
|
12
|
+
previous: {},
|
|
13
|
+
isNew: true,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("createEventCollector", () => {
|
|
17
|
+
test("starts empty", () => {
|
|
18
|
+
const collector = createEventCollector();
|
|
19
|
+
expect(collector.sse).toEqual([]);
|
|
20
|
+
expect(collector.postSave).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("reset() clears both arrays in place, keeping the same references", () => {
|
|
24
|
+
const collector = createEventCollector();
|
|
25
|
+
const sseRef = collector.sse;
|
|
26
|
+
const postSaveRef = collector.postSave;
|
|
27
|
+
collector.sse.push(probeSseEvent);
|
|
28
|
+
collector.postSave.push(probeSaveContext);
|
|
29
|
+
|
|
30
|
+
expect(collector.sse).toHaveLength(1);
|
|
31
|
+
expect(collector.postSave).toHaveLength(1);
|
|
32
|
+
|
|
33
|
+
collector.reset();
|
|
34
|
+
|
|
35
|
+
expect(collector.sse).toHaveLength(0);
|
|
36
|
+
expect(collector.postSave).toHaveLength(0);
|
|
37
|
+
// Same array instances — callers that captured a reference before reset
|
|
38
|
+
// still see the cleared state, not a stale snapshot.
|
|
39
|
+
expect(collector.sse).toBe(sseRef);
|
|
40
|
+
expect(collector.postSave).toBe(postSaveRef);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Drives the manual `ctx.jobRunner.dispatch(...)` path a write handler
|
|
2
|
+
// uses directly — distinct from the auto-trigger path covered by
|
|
3
|
+
// entrypoint-job-wiring.integration.test.ts, which reads a different
|
|
4
|
+
// context slot and isn't guaranteed by this test passing.
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { defineFeature } from "../../engine";
|
|
9
|
+
import { InternalError, writeFailure } from "../../errors";
|
|
10
|
+
import { waitFor } from "../../testing";
|
|
11
|
+
import { setupTestStack, type TestStack } from "../test-stack";
|
|
12
|
+
import { TestUsers } from "../test-users";
|
|
13
|
+
|
|
14
|
+
const jobRuns: Array<{ name: string; payload: Record<string, unknown> }> = [];
|
|
15
|
+
|
|
16
|
+
const manualDispatchFeature = defineFeature("manualdispatch", (r) => {
|
|
17
|
+
r.writeHandler(
|
|
18
|
+
"create",
|
|
19
|
+
z.object({ note: z.string() }),
|
|
20
|
+
async (event, ctx) => {
|
|
21
|
+
// Exact repro from #983: bracket-access, dynamic context extension —
|
|
22
|
+
// not a typed HandlerContext field (mirrors trigger.write.ts).
|
|
23
|
+
const jobRunner = ctx["jobRunner"] as
|
|
24
|
+
| { dispatch: (name: string, payload: Record<string, unknown>) => Promise<string> }
|
|
25
|
+
| undefined;
|
|
26
|
+
if (!jobRunner) {
|
|
27
|
+
return writeFailure(new InternalError({ message: "no jobRunner on ctx" }));
|
|
28
|
+
}
|
|
29
|
+
await jobRunner.dispatch("manualdispatch:job:record", { note: event.payload.note });
|
|
30
|
+
return { isSuccess: true as const, data: { id: 1, note: event.payload.note } };
|
|
31
|
+
},
|
|
32
|
+
{ access: { openToAll: true } },
|
|
33
|
+
);
|
|
34
|
+
r.job("record", { trigger: { manual: true }, runIn: "worker" }, async (payload) => {
|
|
35
|
+
jobRuns.push({ name: "manualdispatch:job:record", payload });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// kumiko-framework#1232: setupTestStack used to hand jobs a bare
|
|
40
|
+
// `{ db, registry }` context literal — nothing else buildServer's request
|
|
41
|
+
// path gets (tracer/meter, searchAdapter, ...). A job could pass in tests
|
|
42
|
+
// while reading a field only prod's context has, then break silently on
|
|
43
|
+
// deploy. This job records which fields actually arrived.
|
|
44
|
+
const jobContextFields: Array<{
|
|
45
|
+
hasTracer: boolean;
|
|
46
|
+
hasSearchAdapter: boolean;
|
|
47
|
+
hasEffectiveFeatures: boolean;
|
|
48
|
+
}> = [];
|
|
49
|
+
|
|
50
|
+
const jobContextFeature = defineFeature("jobcontextcheck", (r) => {
|
|
51
|
+
r.job("record", { trigger: { manual: true }, runIn: "worker" }, async (_payload, context) => {
|
|
52
|
+
jobContextFields.push({
|
|
53
|
+
hasTracer: context.tracer !== undefined,
|
|
54
|
+
hasSearchAdapter: context.searchAdapter !== undefined,
|
|
55
|
+
hasEffectiveFeatures: context.effectiveFeatures !== undefined,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let stack: TestStack | undefined;
|
|
61
|
+
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
if (stack) await stack.cleanup();
|
|
64
|
+
stack = undefined;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("setupTestStack({ jobs }) wires ctx.jobRunner for manual dispatch", () => {
|
|
68
|
+
test("write handler's ctx.jobRunner.dispatch(...) works end-to-end", async () => {
|
|
69
|
+
jobRuns.length = 0;
|
|
70
|
+
stack = await setupTestStack({
|
|
71
|
+
features: [manualDispatchFeature],
|
|
72
|
+
// consumerLane: this test has no separate consumer (unlike runDevApp's
|
|
73
|
+
// startDevJobRunners), so it must be the one running the job.
|
|
74
|
+
jobs: { consumerLane: "worker" },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await stack.http.writeOk("manualdispatch:write:create", { note: "hi" }, TestUsers.admin);
|
|
78
|
+
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
const run = jobRuns.find((e) => e.name === "manualdispatch:job:record");
|
|
81
|
+
expect(run).toBeDefined();
|
|
82
|
+
expect(run?.payload["note"]).toBe("hi");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("without `jobs` opt-in, ctx.jobRunner stays undefined (no-op, not a throw)", async () => {
|
|
87
|
+
stack = await setupTestStack({ features: [manualDispatchFeature] });
|
|
88
|
+
expect(stack.jobRunner).toBeUndefined();
|
|
89
|
+
|
|
90
|
+
// The handler itself guards against a missing jobRunner (writeFailure,
|
|
91
|
+
// not a thrown TypeError) — same contract as trigger.write.ts's cast.
|
|
92
|
+
const err = await stack.http.writeErr(
|
|
93
|
+
"manualdispatch:write:create",
|
|
94
|
+
{ note: "hi" },
|
|
95
|
+
TestUsers.admin,
|
|
96
|
+
);
|
|
97
|
+
expect(err.code).toBe("internal_error");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("setupTestStack({ jobs }) job context matches the request-path context", () => {
|
|
102
|
+
test("job handler's context carries tracer + searchAdapter, mirrors prod's effectiveFeatures-less job context", async () => {
|
|
103
|
+
jobContextFields.length = 0;
|
|
104
|
+
stack = await setupTestStack({
|
|
105
|
+
features: [jobContextFeature],
|
|
106
|
+
jobs: { consumerLane: "worker" },
|
|
107
|
+
effectiveFeatures: () => new Set(["jobcontextcheck"]),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await stack.jobRunner?.dispatch("jobcontextcheck:job:record");
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(jobContextFields.length).toBeGreaterThan(0);
|
|
114
|
+
});
|
|
115
|
+
// #1255: effectiveFeatures deliberately stays OUT of the job/appContext
|
|
116
|
+
// (prod only carries it in dispatcherOptions.effectiveFeatures, consumed
|
|
117
|
+
// by the command-dispatcher) — putting it here would let a job read
|
|
118
|
+
// `ctx.effectiveFeatures` green in tests and break on deploy.
|
|
119
|
+
expect(jobContextFields[0]).toEqual({
|
|
120
|
+
hasTracer: true,
|
|
121
|
+
hasSearchAdapter: true,
|
|
122
|
+
hasEffectiveFeatures: false,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
package/src/stack/db.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Provider-agnostic via createConnection (DB_PROVIDER env).
|
|
3
3
|
// postgres-js = default. DB_PROVIDER=bun = Bun.SQL (experimentell).
|
|
4
4
|
|
|
5
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-types/db-connection";
|
|
5
6
|
import { createConnection } from "../db/api";
|
|
6
7
|
import { createDatabase, databaseExists, dropDatabaseIfExists } from "../db/queries/test-stack";
|
|
7
8
|
import { ensureTemporalPolyfill } from "../time/polyfill";
|
|
@@ -18,7 +19,7 @@ function requireEnv(name: string): string {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export type TestDb = {
|
|
21
|
-
db:
|
|
22
|
+
db: DbConnection;
|
|
22
23
|
client: unknown;
|
|
23
24
|
dbName: string;
|
|
24
25
|
cleanup: () => Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KUMIKO_NAME_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
1
2
|
import { tableExists } from "../db/schema-inspection";
|
|
2
3
|
import type { Registry } from "../engine/types";
|
|
3
4
|
import { unsafePushTables } from "./table-helpers";
|
|
@@ -9,7 +10,7 @@ const logInfo = (msg: string): void => console.log(msg);
|
|
|
9
10
|
/**
|
|
10
11
|
* Push all implicit-projection tables — one per `r.entity()` — that the
|
|
11
12
|
* registry knows about. setupTestStack already handles explicit
|
|
12
|
-
* projections, MSPs, and `r.
|
|
13
|
+
* projections, MSPs, and `r.storeTable()` declarations in its own loop;
|
|
13
14
|
* implicit projections are the missing piece for a fresh boot.
|
|
14
15
|
*
|
|
15
16
|
* Idempotent via `tableExists` so a persistent dev DB
|
|
@@ -20,7 +21,7 @@ const logInfo = (msg: string): void => console.log(msg);
|
|
|
20
21
|
* Lives next to `setupTestStack` because both are stack-bootstrap
|
|
21
22
|
* helpers that legitimately speak the `unsafe*`-DDL layer; the
|
|
22
23
|
* Table-DDL Guard's stack/** allowlist is the single shared exemption
|
|
23
|
-
* site. Apps still declare data via `r.entity()` / `r.
|
|
24
|
+
* site. Apps still declare data via `r.entity()` / `r.storeTable()` and
|
|
24
25
|
* never call this directly.
|
|
25
26
|
*/
|
|
26
27
|
export async function pushEntityProjectionTables(
|
|
@@ -35,7 +36,7 @@ export async function pushEntityProjectionTables(
|
|
|
35
36
|
if (seen.has(proj.table)) continue;
|
|
36
37
|
seen.add(proj.table);
|
|
37
38
|
const tableRec = proj.table as unknown as Record<symbol, unknown>;
|
|
38
|
-
const physical = tableRec[
|
|
39
|
+
const physical = tableRec[KUMIKO_NAME_SYMBOL] as string;
|
|
39
40
|
if (await tableExists(stack.db, `public.${physical}`)) {
|
|
40
41
|
logInfo(`[kumiko-stack] table ${physical} already exists — skipping create`);
|
|
41
42
|
continue;
|
package/src/stack/redis.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { requireEnv } from "./db";
|
|
|
3
3
|
|
|
4
4
|
export type TestRedis = {
|
|
5
5
|
redis: import("ioredis").default;
|
|
6
|
+
// The exact REDIS_URL used to build `redis` above — for a second, unrelated
|
|
7
|
+
// connection (e.g. the test-stack's JobRunner) that needs its own client
|
|
8
|
+
// rather than sharing this one's keyPrefix. Reconstructing a URL from
|
|
9
|
+
// `redis.options` loses password/username/tls/path (pr-review
|
|
10
|
+
// kumiko-framework #1036/2) — callers needing a fresh connection should use
|
|
11
|
+
// this raw string, not rebuild one from parsed options.
|
|
12
|
+
redisUrl: string;
|
|
6
13
|
/** Delete every key this test created (prefix-scoped). Replaces the old
|
|
7
14
|
* `redis.flushdb()` — that wiped other parallel tests' BullMQ state. */
|
|
8
15
|
flushNamespace: () => Promise<void>;
|
|
@@ -35,6 +42,7 @@ export async function createTestRedis(): Promise<TestRedis> {
|
|
|
35
42
|
|
|
36
43
|
return {
|
|
37
44
|
redis,
|
|
45
|
+
redisUrl,
|
|
38
46
|
flushNamespace,
|
|
39
47
|
cleanup: async () => {
|
|
40
48
|
await flushNamespace();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Hono } from "hono";
|
|
2
|
+
import type { SessionCreator } from "../api/auth-routes";
|
|
2
3
|
import type { JwtHelper } from "../api/jwt";
|
|
3
4
|
import type { SessionUser } from "../engine/types";
|
|
4
5
|
|
|
@@ -98,9 +99,44 @@ export type RequestHelper = {
|
|
|
98
99
|
) => Promise<Response>;
|
|
99
100
|
};
|
|
100
101
|
|
|
101
|
-
export
|
|
102
|
+
export type RequestHelperOptions = {
|
|
103
|
+
// When sessionChecker is wired (sessions feature), JWTs without jti are
|
|
104
|
+
// rejected as no_sid. Seed/test helpers that only jwt.sign(user) need a
|
|
105
|
+
// live sid — create one via the same sessionCreator login uses (#1372).
|
|
106
|
+
readonly sessionCreator?: SessionCreator;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export function createRequestHelper(
|
|
110
|
+
app: Hono,
|
|
111
|
+
jwt: JwtHelper,
|
|
112
|
+
options: RequestHelperOptions = {},
|
|
113
|
+
): RequestHelper {
|
|
114
|
+
// sid per (user.id, tenantId), not one mint per authHeader() call:
|
|
115
|
+
// sessionCreator opens a live session row, so an unmemoized call mints a
|
|
116
|
+
// fresh one on every request a test makes for the same user — tests
|
|
117
|
+
// asserting on session counts / massRevoker / revokeAllOthers behavior
|
|
118
|
+
// then see extra live sids that have nothing to do with what they're
|
|
119
|
+
// testing. Keyed on tenantId too — the same user.id can hold sessions in
|
|
120
|
+
// more than one tenant, and sessionCreator writes the row scoped to
|
|
121
|
+
// `user.tenantId` (session-callbacks.ts), so a user.id-only key would
|
|
122
|
+
// hand a cross-tenant test its wrong tenant's sid.
|
|
123
|
+
// Cache the Promise (not the settled sid) so two concurrent authHeader
|
|
124
|
+
// calls for the same key share one mint instead of racing on a miss.
|
|
125
|
+
const sidByUserKey = new Map<string, Promise<string>>();
|
|
126
|
+
|
|
102
127
|
async function authHeader(user: SessionUser): Promise<Record<string, string>> {
|
|
103
|
-
|
|
128
|
+
let forJwt = user;
|
|
129
|
+
if (options.sessionCreator && !user.sid) {
|
|
130
|
+
const key = `${user.id}:${user.tenantId}`;
|
|
131
|
+
let sidPromise = sidByUserKey.get(key);
|
|
132
|
+
if (sidPromise === undefined) {
|
|
133
|
+
sidPromise = options.sessionCreator(user, { ip: "test", userAgent: "request-helper" });
|
|
134
|
+
sidByUserKey.set(key, sidPromise);
|
|
135
|
+
}
|
|
136
|
+
const sid = await sidPromise;
|
|
137
|
+
forJwt = { ...user, sid };
|
|
138
|
+
}
|
|
139
|
+
const token = await jwt.sign(forJwt);
|
|
104
140
|
return { Authorization: `Bearer ${token}` };
|
|
105
141
|
}
|
|
106
142
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KUMIKO_META_SYMBOL,
|
|
3
|
+
KUMIKO_NAME_SYMBOL,
|
|
4
|
+
} from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
1
5
|
import type { DbConnection } from "../db/connection";
|
|
2
6
|
import { pgTypeToSqlType } from "../db/dialect";
|
|
3
7
|
import type { ColumnMeta, EntityTableMeta } from "../db/entity-table-meta";
|
|
@@ -5,15 +9,13 @@ import {
|
|
|
5
9
|
alterTableAddColumn,
|
|
6
10
|
createIndexIfNotExists,
|
|
7
11
|
executeDdlStatement,
|
|
8
|
-
|
|
9
|
-
} from "../db/queries/test-stack";
|
|
12
|
+
} from "../db/queries/ddl";
|
|
13
|
+
import { truncateTablesRestartIdentity } from "../db/queries/test-stack";
|
|
10
14
|
import { renderTableDdl } from "../db/render-ddl";
|
|
11
15
|
import { tableExists } from "../db/schema-inspection";
|
|
12
16
|
import { buildEntityTable, toTableName } from "../db/table-builder";
|
|
13
17
|
import type { EventDispatcher } from "../pipeline";
|
|
14
18
|
|
|
15
|
-
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
16
|
-
const KUMIKO_META_SYMBOL = Symbol.for("kumiko:schema:Meta");
|
|
17
19
|
function tableNameOf(table: unknown): string {
|
|
18
20
|
if (typeof table !== "object" || table === null) {
|
|
19
21
|
throw new Error("table-helpers: table is not a SchemaTable object");
|