@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,103 @@
|
|
|
1
|
+
// Response-Tripwire (#820): ein kumiko-pii:-Ciphertext in einer JSON-Response
|
|
2
|
+
// ist immer ein Bug (raw-Read am Decrypt vorbei). Dev/Test → 500 (Test wird
|
|
3
|
+
// rot), Prod → redact + Error-Log, ohne KMS → kein Scan (pass-through).
|
|
4
|
+
|
|
5
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
configurePiiSubjectKms,
|
|
9
|
+
InMemoryKmsAdapter,
|
|
10
|
+
resetPiiSubjectKmsForTests,
|
|
11
|
+
} from "../../crypto";
|
|
12
|
+
import { defineFeature } from "../../engine/define-feature";
|
|
13
|
+
import { defineQueryHandler } from "../../engine/define-handler";
|
|
14
|
+
import { setupTestStack, type TestStack, TestUsers } from "../../stack";
|
|
15
|
+
|
|
16
|
+
const CIPHERTEXT = "kumiko-pii:v1:user:6b2f4a0e-1c9d-4f3a-9d2e-00000000000a:8e2Rkjj+ww==";
|
|
17
|
+
|
|
18
|
+
const leakyFeature = defineFeature("leaky", (r) => {
|
|
19
|
+
r.queryHandler(
|
|
20
|
+
defineQueryHandler({
|
|
21
|
+
name: "raw",
|
|
22
|
+
schema: z.object({}),
|
|
23
|
+
access: { openToAll: true },
|
|
24
|
+
handler: async () => ({ email: CIPHERTEXT, note: "plain" }),
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
r.queryHandler(
|
|
28
|
+
defineQueryHandler({
|
|
29
|
+
name: "clean",
|
|
30
|
+
schema: z.object({}),
|
|
31
|
+
access: { openToAll: true },
|
|
32
|
+
handler: async () => ({ email: "marc@example.com" }),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let stack: TestStack;
|
|
38
|
+
const originalNodeEnv = process.env["NODE_ENV"];
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
stack = await setupTestStack({ features: [leakyFeature] });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(async () => {
|
|
45
|
+
await stack.cleanup();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
resetPiiSubjectKmsForTests();
|
|
50
|
+
if (originalNodeEnv === undefined) delete process.env["NODE_ENV"];
|
|
51
|
+
else process.env["NODE_ENV"] = originalNodeEnv;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
async function callLeakyQuery(): Promise<Response> {
|
|
55
|
+
const token = await stack.jwt.sign(TestUsers.admin);
|
|
56
|
+
return stack.http.raw(
|
|
57
|
+
"POST",
|
|
58
|
+
"/api/query",
|
|
59
|
+
{ type: "leaky:query:raw", payload: {} },
|
|
60
|
+
{ Authorization: `Bearer ${token}` },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("piiCiphertextResponseGuard", () => {
|
|
65
|
+
test("no KMS configured → response passes through unscanned", async () => {
|
|
66
|
+
const res = await callLeakyQuery();
|
|
67
|
+
expect(res.status).toBe(200);
|
|
68
|
+
expect(await res.text()).toContain(CIPHERTEXT);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("KMS active, dev: leaking response becomes a loud 500", async () => {
|
|
72
|
+
configurePiiSubjectKms(new InMemoryKmsAdapter());
|
|
73
|
+
const res = await callLeakyQuery();
|
|
74
|
+
expect(res.status).toBe(500);
|
|
75
|
+
const body = (await res.json()) as { error?: { code?: string; message?: string } };
|
|
76
|
+
expect(body.error?.code).toBe("pii_ciphertext_leak");
|
|
77
|
+
expect(body.error?.message).toContain("/api/query");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("KMS active, production: leak is redacted, request succeeds", async () => {
|
|
81
|
+
configurePiiSubjectKms(new InMemoryKmsAdapter());
|
|
82
|
+
process.env["NODE_ENV"] = "production";
|
|
83
|
+
const res = await callLeakyQuery();
|
|
84
|
+
expect(res.status).toBe(200);
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
expect(text).not.toContain("kumiko-pii:");
|
|
87
|
+
expect(text).toContain("[pii-redacted]");
|
|
88
|
+
expect(text).toContain("plain");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("KMS active, clean response stays untouched", async () => {
|
|
92
|
+
configurePiiSubjectKms(new InMemoryKmsAdapter());
|
|
93
|
+
const token = await stack.jwt.sign(TestUsers.admin);
|
|
94
|
+
const res = await stack.http.raw(
|
|
95
|
+
"POST",
|
|
96
|
+
"/api/query",
|
|
97
|
+
{ type: "leaky:query:clean", payload: {} },
|
|
98
|
+
{ Authorization: `Bearer ${token}` },
|
|
99
|
+
);
|
|
100
|
+
expect(res.status).toBe(200);
|
|
101
|
+
expect(await res.text()).toContain("marc@example.com");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createTestRedis, type TestRedis } from "../../stack";
|
|
3
|
+
import { createRedisLoginRateLimiter } from "../auth-routes";
|
|
4
|
+
|
|
5
|
+
let testRedis: TestRedis;
|
|
6
|
+
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
testRedis = await createTestRedis();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterAll(async () => {
|
|
12
|
+
await testRedis.cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
await testRedis.flushNamespace();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("createRedisLoginRateLimiter", () => {
|
|
20
|
+
test("allows exactly maxAttempts checks, then blocks", async () => {
|
|
21
|
+
const limiter = createRedisLoginRateLimiter(testRedis.redis, 3, 60_000);
|
|
22
|
+
|
|
23
|
+
expect(await limiter.check("1.2.3.4|user@test.local")).toBe(true);
|
|
24
|
+
expect(await limiter.check("1.2.3.4|user@test.local")).toBe(true);
|
|
25
|
+
expect(await limiter.check("1.2.3.4|user@test.local")).toBe(true);
|
|
26
|
+
expect(await limiter.check("1.2.3.4|user@test.local")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("reset clears the counter for that key", async () => {
|
|
30
|
+
const limiter = createRedisLoginRateLimiter(testRedis.redis, 1, 60_000);
|
|
31
|
+
|
|
32
|
+
expect(await limiter.check("k")).toBe(true);
|
|
33
|
+
expect(await limiter.check("k")).toBe(false);
|
|
34
|
+
|
|
35
|
+
await limiter.reset("k");
|
|
36
|
+
expect(await limiter.check("k")).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("buckets are independent per key", async () => {
|
|
40
|
+
const limiter = createRedisLoginRateLimiter(testRedis.redis, 1, 60_000);
|
|
41
|
+
|
|
42
|
+
expect(await limiter.check("a")).toBe(true);
|
|
43
|
+
expect(await limiter.check("b")).toBe(true);
|
|
44
|
+
expect(await limiter.check("a")).toBe(false);
|
|
45
|
+
expect(await limiter.check("b")).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("namespace keeps two limiter instances from sharing a keyspace", async () => {
|
|
49
|
+
const login = createRedisLoginRateLimiter(testRedis.redis, 1, 60_000, "login");
|
|
50
|
+
const mfa = createRedisLoginRateLimiter(testRedis.redis, 1, 60_000, "mfa-verify");
|
|
51
|
+
|
|
52
|
+
expect(await login.check("1.2.3.4")).toBe(true);
|
|
53
|
+
expect(await mfa.check("1.2.3.4")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("counter resets once windowMs elapses", async () => {
|
|
57
|
+
const limiter = createRedisLoginRateLimiter(testRedis.redis, 1, 150);
|
|
58
|
+
|
|
59
|
+
expect(await limiter.check("k")).toBe(true);
|
|
60
|
+
expect(await limiter.check("k")).toBe(false);
|
|
61
|
+
|
|
62
|
+
await Bun.sleep(200);
|
|
63
|
+
|
|
64
|
+
expect(await limiter.check("k")).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Regression for the INCR-then-PEXPIRE race: two separate round-trips left
|
|
68
|
+
// a window for a crash/network blip right after the key is created
|
|
69
|
+
// (count===1) to skip the PEXPIRE call entirely, leaving the key
|
|
70
|
+
// permanently without a TTL. The single-eval fix closes that window by
|
|
71
|
+
// construction (one round-trip, can't be interrupted mid-way). "TTL
|
|
72
|
+
// exists after check()" alone doesn't distinguish old from new — the old
|
|
73
|
+
// two-call code also leaves a TTL set on the non-crashing path — so this
|
|
74
|
+
// pins the actual mechanism instead: exactly one `eval` round-trip, and
|
|
75
|
+
// `incr`/`pexpire` are never called directly (the only path is atomic).
|
|
76
|
+
test("check() goes through a single atomic eval — never incr/pexpire directly", async () => {
|
|
77
|
+
const limiter = createRedisLoginRateLimiter(testRedis.redis, 3, 60_000);
|
|
78
|
+
const key = "1.2.3.4|ttl-race@test.local";
|
|
79
|
+
|
|
80
|
+
// Manual call-counting instrumentation, not a test-library mock: each
|
|
81
|
+
// wrapper delegates to the real bound method on the real Redis client
|
|
82
|
+
// (setupTestStack), so behavior is unchanged — this only counts which
|
|
83
|
+
// wire commands actually went out, without the bun:test spy helper the
|
|
84
|
+
// integration-mock-guard forbids in *.integration.test.ts files.
|
|
85
|
+
const origEval = testRedis.redis.eval.bind(testRedis.redis);
|
|
86
|
+
const origIncr = testRedis.redis.incr.bind(testRedis.redis);
|
|
87
|
+
const origPexpire = testRedis.redis.pexpire.bind(testRedis.redis);
|
|
88
|
+
let evalCalls = 0;
|
|
89
|
+
let incrCalls = 0;
|
|
90
|
+
let pexpireCalls = 0;
|
|
91
|
+
testRedis.redis.eval = ((...args: Parameters<typeof origEval>) => {
|
|
92
|
+
evalCalls++;
|
|
93
|
+
return origEval(...args);
|
|
94
|
+
}) as typeof origEval;
|
|
95
|
+
testRedis.redis.incr = ((...args: Parameters<typeof origIncr>) => {
|
|
96
|
+
incrCalls++;
|
|
97
|
+
return origIncr(...args);
|
|
98
|
+
}) as typeof origIncr;
|
|
99
|
+
testRedis.redis.pexpire = ((...args: Parameters<typeof origPexpire>) => {
|
|
100
|
+
pexpireCalls++;
|
|
101
|
+
return origPexpire(...args);
|
|
102
|
+
}) as typeof origPexpire;
|
|
103
|
+
try {
|
|
104
|
+
expect(await limiter.check(key)).toBe(true);
|
|
105
|
+
|
|
106
|
+
expect(evalCalls).toBe(1);
|
|
107
|
+
expect(incrCalls).toBe(0);
|
|
108
|
+
expect(pexpireCalls).toBe(0);
|
|
109
|
+
|
|
110
|
+
const ttlMs = await testRedis.redis.pttl(`kumiko:auth:ratelimit:login:${key}`);
|
|
111
|
+
expect(ttlMs).toBeGreaterThan(0);
|
|
112
|
+
} finally {
|
|
113
|
+
testRedis.redis.eval = origEval;
|
|
114
|
+
testRedis.redis.incr = origIncr;
|
|
115
|
+
testRedis.redis.pexpire = origPexpire;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// kumiko-framework#1522 idx=5: a key that already exists WITHOUT a TTL
|
|
120
|
+
// (exactly what the pre-fix INCR-then-PEXPIRE race could leave behind)
|
|
121
|
+
// must get one applied on the very next check(), not stay permanently
|
|
122
|
+
// uncapped. Simulates the orphan directly via INCR (no TTL) rather than
|
|
123
|
+
// trying to hit the original race window, which isn't reproducible from
|
|
124
|
+
// outside the eval script.
|
|
125
|
+
test("heals a pre-existing key that has no TTL (orphaned by the old two-call race)", async () => {
|
|
126
|
+
const limiter = createRedisLoginRateLimiter(testRedis.redis, 5, 60_000, "heal-test");
|
|
127
|
+
const key = "1.2.3.4|orphan@test.local";
|
|
128
|
+
const redisKey = `kumiko:auth:ratelimit:heal-test:${key}`;
|
|
129
|
+
|
|
130
|
+
await testRedis.redis.incr(redisKey);
|
|
131
|
+
expect(await testRedis.redis.pttl(redisKey)).toBe(-1);
|
|
132
|
+
|
|
133
|
+
await limiter.check(key);
|
|
134
|
+
|
|
135
|
+
const ttlMs = await testRedis.redis.pttl(redisKey);
|
|
136
|
+
expect(ttlMs).toBeGreaterThan(0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -70,3 +70,54 @@ describe("requestIdMiddleware — signal propagation", () => {
|
|
|
70
70
|
expect(captured?.aborted).toBe(true);
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
|
+
describe("requestIdMiddleware — ip + userAgent capture (603/2)", () => {
|
|
74
|
+
test("populates ip from x-forwarded-for and userAgent from the User-Agent header", async () => {
|
|
75
|
+
let captured: { ip: string | undefined; userAgent: string | undefined } = {
|
|
76
|
+
ip: undefined,
|
|
77
|
+
userAgent: undefined,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const app = new Hono();
|
|
81
|
+
app.use("/probe", requestIdMiddleware());
|
|
82
|
+
app.get("/probe", (c) => {
|
|
83
|
+
const ctx = requestContext.get();
|
|
84
|
+
captured = { ip: ctx?.ip, userAgent: ctx?.userAgent };
|
|
85
|
+
return c.text("ok");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const res = await app.request(
|
|
89
|
+
new Request("http://test.local/probe", {
|
|
90
|
+
method: "GET",
|
|
91
|
+
headers: {
|
|
92
|
+
"x-forwarded-for": "203.0.113.7, 10.0.0.1",
|
|
93
|
+
"user-agent": "Mozilla/5.0 (probe-test)",
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(res.status).toBe(200);
|
|
99
|
+
expect(captured.ip).toBe("203.0.113.7");
|
|
100
|
+
expect(captured.userAgent).toBe("Mozilla/5.0 (probe-test)");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("no User-Agent header → userAgent stays undefined, not an empty string", async () => {
|
|
104
|
+
let captured: string | undefined = "unset";
|
|
105
|
+
|
|
106
|
+
const app = new Hono();
|
|
107
|
+
app.use("/probe", requestIdMiddleware());
|
|
108
|
+
app.get("/probe", (c) => {
|
|
109
|
+
captured = requestContext.get()?.userAgent;
|
|
110
|
+
return c.text("ok");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// fetch/undici always add a default User-Agent unless explicitly cleared —
|
|
114
|
+
// Hono's app.request accepts a raw Request, so an empty-but-present header
|
|
115
|
+
// simulates a client that sends the header with no value.
|
|
116
|
+
const res = await app.request(
|
|
117
|
+
new Request("http://test.local/probe", { method: "GET", headers: {} }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(res.status).toBe(200);
|
|
121
|
+
expect(captured).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// buildServer boot-time guards + httpRoute verb wiring (PUT branch).
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import {
|
|
5
|
+
createEntity,
|
|
6
|
+
createFileField,
|
|
7
|
+
createRegistry,
|
|
8
|
+
createTextField,
|
|
9
|
+
defineFeature,
|
|
10
|
+
} from "../../engine";
|
|
11
|
+
import { buildServer } from "../server";
|
|
12
|
+
|
|
13
|
+
const JWT_SECRET = "server-boot-guards-test-secret-min-32-chars";
|
|
14
|
+
|
|
15
|
+
describe("buildServer — file-storage provider guard", () => {
|
|
16
|
+
const fileFieldFeature = defineFeature("needs-files", (r) => {
|
|
17
|
+
r.entity(
|
|
18
|
+
"doc",
|
|
19
|
+
createEntity({
|
|
20
|
+
table: "boot_guard_docs",
|
|
21
|
+
fields: { title: createTextField(), attachment: createFileField() },
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("throws when registry declares file fields but no provider is mounted", () => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
buildServer({
|
|
29
|
+
registry: createRegistry([fileFieldFeature]),
|
|
30
|
+
context: {},
|
|
31
|
+
jwtSecret: JWT_SECRET,
|
|
32
|
+
}),
|
|
33
|
+
).toThrow(/no file-storage provider is mounted/);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("buildServer — rateLimit resolver guard", () => {
|
|
38
|
+
test("throws when L1 global middleware requested without resolver", () => {
|
|
39
|
+
expect(() =>
|
|
40
|
+
buildServer({
|
|
41
|
+
registry: createRegistry([]),
|
|
42
|
+
context: {},
|
|
43
|
+
jwtSecret: JWT_SECRET,
|
|
44
|
+
rateLimit: { global: { limit: 100, windowSeconds: 60 } },
|
|
45
|
+
}),
|
|
46
|
+
).toThrow(/rateLimit middleware requested but no resolver available/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("buildServer — feature httpRoute PUT mounting", () => {
|
|
51
|
+
const putFeature = defineFeature("put-route", (r) => {
|
|
52
|
+
r.httpRoute({
|
|
53
|
+
method: "PUT",
|
|
54
|
+
path: "/resource/42",
|
|
55
|
+
anonymous: true,
|
|
56
|
+
handler: (c) => c.json({ method: "PUT", ok: true }),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { app } = buildServer({
|
|
61
|
+
registry: createRegistry([putFeature]),
|
|
62
|
+
context: {},
|
|
63
|
+
jwtSecret: JWT_SECRET,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("PUT /resource/42 reaches the declared handler", async () => {
|
|
67
|
+
const res = await app.request("/resource/42", { method: "PUT" });
|
|
68
|
+
expect(res.status).toBe(200);
|
|
69
|
+
expect(await res.json()).toEqual({ method: "PUT", ok: true });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createRegistry } from "../../engine";
|
|
3
|
+
import { buildServer } from "../server";
|
|
4
|
+
|
|
5
|
+
const JWT_SECRET = "x".repeat(40);
|
|
6
|
+
const registry = createRegistry([]);
|
|
7
|
+
|
|
8
|
+
describe("buildServer — jwtTtl default depends on sessionChecker wiring", () => {
|
|
9
|
+
test("no auth config → stateless default (1h)", () => {
|
|
10
|
+
const { jwt } = buildServer({ registry, context: {}, jwtSecret: JWT_SECRET });
|
|
11
|
+
expect(jwt.ttlSeconds).toBe(60 * 60);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("auth without sessionChecker → stateless default (1h)", () => {
|
|
15
|
+
const { jwt } = buildServer({
|
|
16
|
+
registry,
|
|
17
|
+
context: {},
|
|
18
|
+
jwtSecret: JWT_SECRET,
|
|
19
|
+
auth: { membershipQuery: "unused:query" },
|
|
20
|
+
});
|
|
21
|
+
expect(jwt.ttlSeconds).toBe(60 * 60);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("auth with sessionChecker → session-backed default (24h)", () => {
|
|
25
|
+
const { jwt } = buildServer({
|
|
26
|
+
registry,
|
|
27
|
+
context: {},
|
|
28
|
+
jwtSecret: JWT_SECRET,
|
|
29
|
+
auth: {
|
|
30
|
+
membershipQuery: "unused:query",
|
|
31
|
+
sessionChecker: async () => "live",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
expect(jwt.ttlSeconds).toBe(24 * 60 * 60);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("explicit jwtTtl wins regardless of sessionChecker wiring", () => {
|
|
38
|
+
const withChecker = buildServer({
|
|
39
|
+
registry,
|
|
40
|
+
context: {},
|
|
41
|
+
jwtSecret: JWT_SECRET,
|
|
42
|
+
jwtTtl: 42,
|
|
43
|
+
auth: {
|
|
44
|
+
membershipQuery: "unused:query",
|
|
45
|
+
sessionChecker: async () => "live",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
expect(withChecker.jwt.ttlSeconds).toBe(42);
|
|
49
|
+
|
|
50
|
+
const withoutChecker = buildServer({
|
|
51
|
+
registry,
|
|
52
|
+
context: {},
|
|
53
|
+
jwtSecret: JWT_SECRET,
|
|
54
|
+
jwtTtl: 42,
|
|
55
|
+
});
|
|
56
|
+
expect(withoutChecker.jwt.ttlSeconds).toBe(42);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -55,4 +55,61 @@ describe("SSE broker", () => {
|
|
|
55
55
|
expect(() => broker.removeClient("unknown", "fake-id")).not.toThrow();
|
|
56
56
|
expect(broker.getClientCount("unknown")).toBe(0);
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
test("subscribeAccessInvalidation fires only listeners on the same user's channel", () => {
|
|
60
|
+
const broker = createSseBroker();
|
|
61
|
+
const onInvalidateA = mock();
|
|
62
|
+
const onInvalidateB = mock();
|
|
63
|
+
|
|
64
|
+
broker.subscribeAccessInvalidation("user-a", onInvalidateA);
|
|
65
|
+
broker.subscribeAccessInvalidation("user-b", onInvalidateB);
|
|
66
|
+
|
|
67
|
+
broker.publishAccessInvalidation("user-a");
|
|
68
|
+
|
|
69
|
+
expect(onInvalidateA).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(onInvalidateB).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("subscribeAccessInvalidation supports multiple listeners on the same user", () => {
|
|
74
|
+
const broker = createSseBroker();
|
|
75
|
+
const first = mock();
|
|
76
|
+
const second = mock();
|
|
77
|
+
|
|
78
|
+
broker.subscribeAccessInvalidation("user-a", first);
|
|
79
|
+
broker.subscribeAccessInvalidation("user-a", second);
|
|
80
|
+
broker.publishAccessInvalidation("user-a");
|
|
81
|
+
|
|
82
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("unsubscribe (returned closure) stops further delivery", () => {
|
|
87
|
+
const broker = createSseBroker();
|
|
88
|
+
const onInvalidate = mock();
|
|
89
|
+
|
|
90
|
+
const unsubscribe = broker.subscribeAccessInvalidation("user-a", onInvalidate);
|
|
91
|
+
unsubscribe();
|
|
92
|
+
broker.publishAccessInvalidation("user-a");
|
|
93
|
+
|
|
94
|
+
expect(onInvalidate).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a fired listener can unsubscribe itself without skipping other listeners", () => {
|
|
98
|
+
const broker = createSseBroker();
|
|
99
|
+
let unsubscribeSelf: () => void = () => {};
|
|
100
|
+
const self = mock(() => unsubscribeSelf());
|
|
101
|
+
const other = mock();
|
|
102
|
+
|
|
103
|
+
unsubscribeSelf = broker.subscribeAccessInvalidation("user-a", self);
|
|
104
|
+
broker.subscribeAccessInvalidation("user-a", other);
|
|
105
|
+
broker.publishAccessInvalidation("user-a");
|
|
106
|
+
|
|
107
|
+
expect(self).toHaveBeenCalledTimes(1);
|
|
108
|
+
expect(other).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("publishAccessInvalidation to a user with no listeners does nothing", () => {
|
|
112
|
+
const broker = createSseBroker();
|
|
113
|
+
expect(() => broker.publishAccessInvalidation("nobody-listening")).not.toThrow();
|
|
114
|
+
});
|
|
58
115
|
});
|
|
@@ -27,6 +27,10 @@ function createTrackingBroker(): { broker: SseBroker; subscribedChannel: Promise
|
|
|
27
27
|
getTotalClientCount() {
|
|
28
28
|
return 0;
|
|
29
29
|
},
|
|
30
|
+
subscribeAccessInvalidation() {
|
|
31
|
+
return () => {};
|
|
32
|
+
},
|
|
33
|
+
publishAccessInvalidation() {},
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
return { broker, subscribedChannel };
|
package/src/api/api-constants.ts
CHANGED
|
@@ -10,8 +10,12 @@ export const Routes = {
|
|
|
10
10
|
query: "/query",
|
|
11
11
|
command: "/command",
|
|
12
12
|
sse: "/sse",
|
|
13
|
+
stream: "/stream",
|
|
13
14
|
auth: "/auth",
|
|
14
15
|
authLogin: "/auth/login",
|
|
16
|
+
authMfaVerify: "/auth/mfa/verify",
|
|
17
|
+
authMfaPreauthEnableStart: "/auth/mfa/preauth-enable-start",
|
|
18
|
+
authMfaPreauthConfirm: "/auth/mfa/preauth-confirm",
|
|
15
19
|
authLogout: "/auth/logout",
|
|
16
20
|
authTenants: "/auth/tenants",
|
|
17
21
|
authSwitchTenant: "/auth/switch-tenant",
|
|
@@ -19,6 +23,8 @@ export const Routes = {
|
|
|
19
23
|
authResetPassword: "/auth/reset-password",
|
|
20
24
|
authRequestEmailVerification: "/auth/request-email-verification",
|
|
21
25
|
authVerifyEmail: "/auth/verify-email",
|
|
26
|
+
authRequestAccountUnlock: "/auth/request-account-unlock",
|
|
27
|
+
authConfirmAccountUnlock: "/auth/confirm-account-unlock",
|
|
22
28
|
authSignupRequest: "/auth/signup-request",
|
|
23
29
|
authSignupConfirm: "/auth/signup-confirm",
|
|
24
30
|
// Tenant-Invite (Magic-Link): 3 separate accept-Endpoints für klare
|
|
@@ -36,10 +42,15 @@ export const Routes = {
|
|
|
36
42
|
// The auth middleware skips these paths.
|
|
37
43
|
export const PUBLIC_API_PATHS: ReadonlySet<string> = new Set([
|
|
38
44
|
`/api${Routes.authLogin}`,
|
|
45
|
+
`/api${Routes.authMfaVerify}`,
|
|
46
|
+
`/api${Routes.authMfaPreauthEnableStart}`,
|
|
47
|
+
`/api${Routes.authMfaPreauthConfirm}`,
|
|
39
48
|
`/api${Routes.authRequestPasswordReset}`,
|
|
40
49
|
`/api${Routes.authResetPassword}`,
|
|
41
50
|
`/api${Routes.authRequestEmailVerification}`,
|
|
42
51
|
`/api${Routes.authVerifyEmail}`,
|
|
52
|
+
`/api${Routes.authRequestAccountUnlock}`,
|
|
53
|
+
`/api${Routes.authConfirmAccountUnlock}`,
|
|
43
54
|
`/api${Routes.authSignupRequest}`,
|
|
44
55
|
`/api${Routes.authSignupConfirm}`,
|
|
45
56
|
// invite-accept braucht JWT (logged-in User, Branch 1) — NICHT public.
|