@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
package/src/api/server.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { DbConnection, PgClient } from "../db/connection";
|
|
|
3
3
|
import { createTenantDb } from "../db/tenant-db";
|
|
4
4
|
import { EXT_FILE_PROVIDER } from "../engine/extension-names";
|
|
5
5
|
import { runsInLane } from "../engine/run-in";
|
|
6
|
+
import { createAnonymousUser } from "../engine/system-user";
|
|
6
7
|
import {
|
|
7
8
|
type AppContext,
|
|
8
9
|
isFileField,
|
|
@@ -33,6 +34,8 @@ import { createEventDispatcher } from "../pipeline/event-dispatcher";
|
|
|
33
34
|
import { createLifecycleHooks, type SystemHooks } from "../pipeline/lifecycle-pipeline";
|
|
34
35
|
import { createMultiStreamApplyContext } from "../pipeline/multi-stream-apply-context";
|
|
35
36
|
import {
|
|
37
|
+
createAccessInvalidationEventConsumer,
|
|
38
|
+
createJobTriggerEventConsumer,
|
|
36
39
|
createSearchEventConsumer,
|
|
37
40
|
createSseBroadcastEventConsumer,
|
|
38
41
|
} from "../pipeline/system-hooks";
|
|
@@ -46,12 +49,13 @@ import {
|
|
|
46
49
|
import type { SearchAdapter } from "../search/types";
|
|
47
50
|
import { assertUnreachable, generateId } from "../utils";
|
|
48
51
|
import { PUBLIC_API_PATHS } from "./api-constants";
|
|
49
|
-
import { type
|
|
52
|
+
import { type AnonymousAccessResolved, authMiddleware, getUser } from "./auth-middleware";
|
|
50
53
|
import { type AuthRoutesConfig, createAuthRoutes } from "./auth-routes";
|
|
51
54
|
import { csrfMiddleware } from "./csrf-middleware";
|
|
52
|
-
import { createJwtHelper, type JwtHelper } from "./jwt";
|
|
55
|
+
import { createJwtHelper, type JwtHelper, type JwtKeyring } from "./jwt";
|
|
53
56
|
import { observabilityMiddleware } from "./observability-middleware";
|
|
54
57
|
import { assertOriginGuardConfig, originMiddleware } from "./origin-middleware";
|
|
58
|
+
import { piiCiphertextResponseGuard } from "./pii-leak-guard";
|
|
55
59
|
import { requestIdMiddleware } from "./request-id-middleware";
|
|
56
60
|
import {
|
|
57
61
|
DEFAULT_MAX_REQUEST_BYTES,
|
|
@@ -67,8 +71,13 @@ import { createSseRoute } from "./sse-route";
|
|
|
67
71
|
export type ServerOptions = {
|
|
68
72
|
registry: Registry;
|
|
69
73
|
context: AppContext;
|
|
70
|
-
jwtSecret: string;
|
|
74
|
+
jwtSecret: string | JwtKeyring;
|
|
71
75
|
jwtIssuer?: string;
|
|
76
|
+
// JWT lifetime in seconds. Explicit always wins. When omitted, the default
|
|
77
|
+
// depends on `auth.sessionChecker`: wired (revocation possible) keeps the
|
|
78
|
+
// long-lived 24h default; unwired (stateless JWTs, no revocation) drops to
|
|
79
|
+
// 1h so a leaked stateless token has a much smaller exposure window.
|
|
80
|
+
jwtTtl?: number;
|
|
72
81
|
dispatcherOptions?: Omit<DispatcherOptions, "lifecycle">;
|
|
73
82
|
systemHooks?: SystemHooks;
|
|
74
83
|
eventDedup?: EventDedup;
|
|
@@ -92,16 +101,25 @@ export type ServerOptions = {
|
|
|
92
101
|
pollIntervalMs?: number;
|
|
93
102
|
batchSize?: number;
|
|
94
103
|
maxAttempts?: number;
|
|
104
|
+
rearmCooldownMs?: number;
|
|
105
|
+
maxRearmCount?: number;
|
|
95
106
|
// Opt out of building the dispatcher even if consumers exist — e.g. ops
|
|
96
107
|
// runs a dedicated dispatcher process, or a test needs to control the
|
|
97
108
|
// consumer lifecycle manually.
|
|
98
109
|
disabled?: boolean;
|
|
99
|
-
// Opt out of the auto-built system consumers (SSE, Search)
|
|
100
|
-
// running feature r.multiStreamProjection consumers. Useful
|
|
101
|
-
// that assert only on subscriber behaviour, or for a
|
|
102
|
-
// routes SSE via a different transport. Default:
|
|
103
|
-
// respective dependency (sseBroker /
|
|
104
|
-
|
|
110
|
+
// Opt out of the auto-built system consumers (SSE, Search, Job-Trigger)
|
|
111
|
+
// while still running feature r.multiStreamProjection consumers. Useful
|
|
112
|
+
// for tests that assert only on subscriber behaviour, or for a
|
|
113
|
+
// deployment that routes SSE via a different transport. Default: sse/
|
|
114
|
+
// search enabled when the respective dependency (sseBroker /
|
|
115
|
+
// context.searchAdapter) is available; jobTrigger enabled when a
|
|
116
|
+
// jobRunner is wired via dispatcherOptions.
|
|
117
|
+
systemConsumers?: {
|
|
118
|
+
sse?: boolean;
|
|
119
|
+
search?: boolean;
|
|
120
|
+
jobTrigger?: boolean;
|
|
121
|
+
accessInvalidation?: boolean;
|
|
122
|
+
};
|
|
105
123
|
// Raw postgres.js client for LISTEN/NOTIFY wake-up (Sprint E.4). When
|
|
106
124
|
// present, `.start()` subscribes to EVENTS_PUBSUB_CHANNEL — delivery
|
|
107
125
|
// latency drops from pollIntervalMs to TCP-round-trip. The poll timer
|
|
@@ -192,9 +210,10 @@ export type ServerOptions = {
|
|
|
192
210
|
instanceId?: string;
|
|
193
211
|
// Opt-in: serve unauthenticated requests on handlers that allow
|
|
194
212
|
// roles=["anonymous"]. When omitted, every /api/* request still requires
|
|
195
|
-
// a valid JWT (status quo).
|
|
196
|
-
//
|
|
197
|
-
|
|
213
|
+
// a valid JWT (status quo). App-facing config is AnonymousAccessConfig
|
|
214
|
+
// (defaultTenantId only); run{Prod,Dev}App merge auth-foundation tenant
|
|
215
|
+
// providers into AnonymousAccessResolved before calling buildServer.
|
|
216
|
+
anonymousAccess?: AnonymousAccessResolved;
|
|
198
217
|
};
|
|
199
218
|
|
|
200
219
|
export type KumikoServer = {
|
|
@@ -236,7 +255,15 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
236
255
|
);
|
|
237
256
|
}
|
|
238
257
|
|
|
239
|
-
|
|
258
|
+
// Stateless JWTs (no sessionChecker → no revocation) default to a shorter
|
|
259
|
+
// TTL than session-backed ones, since a leaked stateless token can't be
|
|
260
|
+
// revoked and stays valid until it expires. Explicit jwtTtl always wins.
|
|
261
|
+
const defaultJwtTtl = options.auth?.sessionChecker ? 24 * 60 * 60 : 60 * 60;
|
|
262
|
+
const jwt = createJwtHelper(
|
|
263
|
+
options.jwtSecret,
|
|
264
|
+
options.jwtIssuer,
|
|
265
|
+
options.jwtTtl ?? defaultJwtTtl,
|
|
266
|
+
);
|
|
240
267
|
const sseBroker = options.sseBroker ?? createSseBroker();
|
|
241
268
|
|
|
242
269
|
// Resolve the per-process instance identifier. Prefer explicit
|
|
@@ -358,6 +385,7 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
358
385
|
const dispatcher = createDispatcher(options.registry, contextWithObservability, {
|
|
359
386
|
...options.dispatcherOptions,
|
|
360
387
|
lifecycle,
|
|
388
|
+
sseBroker,
|
|
361
389
|
});
|
|
362
390
|
|
|
363
391
|
// Async event-dispatcher — the replacement for the old transactional
|
|
@@ -380,6 +408,8 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
380
408
|
|
|
381
409
|
const sseConsumerEnabled = options.eventDispatcher?.systemConsumers?.sse ?? true;
|
|
382
410
|
const searchConsumerEnabled = options.eventDispatcher?.systemConsumers?.search ?? true;
|
|
411
|
+
const jobTriggerConsumerEnabled = options.eventDispatcher?.systemConsumers?.jobTrigger ?? true;
|
|
412
|
+
const jobRunnerForTriggers = options.dispatcherOptions?.jobRunner;
|
|
383
413
|
|
|
384
414
|
const systemConsumers: EventConsumer[] = [];
|
|
385
415
|
if (sseConsumerEnabled) {
|
|
@@ -388,6 +418,18 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
388
418
|
if (searchConsumerEnabled && searchAdapter) {
|
|
389
419
|
systemConsumers.push(createSearchEventConsumer(searchAdapter, options.registry));
|
|
390
420
|
}
|
|
421
|
+
if (jobTriggerConsumerEnabled && jobRunnerForTriggers) {
|
|
422
|
+
systemConsumers.push(createJobTriggerEventConsumer(jobRunnerForTriggers, options.registry));
|
|
423
|
+
}
|
|
424
|
+
// Default ON (#1524 global-by-default). Tests that opt out of SSE via
|
|
425
|
+
// systemConsumers.accessInvalidation=false (test-stack mirrors sse off)
|
|
426
|
+
// skip the row so retention prune suites are not blocked by a lagging
|
|
427
|
+
// cursor=0 consumer they never drain.
|
|
428
|
+
const accessInvalidationEnabled =
|
|
429
|
+
options.eventDispatcher?.systemConsumers?.accessInvalidation ?? true;
|
|
430
|
+
if (accessInvalidationEnabled) {
|
|
431
|
+
systemConsumers.push(createAccessInvalidationEventConsumer(sseBroker));
|
|
432
|
+
}
|
|
391
433
|
|
|
392
434
|
// MultiStreamProjections: one EventConsumer per MSP. Handler routes by
|
|
393
435
|
// event.type into the MSP's apply map. MSPs aggregate cross-aggregate but
|
|
@@ -553,6 +595,10 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
553
595
|
}),
|
|
554
596
|
);
|
|
555
597
|
|
|
598
|
+
// Tripwire: a kumiko-pii: ciphertext in a JSON response is always a bug
|
|
599
|
+
// (raw read leaked past a decrypt) — dev fails loud, prod redacts (#820).
|
|
600
|
+
app.use("/api/*", piiCiphertextResponseGuard());
|
|
601
|
+
|
|
556
602
|
// Auth middleware skips public paths (login, health) — those routes need
|
|
557
603
|
// to be callable without a valid JWT. Every other /api/* request requires
|
|
558
604
|
// a token (or, when anonymousAccess is wired, falls through as anonymous).
|
|
@@ -560,7 +606,10 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
560
606
|
// middleware can reject revoked sids on every request.
|
|
561
607
|
const jwtGuard = authMiddleware(jwt, {
|
|
562
608
|
...(options.auth?.sessionChecker ? { sessionChecker: options.auth.sessionChecker } : {}),
|
|
563
|
-
...(options.auth?.
|
|
609
|
+
...(options.auth?.tokenVerifier ? { tokenVerifier: options.auth.tokenVerifier } : {}),
|
|
610
|
+
...(options.auth?.resolveTenantLifecycleStatus
|
|
611
|
+
? { resolveTenantLifecycleStatus: options.auth.resolveTenantLifecycleStatus }
|
|
612
|
+
: {}),
|
|
564
613
|
...(options.anonymousAccess ? { anonymousAccess: options.anonymousAccess } : {}),
|
|
565
614
|
});
|
|
566
615
|
app.use("/api/*", async (c, next) => {
|
|
@@ -568,6 +617,33 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
568
617
|
return jwtGuard(c, next);
|
|
569
618
|
});
|
|
570
619
|
|
|
620
|
+
// PAT rate limiting — runs AFTER the auth guard so the resolved principal is
|
|
621
|
+
// available. Only PAT-authenticated requests are counted (keyed by token id);
|
|
622
|
+
// cookie/JWT users pass through untouched. In-memory limiter is per-instance
|
|
623
|
+
// (see run-prod-app) — a multi-node deployment wanting a shared counter swaps
|
|
624
|
+
// in a Redis-backed LoginRateLimiter.
|
|
625
|
+
const patRateLimiter = options.auth?.patRateLimiter;
|
|
626
|
+
if (patRateLimiter) {
|
|
627
|
+
app.use("/api/*", async (c, next) => {
|
|
628
|
+
if (PUBLIC_API_PATHS.has(c.req.path)) return next();
|
|
629
|
+
const pat = getUser(c)?.pat;
|
|
630
|
+
if (pat && !(await patRateLimiter.check(pat.tokenId))) {
|
|
631
|
+
return c.json(
|
|
632
|
+
{
|
|
633
|
+
error: {
|
|
634
|
+
code: "pat_rate_limited",
|
|
635
|
+
httpStatus: 429,
|
|
636
|
+
message: "personal access token rate limit exceeded",
|
|
637
|
+
i18nKey: "auth.errors.patRateLimited",
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
429,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
return next();
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
571
647
|
// Origin-allowlist guard — additional CSRF-hardening for deployments that
|
|
572
648
|
// widen the auth cookie across subdomains (auth.cookieDomain). Registered
|
|
573
649
|
// only when allowedOrigins is non-empty, with the same /api/* + public-skip
|
|
@@ -634,12 +710,32 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
634
710
|
// /api/* immer Vorrang hat — feature-Routes liegen ohnehin außerhalb
|
|
635
711
|
// (Boot-Validator blockt /api-Prefix). deps.app ist die Outer-App, sodass
|
|
636
712
|
// der Handler /api/query intern via app.fetch(...) nutzen kann (gleicher
|
|
637
|
-
// Auth-Pfad wie ein echter HTTP-Call).
|
|
713
|
+
// Auth-Pfad wie ein echter HTTP-Call). deps.systemQuery umgeht diesen
|
|
714
|
+
// Pfad bewusst — für Routes die einen FESTEN Tenant erzwingen müssen
|
|
715
|
+
// (z.B. legal-pages → immer SYSTEM_TENANT_ID), statt ihn per X-Tenant-
|
|
716
|
+
// Header vorzutäuschen (das sieht für die anonymousAccess-Auflösung
|
|
717
|
+
// wie ein normaler Client-Header aus und wird von resolverTrust:
|
|
718
|
+
// "authoritative"-Configs zurecht als Tenant-Override abgelehnt).
|
|
638
719
|
for (const feature of options.registry.features.values()) {
|
|
639
720
|
for (const route of Object.values(feature.httpRoutes)) {
|
|
640
721
|
// @wrapper-known semantic-alias
|
|
641
722
|
const honoHandler = async (c: import("hono").Context): Promise<Response> =>
|
|
642
|
-
route.handler(c, {
|
|
723
|
+
route.handler(c, {
|
|
724
|
+
app,
|
|
725
|
+
systemQuery: (type, payload, tenantId) =>
|
|
726
|
+
// createAnonymousUser, NOT createSystemUser: httpRoute handlers
|
|
727
|
+
// using systemQuery are, by construction, `anonymous: true`
|
|
728
|
+
// public routes — the synthesized user must clear the SAME
|
|
729
|
+
// access gate a real anonymous visitor would, no more. The
|
|
730
|
+
// system role would ALSO satisfy that gate here, but it can
|
|
731
|
+
// read fields gated to "system" that "anonymous" can't
|
|
732
|
+
// (filterReadFields is a plain role-in-map check) — a future
|
|
733
|
+
// systemQuery caller reading a system-gated field would leak
|
|
734
|
+
// it into a public response. The forced tenant already comes
|
|
735
|
+
// from bypassing the HTTP layer entirely; no elevated role
|
|
736
|
+
// is needed or wanted on top of that.
|
|
737
|
+
dispatcher.query(type, payload, createAnonymousUser(tenantId)),
|
|
738
|
+
});
|
|
643
739
|
switch (route.method) {
|
|
644
740
|
case "GET":
|
|
645
741
|
app.get(route.path, honoHandler);
|
package/src/api/sse-broker.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { userAccessChannel } from "../engine/constants";
|
|
1
2
|
import { generateId } from "../utils";
|
|
2
3
|
|
|
3
4
|
export type SseClient = {
|
|
@@ -17,10 +18,18 @@ export type SseBroker = {
|
|
|
17
18
|
pushToChannel(channel: string, event: SseEvent): void;
|
|
18
19
|
getClientCount(channel: string): number;
|
|
19
20
|
getTotalClientCount(): number;
|
|
21
|
+
// Internal (non-SSE-client) subscription, e.g. dispatch-stream watching
|
|
22
|
+
// for mid-stream access revocation. Kept separate from addClient/
|
|
23
|
+
// pushToChannel: those count towards getClientCount/getTotalClientCount
|
|
24
|
+
// (real SSE connections) and their send/close shape doesn't fit a plain
|
|
25
|
+
// callback listener. Returns an unsubscribe function.
|
|
26
|
+
subscribeAccessInvalidation(userId: string, onInvalidate: () => void): () => void;
|
|
27
|
+
publishAccessInvalidation(userId: string): void;
|
|
20
28
|
};
|
|
21
29
|
|
|
22
30
|
export function createSseBroker(): SseBroker {
|
|
23
31
|
const channels = new Map<string, Map<string, SseClient>>();
|
|
32
|
+
const accessInvalidationListeners = new Map<string, Map<string, () => void>>();
|
|
24
33
|
|
|
25
34
|
function getOrCreateChannel(channel: string): Map<string, SseClient> {
|
|
26
35
|
let clients = channels.get(channel);
|
|
@@ -67,5 +76,35 @@ export function createSseBroker(): SseBroker {
|
|
|
67
76
|
}
|
|
68
77
|
return total;
|
|
69
78
|
},
|
|
79
|
+
|
|
80
|
+
subscribeAccessInvalidation(userId, onInvalidate) {
|
|
81
|
+
const channel = userAccessChannel(userId);
|
|
82
|
+
const listenerId = generateId();
|
|
83
|
+
let listeners = accessInvalidationListeners.get(channel);
|
|
84
|
+
if (!listeners) {
|
|
85
|
+
listeners = new Map();
|
|
86
|
+
accessInvalidationListeners.set(channel, listeners);
|
|
87
|
+
}
|
|
88
|
+
listeners.set(listenerId, onInvalidate);
|
|
89
|
+
return () => {
|
|
90
|
+
const current = accessInvalidationListeners.get(channel);
|
|
91
|
+
// skip: already unsubscribed (e.g. stream ended after a publish already fired)
|
|
92
|
+
if (!current) return;
|
|
93
|
+
current.delete(listenerId);
|
|
94
|
+
if (current.size === 0) accessInvalidationListeners.delete(channel);
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
publishAccessInvalidation(userId) {
|
|
99
|
+
const channel = userAccessChannel(userId);
|
|
100
|
+
const listeners = accessInvalidationListeners.get(channel);
|
|
101
|
+
// skip: no live stream is watching this user right now
|
|
102
|
+
if (!listeners) return;
|
|
103
|
+
// Snapshot before iterating — a fired listener unsubscribes itself,
|
|
104
|
+
// which would mutate `listeners` mid-iteration otherwise.
|
|
105
|
+
for (const onInvalidate of [...listeners.values()]) {
|
|
106
|
+
onInvalidate();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
70
109
|
};
|
|
71
110
|
}
|
|
@@ -8,7 +8,6 @@ db/postgres-provider.ts postgres-js Factory (default, stabil)
|
|
|
8
8
|
db/bun-provider.ts Bun.SQL Factory (DB_PROVIDER=bun, experimentell)
|
|
9
9
|
stack/db.ts createTestDb() — provider-agnostisch via createConnection
|
|
10
10
|
stack/test-stack.ts setupTestStack() — provider-agnostisch via createTestDb
|
|
11
|
-
bun-db/__tests__/bun-test-stack.ts Alias: setupBunTestStack → setupTestStack
|
|
12
11
|
bun-db/__tests__/bun-test-db.ts Alias: createBunTestDb → createTestDb
|
|
13
12
|
```
|
|
14
13
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// kumiko-framework#1480: instantFromDriver referenced the global `Temporal`
|
|
2
|
+
// without importing it. Bun doesn't expose Temporal as a globalThis property
|
|
3
|
+
// reliably — any storeTable read of a timestamptz column crashed with
|
|
4
|
+
// "Temporal is not defined" unless some other boot path happened to install
|
|
5
|
+
// the polyfill globally first (order-dependent, easy to miss in a fresh
|
|
6
|
+
// process). The fix is a static `import { Temporal } from "temporal-polyfill"`
|
|
7
|
+
// in query.ts, so this test deletes globalThis.Temporal before calling
|
|
8
|
+
// coerceRow — proving the coercion no longer depends on the global at all.
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
import { Temporal } from "temporal-polyfill";
|
|
12
|
+
import { coerceRow, type TableInfo } from "../query";
|
|
13
|
+
|
|
14
|
+
function timestamptzTableInfo(): TableInfo {
|
|
15
|
+
return {
|
|
16
|
+
name: "probe",
|
|
17
|
+
columnOf: (f) => f,
|
|
18
|
+
pgTypeOf: (c) => (c === "updated_at" ? "timestamptz" : undefined),
|
|
19
|
+
bigintJsModeOf: () => undefined,
|
|
20
|
+
fieldOf: (c) => c,
|
|
21
|
+
hasColumn: () => true,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("coerceRow — timestamptz → Temporal.Instant", () => {
|
|
26
|
+
test("coerces without relying on a global Temporal", () => {
|
|
27
|
+
const savedGlobal = (globalThis as { Temporal?: unknown }).Temporal;
|
|
28
|
+
delete (globalThis as { Temporal?: unknown }).Temporal;
|
|
29
|
+
try {
|
|
30
|
+
const row = { updated_at: new Date("2026-04-18T10:00:00Z") };
|
|
31
|
+
const result = coerceRow(row, timestamptzTableInfo());
|
|
32
|
+
expect(result.updated_at).toBeInstanceOf(Temporal.Instant);
|
|
33
|
+
expect((result.updated_at as unknown as Temporal.Instant).toString()).toBe(
|
|
34
|
+
"2026-04-18T10:00:00Z",
|
|
35
|
+
);
|
|
36
|
+
} finally {
|
|
37
|
+
if (savedGlobal === undefined) delete (globalThis as { Temporal?: unknown }).Temporal;
|
|
38
|
+
else (globalThis as { Temporal?: unknown }).Temporal = savedGlobal;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// #1163: Bun.SQL can hand out a closed connection under load (AbortError
|
|
2
|
+
// "The connection was closed."). Pure reads retry exactly once on a fresh
|
|
3
|
+
// pool checkout; tx handles, non-matching errors, and genuine user aborts
|
|
4
|
+
// must NOT retry.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { buildEntityTable } from "../../db/table-builder";
|
|
8
|
+
import { selectMany } from "../query";
|
|
9
|
+
|
|
10
|
+
function closedConnectionError(): Error {
|
|
11
|
+
return Object.assign(new Error("The connection was closed."), { name: "AbortError" });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type FakeClient = {
|
|
15
|
+
unsafe: (sql: string, params?: readonly unknown[]) => Promise<readonly unknown[]>;
|
|
16
|
+
begin?: () => never;
|
|
17
|
+
calls: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function fakeClient(failures: Error[], opts: { tx?: boolean } = {}): FakeClient {
|
|
21
|
+
const remaining = [...failures];
|
|
22
|
+
const client: FakeClient = {
|
|
23
|
+
calls: 0,
|
|
24
|
+
unsafe: async () => {
|
|
25
|
+
client.calls++;
|
|
26
|
+
const err = remaining.shift();
|
|
27
|
+
if (err) throw err;
|
|
28
|
+
return [{ id: "r1", title: "ok", tenant_id: "t1", inserted_at: null, updated_at: null }];
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
// A top-level pool client has begin(); a transaction handle does not.
|
|
32
|
+
if (!opts.tx)
|
|
33
|
+
client.begin = () => {
|
|
34
|
+
throw new Error("not used in test");
|
|
35
|
+
};
|
|
36
|
+
return client;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const table = buildEntityTable("note", {
|
|
40
|
+
fields: { title: { type: "text", required: true } },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("selectMany — closed-connection retry (#1163)", () => {
|
|
44
|
+
test("retries once on AbortError 'connection was closed' and returns rows", async () => {
|
|
45
|
+
const db = fakeClient([closedConnectionError()]);
|
|
46
|
+
const rows = await selectMany(db, table);
|
|
47
|
+
expect(rows).toHaveLength(1);
|
|
48
|
+
expect(rows[0]?.title).toBe("ok");
|
|
49
|
+
expect(db.calls).toBe(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("gives up after the single retry when the connection stays closed", async () => {
|
|
53
|
+
const db = fakeClient([closedConnectionError(), closedConnectionError()]);
|
|
54
|
+
await expect(selectMany(db, table)).rejects.toThrow("connection was closed");
|
|
55
|
+
expect(db.calls).toBe(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("never retries on a transaction handle (no begin)", async () => {
|
|
59
|
+
const db = fakeClient([closedConnectionError()], { tx: true });
|
|
60
|
+
await expect(selectMany(db, table)).rejects.toThrow("connection was closed");
|
|
61
|
+
expect(db.calls).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("does not retry a genuine user abort (different message)", async () => {
|
|
65
|
+
const userAbort = Object.assign(new Error("The operation was aborted."), {
|
|
66
|
+
name: "AbortError",
|
|
67
|
+
});
|
|
68
|
+
const db = fakeClient([userAbort]);
|
|
69
|
+
await expect(selectMany(db, table)).rejects.toThrow("operation was aborted");
|
|
70
|
+
expect(db.calls).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("does not retry generic query errors", async () => {
|
|
74
|
+
const syntax = Object.assign(new Error("syntax error at or near"), { name: "PostgresError" });
|
|
75
|
+
const db = fakeClient([syntax]);
|
|
76
|
+
await expect(selectMany(db, table)).rejects.toThrow("syntax error");
|
|
77
|
+
expect(db.calls).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { expect, test } from "bun:test";
|
|
9
9
|
import { defineUnmanagedTable } from "../../db/entity-table-meta";
|
|
10
10
|
import { buildEntityTable } from "../../db/table-builder";
|
|
11
|
+
import type { TenantDb } from "../../db/tenant-db";
|
|
11
12
|
import { createEntity, createTextField } from "../../engine";
|
|
12
13
|
import { type AnyDb, deleteMany, insertOne, selectMany, updateMany } from "../query";
|
|
13
14
|
|
|
@@ -41,8 +42,28 @@ async function _readAllowsManagedEntity(db: AnyDb): Promise<void> {
|
|
|
41
42
|
await selectMany(db, brandedEntity, { id: "1" });
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// Method-form (ctx.db.insertOne/updateMany/deleteMany) rejects the brand too — a
|
|
46
|
+
// projection written past its event stream is wiped on rebuild whether the write
|
|
47
|
+
// went through the free function or the TenantDb method.
|
|
48
|
+
async function _methodFormRejectsManagedEntity(db: TenantDb): Promise<void> {
|
|
49
|
+
// @ts-expect-error — managed EntityTable is executor-only; method-form insert is a compile error.
|
|
50
|
+
await db.insertOne(brandedEntity, { title: "x" });
|
|
51
|
+
// @ts-expect-error — method-form update on a managed EntityTable is rejected.
|
|
52
|
+
await db.updateMany(brandedEntity, { title: "y" }, { id: "1" });
|
|
53
|
+
// @ts-expect-error — method-form delete on a managed EntityTable is rejected.
|
|
54
|
+
await db.deleteMany(brandedEntity, { id: "1" });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function _methodFormReadAllowsManagedEntity(db: TenantDb): Promise<void> {
|
|
58
|
+
// Method-form reads on a managed EntityTable stay fine (reads keep the plain param).
|
|
59
|
+
await db.selectMany(brandedEntity, { id: "1" });
|
|
60
|
+
await db.fetchOne(brandedEntity, { id: "1" });
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
test("ES-write brand: compile-time contracts are wired", () => {
|
|
45
64
|
expect(_writeRejectsManagedEntity).toBeDefined();
|
|
46
65
|
expect(_writeAllowsUnmanagedTable).toBeDefined();
|
|
47
66
|
expect(_readAllowsManagedEntity).toBeDefined();
|
|
67
|
+
expect(_methodFormRejectsManagedEntity).toBeDefined();
|
|
68
|
+
expect(_methodFormReadAllowsManagedEntity).toBeDefined();
|
|
48
69
|
});
|
package/src/bun-db/connection.ts
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
// event-dispatcher.ts. Bun.sql 1.2.20 hat kein listen() (PR oven-sh/bun#25511
|
|
7
7
|
// pending). Nach Landung des Bun-LISTEN-Supports: peer raus.
|
|
8
8
|
|
|
9
|
+
import type { PgListenClient } from "@cosmicdrift/kumiko-types/db-connection";
|
|
9
10
|
import postgres from "postgres";
|
|
10
11
|
import { readPositiveIntEnv } from "../utils/env-parse";
|
|
11
12
|
|
|
13
|
+
export type { PgListenClient };
|
|
14
|
+
|
|
12
15
|
// Bun.SQL ist callable als tagged template `sql\`...\`` PLUS hat methods
|
|
13
16
|
// (.begin / .unsafe / .end / .file / .reserve etc.). DbConnection-Type
|
|
14
17
|
// reflektiert die Instance-Shape.
|
|
@@ -21,9 +24,6 @@ export type BunDbTx = BunDbConnection;
|
|
|
21
24
|
// Beide austauschbar im normalen call-path.
|
|
22
25
|
export type BunDbRunner = BunDbConnection | BunDbTx;
|
|
23
26
|
|
|
24
|
-
// Postgres-js peer NUR für event-dispatcher LISTEN.
|
|
25
|
-
export type PgListenClient = ReturnType<typeof postgres>;
|
|
26
|
-
|
|
27
27
|
export type BunDbConnectionOptions = {
|
|
28
28
|
readonly maxConnections?: number;
|
|
29
29
|
readonly idleTimeoutSeconds?: number;
|
package/src/bun-db/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type {
|
|
|
11
11
|
export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection";
|
|
12
12
|
export type { SelectOptions, TableInfo, WhereObject, WhereOperator, WhereValue } from "./query";
|
|
13
13
|
export {
|
|
14
|
+
asEntityTableMeta,
|
|
14
15
|
asRawClient,
|
|
15
16
|
countWhere,
|
|
16
17
|
type DeleteManyBatchedOptions,
|
|
@@ -23,6 +24,7 @@ export {
|
|
|
23
24
|
incrementCounter,
|
|
24
25
|
insertMany,
|
|
25
26
|
insertOne,
|
|
27
|
+
runInSavepoint,
|
|
26
28
|
selectMany,
|
|
27
29
|
transaction,
|
|
28
30
|
type UpsertOnConflictOptions,
|