@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,206 @@
|
|
|
1
|
+
import { ROLES } from "../auth/roles.js";
|
|
2
|
+
import { createSystemUser } from "../engine/system-user.js";
|
|
3
|
+
import { createFallbackLogger } from "../logging/index.js";
|
|
4
|
+
import { assertUnreachable } from "../utils/index.js";
|
|
5
|
+
import { NON_PUBLIC_API_PATHS, PUBLIC_API_PATHS } from "./api-constants.js";
|
|
6
|
+
import { getUser } from "./auth-middleware.js";
|
|
7
|
+
import { clientIpSourceFromHonoContext } from "./client-ip.js";
|
|
8
|
+
import { ExtraRouteEntries, ExtraRouteRejection, } from "./extra-route.js";
|
|
9
|
+
import { bodyLimitForRoutePath, DEFAULT_MAX_REQUEST_BYTES } from "./route-registrars.js";
|
|
10
|
+
import { isMissingOrAnonymousUser, makeSystemQuery, mountHonoRoute, unauthenticatedResponse, } from "./server-routes.js";
|
|
11
|
+
// Only entry:"signature" bypasses jwtGuard (verify() authenticates
|
|
12
|
+
// itself); entry:"anonymous" still needs the anonymousAccess fallthrough
|
|
13
|
+
// (tenant-by-host), entry:"user" needs c.get("user") populated.
|
|
14
|
+
export function compilePublicApiRequestMatcher(extraRoutes) {
|
|
15
|
+
const matchers = (extraRoutes ?? [])
|
|
16
|
+
.filter((route) => route.entry === ExtraRouteEntries.signature)
|
|
17
|
+
.map((route) => ({ method: route.method, pattern: honoPathToRegex(route.path) }));
|
|
18
|
+
return (c) => PUBLIC_API_PATHS.has(c.req.path) ||
|
|
19
|
+
matchers.some((m) => m.method === c.req.method && m.pattern.test(c.req.path));
|
|
20
|
+
}
|
|
21
|
+
export function mountExtraRoutes(app, wiring) {
|
|
22
|
+
const { options, dispatcher, context, clientIpResolver } = wiring;
|
|
23
|
+
// extraRoutes — declarative HTTP-routes with a
|
|
24
|
+
// required `entry` tier. Mounted after r.httpRoute: an
|
|
25
|
+
// extraRoute dispatching through `dispatcher` builds Hono's matcher, so
|
|
26
|
+
// this must run before any seed that also dispatches (runProdApp/
|
|
27
|
+
// createKumikoServer call buildServer before seeding).
|
|
28
|
+
if (options.extraRoutes) {
|
|
29
|
+
// Boot-time validation for the whole list BEFORE mounting anything —
|
|
30
|
+
// an app with one bad route should fail loud at boot, not mount N-1
|
|
31
|
+
// routes and then throw on route N.
|
|
32
|
+
for (const route of options.extraRoutes) {
|
|
33
|
+
assertValidExtraRoute(route);
|
|
34
|
+
}
|
|
35
|
+
const dispatchSystemWrite = makeDispatchSystemWrite(dispatcher);
|
|
36
|
+
const dispatchSystemQuery = makeDispatchSystemQuery(dispatcher);
|
|
37
|
+
const dispatchWriteAs = makeDispatchWriteAs(dispatcher);
|
|
38
|
+
for (const route of options.extraRoutes) {
|
|
39
|
+
const honoHandler = buildExtraRouteHonoHandler(route, {
|
|
40
|
+
app,
|
|
41
|
+
dispatcher,
|
|
42
|
+
registry: options.registry,
|
|
43
|
+
secrets: context.secrets,
|
|
44
|
+
dispatchSystemWrite,
|
|
45
|
+
dispatchSystemQuery,
|
|
46
|
+
dispatchWriteAs,
|
|
47
|
+
clientIpResolver,
|
|
48
|
+
});
|
|
49
|
+
mountHonoRoute(app, route.method, route.path, honoHandler, bodyLimitForRoutePath(route.path, options.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function assertValidExtraRoute(route) {
|
|
54
|
+
if (!isKnownExtraRouteEntry(route.entry)) {
|
|
55
|
+
throw new Error(`[kumiko] extraRoutes: unknown entry "${String(route.entry)}" on ` +
|
|
56
|
+
`"${route.method} ${route.path}" — expected "anonymous" | "user" | "signature". ` +
|
|
57
|
+
"A JS caller without the ExtraRouteDefinition type can hit this at boot.");
|
|
58
|
+
}
|
|
59
|
+
if (route.entry === ExtraRouteEntries.user && !route.path.startsWith("/api/")) {
|
|
60
|
+
throw new Error(`[kumiko] extraRoutes: entry:"user" route "${route.method} ${route.path}" must be ` +
|
|
61
|
+
"mounted under \"/api/\" — that's the only path prefix that rides the framework's " +
|
|
62
|
+
"jwtGuard chain, which is what populates deps.user.");
|
|
63
|
+
}
|
|
64
|
+
// A signature route under /api/ skips jwtGuard/origin/csrf for every
|
|
65
|
+
// path its pattern matches — a wildcard would switch off auth for
|
|
66
|
+
// unrelated /api/* handlers (e.g. "/api/*" swallows /api/write).
|
|
67
|
+
if (route.entry === ExtraRouteEntries.signature &&
|
|
68
|
+
route.path.startsWith("/api/") &&
|
|
69
|
+
route.path.includes("*")) {
|
|
70
|
+
throw new Error(`[kumiko] extraRoutes: entry:"signature" route "${route.method} ${route.path}" must not ` +
|
|
71
|
+
'use a wildcard under "/api/" — it would bypass the auth chain for every matching path.');
|
|
72
|
+
}
|
|
73
|
+
// A `:param` segment is as broad as a wildcard, and the public matchers test
|
|
74
|
+
// the bare request path: "/:seg/write" and "/api/:hook" both match /api/write.
|
|
75
|
+
if (route.entry === ExtraRouteEntries.signature) {
|
|
76
|
+
const routePattern = honoPathToRegex(route.path);
|
|
77
|
+
const shadowedFrameworkPath = [...PUBLIC_API_PATHS, ...NON_PUBLIC_API_PATHS].find((path) => routePattern.test(path));
|
|
78
|
+
if (shadowedFrameworkPath !== undefined) {
|
|
79
|
+
throw new Error(`[kumiko] extraRoutes: entry:"signature" route "${route.method} ${route.path}" must not ` +
|
|
80
|
+
`match the framework path "${shadowedFrameworkPath}" — it would bypass the auth chain for it.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// SystemAdmin write/query builders shared by mountExtraRoutes
|
|
85
|
+
// and server-runtime's `wire` hook (runProdApp/createKumikoServer,
|
|
86
|
+
// after buildServer). Privilege-scope: SystemAdmin is the highest
|
|
87
|
+
// non-tenant-scoped role — reaches ANY SystemAdmin-gated handler on ANY
|
|
88
|
+
// tenant. Only safe for callers that already proved their own authenticity
|
|
89
|
+
// (signature verify(), HMAC state, ...), never exposed to a raw request.
|
|
90
|
+
export function makeDispatchSystemWrite(dispatcher) {
|
|
91
|
+
return ({ handlerQn, payload, tenantId }) => dispatcher.write(handlerQn, payload, createSystemUser(tenantId, [ROLES.SystemAdmin]));
|
|
92
|
+
}
|
|
93
|
+
// Privilege-scope: the system user gets exactly the roles the caller names —
|
|
94
|
+
// no implicit SystemAdmin — so a seed can write data gated on app roles.
|
|
95
|
+
// Same trust requirement as makeDispatchSystemWrite: never expose to a raw
|
|
96
|
+
// request, only to code that already proved its own authenticity.
|
|
97
|
+
export function makeDispatchWriteAs(dispatcher) {
|
|
98
|
+
return ({ handlerQn, payload, tenantId, roles }) => dispatcher.write(handlerQn, payload, createSystemUser(tenantId, roles));
|
|
99
|
+
}
|
|
100
|
+
export function makeDispatchSystemQuery(dispatcher) {
|
|
101
|
+
return ({ handlerQn, payload, tenantId }) => dispatcher.query(handlerQn, payload, createSystemUser(tenantId, [ROLES.SystemAdmin]));
|
|
102
|
+
}
|
|
103
|
+
// Same dispatcher.write(...) as entry:"user", but the user is getUser(c) —
|
|
104
|
+
// see AnonymousExtraRouteDeps.write for the privilege/tenant contract.
|
|
105
|
+
function makeAnonymousWrite(
|
|
106
|
+
// biome-ignore lint/suspicious/noExplicitAny: Hono context generics are invisible at the framework boundary
|
|
107
|
+
c, dispatcher) {
|
|
108
|
+
return (type, payload) => {
|
|
109
|
+
const user = getUser(c);
|
|
110
|
+
if (!user) {
|
|
111
|
+
throw new Error('[kumiko] extraRoutes: entry:"anonymous" deps.write requires this route to be mounted ' +
|
|
112
|
+
'under "/api/" with anonymousAccess wired — no request-resolved session user was found.');
|
|
113
|
+
}
|
|
114
|
+
return dispatcher.write(type, payload, user);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const extraRouteLog = createFallbackLogger("extra-route");
|
|
118
|
+
function isKnownExtraRouteEntry(entry) {
|
|
119
|
+
return (entry === ExtraRouteEntries.anonymous ||
|
|
120
|
+
entry === ExtraRouteEntries.user ||
|
|
121
|
+
entry === ExtraRouteEntries.signature);
|
|
122
|
+
}
|
|
123
|
+
// Hono path pattern ("/api/foo/:bar", "/api/foo/*") → RegExp. Only the
|
|
124
|
+
// subset extraRoutes actually uses (static segments, `:param`, trailing
|
|
125
|
+
// `*`) — no inline `:param{regex}` constraints, no optional `:param?`.
|
|
126
|
+
function honoPathToRegex(path) {
|
|
127
|
+
const segments = path
|
|
128
|
+
.split("/")
|
|
129
|
+
.map((segment) => {
|
|
130
|
+
if (segment.startsWith(":"))
|
|
131
|
+
return "[^/]+";
|
|
132
|
+
if (segment === "*")
|
|
133
|
+
return ".*";
|
|
134
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
135
|
+
})
|
|
136
|
+
.join("/");
|
|
137
|
+
return new RegExp(`^${segments}$`);
|
|
138
|
+
}
|
|
139
|
+
function buildExtraRouteHonoHandler(route, shared) {
|
|
140
|
+
switch (route.entry) {
|
|
141
|
+
case ExtraRouteEntries.anonymous:
|
|
142
|
+
return async (c) => route.handler(c, {
|
|
143
|
+
app: shared.app,
|
|
144
|
+
registry: shared.registry,
|
|
145
|
+
systemQuery: makeSystemQuery(c, shared.dispatcher, shared.clientIpResolver),
|
|
146
|
+
write: makeAnonymousWrite(c, shared.dispatcher),
|
|
147
|
+
clientIp: shared.clientIpResolver.resolve(clientIpSourceFromHonoContext(c)),
|
|
148
|
+
});
|
|
149
|
+
case ExtraRouteEntries.user:
|
|
150
|
+
return async (c) => {
|
|
151
|
+
const user = getUser(c);
|
|
152
|
+
if (isMissingOrAnonymousUser(user)) {
|
|
153
|
+
return unauthenticatedResponse(c);
|
|
154
|
+
}
|
|
155
|
+
return route.handler(c, {
|
|
156
|
+
app: shared.app,
|
|
157
|
+
registry: shared.registry,
|
|
158
|
+
user,
|
|
159
|
+
query: (type, payload) => shared.dispatcher.query(type, payload, user),
|
|
160
|
+
write: (type, payload) => shared.dispatcher.write(type, payload, user),
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
case ExtraRouteEntries.signature:
|
|
164
|
+
return async (c) => {
|
|
165
|
+
const rawBody = await c.req.text();
|
|
166
|
+
const headers = {};
|
|
167
|
+
c.req.raw.headers.forEach((value, key) => {
|
|
168
|
+
headers[key.toLowerCase()] = value;
|
|
169
|
+
});
|
|
170
|
+
let verified;
|
|
171
|
+
try {
|
|
172
|
+
verified = await route.verify({ rawBody, headers, params: c.req.param(), query: c.req.query() }, { registry: shared.registry, secrets: shared.secrets });
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
if (e instanceof ExtraRouteRejection) {
|
|
176
|
+
if (e.retryAfterSeconds !== undefined) {
|
|
177
|
+
return c.json(e.body, e.status, { "Retry-After": String(e.retryAfterSeconds) });
|
|
178
|
+
}
|
|
179
|
+
return c.json(e.body, e.status);
|
|
180
|
+
}
|
|
181
|
+
// The body goes to unauthenticated callers; infra errors thrown by
|
|
182
|
+
// verify() (secrets, DB) must not leak, so the detail stays in the log.
|
|
183
|
+
extraRouteLog.error(`signature verify() threw on ${c.req.path}`, {
|
|
184
|
+
error: e instanceof Error ? e.message : String(e),
|
|
185
|
+
});
|
|
186
|
+
return c.json({
|
|
187
|
+
error: {
|
|
188
|
+
code: "extra_route_signature_invalid",
|
|
189
|
+
message: "signature verification failed",
|
|
190
|
+
},
|
|
191
|
+
}, 401);
|
|
192
|
+
}
|
|
193
|
+
return route.handler(c, verified, {
|
|
194
|
+
app: shared.app,
|
|
195
|
+
registry: shared.registry,
|
|
196
|
+
secrets: shared.secrets,
|
|
197
|
+
systemQuery: makeSystemQuery(c, shared.dispatcher, shared.clientIpResolver),
|
|
198
|
+
dispatchSystemWrite: shared.dispatchSystemWrite,
|
|
199
|
+
dispatchSystemQuery: shared.dispatchSystemQuery,
|
|
200
|
+
dispatchWriteAs: shared.dispatchWriteAs,
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
default:
|
|
204
|
+
return assertUnreachable(route, "extra route entry");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Hono, MiddlewareHandler } from "hono";
|
|
2
|
+
import type { ObservabilityProvider } from "../observability/index.ts";
|
|
3
|
+
import { type TenantLifecycleStatusResolver } from "./auth-middleware.ts";
|
|
4
|
+
import type { ServerOptions } from "./server.ts";
|
|
5
|
+
import type { PublicApiRequestMatcher } from "./server-extra-routes.ts";
|
|
6
|
+
import type { ServerWiring } from "./server-wiring.ts";
|
|
7
|
+
import { type ResolvedSessionCookies } from "./session-cookies.ts";
|
|
8
|
+
export declare function installSessionCookies(app: Hono, auth: ServerOptions["auth"]): ResolvedSessionCookies;
|
|
9
|
+
export declare function installRequestId(app: Hono, wiring: Pick<ServerWiring, "options" | "clientIpResolver">): void;
|
|
10
|
+
export declare function installRateLimitMiddleware(app: Hono, wiring: Pick<ServerWiring, "options" | "rateLimitResolver" | "rateLimitMiddlewareRequested" | "clientIpResolver" | "observability">): MiddlewareHandler | undefined;
|
|
11
|
+
export declare function installObservability(app: Hono, observability: ObservabilityProvider): void;
|
|
12
|
+
export declare function installPiiLeakGuard(app: Hono): void;
|
|
13
|
+
export type AuthGuardChain = {
|
|
14
|
+
readonly tenantLifecycleResolver: TenantLifecycleStatusResolver | undefined;
|
|
15
|
+
readonly sessionOnlyHttpRouteGuards: readonly MiddlewareHandler[];
|
|
16
|
+
};
|
|
17
|
+
export declare function installAuthGuardChain(app: Hono, wiring: Pick<ServerWiring, "options" | "jwt" | "baseDb">, scope: {
|
|
18
|
+
readonly isPublicApiRequest: PublicApiRequestMatcher;
|
|
19
|
+
readonly globalRateLimitGuard: MiddlewareHandler | undefined;
|
|
20
|
+
}): AuthGuardChain;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { resolveTenantLifecyclePlugin } from "../pipeline/active-membership.js";
|
|
2
|
+
import { authEndpointRateLimit, globalIpRateLimit } from "../rate-limit/index.js";
|
|
3
|
+
import { authMiddleware, getUser } from "./auth-middleware.js";
|
|
4
|
+
import { assertSessionCallbacksPaired } from "./auth-routes.js";
|
|
5
|
+
import { csrfMiddleware } from "./csrf-middleware.js";
|
|
6
|
+
import { observabilityMiddleware } from "./observability-middleware.js";
|
|
7
|
+
import { assertOriginGuardConfig, normalizeOrigin, resolveOriginGuard, } from "./origin-middleware.js";
|
|
8
|
+
import { patRouteGuard } from "./pat-route-guard.js";
|
|
9
|
+
import { piiCiphertextResponseGuard } from "./pii-leak-guard.js";
|
|
10
|
+
import { requestIdMiddleware } from "./request-id-middleware.js";
|
|
11
|
+
import { resolveSessionCookies, sessionCookiesMiddleware, } from "./session-cookies.js";
|
|
12
|
+
export function installSessionCookies(app, auth) {
|
|
13
|
+
// Resolved once; auth routes, auth/csrf middleware and the tenant-cookie
|
|
14
|
+
// helpers read the cookie names from the request context.
|
|
15
|
+
const sessionCookies = resolveSessionCookies({
|
|
16
|
+
secureCookies: auth?.secureCookies,
|
|
17
|
+
cookieDomain: auth?.cookieDomain,
|
|
18
|
+
retiredCookieDomains: auth?.retiredCookieDomains,
|
|
19
|
+
});
|
|
20
|
+
app.use("*", sessionCookiesMiddleware(sessionCookies));
|
|
21
|
+
return sessionCookies;
|
|
22
|
+
}
|
|
23
|
+
export function installRequestId(app, wiring) {
|
|
24
|
+
const { options, clientIpResolver } = wiring;
|
|
25
|
+
app.use("/api/*", requestIdMiddleware({
|
|
26
|
+
resolver: clientIpResolver,
|
|
27
|
+
...(options.errorDocs && { errorDocs: options.errorDocs }),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
export function installRateLimitMiddleware(app, wiring) {
|
|
31
|
+
const { options, rateLimitResolver, clientIpResolver, observability } = wiring;
|
|
32
|
+
// L1/L2 rate-limit middleware run BEFORE auth so an unauthenticated
|
|
33
|
+
// flood can't even reach the JWT-verify code path. Wired only when
|
|
34
|
+
// the caller passed `rateLimit.global` or `rateLimit.auth`. The
|
|
35
|
+
// resolver is the auto-wired one (or `context.rateLimit` if set);
|
|
36
|
+
// boot-fails loudly when the caller asked for middleware without a
|
|
37
|
+
// working Redis to back it.
|
|
38
|
+
let globalRateLimitGuard;
|
|
39
|
+
if (wiring.rateLimitMiddlewareRequested) {
|
|
40
|
+
if (!rateLimitResolver) {
|
|
41
|
+
throw new Error("rateLimit middleware requested but no resolver available — pass `context.redis` " +
|
|
42
|
+
"or `context.rateLimit` so the resolver can be built.");
|
|
43
|
+
}
|
|
44
|
+
if (options.rateLimit?.global) {
|
|
45
|
+
globalRateLimitGuard = globalIpRateLimit({
|
|
46
|
+
...options.rateLimit.global,
|
|
47
|
+
resolver: rateLimitResolver,
|
|
48
|
+
clientIpResolver,
|
|
49
|
+
meter: observability.meter,
|
|
50
|
+
});
|
|
51
|
+
app.use("/api/*", globalRateLimitGuard);
|
|
52
|
+
}
|
|
53
|
+
if (options.rateLimit?.auth) {
|
|
54
|
+
const { path: l2Path = "/api/auth/*", ...l2Opts } = options.rateLimit.auth;
|
|
55
|
+
app.use(l2Path, authEndpointRateLimit({ ...l2Opts, resolver: rateLimitResolver, clientIpResolver }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return globalRateLimitGuard;
|
|
59
|
+
}
|
|
60
|
+
export function installObservability(app, observability) {
|
|
61
|
+
// Observability span wraps everything that follows (auth, routes).
|
|
62
|
+
// Must come AFTER request-id (so span can carry the id) and BEFORE auth
|
|
63
|
+
// (so auth-verify can be a child span once we instrument it in v2).
|
|
64
|
+
app.use("/api/*", observabilityMiddleware({
|
|
65
|
+
tracer: observability.tracer,
|
|
66
|
+
meter: observability.meter,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
export function installPiiLeakGuard(app) {
|
|
70
|
+
// Tripwire: a kumiko-pii: ciphertext in a JSON response is always a bug
|
|
71
|
+
// (raw read leaked past a decrypt) — dev fails loud, prod redacts.
|
|
72
|
+
app.use("/api/*", piiCiphertextResponseGuard());
|
|
73
|
+
}
|
|
74
|
+
export function installAuthGuardChain(app, wiring, scope) {
|
|
75
|
+
const { options, jwt, baseDb } = wiring;
|
|
76
|
+
const { isPublicApiRequest, globalRateLimitGuard } = scope;
|
|
77
|
+
// Auth middleware skips public paths (login, health) — those routes need
|
|
78
|
+
// to be callable without a valid JWT. Every other /api/* request requires
|
|
79
|
+
// a token (or, when anonymousAccess is wired, falls through as anonymous).
|
|
80
|
+
// A session-checker is forwarded when the auth-config wires one, so the
|
|
81
|
+
// middleware can reject revoked sids on every request.
|
|
82
|
+
// Mounting a tenantLifecycleStatus provider is what turns the 410 on, so no
|
|
83
|
+
// entrypoint can forget the wiring; `??` short-circuits, so an explicit
|
|
84
|
+
// auth.resolveTenantLifecycleStatus remains the override.
|
|
85
|
+
const tenantLifecycleResolver = options.auth?.resolveTenantLifecycleStatus ??
|
|
86
|
+
deriveTenantLifecycleResolver(options.registry, baseDb);
|
|
87
|
+
// sessionOnlyGuard deliberately does NOT get this — its callers have no
|
|
88
|
+
// anonymousAccess fallback, so a foreign-origin cookie there must keep
|
|
89
|
+
// failing through the sibling originMiddleware 403.
|
|
90
|
+
const normalizedForeignCookieOrigins = options.auth?.allowedOrigins?.length
|
|
91
|
+
? new Set(options.auth.allowedOrigins.map(normalizeOrigin))
|
|
92
|
+
: undefined;
|
|
93
|
+
const jwtGuard = buildJwtGuard(options, jwt, tenantLifecycleResolver, normalizedForeignCookieOrigins);
|
|
94
|
+
useOnProtectedApiPaths(app, isPublicApiRequest, jwtGuard);
|
|
95
|
+
// Fail-closed PAT scoping: a PAT-authenticated caller (user.pat set by
|
|
96
|
+
// jwtGuard's tokenVerifier above) may only reach the dispatcher routes —
|
|
97
|
+
// everywhere else on /api/* is off-limits regardless of granted scopes.
|
|
98
|
+
const patRouteGuardMiddleware = patRouteGuard();
|
|
99
|
+
useOnProtectedApiPaths(app, isPublicApiRequest, patRouteGuardMiddleware);
|
|
100
|
+
// Without anonymousAccess a missing token 401s instead of falling through as anonymous.
|
|
101
|
+
const sessionOnlyGuard = buildSessionOnlyGuard(options, jwt, tenantLifecycleResolver);
|
|
102
|
+
// PAT rate limiting — runs AFTER the auth guard so the resolved principal is
|
|
103
|
+
// available. Only PAT-authenticated requests are counted (keyed by token id);
|
|
104
|
+
// cookie/JWT users pass through untouched. In-memory limiter is per-instance
|
|
105
|
+
// (see run-prod-app) — a multi-node deployment wanting a shared counter swaps
|
|
106
|
+
// in a Redis-backed LoginRateLimiter.
|
|
107
|
+
const patRateLimiter = options.auth?.patRateLimiter;
|
|
108
|
+
const patRateLimitGuard = patRateLimiter ? buildPatRateLimitGuard(patRateLimiter) : undefined;
|
|
109
|
+
if (patRateLimitGuard) {
|
|
110
|
+
useOnProtectedApiPaths(app, isPublicApiRequest, patRateLimitGuard);
|
|
111
|
+
}
|
|
112
|
+
// Origin-allowlist guard — additional CSRF-hardening for deployments that
|
|
113
|
+
// widen the auth cookie across subdomains (auth.cookieDomain). Registered
|
|
114
|
+
// when allowedOrigins is non-empty, or with host-only cookies when
|
|
115
|
+
// auth.publicOrigin is known (see resolveOriginGuard), with the same /api/* + public-skip
|
|
116
|
+
// scope as the CSRF guard and BEFORE it, so a disallowed cross-site POST
|
|
117
|
+
// surfaces as `origin_not_allowed` rather than `csrf_token_mismatch`. Fails
|
|
118
|
+
// closed (assertOriginGuardConfig throws) when a wide cookieDomain is set
|
|
119
|
+
// without an allowlist and without an explicit opt-out — that config is the
|
|
120
|
+
// unguarded-subdomain-XSS footgun, not a warn-and-continue case.
|
|
121
|
+
assertOriginGuardConfig(options.auth);
|
|
122
|
+
assertSessionCallbacksPaired(options.auth);
|
|
123
|
+
const originGuard = resolveOriginGuard(options.auth);
|
|
124
|
+
if (originGuard) {
|
|
125
|
+
useOnProtectedApiPaths(app, isPublicApiRequest, originGuard);
|
|
126
|
+
}
|
|
127
|
+
// Double-submit CSRF guard — runs only on cookie-authenticated,
|
|
128
|
+
// state-changing requests (POST/PUT/PATCH/DELETE). The guard reads the
|
|
129
|
+
// authTransport flag set by authMiddleware, so public paths (no auth)
|
|
130
|
+
// and bearer-authenticated paths (no cookie vector) fall straight
|
|
131
|
+
// through. Must be registered AFTER the auth middleware above so the
|
|
132
|
+
// flag is populated; registered for the same scope so /api/* routes
|
|
133
|
+
// are covered uniformly.
|
|
134
|
+
const csrfGuard = csrfMiddleware();
|
|
135
|
+
useOnProtectedApiPaths(app, isPublicApiRequest, csrfGuard);
|
|
136
|
+
// No PAT rate-limit here: patRouteGuardMiddleware rejects every PAT on httpRoutes before it could count.
|
|
137
|
+
// httpRoutes sit outside /api/*, so the pre-auth /api/* middlewares do not run for them: the
|
|
138
|
+
// global IP limit (an unauthenticated flood must not reach JWT verify / session checks) and the
|
|
139
|
+
// PII ciphertext tripwire are re-attached here. requestId, body limit and observability are not.
|
|
140
|
+
const sessionOnlyHttpRouteGuards = [
|
|
141
|
+
...(globalRateLimitGuard ? [globalRateLimitGuard] : []),
|
|
142
|
+
piiCiphertextResponseGuard(),
|
|
143
|
+
sessionOnlyGuard,
|
|
144
|
+
patRouteGuardMiddleware,
|
|
145
|
+
...(originGuard ? [originGuard] : []),
|
|
146
|
+
csrfGuard,
|
|
147
|
+
];
|
|
148
|
+
return { tenantLifecycleResolver, sessionOnlyHttpRouteGuards };
|
|
149
|
+
}
|
|
150
|
+
function useOnProtectedApiPaths(app, isPublicApiRequest, guard) {
|
|
151
|
+
app.use("/api/*", async (c, next) => {
|
|
152
|
+
if (isPublicApiRequest(c))
|
|
153
|
+
return next();
|
|
154
|
+
return guard(c, next);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function collectTenantlessAnonymousWriteQns(registry) {
|
|
158
|
+
return new Set([...registry.getAllWriteHandlers()]
|
|
159
|
+
.filter(([, handler]) => handler.tenantlessAnonymous === true)
|
|
160
|
+
.map(([qn]) => qn));
|
|
161
|
+
}
|
|
162
|
+
function buildJwtGuard(options, jwt, tenantLifecycleResolver, normalizedForeignCookieOrigins) {
|
|
163
|
+
const tenantlessAnonymousQns = collectTenantlessAnonymousWriteQns(options.registry);
|
|
164
|
+
return authMiddleware(jwt, {
|
|
165
|
+
...(tenantlessAnonymousQns.size > 0
|
|
166
|
+
? { isTenantlessAnonymousWrite: (qn) => tenantlessAnonymousQns.has(qn) }
|
|
167
|
+
: {}),
|
|
168
|
+
...(options.auth?.sessionChecker ? { sessionChecker: options.auth.sessionChecker } : {}),
|
|
169
|
+
...(options.auth?.tokenVerifier ? { tokenVerifier: options.auth.tokenVerifier } : {}),
|
|
170
|
+
...(tenantLifecycleResolver ? { resolveTenantLifecycleStatus: tenantLifecycleResolver } : {}),
|
|
171
|
+
...(options.anonymousAccess ? { anonymousAccess: options.anonymousAccess } : {}),
|
|
172
|
+
...(normalizedForeignCookieOrigins
|
|
173
|
+
? { foreignCookieOrigins: normalizedForeignCookieOrigins }
|
|
174
|
+
: {}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function buildSessionOnlyGuard(options, jwt, tenantLifecycleResolver) {
|
|
178
|
+
return authMiddleware(jwt, {
|
|
179
|
+
...(options.auth?.sessionChecker ? { sessionChecker: options.auth.sessionChecker } : {}),
|
|
180
|
+
...(options.auth?.tokenVerifier ? { tokenVerifier: options.auth.tokenVerifier } : {}),
|
|
181
|
+
...(tenantLifecycleResolver ? { resolveTenantLifecycleStatus: tenantLifecycleResolver } : {}),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Must run after the auth guard so getUser(c) carries the PAT.
|
|
185
|
+
function buildPatRateLimitGuard(patRateLimiter) {
|
|
186
|
+
return async (c, next) => {
|
|
187
|
+
const pat = getUser(c)?.pat;
|
|
188
|
+
if (pat && !(await patRateLimiter.check(pat.tokenId))) {
|
|
189
|
+
return c.json({
|
|
190
|
+
error: {
|
|
191
|
+
code: "pat_rate_limited",
|
|
192
|
+
httpStatus: 429,
|
|
193
|
+
message: "personal access token rate limit exceeded",
|
|
194
|
+
i18nKey: "auth.errors.patRateLimited",
|
|
195
|
+
},
|
|
196
|
+
}, 429);
|
|
197
|
+
}
|
|
198
|
+
return next();
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function deriveTenantLifecycleResolver(registry, db) {
|
|
202
|
+
const plugin = resolveTenantLifecyclePlugin(registry, "buildServer");
|
|
203
|
+
if (!plugin)
|
|
204
|
+
return undefined;
|
|
205
|
+
if (!db) {
|
|
206
|
+
throw new Error("[kumiko] a tenantLifecycleStatus provider is mounted (tenant-lifecycle) but context.db is " +
|
|
207
|
+
"missing — the request-level teardown gate needs a DbConnection. Pass context.db, or wire " +
|
|
208
|
+
"auth.resolveTenantLifecycleStatus explicitly.");
|
|
209
|
+
}
|
|
210
|
+
return (tenantId) => plugin.resolveStatus(tenantId, { db });
|
|
211
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Hono, MiddlewareHandler } from "hono";
|
|
2
|
+
import type { HttpRouteMethod, SessionUser, TenantId } from "../engine/types/index.ts";
|
|
3
|
+
import type { Dispatcher } from "../pipeline/dispatcher.ts";
|
|
4
|
+
import { type ClientIpResolver } from "./client-ip.ts";
|
|
5
|
+
import type { ServerOptions } from "./server.ts";
|
|
6
|
+
import type { ServerWiring } from "./server-wiring.ts";
|
|
7
|
+
import type { ResolvedSessionCookies } from "./session-cookies.ts";
|
|
8
|
+
export declare function mountOperationalRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "baseDb" | "eventConsumers" | "observability" | "bootLog">): void;
|
|
9
|
+
export declare function mountAuthRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "jwt" | "clientIpResolver">, sessionCookies: ResolvedSessionCookies): void;
|
|
10
|
+
export declare function mountClientErrorRoute(app: Hono, wiring: Pick<ServerWiring, "rateLimitResolver" | "tracedRedis" | "clientIpResolver" | "observability" | "jwt">): void;
|
|
11
|
+
export declare function mountDispatcherRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "sseBroker">): void;
|
|
12
|
+
export declare function mountSchemaRoute(app: Hono, options: Pick<ServerOptions, "registry" | "dispatcherOptions" | "context">): void;
|
|
13
|
+
export declare function mountFileRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "fileProviderResolver" | "rateLimitResolver" | "tracedRedis">): void;
|
|
14
|
+
export declare function mountFeatureHttpRoutes(app: Hono, wiring: Pick<ServerWiring, "options" | "dispatcher" | "clientIpResolver" | "rateLimitResolver" | "observability">, sessionOnlyHttpRouteGuards: readonly MiddlewareHandler[]): void;
|
|
15
|
+
export declare function installNoRouteMatchMarker(app: Hono): void;
|
|
16
|
+
export declare function mountHonoRoute(app: Hono, method: HttpRouteMethod, path: string, handler: (c: import("hono").Context<any, any>) => Response | Promise<Response>, middlewares?: readonly MiddlewareHandler[]): void;
|
|
17
|
+
export declare function makeSystemQuery(c: import("hono").Context<any, any>, dispatcher: Dispatcher, clientIpResolver?: ClientIpResolver): (type: string, payload: unknown, tenantId: TenantId) => Promise<unknown>;
|
|
18
|
+
export declare function isMissingOrAnonymousUser(user: SessionUser | undefined): boolean;
|
|
19
|
+
export declare function unauthenticatedResponse(c: import("hono").Context): Response;
|