@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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 +9 -1
- 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/golden-path.md +13 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +55 -2
- package/agents/examples/list-table-options.md +466 -0
- package/agents/examples/pdf-audit-export.md +4 -2
- 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/state-machine.md +3 -2
- package/agents/examples/tenant-isolation.md +73 -1
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/examples.md +1 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +2 -0
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +2 -0
- package/dist/api/auth-routes-shared.d.ts +2 -0
- package/dist/api/auth-routes-shared.js +5 -0
- package/dist/api/client-error-route.js +10 -5
- package/dist/api/entity-read-access.d.ts +2 -1
- package/dist/api/entity-read-access.js +14 -0
- package/dist/api/export/export-cells-reference.d.ts +9 -0
- package/dist/api/export/export-cells-reference.js +21 -0
- package/dist/api/export/export-cells.d.ts +11 -0
- package/dist/api/export/export-cells.js +136 -0
- package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
- package/dist/api/export/export-ciphertext-guard.js +27 -0
- package/dist/api/export/export-columns.d.ts +29 -0
- package/dist/api/export/export-columns.js +85 -0
- package/dist/api/export/export-errors.d.ts +4 -0
- package/dist/api/export/export-errors.js +14 -0
- package/dist/api/export/export-route.d.ts +4 -0
- package/dist/api/export/export-route.js +74 -0
- package/dist/api/export/export-rows.d.ts +13 -0
- package/dist/api/export/export-rows.js +62 -0
- package/dist/api/export/export-screen.d.ts +12 -0
- package/dist/api/export/export-screen.js +47 -0
- package/dist/api/export/export-xlsx-writer.d.ts +4 -0
- package/dist/api/export/export-xlsx-writer.js +21 -0
- package/dist/api/extra-route.d.ts +4 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.d.ts +3 -0
- package/dist/api/pii-leak-guard.js +3 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.d.ts +16 -0
- 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 +206 -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 +299 -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 +238 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.d.ts +2 -1
- package/dist/api/sse-route.js +27 -13
- package/dist/config/index.d.ts +3 -1
- 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.d.ts +1 -0
- package/dist/crypto/pg-kms-adapter.js +12 -18
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +23 -0
- package/dist/db/aggregate-sql.js +165 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +88 -0
- package/dist/db/connection.d.ts +4 -5
- package/dist/db/connection.js +29 -20
- package/dist/db/dialect.js +1 -1
- 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 +101 -0
- 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.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
- package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +143 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +206 -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 +48 -701
- package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
- package/dist/db/event-store-executor-write-atomic.js +41 -0
- package/dist/db/event-store-executor-write-steps.d.ts +17 -0
- package/dist/db/event-store-executor-write-steps.js +95 -0
- package/dist/db/event-store-executor-write.js +405 -509
- package/dist/db/event-store-executor.js +2 -1
- package/dist/db/executor-managed-tables.js +1 -1
- package/dist/db/index.d.ts +7 -3
- package/dist/db/index.js +6 -2
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +7 -4
- package/dist/db/pg-error.js +26 -21
- package/dist/db/queries/backfill-pii.js +3 -6
- package/dist/db/queries/database-now.js +2 -1
- package/dist/db/queries/shadow-swap.js +2 -3
- package/dist/db/query.d.ts +16 -15
- package/dist/db/query.js +76 -804
- package/dist/db/reference-data.js +19 -2
- package/dist/db/statement-timeout.js +2 -1
- package/dist/db/table-builder.js +113 -166
- 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-upsert.d.ts +5 -0
- package/dist/db/tenant-db-upsert.js +91 -0
- package/dist/db/tenant-db.js +50 -10
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
- package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
- package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
- package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
- package/dist/engine/boot-validator/list-column-layout.js +41 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
- package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-export-options.js +27 -0
- package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
- package/dist/engine/boot-validator/list-table-options.js +77 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +3 -13
- package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
- package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- 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 +235 -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 +56 -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 +24 -11
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.d.ts +2 -1
- package/dist/engine/build-app-schema.js +65 -97
- package/dist/engine/build-config-feature-schema.js +6 -1
- package/dist/engine/define-feature.js +13 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round1.js +6 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +2 -0
- package/dist/engine/feature-ast/patch.js +5 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
- package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
- package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
- package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
- package/dist/engine/feature-ast/patterns.d.ts +14 -1
- package/dist/engine/feature-ast/patterns.js +44 -51
- package/dist/engine/feature-ast/render.js +52 -87
- package/dist/engine/feature-builder-state.d.ts +1 -0
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/field-access.d.ts +1 -0
- package/dist/engine/field-access.js +12 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +9 -0
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-filter-ops.js +2 -0
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
- package/dist/engine/types/feature.d.ts +1 -0
- package/dist/engine/types/index.d.ts +4 -3
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +6 -1
- package/dist/entrypoint/index.js +10 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +17 -8
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +7 -1
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +11 -2
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.d.ts +2 -0
- package/dist/files/file-routes.js +40 -10
- package/dist/files/provider-resolver.d.ts +1 -0
- package/dist/files/provider-resolver.js +1 -1
- package/dist/files/record-write-check.d.ts +6 -0
- package/dist/files/record-write-check.js +13 -0
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/files/upload-ledger.js +11 -4
- package/dist/i18n/required-surface-keys.js +189 -208
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/internal/testing.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 +61 -2
- 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 +50 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +21 -3
- package/dist/pipeline/after-rollback-sink.d.ts +6 -0
- package/dist/pipeline/after-rollback-sink.js +32 -0
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +8 -1
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +62 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +6 -6
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +6 -9
- 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 +23 -0
- package/dist/pipeline/handler-context-dispatch.js +101 -0
- package/dist/pipeline/handler-context-events.d.ts +41 -0
- package/dist/pipeline/handler-context-events.js +239 -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/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 +2 -1
- package/dist/pipeline/system-hooks.d.ts +3 -1
- package/dist/pipeline/system-hooks.js +55 -6
- package/dist/pipeline/write-result-masking.js +13 -10
- 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 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/reindex-entity.js +15 -5
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-changelog.d.ts +2 -0
- package/dist/upgrade-changelog.js +14 -0
- package/dist/upgrade-cli.d.ts +3 -12
- package/dist/upgrade-cli.js +171 -197
- package/dist/upgrade-marker-parse.d.ts +32 -0
- package/dist/upgrade-marker-parse.js +73 -0
- package/package.json +9 -6
- package/src/changes.json +555 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +10 -2
- 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 +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/dist/engine/provider-plugin-context.d.ts +0 -3
- package/dist/engine/provider-plugin-context.js +0 -28
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { hasAccess } from "../../engine/access.js";
|
|
2
|
+
import { toKebab } from "../../engine/qualified-name.js";
|
|
3
|
+
import { DEFAULT_EXPORT_MAX_ROWS } from "../../engine/types/screen.js";
|
|
4
|
+
import { AccessDeniedError } from "../../errors/index.js";
|
|
5
|
+
import { resolveExportColumns } from "./export-columns.js";
|
|
6
|
+
import { exportRejection } from "./export-errors.js";
|
|
7
|
+
const SCREEN_QN = /^[A-Za-z0-9_-]+:screen:[A-Za-z0-9_-]+$/;
|
|
8
|
+
function findExportScreen(registry, screenQn) {
|
|
9
|
+
// The client sends the feature name as authored (camelCase allowed); the registry keys are kebab.
|
|
10
|
+
const registryQn = toKebab(screenQn);
|
|
11
|
+
const screen = SCREEN_QN.test(screenQn) ? registry.getScreen(registryQn) : undefined;
|
|
12
|
+
if (screen === undefined || (screen.type !== "entityList" && screen.type !== "projectionList")) {
|
|
13
|
+
throw exportRejection("export_unknown_screen", "screen");
|
|
14
|
+
}
|
|
15
|
+
return { registryQn, screen };
|
|
16
|
+
}
|
|
17
|
+
export function resolveExportPlan(registry, screenQn, requestedColumns, user) {
|
|
18
|
+
const { registryQn, screen } = findExportScreen(registry, screenQn);
|
|
19
|
+
if (screen.table?.export?.xlsx !== true)
|
|
20
|
+
throw exportRejection("export_not_enabled", "screen");
|
|
21
|
+
if (screen.access !== undefined && !hasAccess(user, screen.access)) {
|
|
22
|
+
throw new AccessDeniedError({ details: { screen: screenQn } });
|
|
23
|
+
}
|
|
24
|
+
// Label keys use the feature name as authored, query types the kebab form.
|
|
25
|
+
const featureName = registry.getScreenFeature(registryQn) ?? "";
|
|
26
|
+
const screenShortId = registryQn.split(":")[2] ?? "";
|
|
27
|
+
const maxRows = screen.table.export.maxRows ?? DEFAULT_EXPORT_MAX_ROWS;
|
|
28
|
+
if (screen.type === "projectionList") {
|
|
29
|
+
return {
|
|
30
|
+
screenShortId,
|
|
31
|
+
queryType: screen.query,
|
|
32
|
+
screenFilter: screen.filter,
|
|
33
|
+
columns: resolveExportColumns({ featureName, columns: screen.columns, entity: undefined, entityName: undefined, user }, requestedColumns),
|
|
34
|
+
maxRows,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const entity = registry.getEntity(screen.entity);
|
|
38
|
+
if (entity === undefined)
|
|
39
|
+
throw exportRejection("export_unknown_screen", "screen");
|
|
40
|
+
return {
|
|
41
|
+
screenShortId,
|
|
42
|
+
queryType: `${toKebab(featureName)}:query:${toKebab(screen.entity)}:list`,
|
|
43
|
+
screenFilter: screen.filter,
|
|
44
|
+
columns: resolveExportColumns({ featureName, columns: screen.columns, entity, entityName: screen.entity, user }, requestedColumns),
|
|
45
|
+
maxRows,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Translate } from "./export-cells.ts";
|
|
2
|
+
import type { ExportColumn } from "./export-columns.ts";
|
|
3
|
+
import type { ExportRow } from "./export-rows.ts";
|
|
4
|
+
export declare function writeExportWorkbook(columns: readonly ExportColumn[], rows: readonly ExportRow[], translate: Translate, timeZone: string): Promise<Uint8Array<ArrayBuffer>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import writeXlsxFile, {} from "write-excel-file/node";
|
|
2
|
+
import { toExportCell } from "./export-cells.js";
|
|
3
|
+
import { guardSheetAgainstCiphertext } from "./export-ciphertext-guard.js";
|
|
4
|
+
function headerLabel(column, translate) {
|
|
5
|
+
const translated = translate(column.headerKey);
|
|
6
|
+
return translated === column.headerKey ? column.headerFallback : translated;
|
|
7
|
+
}
|
|
8
|
+
export async function writeExportWorkbook(columns, rows, translate, timeZone) {
|
|
9
|
+
const header = columns.map((column) => ({
|
|
10
|
+
value: headerLabel(column, translate),
|
|
11
|
+
type: String,
|
|
12
|
+
fontWeight: "bold",
|
|
13
|
+
}));
|
|
14
|
+
const body = rows.map((row) => columns.map((column) => toExportCell(column, { row, translate, timeZone })));
|
|
15
|
+
const sheetData = guardSheetAgainstCiphertext([header, ...body]);
|
|
16
|
+
const buffer = await writeXlsxFile(sheetData, {}).toBuffer();
|
|
17
|
+
// Buffer is typed over ArrayBufferLike; a plain copy gives Response a BodyInit-compatible view.
|
|
18
|
+
const bytes = new Uint8Array(buffer.byteLength);
|
|
19
|
+
bytes.set(buffer);
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
@@ -70,6 +70,10 @@ export type SignatureExtraRouteDeps = {
|
|
|
70
70
|
* caller's authenticity (signature, HMAC state, etc.). */
|
|
71
71
|
readonly dispatchSystemWrite: (args: SystemDispatchArgs) => Promise<WriteResult>;
|
|
72
72
|
readonly dispatchSystemQuery: (args: SystemDispatchArgs) => Promise<unknown>;
|
|
73
|
+
/** Same trust as dispatchSystemWrite, but runs as the tenant's system user
|
|
74
|
+
* with the "system" role plus exactly `roles` (no SystemAdmin), so the write
|
|
75
|
+
* passes app-role access checks. */
|
|
76
|
+
readonly dispatchWriteAs: (args: RoleDispatchArgs) => Promise<WriteResult>;
|
|
73
77
|
};
|
|
74
78
|
export type SignatureExtraRoute<TVerified> = {
|
|
75
79
|
readonly method: HttpRouteMethod;
|
package/dist/api/index.d.ts
CHANGED
|
@@ -27,7 +27,9 @@ export type { MetricsRouteOptions } from "./route-registrars.ts";
|
|
|
27
27
|
export { createMetricsApp, DEFAULT_MAX_REQUEST_BYTES, isPrometheusMeter, } from "./route-registrars.ts";
|
|
28
28
|
export { createApiRoutes } from "./routes.ts";
|
|
29
29
|
export type { KumikoServer, ServerOptions } from "./server.ts";
|
|
30
|
-
export { buildServer
|
|
30
|
+
export { buildServer } from "./server.ts";
|
|
31
|
+
export { makeDispatchSystemQuery, makeDispatchSystemWrite, makeDispatchWriteAs, } from "./server-extra-routes.ts";
|
|
32
|
+
export { withFileProviderResolver } from "./server-wiring.ts";
|
|
31
33
|
export type { ResolvedSessionCookies, SessionCookieBase, SessionCookieNames, } from "./session-cookies.ts";
|
|
32
34
|
export { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, getSessionCookies, HOST_COOKIE_PREFIX, isHostPrefixedCookieName, resolveSessionCookieNames, resolveSessionCookies, sessionCookieName, sessionCookiesMiddleware, TENANT_COOKIE_BASE, } from "./session-cookies.ts";
|
|
33
35
|
export type { SseBroker, SseClient, SseEvent } from "./sse-broker.ts";
|
package/dist/api/index.js
CHANGED
|
@@ -16,7 +16,9 @@ export { readonlyRequestContext as requestContext, } from "./request-context.js"
|
|
|
16
16
|
export { buildRequestContextDataFromRequest, requestIdMiddleware, } from "./request-id-middleware.js";
|
|
17
17
|
export { createMetricsApp, DEFAULT_MAX_REQUEST_BYTES, isPrometheusMeter, } from "./route-registrars.js";
|
|
18
18
|
export { createApiRoutes } from "./routes.js";
|
|
19
|
-
export { buildServer
|
|
19
|
+
export { buildServer } from "./server.js";
|
|
20
|
+
export { makeDispatchSystemQuery, makeDispatchSystemWrite, makeDispatchWriteAs, } from "./server-extra-routes.js";
|
|
21
|
+
export { withFileProviderResolver } from "./server-wiring.js";
|
|
20
22
|
export { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, getSessionCookies, HOST_COOKIE_PREFIX, isHostPrefixedCookieName, resolveSessionCookieNames, resolveSessionCookies, sessionCookieName, sessionCookiesMiddleware, TENANT_COOKIE_BASE, } from "./session-cookies.js";
|
|
21
23
|
export { createSseBroker } from "./sse-broker.js";
|
|
22
24
|
export { createSseRoute, SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route.js";
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
export declare const CIPHERTEXT_RE: RegExp;
|
|
3
|
+
export declare const CIPHERTEXT_REDACT_RE: RegExp;
|
|
4
|
+
export declare const CIPHERTEXT_REDACTION = "[pii-redacted]";
|
|
2
5
|
export declare function piiCiphertextResponseGuard(): MiddlewareHandler;
|
|
@@ -8,8 +8,9 @@ const isProductionEnv = () => process.env["NODE_ENV"] === "production";
|
|
|
8
8
|
// Bound to the full ciphertext shape (prefix, "<kind>:<id>" subject key, base64
|
|
9
9
|
// blob of at least IV + GCM tag = 28 bytes) so a user-typed "kumiko-pii:v" in a
|
|
10
10
|
// note or name field cannot turn every read of that record into a 500.
|
|
11
|
-
const CIPHERTEXT_RE = /kumiko-pii:v\d+:[^:"\s<>\\]+:[^:"\s<>\\]+:[A-Za-z0-9+/=]{40,}/;
|
|
12
|
-
const CIPHERTEXT_REDACT_RE = new RegExp(CIPHERTEXT_RE.source, "g");
|
|
11
|
+
export const CIPHERTEXT_RE = /kumiko-pii:v\d+:[^:"\s<>\\]+:[^:"\s<>\\]+:[A-Za-z0-9+/=]{40,}/;
|
|
12
|
+
export const CIPHERTEXT_REDACT_RE = new RegExp(CIPHERTEXT_RE.source, "g");
|
|
13
|
+
export const CIPHERTEXT_REDACTION = "[pii-redacted]";
|
|
13
14
|
// A PII subject ciphertext never belongs in an API response — its presence
|
|
14
15
|
// means a raw DB read (fetchOne/selectMany) leaked to the surface. Dev/test
|
|
15
16
|
// fail loud (500) so a forgotten decrypt turns the first integration test
|
|
@@ -41,6 +41,6 @@ export declare function runWithOrigin<T>(origin: {
|
|
|
41
41
|
readonly risk: AgentRisk;
|
|
42
42
|
};
|
|
43
43
|
}, fn: () => T): T;
|
|
44
|
-
export declare function
|
|
44
|
+
export declare function runAsAgentToolCallEntry<T>(fn: () => T): T;
|
|
45
45
|
export declare function isAgentToolCallScope(): boolean;
|
|
46
46
|
export declare function runWithWriteOrigin<T>(origin: WriteOrigin, fn: () => T): T;
|
|
@@ -53,7 +53,7 @@ export function runWithOrigin(origin, fn) {
|
|
|
53
53
|
// surrounding turn/approve handler. Reserved for dispatchToolCall, so it also marks
|
|
54
54
|
// the scope as an agent tool call; it always mints a scope, because an agent loop
|
|
55
55
|
// running outside any request must still carry that marker.
|
|
56
|
-
export function
|
|
56
|
+
export function runAsAgentToolCallEntry(fn) {
|
|
57
57
|
const current = requestContext.get();
|
|
58
58
|
const requestId = current?.requestId ?? requestContext.generateId();
|
|
59
59
|
const { entryHandler: _entryHandler, ...rest } = current ?? {};
|
package/dist/api/routes.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Context, Hono } from "hono";
|
|
2
|
+
import { type KumikoError } from "../errors/index.ts";
|
|
2
3
|
import type { Dispatcher } from "../pipeline/dispatcher.ts";
|
|
3
4
|
export declare const StreamFrame: {
|
|
4
5
|
readonly chunk: "chunk";
|
|
@@ -18,4 +19,19 @@ export type SseWriter = {
|
|
|
18
19
|
};
|
|
19
20
|
export declare function pumpStream(stream: SseWriter, generator: AsyncGenerator<unknown>, heartbeatMs: number, firstPull?: Promise<IteratorResult<unknown>>): Promise<void>;
|
|
20
21
|
export declare const MAX_PAYLOAD_DEPTH = 20;
|
|
22
|
+
export declare function assertPayloadDepthAllowed(payload: unknown): void;
|
|
23
|
+
export declare function queryErrorResponse(c: Context, err: KumikoError, type?: unknown): (Response & import("hono").TypedResponse<{
|
|
24
|
+
readonly error: {
|
|
25
|
+
readonly code: string;
|
|
26
|
+
readonly i18nKey: string;
|
|
27
|
+
readonly i18nParams?: {
|
|
28
|
+
readonly [x: string]: import("hono/utils/types").JSONValue;
|
|
29
|
+
} | undefined;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly details?: import("hono/utils/types").JSONValue | undefined;
|
|
32
|
+
readonly docsUrl?: string;
|
|
33
|
+
readonly requestId?: string;
|
|
34
|
+
readonly timestamp: string;
|
|
35
|
+
};
|
|
36
|
+
}, -1 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511, "json">) | (Response & import("hono").TypedResponse<null, -1 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511, "body">);
|
|
21
37
|
export declare function handleUncaughtRouteError(err: Error, c: Context): Response | Promise<Response>;
|
package/dist/api/routes.js
CHANGED
|
@@ -312,7 +312,7 @@ async function readDispatchBody(c) {
|
|
|
312
312
|
// jsonb serialization of custom-fields payloads) walks the same object
|
|
313
313
|
// graph an attacker controls, so an uncapped depth is a stack-overflow /
|
|
314
314
|
// CPU-DoS vector even under the 1MB body-byte limit.
|
|
315
|
-
function assertPayloadDepthAllowed(payload) {
|
|
315
|
+
export function assertPayloadDepthAllowed(payload) {
|
|
316
316
|
if (payloadDepth(payload, 0) > MAX_PAYLOAD_DEPTH) {
|
|
317
317
|
throw new ValidationError({
|
|
318
318
|
fields: [
|
|
@@ -428,7 +428,7 @@ function clientAbortResponse(c, requestId, type) {
|
|
|
428
428
|
}
|
|
429
429
|
// For /query + /command: no isSuccess on success (just { data } / {ok}), so we
|
|
430
430
|
// keep the same lean shape on failure — only the `error` key.
|
|
431
|
-
function queryErrorResponse(c, err, type) {
|
|
431
|
+
export function queryErrorResponse(c, err, type) {
|
|
432
432
|
const requestId = requestContext.get()?.requestId;
|
|
433
433
|
if (isClientAbort(err))
|
|
434
434
|
return clientAbortResponse(c, requestId, type);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AppContext, type Registry } from "../engine/types/index.ts";
|
|
2
|
+
import type { createFallbackLogger } from "../logging/index.ts";
|
|
3
|
+
import type { AuthRoutesConfig } from "./auth-routes.ts";
|
|
4
|
+
export type BootLogger = ReturnType<typeof createFallbackLogger>;
|
|
5
|
+
export declare function assertNoPayloadRateLimitsWithoutResolver(registry: Registry): void;
|
|
6
|
+
export declare function assertFileStorageProviderForFileFields(registry: Registry, context: AppContext): void;
|
|
7
|
+
export declare function warnSearchableScreensWithoutSearchAdapter(registry: Registry, context: AppContext, bootLog: BootLogger): void;
|
|
8
|
+
export declare function resolveServerInstanceId(explicitInstanceId: string | undefined, bootLog: BootLogger): string;
|
|
9
|
+
export declare function warnUnlimitedSystemScopeFeatures(registry: Registry, bootLog: BootLogger): void;
|
|
10
|
+
export declare function assertSinglePrincipalStatusProvider(registry: Registry, auth: AuthRoutesConfig): void;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { EXT_FILE_PROVIDER, EXT_PRINCIPAL_STATUS } from "../engine/extension-names.js";
|
|
2
|
+
import { isFileField } from "../engine/types/index.js";
|
|
3
|
+
import { SHARED_INSTANCE_SENTINEL } from "../pipeline/event-consumer-state.js";
|
|
4
|
+
import { generateId } from "../utils/index.js";
|
|
5
|
+
// A payload-bucket handler without a resolver would answer every call with a
|
|
6
|
+
// 500 at request time; fail at boot instead.
|
|
7
|
+
export function assertNoPayloadRateLimitsWithoutResolver(registry) {
|
|
8
|
+
const offenders = [];
|
|
9
|
+
for (const feature of registry.features.values()) {
|
|
10
|
+
for (const [name, handler] of Object.entries(feature.writeHandlers)) {
|
|
11
|
+
if (handler.additionalRateLimits !== undefined) {
|
|
12
|
+
offenders.push(`${feature.name}:write:${name}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// skip: every handler's payload buckets are covered by the resolver
|
|
17
|
+
if (offenders.length === 0)
|
|
18
|
+
return;
|
|
19
|
+
throw new Error(`Handlers declare additionalRateLimits but no RateLimitResolver is configured: ${offenders.join(", ")}. ` +
|
|
20
|
+
"Wire a Redis client on context.redis (or pass context.rateLimit), or drop the payload buckets " +
|
|
21
|
+
"(for the waitlist: emailRateLimits: []).");
|
|
22
|
+
}
|
|
23
|
+
export function assertFileStorageProviderForFileFields(registry, context) {
|
|
24
|
+
// File-storage is resolved per-tenant through file-foundation: a mounted
|
|
25
|
+
// `file-provider-*` plugin (inmemory/s3/s3-env) is the single source for
|
|
26
|
+
// uploads, ctx.files and the GDPR jobs.
|
|
27
|
+
const hasFileProvider = registry.getExtensionUsages(EXT_FILE_PROVIDER).length > 0;
|
|
28
|
+
// A test/advanced caller may inject the resolver directly on the context
|
|
29
|
+
// (e.g. a static in-memory provider) instead of mounting a provider plugin.
|
|
30
|
+
const hasInjectedResolver = context._fileProviderResolver !== undefined;
|
|
31
|
+
// Hard-fail when the registry declares file/image fields but no provider
|
|
32
|
+
// plugin is mounted — uploads would otherwise fail at the first request
|
|
33
|
+
// instead of at boot. Proves a plugin is mounted, not that each tenant
|
|
34
|
+
// selected one; per-tenant misconfig surfaces via createFileProviderForTenant.
|
|
35
|
+
if (registryDeclaresFileFields(registry) && !hasFileProvider && !hasInjectedResolver) {
|
|
36
|
+
throw new Error("Features declare file/image fields but no file-storage provider is mounted — " +
|
|
37
|
+
"mount `file-foundation` + a `file-provider-*` feature (inmemory/s3/s3-env) and " +
|
|
38
|
+
"select one per tenant via the 'file-foundation:config:provider' config-key.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function warnSearchableScreensWithoutSearchAdapter(registry, context, bootLog) {
|
|
42
|
+
// A screen whose search box renders (kumiko-screen.tsx gates it on
|
|
43
|
+
// `screen.searchable ?? entity-has-searchable-field`, the same condition
|
|
44
|
+
// as entitiesWithSearchableScreen) sends `payload.search` on every query. Without a
|
|
45
|
+
// context.searchAdapter that throws UnprocessableError at request time
|
|
46
|
+
// — surface the misconfig at boot instead of on the first search.
|
|
47
|
+
// Warn, not throw: unlike missing file-storage (uploads always fail),
|
|
48
|
+
// list screens still work without search: only the search box is broken.
|
|
49
|
+
// Several deployed apps (offlot-app, publicstatus, kumiko-studio,
|
|
50
|
+
// kumiko-enterprise) currently run in exactly this state.
|
|
51
|
+
if (!context.searchAdapter) {
|
|
52
|
+
const unwiredEntities = entitiesWithSearchableScreen(registry);
|
|
53
|
+
if (unwiredEntities.length > 0) {
|
|
54
|
+
bootLog.warn(`${unwiredEntities.length} entit${unwiredEntities.length === 1 ? "y" : "ies"} ` +
|
|
55
|
+
`have a searchable list screen but no SearchAdapter is wired on context.searchAdapter: ` +
|
|
56
|
+
`${unwiredEntities.join(", ")}. Search requests against ${unwiredEntities.length === 1 ? "it" : "them"} will fail with a 422 (search_adapter_not_wired) at runtime. ` +
|
|
57
|
+
"Wire a SearchAdapter (e.g. createMeilisearchAdapter) on context.searchAdapter, or remove `searchable: true` from the affected fields.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function resolveServerInstanceId(explicitInstanceId, bootLog) {
|
|
62
|
+
// Resolve the per-process instance identifier. Prefer explicit
|
|
63
|
+
// ServerOptions.instanceId (tests, deliberate wiring), fall back to the
|
|
64
|
+
// deploy-env variable, finally a boot-time UUID. Validator rejects the
|
|
65
|
+
// sentinel — a deliberate collision attempt would silently merge this
|
|
66
|
+
// instance's per-instance cursors with the shared-row cursors and
|
|
67
|
+
// deliver events twice to one shard while starving the other.
|
|
68
|
+
const resolvedInstanceId = explicitInstanceId ?? process.env["KUMIKO_INSTANCE_ID"] ?? generateId();
|
|
69
|
+
if (resolvedInstanceId === SHARED_INSTANCE_SENTINEL) {
|
|
70
|
+
throw new Error(`ServerOptions.instanceId / KUMIKO_INSTANCE_ID cannot equal the reserved sentinel "${SHARED_INSTANCE_SENTINEL}" — ` +
|
|
71
|
+
`pick any other stable string.`);
|
|
72
|
+
}
|
|
73
|
+
// Warn when we fell back to a random UUID. The built-in SSE consumer is
|
|
74
|
+
// delivery="shared" (one cursor for all pods), so it no longer
|
|
75
|
+
// writes a row per instance. The id still labels this process in the
|
|
76
|
+
// app.started event and in per-instance consumers (opt-in delivery), where a
|
|
77
|
+
// fresh id per boot leaves an orphaned cursor-row that pins pruneEvents
|
|
78
|
+
// (retention-guard uses MIN(lastProcessedEventId) across all shards). Silent
|
|
79
|
+
// when explicitInstanceId or KUMIKO_INSTANCE_ID is explicit — those are
|
|
80
|
+
// deliberate choices (the test suite sets KUMIKO_INSTANCE_ID="test-instance").
|
|
81
|
+
const instanceIdWasRandom = explicitInstanceId === undefined && !process.env["KUMIKO_INSTANCE_ID"];
|
|
82
|
+
if (instanceIdWasRandom) {
|
|
83
|
+
bootLog.warn(`No ServerOptions.instanceId / KUMIKO_INSTANCE_ID set — generated a random UUID (${resolvedInstanceId}). ` +
|
|
84
|
+
`SSE no longer needs a per-instance cursor, but the id labels this process in the app.started event and in per-instance consumers; without a stable id each restart leaves an orphaned cursor-row behind for those and pins events-retention on its last cursor. ` +
|
|
85
|
+
`Set KUMIKO_INSTANCE_ID to the pod name (e.g. from the Kubernetes downward API metadata.name) in production.`);
|
|
86
|
+
}
|
|
87
|
+
return resolvedInstanceId;
|
|
88
|
+
}
|
|
89
|
+
export function warnUnlimitedSystemScopeFeatures(registry, bootLog) {
|
|
90
|
+
// The systemScope default limit only exists once a resolver does, and an app that
|
|
91
|
+
// declared no explicit rateLimit anywhere never gets one auto-wired (wantsL3 stays
|
|
92
|
+
// false) — so its cross-tenant handlers are silently unlimited.
|
|
93
|
+
const unlimitedSystemScopeFeatures = [...registry.features.values()]
|
|
94
|
+
.filter((feature) => feature.systemScope &&
|
|
95
|
+
Object.keys(feature.writeHandlers).length + Object.keys(feature.queryHandlers).length > 0)
|
|
96
|
+
.map((feature) => feature.name);
|
|
97
|
+
if (unlimitedSystemScopeFeatures.length > 0) {
|
|
98
|
+
bootLog.warn(`r.systemScope() features have handlers but no RateLimitResolver is configured, ` +
|
|
99
|
+
`so their default rate limit is not enforced: ${unlimitedSystemScopeFeatures.join(", ")}. ` +
|
|
100
|
+
"Wire a Redis client on context.redis and declare rateLimit on at least one handler (or pass " +
|
|
101
|
+
"context.rateLimit) to enable the resolver.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function assertSinglePrincipalStatusProvider(registry, auth) {
|
|
105
|
+
// A membershipQuery handler is registered but principalStatus is not provided by exactly
|
|
106
|
+
// one feature — surface that misconfig at boot instead of an InternalError on first login/switch-tenant.
|
|
107
|
+
if (registry.getQueryHandler(auth.membershipQuery)) {
|
|
108
|
+
const principalStatusProviders = registry.getExtensionUsages(EXT_PRINCIPAL_STATUS).length;
|
|
109
|
+
if (principalStatusProviders === 0) {
|
|
110
|
+
throw new Error(`[kumiko] auth membershipQuery "${auth.membershipQuery}" is registered but no feature ` +
|
|
111
|
+
'provides the "principalStatus" contract — mount the user feature (tenant switch and sign-in ' +
|
|
112
|
+
"need it to reject blocked principals).");
|
|
113
|
+
}
|
|
114
|
+
if (principalStatusProviders > 1) {
|
|
115
|
+
throw new Error(`[kumiko] auth membershipQuery "${auth.membershipQuery}" is registered but ` +
|
|
116
|
+
`${principalStatusProviders} features provide the "principalStatus" contract — exactly one ` +
|
|
117
|
+
"is expected (the bundled user feature already provides it).");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Scans every feature's entities for a file/image/files/images field. Short-
|
|
122
|
+
// circuits on the first hit — no need to build a full inventory, we only want
|
|
123
|
+
// the yes/no answer for the boot check.
|
|
124
|
+
function registryDeclaresFileFields(registry) {
|
|
125
|
+
for (const feature of registry.features.values()) {
|
|
126
|
+
for (const entity of Object.values(feature.entities ?? {})) {
|
|
127
|
+
for (const field of Object.values(entity.fields)) {
|
|
128
|
+
if (isFileField(field))
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
// Entities whose entityList screen WOULD render a search box based on
|
|
136
|
+
// screen/field config alone: `screen.searchable` wins when set, otherwise
|
|
137
|
+
// the box shows iff the entity has ≥1 searchable field. `screen.searchable
|
|
138
|
+
// === false` is excluded — the boot-validator (entity-list-screens.ts) only
|
|
139
|
+
// allows that on the whitelisted download-attempt-list-style screens, which
|
|
140
|
+
// never render the box. This is the config-only half of the rule; the
|
|
141
|
+
// client additionally gates on FeatureSchema.searchAdapterMissing
|
|
142
|
+
// (set from the same `!options.context.searchAdapter` check in mountSchemaRoute) so the
|
|
143
|
+
// box is actually suppressed once this function finds a hit.
|
|
144
|
+
function entitiesWithSearchableScreen(registry) {
|
|
145
|
+
const entities = new Set();
|
|
146
|
+
for (const feature of registry.features.values()) {
|
|
147
|
+
for (const screen of Object.values(feature.screens)) {
|
|
148
|
+
if (screen.type !== "entityList")
|
|
149
|
+
continue;
|
|
150
|
+
const isSearchable = screen.searchable === true ||
|
|
151
|
+
(screen.searchable === undefined && registry.getSearchableFields(screen.entity).length > 0);
|
|
152
|
+
if (isSearchable)
|
|
153
|
+
entities.add(screen.entity);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return [...entities];
|
|
157
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DbConnection } from "../db/connection.ts";
|
|
2
|
+
import type { AppContext } from "../engine/types/index.ts";
|
|
3
|
+
import type { Lifecycle } from "../lifecycle/index.ts";
|
|
4
|
+
import type { ObservabilityProvider } from "../observability/index.ts";
|
|
5
|
+
import { type EventConsumer, type EventDispatcher } from "../pipeline/event-dispatcher.ts";
|
|
6
|
+
import type { ResolvedCacheSyncBus } from "../redis/cache-sync-bus.ts";
|
|
7
|
+
import type { RedisSseBroker } from "./redis-sse-broker.ts";
|
|
8
|
+
import type { ServerOptions } from "./server.ts";
|
|
9
|
+
import type { SseBroker } from "./sse-broker.ts";
|
|
10
|
+
export declare function createServerEventDispatcher(deps: {
|
|
11
|
+
readonly options: Pick<ServerOptions, "registry" | "eventDispatcher" | "dispatcherOptions" | "processLane">;
|
|
12
|
+
readonly context: AppContext;
|
|
13
|
+
readonly sseBroker: SseBroker;
|
|
14
|
+
readonly observability: ObservabilityProvider;
|
|
15
|
+
readonly fileProviderResolver: AppContext["_fileProviderResolver"];
|
|
16
|
+
readonly instanceId: string;
|
|
17
|
+
}): {
|
|
18
|
+
readonly baseDb: DbConnection | undefined;
|
|
19
|
+
readonly consumers: readonly EventConsumer[];
|
|
20
|
+
readonly eventDispatcher: EventDispatcher | undefined;
|
|
21
|
+
};
|
|
22
|
+
export declare function registerOwnedShutdownHooks(lifecycle: Lifecycle | undefined, owned: {
|
|
23
|
+
readonly eventDispatcher: EventDispatcher | undefined;
|
|
24
|
+
readonly ownedRedisSseBroker: RedisSseBroker | undefined;
|
|
25
|
+
readonly ownedCacheSync: ResolvedCacheSyncBus["owned"];
|
|
26
|
+
}): void;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { withExecutorManagedWrites } from "../db/tenant-db.js";
|
|
2
|
+
import { createDerivativesContext } from "../derivatives/derivatives-context.js";
|
|
3
|
+
import { runsInLane } from "../engine/run-in.js";
|
|
4
|
+
import { createTenantFileContext } from "../files/file-handle.js";
|
|
5
|
+
import { createEventDispatcher, } from "../pipeline/event-dispatcher.js";
|
|
6
|
+
import { createMultiStreamApplyContext } from "../pipeline/multi-stream-apply-context.js";
|
|
7
|
+
import { createAccessInvalidationEventConsumer, createJobTriggerEventConsumer, createSearchEventConsumer, createSseBroadcastEventConsumer, } from "../pipeline/system-hooks.js";
|
|
8
|
+
import { deriveSearchAdapterConfig } from "../search/derive-search-adapter-config.js";
|
|
9
|
+
export function createServerEventDispatcher(deps) {
|
|
10
|
+
const { options, context, sseBroker, observability, fileProviderResolver, instanceId } = deps;
|
|
11
|
+
// Async event-dispatcher — the replacement for the old transactional
|
|
12
|
+
// outbox. Consumer sources:
|
|
13
|
+
// 1. System: SSE broadcast (iff sseBroker), Search index (iff
|
|
14
|
+
// context.searchAdapter).
|
|
15
|
+
// 2. Features: every r.multiStreamProjection registered in the registry
|
|
16
|
+
// becomes its own consumer row with an independent cursor. The MSP
|
|
17
|
+
// apply map is routed by event.type; apply receives the raw DbRunner
|
|
18
|
+
// of a TX-scoped, tenant-bound DB handle so per-tenant writes stay
|
|
19
|
+
// isolated.
|
|
20
|
+
//
|
|
21
|
+
// The dispatcher is built but NOT started here. Production boot code
|
|
22
|
+
// must call `.start()`; test code typically calls `.runOnce()`.
|
|
23
|
+
const { baseDb, searchAdapter } = resolveBaseDbAndSearchAdapter(context, options.registry);
|
|
24
|
+
const systemConsumers = buildSystemEventConsumers(options, sseBroker, searchAdapter);
|
|
25
|
+
const mspConsumers = buildMultiStreamProjectionConsumers(options, {
|
|
26
|
+
baseDb,
|
|
27
|
+
context,
|
|
28
|
+
fileProviderResolver,
|
|
29
|
+
});
|
|
30
|
+
const consumers = [...systemConsumers, ...mspConsumers];
|
|
31
|
+
const { disabled: dispatcherDisabled, systemConsumers: _systemConsumersOpt, ...dispatcherTunables } = options.eventDispatcher ?? {};
|
|
32
|
+
let eventDispatcher;
|
|
33
|
+
if (consumers.length > 0 && baseDb && !dispatcherDisabled) {
|
|
34
|
+
eventDispatcher = createEventDispatcher({
|
|
35
|
+
db: baseDb,
|
|
36
|
+
consumers,
|
|
37
|
+
context,
|
|
38
|
+
tracer: observability.tracer,
|
|
39
|
+
meter: observability.meter,
|
|
40
|
+
instanceId,
|
|
41
|
+
...dispatcherTunables,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { baseDb, consumers, eventDispatcher };
|
|
45
|
+
}
|
|
46
|
+
function resolveBaseDbAndSearchAdapter(context, registry) {
|
|
47
|
+
// @cast-boundary engine-bridge — context.db union narrows to DbConnection here
|
|
48
|
+
const baseDb = context.db;
|
|
49
|
+
// @cast-boundary engine-bridge — searchAdapter is an optional context-extension
|
|
50
|
+
const searchAdapter = context.searchAdapter;
|
|
51
|
+
// Adapter is built before the registry exists (app boot order) — hand it
|
|
52
|
+
// the registry-derived default config here so it can lazily configure
|
|
53
|
+
// each tenant index on first access instead of requiring per-tenant app
|
|
54
|
+
// wiring.
|
|
55
|
+
const derivedSearchConfig = searchAdapter ? deriveSearchAdapterConfig(registry) : undefined;
|
|
56
|
+
if (searchAdapter && derivedSearchConfig) {
|
|
57
|
+
searchAdapter.setDefaultConfig?.(derivedSearchConfig);
|
|
58
|
+
}
|
|
59
|
+
return { baseDb, searchAdapter };
|
|
60
|
+
}
|
|
61
|
+
function buildSystemEventConsumers(options, sseBroker, searchAdapter) {
|
|
62
|
+
const sseConsumerEnabled = options.eventDispatcher?.systemConsumers?.sse ?? true;
|
|
63
|
+
const searchConsumerEnabled = options.eventDispatcher?.systemConsumers?.search ?? true;
|
|
64
|
+
const jobTriggerConsumerEnabled = options.eventDispatcher?.systemConsumers?.jobTrigger ?? true;
|
|
65
|
+
const jobRunnerForTriggers = options.dispatcherOptions?.jobRunner;
|
|
66
|
+
const systemConsumers = [];
|
|
67
|
+
if (sseConsumerEnabled) {
|
|
68
|
+
systemConsumers.push(createSseBroadcastEventConsumer(sseBroker));
|
|
69
|
+
}
|
|
70
|
+
if (searchConsumerEnabled && searchAdapter) {
|
|
71
|
+
systemConsumers.push(createSearchEventConsumer(searchAdapter, options.registry));
|
|
72
|
+
}
|
|
73
|
+
if (jobTriggerConsumerEnabled && jobRunnerForTriggers) {
|
|
74
|
+
systemConsumers.push(createJobTriggerEventConsumer(jobRunnerForTriggers, options.registry));
|
|
75
|
+
}
|
|
76
|
+
// Default ON (global-by-default). Tests that opt out of SSE via
|
|
77
|
+
// systemConsumers.accessInvalidation=false (test-stack mirrors sse off)
|
|
78
|
+
// skip the row so retention prune suites are not blocked by a lagging
|
|
79
|
+
// cursor=0 consumer they never drain.
|
|
80
|
+
const accessInvalidationEnabled = options.eventDispatcher?.systemConsumers?.accessInvalidation ?? true;
|
|
81
|
+
if (accessInvalidationEnabled) {
|
|
82
|
+
systemConsumers.push(createAccessInvalidationEventConsumer(sseBroker));
|
|
83
|
+
}
|
|
84
|
+
return systemConsumers;
|
|
85
|
+
}
|
|
86
|
+
function buildMultiStreamProjectionConsumers(options, deps) {
|
|
87
|
+
const jobRunnerForTriggers = options.dispatcherOptions?.jobRunner;
|
|
88
|
+
const mspJobs = jobRunnerForTriggers?.dispatch
|
|
89
|
+
? {
|
|
90
|
+
dispatch: async (jobName, payload, dispatchOptions) => {
|
|
91
|
+
await jobRunnerForTriggers.dispatch?.(jobName, payload, dispatchOptions);
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
: undefined;
|
|
95
|
+
// MultiStreamProjections: one EventConsumer per MSP. Handler routes by
|
|
96
|
+
// event.type into the MSP's apply map. MSPs aggregate cross-aggregate but
|
|
97
|
+
// still within one tenant by default — the applier receives the plain
|
|
98
|
+
// baseDb DbRunner; tenant consistency comes from the triggering event's
|
|
99
|
+
// own tenantId, not a per-event TenantDb scope.
|
|
100
|
+
//
|
|
101
|
+
// Lane-filter: MSPs declare `runIn` to pin them to a
|
|
102
|
+
// deploy-lane. An MSP with `runIn: "api"` won't be wired into the
|
|
103
|
+
// worker-process dispatcher (and vice versa). `runIn: "both"` (or the
|
|
104
|
+
// legacy undefined default of "worker") runs wherever a dispatcher is
|
|
105
|
+
// started — SKIP LOCKED on the consumer-cursor handles the race between
|
|
106
|
+
// processes that both want the same event.
|
|
107
|
+
const processLane = options.processLane ?? "worker";
|
|
108
|
+
const mspDefs = [...options.registry.getAllMultiStreamProjections().values()].filter((msp) => runsInLane(msp.runIn, processLane));
|
|
109
|
+
return mspDefs.map((msp) => buildMultiStreamProjectionConsumer(msp, {
|
|
110
|
+
registry: options.registry,
|
|
111
|
+
mspJobs,
|
|
112
|
+
...deps,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
function buildMultiStreamProjectionConsumer(msp, deps) {
|
|
116
|
+
const { registry, mspJobs, baseDb, context, fileProviderResolver } = deps;
|
|
117
|
+
return {
|
|
118
|
+
name: msp.name,
|
|
119
|
+
// Feature-toggle gating: carry the owning feature so the event-dispatcher
|
|
120
|
+
// can pause this consumer when the feature is globally disabled. Events
|
|
121
|
+
// queue up in the store and replay cleanly from the same cursor on resume.
|
|
122
|
+
...(registry.getMultiStreamProjectionFeature(msp.name) && {
|
|
123
|
+
featureName: registry.getMultiStreamProjectionFeature(msp.name), // @cast-boundary engine-bridge
|
|
124
|
+
}),
|
|
125
|
+
// Copy the continuous-lifecycle error policy straight onto the consumer.
|
|
126
|
+
// Rebuild uses its own policy (rebuildProjection reads msp.errorMode.rebuild
|
|
127
|
+
// directly); steady-state delivery runs through this consumer.
|
|
128
|
+
...(msp.errorMode?.continuous && { errorPolicy: msp.errorMode.continuous }),
|
|
129
|
+
// Carry the MSP's declared delivery semantic through to the consumer.
|
|
130
|
+
// Default (shared) is applied inside event-dispatcher, so omitting when
|
|
131
|
+
// the MSP didn't declare one keeps the existing behaviour.
|
|
132
|
+
...(msp.delivery && { delivery: msp.delivery }),
|
|
133
|
+
// Carry the MSP's declared cursor-seed through to the consumer. Default
|
|
134
|
+
// (beginning) is applied inside insertConsumerIfAbsent.
|
|
135
|
+
...(msp.startFrom && { startFrom: msp.startFrom }),
|
|
136
|
+
handler: async (event, ctx) => {
|
|
137
|
+
const applyFn = msp.apply[event.type];
|
|
138
|
+
// skip: this MSP doesn't care about this event type — fast path,
|
|
139
|
+
// every event type passes through every MSP consumer exactly once.
|
|
140
|
+
if (!applyFn)
|
|
141
|
+
return;
|
|
142
|
+
if (!baseDb) {
|
|
143
|
+
// skip: no baseDb wired — consumers.length > 0 + baseDb check in
|
|
144
|
+
// createServerEventDispatcher gates dispatcher creation, so we won't reach here in
|
|
145
|
+
// production. Defensive return for the type-narrowing path.
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Saga/process-manager ctx: apply can call ctx.appendEvent to cascade
|
|
149
|
+
// a follow-up event onto another aggregate. Uses the triggering event's
|
|
150
|
+
// tenantId + userId so the causal chain stays tenant-consistent.
|
|
151
|
+
// MSP qualified names are "<feature>:projection:<short>" — the
|
|
152
|
+
// prefix before the first ":" owns the MSP. Used to reject
|
|
153
|
+
// cross-feature ctx.appendEvent calls at emit-site.
|
|
154
|
+
const mspOwner = msp.name.split(":")[0];
|
|
155
|
+
const mspFiles = fileProviderResolver
|
|
156
|
+
? createTenantFileContext(fileProviderResolver, event.tenantId)
|
|
157
|
+
: undefined;
|
|
158
|
+
const mspDb = withExecutorManagedWrites(baseDb);
|
|
159
|
+
const applyCtx = createMultiStreamApplyContext({
|
|
160
|
+
registry,
|
|
161
|
+
db: mspDb,
|
|
162
|
+
tenantId: event.tenantId,
|
|
163
|
+
userId: event.metadata.userId,
|
|
164
|
+
...(mspOwner && { callerFeature: mspOwner }),
|
|
165
|
+
...(mspFiles && { files: mspFiles }),
|
|
166
|
+
...(context.secrets && { secrets: context.secrets }),
|
|
167
|
+
...(mspJobs && { jobs: mspJobs }),
|
|
168
|
+
...(mspFiles && {
|
|
169
|
+
derivatives: createDerivativesContext({
|
|
170
|
+
files: mspFiles,
|
|
171
|
+
registry,
|
|
172
|
+
db: baseDb,
|
|
173
|
+
tenantId: event.tenantId,
|
|
174
|
+
}),
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
await applyFn(event, mspDb, applyCtx);
|
|
178
|
+
// Keep ctx reachable to satisfy the EventConsumerHandler signature.
|
|
179
|
+
void ctx;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function registerOwnedShutdownHooks(lifecycle, owned) {
|
|
184
|
+
const { eventDispatcher, ownedRedisSseBroker, ownedCacheSync } = owned;
|
|
185
|
+
// Wire the event-dispatcher shutdown into the lifecycle so the caller
|
|
186
|
+
// doesn't have to know the dispatcher exists. Hooks drain LIFO, so this
|
|
187
|
+
// runs before anything registered later by the caller (e.g. DB pool close).
|
|
188
|
+
if (lifecycle && eventDispatcher) {
|
|
189
|
+
const dispatcher = eventDispatcher;
|
|
190
|
+
lifecycle.registerShutdownHook("eventDispatcher", async () => {
|
|
191
|
+
await dispatcher.stop();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Same reasoning, scoped to the broker this function created itself — an
|
|
195
|
+
// app-injected sseBroker (options.sseBroker) owns its own teardown.
|
|
196
|
+
if (lifecycle && ownedRedisSseBroker) {
|
|
197
|
+
const broker = ownedRedisSseBroker;
|
|
198
|
+
lifecycle.registerShutdownHook("redisSseBroker", async () => {
|
|
199
|
+
await broker.close();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (lifecycle && ownedCacheSync) {
|
|
203
|
+
const bus = ownedCacheSync;
|
|
204
|
+
lifecycle.registerShutdownHook("cacheSync", async () => {
|
|
205
|
+
await bus.close();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context, Hono } from "hono";
|
|
2
|
+
import type { WriteResult } from "../engine/types/index.ts";
|
|
3
|
+
import type { Dispatcher } from "../pipeline/dispatcher.ts";
|
|
4
|
+
import { type ExtraRouteDefinition, type RoleDispatchArgs, type SystemDispatchArgs } from "./extra-route.ts";
|
|
5
|
+
import type { ServerWiring } from "./server-wiring.ts";
|
|
6
|
+
export type PublicApiRequestMatcher = (c: Context) => boolean;
|
|
7
|
+
export declare function compilePublicApiRequestMatcher(extraRoutes: readonly ExtraRouteDefinition[] | undefined): PublicApiRequestMatcher;
|
|
8
|
+
export declare function mountExtraRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "context" | "clientIpResolver">): void;
|
|
9
|
+
export declare function makeDispatchSystemWrite(dispatcher: Dispatcher): (args: SystemDispatchArgs) => Promise<WriteResult>;
|
|
10
|
+
export declare function makeDispatchWriteAs(dispatcher: Dispatcher): (args: RoleDispatchArgs) => Promise<WriteResult>;
|
|
11
|
+
export declare function makeDispatchSystemQuery(dispatcher: Dispatcher): (args: SystemDispatchArgs) => Promise<unknown>;
|