@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
|
@@ -7,13 +7,17 @@ export declare function piiCiphertextSubject(value: string): SubjectId;
|
|
|
7
7
|
export declare function encryptPiiValueForSubject(kms: LocalKeyKmsAdapter, subject: SubjectId, value: string, kmsCtx: KmsContext, field: string): Promise<string>;
|
|
8
8
|
export declare function encryptPiiJsonValueForSubject(kms: LocalKeyKmsAdapter, subject: SubjectId, value: unknown, kmsCtx: KmsContext, field: string): Promise<string>;
|
|
9
9
|
export declare function decryptPiiValueForSubject(kms: LocalKeyKmsAdapter, value: string, kmsCtx: KmsContext, field: string): Promise<string>;
|
|
10
|
+
export declare const ErasedSubjectHandlings: {
|
|
11
|
+
readonly sentinel: "sentinel";
|
|
12
|
+
};
|
|
13
|
+
export type ErasedSubjectHandling = (typeof ErasedSubjectHandlings)[keyof typeof ErasedSubjectHandlings];
|
|
10
14
|
export interface EncryptPiiOptions {
|
|
11
15
|
readonly onlyKeys?: Iterable<string>;
|
|
12
16
|
readonly tenantId?: TenantId;
|
|
13
17
|
readonly subjectSource?: Record<string, unknown>;
|
|
14
18
|
readonly entityName: string;
|
|
15
19
|
readonly ownerFallback?: Record<string, unknown>;
|
|
16
|
-
readonly onErasedSubject?:
|
|
20
|
+
readonly onErasedSubject?: ErasedSubjectHandling;
|
|
17
21
|
}
|
|
18
22
|
export type PiiEncryptedRow<TRow extends Record<string, unknown>> = {
|
|
19
23
|
[K in keyof TRow]: TRow[K] extends string ? string : TRow[K] | string;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// PII field encryption with per-subject DEKs (crypto-shredding
|
|
1
|
+
// PII field encryption with per-subject DEKs (crypto-shredding).
|
|
2
2
|
// Same executor hook points as `encrypted: true`, but the key belongs to the
|
|
3
3
|
// erase subject — kms.eraseKey(subject) makes every value unreadable at once.
|
|
4
4
|
// Storage format is a sniffable string that fits existing text columns and
|
|
5
5
|
// names its subject inline, so decrypt needs no schema change and no resolver:
|
|
6
6
|
// kumiko-pii:v2:<subjectKey>:<base64(iv|tag|ciphertext)>
|
|
7
|
-
// v2 GCM-binds the ciphertext to `subjectKey|field` as AAD
|
|
7
|
+
// v2 GCM-binds the ciphertext to `subjectKey|field` as AAD — subject
|
|
8
8
|
// selects the DEK, field stops a cut-and-paste between two fields of the
|
|
9
9
|
// SAME subject (same DEK, so key selection alone can't catch that) from
|
|
10
|
-
// decrypting silently. v1 (no AAD) stays decrypt-only for
|
|
10
|
+
// decrypting silently. v1 (no AAD) stays decrypt-only for older rows;
|
|
11
11
|
// every new write emits v2.
|
|
12
12
|
// v3 = v2 for jsonb fields: the plaintext is JSON.stringify(value) and decrypt
|
|
13
13
|
// JSON.parses it back, so a jsonb string/number/object round-trips by type.
|
|
@@ -130,6 +130,7 @@ export async function decryptPiiValueForSubject(kms, value, kmsCtx, field) {
|
|
|
130
130
|
return PII_ERASED_SENTINEL;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
export const ErasedSubjectHandlings = { sentinel: "sentinel" };
|
|
133
134
|
export async function encryptPiiFieldValues(row, entity, piiFields, kms, kmsCtx, opts) {
|
|
134
135
|
if (piiFields.length === 0)
|
|
135
136
|
return row;
|
|
@@ -166,7 +167,8 @@ export async function encryptPiiFieldValues(row, entity, piiFields, kms, kmsCtx,
|
|
|
166
167
|
out[name] = encryptValue(subject, dek, plaintext, name, isJsonbField);
|
|
167
168
|
}
|
|
168
169
|
catch (e) {
|
|
169
|
-
if (!(e instanceof KeyErasedError) ||
|
|
170
|
+
if (!(e instanceof KeyErasedError) ||
|
|
171
|
+
opts.onErasedSubject !== ErasedSubjectHandlings.sentinel)
|
|
170
172
|
throw e;
|
|
171
173
|
out[name] = PII_ERASED_SENTINEL;
|
|
172
174
|
}
|
|
@@ -187,7 +189,7 @@ export async function decryptPiiFieldValues(row, piiFields, kms, kmsCtx) {
|
|
|
187
189
|
for (const name of piiFields) {
|
|
188
190
|
const value = out[name];
|
|
189
191
|
// Pre-engine plaintext rows pass through unchanged (mixed-state reads
|
|
190
|
-
// work during rollout
|
|
192
|
+
// work during rollout).
|
|
191
193
|
if (!isPiiCiphertext(value))
|
|
192
194
|
continue;
|
|
193
195
|
const { subject, blob, hasAad, isJson } = parseCiphertext(value);
|
|
@@ -33,5 +33,5 @@ export declare function isRecordKeyedField(field: EntityDefinition["fields"][str
|
|
|
33
33
|
export declare function collectPiiSubjectFields(entity: EntityDefinition): readonly string[];
|
|
34
34
|
export declare function collectPiiEncryptedFields(entity: EntityDefinition): readonly string[];
|
|
35
35
|
export declare function collectPiiFileReferenceFields(entity: EntityDefinition): readonly string[];
|
|
36
|
-
/** Subject-annotated fields that may be plaintext in the derived search index
|
|
36
|
+
/** Subject-annotated fields that may be plaintext in the derived search index. */
|
|
37
37
|
export declare function collectSearchableSubjectFields(entity: EntityDefinition): readonly string[];
|
|
@@ -107,7 +107,7 @@ function resolveEnvelopeRecordSubject(fieldName, envelope) {
|
|
|
107
107
|
// backfill catalog path (backfillEventPiiEncryption) — the reason the two
|
|
108
108
|
// can never encrypt the same field under different subjects. A "user"
|
|
109
109
|
// subject whose owner field is empty falls back to the declared whenAbsent
|
|
110
|
-
// stance and throws when none is declared
|
|
110
|
+
// stance and throws when none is declared; "tenant"/"self" throw
|
|
111
111
|
// on an empty envelope, since their facts are structural.
|
|
112
112
|
export function resolveEventSubject(fieldName, spec, payload, envelope) {
|
|
113
113
|
const normalized = normalizeEventPiiSubject(spec);
|
|
@@ -177,7 +177,7 @@ export function collectPiiFileReferenceFields(entity) {
|
|
|
177
177
|
.filter(([, field]) => hasSubjectAnnotation(field) && isFileReferenceField(field))
|
|
178
178
|
.map(([name]) => name);
|
|
179
179
|
}
|
|
180
|
-
/** Subject-annotated fields that may be plaintext in the derived search index
|
|
180
|
+
/** Subject-annotated fields that may be plaintext in the derived search index. */
|
|
181
181
|
export function collectSearchableSubjectFields(entity) {
|
|
182
182
|
return Object.entries(entity.fields)
|
|
183
183
|
.filter(([, field]) => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AggregateDimension, AggregateKey, AggregateSpec } from "@cosmicdrift/kumiko-types/aggregate-types";
|
|
2
|
+
import type { FieldDefinition } from "@cosmicdrift/kumiko-types/fields";
|
|
3
|
+
import type { ListAggregateMeasureKind, ListAggregateValue } from "@cosmicdrift/kumiko-types/list-aggregate-types";
|
|
4
|
+
import type { TableInfo } from "./table-info.ts";
|
|
5
|
+
export declare function isTimestamptzType(pgType: string | undefined): boolean;
|
|
6
|
+
export declare function aggregateKeyOf(raw: unknown): AggregateKey;
|
|
7
|
+
export type AggregateFunction = "count" | "countDistinct" | "sum" | "avg" | "min" | "max";
|
|
8
|
+
export declare function aggregateFunctionSql(fn: AggregateFunction, columnSql: string | undefined, filterSql?: string): string;
|
|
9
|
+
export declare function aggregateMeasureSql(info: TableInfo, measure: AggregateSpec["measure"]): string;
|
|
10
|
+
type FieldType = FieldDefinition["type"];
|
|
11
|
+
export declare function isListAggregateMeasureSupported(kind: ListAggregateMeasureKind, fieldType: FieldType): boolean;
|
|
12
|
+
export declare function isListAggregateGroupable(fieldType: FieldType, multiple: boolean): boolean;
|
|
13
|
+
export declare function listAggregateKeySql(fieldType: FieldType, columnSql: string, alias: string): string;
|
|
14
|
+
export declare function listAggregateMeasureSql(kind: ListAggregateMeasureKind, fieldType: FieldType, columnSql: string, filterSql?: string): string;
|
|
15
|
+
export declare function groupedKeyOrderLimitSql(limit: number): string;
|
|
16
|
+
export declare function decodeListAggregateValue(raw: unknown): ListAggregateValue;
|
|
17
|
+
export declare function emptyListAggregateValue(kind: ListAggregateMeasureKind): ListAggregateValue;
|
|
18
|
+
export declare function aggregateDimensionSql(info: TableInfo, dimension: AggregateDimension, index: number, params: unknown[]): string;
|
|
19
|
+
export declare function assertValidAggregateLimits(spec: AggregateSpec): void;
|
|
20
|
+
export declare function aggregateGroupOrderLimitSql(spec: AggregateSpec, dimensionCount: number): string;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { quoteIdent } from "../crypto/ciphertext-pattern.js";
|
|
2
|
+
const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
|
|
3
|
+
const AGGREGATE_ORDER_DIRECTIONS = ["asc", "desc"];
|
|
4
|
+
function requireAggregateColumn(info, field, role) {
|
|
5
|
+
if (!info.hasColumn(field)) {
|
|
6
|
+
throw new Error(`aggregateWhere: ${role} "${field}" is not a column of ${info.name}`);
|
|
7
|
+
}
|
|
8
|
+
return quoteIdent(info.columnOf(field));
|
|
9
|
+
}
|
|
10
|
+
export function isTimestamptzType(pgType) {
|
|
11
|
+
return pgType === "timestamptz" || pgType === "timestamptz(3)";
|
|
12
|
+
}
|
|
13
|
+
export function aggregateKeyOf(raw) {
|
|
14
|
+
if (raw === null || raw === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean")
|
|
17
|
+
return raw;
|
|
18
|
+
if (typeof raw === "bigint")
|
|
19
|
+
return Number(raw);
|
|
20
|
+
if (raw instanceof Date)
|
|
21
|
+
return raw.toISOString();
|
|
22
|
+
return String(raw);
|
|
23
|
+
}
|
|
24
|
+
// FILTER sits inside the aggregate so a row-dependent read clause restricts only
|
|
25
|
+
// this measure; COALESCE stays outside to keep an all-filtered SUM at 0.
|
|
26
|
+
export function aggregateFunctionSql(fn, columnSql, filterSql) {
|
|
27
|
+
const filter = filterSql === undefined ? "" : ` FILTER (WHERE ${filterSql})`;
|
|
28
|
+
const column = columnSql ?? "*";
|
|
29
|
+
switch (fn) {
|
|
30
|
+
case "count":
|
|
31
|
+
return `COUNT(${column})${filter}`;
|
|
32
|
+
case "countDistinct":
|
|
33
|
+
return `COUNT(DISTINCT ${column})${filter}`;
|
|
34
|
+
case "sum":
|
|
35
|
+
return `COALESCE(SUM(${column})${filter}, 0)`;
|
|
36
|
+
case "avg":
|
|
37
|
+
return `AVG(${column})${filter}`;
|
|
38
|
+
case "min":
|
|
39
|
+
return `MIN(${column})${filter}`;
|
|
40
|
+
case "max":
|
|
41
|
+
return `MAX(${column})${filter}`;
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`aggregateWhere: unknown aggregate fn "${String(fn)}"`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function aggregateMeasureSql(info, measure) {
|
|
47
|
+
if (measure.fn === "count")
|
|
48
|
+
return aggregateFunctionSql("count", undefined);
|
|
49
|
+
return aggregateFunctionSql(measure.fn, requireAggregateColumn(info, measure.field, "measure field"));
|
|
50
|
+
}
|
|
51
|
+
const NUMERIC_MEASURE_TYPES = ["number", "bigInt", "decimal", "money"];
|
|
52
|
+
const ORDERED_MEASURE_TYPES = [...NUMERIC_MEASURE_TYPES, "date", "timestamp"];
|
|
53
|
+
// Group keys must round-trip as a plain `field eq key` filter, which rules out
|
|
54
|
+
// composite, multi-valued, currency-paired and zone-paired types.
|
|
55
|
+
const GROUPABLE_TYPES = [
|
|
56
|
+
"text",
|
|
57
|
+
"longText",
|
|
58
|
+
"select",
|
|
59
|
+
"boolean",
|
|
60
|
+
"number",
|
|
61
|
+
"bigInt",
|
|
62
|
+
"decimal",
|
|
63
|
+
"date",
|
|
64
|
+
"timestamp",
|
|
65
|
+
];
|
|
66
|
+
export function isListAggregateMeasureSupported(kind, fieldType) {
|
|
67
|
+
if (kind === "count")
|
|
68
|
+
return true;
|
|
69
|
+
const allowed = kind === "sum" || kind === "avg" ? NUMERIC_MEASURE_TYPES : ORDERED_MEASURE_TYPES;
|
|
70
|
+
return allowed.includes(fieldType);
|
|
71
|
+
}
|
|
72
|
+
export function isListAggregateGroupable(fieldType, multiple) {
|
|
73
|
+
return fieldType === "reference" ? !multiple : GROUPABLE_TYPES.includes(fieldType);
|
|
74
|
+
}
|
|
75
|
+
export function listAggregateKeySql(fieldType, columnSql, alias) {
|
|
76
|
+
// A date column must not pass through a JS Date, so it travels as text.
|
|
77
|
+
return fieldType === "date" ? `${columnSql}::text AS ${alias}` : `${columnSql} AS ${alias}`;
|
|
78
|
+
}
|
|
79
|
+
const ISO_UTC_TIMESTAMP_FORMAT = `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'`;
|
|
80
|
+
// Numbers leave PG as float8 and dates/timestamps as text, so the driver never
|
|
81
|
+
// hands back a bigint or numeric string that JSON could not carry.
|
|
82
|
+
export function listAggregateMeasureSql(kind, fieldType, columnSql, filterSql) {
|
|
83
|
+
const fn = aggregateFunctionSql(kind, columnSql, filterSql);
|
|
84
|
+
if (kind === "count")
|
|
85
|
+
return `(${fn})::float8`;
|
|
86
|
+
if (kind === "min" || kind === "max") {
|
|
87
|
+
if (fieldType === "date")
|
|
88
|
+
return `(${fn})::text`;
|
|
89
|
+
if (fieldType === "timestamp") {
|
|
90
|
+
return `to_char((${fn}) AT TIME ZONE 'UTC', ${ISO_UTC_TIMESTAMP_FORMAT})`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Money is integer minor units, so an average rounds to one.
|
|
94
|
+
if (kind === "avg" && fieldType === "money")
|
|
95
|
+
return `ROUND(${fn})::float8`;
|
|
96
|
+
return `(${fn})::float8`;
|
|
97
|
+
}
|
|
98
|
+
export function groupedKeyOrderLimitSql(limit) {
|
|
99
|
+
return ` GROUP BY 1 ORDER BY 1 ASC NULLS LAST LIMIT ${limit}`;
|
|
100
|
+
}
|
|
101
|
+
export function decodeListAggregateValue(raw) {
|
|
102
|
+
if (raw === null || raw === undefined)
|
|
103
|
+
return null;
|
|
104
|
+
if (typeof raw === "number" || typeof raw === "string")
|
|
105
|
+
return raw;
|
|
106
|
+
if (typeof raw === "bigint")
|
|
107
|
+
return Number(raw);
|
|
108
|
+
if (raw instanceof Date)
|
|
109
|
+
return raw.toISOString();
|
|
110
|
+
return String(raw);
|
|
111
|
+
}
|
|
112
|
+
// What the measure reports over zero rows, mirroring the SQL (SUM coalesces, COUNT is 0).
|
|
113
|
+
export function emptyListAggregateValue(kind) {
|
|
114
|
+
return kind === "sum" || kind === "count" ? 0 : null;
|
|
115
|
+
}
|
|
116
|
+
export function aggregateDimensionSql(info, dimension, index, params) {
|
|
117
|
+
const column = requireAggregateColumn(info, dimension.field, "groupBy field");
|
|
118
|
+
if (!("bucket" in dimension))
|
|
119
|
+
return `${column} AS k${index}`;
|
|
120
|
+
if (!AGGREGATE_BUCKET_UNITS.includes(dimension.bucket)) {
|
|
121
|
+
throw new Error(`aggregateWhere: unknown bucket "${String(dimension.bucket)}"`);
|
|
122
|
+
}
|
|
123
|
+
if (!isTimestamptzType(info.pgTypeOf(info.columnOf(dimension.field)))) {
|
|
124
|
+
throw new Error(`aggregateWhere: bucket on "${dimension.field}" requires a timestamptz column`);
|
|
125
|
+
}
|
|
126
|
+
params.push(dimension.timeZone);
|
|
127
|
+
return `(EXTRACT(EPOCH FROM date_trunc('${dimension.bucket}', ${column}, $${params.length}::text)) * 1000)::float8 AS k${index}`;
|
|
128
|
+
}
|
|
129
|
+
export function assertValidAggregateLimits(spec) {
|
|
130
|
+
if (spec.limit !== undefined && (!Number.isInteger(spec.limit) || spec.limit < 0)) {
|
|
131
|
+
throw new Error(`aggregateWhere: limit must be a non-negative integer, got ${spec.limit}`);
|
|
132
|
+
}
|
|
133
|
+
if (spec.orderByValue !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByValue)) {
|
|
134
|
+
throw new Error(`aggregateWhere: unknown orderByValue "${String(spec.orderByValue)}"`);
|
|
135
|
+
}
|
|
136
|
+
if (spec.orderByKeys !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByKeys)) {
|
|
137
|
+
throw new Error(`aggregateWhere: unknown orderByKeys "${String(spec.orderByKeys)}"`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function aggregateGroupOrderLimitSql(spec, dimensionCount) {
|
|
141
|
+
let sqlText = "";
|
|
142
|
+
if (dimensionCount > 0) {
|
|
143
|
+
const positions = Array.from({ length: dimensionCount }, (_, index) => String(index + 1));
|
|
144
|
+
sqlText += ` GROUP BY ${positions.join(", ")}`;
|
|
145
|
+
const keyDirection = spec.orderByKeys === "desc" ? "DESC" : "ASC";
|
|
146
|
+
const keyParts = positions.map((position) => `${position} ${keyDirection}`);
|
|
147
|
+
const orderParts = spec.orderByValue === undefined
|
|
148
|
+
? keyParts
|
|
149
|
+
: [`${dimensionCount + 1} ${spec.orderByValue === "desc" ? "DESC" : "ASC"}`, ...keyParts];
|
|
150
|
+
sqlText += ` ORDER BY ${orderParts.join(", ")}`;
|
|
151
|
+
}
|
|
152
|
+
if (spec.limit !== undefined)
|
|
153
|
+
sqlText += ` LIMIT ${spec.limit}`;
|
|
154
|
+
return sqlText;
|
|
155
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { EntityDefinition } from "../engine/types/index.ts";
|
|
|
3
3
|
import type { StoredEvent } from "../event-store/index.ts";
|
|
4
4
|
import type { DbRow } from "./connection.ts";
|
|
5
5
|
import type { TableColumns } from "./dialect.ts";
|
|
6
|
-
type Table = TableColumns
|
|
6
|
+
type Table = TableColumns;
|
|
7
7
|
export type AutoVerb = "created" | "updated" | "deleted" | "restored" | "forgotten";
|
|
8
8
|
export type ApplyResult = {
|
|
9
9
|
readonly kind: "applied";
|
|
@@ -12,11 +12,11 @@ export type ApplyResult = {
|
|
|
12
12
|
} | {
|
|
13
13
|
readonly kind: "skipped";
|
|
14
14
|
};
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
15
|
+
/** Parses event.type into an AutoVerb when the event is an auto-verb
|
|
16
|
+
* for the given aggregate; otherwise returns null (a domain event). */
|
|
17
17
|
export declare function parseAutoVerb(event: StoredEvent): AutoVerb | null;
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
/** Applies an auto-event idempotently to the entity table.
|
|
19
|
+
* Called both during live append (inside the write TX) and during rebuild
|
|
20
|
+
* (inside the rebuild TX), using identical logic. */
|
|
21
21
|
export declare function applyEntityEvent(event: StoredEvent, table: Table, entity: EntityDefinition, tx: ExecutorManagedRunner): Promise<ApplyResult>;
|
|
22
22
|
export {};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// applyEntityEvent —
|
|
2
|
-
//
|
|
1
|
+
// applyEntityEvent — the ONLY write logic for r.entity tables from
|
|
2
|
+
// StoredEvents. Both callers use it:
|
|
3
3
|
//
|
|
4
|
-
// - createEventStoreExecutor (live,
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// - rebuildProjection via ImplicitProjection (replay,
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// - createEventStoreExecutor (live, in the write TX) passes the just-appended
|
|
5
|
+
// StoredEvent directly; sensitive fields are table ciphertext in the payload
|
|
6
|
+
// (validated at boot as pii/encrypted).
|
|
7
|
+
// - rebuildProjection via ImplicitProjection (replay, in the rebuild TX)
|
|
8
|
+
// passes the same StoredEvent; apply copies ciphertext byte-for-byte,
|
|
9
|
+
// while bidx is recalculated below.
|
|
10
10
|
//
|
|
11
|
-
// Live==Rebuild
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// (DEK erased →
|
|
11
|
+
// Live==Rebuild equivalence therefore holds by construction for ALL fields —
|
|
12
|
+
// changed write logic needs to be maintained in only ONE place, with no sync
|
|
13
|
+
// contract. The only legitimate divergence is crypto-shredding
|
|
14
|
+
// (DEK erased → value unreadable, bidx NULL). Load-bearing test:
|
|
15
15
|
// db/__tests__/implicit-projection-equivalence.integration.test.ts.
|
|
16
16
|
//
|
|
17
17
|
// Tenant isolation: applyEntityEvent expects an ExecutorManagedRunner (tx or pool), NOT
|
|
@@ -23,26 +23,27 @@
|
|
|
23
23
|
// the TenantDb wrapper default.
|
|
24
24
|
// The missing TenantDb wrapper in this function is therefore not a hole.
|
|
25
25
|
//
|
|
26
|
-
// Auto-
|
|
26
|
+
// Auto-verbs:
|
|
27
27
|
// <entity>.created → INSERT
|
|
28
28
|
// <entity>.updated → UPDATE WHERE id=aggregateId
|
|
29
|
-
// <entity>.deleted → soft-delete-UPDATE
|
|
30
|
-
// <entity>.restored → undelete-UPDATE (
|
|
31
|
-
// <entity>.forgotten → hard-DELETE
|
|
32
|
-
// via executor.forget(). Rebuild
|
|
33
|
-
// →
|
|
29
|
+
// <entity>.deleted → soft-delete-UPDATE if entity.softDelete, otherwise hard-DELETE
|
|
30
|
+
// <entity>.restored → undelete-UPDATE (meaningful only for softDelete)
|
|
31
|
+
// <entity>.forgotten → always hard-DELETE (Art.17 purge, including softDelete);
|
|
32
|
+
// via executor.forget(). Rebuild replays created→forgotten
|
|
33
|
+
// → the row is gone, making this rebuild-safe (unlike a
|
|
34
|
+
// direct deleteMany).
|
|
34
35
|
//
|
|
35
|
-
// Domain
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
36
|
+
// Domain events (r.defineEvent) on the same aggregate are NOT handled here —
|
|
37
|
+
// they never went through the Executor in the live path and must be handled by
|
|
38
|
+
// explicit r.projection-apply handlers or r.multiStreamProjection.
|
|
39
|
+
// ImplicitProjection registers the auto-verbs.
|
|
39
40
|
//
|
|
40
|
-
// Return
|
|
41
|
-
// - "applied" →
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// - "skipped" →
|
|
45
|
-
//
|
|
41
|
+
// Return shape: ApplyResult with `kind` and an optional `row`.
|
|
42
|
+
// - "applied" → the write completed. `row` contains the written row for
|
|
43
|
+
// create/update/soft-delete/restore. For hard-delete, `row` is null
|
|
44
|
+
// (DELETE statements do not provide a returning row).
|
|
45
|
+
// - "skipped" → the event is not an auto-verb (a domain event on the same
|
|
46
|
+
// aggregate). Caller no-op.
|
|
46
47
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
47
48
|
import { requestContext } from "../api/request-context.js";
|
|
48
49
|
import { collectLookupableFields, computeBlindIndexValues } from "../crypto/blind-index.js";
|
|
@@ -51,8 +52,8 @@ import { collectPiiFileReferenceFields } from "../crypto/subject-resolver.js";
|
|
|
51
52
|
import { deleteMany, insertOne, updateMany } from "../db/query.js";
|
|
52
53
|
import { InternalError } from "../errors/index.js";
|
|
53
54
|
const AUTO_VERBS = ["created", "updated", "deleted", "restored", "forgotten"];
|
|
54
|
-
/**
|
|
55
|
-
*
|
|
55
|
+
/** Parses event.type into an AutoVerb when the event is an auto-verb
|
|
56
|
+
* for the given aggregate; otherwise returns null (a domain event). */
|
|
56
57
|
export function parseAutoVerb(event) {
|
|
57
58
|
const prefix = `${event.aggregateType}.`;
|
|
58
59
|
if (!event.type.startsWith(prefix))
|
|
@@ -81,9 +82,9 @@ async function decryptFileReferenceCiphertext(entity, values) {
|
|
|
81
82
|
}
|
|
82
83
|
return decrypted;
|
|
83
84
|
}
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
85
|
+
/** Applies an auto-event idempotently to the entity table.
|
|
86
|
+
* Called both during live append (inside the write TX) and during rebuild
|
|
87
|
+
* (inside the rebuild TX), using identical logic. */
|
|
87
88
|
export async function applyEntityEvent(event, table, entity, tx) {
|
|
88
89
|
const verb = parseAutoVerb(event);
|
|
89
90
|
if (verb === null)
|
|
@@ -91,17 +92,17 @@ export async function applyEntityEvent(event, table, entity, tx) {
|
|
|
91
92
|
const softDelete = entity.softDelete ?? false;
|
|
92
93
|
switch (verb) {
|
|
93
94
|
case "created": {
|
|
94
|
-
// tenantId
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
95
|
+
// Resolve tenantId explicitly rather than through spread order:
|
|
96
|
+
// the live path uses tx=tenantDbRunner(db) (with no TenantDb wrapper
|
|
97
|
+
// auto-injection), and replay has no wrapper either. The default is
|
|
98
|
+
// event.tenantId; the payload wins ONLY when it is a non-empty string
|
|
99
|
+
// (seedTenantMembership path: the operator writes in the target tenant,
|
|
100
|
+
// while the event belongs to the operator tenant).
|
|
100
101
|
//
|
|
101
|
-
// Fail
|
|
102
|
-
// non-string):
|
|
103
|
-
//
|
|
104
|
-
//
|
|
102
|
+
// Fail loudly when payload.tenantId is set but invalid (empty/null/
|
|
103
|
+
// non-string): this is critical for tenant isolation. Silently falling
|
|
104
|
+
// back to event.tenantId would write a bad payload into the operator
|
|
105
|
+
// tenant and cause cross-tenant data drift.
|
|
105
106
|
const payloadTenantId = event.payload["tenantId"];
|
|
106
107
|
let tenantId;
|
|
107
108
|
if (payloadTenantId === undefined) {
|
|
@@ -117,10 +118,10 @@ export async function applyEntityEvent(event, table, entity, tx) {
|
|
|
117
118
|
}
|
|
118
119
|
const row = await insertOne(tx, table, {
|
|
119
120
|
...(await decryptFileReferenceCiphertext(entity, event.payload)),
|
|
120
|
-
//
|
|
121
|
-
// HMAC, plaintext → HMAC, erased → NULL).
|
|
122
|
-
//
|
|
123
|
-
//
|
|
121
|
+
// Compute blind-index columns from the payload value (ciphertext → decrypt →
|
|
122
|
+
// HMAC, plaintext → HMAC, erased → NULL). This is done here rather
|
|
123
|
+
// than in the Executor so live writes and rebuilds produce the same
|
|
124
|
+
// value and the deterministic HMAC never enters the event log.
|
|
124
125
|
...(await computeBlindIndexValues(event.payload, collectLookupableFields(entity))),
|
|
125
126
|
tenantId,
|
|
126
127
|
id: event.aggregateId,
|
|
@@ -131,7 +132,7 @@ export async function applyEntityEvent(event, table, entity, tx) {
|
|
|
131
132
|
return { kind: "applied", verb, row: row ?? null };
|
|
132
133
|
}
|
|
133
134
|
case "updated": {
|
|
134
|
-
//
|
|
135
|
+
// Payload shape: { changes, previous } — see event-store-executor.ts.
|
|
135
136
|
const rawChanges = (event.payload["changes"] ?? {}); // @cast-boundary engine-payload
|
|
136
137
|
const changes = {};
|
|
137
138
|
for (const [key, value] of Object.entries(await decryptFileReferenceCiphertext(entity, rawChanges))) {
|
|
@@ -146,7 +147,7 @@ export async function applyEntityEvent(event, table, entity, tx) {
|
|
|
146
147
|
}
|
|
147
148
|
const rows = await updateMany(tx, table, {
|
|
148
149
|
...changes,
|
|
149
|
-
// bidx
|
|
150
|
+
// Recompute bidx only for the lookupable fields that changed (see created).
|
|
150
151
|
...(await computeBlindIndexValues(changes, collectLookupableFields(entity))),
|
|
151
152
|
version: event.version,
|
|
152
153
|
modifiedAt: event.createdAt,
|
|
@@ -166,16 +167,15 @@ export async function applyEntityEvent(event, table, entity, tx) {
|
|
|
166
167
|
}, { id: event.aggregateId });
|
|
167
168
|
return { kind: "applied", verb, row: rows[0] ?? null };
|
|
168
169
|
}
|
|
169
|
-
// Hard-
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
// nicht weiter.
|
|
170
|
+
// Hard-delete: the DELETE statement provides no returning row, and
|
|
171
|
+
// the live path already uses `existing` (the pre-delete snapshot) for
|
|
172
|
+
// the response. During replay, the caller does not need the row.
|
|
173
173
|
await deleteMany(tx, table, { id: event.aggregateId });
|
|
174
174
|
return { kind: "applied", verb, row: null };
|
|
175
175
|
}
|
|
176
176
|
case "restored": {
|
|
177
|
-
// Restore
|
|
178
|
-
//
|
|
177
|
+
// Restore is meaningful only for softDelete. Hard-delete entities should
|
|
178
|
+
// not receive restored events; if they do, defensively skip them.
|
|
179
179
|
if (!softDelete)
|
|
180
180
|
return { kind: "skipped" };
|
|
181
181
|
const rows = await updateMany(tx, table, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Immediate blind-index nulling after a subject erase
|
|
1
|
+
// Immediate blind-index nulling after a subject erase.
|
|
2
2
|
//
|
|
3
3
|
// After kms.eraseKey the ciphertext is unreadable, but the deterministic
|
|
4
4
|
// bidx column would stay matchable until the next write/rebuild — a
|
|
@@ -33,9 +33,9 @@ export async function nullBlindIndexesForSubject(db, features, subjectKey) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
// Skip entities whose projection was never migrated — same class as
|
|
36
|
-
// subjectRowExistsInTenant
|
|
36
|
+
// subjectRowExistsInTenant. A throw here runs AFTER eraseKey
|
|
37
37
|
// in forget-subject and would leave deterministic bidx columns still
|
|
38
|
-
// linkable
|
|
38
|
+
// linkable. One batched existence query instead of one
|
|
39
39
|
// roundtrip per entity.
|
|
40
40
|
const existing = await existingTableNames(db, candidates.map((c) => c.tableName));
|
|
41
41
|
for (const { tableName, lookupable } of candidates) {
|
|
@@ -59,7 +59,7 @@ async function existingTableNames(db, tableNames) {
|
|
|
59
59
|
const rows = await executeRawQueryRead(db, `SELECT t.name FROM unnest($1::text[]) AS t(name) WHERE to_regclass('public.' || quote_ident(t.name)) IS NOT NULL`, [toPostgresArrayLiteral(tableNames)]);
|
|
60
60
|
return new Set(rows.map((r) => r.name));
|
|
61
61
|
}
|
|
62
|
-
// Tenant-scope oracle for crypto-shredding's forget-subject
|
|
62
|
+
// Tenant-scope oracle for crypto-shredding's forget-subject: a
|
|
63
63
|
// "user"-kind subject id is often not a real user (share-token recipient,
|
|
64
64
|
// email subscriber, ...) — those entities self-own their PII (`pii: true`,
|
|
65
65
|
// i.e. their own row id IS the subject) and carry a real tenant_id,
|
|
@@ -71,14 +71,14 @@ async function existingTableNames(db, tableNames) {
|
|
|
71
71
|
// Invariant: `id` must never be client-settable on self-PII entities — the
|
|
72
72
|
// framework create path strips client ids; app write paths MUST do the same
|
|
73
73
|
// or a DPO could plant a foreign subject id in their tenant and pass this
|
|
74
|
-
// oracle
|
|
74
|
+
// oracle. Upgrade path: require an event-store provenance check
|
|
75
75
|
// (`aggregate_id = subjectId` under the actor tenant) when apps need
|
|
76
76
|
// client-supplied ids.
|
|
77
77
|
export async function subjectRowExistsInTenant(db, features, subjectId, tenantId) {
|
|
78
78
|
// Prefetch which self-PII projection tables actually exist — probing a
|
|
79
79
|
// missing relation used to throw (and get swallowed), which both hid
|
|
80
80
|
// real schema bugs and risked poisoning the driver connection for the
|
|
81
|
-
// rest of the forget TX
|
|
81
|
+
// rest of the forget TX.
|
|
82
82
|
const candidateTables = [];
|
|
83
83
|
for (const feature of features.values()) {
|
|
84
84
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
@@ -121,7 +121,7 @@ function findEntityByExactName(features, entityName) {
|
|
|
121
121
|
//
|
|
122
122
|
// Invariant: sound only because `id` is a single-column PK per entity
|
|
123
123
|
// table (globally unique, unspoofable via a client-supplied explicitId
|
|
124
|
-
// at create) — a composite PK or shared table would break this
|
|
124
|
+
// at create) — a composite PK or shared table would break this.
|
|
125
125
|
export async function recordRowExistsInTenant(db, features, entityName, recordId, tenantId) {
|
|
126
126
|
const entity = findEntityByExactName(features, entityName);
|
|
127
127
|
if (!entity)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function isClosedConnectionError(err: unknown): boolean;
|
|
2
|
+
export declare function isPooledClient(raw: unknown): boolean;
|
|
3
|
+
export declare function describeClosedConnectionFailure(raw: unknown, err: unknown, attempts: number): Record<string, unknown> | undefined;
|
|
4
|
+
export type ClosedConnectionContext = {
|
|
5
|
+
readonly operation: string;
|
|
6
|
+
readonly table?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function logClosedConnection(raw: unknown, err: unknown, attempts: number, context?: ClosedConnectionContext): void;
|
|
9
|
+
export declare function poolMaxOf(raw: unknown): number;
|