ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__py3-none-any.whl
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.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Base models for authentication across all providers."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, computed_field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseTokenInfo(BaseModel):
|
|
10
|
+
"""Base model for token information across all providers.
|
|
11
|
+
|
|
12
|
+
This abstract base provides a common interface for token operations
|
|
13
|
+
while allowing each provider to maintain its specific implementation.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@computed_field
|
|
17
|
+
def access_token_value(self) -> str:
|
|
18
|
+
"""Get the actual access token string.
|
|
19
|
+
Must be implemented by provider-specific subclasses.
|
|
20
|
+
"""
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
@computed_field
|
|
24
|
+
def is_expired(self) -> bool:
|
|
25
|
+
"""Check if token is expired.
|
|
26
|
+
Uses the expires_at_datetime property for comparison.
|
|
27
|
+
"""
|
|
28
|
+
now = datetime.now(UTC)
|
|
29
|
+
return now >= self.expires_at_datetime
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def expires_at_datetime(self) -> datetime:
|
|
33
|
+
"""Get expiration as datetime object.
|
|
34
|
+
Must be implemented by provider-specific subclasses.
|
|
35
|
+
"""
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def refresh_token_value(self) -> str | None:
|
|
40
|
+
"""Get refresh token if available.
|
|
41
|
+
Default returns None, override if provider supports refresh.
|
|
42
|
+
"""
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BaseProfileInfo(BaseModel):
|
|
47
|
+
"""Base model for user profile information across all providers.
|
|
48
|
+
|
|
49
|
+
Provides common fields with a flexible extras dict for
|
|
50
|
+
provider-specific data.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
account_id: str
|
|
54
|
+
provider_type: str
|
|
55
|
+
|
|
56
|
+
# Common fields with sensible defaults
|
|
57
|
+
email: str = ""
|
|
58
|
+
display_name: str | None = None
|
|
59
|
+
|
|
60
|
+
# All provider-specific data stored here
|
|
61
|
+
# This preserves all information for future use
|
|
62
|
+
extras: dict[str, Any] = Field(
|
|
63
|
+
default_factory=dict,
|
|
64
|
+
description="Provider-specific data (JWT claims, API responses, etc.)",
|
|
65
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Base credentials protocol for all authentication implementations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class BaseCredentials(Protocol):
|
|
8
|
+
"""Protocol that all credential implementations must follow.
|
|
9
|
+
|
|
10
|
+
This defines the contract for credentials without depending on
|
|
11
|
+
any specific provider implementation.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def is_expired(self) -> bool:
|
|
15
|
+
"""Check if the credentials are expired.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
True if expired, False otherwise
|
|
19
|
+
"""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict[str, Any]:
|
|
23
|
+
"""Convert credentials to dictionary for storage.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Dictionary representation of credentials
|
|
27
|
+
"""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_dict(cls, data: dict[str, Any]) -> "BaseCredentials":
|
|
32
|
+
"""Create credentials from dictionary.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
data: Dictionary containing credential data
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Credentials instance
|
|
39
|
+
"""
|
|
40
|
+
...
|
ccproxy/auth/oauth/__init__.py
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Public router shim for OAuth flows."""
|
|
2
2
|
|
|
3
|
-
from ccproxy.auth.oauth.
|
|
4
|
-
|
|
5
|
-
OAuthState,
|
|
6
|
-
OAuthTokenRequest,
|
|
7
|
-
OAuthTokenResponse,
|
|
8
|
-
)
|
|
9
|
-
from ccproxy.auth.oauth.routes import (
|
|
10
|
-
get_oauth_flow_result,
|
|
11
|
-
register_oauth_flow,
|
|
12
|
-
router,
|
|
13
|
-
)
|
|
3
|
+
from ccproxy.auth.oauth.registry import OAuthProviderProtocol
|
|
4
|
+
from ccproxy.auth.oauth.routes import get_oauth_flow_result, register_oauth_flow, router
|
|
14
5
|
|
|
15
6
|
|
|
16
7
|
__all__ = [
|
|
17
|
-
# Router
|
|
18
8
|
"router",
|
|
19
9
|
"register_oauth_flow",
|
|
20
10
|
"get_oauth_flow_result",
|
|
21
|
-
|
|
22
|
-
"OAuthState",
|
|
23
|
-
"OAuthCallbackRequest",
|
|
24
|
-
"OAuthTokenRequest",
|
|
25
|
-
"OAuthTokenResponse",
|
|
11
|
+
"OAuthProviderProtocol",
|
|
26
12
|
]
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""Base OAuth client with common PKCE flow implementation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import secrets
|
|
7
|
+
import urllib.parse
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from datetime import UTC, datetime, timedelta
|
|
10
|
+
from typing import Any, Generic, TypeVar
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from ccproxy.auth.exceptions import (
|
|
15
|
+
OAuthError,
|
|
16
|
+
OAuthTokenRefreshError,
|
|
17
|
+
)
|
|
18
|
+
from ccproxy.auth.models.credentials import BaseCredentials
|
|
19
|
+
from ccproxy.auth.storage.base import TokenStorage
|
|
20
|
+
from ccproxy.config.settings import Settings
|
|
21
|
+
from ccproxy.core.logging import get_logger
|
|
22
|
+
from ccproxy.http.client import HTTPClientFactory
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
CredentialsT = TypeVar("CredentialsT", bound=BaseCredentials)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BaseOAuthClient(ABC, Generic[CredentialsT]):
|
|
31
|
+
"""Abstract base class for OAuth PKCE flow implementations."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
client_id: str,
|
|
36
|
+
redirect_uri: str,
|
|
37
|
+
base_url: str,
|
|
38
|
+
scopes: list[str],
|
|
39
|
+
storage: TokenStorage[CredentialsT] | None = None,
|
|
40
|
+
http_client: httpx.AsyncClient | None = None,
|
|
41
|
+
hook_manager: Any | None = None,
|
|
42
|
+
settings: Settings | None = None,
|
|
43
|
+
):
|
|
44
|
+
"""Initialize OAuth client with common parameters.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
client_id: OAuth client ID
|
|
48
|
+
redirect_uri: OAuth callback redirect URI
|
|
49
|
+
base_url: OAuth provider base URL
|
|
50
|
+
scopes: List of OAuth scopes to request
|
|
51
|
+
storage: Optional token storage backend
|
|
52
|
+
http_client: Optional HTTP client (for request tracing support)
|
|
53
|
+
hook_manager: Optional hook manager for emitting events
|
|
54
|
+
settings: Optional settings for HTTP client configuration
|
|
55
|
+
"""
|
|
56
|
+
self.client_id = client_id
|
|
57
|
+
self.redirect_uri = redirect_uri
|
|
58
|
+
self.base_url = base_url
|
|
59
|
+
self.scopes = scopes
|
|
60
|
+
self.storage = storage
|
|
61
|
+
self.hook_manager = hook_manager
|
|
62
|
+
|
|
63
|
+
# Always have an HTTP client
|
|
64
|
+
if http_client:
|
|
65
|
+
self.http_client = http_client
|
|
66
|
+
self._owns_http_client = False # Don't close provided client
|
|
67
|
+
logger.debug(
|
|
68
|
+
"oauth_client_using_provided_http_client",
|
|
69
|
+
http_client_id=id(http_client),
|
|
70
|
+
has_hooks=hasattr(http_client, "hook_manager")
|
|
71
|
+
and http_client.hook_manager is not None,
|
|
72
|
+
hook_manager_id=id(hook_manager) if hook_manager else None,
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
# Create client with hook support if hook_manager is provided
|
|
76
|
+
self.http_client = HTTPClientFactory.create_client(
|
|
77
|
+
settings=settings,
|
|
78
|
+
timeout_connect=10.0,
|
|
79
|
+
timeout_read=30.0,
|
|
80
|
+
http2=True,
|
|
81
|
+
hook_manager=hook_manager, # Pass hook manager to client
|
|
82
|
+
)
|
|
83
|
+
self._owns_http_client = True # We own it, close on cleanup
|
|
84
|
+
logger.debug(
|
|
85
|
+
"oauth_client_created_new_http_client",
|
|
86
|
+
http_client_id=id(self.http_client),
|
|
87
|
+
has_hooks=hasattr(self.http_client, "hook_manager")
|
|
88
|
+
and self.http_client.hook_manager is not None,
|
|
89
|
+
hook_manager_id=id(hook_manager) if hook_manager else None,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self._callback_server: asyncio.Task[None] | None = None
|
|
93
|
+
self._auth_complete = asyncio.Event()
|
|
94
|
+
self._auth_result: Any | None = None
|
|
95
|
+
self._auth_error: str | None = None
|
|
96
|
+
|
|
97
|
+
async def close(self) -> None:
|
|
98
|
+
"""Close resources if we own them."""
|
|
99
|
+
if self._owns_http_client and self.http_client:
|
|
100
|
+
await self.http_client.aclose()
|
|
101
|
+
|
|
102
|
+
def __del__(self) -> None:
|
|
103
|
+
"""Cleanup on deletion."""
|
|
104
|
+
if (
|
|
105
|
+
self._owns_http_client
|
|
106
|
+
and self.http_client
|
|
107
|
+
and not self.http_client.is_closed
|
|
108
|
+
):
|
|
109
|
+
try:
|
|
110
|
+
# Try to get the current event loop
|
|
111
|
+
loop = asyncio.get_running_loop()
|
|
112
|
+
loop.create_task(self.http_client.aclose())
|
|
113
|
+
except RuntimeError:
|
|
114
|
+
# No running event loop, can't clean up async resources
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
def _generate_pkce_pair(self) -> tuple[str, str]:
|
|
118
|
+
"""Generate PKCE code verifier and challenge.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Tuple of (code_verifier, code_challenge)
|
|
122
|
+
"""
|
|
123
|
+
# Generate code verifier (43-128 characters, URL-safe)
|
|
124
|
+
code_verifier = (
|
|
125
|
+
base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip("=")
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Generate code challenge using SHA256
|
|
129
|
+
challenge_bytes = hashlib.sha256(code_verifier.encode()).digest()
|
|
130
|
+
code_challenge = base64.urlsafe_b64encode(challenge_bytes).decode().rstrip("=")
|
|
131
|
+
|
|
132
|
+
logger.debug(
|
|
133
|
+
"pkce_pair_generated",
|
|
134
|
+
verifier_length=len(code_verifier),
|
|
135
|
+
challenge_length=len(code_challenge),
|
|
136
|
+
category="auth",
|
|
137
|
+
)
|
|
138
|
+
return code_verifier, code_challenge
|
|
139
|
+
|
|
140
|
+
def _generate_state(self) -> str:
|
|
141
|
+
"""Generate secure random state parameter.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
URL-safe random state string
|
|
145
|
+
"""
|
|
146
|
+
return secrets.token_urlsafe(32)
|
|
147
|
+
|
|
148
|
+
def _build_auth_url(self, code_challenge: str, state: str) -> str:
|
|
149
|
+
"""Build OAuth authorization URL with PKCE parameters.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
code_challenge: PKCE code challenge
|
|
153
|
+
state: Random state parameter
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Complete authorization URL
|
|
157
|
+
"""
|
|
158
|
+
params = self._get_auth_params(code_challenge, state)
|
|
159
|
+
query_string = urllib.parse.urlencode(params)
|
|
160
|
+
auth_endpoint = self._get_auth_endpoint()
|
|
161
|
+
return f"{auth_endpoint}?{query_string}"
|
|
162
|
+
|
|
163
|
+
def _get_auth_params(self, code_challenge: str, state: str) -> dict[str, str]:
|
|
164
|
+
"""Get authorization URL parameters.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
code_challenge: PKCE code challenge
|
|
168
|
+
state: Random state parameter
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Dictionary of URL parameters
|
|
172
|
+
"""
|
|
173
|
+
base_params = {
|
|
174
|
+
"response_type": "code",
|
|
175
|
+
"client_id": self.client_id,
|
|
176
|
+
"redirect_uri": self.redirect_uri,
|
|
177
|
+
"scope": " ".join(self.scopes),
|
|
178
|
+
"state": state,
|
|
179
|
+
"code_challenge": code_challenge,
|
|
180
|
+
"code_challenge_method": "S256",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Allow providers to add custom parameters
|
|
184
|
+
custom_params = self.get_custom_auth_params()
|
|
185
|
+
base_params.update(custom_params)
|
|
186
|
+
|
|
187
|
+
return base_params
|
|
188
|
+
|
|
189
|
+
async def _exchange_code_for_tokens(
|
|
190
|
+
self, code: str, code_verifier: str, state: str | None = None
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
"""Exchange authorization code for tokens.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
code: Authorization code from OAuth callback
|
|
196
|
+
code_verifier: PKCE code verifier
|
|
197
|
+
state: OAuth state parameter
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Token response dictionary from provider
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
OAuthTokenRefreshError: If token exchange fails
|
|
204
|
+
"""
|
|
205
|
+
token_endpoint = self._get_token_endpoint()
|
|
206
|
+
token_data = self._get_token_exchange_data(code, code_verifier, state)
|
|
207
|
+
headers = self._get_token_exchange_headers()
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
logger.debug(
|
|
211
|
+
"token_exchange_start",
|
|
212
|
+
endpoint=token_endpoint,
|
|
213
|
+
has_code=bool(code),
|
|
214
|
+
has_verifier=bool(code_verifier),
|
|
215
|
+
category="auth",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# No need for OAuth-specific hooks here - generic HTTP hooks will capture everything
|
|
219
|
+
|
|
220
|
+
# Just use self.http_client - it always exists!
|
|
221
|
+
response = await self.http_client.post(
|
|
222
|
+
token_endpoint,
|
|
223
|
+
data=token_data if not self._use_json_for_token_exchange() else None,
|
|
224
|
+
json=token_data if self._use_json_for_token_exchange() else None,
|
|
225
|
+
headers=headers,
|
|
226
|
+
timeout=30.0,
|
|
227
|
+
)
|
|
228
|
+
response.raise_for_status()
|
|
229
|
+
|
|
230
|
+
token_response = response.json()
|
|
231
|
+
|
|
232
|
+
# No need for OAuth-specific hooks here - generic HTTP hooks will capture everything
|
|
233
|
+
logger.debug(
|
|
234
|
+
"token_exchange_success",
|
|
235
|
+
has_access_token="access_token" in token_response,
|
|
236
|
+
has_refresh_token="refresh_token" in token_response,
|
|
237
|
+
expires_in=token_response.get("expires_in"),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
from typing import cast
|
|
241
|
+
|
|
242
|
+
return cast(dict[str, Any], token_response)
|
|
243
|
+
|
|
244
|
+
except httpx.HTTPStatusError as e:
|
|
245
|
+
error_detail = self._extract_error_detail(e.response)
|
|
246
|
+
logger.error(
|
|
247
|
+
"token_exchange_http_error",
|
|
248
|
+
status_code=e.response.status_code,
|
|
249
|
+
error_detail=error_detail,
|
|
250
|
+
exc_info=e,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# No need for OAuth-specific hooks here - generic HTTP hooks will capture everything
|
|
254
|
+
|
|
255
|
+
raise OAuthTokenRefreshError(
|
|
256
|
+
f"Token exchange failed: {error_detail}"
|
|
257
|
+
) from e
|
|
258
|
+
|
|
259
|
+
except httpx.TimeoutException as e:
|
|
260
|
+
logger.error(
|
|
261
|
+
"token_exchange_timeout", error=str(e), exc_info=e, category="auth"
|
|
262
|
+
)
|
|
263
|
+
raise OAuthTokenRefreshError("Token exchange timed out") from e
|
|
264
|
+
|
|
265
|
+
except httpx.HTTPError as e:
|
|
266
|
+
logger.error(
|
|
267
|
+
"token_exchange_http_error",
|
|
268
|
+
error=str(e),
|
|
269
|
+
exc_info=e,
|
|
270
|
+
category="auth",
|
|
271
|
+
)
|
|
272
|
+
raise OAuthTokenRefreshError(
|
|
273
|
+
f"HTTP error during token exchange: {e}"
|
|
274
|
+
) from e
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.error("token_exchange_unexpected_error", error=str(e), exc_info=e)
|
|
278
|
+
raise OAuthTokenRefreshError(
|
|
279
|
+
f"Unexpected error during token exchange: {e}"
|
|
280
|
+
) from e
|
|
281
|
+
|
|
282
|
+
def _get_token_exchange_data(
|
|
283
|
+
self, code: str, code_verifier: str, state: str | None = None
|
|
284
|
+
) -> dict[str, str]:
|
|
285
|
+
"""Get token exchange request data.
|
|
286
|
+
|
|
287
|
+
Note: RFC 6749 Section 4.1.3 specifies that the state parameter should
|
|
288
|
+
NOT be included in token exchange requests. However, some providers
|
|
289
|
+
(like Claude) have non-standard implementations that require it.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
code: Authorization code
|
|
293
|
+
code_verifier: PKCE code verifier
|
|
294
|
+
state: OAuth state parameter
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Dictionary of token exchange parameters
|
|
298
|
+
"""
|
|
299
|
+
base_data = {
|
|
300
|
+
"grant_type": "authorization_code",
|
|
301
|
+
"code": code,
|
|
302
|
+
"redirect_uri": self.redirect_uri,
|
|
303
|
+
"client_id": self.client_id,
|
|
304
|
+
"code_verifier": code_verifier,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# RFC 6749 compliant: state parameter should be excluded
|
|
308
|
+
# Override in provider-specific clients if needed (e.g., Claude)
|
|
309
|
+
|
|
310
|
+
# Allow providers to add custom parameters
|
|
311
|
+
custom_data = self.get_custom_token_params()
|
|
312
|
+
base_data.update(custom_data)
|
|
313
|
+
|
|
314
|
+
return base_data
|
|
315
|
+
|
|
316
|
+
def _get_token_exchange_headers(self) -> dict[str, str]:
|
|
317
|
+
"""Get headers for token exchange request.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Dictionary of HTTP headers
|
|
321
|
+
"""
|
|
322
|
+
base_headers = {
|
|
323
|
+
"Accept": "application/json",
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Use form encoding by default, unless provider uses JSON
|
|
327
|
+
if not self._use_json_for_token_exchange():
|
|
328
|
+
base_headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
329
|
+
else:
|
|
330
|
+
base_headers["Content-Type"] = "application/json"
|
|
331
|
+
|
|
332
|
+
# Allow providers to add custom headers
|
|
333
|
+
custom_headers = self.get_custom_headers()
|
|
334
|
+
base_headers.update(custom_headers)
|
|
335
|
+
|
|
336
|
+
return base_headers
|
|
337
|
+
|
|
338
|
+
def _extract_error_detail(self, response: httpx.Response) -> str:
|
|
339
|
+
"""Extract error detail from HTTP response.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
response: HTTP response object
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Human-readable error detail
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
error_data = response.json()
|
|
349
|
+
return str(
|
|
350
|
+
error_data.get(
|
|
351
|
+
"error_description", error_data.get("error", str(response.text))
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
except Exception:
|
|
355
|
+
return response.text[:200] if len(response.text) > 200 else response.text
|
|
356
|
+
|
|
357
|
+
def _calculate_expiration(self, expires_in: int | None) -> datetime:
|
|
358
|
+
"""Calculate token expiration timestamp.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
expires_in: Seconds until token expires (None defaults to 1 hour)
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Expiration datetime in UTC
|
|
365
|
+
"""
|
|
366
|
+
expires_in = expires_in or 3600 # Default to 1 hour
|
|
367
|
+
return datetime.now(UTC).replace(microsecond=0) + timedelta(seconds=expires_in)
|
|
368
|
+
|
|
369
|
+
# ==================== Abstract Methods ====================
|
|
370
|
+
|
|
371
|
+
@abstractmethod
|
|
372
|
+
async def parse_token_response(self, data: dict[str, Any]) -> CredentialsT:
|
|
373
|
+
"""Parse provider-specific token response into credentials.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
data: Raw token response from provider
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Provider-specific credentials object
|
|
380
|
+
"""
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
@abstractmethod
|
|
384
|
+
def _get_auth_endpoint(self) -> str:
|
|
385
|
+
"""Get OAuth authorization endpoint URL.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Full authorization endpoint URL
|
|
389
|
+
"""
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
@abstractmethod
|
|
393
|
+
def _get_token_endpoint(self) -> str:
|
|
394
|
+
"""Get OAuth token exchange endpoint URL.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Full token endpoint URL
|
|
398
|
+
"""
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
# ==================== Optional Override Methods ====================
|
|
402
|
+
|
|
403
|
+
def get_custom_auth_params(self) -> dict[str, str]:
|
|
404
|
+
"""Get provider-specific authorization parameters.
|
|
405
|
+
|
|
406
|
+
Override this to add custom parameters to auth URL.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Dictionary of custom parameters (empty by default)
|
|
410
|
+
"""
|
|
411
|
+
return {}
|
|
412
|
+
|
|
413
|
+
def get_custom_token_params(self) -> dict[str, str]:
|
|
414
|
+
"""Get provider-specific token exchange parameters.
|
|
415
|
+
|
|
416
|
+
Override this to add custom parameters to token request.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Dictionary of custom parameters (empty by default)
|
|
420
|
+
"""
|
|
421
|
+
return {}
|
|
422
|
+
|
|
423
|
+
def get_custom_headers(self) -> dict[str, str]:
|
|
424
|
+
"""Get provider-specific HTTP headers.
|
|
425
|
+
|
|
426
|
+
Override this to add custom headers to requests.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Dictionary of custom headers (empty by default)
|
|
430
|
+
"""
|
|
431
|
+
return {}
|
|
432
|
+
|
|
433
|
+
def _use_json_for_token_exchange(self) -> bool:
|
|
434
|
+
"""Whether to use JSON instead of form encoding for token exchange.
|
|
435
|
+
|
|
436
|
+
Override this if provider requires JSON body.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
False by default (uses form encoding)
|
|
440
|
+
"""
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
# ==================== Public Methods ====================
|
|
444
|
+
|
|
445
|
+
async def authenticate(
|
|
446
|
+
self, code_verifier: str | None = None, state: str | None = None
|
|
447
|
+
) -> tuple[str, str, str]:
|
|
448
|
+
"""Start OAuth authentication flow.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
code_verifier: Optional pre-generated PKCE verifier
|
|
452
|
+
state: Optional pre-generated state parameter
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Tuple of (auth_url, code_verifier, state)
|
|
456
|
+
"""
|
|
457
|
+
# Generate PKCE parameters if not provided
|
|
458
|
+
if not code_verifier:
|
|
459
|
+
code_verifier, code_challenge = self._generate_pkce_pair()
|
|
460
|
+
else:
|
|
461
|
+
# Calculate challenge from provided verifier
|
|
462
|
+
challenge_bytes = hashlib.sha256(code_verifier.encode()).digest()
|
|
463
|
+
code_challenge = (
|
|
464
|
+
base64.urlsafe_b64encode(challenge_bytes).decode().rstrip("=")
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Generate state if not provided
|
|
468
|
+
if not state:
|
|
469
|
+
state = self._generate_state()
|
|
470
|
+
|
|
471
|
+
# Build authorization URL
|
|
472
|
+
auth_url = self._build_auth_url(code_challenge, state)
|
|
473
|
+
|
|
474
|
+
logger.info(
|
|
475
|
+
"oauth_flow_started",
|
|
476
|
+
provider=self.__class__.__name__,
|
|
477
|
+
has_storage=bool(self.storage),
|
|
478
|
+
scopes=self.scopes,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
return auth_url, code_verifier, state
|
|
482
|
+
|
|
483
|
+
async def handle_callback(
|
|
484
|
+
self, code: str, state: str, code_verifier: str
|
|
485
|
+
) -> CredentialsT:
|
|
486
|
+
"""Handle OAuth callback and exchange code for tokens.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
code: Authorization code from callback
|
|
490
|
+
state: State parameter from callback
|
|
491
|
+
code_verifier: PKCE code verifier
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Provider-specific credentials object
|
|
495
|
+
|
|
496
|
+
Raises:
|
|
497
|
+
OAuthError: If callback handling fails
|
|
498
|
+
"""
|
|
499
|
+
try:
|
|
500
|
+
# Exchange code for tokens
|
|
501
|
+
token_response = await self._exchange_code_for_tokens(
|
|
502
|
+
code, code_verifier, state
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Parse provider-specific response
|
|
506
|
+
credentials: CredentialsT = await self.parse_token_response(token_response)
|
|
507
|
+
|
|
508
|
+
# Save to storage if available
|
|
509
|
+
if self.storage:
|
|
510
|
+
success = await self.storage.save(credentials)
|
|
511
|
+
if not success:
|
|
512
|
+
logger.warning(
|
|
513
|
+
"credentials_save_failed", provider=self.__class__.__name__
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
logger.info(
|
|
517
|
+
"oauth_callback_success",
|
|
518
|
+
provider=self.__class__.__name__,
|
|
519
|
+
has_refresh_token=bool(token_response.get("refresh_token")),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
return credentials
|
|
523
|
+
|
|
524
|
+
except OAuthTokenRefreshError:
|
|
525
|
+
raise
|
|
526
|
+
except Exception as e:
|
|
527
|
+
logger.error(
|
|
528
|
+
"oauth_callback_error",
|
|
529
|
+
provider=self.__class__.__name__,
|
|
530
|
+
error=str(e),
|
|
531
|
+
exc_info=e,
|
|
532
|
+
)
|
|
533
|
+
raise OAuthError(f"OAuth callback failed: {e}") from e
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Error taxonomy for CLI authentication flows."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthError(Exception):
|
|
5
|
+
"""Base class for authentication errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthTimedOutError(AuthError):
|
|
11
|
+
"""Authentication process timed out."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthUserAbortedError(AuthError):
|
|
17
|
+
"""User cancelled authentication."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthProviderError(AuthError):
|
|
23
|
+
"""Provider-specific authentication error."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NetworkError(AuthError):
|
|
29
|
+
"""Network connectivity error."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PortBindError(AuthError):
|
|
35
|
+
"""Failed to bind to required port."""
|
|
36
|
+
|
|
37
|
+
pass
|