@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
package/AGENTS.md
CHANGED
|
@@ -208,7 +208,15 @@ produces code that compiles and then fails in production.
|
|
|
208
208
|
one page (default 50, `limit` at most 200). Pass
|
|
209
209
|
`filter: { field, op, value }` (`eq`, `ne`, `lt`, `gt`, `lte`, `gte`, `in`)
|
|
210
210
|
so the database filters, and `totalCount: true` to get `total` over all
|
|
211
|
-
matching rows; `limit: 1, totalCount: true` is a count query.
|
|
211
|
+
matching rows; `limit: 1, totalCount: true` is a count query. Groups and
|
|
212
|
+
sums are the same: pass `aggregate: { groupBy?, measures: [{ field, kind }] }`
|
|
213
|
+
(`kind` is `sum`, `avg`, `count`, `min` or `max`) and the list answers with
|
|
214
|
+
`{ groups: [{ key, label?, count, measures }] }` over the same filters,
|
|
215
|
+
search and ownership, at most 200 groups (`truncated: true` beyond that).
|
|
216
|
+
A money measure answers per currency, `[{ currency, value }]` with `value`
|
|
217
|
+
in minor units, never one sum across currencies. `bigInt` measures come
|
|
218
|
+
back as JS numbers and are exact only up to 2^53.
|
|
219
|
+
Custom handlers call `executor.aggregate(...)` or `answerListAggregate`.
|
|
212
220
|
- Never change an event schema in place once it has been emitted. Version it
|
|
213
221
|
(see `agents/examples/event-sourcing.md`).
|
|
214
222
|
- Never read or write another tenant's data. `ctx.db` is tenant scoped on
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
|
|
61
61
|
const TENANT_ID = "00000000-0000-4000-8000-000000000001" as TenantId;
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// Each value is written by exactly one test; the tenant is fixed, so rows are told apart by value, not by reset.
|
|
64
64
|
const PRODUCT_NAME = "Espresso Beans (anonymous read)";
|
|
65
65
|
const GUEST_EMAIL = "guest-checkout@example.com";
|
|
66
66
|
|
|
@@ -41,6 +41,210 @@ bun run screenshot # → screenshots/landing.png + lightbox.png
|
|
|
41
41
|
|
|
42
42
|
## Source files
|
|
43
43
|
|
|
44
|
+
### e2e/header-layout.spec.ts
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// Regression for issue #2038: below the 1120px .container breakpoint, the
|
|
48
|
+
// header's `<div class="container nav">` lost its horizontal padding because
|
|
49
|
+
// `.nav`'s `padding` shorthand (same specificity, declared later) zeroed out
|
|
50
|
+
// the inline padding `.container` set. Verified against a real narrow
|
|
51
|
+
// viewport, not derived from source — matches how the bug was first found.
|
|
52
|
+
|
|
53
|
+
import { expect, boundaryGuardedTest as test } from "@cosmicdrift/kumiko-testing/e2e";
|
|
54
|
+
import { renderLanding, SAMPLE_PLANS } from "../src/feature";
|
|
55
|
+
|
|
56
|
+
test("header keeps the page's horizontal padding below 1120px (#2038)", async ({ page }) => {
|
|
57
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
58
|
+
await page.setContent(renderLanding({ plans: SAMPLE_PLANS }), {
|
|
59
|
+
waitUntil: "domcontentloaded",
|
|
60
|
+
url: "http://localhost/",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const headerContainer = page.locator("header .container.nav");
|
|
64
|
+
const footerContainer = page.locator("footer .container");
|
|
65
|
+
|
|
66
|
+
const [headerPadding, footerPadding] = await Promise.all([
|
|
67
|
+
headerContainer.evaluate((el) => {
|
|
68
|
+
const s = getComputedStyle(el);
|
|
69
|
+
return { left: s.paddingLeft, right: s.paddingRight };
|
|
70
|
+
}),
|
|
71
|
+
footerContainer.evaluate((el) => {
|
|
72
|
+
const s = getComputedStyle(el);
|
|
73
|
+
return { left: s.paddingLeft, right: s.paddingRight };
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// The header is indented exactly like the rest of the page — not 0.
|
|
78
|
+
expect(headerPadding.left).not.toBe("0px");
|
|
79
|
+
expect(headerPadding).toEqual(footerPadding);
|
|
80
|
+
|
|
81
|
+
// The nav's rightmost action stays inside the viewport, not clipped at the edge.
|
|
82
|
+
const actionBox = await page.locator("header .nav-actions a").last().boundingBox();
|
|
83
|
+
expect(actionBox).not.toBeNull();
|
|
84
|
+
expect(actionBox!.x + actionBox!.width).toBeLessThanOrEqual(390);
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### e2e/screenshot.spec.ts
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// E2E screenshot renderer for the Apex landing example. Renders the sample page
|
|
92
|
+
// to HTML, paints it in a real browser and shoots a full-page PNG. The output is
|
|
93
|
+
// what the docs guide embeds — regenerate with `bun run screenshot`, never hand-
|
|
94
|
+
// captured, so it can't drift from the renderer.
|
|
95
|
+
//
|
|
96
|
+
// SCREENSHOT_DIR overrides the output dir (CI writes straight into the docs repo).
|
|
97
|
+
|
|
98
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
99
|
+
import { resolve } from "node:path";
|
|
100
|
+
import { expect, boundaryGuardedTest as test } from "@cosmicdrift/kumiko-testing/e2e";
|
|
101
|
+
import type { Page } from "@playwright/test";
|
|
102
|
+
import { HERO_SCREENSHOT } from "../src/constants";
|
|
103
|
+
import { renderLanding, SAMPLE_PLANS } from "../src/feature";
|
|
104
|
+
|
|
105
|
+
const OUT_DIR = process.env["SCREENSHOT_DIR"] ?? resolve(import.meta.dirname, "../screenshots");
|
|
106
|
+
const HERO_APP_PNG = resolve(OUT_DIR, "hero-app.png");
|
|
107
|
+
const LANDING_PNG = resolve(OUT_DIR, "landing.png");
|
|
108
|
+
const LIGHTBOX_PNG = resolve(OUT_DIR, "lightbox.png");
|
|
109
|
+
|
|
110
|
+
/** Minimal in-app board mock — hero frame shows product UI, not the full landing PNG. */
|
|
111
|
+
const HERO_APP_HTML = `<!doctype html>
|
|
112
|
+
<html lang="en">
|
|
113
|
+
<head>
|
|
114
|
+
<meta charset="utf-8" />
|
|
115
|
+
<style>
|
|
116
|
+
body { margin: 0; background: #f4f4f5; font-family: system-ui, sans-serif; }
|
|
117
|
+
#board {
|
|
118
|
+
width: 720px; padding: 16px; background: #fff; border-radius: 12px;
|
|
119
|
+
border: 1px solid #e4e4e7; box-sizing: border-box;
|
|
120
|
+
}
|
|
121
|
+
.top { display: flex; justify-content: space-between; margin-bottom: 16px; font-weight: 600; }
|
|
122
|
+
.cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
|
123
|
+
.col { background: #fafafa; border-radius: 8px; padding: 10px; min-height: 140px; }
|
|
124
|
+
.label { font-size: 11px; color: #71717a; margin-bottom: 8px; }
|
|
125
|
+
.card {
|
|
126
|
+
background: #fff; border: 1px solid #e4e4e7; border-radius: 6px;
|
|
127
|
+
padding: 8px; margin-bottom: 8px; font-size: 13px;
|
|
128
|
+
}
|
|
129
|
+
.tag {
|
|
130
|
+
display: inline-block; font-size: 10px; padding: 2px 6px; border-radius: 4px;
|
|
131
|
+
background: #ede9fe; color: #5b21b6; margin-left: 4px;
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<div id="board">
|
|
137
|
+
<div class="top"><span>Tasklane — Q3 roadmap</span><span style="color:#71717a;font-weight:400">3 projects</span></div>
|
|
138
|
+
<div class="cols">
|
|
139
|
+
<div class="col">
|
|
140
|
+
<div class="label">Backlog</div>
|
|
141
|
+
<div class="card">Auth hardening<span class="tag">P1</span></div>
|
|
142
|
+
<div class="card">Export CSV</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="col">
|
|
145
|
+
<div class="label">In progress</div>
|
|
146
|
+
<div class="card">Board filters</div>
|
|
147
|
+
<div class="card">Invite flow</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="col">
|
|
150
|
+
<div class="label">Done</div>
|
|
151
|
+
<div class="card">Dark mode</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</body>
|
|
156
|
+
</html>`;
|
|
157
|
+
|
|
158
|
+
function heroForMount(): typeof HERO_SCREENSHOT {
|
|
159
|
+
const b64 = readFileSync(HERO_APP_PNG).toString("base64");
|
|
160
|
+
return { ...HERO_SCREENSHOT, src: `data:image/png;base64,${b64}` };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function assertHeroImageLoaded(page: Page): Promise<void> {
|
|
164
|
+
const img = page.locator(".shot-frame img");
|
|
165
|
+
await expect(img).toBeVisible();
|
|
166
|
+
await expect
|
|
167
|
+
.poll(async () => img.evaluate((el) => (el as HTMLImageElement).naturalWidth))
|
|
168
|
+
.toBeGreaterThan(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** setContent does not reliably fetch routed /screenshots/* — inline data: URL instead. */
|
|
172
|
+
async function mountLanding(page: Page): Promise<void> {
|
|
173
|
+
if (!existsSync(HERO_APP_PNG)) {
|
|
174
|
+
throw new Error("hero-app.png missing — run hero-app test first (serial suite)");
|
|
175
|
+
}
|
|
176
|
+
await page.setContent(renderLanding({ plans: SAMPLE_PLANS, heroScreenshot: heroForMount() }), {
|
|
177
|
+
waitUntil: "networkidle",
|
|
178
|
+
url: "http://localhost/",
|
|
179
|
+
});
|
|
180
|
+
await assertHeroImageLoaded(page);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test.describe.configure({ mode: "serial" });
|
|
184
|
+
|
|
185
|
+
test("hero-app — product screenshot asset", async ({ page }) => {
|
|
186
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
187
|
+
await page.setContent(HERO_APP_HTML, { waitUntil: "networkidle" });
|
|
188
|
+
await page.locator("#board").screenshot({ path: HERO_APP_PNG });
|
|
189
|
+
expect(statSync(HERO_APP_PNG).size).toBeGreaterThan(5 * 1024);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("apex landing — full page", async ({ page }) => {
|
|
193
|
+
await mountLanding(page);
|
|
194
|
+
await page.screenshot({ path: LANDING_PNG, fullPage: true });
|
|
195
|
+
expect(statSync(LANDING_PNG).size).toBeGreaterThan(5 * 1024);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("apex lightbox — open (screenshot)", async ({ page }) => {
|
|
199
|
+
await mountLanding(page);
|
|
200
|
+
await page.locator(".shot-frame img").click();
|
|
201
|
+
await expect(page.locator("#apex-lightbox")).toBeVisible();
|
|
202
|
+
await expect
|
|
203
|
+
.poll(async () =>
|
|
204
|
+
page.locator("#apex-lightbox img").evaluate((el) => (el as HTMLImageElement).naturalWidth),
|
|
205
|
+
)
|
|
206
|
+
.toBeGreaterThan(0);
|
|
207
|
+
await page.screenshot({ path: LIGHTBOX_PNG });
|
|
208
|
+
expect(statSync(LIGHTBOX_PNG).size).toBeGreaterThan(5 * 1024);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("apex lightbox — closes on button", async ({ page }) => {
|
|
212
|
+
await mountLanding(page);
|
|
213
|
+
await page.locator(".shot-frame img").click();
|
|
214
|
+
await expect(page.locator("#apex-lightbox")).toBeVisible();
|
|
215
|
+
await page.locator(".apex-lightbox__close").click();
|
|
216
|
+
await expect(page.locator("#apex-lightbox")).not.toBeVisible();
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### playwright.config.ts
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
224
|
+
|
|
225
|
+
// No webServer: the Apex renderer is a pure function, so the runner feeds its
|
|
226
|
+
// HTML straight into the page via setContent — nothing to boot.
|
|
227
|
+
export default defineConfig({
|
|
228
|
+
testDir: "./e2e",
|
|
229
|
+
fullyParallel: false,
|
|
230
|
+
forbidOnly: !!process.env["CI"],
|
|
231
|
+
retries: 0,
|
|
232
|
+
workers: 1,
|
|
233
|
+
reporter: [["list"]],
|
|
234
|
+
timeout: 60_000,
|
|
235
|
+
use: {
|
|
236
|
+
viewport: { width: 1920, height: 1080 },
|
|
237
|
+
deviceScaleFactor: 2,
|
|
238
|
+
},
|
|
239
|
+
projects: [
|
|
240
|
+
{
|
|
241
|
+
name: "chromium",
|
|
242
|
+
use: { ...devices["Desktop Chrome"], viewport: { width: 1920, height: 1080 } },
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
44
248
|
### src/__tests__/landing.test.ts
|
|
45
249
|
|
|
46
250
|
```ts
|
|
@@ -85,6 +85,70 @@ before the worker picks up the job and writes the fulfillment back.
|
|
|
85
85
|
|
|
86
86
|
## Source files
|
|
87
87
|
|
|
88
|
+
### bin/api.ts
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// API process — `runSingleInstance: false` makes this process API-only:
|
|
92
|
+
// HTTP + event writes + BullMQ ENQUEUE, no worker-lane consumption, no
|
|
93
|
+
// multiStreamProjection application. A dedicated worker MUST run next to
|
|
94
|
+
// it (bin/worker.ts) or the read-side stays empty.
|
|
95
|
+
import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";
|
|
96
|
+
import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
|
|
97
|
+
import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
|
|
98
|
+
import { createApiWorkerSplitFeature } from "../src/feature";
|
|
99
|
+
|
|
100
|
+
const features = [createApiWorkerSplitFeature()];
|
|
101
|
+
|
|
102
|
+
await runProdApp({
|
|
103
|
+
features,
|
|
104
|
+
envSchema: composeEnvSchema({ core: frameworkCoreEnvSchema, features }),
|
|
105
|
+
runSingleInstance: false,
|
|
106
|
+
// worker-lane queue prefix; must match bin/worker.ts.
|
|
107
|
+
jobs: { queueNamePrefix: "api-worker-split" },
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### bin/kumiko.ts
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
#!/usr/bin/env bun
|
|
115
|
+
// Schema CLI for the sample: creates the framework infra tables
|
|
116
|
+
// (idempotent) and applies kumiko/migrations. Requires DATABASE_URL.
|
|
117
|
+
import { runStandaloneSchemaCli } from "@cosmicdrift/kumiko-dev-server/schema-apply";
|
|
118
|
+
import { createApiWorkerSplitFeature } from "../src/feature";
|
|
119
|
+
|
|
120
|
+
await runStandaloneSchemaCli({ features: [createApiWorkerSplitFeature()] });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### bin/worker.ts
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// Worker process — consumes the worker-lane BullMQ queue and applies the
|
|
127
|
+
// multiStreamProjections the API skipped. No HTTP surface.
|
|
128
|
+
//
|
|
129
|
+
// wireComponents runs after the entrypoint is up and hands over
|
|
130
|
+
// `dispatchSystemWrite` — a dispatcher-scoped system write. The job's
|
|
131
|
+
// write-bridge is wired here (fw#1717): JobContext has no write/query, so
|
|
132
|
+
// background components persist through this dispatcher instead.
|
|
133
|
+
//
|
|
134
|
+
// ponytail: BullMQ may pop jobs between entrypoint.start() and this hook —
|
|
135
|
+
// those hits throw "no fulfill-write bridge wired" and rely on BullMQ retry
|
|
136
|
+
// until the bridge is set. Upgrade: defer job registration until after wire,
|
|
137
|
+
// or hold fulfillWrite behind a deferred Promise.
|
|
138
|
+
import { runWorkerApp } from "@cosmicdrift/kumiko-server-runtime";
|
|
139
|
+
import { createApiWorkerSplitFeature, setOrderFulfillWrite } from "../src/feature";
|
|
140
|
+
|
|
141
|
+
await runWorkerApp({
|
|
142
|
+
features: [createApiWorkerSplitFeature()],
|
|
143
|
+
jobs: { queueNamePrefix: "api-worker-split" },
|
|
144
|
+
wireComponents: async ({ dispatchSystemWrite }) => {
|
|
145
|
+
setOrderFulfillWrite(({ handlerQn, payload, tenantId }) =>
|
|
146
|
+
dispatchSystemWrite({ handlerQn, payload, tenantId }),
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
88
152
|
### src/__tests__/feature.integration.test.ts
|
|
89
153
|
|
|
90
154
|
```ts
|
|
@@ -37,6 +37,20 @@ for the files around it.
|
|
|
37
37
|
|
|
38
38
|
## Source files
|
|
39
39
|
|
|
40
|
+
### kumiko.config.ts
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { defineAppConfig } from "@cosmicdrift/kumiko-framework/config";
|
|
44
|
+
import { auditExportFeature, notesFeature } from "./src/feature";
|
|
45
|
+
|
|
46
|
+
export default defineAppConfig({
|
|
47
|
+
features: [notesFeature],
|
|
48
|
+
prodFeatures: [auditExportFeature],
|
|
49
|
+
client: { entries: ["src/client.tsx"] },
|
|
50
|
+
ports: { dev: 4510, e2e: 4511, infra: { postgres: 15510 } },
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
40
54
|
### src/__tests__/app-config.test.ts
|
|
41
55
|
|
|
42
56
|
```ts
|
|
@@ -83,11 +83,16 @@ features: [..., createSessionsFeature(), mfaFeature, createAccountSecurityFeatur
|
|
|
83
83
|
5. **MFA state changes revoke every OTHER live session** (stolen-session defense) — never the session that made the change.
|
|
84
84
|
6. **Enforcement is opt-in.** Without a tenant enabling `auth-mfa:config:required`, users choose for themselves whether to enroll.
|
|
85
85
|
|
|
86
|
+
## Confirmation code input
|
|
87
|
+
|
|
88
|
+
`src/web/confirmation-code-step.tsx` shows the `Input kind="code"` renderer primitive outside the MFA screen, for example an e-mail or invite confirmation code. Pass `length` (here 6), `autoFocus` and `onComplete`. The input accepts digits only, so pasting "123 456" yields `123456`, and `onComplete` fires once when the value reaches `length`. `autoComplete` defaults to `one-time-code`, so mobile keyboards offer the code from the SMS or mail. After a wrong code the step clears the value and bumps a `key`, which remounts the input and returns focus to it.
|
|
89
|
+
|
|
86
90
|
## Tests
|
|
87
91
|
|
|
88
92
|
- **Baseline:** a user without MFA enrolled still gets a JWT straight from `/auth/login`.
|
|
89
93
|
- **End-to-end:** enroll via `enable-start`/`enable-confirm`, log in (gets a challenge, no JWT), complete via `/auth/mfa/verify` (gets a JWT) — copies the `buildServer({ auth: ... })` block from the sample comment verbatim.
|
|
90
94
|
- **Account security:** the composed screen boot-validates next to `auth-mfa` and `sessions` and shows a sidebar entry once `nav` is set.
|
|
95
|
+
- **Confirmation code step:** typing or pasting six digits calls `onConfirm` once; a rejected code shows the error, clears the field and refocuses it.
|
|
91
96
|
|
|
92
97
|
## Source files
|
|
93
98
|
|
|
@@ -388,3 +393,113 @@ export {
|
|
|
388
393
|
} from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
389
394
|
export { currentTotpCode } from "@cosmicdrift/kumiko-bundled-features/auth-mfa/testing";
|
|
390
395
|
```
|
|
396
|
+
|
|
397
|
+
### src/web/__tests__/confirmation-code-step.test.tsx
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
401
|
+
import { PrimitivesProvider } from "@cosmicdrift/kumiko-renderer";
|
|
402
|
+
import { defaultPrimitives } from "@cosmicdrift/kumiko-renderer/web";
|
|
403
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
404
|
+
import { ConfirmationCodeStep } from "../confirmation-code-step.tsx";
|
|
405
|
+
|
|
406
|
+
function renderStep(onConfirm: (code: string) => Promise<boolean>): void {
|
|
407
|
+
render(
|
|
408
|
+
<PrimitivesProvider value={defaultPrimitives}>
|
|
409
|
+
<ConfirmationCodeStep onConfirm={onConfirm} />
|
|
410
|
+
</PrimitivesProvider>,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function type(value: string): void {
|
|
415
|
+
fireEvent.change(screen.getByTestId("confirmation-code"), { target: { value } });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
describe("ConfirmationCodeStep", () => {
|
|
419
|
+
test("focuses the input on mount", () => {
|
|
420
|
+
renderStep(async () => true);
|
|
421
|
+
expect(document.activeElement).toBe(screen.getByTestId("confirmation-code"));
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("does not confirm before six digits", () => {
|
|
425
|
+
const onConfirm = mock(async (_code: string) => true);
|
|
426
|
+
renderStep(onConfirm);
|
|
427
|
+
type("12345");
|
|
428
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("pasted '123 456' confirms once with 123456", async () => {
|
|
432
|
+
const onConfirm = mock(async (_code: string) => true);
|
|
433
|
+
renderStep(onConfirm);
|
|
434
|
+
type("123 456");
|
|
435
|
+
await waitFor(() => expect(onConfirm).toHaveBeenCalledTimes(1));
|
|
436
|
+
expect(onConfirm).toHaveBeenCalledWith("123456");
|
|
437
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("a rejected code shows the error, clears the field and refocuses", async () => {
|
|
441
|
+
const onConfirm = mock(async (_code: string) => false);
|
|
442
|
+
renderStep(onConfirm);
|
|
443
|
+
type("123456");
|
|
444
|
+
await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy());
|
|
445
|
+
const input = screen.getByTestId("confirmation-code");
|
|
446
|
+
expect(input).toHaveProperty("value", "");
|
|
447
|
+
await waitFor(() => expect(document.activeElement).toBe(input));
|
|
448
|
+
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### src/web/confirmation-code-step.tsx
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
457
|
+
import { type ReactNode, useState } from "react";
|
|
458
|
+
|
|
459
|
+
const CONFIRMATION_CODE_LENGTH = 6;
|
|
460
|
+
|
|
461
|
+
export type ConfirmationCodeStepProps = {
|
|
462
|
+
/** Resolves true when the server accepted the code. */
|
|
463
|
+
readonly onConfirm: (code: string) => Promise<boolean>;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
export function ConfirmationCodeStep({ onConfirm }: ConfirmationCodeStepProps): ReactNode {
|
|
467
|
+
const { Input, Banner } = usePrimitives();
|
|
468
|
+
const [code, setCode] = useState("");
|
|
469
|
+
const [attempt, setAttempt] = useState(0);
|
|
470
|
+
const [pending, setPending] = useState(false);
|
|
471
|
+
const [rejected, setRejected] = useState(false);
|
|
472
|
+
|
|
473
|
+
const confirm = async (submittedCode: string): Promise<void> => {
|
|
474
|
+
if (pending) return;
|
|
475
|
+
setPending(true);
|
|
476
|
+
setRejected(false);
|
|
477
|
+
const accepted = await onConfirm(submittedCode).finally(() => setPending(false));
|
|
478
|
+
if (accepted) return;
|
|
479
|
+
setRejected(true);
|
|
480
|
+
setCode("");
|
|
481
|
+
// The input was disabled while pending and lost focus; remounting restores autoFocus.
|
|
482
|
+
setAttempt((n) => n + 1);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<>
|
|
487
|
+
<Input
|
|
488
|
+
key={attempt}
|
|
489
|
+
kind="code"
|
|
490
|
+
length={CONFIRMATION_CODE_LENGTH}
|
|
491
|
+
id="confirmation-code"
|
|
492
|
+
name="confirmation-code"
|
|
493
|
+
testId="confirmation-code"
|
|
494
|
+
value={code}
|
|
495
|
+
onChange={setCode}
|
|
496
|
+
onComplete={(next) => void confirm(next)}
|
|
497
|
+
disabled={pending}
|
|
498
|
+
hasError={rejected}
|
|
499
|
+
autoFocus
|
|
500
|
+
/>
|
|
501
|
+
{rejected ? <Banner variant="error">That code is not valid. Try again.</Banner> : null}
|
|
502
|
+
</>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
```
|
|
@@ -18,7 +18,8 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
18
18
|
`kmsEnvSchema` and `s3EnvSchema`. `composeEnvSchema` collects them and
|
|
19
19
|
records which feature declared each variable.
|
|
20
20
|
- **App-level blocks in `extend`**: `buildInfoEnvSchema`, `demoAdminEnvSchema`,
|
|
21
|
-
`stripeEnvSchema({ prices })
|
|
21
|
+
`stripeEnvSchema({ prices })`, `dodoEnvSchema({ prices })` and
|
|
22
|
+
`createS3EnvSchema({ prefix })` for a
|
|
22
23
|
second bucket. These belong to no mounted feature, so the app adds them.
|
|
23
24
|
- **Typed result**: `parseEnv(composed.schema, …)` returns the merged type,
|
|
24
25
|
so `parsed.S3_BUCKET` is `string | undefined` and `parsed.APP_PUBLIC_URL` is
|
|
@@ -34,6 +35,7 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
34
35
|
| `smtpEnvSchema` | `channel-email` | `createChannelEmailFeature()` |
|
|
35
36
|
| `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3` | `createFileProviderS3Feature({ fromEnv: true })` |
|
|
36
37
|
| `stripeEnvSchema({ prices })` | `subscription-stripe` | app (`extend`); the feature registers the bridge keys |
|
|
38
|
+
| `dodoEnvSchema({ prices })` | `subscription-dodo` | app (`extend`); the feature registers the bridge keys |
|
|
37
39
|
| `mfaTokenSecretsEnvSchema` | `auth-mfa` | `createAuthMfaFeature()` |
|
|
38
40
|
| `buildInfoEnvSchema`, `demoAdminEnvSchema` | `foundation-shared` | app (`extend`) |
|
|
39
41
|
| `prometheusMetricsEnvSchema` | `framework/observability` | app (`extend`) |
|
|
@@ -45,6 +47,26 @@ Every app that boots with `runProdApp`. Pass the composed schema as
|
|
|
45
47
|
|
|
46
48
|
## Source files
|
|
47
49
|
|
|
50
|
+
### bin/server.ts
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// Production entrypoint. The env schema covers the same feature list that
|
|
54
|
+
// runProdApp mounts, so `KUMIKO_DRY_RUN_ENV=boot bun bin/server.ts` lists
|
|
55
|
+
// every variable of this app without connecting anywhere.
|
|
56
|
+
import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
|
|
57
|
+
import { composeAppEnv, createEnvCompositionFeatures } from "../src/env";
|
|
58
|
+
|
|
59
|
+
const features = createEnvCompositionFeatures();
|
|
60
|
+
|
|
61
|
+
await runProdApp({
|
|
62
|
+
features: [...features],
|
|
63
|
+
envSchema: composeAppEnv(features),
|
|
64
|
+
// The recipe declares no PII fields; crypto-shredding is mounted only for
|
|
65
|
+
// its env block.
|
|
66
|
+
allowPlaintextPii: "env-composition recipe has no PII fields",
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
48
70
|
### src/__tests__/dry-run-boot.test.ts
|
|
49
71
|
|
|
50
72
|
```ts
|
|
@@ -175,6 +197,7 @@ describe("env composition", () => {
|
|
|
175
197
|
expect(sources["S3_BUCKET"]).toBe("file-provider-s3-env");
|
|
176
198
|
expect(sources["RELEASES_S3_BUCKET"]).toBe("app");
|
|
177
199
|
expect(sources["STRIPE_PRICE_PRO"]).toBe("app");
|
|
200
|
+
expect(sources["DODO_PRODUCT_PRO"]).toBe("app");
|
|
178
201
|
});
|
|
179
202
|
|
|
180
203
|
test("parses into a typed env", () => {
|
|
@@ -183,6 +206,7 @@ describe("env composition", () => {
|
|
|
183
206
|
...minimalValidValues,
|
|
184
207
|
S3_BUCKET: "uploads",
|
|
185
208
|
STRIPE_PRICE_PRO: "price_pro",
|
|
209
|
+
DODO_PRODUCT_PRO: "pdt_pro",
|
|
186
210
|
});
|
|
187
211
|
|
|
188
212
|
const bucket: string | undefined = parsed.S3_BUCKET;
|
|
@@ -190,6 +214,7 @@ describe("env composition", () => {
|
|
|
190
214
|
expect(bucket).toBe("uploads");
|
|
191
215
|
expect(publicUrl).toBe("https://app.example.com");
|
|
192
216
|
expect(parsed.KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION).toBe("1");
|
|
217
|
+
expect(parsed.DODO_PRODUCT_PRO).toBe("pdt_pro");
|
|
193
218
|
});
|
|
194
219
|
|
|
195
220
|
test("reports every missing required variable with its source", () => {
|
|
@@ -245,6 +270,7 @@ import {
|
|
|
245
270
|
demoAdminEnvSchema,
|
|
246
271
|
} from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
|
|
247
272
|
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
273
|
+
import { dodoEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
|
|
248
274
|
import { stripeEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
249
275
|
import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";
|
|
250
276
|
import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
|
|
@@ -266,6 +292,7 @@ const appEnvSchema = z.object({
|
|
|
266
292
|
...buildInfoEnvSchema.shape,
|
|
267
293
|
...demoAdminEnvSchema.shape,
|
|
268
294
|
...stripeEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
295
|
+
...dodoEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
269
296
|
...createS3EnvSchema({ prefix: "RELEASES_S3_" }).shape,
|
|
270
297
|
APP_PUBLIC_URL: z.url().describe("Public origin of the app, used in mail links."),
|
|
271
298
|
});
|
|
@@ -107,6 +107,17 @@ describe("golden path: ticket", () => {
|
|
|
107
107
|
expect(list.rows.map((row) => row.id)).toEqual([id]);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
test("closing an already closed ticket is rejected with 422", async () => {
|
|
111
|
+
const id = await openTicket("Duplicate close");
|
|
112
|
+
const close = { id, resolution: "Fixed" };
|
|
113
|
+
await stack.http.writeOk("helpdesk:write:ticket:close", close, admin);
|
|
114
|
+
|
|
115
|
+
const again = await stack.http.writeErr("helpdesk:write:ticket:close", close, admin);
|
|
116
|
+
|
|
117
|
+
expect(again.httpStatus).toBe(422);
|
|
118
|
+
expect(again.details).toEqual({ reason: "ticket_already_closed" });
|
|
119
|
+
});
|
|
120
|
+
|
|
110
121
|
test("only an Admin may close a ticket", async () => {
|
|
111
122
|
const id = await openTicket("Slow wifi");
|
|
112
123
|
|
|
@@ -144,7 +155,7 @@ import {
|
|
|
144
155
|
emitEvent,
|
|
145
156
|
setFields,
|
|
146
157
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
147
|
-
import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
|
|
158
|
+
import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
|
|
148
159
|
import { z } from "zod";
|
|
149
160
|
|
|
150
161
|
export const ticketEntity = createEntity({
|
|
@@ -180,6 +191,7 @@ export const helpdeskFeature = defineFeature("helpdesk", (r) => {
|
|
|
180
191
|
if (!ticket) {
|
|
181
192
|
return failNotFound("ticket", event.payload.id);
|
|
182
193
|
}
|
|
194
|
+
if (ticket["isClosed"]) return failUnprocessable("ticket_already_closed");
|
|
183
195
|
await emitEvent(ctx, ticketClosed, {
|
|
184
196
|
aggregateId: event.payload.id,
|
|
185
197
|
aggregateType: "ticket",
|
|
@@ -80,7 +80,7 @@ await runProdApp({
|
|
|
80
80
|
// Required (1): routes run anonymous, need tenant resolution
|
|
81
81
|
anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
|
|
82
82
|
// Required (2): boot check + internal lookup use ctx.templateResolver
|
|
83
|
-
extraContext: ({ db }) => ({
|
|
83
|
+
extraContext: ({ db, registry }) => ({
|
|
84
84
|
templateResolver: createTemplateResolverApi(db, registry),
|
|
85
85
|
}),
|
|
86
86
|
});
|