@kindgi/api 0.1.4-rc.5 → 0.1.5-rc.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/dist/agent-binding.d.ts +13 -0
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/agent-pins.d.ts.map +1 -1
- package/dist/agent-pins.js +18 -2
- package/dist/agent-pins.js.map +1 -1
- package/dist/app.d.ts +110 -17
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +163 -35
- package/dist/app.js.map +1 -1
- package/dist/block-publish.d.ts +56 -0
- package/dist/block-publish.d.ts.map +1 -0
- package/dist/block-publish.js +132 -0
- package/dist/block-publish.js.map +1 -0
- package/dist/caller.d.ts +24 -0
- package/dist/caller.d.ts.map +1 -0
- package/dist/caller.js +47 -0
- package/dist/caller.js.map +1 -0
- package/dist/capability-binding.d.ts +11 -0
- package/dist/capability-binding.d.ts.map +1 -1
- package/dist/derive-agent-version.d.ts +7 -0
- package/dist/derive-agent-version.d.ts.map +1 -1
- package/dist/derive-agent-version.js +4 -0
- package/dist/derive-agent-version.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +73 -7
- package/dist/errors.js.map +1 -1
- package/dist/eval-case-binding.d.ts +16 -4
- package/dist/eval-case-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.d.ts +31 -0
- package/dist/eval-run-binding.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.d.ts +7 -0
- package/dist/eval-run-dispatcher.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.js.map +1 -1
- package/dist/eval-sample.d.ts +11 -0
- package/dist/eval-sample.d.ts.map +1 -0
- package/dist/eval-sample.js +34 -0
- package/dist/eval-sample.js.map +1 -0
- package/dist/eval-suite-binding.d.ts +13 -0
- package/dist/eval-suite-binding.d.ts.map +1 -1
- package/dist/eval-suite-binding.js.map +1 -1
- package/dist/flow-binding.d.ts +18 -0
- package/dist/flow-binding.d.ts.map +1 -1
- package/dist/flow-pins.d.ts.map +1 -1
- package/dist/flow-pins.js +18 -2
- package/dist/flow-pins.js.map +1 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +25 -0
- package/dist/gate.js.map +1 -1
- package/dist/handler-binding.d.ts +32 -0
- package/dist/handler-binding.d.ts.map +1 -1
- package/dist/hitl-binding.d.ts +23 -0
- package/dist/hitl-binding.d.ts.map +1 -1
- package/dist/identity-directory-binding.d.ts +82 -4
- package/dist/identity-directory-binding.d.ts.map +1 -1
- package/dist/identity-provider-binding.d.ts +197 -17
- package/dist/identity-provider-binding.d.ts.map +1 -1
- package/dist/improvement-pass-binding.d.ts +196 -0
- package/dist/improvement-pass-binding.d.ts.map +1 -0
- package/dist/improvement-pass-binding.js +9 -0
- package/dist/improvement-pass-binding.js.map +1 -0
- package/dist/index.d.ts +35 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +25 -4
- package/dist/judged-dispatcher.d.ts.map +1 -1
- package/dist/judged-dispatcher.js +43 -6
- package/dist/judged-dispatcher.js.map +1 -1
- package/dist/judgment-binding.d.ts +25 -1
- package/dist/judgment-binding.d.ts.map +1 -1
- package/dist/mcp-endpoint-binding.d.ts +7 -0
- package/dist/mcp-endpoint-binding.d.ts.map +1 -1
- package/dist/memory-binding.d.ts +124 -17
- package/dist/memory-binding.d.ts.map +1 -1
- package/dist/memory-erasure-binding.d.ts +145 -0
- package/dist/memory-erasure-binding.d.ts.map +1 -0
- package/dist/memory-erasure-binding.js +4 -0
- package/dist/memory-erasure-binding.js.map +1 -0
- package/dist/middleware/auth.d.ts +66 -13
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +150 -50
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/authorize.d.ts +6 -0
- package/dist/middleware/authorize.d.ts.map +1 -1
- package/dist/middleware/authorize.js +113 -4
- package/dist/middleware/authorize.js.map +1 -1
- package/dist/middleware/error-mapper.d.ts +5 -0
- package/dist/middleware/error-mapper.d.ts.map +1 -1
- package/dist/middleware/error-mapper.js +14 -1
- package/dist/middleware/error-mapper.js.map +1 -1
- package/dist/middleware/idempotency.d.ts +70 -5
- package/dist/middleware/idempotency.d.ts.map +1 -1
- package/dist/middleware/idempotency.js +174 -41
- package/dist/middleware/idempotency.js.map +1 -1
- package/dist/middleware/key-project.d.ts +17 -0
- package/dist/middleware/key-project.d.ts.map +1 -0
- package/dist/middleware/key-project.js +75 -0
- package/dist/middleware/key-project.js.map +1 -0
- package/dist/middleware/principal.d.ts.map +1 -1
- package/dist/middleware/principal.js +9 -5
- package/dist/middleware/principal.js.map +1 -1
- package/dist/middleware/project-ref.d.ts.map +1 -1
- package/dist/middleware/project-ref.js +3 -0
- package/dist/middleware/project-ref.js.map +1 -1
- package/dist/openapi/generate.d.ts.map +1 -1
- package/dist/openapi/generate.js +5 -2
- package/dist/openapi/generate.js.map +1 -1
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +918 -139
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +92 -13
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +2122 -370
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/person-grants-binding.d.ts +77 -0
- package/dist/person-grants-binding.d.ts.map +1 -0
- package/dist/person-grants-binding.js +4 -0
- package/dist/person-grants-binding.js.map +1 -0
- package/dist/proposal-service.d.ts +121 -0
- package/dist/proposal-service.d.ts.map +1 -0
- package/dist/proposal-service.js +447 -0
- package/dist/proposal-service.js.map +1 -0
- package/dist/proposal-status.d.ts +60 -0
- package/dist/proposal-status.d.ts.map +1 -0
- package/dist/proposal-status.js +109 -0
- package/dist/proposal-status.js.map +1 -0
- package/dist/provider-binding.d.ts +10 -0
- package/dist/provider-binding.d.ts.map +1 -1
- package/dist/publish-refused.d.ts +18 -3
- package/dist/publish-refused.d.ts.map +1 -1
- package/dist/publish-refused.js +18 -3
- package/dist/publish-refused.js.map +1 -1
- package/dist/routes/adapters.d.ts +2 -1
- package/dist/routes/adapters.d.ts.map +1 -1
- package/dist/routes/adapters.js +3 -1
- package/dist/routes/adapters.js.map +1 -1
- package/dist/routes/agent-releases.d.ts +55 -3
- package/dist/routes/agent-releases.d.ts.map +1 -1
- package/dist/routes/agent-releases.js +85 -55
- package/dist/routes/agent-releases.js.map +1 -1
- package/dist/routes/agents.d.ts +12 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +100 -2
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/approvals.d.ts +15 -3
- package/dist/routes/approvals.d.ts.map +1 -1
- package/dist/routes/approvals.js +133 -122
- package/dist/routes/approvals.js.map +1 -1
- package/dist/routes/artifacts.d.ts +25 -3
- package/dist/routes/artifacts.d.ts.map +1 -1
- package/dist/routes/artifacts.js +116 -7
- package/dist/routes/artifacts.js.map +1 -1
- package/dist/routes/auth.d.ts +25 -1
- package/dist/routes/auth.d.ts.map +1 -1
- package/dist/routes/auth.js +655 -281
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/blocks.d.ts.map +1 -1
- package/dist/routes/blocks.js +2 -42
- package/dist/routes/blocks.js.map +1 -1
- package/dist/routes/capabilities.d.ts +2 -1
- package/dist/routes/capabilities.d.ts.map +1 -1
- package/dist/routes/capabilities.js +6 -1
- package/dist/routes/capabilities.js.map +1 -1
- package/dist/routes/compliance.d.ts +9 -10
- package/dist/routes/compliance.d.ts.map +1 -1
- package/dist/routes/compliance.js +42 -61
- package/dist/routes/compliance.js.map +1 -1
- package/dist/routes/conversations.d.ts +8 -1
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +87 -8
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/cost.d.ts +7 -1
- package/dist/routes/cost.d.ts.map +1 -1
- package/dist/routes/cost.js +22 -3
- package/dist/routes/cost.js.map +1 -1
- package/dist/routes/denied.d.ts +11 -0
- package/dist/routes/denied.d.ts.map +1 -0
- package/dist/routes/denied.js +17 -0
- package/dist/routes/denied.js.map +1 -0
- package/dist/routes/deployments.d.ts +2 -1
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +44 -12
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts.map +1 -1
- package/dist/routes/eval-comparison.js +69 -1
- package/dist/routes/eval-comparison.js.map +1 -1
- package/dist/routes/eval-overrides.d.ts +18 -0
- package/dist/routes/eval-overrides.d.ts.map +1 -0
- package/dist/routes/eval-overrides.js +81 -0
- package/dist/routes/eval-overrides.js.map +1 -0
- package/dist/routes/eval-runs.d.ts +10 -1
- package/dist/routes/eval-runs.d.ts.map +1 -1
- package/dist/routes/eval-runs.js +63 -7
- package/dist/routes/eval-runs.js.map +1 -1
- package/dist/routes/eval-suites.d.ts.map +1 -1
- package/dist/routes/eval-suites.js +20 -1
- package/dist/routes/eval-suites.js.map +1 -1
- package/dist/routes/event-triggers.d.ts +5 -1
- package/dist/routes/event-triggers.d.ts.map +1 -1
- package/dist/routes/event-triggers.js +26 -3
- package/dist/routes/event-triggers.js.map +1 -1
- package/dist/routes/export-signing-keys.d.ts +12 -0
- package/dist/routes/export-signing-keys.d.ts.map +1 -0
- package/dist/routes/export-signing-keys.js +24 -0
- package/dist/routes/export-signing-keys.js.map +1 -0
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +9 -1
- package/dist/routes/flows.js.map +1 -1
- package/dist/routes/gate-policies.d.ts +4 -1
- package/dist/routes/gate-policies.d.ts.map +1 -1
- package/dist/routes/gate-policies.js +29 -5
- package/dist/routes/gate-policies.js.map +1 -1
- package/dist/routes/guardrails.d.ts +16 -14
- package/dist/routes/guardrails.d.ts.map +1 -1
- package/dist/routes/guardrails.js +39 -3
- package/dist/routes/guardrails.js.map +1 -1
- package/dist/routes/identity.d.ts +20 -4
- package/dist/routes/identity.d.ts.map +1 -1
- package/dist/routes/identity.js +246 -6
- package/dist/routes/identity.js.map +1 -1
- package/dist/routes/improvement-passes.d.ts +52 -0
- package/dist/routes/improvement-passes.d.ts.map +1 -0
- package/dist/routes/improvement-passes.js +449 -0
- package/dist/routes/improvement-passes.js.map +1 -0
- package/dist/routes/judged-suites.d.ts +50 -0
- package/dist/routes/judged-suites.d.ts.map +1 -1
- package/dist/routes/judged-suites.js +66 -27
- package/dist/routes/judged-suites.js.map +1 -1
- package/dist/routes/judgment-context.d.ts +18 -2
- package/dist/routes/judgment-context.d.ts.map +1 -1
- package/dist/routes/judgment-context.js +63 -5
- package/dist/routes/judgment-context.js.map +1 -1
- package/dist/routes/judgment-flow-context.d.ts.map +1 -1
- package/dist/routes/judgment-flow-context.js +14 -2
- package/dist/routes/judgment-flow-context.js.map +1 -1
- package/dist/routes/judgments.d.ts.map +1 -1
- package/dist/routes/judgments.js +8 -1
- package/dist/routes/judgments.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +26 -1
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/memory-access.d.ts +43 -0
- package/dist/routes/memory-access.d.ts.map +1 -0
- package/dist/routes/memory-access.js +129 -0
- package/dist/routes/memory-access.js.map +1 -0
- package/dist/routes/memory-erasures.d.ts +25 -0
- package/dist/routes/memory-erasures.d.ts.map +1 -0
- package/dist/routes/memory-erasures.js +214 -0
- package/dist/routes/memory-erasures.js.map +1 -0
- package/dist/routes/memory-parse.d.ts +38 -0
- package/dist/routes/memory-parse.d.ts.map +1 -0
- package/dist/routes/memory-parse.js +199 -0
- package/dist/routes/memory-parse.js.map +1 -0
- package/dist/routes/memory.d.ts +9 -4
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +226 -234
- package/dist/routes/memory.js.map +1 -1
- package/dist/routes/observations.d.ts +4 -1
- package/dist/routes/observations.d.ts.map +1 -1
- package/dist/routes/observations.js +8 -2
- package/dist/routes/observations.js.map +1 -1
- package/dist/routes/pagination.d.ts +7 -0
- package/dist/routes/pagination.d.ts.map +1 -1
- package/dist/routes/pagination.js +10 -0
- package/dist/routes/pagination.js.map +1 -1
- package/dist/routes/policies.d.ts +2 -1
- package/dist/routes/policies.d.ts.map +1 -1
- package/dist/routes/policies.js +3 -1
- package/dist/routes/policies.js.map +1 -1
- package/dist/routes/project-mismatch.d.ts +22 -0
- package/dist/routes/project-mismatch.d.ts.map +1 -0
- package/dist/routes/project-mismatch.js +34 -0
- package/dist/routes/project-mismatch.js.map +1 -0
- package/dist/routes/projects.d.ts +15 -4
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +100 -9
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/proposals.d.ts +25 -17
- package/dist/routes/proposals.d.ts.map +1 -1
- package/dist/routes/proposals.js +340 -507
- package/dist/routes/proposals.js.map +1 -1
- package/dist/routes/provenance.d.ts +17 -3
- package/dist/routes/provenance.d.ts.map +1 -1
- package/dist/routes/provenance.js +79 -103
- package/dist/routes/provenance.js.map +1 -1
- package/dist/routes/providers.d.ts +17 -1
- package/dist/routes/providers.d.ts.map +1 -1
- package/dist/routes/providers.js +188 -8
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/public-run-tokens.d.ts.map +1 -1
- package/dist/routes/public-run-tokens.js +3 -0
- package/dist/routes/public-run-tokens.js.map +1 -1
- package/dist/routes/runs.d.ts +10 -0
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +92 -42
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/schedules.d.ts +21 -13
- package/dist/routes/schedules.d.ts.map +1 -1
- package/dist/routes/schedules.js +416 -55
- package/dist/routes/schedules.js.map +1 -1
- package/dist/routes/segments.d.ts +2 -0
- package/dist/routes/segments.d.ts.map +1 -1
- package/dist/routes/segments.js +5 -0
- package/dist/routes/segments.js.map +1 -1
- package/dist/routes/service-accounts.d.ts +17 -0
- package/dist/routes/service-accounts.d.ts.map +1 -0
- package/dist/routes/service-accounts.js +212 -0
- package/dist/routes/service-accounts.js.map +1 -0
- package/dist/routes/sign-in-options.d.ts +50 -0
- package/dist/routes/sign-in-options.d.ts.map +1 -0
- package/dist/routes/sign-in-options.js +98 -0
- package/dist/routes/sign-in-options.js.map +1 -0
- package/dist/routes/signing-keys.d.ts +2 -1
- package/dist/routes/signing-keys.d.ts.map +1 -1
- package/dist/routes/signing-keys.js +3 -1
- package/dist/routes/signing-keys.js.map +1 -1
- package/dist/routes/tenant-access.d.ts +13 -0
- package/dist/routes/tenant-access.d.ts.map +1 -0
- package/dist/routes/tenant-access.js +26 -0
- package/dist/routes/tenant-access.js.map +1 -0
- package/dist/routes/tenant.d.ts.map +1 -1
- package/dist/routes/tenant.js +4 -0
- package/dist/routes/tenant.js.map +1 -1
- package/dist/routes/token-sign-in.d.ts +36 -0
- package/dist/routes/token-sign-in.d.ts.map +1 -0
- package/dist/routes/token-sign-in.js +101 -0
- package/dist/routes/token-sign-in.js.map +1 -0
- package/dist/routes/tokens.d.ts +15 -5
- package/dist/routes/tokens.d.ts.map +1 -1
- package/dist/routes/tokens.js +162 -43
- package/dist/routes/tokens.js.map +1 -1
- package/dist/routes/tools.d.ts.map +1 -1
- package/dist/routes/tools.js +9 -1
- package/dist/routes/tools.js.map +1 -1
- package/dist/routes/trigger-access.d.ts +29 -0
- package/dist/routes/trigger-access.d.ts.map +1 -0
- package/dist/routes/trigger-access.js +33 -0
- package/dist/routes/trigger-access.js.map +1 -0
- package/dist/routes/webhook-endpoints.d.ts +2 -1
- package/dist/routes/webhook-endpoints.d.ts.map +1 -1
- package/dist/routes/webhook-endpoints.js +11 -3
- package/dist/routes/webhook-endpoints.js.map +1 -1
- package/dist/routes/webhooks.d.ts +5 -1
- package/dist/routes/webhooks.d.ts.map +1 -1
- package/dist/routes/webhooks.js +25 -2
- package/dist/routes/webhooks.js.map +1 -1
- package/dist/run-failure.d.ts +21 -0
- package/dist/run-failure.d.ts.map +1 -0
- package/dist/run-failure.js +29 -0
- package/dist/run-failure.js.map +1 -0
- package/dist/service-account-binding.d.ts +108 -0
- package/dist/service-account-binding.d.ts.map +1 -0
- package/dist/service-account-binding.js +4 -0
- package/dist/service-account-binding.js.map +1 -0
- package/dist/session-store-binding.d.ts +61 -13
- package/dist/session-store-binding.d.ts.map +1 -1
- package/dist/signed-export.d.ts +89 -0
- package/dist/signed-export.d.ts.map +1 -0
- package/dist/signed-export.js +149 -0
- package/dist/signed-export.js.map +1 -0
- package/dist/supervisor-binding.d.ts +186 -413
- package/dist/supervisor-binding.d.ts.map +1 -1
- package/dist/testing/app-bindings.d.ts +33 -0
- package/dist/testing/app-bindings.d.ts.map +1 -0
- package/dist/testing/app-bindings.js +112 -0
- package/dist/testing/app-bindings.js.map +1 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +13 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/stub-binding.d.ts +34 -0
- package/dist/testing/stub-binding.d.ts.map +1 -0
- package/dist/testing/stub-binding.js +42 -0
- package/dist/testing/stub-binding.js.map +1 -0
- package/dist/testing/trigger-registry.d.ts +21 -0
- package/dist/testing/trigger-registry.d.ts.map +1 -0
- package/dist/testing/trigger-registry.js +206 -0
- package/dist/testing/trigger-registry.js.map +1 -0
- package/dist/token-admin.d.ts +46 -8
- package/dist/token-admin.d.ts.map +1 -1
- package/dist/token-admin.js.map +1 -1
- package/dist/tool-binding.d.ts +13 -0
- package/dist/tool-binding.d.ts.map +1 -1
- package/dist/trigger-binding.d.ts +2 -2
- package/dist/trigger-binding.d.ts.map +1 -1
- package/dist/trigger-binding.js +1 -1
- package/dist/trigger-binding.js.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/webhook-endpoint-binding.d.ts +19 -3
- package/dist/webhook-endpoint-binding.d.ts.map +1 -1
- package/dist/webhook-endpoint-binding.js +1 -1
- package/dist/webhook-endpoint-binding.js.map +1 -1
- package/openapi.json +15286 -8653
- package/package.json +26 -22
- package/src/agent-binding.ts +14 -0
- package/src/agent-pins.ts +18 -2
- package/src/app.ts +366 -80
- package/src/block-publish.ts +181 -0
- package/src/caller.ts +62 -0
- package/src/capability-binding.ts +12 -0
- package/src/derive-agent-version.ts +9 -1
- package/src/errors.ts +73 -7
- package/src/eval-case-binding.ts +17 -1
- package/src/eval-run-binding.ts +31 -0
- package/src/eval-run-dispatcher.ts +7 -0
- package/src/eval-sample.ts +42 -0
- package/src/eval-suite-binding.ts +14 -0
- package/src/flow-binding.ts +19 -0
- package/src/flow-pins.ts +18 -2
- package/src/gate.ts +26 -0
- package/src/handler-binding.ts +36 -1
- package/src/hitl-binding.ts +24 -0
- package/src/identity-directory-binding.ts +85 -4
- package/src/identity-provider-binding.ts +216 -21
- package/src/improvement-pass-binding.ts +193 -0
- package/src/index.ts +153 -31
- package/src/judged-dispatcher.ts +78 -11
- package/src/judgment-binding.ts +25 -1
- package/src/mcp-endpoint-binding.ts +7 -0
- package/src/memory-binding.ts +121 -17
- package/src/memory-erasure-binding.ts +156 -0
- package/src/middleware/auth.ts +228 -62
- package/src/middleware/authorize.ts +139 -3
- package/src/middleware/error-mapper.ts +15 -1
- package/src/middleware/idempotency.ts +230 -43
- package/src/middleware/key-project.ts +86 -0
- package/src/middleware/principal.ts +9 -5
- package/src/middleware/project-ref.ts +3 -0
- package/src/openapi/generate.ts +11 -2
- package/src/openapi/operations.ts +1059 -142
- package/src/openapi/schemas.ts +2438 -485
- package/src/person-grants-binding.ts +76 -0
- package/src/proposal-service.ts +640 -0
- package/src/proposal-status.ts +176 -0
- package/src/provider-binding.ts +10 -0
- package/src/publish-refused.ts +38 -9
- package/src/routes/adapters.ts +4 -0
- package/src/routes/agent-releases.ts +121 -69
- package/src/routes/agents.ts +128 -1
- package/src/routes/approvals.ts +164 -154
- package/src/routes/artifacts.ts +181 -11
- package/src/routes/auth.ts +804 -336
- package/src/routes/blocks.ts +2 -53
- package/src/routes/capabilities.ts +10 -1
- package/src/routes/compliance.ts +60 -99
- package/src/routes/conversations.ts +101 -7
- package/src/routes/cost.ts +32 -3
- package/src/routes/denied.ts +28 -0
- package/src/routes/deployments.ts +62 -16
- package/src/routes/eval-comparison.ts +81 -2
- package/src/routes/eval-overrides.ts +108 -0
- package/src/routes/eval-runs.ts +86 -6
- package/src/routes/eval-suites.ts +25 -1
- package/src/routes/event-triggers.ts +27 -3
- package/src/routes/export-signing-keys.ts +31 -0
- package/src/routes/flows.ts +12 -1
- package/src/routes/gate-policies.ts +37 -6
- package/src/routes/guardrails.ts +61 -2
- package/src/routes/identity.ts +328 -10
- package/src/routes/improvement-passes.ts +541 -0
- package/src/routes/judged-suites.ts +113 -43
- package/src/routes/judgment-context.ts +76 -5
- package/src/routes/judgment-flow-context.ts +16 -2
- package/src/routes/judgments.ts +17 -3
- package/src/routes/mcp.ts +29 -1
- package/src/routes/memory-access.ts +179 -0
- package/src/routes/memory-erasures.ts +240 -0
- package/src/routes/memory-parse.ts +250 -0
- package/src/routes/memory.ts +244 -286
- package/src/routes/observations.ts +15 -2
- package/src/routes/pagination.ts +12 -0
- package/src/routes/policies.ts +7 -1
- package/src/routes/project-mismatch.ts +56 -0
- package/src/routes/projects.ts +107 -13
- package/src/routes/proposals.ts +370 -611
- package/src/routes/provenance.ts +99 -133
- package/src/routes/providers.ts +223 -8
- package/src/routes/public-run-tokens.ts +3 -0
- package/src/routes/runs.ts +119 -39
- package/src/routes/schedules.ts +472 -60
- package/src/routes/segments.ts +11 -0
- package/src/routes/service-accounts.ts +262 -0
- package/src/routes/sign-in-options.ts +168 -0
- package/src/routes/signing-keys.ts +7 -1
- package/src/routes/tenant-access.ts +37 -0
- package/src/routes/tenant.ts +6 -0
- package/src/routes/token-sign-in.ts +161 -0
- package/src/routes/tokens.ts +172 -46
- package/src/routes/tools.ts +12 -1
- package/src/routes/trigger-access.ts +72 -0
- package/src/routes/webhook-endpoints.ts +15 -3
- package/src/routes/webhooks.ts +26 -2
- package/src/run-failure.ts +42 -0
- package/src/service-account-binding.ts +113 -0
- package/src/session-store-binding.ts +65 -13
- package/src/signed-export.ts +250 -0
- package/src/supervisor-binding.ts +169 -409
- package/src/testing/app-bindings.ts +142 -0
- package/src/testing/index.ts +21 -0
- package/src/testing/stub-binding.ts +53 -0
- package/src/testing/trigger-registry.ts +259 -0
- package/src/token-admin.ts +40 -8
- package/src/tool-binding.ts +14 -0
- package/src/trigger-binding.ts +8 -1
- package/src/types.ts +16 -0
- package/src/webhook-endpoint-binding.ts +18 -3
package/src/routes/auth.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
5
5
|
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
7
|
import type { Context } from 'hono';
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { AuditEventBinding } from '@kindgi/audit-events';
|
|
10
|
+
import type { SessionId, TenantId, Timestamp, UserId } from '@kindgi/types';
|
|
10
11
|
|
|
11
12
|
import { statusFor, toWireError } from '../errors.js';
|
|
12
13
|
import type {
|
|
@@ -16,11 +17,19 @@ import type {
|
|
|
16
17
|
IdentityProviderBinding,
|
|
17
18
|
ProviderConfig,
|
|
18
19
|
RefreshTokenFn,
|
|
20
|
+
SamlAttributeMapping,
|
|
19
21
|
} from '../identity-provider-binding.js';
|
|
20
22
|
import { encodeSessionToken } from '../middleware/auth.js';
|
|
21
|
-
import type {
|
|
23
|
+
import type { Authorizer } from '../middleware/authorize.js';
|
|
24
|
+
import { withholdFromReplay } from '../middleware/idempotency.js';
|
|
25
|
+
import type {
|
|
26
|
+
Session,
|
|
27
|
+
SessionCreateOutput,
|
|
28
|
+
SessionStoreBinding,
|
|
29
|
+
} from '../session-store-binding.js';
|
|
22
30
|
import type { OauthStateStore } from '../state-store-binding.js';
|
|
23
31
|
import type { AppEnv } from '../types.js';
|
|
32
|
+
import { tenantResourceAccess } from './tenant-access.js';
|
|
24
33
|
|
|
25
34
|
/**
|
|
26
35
|
* Auth routes. Layer OAuth 2.0 / OIDC on top of the static
|
|
@@ -40,7 +49,13 @@ export interface AuthRouterOptions {
|
|
|
40
49
|
readonly sessionStore: SessionStoreBinding;
|
|
41
50
|
readonly identityProvider: IdentityProviderBinding;
|
|
42
51
|
readonly stateStore: OauthStateStore;
|
|
43
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The deployment's own code exchange. With it, `POST /login/:providerId`
|
|
54
|
+
* and the callback mount (the OAuth flow run by this package); without
|
|
55
|
+
* it (sign-in runs elsewhere, e.g. a browser flow in the deployment),
|
|
56
|
+
* only the provider catalog, refresh and logout do.
|
|
57
|
+
*/
|
|
58
|
+
readonly exchangeCode?: ExchangeCodeFn;
|
|
44
59
|
readonly refreshToken?: RefreshTokenFn;
|
|
45
60
|
/**
|
|
46
61
|
* Default TTL for the CSRF/PKCE state cache entries. 10 minutes covers
|
|
@@ -48,6 +63,15 @@ export interface AuthRouterOptions {
|
|
|
48
63
|
* without keeping a lost row around forever.
|
|
49
64
|
*/
|
|
50
65
|
readonly stateTtlMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* With one (T243 A): the provider catalog is tenant-wide, so reading it
|
|
68
|
+
* needs `read` on the tenant and changing it `admin`, as for every
|
|
69
|
+
* tenant-wide resource (`tenantResourceAccess`). Logging in, refreshing
|
|
70
|
+
* and logging out are the caller's own, and stay unchecked.
|
|
71
|
+
*/
|
|
72
|
+
readonly authorizer?: Authorizer;
|
|
73
|
+
/** `signed-out` events, best effort. */
|
|
74
|
+
readonly auditEvents?: AuditEventBinding;
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
const DEFAULT_STATE_TTL_MS = 10 * 60 * 1000;
|
|
@@ -72,6 +96,12 @@ export function authRouters(options: AuthRouterOptions): {
|
|
|
72
96
|
|
|
73
97
|
const authed = new Hono<AppEnv>();
|
|
74
98
|
|
|
99
|
+
// The provider catalog is tenant-wide: any GET (the list, one provider,
|
|
100
|
+
// its sign-in URLs) needs `read` on the tenant, any change `admin`.
|
|
101
|
+
const providerAccess = tenantResourceAccess(options.authorizer);
|
|
102
|
+
authed.use('/providers', providerAccess);
|
|
103
|
+
authed.use('/providers/*', providerAccess);
|
|
104
|
+
|
|
75
105
|
// ---------- GET /providers ----------
|
|
76
106
|
authed.get('/providers', async (c) => {
|
|
77
107
|
const tenantId = c.get('tenantId') as TenantId;
|
|
@@ -111,10 +141,122 @@ export function authRouters(options: AuthRouterOptions): {
|
|
|
111
141
|
}
|
|
112
142
|
|
|
113
143
|
const outcome = await identityProvider.register({ tenantId, config: parsed.value });
|
|
144
|
+
if (outcome.kind === 'invalid') {
|
|
145
|
+
c.status(statusFor('identity-provider-invalid') as never);
|
|
146
|
+
return c.json(
|
|
147
|
+
toWireError(
|
|
148
|
+
{
|
|
149
|
+
code: 'identity-provider-invalid',
|
|
150
|
+
message: outcome.message,
|
|
151
|
+
providerId: parsed.value.providerId,
|
|
152
|
+
},
|
|
153
|
+
requestId,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
114
157
|
c.status(201);
|
|
115
|
-
return c.json({
|
|
158
|
+
return c.json({
|
|
159
|
+
providerId: outcome.providerId,
|
|
160
|
+
...(outcome.provider !== undefined && {
|
|
161
|
+
provider: serializeProviderConfig(outcome.provider),
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ---------- GET /providers/:providerId ----------
|
|
167
|
+
authed.get('/providers/:providerId', async (c) => {
|
|
168
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
169
|
+
const providerId = c.req.param('providerId');
|
|
170
|
+
const found = await identityProvider.get({ tenantId, providerId });
|
|
171
|
+
if (found === null) return providerNotFound(c, providerId);
|
|
172
|
+
return c.json(serializeProviderConfig(found));
|
|
116
173
|
});
|
|
117
174
|
|
|
175
|
+
// ---------- GET /providers/:providerId/sign-in ----------
|
|
176
|
+
const signInUrls = identityProvider.signInUrls;
|
|
177
|
+
if (signInUrls !== undefined) {
|
|
178
|
+
authed.get('/providers/:providerId/sign-in', async (c) => {
|
|
179
|
+
const requestId = c.get('requestId');
|
|
180
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
181
|
+
const providerId = c.req.param('providerId');
|
|
182
|
+
const asked = c.req.query('kind');
|
|
183
|
+
if (asked !== undefined && asked !== 'oidc' && asked !== 'saml' && asked !== 'oauth2') {
|
|
184
|
+
c.status(statusFor('bad-input') as never);
|
|
185
|
+
return c.json(
|
|
186
|
+
toWireError(
|
|
187
|
+
{ code: 'bad-input', message: '`kind` must be `oidc`, `saml` or `oauth2`' },
|
|
188
|
+
requestId,
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const registered = await identityProvider.get({ tenantId, providerId });
|
|
193
|
+
const kind = asked ?? registered?.kind;
|
|
194
|
+
if (kind === undefined) {
|
|
195
|
+
c.status(statusFor('bad-input') as never);
|
|
196
|
+
return c.json(
|
|
197
|
+
toWireError(
|
|
198
|
+
{
|
|
199
|
+
code: 'bad-input',
|
|
200
|
+
message: `No identity provider "${providerId}" is registered yet: say which \`kind\` it will be (\`?kind=oidc\` or \`?kind=saml\`)`,
|
|
201
|
+
},
|
|
202
|
+
requestId,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const signIn = await signInUrls({ tenantId, providerId, kind });
|
|
207
|
+
if (signIn === undefined) {
|
|
208
|
+
c.status(statusFor('bad-input') as never);
|
|
209
|
+
return c.json(
|
|
210
|
+
toWireError(
|
|
211
|
+
{
|
|
212
|
+
code: 'bad-input',
|
|
213
|
+
message: `This deployment doesn't sign in with \`${kind}\` providers`,
|
|
214
|
+
},
|
|
215
|
+
requestId,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return c.json({ providerId, kind, signIn, registered: registered !== null });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------- PATCH /providers/:providerId ----------
|
|
224
|
+
const update = identityProvider.update;
|
|
225
|
+
if (update !== undefined) {
|
|
226
|
+
authed.patch('/providers/:providerId', async (c) => {
|
|
227
|
+
const requestId = c.get('requestId');
|
|
228
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
229
|
+
const providerId = c.req.param('providerId');
|
|
230
|
+
|
|
231
|
+
const parsedJson = await parseJsonBody(c);
|
|
232
|
+
if (parsedJson.kind === 'err') {
|
|
233
|
+
c.status(statusFor('bad-input') as never);
|
|
234
|
+
return c.json(toWireError(parsedJson.error, requestId));
|
|
235
|
+
}
|
|
236
|
+
const existing = await identityProvider.get({ tenantId, providerId });
|
|
237
|
+
if (existing === null) return providerNotFound(c, providerId);
|
|
238
|
+
const merged = mergeProviderChanges(existing, parsedJson.value);
|
|
239
|
+
const parsed = merged.kind === 'err' ? merged : parseProviderConfig(merged.value);
|
|
240
|
+
if (parsed.kind === 'err') {
|
|
241
|
+
c.status(statusFor(parsed.error.code) as never);
|
|
242
|
+
return c.json(toWireError(parsed.error, requestId));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const outcome = await update({ tenantId, config: parsed.value });
|
|
246
|
+
if (outcome.kind === 'not-found') return providerNotFound(c, providerId);
|
|
247
|
+
if (outcome.kind === 'invalid') {
|
|
248
|
+
c.status(statusFor('identity-provider-invalid') as never);
|
|
249
|
+
return c.json(
|
|
250
|
+
toWireError(
|
|
251
|
+
{ code: 'identity-provider-invalid', message: outcome.message, providerId },
|
|
252
|
+
requestId,
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return c.json({ providerId, provider: serializeProviderConfig(outcome.provider) });
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
118
260
|
// ---------- POST /providers/:providerId/unregister ----------
|
|
119
261
|
authed.post('/providers/:providerId/unregister', async (c) => {
|
|
120
262
|
const requestId = c.get('requestId');
|
|
@@ -138,119 +280,135 @@ export function authRouters(options: AuthRouterOptions): {
|
|
|
138
280
|
return c.json({ providerId, unregistered: true });
|
|
139
281
|
});
|
|
140
282
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
283
|
+
if (exchangeCode !== undefined) {
|
|
284
|
+
// ---------- POST /login/:providerId ----------
|
|
285
|
+
authed.post('/login/:providerId', async (c) => {
|
|
286
|
+
const requestId = c.get('requestId');
|
|
287
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
288
|
+
const providerId = c.req.param('providerId');
|
|
146
289
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
290
|
+
const found = await identityProvider.get({ tenantId, providerId });
|
|
291
|
+
if (found === null) {
|
|
292
|
+
c.status(statusFor('identity-provider-not-found') as never);
|
|
293
|
+
return c.json(
|
|
294
|
+
toWireError(
|
|
295
|
+
{
|
|
296
|
+
code: 'identity-provider-not-found',
|
|
297
|
+
message: `No identity provider registered with id "${providerId}"`,
|
|
298
|
+
providerId,
|
|
299
|
+
},
|
|
300
|
+
requestId,
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const config = oauthFlowOf(found);
|
|
305
|
+
if (config === null) {
|
|
306
|
+
c.status(statusFor('invalid-provider-config') as never);
|
|
307
|
+
return c.json(
|
|
308
|
+
toWireError(
|
|
309
|
+
{
|
|
310
|
+
code: 'invalid-provider-config',
|
|
311
|
+
message: `Identity provider "${providerId}" (${found.kind}) signs in through the deployment's own sign-in, not this endpoint`,
|
|
312
|
+
providerId,
|
|
313
|
+
},
|
|
314
|
+
requestId,
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
161
318
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
319
|
+
let redirectUri: string | undefined;
|
|
320
|
+
const parsedBody = await parseOptionalJsonBody(c);
|
|
321
|
+
if (parsedBody.kind === 'err') {
|
|
322
|
+
c.status(statusFor('bad-input') as never);
|
|
323
|
+
return c.json(toWireError(parsedBody.error, requestId));
|
|
324
|
+
}
|
|
325
|
+
if (parsedBody.value !== undefined) {
|
|
326
|
+
const raw = (parsedBody.value as { redirectUri?: unknown }).redirectUri;
|
|
327
|
+
if (raw !== undefined) {
|
|
328
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
329
|
+
c.status(statusFor('bad-input') as never);
|
|
330
|
+
return c.json(
|
|
331
|
+
toWireError(
|
|
332
|
+
{ code: 'bad-input', message: '`redirectUri` must be a non-empty string' },
|
|
333
|
+
requestId,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
redirectUri = raw;
|
|
179
338
|
}
|
|
180
|
-
redirectUri = raw;
|
|
181
339
|
}
|
|
182
|
-
}
|
|
183
340
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
341
|
+
const state = base64Url(randomBytes(32));
|
|
342
|
+
const codeVerifier = base64Url(randomBytes(64));
|
|
343
|
+
const codeChallenge = base64Url(createHash('sha256').update(codeVerifier).digest());
|
|
344
|
+
const effectiveRedirect =
|
|
345
|
+
redirectUri ??
|
|
346
|
+
(typeof config.metadata?.defaultRedirectUri === 'string'
|
|
347
|
+
? String(config.metadata?.defaultRedirectUri)
|
|
348
|
+
: '');
|
|
349
|
+
if (effectiveRedirect.length === 0) {
|
|
350
|
+
c.status(statusFor('bad-input') as never);
|
|
351
|
+
return c.json(
|
|
352
|
+
toWireError(
|
|
353
|
+
{
|
|
354
|
+
code: 'bad-input',
|
|
355
|
+
message:
|
|
356
|
+
'`redirectUri` must be supplied in the body or via `metadata.defaultRedirectUri` on the provider config',
|
|
357
|
+
},
|
|
358
|
+
requestId,
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
205
362
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
363
|
+
// OAuth 2.1 BCP redirect-URI allowlist enforcement. Absent /
|
|
364
|
+
// empty list means pass-through (no allowlist check); populated
|
|
365
|
+
// list requires an exact-string match against the effective
|
|
366
|
+
// redirect URI (either the body-supplied value or the resolved
|
|
367
|
+
// `metadata.defaultRedirectUri`). This is the primary gate — the
|
|
368
|
+
// callback route re-verifies against the same list for
|
|
369
|
+
// belt-and-suspenders defense in case the allowlist tightened
|
|
370
|
+
// between login and callback.
|
|
371
|
+
if (
|
|
372
|
+
config.allowedRedirectUris !== undefined &&
|
|
373
|
+
config.allowedRedirectUris.length > 0 &&
|
|
374
|
+
!config.allowedRedirectUris.includes(effectiveRedirect)
|
|
375
|
+
) {
|
|
376
|
+
c.status(statusFor('redirect-uri-not-allowed') as never);
|
|
377
|
+
return c.json(
|
|
378
|
+
toWireError(
|
|
379
|
+
{
|
|
380
|
+
code: 'redirect-uri-not-allowed',
|
|
381
|
+
message: `redirect_uri "${effectiveRedirect}" is not in the provider's allowedRedirectUris list`,
|
|
382
|
+
providerId,
|
|
383
|
+
},
|
|
384
|
+
requestId,
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
231
388
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
389
|
+
await stateStore.put({
|
|
390
|
+
state,
|
|
391
|
+
tenantId,
|
|
392
|
+
providerId,
|
|
393
|
+
codeVerifier,
|
|
394
|
+
redirectUri: effectiveRedirect,
|
|
395
|
+
expiresAt: Date.now() + stateTtlMs,
|
|
396
|
+
});
|
|
240
397
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
398
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
399
|
+
config,
|
|
400
|
+
state,
|
|
401
|
+
codeChallenge,
|
|
402
|
+
redirectUri: effectiveRedirect,
|
|
403
|
+
});
|
|
404
|
+
return c.json({
|
|
405
|
+
authorizationUrl,
|
|
406
|
+
state,
|
|
407
|
+
codeChallenge,
|
|
408
|
+
codeChallengeMethod: 'S256',
|
|
409
|
+
});
|
|
252
410
|
});
|
|
253
|
-
}
|
|
411
|
+
}
|
|
254
412
|
|
|
255
413
|
// ---------- POST /refresh ----------
|
|
256
414
|
authed.post('/refresh', async (c) => {
|
|
@@ -270,6 +428,20 @@ export function authRouters(options: AuthRouterOptions): {
|
|
|
270
428
|
),
|
|
271
429
|
);
|
|
272
430
|
}
|
|
431
|
+
if (c.get('sessionCookieName') !== undefined) {
|
|
432
|
+
// Refresh answers with the new token in its body, which a browser
|
|
433
|
+
// session must never hand to page scripts: it ends at its TTL.
|
|
434
|
+
c.status(statusFor('cookie-session-not-refreshable') as never);
|
|
435
|
+
return c.json(
|
|
436
|
+
toWireError(
|
|
437
|
+
{
|
|
438
|
+
code: 'cookie-session-not-refreshable',
|
|
439
|
+
message: 'A browser session (cookie) is not refreshed: sign in again when it ends',
|
|
440
|
+
},
|
|
441
|
+
requestId,
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
273
445
|
const current = await sessionStore.get({ tenantId, sessionId });
|
|
274
446
|
if (current === null || current.revokedAt !== undefined) {
|
|
275
447
|
c.status(statusFor('session-not-found') as never);
|
|
@@ -314,14 +486,14 @@ export function authRouters(options: AuthRouterOptions): {
|
|
|
314
486
|
});
|
|
315
487
|
const fresh = await sessionStore.get({ tenantId, sessionId: createdSession.sessionId });
|
|
316
488
|
if (fresh === null) throw new Error('session vanished immediately after create');
|
|
317
|
-
created = { session: fresh, rawToken:
|
|
489
|
+
created = { session: fresh, rawToken: sessionTokenOf(createdSession) };
|
|
318
490
|
} else {
|
|
319
491
|
// Rotate the framework token only; keep provider tokens as-is.
|
|
320
492
|
const createdSession = await sessionStore.create({
|
|
321
493
|
tenantId,
|
|
322
494
|
userId: current.userId,
|
|
323
495
|
providerId: current.providerId,
|
|
324
|
-
accessToken: current.accessToken,
|
|
496
|
+
...(current.accessToken !== undefined && { accessToken: current.accessToken }),
|
|
325
497
|
...(current.refreshToken !== undefined && { refreshToken: current.refreshToken }),
|
|
326
498
|
expiresAt: current.expiresAt,
|
|
327
499
|
scopes: current.scopes,
|
|
@@ -329,7 +501,7 @@ export function authRouters(options: AuthRouterOptions): {
|
|
|
329
501
|
});
|
|
330
502
|
const fresh = await sessionStore.get({ tenantId, sessionId: createdSession.sessionId });
|
|
331
503
|
if (fresh === null) throw new Error('session vanished immediately after create');
|
|
332
|
-
created = { session: fresh, rawToken:
|
|
504
|
+
created = { session: fresh, rawToken: sessionTokenOf(createdSession) };
|
|
333
505
|
}
|
|
334
506
|
|
|
335
507
|
// OAuth 2.1 BCP: mark the old session as ROTATED (not just revoked)
|
|
@@ -339,6 +511,8 @@ export function authRouters(options: AuthRouterOptions): {
|
|
|
339
511
|
// a full re-auth.
|
|
340
512
|
await sessionStore.revoke({ tenantId, sessionId, reason: 'rotate' });
|
|
341
513
|
|
|
514
|
+
// A session token: an Idempotency-Key repeat doesn't get it.
|
|
515
|
+
withholdFromReplay(c);
|
|
342
516
|
return c.json({
|
|
343
517
|
sessionToken: created.rawToken,
|
|
344
518
|
sessionId: created.session.id,
|
|
@@ -347,147 +521,168 @@ export function authRouters(options: AuthRouterOptions): {
|
|
|
347
521
|
});
|
|
348
522
|
|
|
349
523
|
// ---------- POST /logout ----------
|
|
350
|
-
authed.post('/logout',
|
|
351
|
-
const requestId = c.get('requestId');
|
|
352
|
-
const tenantId = c.get('tenantId') as TenantId;
|
|
353
|
-
const sessionId = c.get('sessionId') as SessionId | undefined;
|
|
354
|
-
if (sessionId === undefined) {
|
|
355
|
-
c.status(statusFor('auth-not-session-token') as never);
|
|
356
|
-
return c.json(
|
|
357
|
-
toWireError(
|
|
358
|
-
{
|
|
359
|
-
code: 'auth-not-session-token',
|
|
360
|
-
message:
|
|
361
|
-
'Logout is only valid for session tokens (`kgi_sk_*`); bearer tokens are managed via `/v1/tokens`',
|
|
362
|
-
},
|
|
363
|
-
requestId,
|
|
364
|
-
),
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
const outcome = await sessionStore.revoke({ tenantId, sessionId });
|
|
368
|
-
return c.json({ sessionId, revoked: outcome.revoked });
|
|
369
|
-
});
|
|
524
|
+
authed.post('/logout', logoutHandler(sessionStore, options.auditEvents));
|
|
370
525
|
|
|
371
526
|
const callback = new Hono<AppEnv>();
|
|
372
527
|
|
|
373
528
|
// ---------- POST /callback/:providerId (mounted outside the bearer chain) ----------
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
529
|
+
if (exchangeCode !== undefined) {
|
|
530
|
+
callback.post('/:providerId', async (c) => {
|
|
531
|
+
const requestId = c.get('requestId');
|
|
532
|
+
const providerId = c.req.param('providerId');
|
|
377
533
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// `state` alone is uniquely identifying (256 bits of entropy). The
|
|
391
|
-
// store returns the row's `tenantId` so we know which tenant the
|
|
392
|
-
// callback belongs to — cross-checking `providerId` guards against
|
|
393
|
-
// a stolen `state` being replayed against the wrong provider mount.
|
|
394
|
-
const entry = await stateStore.take({ state, providerId });
|
|
395
|
-
if (entry === null) {
|
|
396
|
-
c.status(statusFor('oauth-state-invalid') as never);
|
|
397
|
-
return c.json(
|
|
398
|
-
toWireError(
|
|
399
|
-
{
|
|
400
|
-
code: 'oauth-state-invalid',
|
|
401
|
-
message: '`state` is unknown, expired, or already consumed',
|
|
402
|
-
},
|
|
403
|
-
requestId,
|
|
404
|
-
),
|
|
405
|
-
);
|
|
406
|
-
}
|
|
534
|
+
const parsedJson = await parseJsonBody(c);
|
|
535
|
+
if (parsedJson.kind === 'err') {
|
|
536
|
+
c.status(statusFor('bad-input') as never);
|
|
537
|
+
return c.json(toWireError(parsedJson.error, requestId));
|
|
538
|
+
}
|
|
539
|
+
const parsedCallback = parseCallbackBody(parsedJson.value);
|
|
540
|
+
if (parsedCallback.kind === 'err') {
|
|
541
|
+
c.status(statusFor(parsedCallback.error.code) as never);
|
|
542
|
+
return c.json(toWireError(parsedCallback.error, requestId));
|
|
543
|
+
}
|
|
544
|
+
const { code, state } = parsedCallback.value;
|
|
407
545
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
c.status(statusFor('redirect-uri-mismatch') as never);
|
|
426
|
-
return c.json(
|
|
427
|
-
toWireError(
|
|
428
|
-
{
|
|
429
|
-
code: 'redirect-uri-mismatch',
|
|
430
|
-
message:
|
|
431
|
-
"redirect_uri from the login state row is not in the provider's current allowedRedirectUris list",
|
|
432
|
-
providerId,
|
|
433
|
-
},
|
|
434
|
-
requestId,
|
|
435
|
-
),
|
|
436
|
-
);
|
|
437
|
-
}
|
|
546
|
+
// `state` alone is uniquely identifying (256 bits of entropy). The
|
|
547
|
+
// store returns the row's `tenantId` so we know which tenant the
|
|
548
|
+
// callback belongs to — cross-checking `providerId` guards against
|
|
549
|
+
// a stolen `state` being replayed against the wrong provider mount.
|
|
550
|
+
const entry = await stateStore.take({ state, providerId });
|
|
551
|
+
if (entry === null) {
|
|
552
|
+
c.status(statusFor('oauth-state-invalid') as never);
|
|
553
|
+
return c.json(
|
|
554
|
+
toWireError(
|
|
555
|
+
{
|
|
556
|
+
code: 'oauth-state-invalid',
|
|
557
|
+
message: '`state` is unknown, expired, or already consumed',
|
|
558
|
+
},
|
|
559
|
+
requestId,
|
|
560
|
+
),
|
|
561
|
+
);
|
|
562
|
+
}
|
|
438
563
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
564
|
+
// Belt-and-suspenders redirect-URI check. Login already validated
|
|
565
|
+
// the stored redirect_uri against `allowedRedirectUris` when the
|
|
566
|
+
// row was written, but the allowlist may have tightened between
|
|
567
|
+
// login and callback — if so, refuse the exchange rather than
|
|
568
|
+
// handing the caller a session under a redirect the tenant no
|
|
569
|
+
// longer trusts. Also runs when the provider config went missing
|
|
570
|
+
// (unregister mid-flight) so we don't silently proceed.
|
|
571
|
+
const currentConfig = await identityProvider.get({
|
|
442
572
|
tenantId: entry.tenantId,
|
|
443
573
|
providerId,
|
|
444
|
-
code,
|
|
445
|
-
codeVerifier: entry.codeVerifier,
|
|
446
|
-
redirectUri: entry.redirectUri,
|
|
447
574
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
575
|
+
const currentAllowlist =
|
|
576
|
+
currentConfig === null ? undefined : oauthFlowOf(currentConfig)?.allowedRedirectUris;
|
|
577
|
+
if (
|
|
578
|
+
currentAllowlist !== undefined &&
|
|
579
|
+
currentAllowlist.length > 0 &&
|
|
580
|
+
!currentAllowlist.includes(entry.redirectUri)
|
|
581
|
+
) {
|
|
582
|
+
c.status(statusFor('redirect-uri-mismatch') as never);
|
|
583
|
+
return c.json(
|
|
584
|
+
toWireError(
|
|
585
|
+
{
|
|
586
|
+
code: 'redirect-uri-mismatch',
|
|
587
|
+
message:
|
|
588
|
+
"redirect_uri from the login state row is not in the provider's current allowedRedirectUris list",
|
|
589
|
+
providerId,
|
|
590
|
+
},
|
|
591
|
+
requestId,
|
|
592
|
+
),
|
|
593
|
+
);
|
|
594
|
+
}
|
|
460
595
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
596
|
+
let outcome: Awaited<ReturnType<ExchangeCodeFn>>;
|
|
597
|
+
try {
|
|
598
|
+
outcome = await exchangeCode({
|
|
599
|
+
tenantId: entry.tenantId,
|
|
600
|
+
providerId,
|
|
601
|
+
code,
|
|
602
|
+
codeVerifier: entry.codeVerifier,
|
|
603
|
+
redirectUri: entry.redirectUri,
|
|
604
|
+
});
|
|
605
|
+
} catch (err) {
|
|
606
|
+
c.status(statusFor('oauth-code-exchange-failed') as never);
|
|
607
|
+
return c.json(
|
|
608
|
+
toWireError(
|
|
609
|
+
{
|
|
610
|
+
code: 'oauth-code-exchange-failed',
|
|
611
|
+
message: err instanceof Error ? err.message : 'code exchange failed',
|
|
612
|
+
},
|
|
613
|
+
requestId,
|
|
614
|
+
),
|
|
615
|
+
);
|
|
616
|
+
}
|
|
471
617
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
618
|
+
const created = await sessionStore.create({
|
|
619
|
+
tenantId: entry.tenantId,
|
|
620
|
+
userId: outcome.userId as unknown as UserId,
|
|
621
|
+
providerId,
|
|
622
|
+
accessToken: outcome.accessToken,
|
|
623
|
+
...(outcome.refreshToken !== undefined && { refreshToken: outcome.refreshToken }),
|
|
624
|
+
expiresAt: outcome.expiresAt.toISOString() as never,
|
|
625
|
+
scopes: outcome.scopes,
|
|
626
|
+
...(outcome.claims !== undefined && { metadata: outcome.claims }),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// A session token: an Idempotency-Key repeat doesn't get it.
|
|
630
|
+
withholdFromReplay(c);
|
|
631
|
+
c.status(201);
|
|
632
|
+
return c.json({
|
|
633
|
+
sessionToken: sessionTokenOf(created),
|
|
634
|
+
sessionId: created.sessionId,
|
|
635
|
+
expiresAt: created.expiresAt,
|
|
636
|
+
});
|
|
477
637
|
});
|
|
478
|
-
}
|
|
638
|
+
}
|
|
479
639
|
|
|
480
640
|
return { authed, callback };
|
|
481
641
|
}
|
|
482
642
|
|
|
483
643
|
// ---------- helpers ----------
|
|
484
644
|
|
|
645
|
+
/** The token a store minted, or the older `kgi_sk_<sessionId>` for a store that mints none. */
|
|
646
|
+
function sessionTokenOf(created: SessionCreateOutput): string {
|
|
647
|
+
return created.token ?? encodeSessionToken(created.sessionId);
|
|
648
|
+
}
|
|
649
|
+
|
|
485
650
|
function base64Url(buf: Buffer): string {
|
|
486
651
|
return buf.toString('base64url');
|
|
487
652
|
}
|
|
488
653
|
|
|
654
|
+
/** What this package's own OAuth flow (login + callback) needs from a provider. */
|
|
655
|
+
interface OAuthFlowConfig {
|
|
656
|
+
readonly clientId: string;
|
|
657
|
+
readonly authorizationEndpoint: string;
|
|
658
|
+
readonly scopes: readonly string[];
|
|
659
|
+
readonly allowedRedirectUris?: readonly string[];
|
|
660
|
+
readonly metadata?: Record<string, unknown>;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const DEFAULT_OIDC_SCOPES: readonly string[] = ['openid', 'email', 'profile'];
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* The provider's OAuth flow settings, or `null` when it has none: a SAML
|
|
667
|
+
* provider, or an OIDC one whose endpoints the deployment hasn't
|
|
668
|
+
* discovered (it signs in through the deployment's own browser flow).
|
|
669
|
+
*/
|
|
670
|
+
function oauthFlowOf(config: ProviderConfig): OAuthFlowConfig | null {
|
|
671
|
+
if (config.kind === 'saml') return null;
|
|
672
|
+
if (config.kind === 'oidc' && config.authorizationEndpoint === undefined) return null;
|
|
673
|
+
return {
|
|
674
|
+
clientId: config.clientId,
|
|
675
|
+
authorizationEndpoint: config.authorizationEndpoint as string,
|
|
676
|
+
scopes: config.scopes ?? DEFAULT_OIDC_SCOPES,
|
|
677
|
+
...(config.allowedRedirectUris !== undefined && {
|
|
678
|
+
allowedRedirectUris: config.allowedRedirectUris,
|
|
679
|
+
}),
|
|
680
|
+
...(config.metadata !== undefined && { metadata: config.metadata }),
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
489
684
|
function buildAuthorizationUrl(input: {
|
|
490
|
-
readonly config:
|
|
685
|
+
readonly config: OAuthFlowConfig;
|
|
491
686
|
readonly state: string;
|
|
492
687
|
readonly codeChallenge: string;
|
|
493
688
|
readonly redirectUri: string;
|
|
@@ -504,25 +699,129 @@ function buildAuthorizationUrl(input: {
|
|
|
504
699
|
return url.toString();
|
|
505
700
|
}
|
|
506
701
|
|
|
702
|
+
function providerNotFound(c: Context<AppEnv>, providerId: string): Response {
|
|
703
|
+
c.status(statusFor('identity-provider-not-found') as never);
|
|
704
|
+
return c.json(
|
|
705
|
+
toWireError(
|
|
706
|
+
{
|
|
707
|
+
code: 'identity-provider-not-found',
|
|
708
|
+
message: `No identity provider registered with id "${providerId}"`,
|
|
709
|
+
providerId,
|
|
710
|
+
},
|
|
711
|
+
c.get('requestId'),
|
|
712
|
+
),
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** What the issuer's discovery gave: stale once the issuer changes. */
|
|
717
|
+
const DISCOVERED_OIDC_FIELDS = [
|
|
718
|
+
'authorizationEndpoint',
|
|
719
|
+
'tokenEndpoint',
|
|
720
|
+
'userinfoEndpoint',
|
|
721
|
+
'jwksEndpoint',
|
|
722
|
+
] as const;
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* A PATCH body merged into the stored provider: a field given replaces
|
|
726
|
+
* it, `null` removes it, `providerId` and `kind` can't change. `signIn`
|
|
727
|
+
* is the deployment's, so it never comes from the stored copy. A new
|
|
728
|
+
* `issuer` drops the endpoints discovered from the old one.
|
|
729
|
+
*/
|
|
730
|
+
function mergeProviderChanges(
|
|
731
|
+
existing: ProviderConfig,
|
|
732
|
+
body: unknown,
|
|
733
|
+
): ParsedOk<Record<string, unknown>> | ParsedErr {
|
|
734
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
735
|
+
return { kind: 'err', error: { code: 'bad-input', message: 'Request body must be an object' } };
|
|
736
|
+
}
|
|
737
|
+
const changes = body as Record<string, unknown>;
|
|
738
|
+
if (changes.providerId !== undefined && changes.providerId !== existing.providerId) {
|
|
739
|
+
return invalid(
|
|
740
|
+
"`providerId` can't change: register the provider under the new id, then unregister this one",
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (changes.kind !== undefined && changes.kind !== existing.kind) {
|
|
744
|
+
return invalid(
|
|
745
|
+
"`kind` can't change: register a new provider of that kind, then unregister this one",
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
const { signIn: _deployments, ...stored } = serializeProviderConfig(existing);
|
|
749
|
+
const merged: Record<string, unknown> = stored;
|
|
750
|
+
if (
|
|
751
|
+
existing.kind === 'oidc' &&
|
|
752
|
+
changes.issuer !== undefined &&
|
|
753
|
+
changes.issuer !== existing.issuer
|
|
754
|
+
) {
|
|
755
|
+
for (const field of DISCOVERED_OIDC_FIELDS) delete merged[field];
|
|
756
|
+
}
|
|
757
|
+
for (const [field, value] of Object.entries(changes)) {
|
|
758
|
+
if (value === null) delete merged[field];
|
|
759
|
+
else merged[field] = value;
|
|
760
|
+
}
|
|
761
|
+
return { kind: 'ok', value: merged };
|
|
762
|
+
}
|
|
763
|
+
|
|
507
764
|
function serializeProviderConfig(c: ProviderConfig): Record<string, unknown> {
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// "
|
|
511
|
-
|
|
765
|
+
// References to secrets (`clientSecretRef`, `spSigningKeyRef`…) are safe
|
|
766
|
+
// to return: they aren't the secrets. Clients need enough to render a
|
|
767
|
+
// "Sign in with X" button and to configure the identity provider.
|
|
768
|
+
const base = {
|
|
512
769
|
providerId: c.providerId,
|
|
513
770
|
kind: c.kind,
|
|
514
|
-
|
|
515
|
-
clientSecretRef: c.clientSecretRef,
|
|
516
|
-
authorizationEndpoint: c.authorizationEndpoint,
|
|
517
|
-
tokenEndpoint: c.tokenEndpoint,
|
|
518
|
-
...(c.userinfoEndpoint !== undefined && { userinfoEndpoint: c.userinfoEndpoint }),
|
|
519
|
-
scopes: c.scopes,
|
|
520
|
-
...(c.allowedRedirectUris !== undefined && {
|
|
521
|
-
allowedRedirectUris: c.allowedRedirectUris,
|
|
522
|
-
}),
|
|
523
|
-
...(c.claimMapping !== undefined && { claimMapping: c.claimMapping }),
|
|
524
|
-
...(c.metadata !== undefined && { metadata: c.metadata }),
|
|
771
|
+
...definedOf(c, ['displayName', 'domains', 'join', 'signIn', 'metadata']),
|
|
525
772
|
};
|
|
773
|
+
switch (c.kind) {
|
|
774
|
+
case 'oauth2':
|
|
775
|
+
return {
|
|
776
|
+
...base,
|
|
777
|
+
clientId: c.clientId,
|
|
778
|
+
clientSecretRef: c.clientSecretRef,
|
|
779
|
+
authorizationEndpoint: c.authorizationEndpoint,
|
|
780
|
+
tokenEndpoint: c.tokenEndpoint,
|
|
781
|
+
scopes: c.scopes,
|
|
782
|
+
...definedOf(c, ['userinfoEndpoint', 'allowedRedirectUris', 'claimMapping']),
|
|
783
|
+
};
|
|
784
|
+
case 'oidc':
|
|
785
|
+
return {
|
|
786
|
+
...base,
|
|
787
|
+
issuer: c.issuer,
|
|
788
|
+
clientId: c.clientId,
|
|
789
|
+
clientSecretRef: c.clientSecretRef,
|
|
790
|
+
...definedOf(c, [
|
|
791
|
+
'scopes',
|
|
792
|
+
'authorizationEndpoint',
|
|
793
|
+
'tokenEndpoint',
|
|
794
|
+
'userinfoEndpoint',
|
|
795
|
+
'jwksEndpoint',
|
|
796
|
+
'allowedRedirectUris',
|
|
797
|
+
'claimMapping',
|
|
798
|
+
]),
|
|
799
|
+
};
|
|
800
|
+
case 'saml':
|
|
801
|
+
return {
|
|
802
|
+
...base,
|
|
803
|
+
...definedOf(c, [
|
|
804
|
+
'idpMetadataXml',
|
|
805
|
+
'idpEntityId',
|
|
806
|
+
'idpSsoUrl',
|
|
807
|
+
'idpCertificates',
|
|
808
|
+
'spSigningKeyRef',
|
|
809
|
+
'spDecryptionKeyRef',
|
|
810
|
+
'wantAssertionsSigned',
|
|
811
|
+
'attributeMapping',
|
|
812
|
+
]),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** The named fields of `obj` that are set. */
|
|
818
|
+
function definedOf<T extends object, K extends keyof T>(
|
|
819
|
+
obj: T,
|
|
820
|
+
keys: readonly K[],
|
|
821
|
+
): Partial<Pick<T, K>> {
|
|
822
|
+
const out: Partial<Pick<T, K>> = {};
|
|
823
|
+
for (const k of keys) if (obj[k] !== undefined) out[k] = obj[k];
|
|
824
|
+
return out;
|
|
526
825
|
}
|
|
527
826
|
|
|
528
827
|
type ParsedOk<T> = { readonly kind: 'ok'; readonly value: T };
|
|
@@ -561,124 +860,234 @@ async function parseOptionalJsonBody(
|
|
|
561
860
|
}
|
|
562
861
|
}
|
|
563
862
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
863
|
+
/**
|
|
864
|
+
* Fields that would carry a secret itself rather than a reference to one:
|
|
865
|
+
* refused outright, so a secret sent by mistake is never stored.
|
|
866
|
+
*/
|
|
867
|
+
const PLAINTEXT_SECRET_FIELDS: Readonly<Record<string, string>> = {
|
|
868
|
+
clientSecret: 'clientSecretRef',
|
|
869
|
+
spSigningKey: 'spSigningKeyRef',
|
|
870
|
+
spDecryptionKey: 'spDecryptionKeyRef',
|
|
871
|
+
privateKey: 'spSigningKeyRef',
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const DOMAIN_RE = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
875
|
+
|
|
876
|
+
function invalid(message: string): ParsedErr {
|
|
877
|
+
return { kind: 'err', error: { code: 'invalid-provider-config', message } };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Reads typed fields off a request body, collecting the first problem. */
|
|
881
|
+
class FieldReader {
|
|
882
|
+
error: ParsedErr | undefined;
|
|
883
|
+
constructor(private readonly b: Record<string, unknown>) {}
|
|
884
|
+
|
|
885
|
+
str(k: string): string {
|
|
886
|
+
const v = this.b[k];
|
|
887
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
888
|
+
this.error ??= invalid(`Field \`${k}\` must be a non-empty string`);
|
|
889
|
+
return '';
|
|
890
|
+
}
|
|
891
|
+
return v;
|
|
567
892
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const v = b[k];
|
|
893
|
+
|
|
894
|
+
optStr(k: string): string | undefined {
|
|
895
|
+
const v = this.b[k];
|
|
896
|
+
if (v === undefined) return undefined;
|
|
571
897
|
if (typeof v !== 'string' || v.length === 0) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
error: {
|
|
575
|
-
code: 'invalid-provider-config',
|
|
576
|
-
message: `Field \`${k}\` must be a non-empty string`,
|
|
577
|
-
},
|
|
578
|
-
};
|
|
898
|
+
this.error ??= invalid(`\`${k}\` must be a non-empty string when supplied`);
|
|
899
|
+
return undefined;
|
|
579
900
|
}
|
|
580
901
|
return v;
|
|
581
|
-
};
|
|
582
|
-
const providerId = requiredStr('providerId');
|
|
583
|
-
if (typeof providerId !== 'string') return providerId;
|
|
584
|
-
const clientId = requiredStr('clientId');
|
|
585
|
-
if (typeof clientId !== 'string') return clientId;
|
|
586
|
-
const clientSecretRef = requiredStr('clientSecretRef');
|
|
587
|
-
if (typeof clientSecretRef !== 'string') return clientSecretRef;
|
|
588
|
-
const authorizationEndpoint = requiredStr('authorizationEndpoint');
|
|
589
|
-
if (typeof authorizationEndpoint !== 'string') return authorizationEndpoint;
|
|
590
|
-
const tokenEndpoint = requiredStr('tokenEndpoint');
|
|
591
|
-
if (typeof tokenEndpoint !== 'string') return tokenEndpoint;
|
|
592
|
-
|
|
593
|
-
const kindRaw = b.kind;
|
|
594
|
-
if (kindRaw !== 'oauth2' && kindRaw !== 'oidc') {
|
|
595
|
-
return {
|
|
596
|
-
kind: 'err',
|
|
597
|
-
error: {
|
|
598
|
-
code: 'invalid-provider-config',
|
|
599
|
-
message: '`kind` must be `oauth2` or `oidc`',
|
|
600
|
-
},
|
|
601
|
-
};
|
|
602
902
|
}
|
|
603
903
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
904
|
+
url(k: string, required: boolean): string | undefined {
|
|
905
|
+
const v = required ? this.str(k) : this.optStr(k);
|
|
906
|
+
if (v === undefined || v === '') return v;
|
|
907
|
+
try {
|
|
908
|
+
new URL(v);
|
|
909
|
+
} catch {
|
|
910
|
+
this.error ??= invalid(`\`${k}\` must be an absolute URL`);
|
|
911
|
+
}
|
|
912
|
+
return v;
|
|
613
913
|
}
|
|
614
|
-
const scopes = scopesRaw as string[];
|
|
615
914
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
915
|
+
strings(k: string, required: boolean): readonly string[] | undefined {
|
|
916
|
+
const v = this.b[k];
|
|
917
|
+
if (v === undefined) {
|
|
918
|
+
if (required) this.error ??= invalid(`\`${k}\` must be an array of strings`);
|
|
919
|
+
return undefined;
|
|
920
|
+
}
|
|
921
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string' || x.length === 0)) {
|
|
922
|
+
this.error ??= invalid(`\`${k}\` must be an array of non-empty strings`);
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
return v as string[];
|
|
625
926
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
message: '`metadata` must be an object when supplied',
|
|
636
|
-
},
|
|
637
|
-
};
|
|
927
|
+
|
|
928
|
+
bool(k: string): boolean | undefined {
|
|
929
|
+
const v = this.b[k];
|
|
930
|
+
if (v === undefined) return undefined;
|
|
931
|
+
if (typeof v !== 'boolean') {
|
|
932
|
+
this.error ??= invalid(`\`${k}\` must be a boolean when supplied`);
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
return v;
|
|
638
936
|
}
|
|
639
937
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
938
|
+
object(k: string): Record<string, unknown> | undefined {
|
|
939
|
+
const v = this.b[k];
|
|
940
|
+
if (v === undefined) return undefined;
|
|
941
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
|
|
942
|
+
this.error ??= invalid(`\`${k}\` must be an object when supplied`);
|
|
943
|
+
return undefined;
|
|
944
|
+
}
|
|
945
|
+
return v as Record<string, unknown>;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function parseProviderConfig(body: unknown): ParsedOk<ProviderConfig> | ParsedErr {
|
|
950
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
951
|
+
return { kind: 'err', error: { code: 'bad-input', message: 'Request body must be an object' } };
|
|
952
|
+
}
|
|
953
|
+
const b = body as Record<string, unknown>;
|
|
954
|
+
for (const [field, instead] of Object.entries(PLAINTEXT_SECRET_FIELDS)) {
|
|
955
|
+
if (b[field] !== undefined) {
|
|
956
|
+
return invalid(
|
|
957
|
+
`\`${field}\` is never accepted: register \`${instead}\`, a reference the deployment resolves`,
|
|
958
|
+
);
|
|
654
959
|
}
|
|
655
|
-
allowedRedirectUris = allowedRedirectUrisRaw as string[];
|
|
656
960
|
}
|
|
961
|
+
const kind = b.kind;
|
|
962
|
+
if (kind !== 'oauth2' && kind !== 'oidc' && kind !== 'saml') {
|
|
963
|
+
return invalid('`kind` must be `oidc`, `saml` or `oauth2`');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const r = new FieldReader(b);
|
|
967
|
+
const providerId = r.str('providerId');
|
|
968
|
+
const displayName = r.optStr('displayName');
|
|
969
|
+
const domainsRaw = r.strings('domains', false);
|
|
970
|
+
const domains = domainsRaw?.map((d) => d.toLowerCase());
|
|
971
|
+
if (domains?.some((d) => !DOMAIN_RE.test(d))) {
|
|
972
|
+
r.error ??= invalid('`domains` must be email domains, e.g. `acme.com`');
|
|
973
|
+
}
|
|
974
|
+
const join = b.join;
|
|
975
|
+
if (join !== undefined && join !== 'invite' && join !== 'domain') {
|
|
976
|
+
r.error ??= invalid('`join` must be `invite` or `domain`');
|
|
977
|
+
}
|
|
978
|
+
if (join === 'domain' && (domains === undefined || domains.length === 0)) {
|
|
979
|
+
r.error ??= invalid('`join: domain` needs `domains`');
|
|
980
|
+
}
|
|
981
|
+
const metadata = r.object('metadata');
|
|
982
|
+
const base = {
|
|
983
|
+
providerId,
|
|
984
|
+
...(displayName !== undefined && { displayName }),
|
|
985
|
+
...(domains !== undefined && { domains }),
|
|
986
|
+
...(join !== undefined && { join: join as 'invite' | 'domain' }),
|
|
987
|
+
...(metadata !== undefined && { metadata }),
|
|
988
|
+
};
|
|
657
989
|
|
|
658
|
-
const claimMappingRaw = b.claimMapping;
|
|
659
990
|
let claimMapping: ClaimMappingSpec | undefined;
|
|
660
|
-
if (
|
|
661
|
-
const parsed = parseClaimMapping(
|
|
991
|
+
if (kind !== 'saml' && b.claimMapping !== undefined) {
|
|
992
|
+
const parsed = parseClaimMapping(b.claimMapping);
|
|
662
993
|
if (parsed.kind === 'err') return parsed;
|
|
663
994
|
claimMapping = parsed.value;
|
|
664
995
|
}
|
|
665
996
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
997
|
+
let value: ProviderConfig;
|
|
998
|
+
if (kind === 'oauth2') {
|
|
999
|
+
const clientId = r.str('clientId');
|
|
1000
|
+
const clientSecretRef = r.str('clientSecretRef');
|
|
1001
|
+
const authorizationEndpoint = r.str('authorizationEndpoint');
|
|
1002
|
+
const tokenEndpoint = r.str('tokenEndpoint');
|
|
1003
|
+
const userinfoEndpoint = r.optStr('userinfoEndpoint');
|
|
1004
|
+
const scopes = r.strings('scopes', true) ?? [];
|
|
1005
|
+
const allowedRedirectUris = r.strings('allowedRedirectUris', false);
|
|
1006
|
+
value = {
|
|
1007
|
+
...base,
|
|
1008
|
+
kind,
|
|
671
1009
|
clientId,
|
|
672
1010
|
clientSecretRef,
|
|
673
1011
|
authorizationEndpoint,
|
|
674
1012
|
tokenEndpoint,
|
|
675
|
-
...(typeof userinfoEndpointRaw === 'string' && { userinfoEndpoint: userinfoEndpointRaw }),
|
|
676
1013
|
scopes,
|
|
1014
|
+
...(userinfoEndpoint !== undefined && { userinfoEndpoint }),
|
|
677
1015
|
...(allowedRedirectUris !== undefined && { allowedRedirectUris }),
|
|
678
1016
|
...(claimMapping !== undefined && { claimMapping }),
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1017
|
+
};
|
|
1018
|
+
} else if (kind === 'oidc') {
|
|
1019
|
+
const issuer = r.url('issuer', true) ?? '';
|
|
1020
|
+
const clientId = r.str('clientId');
|
|
1021
|
+
const clientSecretRef = r.str('clientSecretRef');
|
|
1022
|
+
const scopes = r.strings('scopes', false);
|
|
1023
|
+
const authorizationEndpoint = r.url('authorizationEndpoint', false);
|
|
1024
|
+
const tokenEndpoint = r.url('tokenEndpoint', false);
|
|
1025
|
+
const userinfoEndpoint = r.url('userinfoEndpoint', false);
|
|
1026
|
+
const jwksEndpoint = r.url('jwksEndpoint', false);
|
|
1027
|
+
const allowedRedirectUris = r.strings('allowedRedirectUris', false);
|
|
1028
|
+
value = {
|
|
1029
|
+
...base,
|
|
1030
|
+
kind,
|
|
1031
|
+
issuer,
|
|
1032
|
+
clientId,
|
|
1033
|
+
clientSecretRef,
|
|
1034
|
+
...(scopes !== undefined && { scopes }),
|
|
1035
|
+
...(authorizationEndpoint !== undefined && { authorizationEndpoint }),
|
|
1036
|
+
...(tokenEndpoint !== undefined && { tokenEndpoint }),
|
|
1037
|
+
...(userinfoEndpoint !== undefined && { userinfoEndpoint }),
|
|
1038
|
+
...(jwksEndpoint !== undefined && { jwksEndpoint }),
|
|
1039
|
+
...(allowedRedirectUris !== undefined && { allowedRedirectUris }),
|
|
1040
|
+
...(claimMapping !== undefined && { claimMapping }),
|
|
1041
|
+
};
|
|
1042
|
+
} else {
|
|
1043
|
+
const idpMetadataXml = r.optStr('idpMetadataXml');
|
|
1044
|
+
const idpEntityId = r.optStr('idpEntityId');
|
|
1045
|
+
const idpSsoUrl = r.url('idpSsoUrl', false);
|
|
1046
|
+
const idpCertificates = r.strings('idpCertificates', false);
|
|
1047
|
+
if (
|
|
1048
|
+
idpMetadataXml === undefined &&
|
|
1049
|
+
(idpEntityId === undefined ||
|
|
1050
|
+
idpSsoUrl === undefined ||
|
|
1051
|
+
idpCertificates === undefined ||
|
|
1052
|
+
idpCertificates.length === 0)
|
|
1053
|
+
) {
|
|
1054
|
+
r.error ??= invalid(
|
|
1055
|
+
'A SAML provider needs `idpMetadataXml`, or `idpEntityId` + `idpSsoUrl` + `idpCertificates`',
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
const spSigningKeyRef = r.optStr('spSigningKeyRef');
|
|
1059
|
+
const spDecryptionKeyRef = r.optStr('spDecryptionKeyRef');
|
|
1060
|
+
const wantAssertionsSigned = r.bool('wantAssertionsSigned');
|
|
1061
|
+
const mappingRaw = r.object('attributeMapping');
|
|
1062
|
+
let attributeMapping: SamlAttributeMapping | undefined;
|
|
1063
|
+
if (mappingRaw !== undefined) {
|
|
1064
|
+
const m = new FieldReader(mappingRaw);
|
|
1065
|
+
const userId = m.optStr('userId');
|
|
1066
|
+
const email = m.optStr('email');
|
|
1067
|
+
const displayName = m.optStr('displayName');
|
|
1068
|
+
if (m.error !== undefined)
|
|
1069
|
+
r.error ??= invalid(`\`attributeMapping\`: ${m.error.error.message}`);
|
|
1070
|
+
attributeMapping = {
|
|
1071
|
+
...(userId !== undefined && { userId }),
|
|
1072
|
+
...(email !== undefined && { email }),
|
|
1073
|
+
...(displayName !== undefined && { displayName }),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
value = {
|
|
1077
|
+
...base,
|
|
1078
|
+
kind,
|
|
1079
|
+
...(idpMetadataXml !== undefined && { idpMetadataXml }),
|
|
1080
|
+
...(idpEntityId !== undefined && { idpEntityId }),
|
|
1081
|
+
...(idpSsoUrl !== undefined && { idpSsoUrl }),
|
|
1082
|
+
...(idpCertificates !== undefined && { idpCertificates }),
|
|
1083
|
+
...(spSigningKeyRef !== undefined && { spSigningKeyRef }),
|
|
1084
|
+
...(spDecryptionKeyRef !== undefined && { spDecryptionKeyRef }),
|
|
1085
|
+
...(wantAssertionsSigned !== undefined && { wantAssertionsSigned }),
|
|
1086
|
+
...(attributeMapping !== undefined && { attributeMapping }),
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (r.error !== undefined) return r.error;
|
|
1090
|
+
return { kind: 'ok', value };
|
|
682
1091
|
}
|
|
683
1092
|
|
|
684
1093
|
function parseClaimMapping(raw: unknown): ParsedOk<ClaimMappingSpec> | ParsedErr {
|
|
@@ -804,3 +1213,62 @@ function parseCallbackBody(
|
|
|
804
1213
|
}
|
|
805
1214
|
return { kind: 'ok', value: { code, state } };
|
|
806
1215
|
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* `POST /v1/auth/logout`: revokes the caller's session; a browser session
|
|
1219
|
+
* loses its cookie too. Also mounted on its own with cookie sessions and
|
|
1220
|
+
* no identity providers (console token sign-in).
|
|
1221
|
+
*/
|
|
1222
|
+
export function logoutHandler(
|
|
1223
|
+
sessionStore: SessionStoreBinding,
|
|
1224
|
+
auditEvents?: AuditEventBinding,
|
|
1225
|
+
): (c: Context<AppEnv>) => Promise<Response> {
|
|
1226
|
+
return async (c) => {
|
|
1227
|
+
const requestId = c.get('requestId');
|
|
1228
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
1229
|
+
const sessionId = c.get('sessionId') as SessionId | undefined;
|
|
1230
|
+
if (sessionId === undefined) {
|
|
1231
|
+
c.status(statusFor('auth-not-session-token') as never);
|
|
1232
|
+
return c.json(
|
|
1233
|
+
toWireError(
|
|
1234
|
+
{
|
|
1235
|
+
code: 'auth-not-session-token',
|
|
1236
|
+
message:
|
|
1237
|
+
'Logout is only valid for session tokens (`kgi_sk_*`); bearer tokens are managed via `/v1/tokens`',
|
|
1238
|
+
},
|
|
1239
|
+
requestId,
|
|
1240
|
+
),
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const outcome = await sessionStore.revoke({ tenantId, sessionId });
|
|
1244
|
+
const userId = c.get('userId');
|
|
1245
|
+
if (auditEvents !== undefined && outcome.revoked) {
|
|
1246
|
+
try {
|
|
1247
|
+
const appended = await auditEvents.append([
|
|
1248
|
+
{
|
|
1249
|
+
id: randomUUID(),
|
|
1250
|
+
tenantId,
|
|
1251
|
+
kind: 'signed-out',
|
|
1252
|
+
timestamp: new Date().toISOString() as Timestamp,
|
|
1253
|
+
actor: userId !== undefined ? `user:${userId}` : 'system',
|
|
1254
|
+
outcome: 'succeeded',
|
|
1255
|
+
payload: { v: 1, doc: { sessionId } },
|
|
1256
|
+
},
|
|
1257
|
+
]);
|
|
1258
|
+
if (appended.kind === 'err') {
|
|
1259
|
+
c.get('log').warn(`signed-out audit event failed: ${appended.error.message}`);
|
|
1260
|
+
}
|
|
1261
|
+
} catch (cause) {
|
|
1262
|
+
c.get('log').warn(
|
|
1263
|
+
`signed-out audit event failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const cookieName = c.get('sessionCookieName');
|
|
1268
|
+
if (cookieName !== undefined) {
|
|
1269
|
+
// A browser session: the cookie goes with it.
|
|
1270
|
+
c.header('Set-Cookie', `${cookieName}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`);
|
|
1271
|
+
}
|
|
1272
|
+
return c.json({ sessionId, revoked: outcome.revoked });
|
|
1273
|
+
};
|
|
1274
|
+
}
|