@cosmicdrift/kumiko-framework 0.386.0 → 0.388.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/AGENTS.md +6 -1
- package/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +40 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-error-route.js +8 -2
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +203 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +293 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1250
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +31 -27
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +7 -5
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +3 -3
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +99 -0
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +105 -112
- package/dist/db/event-store-executor-context.d.ts +3 -2
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +50 -704
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +401 -510
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +97 -105
- package/dist/db/migrate-runner.js +29 -26
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/query.d.ts +3 -16
- package/dist/db/query.js +8 -761
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/reference-data.js +6 -1
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +137 -190
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +48 -11
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +6 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +4 -4
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +77 -61
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +6 -4
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +22 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +258 -187
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +23 -17
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +12 -17
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +26 -24
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +102 -869
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +130 -170
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +21 -13
- package/dist/engine/define-handler.js +13 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +22 -34
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +2 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +11 -2
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +50 -89
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +12 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +7 -5
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +13 -13
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +251 -233
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +9 -28
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.d.ts +4 -1
- package/dist/entrypoint/index.js +10 -4
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +29 -29
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +7 -8
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +12 -6
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +7 -9
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +22 -13
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/upload-ledger.js +7 -0
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +188 -212
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +69 -4
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +57 -669
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +20 -13
- package/dist/pipeline/dispatch-shared.d.ts +14 -3
- package/dist/pipeline/dispatch-shared.js +67 -721
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +297 -190
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +4 -6
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +15 -13
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -510
- package/dist/search/index.js +4 -4
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +159 -130
- package/dist/utils/compare.js +1 -1
- package/package.json +5 -5
- package/src/changes.json +358 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +192 -153
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -63,9 +63,7 @@ export async function runInMemberReadOnlyTransaction(ctx, tx, fn) {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
// Savepoint even on the pool path: Postgres rejects a READ WRITE reset (SQLSTATE 25001) inside a subtransaction.
|
|
66
|
-
return await transaction(pool, (outer) =>
|
|
67
|
-
// @cast-boundary driver begin() handle — structurally a DbTx, same shape runInSavepoint's caller relies on
|
|
68
|
-
runInDiscardedReadOnlySavepoint(outer, timeoutMs, true, fn));
|
|
66
|
+
return await transaction(pool, (outer) => runInDiscardedReadOnlySavepoint(outer, timeoutMs, true, fn));
|
|
69
67
|
}
|
|
70
68
|
catch (e) {
|
|
71
69
|
if (e instanceof AccessDeniedError)
|
|
@@ -159,7 +159,7 @@ async function replayMultiStreamProjectionIntoShadow(mspName, deps) {
|
|
|
159
159
|
durationMs: Date.now() - startedAt,
|
|
160
160
|
// countColumnDrift is single-table (public.<t> vs kumiko_rebuild.<t>) and
|
|
161
161
|
// not wired into the multi-stream rebuild path — MSPs don't run the
|
|
162
|
-
// implicit-projection ghost-row guard either.
|
|
162
|
+
// implicit-projection ghost-row guard either. The scope is single-table.
|
|
163
163
|
columnDriftCount: 0,
|
|
164
164
|
};
|
|
165
165
|
if (deps.meter) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertNotificationVariables } from "../engine/template-variables.js";
|
|
2
2
|
import { AccessDeniedError, FrameworkReasons } from "../errors/index.js";
|
|
3
|
+
import { parseJsonOrThrow } from "../utils/safe-json.js";
|
|
3
4
|
function isTenantBroadcast(to) {
|
|
4
5
|
return typeof to === "object" && to !== null && !Array.isArray(to) && "tenant" in to;
|
|
5
6
|
}
|
|
@@ -12,6 +13,16 @@ export function assertNotifyTargetsOwnTenant(options, tenantId) {
|
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
15
|
}
|
|
16
|
+
// Temporal values, functions and class instances in `data` are not structured-cloneable; the
|
|
17
|
+
// job pipeline serializes them to JSON on enqueue anyway, so the JSON form is the faithful snapshot.
|
|
18
|
+
function snapshotNotifyOptions(options) {
|
|
19
|
+
try {
|
|
20
|
+
return structuredClone(options);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return parseJsonOrThrow(JSON.stringify(options), "notify options snapshot");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
15
26
|
// Without `immediate`, a notify inside a write transaction waits for the commit so a
|
|
16
27
|
// rolled-back write leaves no notification behind.
|
|
17
28
|
export function createDeferringNotify(deps) {
|
|
@@ -24,7 +35,7 @@ export function createDeferringNotify(deps) {
|
|
|
24
35
|
if (options.immediate === true)
|
|
25
36
|
return notifyNow(notificationType, options);
|
|
26
37
|
// The hook runs after the handler returned; a caller reusing `data` meanwhile must not change what is sent.
|
|
27
|
-
const optionsAtCallTime =
|
|
38
|
+
const optionsAtCallTime = snapshotNotifyOptions(options);
|
|
28
39
|
scheduleAfterCommit(async () => {
|
|
29
40
|
await notifyAfterCommit(notificationType, optionsAtCallTime);
|
|
30
41
|
});
|
|
@@ -2,16 +2,17 @@ import { hasAccess } from "../engine/access.js";
|
|
|
2
2
|
import { tenantOverrideDenied } from "../engine/cross-tenant.js";
|
|
3
3
|
import { checkWriteFieldRoles } from "../engine/field-access.js";
|
|
4
4
|
import { runValidation } from "../engine/validation.js";
|
|
5
|
-
import { AccessDeniedError, FrameworkReasons, isKumikoError, ValidationError, validationErrorFromZod, writeFailure, } from "../errors/index.js";
|
|
5
|
+
import { AccessDeniedError, FrameworkReasons, InternalError, isKumikoError, ValidationError, validationErrorFromZod, writeFailure, } from "../errors/index.js";
|
|
6
6
|
import { checkFeatureEnabled, enforcePayloadRateLimits, enforceRateLimit, } from "./dispatch-shared.js";
|
|
7
7
|
import { handlerAccessError } from "./handler-access-error.js";
|
|
8
8
|
async function rejectDisabledFeature(ctx, type, user) {
|
|
9
9
|
const disabledErr = await checkFeatureEnabled(ctx, type, user.tenantId);
|
|
10
10
|
return disabledErr ? writeFailure(disabledErr) : undefined;
|
|
11
11
|
}
|
|
12
|
-
async function rejectRateLimited(ctx, handler, type, user) {
|
|
12
|
+
async function rejectRateLimited(ctx, handler, type, user, resolveCostOverride) {
|
|
13
13
|
try {
|
|
14
|
-
|
|
14
|
+
const costOverride = resolveCostOverride?.();
|
|
15
|
+
await enforceRateLimit(ctx, handler.rateLimit, type, user, ctx.registry.isHandlerSystemScoped(type), costOverride);
|
|
15
16
|
}
|
|
16
17
|
catch (e) {
|
|
17
18
|
if (isKumikoError(e))
|
|
@@ -20,6 +21,14 @@ async function rejectRateLimited(ctx, handler, type, user) {
|
|
|
20
21
|
}
|
|
21
22
|
return undefined;
|
|
22
23
|
}
|
|
24
|
+
function normalizeRateLimitCost(handlerName, cost) {
|
|
25
|
+
if (!Number.isFinite(cost)) {
|
|
26
|
+
throw new InternalError({
|
|
27
|
+
message: `Handler "${handlerName}" rateLimitCost returned ${cost}; expected a finite number.`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return Math.max(1, Math.ceil(cost));
|
|
31
|
+
}
|
|
23
32
|
async function rejectPayloadRateLimited(ctx, handler, type, user, payload) {
|
|
24
33
|
try {
|
|
25
34
|
await enforcePayloadRateLimits(ctx, handler.additionalRateLimits, type, user, payload);
|
|
@@ -70,7 +79,7 @@ async function rejectBeforeParse(ctx, handler, type, user, mode) {
|
|
|
70
79
|
const disabled = await rejectDisabledFeature(ctx, type, user);
|
|
71
80
|
if (disabled)
|
|
72
81
|
return disabled;
|
|
73
|
-
if (mode === "charge") {
|
|
82
|
+
if (mode === "charge" && !handler.rateLimitCost) {
|
|
74
83
|
const rateLimited = await rejectRateLimited(ctx, handler, type, user);
|
|
75
84
|
if (rateLimited)
|
|
76
85
|
return rateLimited;
|
|
@@ -85,6 +94,13 @@ async function rejectAfterParse(ctx, handler, type, user, payload, mode) {
|
|
|
85
94
|
const overrideDenied = tenantOverrideDenied(user, payload);
|
|
86
95
|
if (overrideDenied)
|
|
87
96
|
return writeFailure(overrideDenied);
|
|
97
|
+
if (mode === "charge" && handler.rateLimitCost) {
|
|
98
|
+
// Cost comes from the validated payload, so this bucket is billed after the parse instead of before it.
|
|
99
|
+
const { rateLimitCost } = handler;
|
|
100
|
+
const rateLimited = await rejectRateLimited(ctx, handler, type, user, () => normalizeRateLimitCost(type, rateLimitCost(payload)));
|
|
101
|
+
if (rateLimited)
|
|
102
|
+
return rateLimited;
|
|
103
|
+
}
|
|
88
104
|
if (mode === "charge") {
|
|
89
105
|
const rateLimited = await rejectPayloadRateLimited(ctx, handler, type, user, payload);
|
|
90
106
|
if (rateLimited)
|
|
@@ -7,6 +7,7 @@ type ReservationConfirm = ReservationHandle["confirmInTransaction"];
|
|
|
7
7
|
export type ConsumedReservation = {
|
|
8
8
|
readonly confirmInTransaction: ReservationConfirm | undefined;
|
|
9
9
|
readonly prepared: unknown;
|
|
10
|
+
readonly parsedPayload: unknown;
|
|
10
11
|
};
|
|
11
12
|
export declare function hasPreTransactionPhase(handler: WriteHandlerDef): boolean;
|
|
12
13
|
export declare function runAsCommand<T>(reserved: {
|
|
@@ -62,10 +62,10 @@ export async function reserveBeforeTransaction(ctx, commands, user, inheritedOri
|
|
|
62
62
|
return { isSuccess: false, error: gates.error, failedIndex: index };
|
|
63
63
|
}
|
|
64
64
|
const origin = effectiveWriteOrigin(rootWriteOrigin(ctx.registry, command.type, user), inheritedOrigin);
|
|
65
|
-
const handlerContext = await buildHandlerContext(ctx, command.type, user, origin);
|
|
66
65
|
const event = { type: command.type, payload: gates.payload, user };
|
|
67
66
|
let confirmInTransaction;
|
|
68
67
|
if (handler.reserveBeforeTransaction) {
|
|
68
|
+
const handlerContext = await buildHandlerContext(ctx, command.type, user, origin);
|
|
69
69
|
const handle = toReservationHandle(await handler.reserveBeforeTransaction(event, denyTransactionBoundSurfaces(handlerContext)));
|
|
70
70
|
if (handle)
|
|
71
71
|
releases.push(handle.release);
|
|
@@ -73,7 +73,9 @@ export async function reserveBeforeTransaction(ctx, commands, user, inheritedOri
|
|
|
73
73
|
}
|
|
74
74
|
let prepared;
|
|
75
75
|
if (handler.beforeTransaction) {
|
|
76
|
-
const phaseContext = await buildHandlerContext(ctx, command.type, user, origin,
|
|
76
|
+
const phaseContext = await buildHandlerContext(ctx, command.type, user, origin, {
|
|
77
|
+
readOnlyForTenant: true,
|
|
78
|
+
});
|
|
77
79
|
const phase = await handler.beforeTransaction(event, applyBeforeTransactionReadOnly(phaseContext));
|
|
78
80
|
if (!phase.isSuccess) {
|
|
79
81
|
await releaseAll(ctx, releases);
|
|
@@ -83,7 +85,11 @@ export async function reserveBeforeTransaction(ctx, commands, user, inheritedOri
|
|
|
83
85
|
releases.push(phase.release);
|
|
84
86
|
prepared = phase.data;
|
|
85
87
|
}
|
|
86
|
-
reservedCommands.set(index, {
|
|
88
|
+
reservedCommands.set(index, {
|
|
89
|
+
confirmInTransaction,
|
|
90
|
+
prepared,
|
|
91
|
+
parsedPayload: gates.payload,
|
|
92
|
+
});
|
|
87
93
|
}
|
|
88
94
|
catch (error) {
|
|
89
95
|
await releaseAll(ctx, releases);
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { countSubscribedEvents, finalizeProjectionRebuild, markProjectionRebuildFailed, markProjectionRebuilding, selectEventsForProjectionRebuildBatch, } from "../db/queries/projection-rebuild.js";
|
|
2
2
|
import { assertLiveColumnsMatchMeta, assertLiveTableHasNoRowLevelSecurity, assertNoBlindIndexLoss, assertNoUnreachableLiveRows, buildShadowTable, countColumnDrift, ensureRebuildSchema, fenceLiveTable, rebuildMetaOrThrow, swapShadowIntoLive, } from "../db/queries/shadow-swap.js";
|
|
3
|
-
import { coerceRow,
|
|
3
|
+
import { coerceRow, runInSavepoint, selectMany } from "../db/query.js";
|
|
4
4
|
import { withoutStatementTimeout } from "../db/statement-timeout.js";
|
|
5
|
+
import { extractTableInfo } from "../db/table-info.js";
|
|
5
6
|
import { withExecutorManagedWrites } from "../db/tenant-db.js";
|
|
6
7
|
import { eventsTable, getEventsHighWaterMark, upcastStoredEvent, } from "../event-store/index.js";
|
|
7
8
|
import { createRebuildDeadLetterTable, recordRebuildDeadLetters, } from "../event-store/rebuild-dead-letter.js";
|
|
9
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
8
10
|
import { emitProjectionRebuild } from "../observability/standard-metrics.js";
|
|
9
11
|
import { projectionStateTable } from "./projection-state.js";
|
|
12
|
+
const log = createFallbackLogger("kumiko:projection-rebuild");
|
|
10
13
|
// Events replayed per catch-up batch. Each batch is a fresh READ COMMITTED
|
|
11
14
|
// SELECT, so a batch shorter than this means the currently-committed tail is
|
|
12
15
|
// drained.
|
|
@@ -32,7 +35,7 @@ function rowToStoredEvent(row) {
|
|
|
32
35
|
createdBy: row.createdBy,
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
// Ops-facing log line for countColumnDrift's result. No-op when clean.
|
|
38
|
+
// Ops-facing log line for countColumnDrift's result. No-op when clean.
|
|
36
39
|
function warnOnColumnDrift(projectionName, tableName, drift) {
|
|
37
40
|
// skip: no drift found — nothing to warn about
|
|
38
41
|
if (drift.rowCount === 0)
|
|
@@ -41,7 +44,7 @@ function warnOnColumnDrift(projectionName, tableName, drift) {
|
|
|
41
44
|
const countLabel = sampledIds.size < drift.rowCount
|
|
42
45
|
? `${drift.rowCount} (sample truncated)`
|
|
43
46
|
: String(drift.rowCount);
|
|
44
|
-
|
|
47
|
+
log.warn(`projection "${projectionName}": ${countLabel} live row(s) in "${tableName}" have ` +
|
|
45
48
|
`column values that differ from the freshly-replayed shadow (sample: ${drift.sample.join(", ")}). ` +
|
|
46
49
|
"Not blocking — replay wins on swap. Likely cause: a legacy direct-write predating the row's " +
|
|
47
50
|
"event history (#494), or a sensitive custom field pending #972. Investigate if unexpected.");
|
|
@@ -71,7 +74,7 @@ async function replayProjectionIntoShadow(projectionName, deps) {
|
|
|
71
74
|
let lastProcessedEventId = 0n;
|
|
72
75
|
const skipApplyErrors = deps.errorPolicy?.skipApplyErrors === true;
|
|
73
76
|
// Quarantined events, collected in memory and written once after the
|
|
74
|
-
// replay settles — the
|
|
77
|
+
// replay settles — the full-re-replay path would otherwise duplicate
|
|
75
78
|
// the dead-letter rows.
|
|
76
79
|
let skipped = [];
|
|
77
80
|
// One chronological batch of events after lastProcessedEventId, applied into
|
|
@@ -145,7 +148,7 @@ async function replayProjectionIntoShadow(projectionName, deps) {
|
|
|
145
148
|
// Fence the live table, then drain the final delta. Once ACCESS
|
|
146
149
|
// EXCLUSIVE is held no concurrent apply can commit a new event, so this
|
|
147
150
|
// loop terminates and the shadow ends up reflecting every committed
|
|
148
|
-
// event — closing
|
|
151
|
+
// event — closing the write-loss window for single-pod rebuilds.
|
|
149
152
|
await fenceLiveTable(tx, meta.tableName, fenceLockTimeoutMs);
|
|
150
153
|
while ((await drainBatch(tx)) === REBUILD_BATCH_SIZE) {
|
|
151
154
|
// keep draining full batches; a short batch ends the loop
|
|
@@ -153,7 +156,7 @@ async function replayProjectionIntoShadow(projectionName, deps) {
|
|
|
153
156
|
// Fenced → the subscribed-event set is final (every live apply blocks
|
|
154
157
|
// on the live-table lock). If fewer events were applied than exist, a
|
|
155
158
|
// lower-id event committed late during the unlocked phase and the
|
|
156
|
-
// id-cursor leapt past it
|
|
159
|
+
// id-cursor leapt past it. Rebuild the shadow from scratch and
|
|
157
160
|
// replay the whole log under the fence: a fresh shadow means no
|
|
158
161
|
// double-apply (the rejected "re-drain into a populated shadow" hazard),
|
|
159
162
|
// and the full-replay cost is paid only on this rare detected path.
|
|
@@ -184,12 +187,12 @@ async function replayProjectionIntoShadow(projectionName, deps) {
|
|
|
184
187
|
}
|
|
185
188
|
// Guard the swap: abort if the process has no blind-index key configured
|
|
186
189
|
// but the live table already has populated bidx columns — the rebuild
|
|
187
|
-
// would otherwise silently null them out
|
|
190
|
+
// would otherwise silently null them out. Applies to every
|
|
188
191
|
// projection, not just implicit ones: the shadow always gets a fresh
|
|
189
192
|
// computeBlindIndexValues() pass regardless of projection kind.
|
|
190
193
|
await assertNoBlindIndexLoss(tx, meta.tableName, meta, projectionName);
|
|
191
194
|
// Guard the swap: abort if the live table holds a row no event can
|
|
192
|
-
// reconstruct (
|
|
195
|
+
// reconstruct (ghost — direct-inserted without a .created event),
|
|
193
196
|
// which the swap would silently drop. Implicit projections only.
|
|
194
197
|
if (projection.isImplicit === true) {
|
|
195
198
|
await assertNoUnreachableLiveRows(tx, projectionName, meta.tableName, sourcesList, projection.extraSourceRowIds);
|
|
@@ -249,11 +252,10 @@ export async function getProjectionState(db, projectionName) {
|
|
|
249
252
|
// The registry is the source-of-truth for which projections exist; the
|
|
250
253
|
// state table holds per-projection rebuild info and may be sparse.
|
|
251
254
|
//
|
|
252
|
-
// Implicit
|
|
253
|
-
//
|
|
254
|
-
// `<feature>:projection:<entity>-entity
|
|
255
|
-
//
|
|
256
|
-
// `{ includeImplicit: true }` opt-in einschalten.
|
|
255
|
+
// Implicit projections (auto-registered by r.entity, one per entity) are filtered
|
|
256
|
+
// out by default. They stay addressable as rebuild targets via
|
|
257
|
+
// `<feature>:projection:<entity>-entity`, but would dominate `kumiko project list`
|
|
258
|
+
// without adding information. Opt in with `{ includeImplicit: true }`.
|
|
257
259
|
export async function listProjectionsWithState(db, registry, options = {}) {
|
|
258
260
|
const projections = registry.getAllProjections();
|
|
259
261
|
const stateRows = await selectMany(db, projectionStateTable);
|
|
@@ -4,25 +4,26 @@ import { collectSearchableSubjectFields, configuredPiiSubjectKms, decryptPiiFiel
|
|
|
4
4
|
import { createTenantDb, entityTableFromRegistry, rehydrateCompoundTypes } from "../db/index.js";
|
|
5
5
|
import { tenantChannel } from "../engine/constants.js";
|
|
6
6
|
import { InternalError } from "../errors/index.js";
|
|
7
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
8
|
+
const log = createFallbackLogger("kumiko:search");
|
|
7
9
|
// --- Search Index Consumer (async, via event-dispatcher) ---
|
|
8
10
|
//
|
|
9
|
-
// Search
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// non-inline side-effects.
|
|
11
|
+
// Search indexing runs as an async EventConsumer through the event dispatcher,
|
|
12
|
+
// rather than as a synchronous postSave/postDelete hook. This mirrors Marten's
|
|
13
|
+
// ISubscription pattern: one async path for all non-inline side effects.
|
|
13
14
|
//
|
|
14
15
|
// Event → Search-Op Mapping:
|
|
15
16
|
//
|
|
16
17
|
// <entity>.created → index(tenantId, doc) // state = event.payload
|
|
17
18
|
// <entity>.updated → index(tenantId, doc) // state = { ...previous, ...changes }
|
|
18
19
|
// <entity>.restored → index(tenantId, doc) // state = event.payload.previous
|
|
19
|
-
// <entity>.deleted
|
|
20
|
-
// <entity>.forgotten
|
|
21
|
-
// <entity>.<named>
|
|
22
|
-
// projection row
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
20
|
+
// <entity>.deleted → remove(tenantId, type, id)
|
|
21
|
+
// <entity>.forgotten → remove(tenantId, type, id)
|
|
22
|
+
// <entity>.<named> → index or remove, state read from the live
|
|
23
|
+
// projection row — a named domain event carries
|
|
24
|
+
// only a payload slice, not the full entity, so
|
|
25
|
+
// there's no fixed shape to reconstruct state from
|
|
26
|
+
// as there is for created/updated/restored.
|
|
26
27
|
// Only queried when the entity declares searchable
|
|
27
28
|
// fields/extensions; no row (hard/soft-deleted) →
|
|
28
29
|
// remove, same as the deleted branch.
|
|
@@ -36,7 +37,7 @@ import { InternalError } from "../errors/index.js";
|
|
|
36
37
|
// batchHandler collapses a turn's events to one search-op per
|
|
37
38
|
// (tenantId, entityType, entityId) and issues one indexBatch/removeBatch
|
|
38
39
|
// call per tenant instead of one round-trip per event; handler stays the
|
|
39
|
-
// per-event fallback the dispatcher
|
|
40
|
+
// per-event fallback the dispatcher uses when a batch throws.
|
|
40
41
|
export const SEARCH_CONSUMER_NAME = "system:consumer:search";
|
|
41
42
|
// An executor update with includeDeleted leaves the row deleted; indexing it would make it findable again.
|
|
42
43
|
function isUpdateOfSoftDeletedRow(event, verb) {
|
|
@@ -58,7 +59,7 @@ async function resolveSearchOperation(event, ctx, registry) {
|
|
|
58
59
|
state = reconstructStateForSearch(event.payload, verb);
|
|
59
60
|
}
|
|
60
61
|
else {
|
|
61
|
-
//
|
|
62
|
+
// Named domain event: no fixed payload shape to reconstruct
|
|
62
63
|
// state from. Only worth a projection read when the entity actually
|
|
63
64
|
// declared searchable fields/extensions — same check
|
|
64
65
|
// buildSearchDocument makes below, done here first so a non-
|
|
@@ -68,7 +69,7 @@ async function resolveSearchOperation(event, ctx, registry) {
|
|
|
68
69
|
(registry.getSearchableFields(entityName).length > 0 ||
|
|
69
70
|
registry.getSearchPayloadExtensions(entityName).length > 0);
|
|
70
71
|
// skip: entity declares no searchable fields/extensions — nothing to
|
|
71
|
-
// index, stays a no-op with no extra query, same as before
|
|
72
|
+
// index, stays a no-op with no extra query, same as before.
|
|
72
73
|
if (!entity || !isSearchable)
|
|
73
74
|
return undefined;
|
|
74
75
|
const row = await readProjectionRowForSearch(ctx, registry, entityName, entity, tenantId, event.aggregateId);
|
|
@@ -183,7 +184,7 @@ export function createSearchEventConsumer(searchAdapter, registry) {
|
|
|
183
184
|
},
|
|
184
185
|
};
|
|
185
186
|
}
|
|
186
|
-
//
|
|
187
|
+
// Reads the live projection row for a named domain event, tenant-
|
|
187
188
|
// scoped through createTenantDb the same way MultiStreamProjection appliers
|
|
188
189
|
// scope their apply()-writes (see api/server.ts), on the table the registry
|
|
189
190
|
// actually booted (entityTableFromRegistry) rather than one freshly derived
|
|
@@ -217,7 +218,7 @@ async function readProjectionRowForSearch(ctx, registry, entityName, entity, ten
|
|
|
217
218
|
}
|
|
218
219
|
return state;
|
|
219
220
|
}
|
|
220
|
-
//
|
|
221
|
+
// Subject-annotated searchable fields are ciphertext in the event
|
|
221
222
|
// payload; decrypt into the derived index only. No KMS → omit ciphertext
|
|
222
223
|
// values rather than indexing blobs.
|
|
223
224
|
export async function decryptSearchableSubjectFields(entityName, state, registry) {
|
|
@@ -242,8 +243,8 @@ export async function decryptSearchableSubjectFields(entityName, state, registry
|
|
|
242
243
|
});
|
|
243
244
|
}
|
|
244
245
|
catch (err) {
|
|
245
|
-
|
|
246
|
-
`dropping ciphertext fields for this document instead of wedging the consumer.`, err);
|
|
246
|
+
log.warn(`decryptSearchableSubjectFields failed for "${entityName}" — ` +
|
|
247
|
+
`dropping ciphertext fields for this document instead of wedging the consumer.`, { err });
|
|
247
248
|
const out = { ...state };
|
|
248
249
|
for (const name of fields) {
|
|
249
250
|
if (isPiiCiphertext(out[name]))
|
|
@@ -295,7 +296,7 @@ function warnOncePerKeyCollision(registry, entityName, key, isBaseField) {
|
|
|
295
296
|
return;
|
|
296
297
|
warned.add(dedupKey);
|
|
297
298
|
const collidesWith = isBaseField ? `base field "${key}"` : `earlier contributor key "${key}"`;
|
|
298
|
-
|
|
299
|
+
log.warn(`searchPayloadExtension on "${entityName}" tried to overwrite ` +
|
|
299
300
|
`${collidesWith} — keeping the first value. Rename the contributor key.`);
|
|
300
301
|
}
|
|
301
302
|
// Build a SearchDocument from raw field-state. Parallel to the old
|
|
@@ -307,10 +308,9 @@ export async function buildSearchDocument(entityName, entityId, state, registry)
|
|
|
307
308
|
return null;
|
|
308
309
|
const searchableFields = registry.getSearchableFields(entityName);
|
|
309
310
|
const extensions = registry.getSearchPayloadExtensions(entityName);
|
|
310
|
-
// Skip
|
|
311
|
-
// Extensions
|
|
312
|
-
// (
|
|
313
|
-
// berücksichtigen.
|
|
311
|
+
// Skip guard: no indexable payload and no extensions means there is no doc.
|
|
312
|
+
// Extensions can populate the index without searchable base fields
|
|
313
|
+
// (for example, customFields-only indexing), so both must be checked.
|
|
314
314
|
if (searchableFields.length === 0 && extensions.length === 0)
|
|
315
315
|
return null;
|
|
316
316
|
const embeddedFields = new Set();
|
|
@@ -348,15 +348,15 @@ export async function buildSearchDocument(entityName, entityId, state, registry)
|
|
|
348
348
|
fields[f] = state[f];
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
|
-
//
|
|
351
|
+
// Search-Payload-Extensions: contributors merge flat fields into the
|
|
352
352
|
// search-doc (customFields-bundle / tags / computed-counts / etc.).
|
|
353
353
|
// Sequential await — extensions are expected sync or sub-millisecond async;
|
|
354
354
|
// sequential keeps the path simple and deterministic.
|
|
355
355
|
//
|
|
356
356
|
// Precedence is base-fields-win: a contributor key that collides with a
|
|
357
|
-
// searchable
|
|
358
|
-
// and warned. A jsonb custom-field that happens to share a
|
|
359
|
-
// must not shadow the indexed
|
|
357
|
+
// searchable base field is dropped (not silently merged over the real value)
|
|
358
|
+
// and warned. A jsonb custom-field that happens to share a base field name
|
|
359
|
+
// must not shadow the indexed base field.
|
|
360
360
|
const baseFieldKeys = new Set(Object.keys(fields));
|
|
361
361
|
for (const contribute of extensions) {
|
|
362
362
|
const contributed = await contribute({ entityName, entityId, state });
|
|
@@ -395,7 +395,7 @@ export const SSE_BROADCAST_CONSUMER_NAME = "system:consumer:sse-broadcast";
|
|
|
395
395
|
export function createSseBroadcastEventConsumer(sseBroker) {
|
|
396
396
|
return {
|
|
397
397
|
name: SSE_BROADCAST_CONSUMER_NAME,
|
|
398
|
-
// Shared delivery: one cursor reads every event exactly once
|
|
398
|
+
// Shared delivery: one cursor reads every event exactly once.
|
|
399
399
|
// Fanout to every process's own SSE clients happens below, through
|
|
400
400
|
// sseBroker.pushToChannel — a Redis-backed broker (the default once
|
|
401
401
|
// REDIS_URL is set) republishes across replicas, so every pod's
|
|
@@ -425,7 +425,7 @@ export function createSseBroadcastEventConsumer(sseBroker) {
|
|
|
425
425
|
// r.multiStreamProjection's ctx.unsafeAppendEvent, or a raw
|
|
426
426
|
// event-store-executor write (e.g. `files`' fileRef.created) — never
|
|
427
427
|
// reached it, so a job could never trigger on an r.defineEvent-registered
|
|
428
|
-
// event
|
|
428
|
+
// event.
|
|
429
429
|
//
|
|
430
430
|
// This consumer closes that gap the same way search/SSE do: read every
|
|
431
431
|
// committed event off the shared cursor and re-check job triggers. It is
|
|
@@ -467,10 +467,9 @@ export function createJobTriggerEventConsumer(jobRunner, registry) {
|
|
|
467
467
|
}
|
|
468
468
|
// --- Access-Invalidation Consumer (async, via event-dispatcher) ---
|
|
469
469
|
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
// pushes an invalidation through sseBroker.publishAccessInvalidation, which
|
|
470
|
+
// This consumer is the last leg: it watches for the two event types that
|
|
471
|
+
// can make an already-issued JWT stale mid-stream and pushes an invalidation
|
|
472
|
+
// through sseBroker.publishAccessInvalidation, which
|
|
474
473
|
// dispatch-stream.ts's subscribeAccessInvalidation listener turns into an
|
|
475
474
|
// AccessDeniedError thrown into the open stream.
|
|
476
475
|
//
|
|
@@ -499,21 +498,20 @@ export function createJobTriggerEventConsumer(jobRunner, registry) {
|
|
|
499
498
|
// payload.previous, the full pre-write entity snapshot written by
|
|
500
499
|
// createEventStoreExecutor. tenantMembershipEntity declares no
|
|
501
500
|
// encrypted/PII fields, so previous.userId is plaintext — no KMS
|
|
502
|
-
// decrypt step
|
|
503
|
-
//
|
|
501
|
+
// decrypt step; the original handoff assumed encryption that doesn't
|
|
502
|
+
// apply here.
|
|
504
503
|
//
|
|
505
504
|
// Over-invalidation (e.g. tenant-membership.updated fired by something
|
|
506
505
|
// other than a role change) is fail-safe: the stream closes, the client
|
|
507
|
-
// reconnects and re-authorizes. No opt-out/allowlist —
|
|
508
|
-
//
|
|
509
|
-
// handler.
|
|
506
|
+
// reconnects and re-authorizes. No opt-out/allowlist — global-by-default
|
|
507
|
+
// specifically so this can't be silently disabled per handler.
|
|
510
508
|
//
|
|
511
509
|
// Scope note: entityEventName also emits "tenant-membership.forgotten" (DSGVO
|
|
512
510
|
// erasure) and ".restored" — .restored re-grants access so ignoring it is
|
|
513
511
|
// correct, .forgotten is not currently produced by any user-data-rights
|
|
514
512
|
// pipeline for memberships but would be revocation-relevant if it ever is.
|
|
515
|
-
// Out of
|
|
516
|
-
//
|
|
513
|
+
// Out of the stated scope (session-revoke + role/membership change), tracked
|
|
514
|
+
// rather than handled speculatively here.
|
|
517
515
|
export const ACCESS_INVALIDATION_CONSUMER_NAME = "system:consumer:access-invalidation";
|
|
518
516
|
const SESSION_REVOKED_EVENT_TYPE = "sessions:event:session-revoked";
|
|
519
517
|
const PAT_REVOKED_EVENT_TYPE = "personal-access-tokens:event:pat-revoked";
|
|
@@ -557,7 +555,7 @@ function readPatRevokedScope(payload) {
|
|
|
557
555
|
export function createAccessInvalidationEventConsumer(sseBroker) {
|
|
558
556
|
return {
|
|
559
557
|
name: ACCESS_INVALIDATION_CONSUMER_NAME,
|
|
560
|
-
// Shared delivery, same reasoning as SSE broadcast
|
|
558
|
+
// Shared delivery, same reasoning as SSE broadcast: one
|
|
561
559
|
// cursor reads every event once and calls sseBroker.publishAccessInvalidation
|
|
562
560
|
// below, which a Redis-backed broker republishes to every replica's
|
|
563
561
|
// subscribeAccessInvalidation listeners — no per-instance cursor needed
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
-
import { withSystemDbUnsafeRawGrant, withUnsafeRawGrant
|
|
2
|
+
import { withSystemDbUnsafeRawGrant, withUnsafeRawGrant } from "../db/tenant-db.js";
|
|
3
3
|
import { SYSTEM_ROLE, SYSTEM_USER_ID } from "../engine/system-user.js";
|
|
4
4
|
import { AccessDeniedError, FrameworkReasons, InternalError } from "../errors/index.js";
|
|
5
5
|
export function isSystemIdentity(user) {
|
|
@@ -235,12 +235,10 @@ export function withHookEscapeHatchGrant(context, callerLabel, escapeHatch) {
|
|
|
235
235
|
}),
|
|
236
236
|
...gatedMemberReaderField(callerLabel, identityHatch, ctxQueryAsMember),
|
|
237
237
|
...gatedProjectionReaderField(callerLabel, identityHatch, ctxQueryProjection),
|
|
238
|
-
// @cast-boundary engine-bridge — withUnsafeRawGrant passes non-TenantDb values (e.g. a guard Proxy) through unchanged.
|
|
239
238
|
...(ctxDb && { db: withUnsafeRawGrant(ctxDb, rawHatch) }),
|
|
240
239
|
...(ctxDbOutsideTransaction && {
|
|
241
240
|
dbOutsideTransaction: withUnsafeRawGrant(ctxDbOutsideTransaction, rawHatch),
|
|
242
241
|
}),
|
|
243
|
-
// @cast-boundary engine-bridge — withSystemDbUnsafeRawGrant passes non-UncheckedSystemDb values through unchanged.
|
|
244
242
|
...(ctxSystemDb && {
|
|
245
243
|
systemDb: withSystemDbUnsafeRawGrant(ctxSystemDb, rawHatch, callerLabel),
|
|
246
244
|
}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Per-dispatcher in-process cache for the resolved tenant:config:timezone
|
|
2
|
-
// value
|
|
2
|
+
// value. dispatch-shared.ts's buildHandlerContext resolves that
|
|
3
3
|
// key on every dispatch to build ctx.tz — without a cache that's a config
|
|
4
4
|
// SELECT (sometimes two, cascade + fallback) per request, often inside an
|
|
5
5
|
// open write transaction. A tenant-keyed TTL cache removes the steady-state
|
|
@@ -1,56 +1,56 @@
|
|
|
1
1
|
export type AdjNounNameOptions = {
|
|
2
|
-
/**
|
|
2
|
+
/** Separator between adj/noun (and, if applicable, suffix). Default "-". */
|
|
3
3
|
readonly separator?: string;
|
|
4
|
-
/**
|
|
5
|
-
* (no-confusable
|
|
6
|
-
*
|
|
4
|
+
/** If set, appends a random suffix of length .length
|
|
5
|
+
* (no-confusable alphabet). 3 characters are recommended = 32^3 = 32,768
|
|
6
|
+
* additional combinations per word pair. */
|
|
7
7
|
readonly suffix?: {
|
|
8
8
|
readonly length: number;
|
|
9
9
|
};
|
|
10
10
|
/** Custom Adjective-Liste — default ADJECTIVES (191 generic). */
|
|
11
11
|
readonly adjectives?: readonly string[];
|
|
12
|
-
/** Custom
|
|
13
|
-
*
|
|
14
|
-
* -receiver/-listener
|
|
12
|
+
/** Custom noun list — default NOUNS (173 generic). Apps that want
|
|
13
|
+
* domain-specific slugs (e.g. webhook features with their own
|
|
14
|
+
* -receiver/-listener nouns) provide their own list. */
|
|
15
15
|
readonly nouns?: readonly string[];
|
|
16
16
|
};
|
|
17
|
-
/** Sync
|
|
18
|
-
* (
|
|
19
|
-
*
|
|
17
|
+
/** Sync generator: produces "happy-cloud" or "happy-cloud-k8x"
|
|
18
|
+
* (with suffix). No conflict check — the caller is responsible for
|
|
19
|
+
* uniqueness, or can use generateUniqueName() for the async variant. */
|
|
20
20
|
export declare function generateAdjNounName(options?: AdjNounNameOptions): string;
|
|
21
|
-
/** Random
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
/** Random string from the no-confusable alphabet. For suffixes after
|
|
22
|
+
* collisions or as standalone short IDs (e.g. webhook verification codes
|
|
23
|
+
* to be read aloud over the phone). length ≥ 1. */
|
|
24
24
|
export declare function generateNoConfusableId(length: number): string;
|
|
25
25
|
export type GenerateUniqueNameOptions = {
|
|
26
|
-
/** Caller
|
|
27
|
-
*
|
|
26
|
+
/** Caller predicate. Returns true if the name is not yet assigned
|
|
27
|
+
* (typically: DB query "select where slug=$1" → row count === 0). */
|
|
28
28
|
readonly isAvailable: (name: string) => Promise<boolean>;
|
|
29
|
-
/**
|
|
30
|
-
* Default 3.
|
|
31
|
-
*
|
|
29
|
+
/** Maximum attempts WITHOUT a suffix before switching to suffix mode.
|
|
30
|
+
* Default 3. With 33,043 default combinations and ~150 existing
|
|
31
|
+
* tenants, p(collision) < 1% — 3 attempts are more than sufficient. */
|
|
32
32
|
readonly maxCleanAttempts?: number;
|
|
33
|
-
/** Suffix
|
|
34
|
-
*
|
|
33
|
+
/** Suffix length in collision mode. Default 3 (= 32,768 combinations
|
|
34
|
+
* per word pair). */
|
|
35
35
|
readonly suffixLength?: number;
|
|
36
|
-
/** Hard
|
|
37
|
-
*
|
|
38
|
-
* isAvailable()
|
|
36
|
+
/** Hard cap on total attempts before giving up. Default 20.
|
|
37
|
+
* Rarely reached in practice — if it is, the word list may be empty or
|
|
38
|
+
* isAvailable() may return false consistently (DB bug). */
|
|
39
39
|
readonly maxTotalAttempts?: number;
|
|
40
40
|
readonly separator?: string;
|
|
41
41
|
readonly adjectives?: readonly string[];
|
|
42
42
|
readonly nouns?: readonly string[];
|
|
43
43
|
};
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
* Attempts.
|
|
47
|
-
*
|
|
44
|
+
/** Generates a unique Adj-Noun slug by trying up to maxCleanAttempts
|
|
45
|
+
* clean word pairs, then using a random suffix up to maxTotal-
|
|
46
|
+
* Attempts. Throws if that also fails (= the caller has a bug
|
|
47
|
+
* in isAvailable, or the word list is defective).
|
|
48
48
|
*
|
|
49
|
-
*
|
|
49
|
+
* Example:
|
|
50
50
|
* const slug = await generateUniqueName({
|
|
51
51
|
* isAvailable: async (s) =>
|
|
52
52
|
* !(await db.select().from(tenants).where(eq(tenants.tenantKey, s)).then(r => r.length > 0)),
|
|
53
53
|
* });
|
|
54
|
-
* // → "happy-cloud"
|
|
54
|
+
* // → "happy-cloud" or "happy-cloud-k8x" on collision
|
|
55
55
|
*/
|
|
56
56
|
export declare function generateUniqueName(options: GenerateUniqueNameOptions): Promise<string>;
|