@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/dist/routes/auth.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
import { statusFor, toWireError } from '../errors.js';
|
|
6
6
|
import { encodeSessionToken } from '../middleware/auth.js';
|
|
7
|
+
import { withholdFromReplay } from '../middleware/idempotency.js';
|
|
8
|
+
import { tenantResourceAccess } from './tenant-access.js';
|
|
7
9
|
const DEFAULT_STATE_TTL_MS = 10 * 60 * 1000;
|
|
8
10
|
/**
|
|
9
11
|
* Build the auth router. Two Hono routers are returned — one gated
|
|
@@ -20,6 +22,11 @@ export function authRouters(options) {
|
|
|
20
22
|
const refreshToken = options.refreshToken;
|
|
21
23
|
const stateTtlMs = options.stateTtlMs ?? DEFAULT_STATE_TTL_MS;
|
|
22
24
|
const authed = new Hono();
|
|
25
|
+
// The provider catalog is tenant-wide: any GET (the list, one provider,
|
|
26
|
+
// its sign-in URLs) needs `read` on the tenant, any change `admin`.
|
|
27
|
+
const providerAccess = tenantResourceAccess(options.authorizer);
|
|
28
|
+
authed.use('/providers', providerAccess);
|
|
29
|
+
authed.use('/providers/*', providerAccess);
|
|
23
30
|
// ---------- GET /providers ----------
|
|
24
31
|
authed.get('/providers', async (c) => {
|
|
25
32
|
const tenantId = c.get('tenantId');
|
|
@@ -50,9 +57,94 @@ export function authRouters(options) {
|
|
|
50
57
|
}, requestId));
|
|
51
58
|
}
|
|
52
59
|
const outcome = await identityProvider.register({ tenantId, config: parsed.value });
|
|
60
|
+
if (outcome.kind === 'invalid') {
|
|
61
|
+
c.status(statusFor('identity-provider-invalid'));
|
|
62
|
+
return c.json(toWireError({
|
|
63
|
+
code: 'identity-provider-invalid',
|
|
64
|
+
message: outcome.message,
|
|
65
|
+
providerId: parsed.value.providerId,
|
|
66
|
+
}, requestId));
|
|
67
|
+
}
|
|
53
68
|
c.status(201);
|
|
54
|
-
return c.json({
|
|
69
|
+
return c.json({
|
|
70
|
+
providerId: outcome.providerId,
|
|
71
|
+
...(outcome.provider !== undefined && {
|
|
72
|
+
provider: serializeProviderConfig(outcome.provider),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// ---------- GET /providers/:providerId ----------
|
|
77
|
+
authed.get('/providers/:providerId', async (c) => {
|
|
78
|
+
const tenantId = c.get('tenantId');
|
|
79
|
+
const providerId = c.req.param('providerId');
|
|
80
|
+
const found = await identityProvider.get({ tenantId, providerId });
|
|
81
|
+
if (found === null)
|
|
82
|
+
return providerNotFound(c, providerId);
|
|
83
|
+
return c.json(serializeProviderConfig(found));
|
|
55
84
|
});
|
|
85
|
+
// ---------- GET /providers/:providerId/sign-in ----------
|
|
86
|
+
const signInUrls = identityProvider.signInUrls;
|
|
87
|
+
if (signInUrls !== undefined) {
|
|
88
|
+
authed.get('/providers/:providerId/sign-in', async (c) => {
|
|
89
|
+
const requestId = c.get('requestId');
|
|
90
|
+
const tenantId = c.get('tenantId');
|
|
91
|
+
const providerId = c.req.param('providerId');
|
|
92
|
+
const asked = c.req.query('kind');
|
|
93
|
+
if (asked !== undefined && asked !== 'oidc' && asked !== 'saml' && asked !== 'oauth2') {
|
|
94
|
+
c.status(statusFor('bad-input'));
|
|
95
|
+
return c.json(toWireError({ code: 'bad-input', message: '`kind` must be `oidc`, `saml` or `oauth2`' }, requestId));
|
|
96
|
+
}
|
|
97
|
+
const registered = await identityProvider.get({ tenantId, providerId });
|
|
98
|
+
const kind = asked ?? registered?.kind;
|
|
99
|
+
if (kind === undefined) {
|
|
100
|
+
c.status(statusFor('bad-input'));
|
|
101
|
+
return c.json(toWireError({
|
|
102
|
+
code: 'bad-input',
|
|
103
|
+
message: `No identity provider "${providerId}" is registered yet: say which \`kind\` it will be (\`?kind=oidc\` or \`?kind=saml\`)`,
|
|
104
|
+
}, requestId));
|
|
105
|
+
}
|
|
106
|
+
const signIn = await signInUrls({ tenantId, providerId, kind });
|
|
107
|
+
if (signIn === undefined) {
|
|
108
|
+
c.status(statusFor('bad-input'));
|
|
109
|
+
return c.json(toWireError({
|
|
110
|
+
code: 'bad-input',
|
|
111
|
+
message: `This deployment doesn't sign in with \`${kind}\` providers`,
|
|
112
|
+
}, requestId));
|
|
113
|
+
}
|
|
114
|
+
return c.json({ providerId, kind, signIn, registered: registered !== null });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// ---------- PATCH /providers/:providerId ----------
|
|
118
|
+
const update = identityProvider.update;
|
|
119
|
+
if (update !== undefined) {
|
|
120
|
+
authed.patch('/providers/:providerId', async (c) => {
|
|
121
|
+
const requestId = c.get('requestId');
|
|
122
|
+
const tenantId = c.get('tenantId');
|
|
123
|
+
const providerId = c.req.param('providerId');
|
|
124
|
+
const parsedJson = await parseJsonBody(c);
|
|
125
|
+
if (parsedJson.kind === 'err') {
|
|
126
|
+
c.status(statusFor('bad-input'));
|
|
127
|
+
return c.json(toWireError(parsedJson.error, requestId));
|
|
128
|
+
}
|
|
129
|
+
const existing = await identityProvider.get({ tenantId, providerId });
|
|
130
|
+
if (existing === null)
|
|
131
|
+
return providerNotFound(c, providerId);
|
|
132
|
+
const merged = mergeProviderChanges(existing, parsedJson.value);
|
|
133
|
+
const parsed = merged.kind === 'err' ? merged : parseProviderConfig(merged.value);
|
|
134
|
+
if (parsed.kind === 'err') {
|
|
135
|
+
c.status(statusFor(parsed.error.code));
|
|
136
|
+
return c.json(toWireError(parsed.error, requestId));
|
|
137
|
+
}
|
|
138
|
+
const outcome = await update({ tenantId, config: parsed.value });
|
|
139
|
+
if (outcome.kind === 'not-found')
|
|
140
|
+
return providerNotFound(c, providerId);
|
|
141
|
+
if (outcome.kind === 'invalid') {
|
|
142
|
+
c.status(statusFor('identity-provider-invalid'));
|
|
143
|
+
return c.json(toWireError({ code: 'identity-provider-invalid', message: outcome.message, providerId }, requestId));
|
|
144
|
+
}
|
|
145
|
+
return c.json({ providerId, provider: serializeProviderConfig(outcome.provider) });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
56
148
|
// ---------- POST /providers/:providerId/unregister ----------
|
|
57
149
|
authed.post('/providers/:providerId/unregister', async (c) => {
|
|
58
150
|
const requestId = c.get('requestId');
|
|
@@ -69,89 +161,100 @@ export function authRouters(options) {
|
|
|
69
161
|
}
|
|
70
162
|
return c.json({ providerId, unregistered: true });
|
|
71
163
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
164
|
+
if (exchangeCode !== undefined) {
|
|
165
|
+
// ---------- POST /login/:providerId ----------
|
|
166
|
+
authed.post('/login/:providerId', async (c) => {
|
|
167
|
+
const requestId = c.get('requestId');
|
|
168
|
+
const tenantId = c.get('tenantId');
|
|
169
|
+
const providerId = c.req.param('providerId');
|
|
170
|
+
const found = await identityProvider.get({ tenantId, providerId });
|
|
171
|
+
if (found === null) {
|
|
172
|
+
c.status(statusFor('identity-provider-not-found'));
|
|
173
|
+
return c.json(toWireError({
|
|
174
|
+
code: 'identity-provider-not-found',
|
|
175
|
+
message: `No identity provider registered with id "${providerId}"`,
|
|
176
|
+
providerId,
|
|
177
|
+
}, requestId));
|
|
178
|
+
}
|
|
179
|
+
const config = oauthFlowOf(found);
|
|
180
|
+
if (config === null) {
|
|
181
|
+
c.status(statusFor('invalid-provider-config'));
|
|
182
|
+
return c.json(toWireError({
|
|
183
|
+
code: 'invalid-provider-config',
|
|
184
|
+
message: `Identity provider "${providerId}" (${found.kind}) signs in through the deployment's own sign-in, not this endpoint`,
|
|
185
|
+
providerId,
|
|
186
|
+
}, requestId));
|
|
187
|
+
}
|
|
188
|
+
let redirectUri;
|
|
189
|
+
const parsedBody = await parseOptionalJsonBody(c);
|
|
190
|
+
if (parsedBody.kind === 'err') {
|
|
191
|
+
c.status(statusFor('bad-input'));
|
|
192
|
+
return c.json(toWireError(parsedBody.error, requestId));
|
|
193
|
+
}
|
|
194
|
+
if (parsedBody.value !== undefined) {
|
|
195
|
+
const raw = parsedBody.value.redirectUri;
|
|
196
|
+
if (raw !== undefined) {
|
|
197
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
198
|
+
c.status(statusFor('bad-input'));
|
|
199
|
+
return c.json(toWireError({ code: 'bad-input', message: '`redirectUri` must be a non-empty string' }, requestId));
|
|
200
|
+
}
|
|
201
|
+
redirectUri = raw;
|
|
98
202
|
}
|
|
99
|
-
redirectUri = raw;
|
|
100
203
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
204
|
+
const state = base64Url(randomBytes(32));
|
|
205
|
+
const codeVerifier = base64Url(randomBytes(64));
|
|
206
|
+
const codeChallenge = base64Url(createHash('sha256').update(codeVerifier).digest());
|
|
207
|
+
const effectiveRedirect = redirectUri ??
|
|
208
|
+
(typeof config.metadata?.defaultRedirectUri === 'string'
|
|
209
|
+
? String(config.metadata?.defaultRedirectUri)
|
|
210
|
+
: '');
|
|
211
|
+
if (effectiveRedirect.length === 0) {
|
|
212
|
+
c.status(statusFor('bad-input'));
|
|
213
|
+
return c.json(toWireError({
|
|
214
|
+
code: 'bad-input',
|
|
215
|
+
message: '`redirectUri` must be supplied in the body or via `metadata.defaultRedirectUri` on the provider config',
|
|
216
|
+
}, requestId));
|
|
217
|
+
}
|
|
218
|
+
// OAuth 2.1 BCP redirect-URI allowlist enforcement. Absent /
|
|
219
|
+
// empty list means pass-through (no allowlist check); populated
|
|
220
|
+
// list requires an exact-string match against the effective
|
|
221
|
+
// redirect URI (either the body-supplied value or the resolved
|
|
222
|
+
// `metadata.defaultRedirectUri`). This is the primary gate — the
|
|
223
|
+
// callback route re-verifies against the same list for
|
|
224
|
+
// belt-and-suspenders defense in case the allowlist tightened
|
|
225
|
+
// between login and callback.
|
|
226
|
+
if (config.allowedRedirectUris !== undefined &&
|
|
227
|
+
config.allowedRedirectUris.length > 0 &&
|
|
228
|
+
!config.allowedRedirectUris.includes(effectiveRedirect)) {
|
|
229
|
+
c.status(statusFor('redirect-uri-not-allowed'));
|
|
230
|
+
return c.json(toWireError({
|
|
231
|
+
code: 'redirect-uri-not-allowed',
|
|
232
|
+
message: `redirect_uri "${effectiveRedirect}" is not in the provider's allowedRedirectUris list`,
|
|
233
|
+
providerId,
|
|
234
|
+
}, requestId));
|
|
235
|
+
}
|
|
236
|
+
await stateStore.put({
|
|
237
|
+
state,
|
|
238
|
+
tenantId,
|
|
131
239
|
providerId,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return c.json({
|
|
149
|
-
authorizationUrl,
|
|
150
|
-
state,
|
|
151
|
-
codeChallenge,
|
|
152
|
-
codeChallengeMethod: 'S256',
|
|
240
|
+
codeVerifier,
|
|
241
|
+
redirectUri: effectiveRedirect,
|
|
242
|
+
expiresAt: Date.now() + stateTtlMs,
|
|
243
|
+
});
|
|
244
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
245
|
+
config,
|
|
246
|
+
state,
|
|
247
|
+
codeChallenge,
|
|
248
|
+
redirectUri: effectiveRedirect,
|
|
249
|
+
});
|
|
250
|
+
return c.json({
|
|
251
|
+
authorizationUrl,
|
|
252
|
+
state,
|
|
253
|
+
codeChallenge,
|
|
254
|
+
codeChallengeMethod: 'S256',
|
|
255
|
+
});
|
|
153
256
|
});
|
|
154
|
-
}
|
|
257
|
+
}
|
|
155
258
|
// ---------- POST /refresh ----------
|
|
156
259
|
authed.post('/refresh', async (c) => {
|
|
157
260
|
const requestId = c.get('requestId');
|
|
@@ -164,6 +267,15 @@ export function authRouters(options) {
|
|
|
164
267
|
message: 'Refresh is only valid for session tokens (`kgi_sk_*`); bearer tokens are managed via `/v1/tokens`',
|
|
165
268
|
}, requestId));
|
|
166
269
|
}
|
|
270
|
+
if (c.get('sessionCookieName') !== undefined) {
|
|
271
|
+
// Refresh answers with the new token in its body, which a browser
|
|
272
|
+
// session must never hand to page scripts: it ends at its TTL.
|
|
273
|
+
c.status(statusFor('cookie-session-not-refreshable'));
|
|
274
|
+
return c.json(toWireError({
|
|
275
|
+
code: 'cookie-session-not-refreshable',
|
|
276
|
+
message: 'A browser session (cookie) is not refreshed: sign in again when it ends',
|
|
277
|
+
}, requestId));
|
|
278
|
+
}
|
|
167
279
|
const current = await sessionStore.get({ tenantId, sessionId });
|
|
168
280
|
if (current === null || current.revokedAt !== undefined) {
|
|
169
281
|
c.status(statusFor('session-not-found'));
|
|
@@ -199,7 +311,7 @@ export function authRouters(options) {
|
|
|
199
311
|
const fresh = await sessionStore.get({ tenantId, sessionId: createdSession.sessionId });
|
|
200
312
|
if (fresh === null)
|
|
201
313
|
throw new Error('session vanished immediately after create');
|
|
202
|
-
created = { session: fresh, rawToken:
|
|
314
|
+
created = { session: fresh, rawToken: sessionTokenOf(createdSession) };
|
|
203
315
|
}
|
|
204
316
|
else {
|
|
205
317
|
// Rotate the framework token only; keep provider tokens as-is.
|
|
@@ -207,7 +319,7 @@ export function authRouters(options) {
|
|
|
207
319
|
tenantId,
|
|
208
320
|
userId: current.userId,
|
|
209
321
|
providerId: current.providerId,
|
|
210
|
-
accessToken: current.accessToken,
|
|
322
|
+
...(current.accessToken !== undefined && { accessToken: current.accessToken }),
|
|
211
323
|
...(current.refreshToken !== undefined && { refreshToken: current.refreshToken }),
|
|
212
324
|
expiresAt: current.expiresAt,
|
|
213
325
|
scopes: current.scopes,
|
|
@@ -216,7 +328,7 @@ export function authRouters(options) {
|
|
|
216
328
|
const fresh = await sessionStore.get({ tenantId, sessionId: createdSession.sessionId });
|
|
217
329
|
if (fresh === null)
|
|
218
330
|
throw new Error('session vanished immediately after create');
|
|
219
|
-
created = { session: fresh, rawToken:
|
|
331
|
+
created = { session: fresh, rawToken: sessionTokenOf(createdSession) };
|
|
220
332
|
}
|
|
221
333
|
// OAuth 2.1 BCP: mark the old session as ROTATED (not just revoked)
|
|
222
334
|
// so the middleware can return `401 refresh-token-invalid` on reuse
|
|
@@ -224,6 +336,8 @@ export function authRouters(options) {
|
|
|
224
336
|
// compliant clients retry with the fresh token instead of forcing
|
|
225
337
|
// a full re-auth.
|
|
226
338
|
await sessionStore.revoke({ tenantId, sessionId, reason: 'rotate' });
|
|
339
|
+
// A session token: an Idempotency-Key repeat doesn't get it.
|
|
340
|
+
withholdFromReplay(c);
|
|
227
341
|
return c.json({
|
|
228
342
|
sessionToken: created.rawToken,
|
|
229
343
|
sessionId: created.session.id,
|
|
@@ -231,110 +345,126 @@ export function authRouters(options) {
|
|
|
231
345
|
});
|
|
232
346
|
});
|
|
233
347
|
// ---------- POST /logout ----------
|
|
234
|
-
authed.post('/logout',
|
|
235
|
-
const requestId = c.get('requestId');
|
|
236
|
-
const tenantId = c.get('tenantId');
|
|
237
|
-
const sessionId = c.get('sessionId');
|
|
238
|
-
if (sessionId === undefined) {
|
|
239
|
-
c.status(statusFor('auth-not-session-token'));
|
|
240
|
-
return c.json(toWireError({
|
|
241
|
-
code: 'auth-not-session-token',
|
|
242
|
-
message: 'Logout is only valid for session tokens (`kgi_sk_*`); bearer tokens are managed via `/v1/tokens`',
|
|
243
|
-
}, requestId));
|
|
244
|
-
}
|
|
245
|
-
const outcome = await sessionStore.revoke({ tenantId, sessionId });
|
|
246
|
-
return c.json({ sessionId, revoked: outcome.revoked });
|
|
247
|
-
});
|
|
348
|
+
authed.post('/logout', logoutHandler(sessionStore, options.auditEvents));
|
|
248
349
|
const callback = new Hono();
|
|
249
350
|
// ---------- POST /callback/:providerId (mounted outside the bearer chain) ----------
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
});
|
|
287
|
-
if (currentConfig !== null &&
|
|
288
|
-
currentConfig.allowedRedirectUris !== undefined &&
|
|
289
|
-
currentConfig.allowedRedirectUris.length > 0 &&
|
|
290
|
-
!currentConfig.allowedRedirectUris.includes(entry.redirectUri)) {
|
|
291
|
-
c.status(statusFor('redirect-uri-mismatch'));
|
|
292
|
-
return c.json(toWireError({
|
|
293
|
-
code: 'redirect-uri-mismatch',
|
|
294
|
-
message: "redirect_uri from the login state row is not in the provider's current allowedRedirectUris list",
|
|
351
|
+
if (exchangeCode !== undefined) {
|
|
352
|
+
callback.post('/:providerId', async (c) => {
|
|
353
|
+
const requestId = c.get('requestId');
|
|
354
|
+
const providerId = c.req.param('providerId');
|
|
355
|
+
const parsedJson = await parseJsonBody(c);
|
|
356
|
+
if (parsedJson.kind === 'err') {
|
|
357
|
+
c.status(statusFor('bad-input'));
|
|
358
|
+
return c.json(toWireError(parsedJson.error, requestId));
|
|
359
|
+
}
|
|
360
|
+
const parsedCallback = parseCallbackBody(parsedJson.value);
|
|
361
|
+
if (parsedCallback.kind === 'err') {
|
|
362
|
+
c.status(statusFor(parsedCallback.error.code));
|
|
363
|
+
return c.json(toWireError(parsedCallback.error, requestId));
|
|
364
|
+
}
|
|
365
|
+
const { code, state } = parsedCallback.value;
|
|
366
|
+
// `state` alone is uniquely identifying (256 bits of entropy). The
|
|
367
|
+
// store returns the row's `tenantId` so we know which tenant the
|
|
368
|
+
// callback belongs to — cross-checking `providerId` guards against
|
|
369
|
+
// a stolen `state` being replayed against the wrong provider mount.
|
|
370
|
+
const entry = await stateStore.take({ state, providerId });
|
|
371
|
+
if (entry === null) {
|
|
372
|
+
c.status(statusFor('oauth-state-invalid'));
|
|
373
|
+
return c.json(toWireError({
|
|
374
|
+
code: 'oauth-state-invalid',
|
|
375
|
+
message: '`state` is unknown, expired, or already consumed',
|
|
376
|
+
}, requestId));
|
|
377
|
+
}
|
|
378
|
+
// Belt-and-suspenders redirect-URI check. Login already validated
|
|
379
|
+
// the stored redirect_uri against `allowedRedirectUris` when the
|
|
380
|
+
// row was written, but the allowlist may have tightened between
|
|
381
|
+
// login and callback — if so, refuse the exchange rather than
|
|
382
|
+
// handing the caller a session under a redirect the tenant no
|
|
383
|
+
// longer trusts. Also runs when the provider config went missing
|
|
384
|
+
// (unregister mid-flight) so we don't silently proceed.
|
|
385
|
+
const currentConfig = await identityProvider.get({
|
|
386
|
+
tenantId: entry.tenantId,
|
|
295
387
|
providerId,
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
388
|
+
});
|
|
389
|
+
const currentAllowlist = currentConfig === null ? undefined : oauthFlowOf(currentConfig)?.allowedRedirectUris;
|
|
390
|
+
if (currentAllowlist !== undefined &&
|
|
391
|
+
currentAllowlist.length > 0 &&
|
|
392
|
+
!currentAllowlist.includes(entry.redirectUri)) {
|
|
393
|
+
c.status(statusFor('redirect-uri-mismatch'));
|
|
394
|
+
return c.json(toWireError({
|
|
395
|
+
code: 'redirect-uri-mismatch',
|
|
396
|
+
message: "redirect_uri from the login state row is not in the provider's current allowedRedirectUris list",
|
|
397
|
+
providerId,
|
|
398
|
+
}, requestId));
|
|
399
|
+
}
|
|
400
|
+
let outcome;
|
|
401
|
+
try {
|
|
402
|
+
outcome = await exchangeCode({
|
|
403
|
+
tenantId: entry.tenantId,
|
|
404
|
+
providerId,
|
|
405
|
+
code,
|
|
406
|
+
codeVerifier: entry.codeVerifier,
|
|
407
|
+
redirectUri: entry.redirectUri,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
c.status(statusFor('oauth-code-exchange-failed'));
|
|
412
|
+
return c.json(toWireError({
|
|
413
|
+
code: 'oauth-code-exchange-failed',
|
|
414
|
+
message: err instanceof Error ? err.message : 'code exchange failed',
|
|
415
|
+
}, requestId));
|
|
416
|
+
}
|
|
417
|
+
const created = await sessionStore.create({
|
|
301
418
|
tenantId: entry.tenantId,
|
|
419
|
+
userId: outcome.userId,
|
|
302
420
|
providerId,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
421
|
+
accessToken: outcome.accessToken,
|
|
422
|
+
...(outcome.refreshToken !== undefined && { refreshToken: outcome.refreshToken }),
|
|
423
|
+
expiresAt: outcome.expiresAt.toISOString(),
|
|
424
|
+
scopes: outcome.scopes,
|
|
425
|
+
...(outcome.claims !== undefined && { metadata: outcome.claims }),
|
|
426
|
+
});
|
|
427
|
+
// A session token: an Idempotency-Key repeat doesn't get it.
|
|
428
|
+
withholdFromReplay(c);
|
|
429
|
+
c.status(201);
|
|
430
|
+
return c.json({
|
|
431
|
+
sessionToken: sessionTokenOf(created),
|
|
432
|
+
sessionId: created.sessionId,
|
|
433
|
+
expiresAt: created.expiresAt,
|
|
306
434
|
});
|
|
307
|
-
}
|
|
308
|
-
catch (err) {
|
|
309
|
-
c.status(statusFor('oauth-code-exchange-failed'));
|
|
310
|
-
return c.json(toWireError({
|
|
311
|
-
code: 'oauth-code-exchange-failed',
|
|
312
|
-
message: err instanceof Error ? err.message : 'code exchange failed',
|
|
313
|
-
}, requestId));
|
|
314
|
-
}
|
|
315
|
-
const created = await sessionStore.create({
|
|
316
|
-
tenantId: entry.tenantId,
|
|
317
|
-
userId: outcome.userId,
|
|
318
|
-
providerId,
|
|
319
|
-
accessToken: outcome.accessToken,
|
|
320
|
-
...(outcome.refreshToken !== undefined && { refreshToken: outcome.refreshToken }),
|
|
321
|
-
expiresAt: outcome.expiresAt.toISOString(),
|
|
322
|
-
scopes: outcome.scopes,
|
|
323
|
-
...(outcome.claims !== undefined && { metadata: outcome.claims }),
|
|
324
|
-
});
|
|
325
|
-
c.status(201);
|
|
326
|
-
return c.json({
|
|
327
|
-
sessionToken: encodeSessionToken(created.sessionId),
|
|
328
|
-
sessionId: created.sessionId,
|
|
329
|
-
expiresAt: created.expiresAt,
|
|
330
435
|
});
|
|
331
|
-
}
|
|
436
|
+
}
|
|
332
437
|
return { authed, callback };
|
|
333
438
|
}
|
|
334
439
|
// ---------- helpers ----------
|
|
440
|
+
/** The token a store minted, or the older `kgi_sk_<sessionId>` for a store that mints none. */
|
|
441
|
+
function sessionTokenOf(created) {
|
|
442
|
+
return created.token ?? encodeSessionToken(created.sessionId);
|
|
443
|
+
}
|
|
335
444
|
function base64Url(buf) {
|
|
336
445
|
return buf.toString('base64url');
|
|
337
446
|
}
|
|
447
|
+
const DEFAULT_OIDC_SCOPES = ['openid', 'email', 'profile'];
|
|
448
|
+
/**
|
|
449
|
+
* The provider's OAuth flow settings, or `null` when it has none: a SAML
|
|
450
|
+
* provider, or an OIDC one whose endpoints the deployment hasn't
|
|
451
|
+
* discovered (it signs in through the deployment's own browser flow).
|
|
452
|
+
*/
|
|
453
|
+
function oauthFlowOf(config) {
|
|
454
|
+
if (config.kind === 'saml')
|
|
455
|
+
return null;
|
|
456
|
+
if (config.kind === 'oidc' && config.authorizationEndpoint === undefined)
|
|
457
|
+
return null;
|
|
458
|
+
return {
|
|
459
|
+
clientId: config.clientId,
|
|
460
|
+
authorizationEndpoint: config.authorizationEndpoint,
|
|
461
|
+
scopes: config.scopes ?? DEFAULT_OIDC_SCOPES,
|
|
462
|
+
...(config.allowedRedirectUris !== undefined && {
|
|
463
|
+
allowedRedirectUris: config.allowedRedirectUris,
|
|
464
|
+
}),
|
|
465
|
+
...(config.metadata !== undefined && { metadata: config.metadata }),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
338
468
|
function buildAuthorizationUrl(input) {
|
|
339
469
|
const { config, state, codeChallenge, redirectUri } = input;
|
|
340
470
|
const url = new URL(config.authorizationEndpoint);
|
|
@@ -347,25 +477,113 @@ function buildAuthorizationUrl(input) {
|
|
|
347
477
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
348
478
|
return url.toString();
|
|
349
479
|
}
|
|
480
|
+
function providerNotFound(c, providerId) {
|
|
481
|
+
c.status(statusFor('identity-provider-not-found'));
|
|
482
|
+
return c.json(toWireError({
|
|
483
|
+
code: 'identity-provider-not-found',
|
|
484
|
+
message: `No identity provider registered with id "${providerId}"`,
|
|
485
|
+
providerId,
|
|
486
|
+
}, c.get('requestId')));
|
|
487
|
+
}
|
|
488
|
+
/** What the issuer's discovery gave: stale once the issuer changes. */
|
|
489
|
+
const DISCOVERED_OIDC_FIELDS = [
|
|
490
|
+
'authorizationEndpoint',
|
|
491
|
+
'tokenEndpoint',
|
|
492
|
+
'userinfoEndpoint',
|
|
493
|
+
'jwksEndpoint',
|
|
494
|
+
];
|
|
495
|
+
/**
|
|
496
|
+
* A PATCH body merged into the stored provider: a field given replaces
|
|
497
|
+
* it, `null` removes it, `providerId` and `kind` can't change. `signIn`
|
|
498
|
+
* is the deployment's, so it never comes from the stored copy. A new
|
|
499
|
+
* `issuer` drops the endpoints discovered from the old one.
|
|
500
|
+
*/
|
|
501
|
+
function mergeProviderChanges(existing, body) {
|
|
502
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
503
|
+
return { kind: 'err', error: { code: 'bad-input', message: 'Request body must be an object' } };
|
|
504
|
+
}
|
|
505
|
+
const changes = body;
|
|
506
|
+
if (changes.providerId !== undefined && changes.providerId !== existing.providerId) {
|
|
507
|
+
return invalid("`providerId` can't change: register the provider under the new id, then unregister this one");
|
|
508
|
+
}
|
|
509
|
+
if (changes.kind !== undefined && changes.kind !== existing.kind) {
|
|
510
|
+
return invalid("`kind` can't change: register a new provider of that kind, then unregister this one");
|
|
511
|
+
}
|
|
512
|
+
const { signIn: _deployments, ...stored } = serializeProviderConfig(existing);
|
|
513
|
+
const merged = stored;
|
|
514
|
+
if (existing.kind === 'oidc' &&
|
|
515
|
+
changes.issuer !== undefined &&
|
|
516
|
+
changes.issuer !== existing.issuer) {
|
|
517
|
+
for (const field of DISCOVERED_OIDC_FIELDS)
|
|
518
|
+
delete merged[field];
|
|
519
|
+
}
|
|
520
|
+
for (const [field, value] of Object.entries(changes)) {
|
|
521
|
+
if (value === null)
|
|
522
|
+
delete merged[field];
|
|
523
|
+
else
|
|
524
|
+
merged[field] = value;
|
|
525
|
+
}
|
|
526
|
+
return { kind: 'ok', value: merged };
|
|
527
|
+
}
|
|
350
528
|
function serializeProviderConfig(c) {
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
// "
|
|
354
|
-
|
|
529
|
+
// References to secrets (`clientSecretRef`, `spSigningKeyRef`…) are safe
|
|
530
|
+
// to return: they aren't the secrets. Clients need enough to render a
|
|
531
|
+
// "Sign in with X" button and to configure the identity provider.
|
|
532
|
+
const base = {
|
|
355
533
|
providerId: c.providerId,
|
|
356
534
|
kind: c.kind,
|
|
357
|
-
|
|
358
|
-
clientSecretRef: c.clientSecretRef,
|
|
359
|
-
authorizationEndpoint: c.authorizationEndpoint,
|
|
360
|
-
tokenEndpoint: c.tokenEndpoint,
|
|
361
|
-
...(c.userinfoEndpoint !== undefined && { userinfoEndpoint: c.userinfoEndpoint }),
|
|
362
|
-
scopes: c.scopes,
|
|
363
|
-
...(c.allowedRedirectUris !== undefined && {
|
|
364
|
-
allowedRedirectUris: c.allowedRedirectUris,
|
|
365
|
-
}),
|
|
366
|
-
...(c.claimMapping !== undefined && { claimMapping: c.claimMapping }),
|
|
367
|
-
...(c.metadata !== undefined && { metadata: c.metadata }),
|
|
535
|
+
...definedOf(c, ['displayName', 'domains', 'join', 'signIn', 'metadata']),
|
|
368
536
|
};
|
|
537
|
+
switch (c.kind) {
|
|
538
|
+
case 'oauth2':
|
|
539
|
+
return {
|
|
540
|
+
...base,
|
|
541
|
+
clientId: c.clientId,
|
|
542
|
+
clientSecretRef: c.clientSecretRef,
|
|
543
|
+
authorizationEndpoint: c.authorizationEndpoint,
|
|
544
|
+
tokenEndpoint: c.tokenEndpoint,
|
|
545
|
+
scopes: c.scopes,
|
|
546
|
+
...definedOf(c, ['userinfoEndpoint', 'allowedRedirectUris', 'claimMapping']),
|
|
547
|
+
};
|
|
548
|
+
case 'oidc':
|
|
549
|
+
return {
|
|
550
|
+
...base,
|
|
551
|
+
issuer: c.issuer,
|
|
552
|
+
clientId: c.clientId,
|
|
553
|
+
clientSecretRef: c.clientSecretRef,
|
|
554
|
+
...definedOf(c, [
|
|
555
|
+
'scopes',
|
|
556
|
+
'authorizationEndpoint',
|
|
557
|
+
'tokenEndpoint',
|
|
558
|
+
'userinfoEndpoint',
|
|
559
|
+
'jwksEndpoint',
|
|
560
|
+
'allowedRedirectUris',
|
|
561
|
+
'claimMapping',
|
|
562
|
+
]),
|
|
563
|
+
};
|
|
564
|
+
case 'saml':
|
|
565
|
+
return {
|
|
566
|
+
...base,
|
|
567
|
+
...definedOf(c, [
|
|
568
|
+
'idpMetadataXml',
|
|
569
|
+
'idpEntityId',
|
|
570
|
+
'idpSsoUrl',
|
|
571
|
+
'idpCertificates',
|
|
572
|
+
'spSigningKeyRef',
|
|
573
|
+
'spDecryptionKeyRef',
|
|
574
|
+
'wantAssertionsSigned',
|
|
575
|
+
'attributeMapping',
|
|
576
|
+
]),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/** The named fields of `obj` that are set. */
|
|
581
|
+
function definedOf(obj, keys) {
|
|
582
|
+
const out = {};
|
|
583
|
+
for (const k of keys)
|
|
584
|
+
if (obj[k] !== undefined)
|
|
585
|
+
out[k] = obj[k];
|
|
586
|
+
return out;
|
|
369
587
|
}
|
|
370
588
|
async function parseJsonBody(c) {
|
|
371
589
|
try {
|
|
@@ -396,120 +614,228 @@ async function parseOptionalJsonBody(c) {
|
|
|
396
614
|
};
|
|
397
615
|
}
|
|
398
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Fields that would carry a secret itself rather than a reference to one:
|
|
619
|
+
* refused outright, so a secret sent by mistake is never stored.
|
|
620
|
+
*/
|
|
621
|
+
const PLAINTEXT_SECRET_FIELDS = {
|
|
622
|
+
clientSecret: 'clientSecretRef',
|
|
623
|
+
spSigningKey: 'spSigningKeyRef',
|
|
624
|
+
spDecryptionKey: 'spDecryptionKeyRef',
|
|
625
|
+
privateKey: 'spSigningKeyRef',
|
|
626
|
+
};
|
|
627
|
+
const DOMAIN_RE = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
628
|
+
function invalid(message) {
|
|
629
|
+
return { kind: 'err', error: { code: 'invalid-provider-config', message } };
|
|
630
|
+
}
|
|
631
|
+
/** Reads typed fields off a request body, collecting the first problem. */
|
|
632
|
+
class FieldReader {
|
|
633
|
+
b;
|
|
634
|
+
error;
|
|
635
|
+
constructor(b) {
|
|
636
|
+
this.b = b;
|
|
637
|
+
}
|
|
638
|
+
str(k) {
|
|
639
|
+
const v = this.b[k];
|
|
640
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
641
|
+
this.error ??= invalid(`Field \`${k}\` must be a non-empty string`);
|
|
642
|
+
return '';
|
|
643
|
+
}
|
|
644
|
+
return v;
|
|
645
|
+
}
|
|
646
|
+
optStr(k) {
|
|
647
|
+
const v = this.b[k];
|
|
648
|
+
if (v === undefined)
|
|
649
|
+
return undefined;
|
|
650
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
651
|
+
this.error ??= invalid(`\`${k}\` must be a non-empty string when supplied`);
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
return v;
|
|
655
|
+
}
|
|
656
|
+
url(k, required) {
|
|
657
|
+
const v = required ? this.str(k) : this.optStr(k);
|
|
658
|
+
if (v === undefined || v === '')
|
|
659
|
+
return v;
|
|
660
|
+
try {
|
|
661
|
+
new URL(v);
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
this.error ??= invalid(`\`${k}\` must be an absolute URL`);
|
|
665
|
+
}
|
|
666
|
+
return v;
|
|
667
|
+
}
|
|
668
|
+
strings(k, required) {
|
|
669
|
+
const v = this.b[k];
|
|
670
|
+
if (v === undefined) {
|
|
671
|
+
if (required)
|
|
672
|
+
this.error ??= invalid(`\`${k}\` must be an array of strings`);
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string' || x.length === 0)) {
|
|
676
|
+
this.error ??= invalid(`\`${k}\` must be an array of non-empty strings`);
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
return v;
|
|
680
|
+
}
|
|
681
|
+
bool(k) {
|
|
682
|
+
const v = this.b[k];
|
|
683
|
+
if (v === undefined)
|
|
684
|
+
return undefined;
|
|
685
|
+
if (typeof v !== 'boolean') {
|
|
686
|
+
this.error ??= invalid(`\`${k}\` must be a boolean when supplied`);
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
return v;
|
|
690
|
+
}
|
|
691
|
+
object(k) {
|
|
692
|
+
const v = this.b[k];
|
|
693
|
+
if (v === undefined)
|
|
694
|
+
return undefined;
|
|
695
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
|
|
696
|
+
this.error ??= invalid(`\`${k}\` must be an object when supplied`);
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
return v;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
399
702
|
function parseProviderConfig(body) {
|
|
400
703
|
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
401
704
|
return { kind: 'err', error: { code: 'bad-input', message: 'Request body must be an object' } };
|
|
402
705
|
}
|
|
403
706
|
const b = body;
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return {
|
|
408
|
-
kind: 'err',
|
|
409
|
-
error: {
|
|
410
|
-
code: 'invalid-provider-config',
|
|
411
|
-
message: `Field \`${k}\` must be a non-empty string`,
|
|
412
|
-
},
|
|
413
|
-
};
|
|
707
|
+
for (const [field, instead] of Object.entries(PLAINTEXT_SECRET_FIELDS)) {
|
|
708
|
+
if (b[field] !== undefined) {
|
|
709
|
+
return invalid(`\`${field}\` is never accepted: register \`${instead}\`, a reference the deployment resolves`);
|
|
414
710
|
}
|
|
415
|
-
return v;
|
|
416
|
-
};
|
|
417
|
-
const providerId = requiredStr('providerId');
|
|
418
|
-
if (typeof providerId !== 'string')
|
|
419
|
-
return providerId;
|
|
420
|
-
const clientId = requiredStr('clientId');
|
|
421
|
-
if (typeof clientId !== 'string')
|
|
422
|
-
return clientId;
|
|
423
|
-
const clientSecretRef = requiredStr('clientSecretRef');
|
|
424
|
-
if (typeof clientSecretRef !== 'string')
|
|
425
|
-
return clientSecretRef;
|
|
426
|
-
const authorizationEndpoint = requiredStr('authorizationEndpoint');
|
|
427
|
-
if (typeof authorizationEndpoint !== 'string')
|
|
428
|
-
return authorizationEndpoint;
|
|
429
|
-
const tokenEndpoint = requiredStr('tokenEndpoint');
|
|
430
|
-
if (typeof tokenEndpoint !== 'string')
|
|
431
|
-
return tokenEndpoint;
|
|
432
|
-
const kindRaw = b.kind;
|
|
433
|
-
if (kindRaw !== 'oauth2' && kindRaw !== 'oidc') {
|
|
434
|
-
return {
|
|
435
|
-
kind: 'err',
|
|
436
|
-
error: {
|
|
437
|
-
code: 'invalid-provider-config',
|
|
438
|
-
message: '`kind` must be `oauth2` or `oidc`',
|
|
439
|
-
},
|
|
440
|
-
};
|
|
441
711
|
}
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
return
|
|
445
|
-
kind: 'err',
|
|
446
|
-
error: {
|
|
447
|
-
code: 'invalid-provider-config',
|
|
448
|
-
message: '`scopes` must be an array of strings',
|
|
449
|
-
},
|
|
450
|
-
};
|
|
712
|
+
const kind = b.kind;
|
|
713
|
+
if (kind !== 'oauth2' && kind !== 'oidc' && kind !== 'saml') {
|
|
714
|
+
return invalid('`kind` must be `oidc`, `saml` or `oauth2`');
|
|
451
715
|
}
|
|
452
|
-
const
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
message: '`userinfoEndpoint` must be a string when supplied',
|
|
460
|
-
},
|
|
461
|
-
};
|
|
716
|
+
const r = new FieldReader(b);
|
|
717
|
+
const providerId = r.str('providerId');
|
|
718
|
+
const displayName = r.optStr('displayName');
|
|
719
|
+
const domainsRaw = r.strings('domains', false);
|
|
720
|
+
const domains = domainsRaw?.map((d) => d.toLowerCase());
|
|
721
|
+
if (domains?.some((d) => !DOMAIN_RE.test(d))) {
|
|
722
|
+
r.error ??= invalid('`domains` must be email domains, e.g. `acme.com`');
|
|
462
723
|
}
|
|
463
|
-
const
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
return {
|
|
467
|
-
kind: 'err',
|
|
468
|
-
error: {
|
|
469
|
-
code: 'invalid-provider-config',
|
|
470
|
-
message: '`metadata` must be an object when supplied',
|
|
471
|
-
},
|
|
472
|
-
};
|
|
724
|
+
const join = b.join;
|
|
725
|
+
if (join !== undefined && join !== 'invite' && join !== 'domain') {
|
|
726
|
+
r.error ??= invalid('`join` must be `invite` or `domain`');
|
|
473
727
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (allowedRedirectUrisRaw !== undefined) {
|
|
477
|
-
if (!Array.isArray(allowedRedirectUrisRaw) ||
|
|
478
|
-
allowedRedirectUrisRaw.some((s) => typeof s !== 'string' || s.length === 0)) {
|
|
479
|
-
return {
|
|
480
|
-
kind: 'err',
|
|
481
|
-
error: {
|
|
482
|
-
code: 'invalid-provider-config',
|
|
483
|
-
message: '`allowedRedirectUris` must be an array of non-empty strings when supplied',
|
|
484
|
-
},
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
allowedRedirectUris = allowedRedirectUrisRaw;
|
|
728
|
+
if (join === 'domain' && (domains === undefined || domains.length === 0)) {
|
|
729
|
+
r.error ??= invalid('`join: domain` needs `domains`');
|
|
488
730
|
}
|
|
489
|
-
const
|
|
731
|
+
const metadata = r.object('metadata');
|
|
732
|
+
const base = {
|
|
733
|
+
providerId,
|
|
734
|
+
...(displayName !== undefined && { displayName }),
|
|
735
|
+
...(domains !== undefined && { domains }),
|
|
736
|
+
...(join !== undefined && { join: join }),
|
|
737
|
+
...(metadata !== undefined && { metadata }),
|
|
738
|
+
};
|
|
490
739
|
let claimMapping;
|
|
491
|
-
if (
|
|
492
|
-
const parsed = parseClaimMapping(
|
|
740
|
+
if (kind !== 'saml' && b.claimMapping !== undefined) {
|
|
741
|
+
const parsed = parseClaimMapping(b.claimMapping);
|
|
493
742
|
if (parsed.kind === 'err')
|
|
494
743
|
return parsed;
|
|
495
744
|
claimMapping = parsed.value;
|
|
496
745
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
746
|
+
let value;
|
|
747
|
+
if (kind === 'oauth2') {
|
|
748
|
+
const clientId = r.str('clientId');
|
|
749
|
+
const clientSecretRef = r.str('clientSecretRef');
|
|
750
|
+
const authorizationEndpoint = r.str('authorizationEndpoint');
|
|
751
|
+
const tokenEndpoint = r.str('tokenEndpoint');
|
|
752
|
+
const userinfoEndpoint = r.optStr('userinfoEndpoint');
|
|
753
|
+
const scopes = r.strings('scopes', true) ?? [];
|
|
754
|
+
const allowedRedirectUris = r.strings('allowedRedirectUris', false);
|
|
755
|
+
value = {
|
|
756
|
+
...base,
|
|
757
|
+
kind,
|
|
502
758
|
clientId,
|
|
503
759
|
clientSecretRef,
|
|
504
760
|
authorizationEndpoint,
|
|
505
761
|
tokenEndpoint,
|
|
506
|
-
...(typeof userinfoEndpointRaw === 'string' && { userinfoEndpoint: userinfoEndpointRaw }),
|
|
507
762
|
scopes,
|
|
763
|
+
...(userinfoEndpoint !== undefined && { userinfoEndpoint }),
|
|
508
764
|
...(allowedRedirectUris !== undefined && { allowedRedirectUris }),
|
|
509
765
|
...(claimMapping !== undefined && { claimMapping }),
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
else if (kind === 'oidc') {
|
|
769
|
+
const issuer = r.url('issuer', true) ?? '';
|
|
770
|
+
const clientId = r.str('clientId');
|
|
771
|
+
const clientSecretRef = r.str('clientSecretRef');
|
|
772
|
+
const scopes = r.strings('scopes', false);
|
|
773
|
+
const authorizationEndpoint = r.url('authorizationEndpoint', false);
|
|
774
|
+
const tokenEndpoint = r.url('tokenEndpoint', false);
|
|
775
|
+
const userinfoEndpoint = r.url('userinfoEndpoint', false);
|
|
776
|
+
const jwksEndpoint = r.url('jwksEndpoint', false);
|
|
777
|
+
const allowedRedirectUris = r.strings('allowedRedirectUris', false);
|
|
778
|
+
value = {
|
|
779
|
+
...base,
|
|
780
|
+
kind,
|
|
781
|
+
issuer,
|
|
782
|
+
clientId,
|
|
783
|
+
clientSecretRef,
|
|
784
|
+
...(scopes !== undefined && { scopes }),
|
|
785
|
+
...(authorizationEndpoint !== undefined && { authorizationEndpoint }),
|
|
786
|
+
...(tokenEndpoint !== undefined && { tokenEndpoint }),
|
|
787
|
+
...(userinfoEndpoint !== undefined && { userinfoEndpoint }),
|
|
788
|
+
...(jwksEndpoint !== undefined && { jwksEndpoint }),
|
|
789
|
+
...(allowedRedirectUris !== undefined && { allowedRedirectUris }),
|
|
790
|
+
...(claimMapping !== undefined && { claimMapping }),
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
const idpMetadataXml = r.optStr('idpMetadataXml');
|
|
795
|
+
const idpEntityId = r.optStr('idpEntityId');
|
|
796
|
+
const idpSsoUrl = r.url('idpSsoUrl', false);
|
|
797
|
+
const idpCertificates = r.strings('idpCertificates', false);
|
|
798
|
+
if (idpMetadataXml === undefined &&
|
|
799
|
+
(idpEntityId === undefined ||
|
|
800
|
+
idpSsoUrl === undefined ||
|
|
801
|
+
idpCertificates === undefined ||
|
|
802
|
+
idpCertificates.length === 0)) {
|
|
803
|
+
r.error ??= invalid('A SAML provider needs `idpMetadataXml`, or `idpEntityId` + `idpSsoUrl` + `idpCertificates`');
|
|
804
|
+
}
|
|
805
|
+
const spSigningKeyRef = r.optStr('spSigningKeyRef');
|
|
806
|
+
const spDecryptionKeyRef = r.optStr('spDecryptionKeyRef');
|
|
807
|
+
const wantAssertionsSigned = r.bool('wantAssertionsSigned');
|
|
808
|
+
const mappingRaw = r.object('attributeMapping');
|
|
809
|
+
let attributeMapping;
|
|
810
|
+
if (mappingRaw !== undefined) {
|
|
811
|
+
const m = new FieldReader(mappingRaw);
|
|
812
|
+
const userId = m.optStr('userId');
|
|
813
|
+
const email = m.optStr('email');
|
|
814
|
+
const displayName = m.optStr('displayName');
|
|
815
|
+
if (m.error !== undefined)
|
|
816
|
+
r.error ??= invalid(`\`attributeMapping\`: ${m.error.error.message}`);
|
|
817
|
+
attributeMapping = {
|
|
818
|
+
...(userId !== undefined && { userId }),
|
|
819
|
+
...(email !== undefined && { email }),
|
|
820
|
+
...(displayName !== undefined && { displayName }),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
value = {
|
|
824
|
+
...base,
|
|
825
|
+
kind,
|
|
826
|
+
...(idpMetadataXml !== undefined && { idpMetadataXml }),
|
|
827
|
+
...(idpEntityId !== undefined && { idpEntityId }),
|
|
828
|
+
...(idpSsoUrl !== undefined && { idpSsoUrl }),
|
|
829
|
+
...(idpCertificates !== undefined && { idpCertificates }),
|
|
830
|
+
...(spSigningKeyRef !== undefined && { spSigningKeyRef }),
|
|
831
|
+
...(spDecryptionKeyRef !== undefined && { spDecryptionKeyRef }),
|
|
832
|
+
...(wantAssertionsSigned !== undefined && { wantAssertionsSigned }),
|
|
833
|
+
...(attributeMapping !== undefined && { attributeMapping }),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
if (r.error !== undefined)
|
|
837
|
+
return r.error;
|
|
838
|
+
return { kind: 'ok', value };
|
|
513
839
|
}
|
|
514
840
|
function parseClaimMapping(raw) {
|
|
515
841
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
@@ -627,4 +953,52 @@ function parseCallbackBody(body) {
|
|
|
627
953
|
}
|
|
628
954
|
return { kind: 'ok', value: { code, state } };
|
|
629
955
|
}
|
|
956
|
+
/**
|
|
957
|
+
* `POST /v1/auth/logout`: revokes the caller's session; a browser session
|
|
958
|
+
* loses its cookie too. Also mounted on its own with cookie sessions and
|
|
959
|
+
* no identity providers (console token sign-in).
|
|
960
|
+
*/
|
|
961
|
+
export function logoutHandler(sessionStore, auditEvents) {
|
|
962
|
+
return async (c) => {
|
|
963
|
+
const requestId = c.get('requestId');
|
|
964
|
+
const tenantId = c.get('tenantId');
|
|
965
|
+
const sessionId = c.get('sessionId');
|
|
966
|
+
if (sessionId === undefined) {
|
|
967
|
+
c.status(statusFor('auth-not-session-token'));
|
|
968
|
+
return c.json(toWireError({
|
|
969
|
+
code: 'auth-not-session-token',
|
|
970
|
+
message: 'Logout is only valid for session tokens (`kgi_sk_*`); bearer tokens are managed via `/v1/tokens`',
|
|
971
|
+
}, requestId));
|
|
972
|
+
}
|
|
973
|
+
const outcome = await sessionStore.revoke({ tenantId, sessionId });
|
|
974
|
+
const userId = c.get('userId');
|
|
975
|
+
if (auditEvents !== undefined && outcome.revoked) {
|
|
976
|
+
try {
|
|
977
|
+
const appended = await auditEvents.append([
|
|
978
|
+
{
|
|
979
|
+
id: randomUUID(),
|
|
980
|
+
tenantId,
|
|
981
|
+
kind: 'signed-out',
|
|
982
|
+
timestamp: new Date().toISOString(),
|
|
983
|
+
actor: userId !== undefined ? `user:${userId}` : 'system',
|
|
984
|
+
outcome: 'succeeded',
|
|
985
|
+
payload: { v: 1, doc: { sessionId } },
|
|
986
|
+
},
|
|
987
|
+
]);
|
|
988
|
+
if (appended.kind === 'err') {
|
|
989
|
+
c.get('log').warn(`signed-out audit event failed: ${appended.error.message}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
catch (cause) {
|
|
993
|
+
c.get('log').warn(`signed-out audit event failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const cookieName = c.get('sessionCookieName');
|
|
997
|
+
if (cookieName !== undefined) {
|
|
998
|
+
// A browser session: the cookie goes with it.
|
|
999
|
+
c.header('Set-Cookie', `${cookieName}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`);
|
|
1000
|
+
}
|
|
1001
|
+
return c.json({ sessionId, revoked: outcome.revoked });
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
630
1004
|
//# sourceMappingURL=auth.js.map
|