ccproxy-api 0.1.0__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/__init__.py +4 -0
- ccproxy/__main__.py +7 -0
- ccproxy/_version.py +21 -0
- ccproxy/adapters/__init__.py +11 -0
- ccproxy/adapters/base.py +80 -0
- ccproxy/adapters/openai/__init__.py +43 -0
- ccproxy/adapters/openai/adapter.py +915 -0
- ccproxy/adapters/openai/models.py +412 -0
- ccproxy/adapters/openai/streaming.py +449 -0
- ccproxy/api/__init__.py +28 -0
- ccproxy/api/app.py +225 -0
- ccproxy/api/dependencies.py +140 -0
- ccproxy/api/middleware/__init__.py +11 -0
- ccproxy/api/middleware/auth.py +0 -0
- ccproxy/api/middleware/cors.py +55 -0
- ccproxy/api/middleware/errors.py +703 -0
- ccproxy/api/middleware/headers.py +51 -0
- ccproxy/api/middleware/logging.py +175 -0
- ccproxy/api/middleware/request_id.py +69 -0
- ccproxy/api/middleware/server_header.py +62 -0
- ccproxy/api/responses.py +84 -0
- ccproxy/api/routes/__init__.py +16 -0
- ccproxy/api/routes/claude.py +181 -0
- ccproxy/api/routes/health.py +489 -0
- ccproxy/api/routes/metrics.py +1033 -0
- ccproxy/api/routes/proxy.py +238 -0
- ccproxy/auth/__init__.py +75 -0
- ccproxy/auth/bearer.py +68 -0
- ccproxy/auth/credentials_adapter.py +93 -0
- ccproxy/auth/dependencies.py +229 -0
- ccproxy/auth/exceptions.py +79 -0
- ccproxy/auth/manager.py +102 -0
- ccproxy/auth/models.py +118 -0
- ccproxy/auth/oauth/__init__.py +26 -0
- ccproxy/auth/oauth/models.py +49 -0
- ccproxy/auth/oauth/routes.py +396 -0
- ccproxy/auth/oauth/storage.py +0 -0
- ccproxy/auth/storage/__init__.py +12 -0
- ccproxy/auth/storage/base.py +57 -0
- ccproxy/auth/storage/json_file.py +159 -0
- ccproxy/auth/storage/keyring.py +192 -0
- ccproxy/claude_sdk/__init__.py +20 -0
- ccproxy/claude_sdk/client.py +169 -0
- ccproxy/claude_sdk/converter.py +331 -0
- ccproxy/claude_sdk/options.py +120 -0
- ccproxy/cli/__init__.py +14 -0
- ccproxy/cli/commands/__init__.py +8 -0
- ccproxy/cli/commands/auth.py +553 -0
- ccproxy/cli/commands/config/__init__.py +14 -0
- ccproxy/cli/commands/config/commands.py +766 -0
- ccproxy/cli/commands/config/schema_commands.py +119 -0
- ccproxy/cli/commands/serve.py +630 -0
- ccproxy/cli/docker/__init__.py +34 -0
- ccproxy/cli/docker/adapter_factory.py +157 -0
- ccproxy/cli/docker/params.py +278 -0
- ccproxy/cli/helpers.py +144 -0
- ccproxy/cli/main.py +193 -0
- ccproxy/cli/options/__init__.py +14 -0
- ccproxy/cli/options/claude_options.py +216 -0
- ccproxy/cli/options/core_options.py +40 -0
- ccproxy/cli/options/security_options.py +48 -0
- ccproxy/cli/options/server_options.py +117 -0
- ccproxy/config/__init__.py +40 -0
- ccproxy/config/auth.py +154 -0
- ccproxy/config/claude.py +124 -0
- ccproxy/config/cors.py +79 -0
- ccproxy/config/discovery.py +87 -0
- ccproxy/config/docker_settings.py +265 -0
- ccproxy/config/loader.py +108 -0
- ccproxy/config/observability.py +158 -0
- ccproxy/config/pricing.py +88 -0
- ccproxy/config/reverse_proxy.py +31 -0
- ccproxy/config/scheduler.py +89 -0
- ccproxy/config/security.py +14 -0
- ccproxy/config/server.py +81 -0
- ccproxy/config/settings.py +534 -0
- ccproxy/config/validators.py +231 -0
- ccproxy/core/__init__.py +274 -0
- ccproxy/core/async_utils.py +675 -0
- ccproxy/core/constants.py +97 -0
- ccproxy/core/errors.py +256 -0
- ccproxy/core/http.py +328 -0
- ccproxy/core/http_transformers.py +428 -0
- ccproxy/core/interfaces.py +247 -0
- ccproxy/core/logging.py +189 -0
- ccproxy/core/middleware.py +114 -0
- ccproxy/core/proxy.py +143 -0
- ccproxy/core/system.py +38 -0
- ccproxy/core/transformers.py +259 -0
- ccproxy/core/types.py +129 -0
- ccproxy/core/validators.py +288 -0
- ccproxy/docker/__init__.py +67 -0
- ccproxy/docker/adapter.py +588 -0
- ccproxy/docker/docker_path.py +207 -0
- ccproxy/docker/middleware.py +103 -0
- ccproxy/docker/models.py +228 -0
- ccproxy/docker/protocol.py +192 -0
- ccproxy/docker/stream_process.py +264 -0
- ccproxy/docker/validators.py +173 -0
- ccproxy/models/__init__.py +123 -0
- ccproxy/models/errors.py +42 -0
- ccproxy/models/messages.py +243 -0
- ccproxy/models/requests.py +85 -0
- ccproxy/models/responses.py +227 -0
- ccproxy/models/types.py +102 -0
- ccproxy/observability/__init__.py +51 -0
- ccproxy/observability/access_logger.py +400 -0
- ccproxy/observability/context.py +447 -0
- ccproxy/observability/metrics.py +539 -0
- ccproxy/observability/pushgateway.py +366 -0
- ccproxy/observability/sse_events.py +303 -0
- ccproxy/observability/stats_printer.py +755 -0
- ccproxy/observability/storage/__init__.py +1 -0
- ccproxy/observability/storage/duckdb_simple.py +665 -0
- ccproxy/observability/storage/models.py +55 -0
- ccproxy/pricing/__init__.py +19 -0
- ccproxy/pricing/cache.py +212 -0
- ccproxy/pricing/loader.py +267 -0
- ccproxy/pricing/models.py +106 -0
- ccproxy/pricing/updater.py +309 -0
- ccproxy/scheduler/__init__.py +39 -0
- ccproxy/scheduler/core.py +335 -0
- ccproxy/scheduler/exceptions.py +34 -0
- ccproxy/scheduler/manager.py +186 -0
- ccproxy/scheduler/registry.py +150 -0
- ccproxy/scheduler/tasks.py +484 -0
- ccproxy/services/__init__.py +10 -0
- ccproxy/services/claude_sdk_service.py +614 -0
- ccproxy/services/credentials/__init__.py +55 -0
- ccproxy/services/credentials/config.py +105 -0
- ccproxy/services/credentials/manager.py +562 -0
- ccproxy/services/credentials/oauth_client.py +482 -0
- ccproxy/services/proxy_service.py +1536 -0
- ccproxy/static/.keep +0 -0
- ccproxy/testing/__init__.py +34 -0
- ccproxy/testing/config.py +148 -0
- ccproxy/testing/content_generation.py +197 -0
- ccproxy/testing/mock_responses.py +262 -0
- ccproxy/testing/response_handlers.py +161 -0
- ccproxy/testing/scenarios.py +241 -0
- ccproxy/utils/__init__.py +6 -0
- ccproxy/utils/cost_calculator.py +210 -0
- ccproxy/utils/streaming_metrics.py +199 -0
- ccproxy_api-0.1.0.dist-info/METADATA +253 -0
- ccproxy_api-0.1.0.dist-info/RECORD +148 -0
- ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
- ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
- ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Authentication exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthenticationError(Exception):
|
|
5
|
+
"""Base authentication error."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthenticationRequiredError(AuthenticationError):
|
|
11
|
+
"""Authentication is required but not provided."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidTokenError(AuthenticationError):
|
|
17
|
+
"""Invalid or expired token."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InsufficientPermissionsError(AuthenticationError):
|
|
23
|
+
"""Insufficient permissions for the requested operation."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CredentialsError(AuthenticationError):
|
|
29
|
+
"""Base credentials error."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CredentialsNotFoundError(CredentialsError):
|
|
35
|
+
"""Credentials not found error."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CredentialsExpiredError(CredentialsError):
|
|
41
|
+
"""Credentials expired error."""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CredentialsInvalidError(CredentialsError):
|
|
47
|
+
"""Credentials are invalid or malformed."""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CredentialsStorageError(CredentialsError):
|
|
53
|
+
"""Error occurred during credentials storage operations."""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class OAuthError(AuthenticationError):
|
|
59
|
+
"""Base OAuth error."""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class OAuthLoginError(OAuthError):
|
|
65
|
+
"""OAuth login failed."""
|
|
66
|
+
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OAuthTokenRefreshError(OAuthError):
|
|
71
|
+
"""OAuth token refresh failed."""
|
|
72
|
+
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class OAuthCallbackError(OAuthError):
|
|
77
|
+
"""OAuth callback failed."""
|
|
78
|
+
|
|
79
|
+
pass
|
ccproxy/auth/manager.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Authentication manager interfaces for centralized auth handling."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
from ccproxy.auth.models import ClaudeCredentials, UserProfile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthManager(Protocol):
|
|
10
|
+
"""Protocol for authentication managers."""
|
|
11
|
+
|
|
12
|
+
async def get_access_token(self) -> str:
|
|
13
|
+
"""Get valid access token.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Access token string
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
AuthenticationError: If authentication fails
|
|
20
|
+
"""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
async def get_credentials(self) -> ClaudeCredentials:
|
|
24
|
+
"""Get valid credentials.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Valid credentials
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
AuthenticationError: If authentication fails
|
|
31
|
+
"""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
async def is_authenticated(self) -> bool:
|
|
35
|
+
"""Check if current authentication is valid.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
True if authenticated, False otherwise
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
async def get_user_profile(self) -> UserProfile | None:
|
|
43
|
+
"""Get user profile information.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
UserProfile if available, None otherwise
|
|
47
|
+
"""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BaseAuthManager(ABC):
|
|
52
|
+
"""Base class for authentication managers."""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def get_access_token(self) -> str:
|
|
56
|
+
"""Get valid access token.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Access token string
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
AuthenticationError: If authentication fails
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
async def get_credentials(self) -> ClaudeCredentials:
|
|
68
|
+
"""Get valid credentials.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Valid credentials
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
AuthenticationError: If authentication fails
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
async def is_authenticated(self) -> bool:
|
|
80
|
+
"""Check if current authentication is valid.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if authenticated, False otherwise
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
async def get_user_profile(self) -> UserProfile | None:
|
|
88
|
+
"""Get user profile information.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
UserProfile if available, None otherwise
|
|
92
|
+
"""
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
async def __aenter__(self) -> "BaseAuthManager":
|
|
96
|
+
"""Async context manager entry."""
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
101
|
+
"""Async context manager exit."""
|
|
102
|
+
pass
|
ccproxy/auth/models.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Data models for authentication."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OAuthToken(BaseModel):
|
|
9
|
+
"""OAuth token information from Claude credentials."""
|
|
10
|
+
|
|
11
|
+
access_token: str = Field(..., alias="accessToken")
|
|
12
|
+
refresh_token: str = Field(..., alias="refreshToken")
|
|
13
|
+
expires_at: int | None = Field(None, alias="expiresAt")
|
|
14
|
+
scopes: list[str] = Field(default_factory=list)
|
|
15
|
+
subscription_type: str | None = Field(None, alias="subscriptionType")
|
|
16
|
+
token_type: str = Field(default="Bearer", alias="tokenType")
|
|
17
|
+
|
|
18
|
+
def __repr__(self) -> str:
|
|
19
|
+
"""Safe string representation that masks sensitive tokens."""
|
|
20
|
+
access_preview = (
|
|
21
|
+
f"{self.access_token[:8]}...{self.access_token[-8:]}"
|
|
22
|
+
if len(self.access_token) > 16
|
|
23
|
+
else "***"
|
|
24
|
+
)
|
|
25
|
+
refresh_preview = (
|
|
26
|
+
f"{self.refresh_token[:8]}...{self.refresh_token[-8:]}"
|
|
27
|
+
if len(self.refresh_token) > 16
|
|
28
|
+
else "***"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
expires_at = (
|
|
32
|
+
datetime.fromtimestamp(self.expires_at / 1000, tz=UTC).isoformat()
|
|
33
|
+
if self.expires_at is not None
|
|
34
|
+
else "None"
|
|
35
|
+
)
|
|
36
|
+
return (
|
|
37
|
+
f"OAuthToken(access_token='{access_preview}', "
|
|
38
|
+
f"refresh_token='{refresh_preview}', "
|
|
39
|
+
f"expires_at={expires_at}, "
|
|
40
|
+
f"scopes={self.scopes}, "
|
|
41
|
+
f"subscription_type='{self.subscription_type}', "
|
|
42
|
+
f"token_type='{self.token_type}')"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_expired(self) -> bool:
|
|
47
|
+
"""Check if the token is expired."""
|
|
48
|
+
if self.expires_at is None:
|
|
49
|
+
# If no expiration info, assume not expired for backward compatibility
|
|
50
|
+
return False
|
|
51
|
+
now = datetime.now(UTC).timestamp() * 1000 # Convert to milliseconds
|
|
52
|
+
return now >= self.expires_at
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def expires_at_datetime(self) -> datetime:
|
|
56
|
+
"""Get expiration as datetime object."""
|
|
57
|
+
if self.expires_at is None:
|
|
58
|
+
# Return a far future date if no expiration info
|
|
59
|
+
return datetime.fromtimestamp(2147483647, tz=UTC) # Year 2038
|
|
60
|
+
return datetime.fromtimestamp(self.expires_at / 1000, tz=UTC)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OrganizationInfo(BaseModel):
|
|
64
|
+
"""Organization information from OAuth API."""
|
|
65
|
+
|
|
66
|
+
uuid: str
|
|
67
|
+
name: str
|
|
68
|
+
organization_type: str | None = None
|
|
69
|
+
billing_type: str | None = None
|
|
70
|
+
rate_limit_tier: str | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AccountInfo(BaseModel):
|
|
74
|
+
"""Account information from OAuth API."""
|
|
75
|
+
|
|
76
|
+
uuid: str
|
|
77
|
+
email: str
|
|
78
|
+
full_name: str | None = None
|
|
79
|
+
display_name: str | None = None
|
|
80
|
+
has_claude_max: bool | None = None
|
|
81
|
+
has_claude_pro: bool | None = None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def email_address(self) -> str:
|
|
85
|
+
"""Compatibility property for email_address."""
|
|
86
|
+
return self.email
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class UserProfile(BaseModel):
|
|
90
|
+
"""User profile information from Anthropic OAuth API."""
|
|
91
|
+
|
|
92
|
+
organization: OrganizationInfo | None = None
|
|
93
|
+
account: AccountInfo | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ClaudeCredentials(BaseModel):
|
|
97
|
+
"""Claude credentials from the credentials file."""
|
|
98
|
+
|
|
99
|
+
claude_ai_oauth: OAuthToken = Field(..., alias="claudeAiOauth")
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
"""Safe string representation that masks sensitive tokens."""
|
|
103
|
+
return f"ClaudeCredentials(claude_ai_oauth={repr(self.claude_ai_oauth)})"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ValidationResult(BaseModel):
|
|
107
|
+
"""Result of credentials validation."""
|
|
108
|
+
|
|
109
|
+
valid: bool
|
|
110
|
+
expired: bool | None = None
|
|
111
|
+
credentials: ClaudeCredentials | None = None
|
|
112
|
+
path: str | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Backwards compatibility - provide common aliases
|
|
116
|
+
User = UserProfile
|
|
117
|
+
Credentials = ClaudeCredentials
|
|
118
|
+
Profile = UserProfile
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""OAuth authentication module for Anthropic OAuth login."""
|
|
2
|
+
|
|
3
|
+
from ccproxy.auth.oauth.models import (
|
|
4
|
+
OAuthCallbackRequest,
|
|
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
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
# Router
|
|
18
|
+
"router",
|
|
19
|
+
"register_oauth_flow",
|
|
20
|
+
"get_oauth_flow_result",
|
|
21
|
+
# Models
|
|
22
|
+
"OAuthState",
|
|
23
|
+
"OAuthCallbackRequest",
|
|
24
|
+
"OAuthTokenRequest",
|
|
25
|
+
"OAuthTokenResponse",
|
|
26
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""OAuth-specific models for authentication."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OAuthState(BaseModel):
|
|
10
|
+
"""OAuth state information for pending flows."""
|
|
11
|
+
|
|
12
|
+
code_verifier: str = Field(..., description="PKCE code verifier")
|
|
13
|
+
custom_paths: list[str] | None = Field(None, description="Custom credential paths")
|
|
14
|
+
completed: bool = Field(default=False, description="Whether the flow is completed")
|
|
15
|
+
success: bool = Field(default=False, description="Whether the flow was successful")
|
|
16
|
+
error: str | None = Field(None, description="Error message if failed")
|
|
17
|
+
created_at: datetime = Field(
|
|
18
|
+
default_factory=datetime.utcnow, description="Creation timestamp"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OAuthCallbackRequest(BaseModel):
|
|
23
|
+
"""OAuth callback request parameters."""
|
|
24
|
+
|
|
25
|
+
code: str | None = Field(None, description="Authorization code")
|
|
26
|
+
state: str | None = Field(None, description="State parameter")
|
|
27
|
+
error: str | None = Field(None, description="OAuth error")
|
|
28
|
+
error_description: str | None = Field(None, description="OAuth error description")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OAuthTokenRequest(BaseModel):
|
|
32
|
+
"""OAuth token exchange request."""
|
|
33
|
+
|
|
34
|
+
grant_type: str = Field(default="authorization_code")
|
|
35
|
+
code: str = Field(..., description="Authorization code")
|
|
36
|
+
redirect_uri: str = Field(..., description="Redirect URI")
|
|
37
|
+
client_id: str = Field(..., description="Client ID")
|
|
38
|
+
code_verifier: str = Field(..., description="PKCE code verifier")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OAuthTokenResponse(BaseModel):
|
|
42
|
+
"""OAuth token exchange response."""
|
|
43
|
+
|
|
44
|
+
access_token: str = Field(..., description="Access token")
|
|
45
|
+
refresh_token: str | None = Field(None, description="Refresh token")
|
|
46
|
+
expires_in: int | None = Field(None, description="Token expiration in seconds")
|
|
47
|
+
scope: str | None = Field(None, description="Granted scopes")
|
|
48
|
+
subscription_type: str | None = Field(None, description="Subscription type")
|
|
49
|
+
token_type: str = Field(default="Bearer", description="Token type")
|