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,430 @@
|
|
|
1
|
+
"""OAuth flow engines for CLI authentication."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import secrets
|
|
6
|
+
import sys
|
|
7
|
+
import webbrowser
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from ccproxy.auth.oauth.cli_errors import AuthProviderError, PortBindError
|
|
15
|
+
from ccproxy.auth.oauth.registry import OAuthProviderProtocol
|
|
16
|
+
from ccproxy.core.logging import get_logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CLICallbackServer:
|
|
24
|
+
"""Temporary HTTP server for handling OAuth callbacks in CLI flows."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, port: int, callback_path: str = "/callback") -> None:
|
|
27
|
+
"""Initialize the callback server.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
port: Port to bind the server to
|
|
31
|
+
callback_path: Path to handle OAuth callbacks
|
|
32
|
+
"""
|
|
33
|
+
self.port = port
|
|
34
|
+
self.callback_path = callback_path
|
|
35
|
+
self.server: Any = None
|
|
36
|
+
self._server_task: asyncio.Task[Any] | None = None
|
|
37
|
+
self.callback_received = False
|
|
38
|
+
self.callback_data: dict[str, Any] = {}
|
|
39
|
+
self.callback_future: asyncio.Future[dict[str, Any]] | None = None
|
|
40
|
+
|
|
41
|
+
async def start(self) -> None:
|
|
42
|
+
"""Start the callback server."""
|
|
43
|
+
import uvicorn
|
|
44
|
+
|
|
45
|
+
# Create minimal ASGI app
|
|
46
|
+
async def app(scope: dict[str, Any], receive: Any, send: Any) -> None:
|
|
47
|
+
if scope["type"] == "http" and scope["path"] == self.callback_path:
|
|
48
|
+
await self._handle_callback(scope, receive, send)
|
|
49
|
+
else:
|
|
50
|
+
# 404 for other paths
|
|
51
|
+
await send(
|
|
52
|
+
{
|
|
53
|
+
"type": "http.response.start",
|
|
54
|
+
"status": 404,
|
|
55
|
+
"headers": [[b"content-type", b"text/plain"]],
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
await send(
|
|
59
|
+
{
|
|
60
|
+
"type": "http.response.body",
|
|
61
|
+
"body": b"Not Found",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Create server config
|
|
66
|
+
config = uvicorn.Config(
|
|
67
|
+
app=app,
|
|
68
|
+
host="localhost",
|
|
69
|
+
port=self.port,
|
|
70
|
+
log_level="error", # Suppress uvicorn logs
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Create and start server
|
|
74
|
+
self.server = uvicorn.Server(config)
|
|
75
|
+
|
|
76
|
+
# Start server in background task with error handling
|
|
77
|
+
async def _serve_with_error_handling() -> None:
|
|
78
|
+
try:
|
|
79
|
+
await self.server.serve()
|
|
80
|
+
except (OSError, SystemExit) as e:
|
|
81
|
+
# Uvicorn calls sys.exit(1) on startup errors, convert to PortBindError
|
|
82
|
+
if isinstance(e, SystemExit):
|
|
83
|
+
raise PortBindError(
|
|
84
|
+
f"Failed to start callback server on port {self.port}"
|
|
85
|
+
) from e
|
|
86
|
+
elif e.errno == 48: # Address already in use
|
|
87
|
+
raise PortBindError(
|
|
88
|
+
f"Port {self.port} is already in use. Please close other applications using this port."
|
|
89
|
+
) from e
|
|
90
|
+
else:
|
|
91
|
+
raise PortBindError(
|
|
92
|
+
f"Failed to start callback server on port {self.port}: {e}"
|
|
93
|
+
) from e
|
|
94
|
+
|
|
95
|
+
self._server_task = asyncio.create_task(_serve_with_error_handling())
|
|
96
|
+
|
|
97
|
+
# Wait briefly and check if server started successfully
|
|
98
|
+
await asyncio.sleep(0.1)
|
|
99
|
+
if self._server_task.done():
|
|
100
|
+
# Server failed to start, re-raise the exception
|
|
101
|
+
await self._server_task
|
|
102
|
+
|
|
103
|
+
logger.debug(
|
|
104
|
+
"cli_callback_server_started", port=self.port, path=self.callback_path
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def stop(self) -> None:
|
|
108
|
+
"""Stop the callback server."""
|
|
109
|
+
if self.server:
|
|
110
|
+
self.server.should_exit = True
|
|
111
|
+
if hasattr(self, "_server_task") and self._server_task is not None:
|
|
112
|
+
try:
|
|
113
|
+
await asyncio.wait_for(self._server_task, timeout=2.0)
|
|
114
|
+
except TimeoutError:
|
|
115
|
+
self._server_task.cancel()
|
|
116
|
+
self.server = None
|
|
117
|
+
logger.debug("cli_callback_server_stopped", port=self.port)
|
|
118
|
+
|
|
119
|
+
async def _handle_callback(
|
|
120
|
+
self, scope: dict[str, Any], receive: Any, send: Any
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Handle OAuth callback requests."""
|
|
123
|
+
from urllib.parse import parse_qs
|
|
124
|
+
|
|
125
|
+
# Extract query parameters from scope
|
|
126
|
+
query_string = scope.get("query_string", b"").decode()
|
|
127
|
+
query_params = {k: v[0] for k, v in parse_qs(query_string).items()}
|
|
128
|
+
|
|
129
|
+
# Store callback data
|
|
130
|
+
self.callback_data = query_params
|
|
131
|
+
self.callback_received = True
|
|
132
|
+
|
|
133
|
+
# Signal that callback was received
|
|
134
|
+
if self.callback_future and not self.callback_future.done():
|
|
135
|
+
self.callback_future.set_result(query_params)
|
|
136
|
+
|
|
137
|
+
logger.debug("cli_callback_received", params=list(query_params.keys()))
|
|
138
|
+
|
|
139
|
+
# Return success page
|
|
140
|
+
html_content = """
|
|
141
|
+
<!DOCTYPE html>
|
|
142
|
+
<html>
|
|
143
|
+
<head>
|
|
144
|
+
<title>Authentication Complete</title>
|
|
145
|
+
<style>
|
|
146
|
+
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
|
|
147
|
+
.success { color: #4CAF50; }
|
|
148
|
+
.info { color: #2196F3; }
|
|
149
|
+
</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<h1 class="success">✓ Authentication Successful</h1>
|
|
153
|
+
<p class="info">You can close this window and return to the command line.</p>
|
|
154
|
+
</body>
|
|
155
|
+
</html>
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
await send(
|
|
159
|
+
{
|
|
160
|
+
"type": "http.response.start",
|
|
161
|
+
"status": 200,
|
|
162
|
+
"headers": [[b"content-type", b"text/html"]],
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
await send(
|
|
166
|
+
{
|
|
167
|
+
"type": "http.response.body",
|
|
168
|
+
"body": html_content.encode(),
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
async def wait_for_callback(
|
|
173
|
+
self, expected_state: str | None = None, timeout: float = 300
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Wait for OAuth callback with optional state validation.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
expected_state: Expected OAuth state parameter for validation
|
|
179
|
+
timeout: Timeout in seconds
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Callback data dictionary
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
asyncio.TimeoutError: If callback is not received within timeout
|
|
186
|
+
ValueError: If state validation fails
|
|
187
|
+
"""
|
|
188
|
+
self.callback_future = asyncio.Future()
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Wait for callback with timeout
|
|
192
|
+
callback_data = await asyncio.wait_for(
|
|
193
|
+
self.callback_future, timeout=timeout
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Validate state if provided
|
|
197
|
+
if expected_state and expected_state != "manual":
|
|
198
|
+
received_state = callback_data.get("state")
|
|
199
|
+
if received_state != expected_state:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"OAuth state mismatch: expected {expected_state}, got {received_state}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Check for OAuth errors
|
|
205
|
+
if "error" in callback_data:
|
|
206
|
+
error = callback_data.get("error")
|
|
207
|
+
error_description = callback_data.get(
|
|
208
|
+
"error_description", "No description provided"
|
|
209
|
+
)
|
|
210
|
+
raise ValueError(f"OAuth error: {error} - {error_description}")
|
|
211
|
+
|
|
212
|
+
# Ensure we have an authorization code
|
|
213
|
+
if "code" not in callback_data:
|
|
214
|
+
raise ValueError("No authorization code received in callback")
|
|
215
|
+
|
|
216
|
+
return callback_data
|
|
217
|
+
|
|
218
|
+
except TimeoutError:
|
|
219
|
+
logger.error("cli_callback_timeout", timeout=timeout, port=self.port)
|
|
220
|
+
raise TimeoutError(f"No OAuth callback received within {timeout} seconds")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def render_qr_code(url: str) -> None:
|
|
224
|
+
"""Render QR code for URL when TTY supports it."""
|
|
225
|
+
if not sys.stdout.isatty():
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
import qrcode # type: ignore[import-untyped]
|
|
230
|
+
|
|
231
|
+
qr = qrcode.QRCode(border=1)
|
|
232
|
+
qr.add_data(url)
|
|
233
|
+
qr.print_ascii(invert=True)
|
|
234
|
+
console.print("[dim]Scan QR code with mobile device[/dim]")
|
|
235
|
+
except ImportError:
|
|
236
|
+
# QR code library not available - graceful degradation
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class BrowserFlow:
|
|
241
|
+
"""Browser-based OAuth flow with callback server."""
|
|
242
|
+
|
|
243
|
+
async def run(
|
|
244
|
+
self,
|
|
245
|
+
provider: OAuthProviderProtocol,
|
|
246
|
+
no_browser: bool,
|
|
247
|
+
save_path: str | Path | None = None,
|
|
248
|
+
) -> Any:
|
|
249
|
+
"""Execute browser OAuth flow with fallback handling."""
|
|
250
|
+
cli_config = provider.cli
|
|
251
|
+
|
|
252
|
+
# Try provider's fixed port
|
|
253
|
+
try:
|
|
254
|
+
callback_server = CLICallbackServer(
|
|
255
|
+
cli_config.callback_port, cli_config.callback_path
|
|
256
|
+
)
|
|
257
|
+
await callback_server.start()
|
|
258
|
+
except PortBindError as e:
|
|
259
|
+
# Offer manual fallback for fixed-port providers
|
|
260
|
+
if cli_config.fixed_redirect_uri:
|
|
261
|
+
console.print(
|
|
262
|
+
f"[yellow]Port {cli_config.callback_port} unavailable. Try --manual mode.[/yellow]"
|
|
263
|
+
)
|
|
264
|
+
raise AuthProviderError(
|
|
265
|
+
f"Required port {cli_config.callback_port} unavailable"
|
|
266
|
+
) from e
|
|
267
|
+
raise
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
# Generate OAuth parameters with PKCE if supported
|
|
271
|
+
state = secrets.token_urlsafe(32)
|
|
272
|
+
code_verifier = None
|
|
273
|
+
if provider.supports_pkce:
|
|
274
|
+
code_verifier = (
|
|
275
|
+
base64.urlsafe_b64encode(secrets.token_bytes(32))
|
|
276
|
+
.decode("utf-8")
|
|
277
|
+
.rstrip("=")
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Use fixed redirect URI or construct from config
|
|
281
|
+
redirect_uri = (
|
|
282
|
+
cli_config.fixed_redirect_uri
|
|
283
|
+
or f"http://localhost:{cli_config.callback_port}{cli_config.callback_path}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Get authorization URL
|
|
287
|
+
auth_url = await provider.get_authorization_url(
|
|
288
|
+
state, code_verifier, redirect_uri
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Always show URL and QR code for fallback
|
|
292
|
+
console.print(f"[bold]Visit: {auth_url}[/bold]")
|
|
293
|
+
render_qr_code(auth_url)
|
|
294
|
+
|
|
295
|
+
# Try to open browser unless explicitly disabled
|
|
296
|
+
if not no_browser:
|
|
297
|
+
try:
|
|
298
|
+
webbrowser.open(auth_url)
|
|
299
|
+
console.print("[dim]Opening browser...[/dim]")
|
|
300
|
+
except Exception:
|
|
301
|
+
console.print(
|
|
302
|
+
"[yellow]Could not open browser automatically[/yellow]"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Wait for callback with timeout and state validation
|
|
306
|
+
try:
|
|
307
|
+
callback_data = await callback_server.wait_for_callback(
|
|
308
|
+
state, timeout=300
|
|
309
|
+
)
|
|
310
|
+
credentials = await provider.handle_callback(
|
|
311
|
+
callback_data["code"], state, code_verifier, redirect_uri
|
|
312
|
+
)
|
|
313
|
+
return await provider.save_credentials(credentials, save_path)
|
|
314
|
+
except TimeoutError:
|
|
315
|
+
# Fallback to manual code entry if callback times out
|
|
316
|
+
console.print(
|
|
317
|
+
"[yellow]Callback timed out. You can enter the code manually.[/yellow]"
|
|
318
|
+
)
|
|
319
|
+
if cli_config.supports_manual_code:
|
|
320
|
+
# Use provider-specific manual redirect URI or fallback to OOB
|
|
321
|
+
manual_redirect_uri = (
|
|
322
|
+
cli_config.manual_redirect_uri or "urn:ietf:wg:oauth:2.0:oob"
|
|
323
|
+
)
|
|
324
|
+
manual_auth_url = await provider.get_authorization_url(
|
|
325
|
+
state, code_verifier, manual_redirect_uri
|
|
326
|
+
)
|
|
327
|
+
console.print(f"[bold]Manual URL: {manual_auth_url}[/bold]")
|
|
328
|
+
|
|
329
|
+
import typer
|
|
330
|
+
|
|
331
|
+
raw_code = typer.prompt("Enter the authorization code")
|
|
332
|
+
|
|
333
|
+
# Parse the code - some providers (like Claude) return code#state format
|
|
334
|
+
# Extract the code and state parts
|
|
335
|
+
code_parts = raw_code.split("#")
|
|
336
|
+
code = code_parts[0].strip()
|
|
337
|
+
|
|
338
|
+
# If there's a state in the input (Claude format), use it instead of our generated state
|
|
339
|
+
if len(code_parts) > 1 and code_parts[1].strip():
|
|
340
|
+
actual_state = code_parts[1].strip()
|
|
341
|
+
else:
|
|
342
|
+
actual_state = state
|
|
343
|
+
|
|
344
|
+
credentials = await provider.handle_callback(
|
|
345
|
+
code, actual_state, code_verifier, manual_redirect_uri
|
|
346
|
+
)
|
|
347
|
+
return await provider.save_credentials(credentials, save_path)
|
|
348
|
+
else:
|
|
349
|
+
raise
|
|
350
|
+
finally:
|
|
351
|
+
await callback_server.stop()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class DeviceCodeFlow:
|
|
355
|
+
"""OAuth device code flow for headless environments."""
|
|
356
|
+
|
|
357
|
+
async def run(
|
|
358
|
+
self, provider: OAuthProviderProtocol, save_path: str | Path | None = None
|
|
359
|
+
) -> Any:
|
|
360
|
+
"""Execute device code flow with polling."""
|
|
361
|
+
(
|
|
362
|
+
device_code,
|
|
363
|
+
user_code,
|
|
364
|
+
verification_uri,
|
|
365
|
+
expires_in,
|
|
366
|
+
) = await provider.start_device_flow()
|
|
367
|
+
|
|
368
|
+
console.print(f"[bold green]Visit: {verification_uri}[/bold green]")
|
|
369
|
+
console.print(f"[bold green]Enter code: {user_code}[/bold green]")
|
|
370
|
+
render_qr_code(verification_uri) # QR code for mobile
|
|
371
|
+
|
|
372
|
+
# Poll for completion with timeout
|
|
373
|
+
with console.status("Waiting for authorization..."):
|
|
374
|
+
credentials = await provider.complete_device_flow(
|
|
375
|
+
device_code, 5, expires_in
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return await provider.save_credentials(credentials, save_path)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class ManualCodeFlow:
|
|
382
|
+
"""Manual authorization code entry for restricted environments."""
|
|
383
|
+
|
|
384
|
+
async def run(
|
|
385
|
+
self, provider: OAuthProviderProtocol, save_path: str | Path | None = None
|
|
386
|
+
) -> Any:
|
|
387
|
+
"""Execute manual code entry flow."""
|
|
388
|
+
# Generate state for manual flow
|
|
389
|
+
state = secrets.token_urlsafe(32)
|
|
390
|
+
code_verifier = None
|
|
391
|
+
if provider.supports_pkce:
|
|
392
|
+
code_verifier = (
|
|
393
|
+
base64.urlsafe_b64encode(secrets.token_bytes(32))
|
|
394
|
+
.decode("utf-8")
|
|
395
|
+
.rstrip("=")
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Get provider-specific manual redirect URI or fallback to OOB
|
|
399
|
+
manual_redirect_uri = (
|
|
400
|
+
provider.cli.manual_redirect_uri or "urn:ietf:wg:oauth:2.0:oob"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Get authorization URL for manual entry
|
|
404
|
+
auth_url = await provider.get_authorization_url(
|
|
405
|
+
state, code_verifier, manual_redirect_uri
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
console.print(f"[bold green]Visit: {auth_url}[/bold green]")
|
|
409
|
+
render_qr_code(auth_url)
|
|
410
|
+
|
|
411
|
+
# Prompt for manual code entry
|
|
412
|
+
raw_code = typer.prompt("[bold]Enter the authorization code[/bold]").strip()
|
|
413
|
+
|
|
414
|
+
# Parse the code - some providers (like Claude) return code#state format
|
|
415
|
+
# Extract the code and state parts
|
|
416
|
+
code_parts = raw_code.split("#")
|
|
417
|
+
code = code_parts[0].strip()
|
|
418
|
+
|
|
419
|
+
# If there's a state in the input (Claude format), use it instead of our generated state
|
|
420
|
+
if len(code_parts) > 1 and code_parts[1].strip():
|
|
421
|
+
actual_state = code_parts[1].strip()
|
|
422
|
+
else:
|
|
423
|
+
actual_state = state
|
|
424
|
+
|
|
425
|
+
# Use the provider's handle_callback method instead of exchange_manual_code
|
|
426
|
+
# to properly handle state validation
|
|
427
|
+
credentials = await provider.handle_callback(
|
|
428
|
+
code, actual_state, code_verifier, manual_redirect_uri
|
|
429
|
+
)
|
|
430
|
+
return await provider.save_credentials(credentials, save_path)
|