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,553 @@
|
|
|
1
|
+
"""Authentication and credential management commands."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from datetime import UTC, datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich import box
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from structlog import get_logger
|
|
14
|
+
|
|
15
|
+
from ccproxy.auth.models import ValidationResult
|
|
16
|
+
from ccproxy.cli.helpers import get_rich_toolkit
|
|
17
|
+
from ccproxy.config.settings import get_settings
|
|
18
|
+
from ccproxy.core.async_utils import get_claude_docker_home_dir
|
|
19
|
+
from ccproxy.services.credentials import CredentialsManager
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(name="auth", help="Authentication and credential management")
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_credentials_manager(
|
|
29
|
+
custom_paths: list[Path] | None = None,
|
|
30
|
+
) -> CredentialsManager:
|
|
31
|
+
"""Get a CredentialsManager instance with custom paths if provided."""
|
|
32
|
+
if custom_paths:
|
|
33
|
+
# Get base settings and update storage paths
|
|
34
|
+
settings = get_settings()
|
|
35
|
+
settings.auth.storage.storage_paths = custom_paths
|
|
36
|
+
return CredentialsManager(config=settings.auth)
|
|
37
|
+
else:
|
|
38
|
+
# Use default settings
|
|
39
|
+
settings = get_settings()
|
|
40
|
+
return CredentialsManager(config=settings.auth)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_docker_credential_paths() -> list[Path]:
|
|
44
|
+
"""Get credential file paths for Docker environment."""
|
|
45
|
+
docker_home = Path(get_claude_docker_home_dir())
|
|
46
|
+
return [
|
|
47
|
+
docker_home / ".claude" / ".credentials.json",
|
|
48
|
+
docker_home / ".config" / "claude" / ".credentials.json",
|
|
49
|
+
Path(".credentials.json"),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command(name="validate")
|
|
54
|
+
def validate_credentials(
|
|
55
|
+
docker: bool = typer.Option(
|
|
56
|
+
False,
|
|
57
|
+
"--docker",
|
|
58
|
+
help="Use Docker credential paths (from get_claude_docker_home_dir())",
|
|
59
|
+
),
|
|
60
|
+
credential_file: str | None = typer.Option(
|
|
61
|
+
None,
|
|
62
|
+
"--credential-file",
|
|
63
|
+
help="Path to specific credential file to validate",
|
|
64
|
+
),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Validate Claude CLI credentials.
|
|
67
|
+
|
|
68
|
+
Checks for valid Claude credentials in standard locations:
|
|
69
|
+
- ~/.claude/credentials.json
|
|
70
|
+
- ~/.config/claude/credentials.json
|
|
71
|
+
|
|
72
|
+
With --docker flag, checks Docker credential paths:
|
|
73
|
+
- {docker_home}/.claude/credentials.json
|
|
74
|
+
- {docker_home}/.config/claude/credentials.json
|
|
75
|
+
|
|
76
|
+
With --credential-file, validates the specified file directly.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
ccproxy auth validate
|
|
80
|
+
ccproxy auth validate --docker
|
|
81
|
+
ccproxy auth validate --credential-file /path/to/credentials.json
|
|
82
|
+
"""
|
|
83
|
+
toolkit = get_rich_toolkit()
|
|
84
|
+
toolkit.print("[bold cyan]Claude Credentials Validation[/bold cyan]", centered=True)
|
|
85
|
+
toolkit.print_line()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Get credential paths based on options
|
|
89
|
+
custom_paths = None
|
|
90
|
+
if credential_file:
|
|
91
|
+
custom_paths = [Path(credential_file)]
|
|
92
|
+
elif docker:
|
|
93
|
+
custom_paths = get_docker_credential_paths()
|
|
94
|
+
|
|
95
|
+
# Validate credentials
|
|
96
|
+
manager = get_credentials_manager(custom_paths)
|
|
97
|
+
validation_result = asyncio.run(manager.validate())
|
|
98
|
+
|
|
99
|
+
if validation_result.valid:
|
|
100
|
+
# Create a status table
|
|
101
|
+
table = Table(
|
|
102
|
+
show_header=True,
|
|
103
|
+
header_style="bold cyan",
|
|
104
|
+
box=box.ROUNDED,
|
|
105
|
+
title="Credential Status",
|
|
106
|
+
title_style="bold white",
|
|
107
|
+
)
|
|
108
|
+
table.add_column("Property", style="cyan")
|
|
109
|
+
table.add_column("Value", style="white")
|
|
110
|
+
|
|
111
|
+
# Status
|
|
112
|
+
status = "Valid" if not validation_result.expired else "Expired"
|
|
113
|
+
status_style = "green" if not validation_result.expired else "red"
|
|
114
|
+
table.add_row("Status", f"[{status_style}]{status}[/{status_style}]")
|
|
115
|
+
|
|
116
|
+
# Path
|
|
117
|
+
if validation_result.path:
|
|
118
|
+
table.add_row("Location", f"[dim]{validation_result.path}[/dim]")
|
|
119
|
+
|
|
120
|
+
# Subscription type
|
|
121
|
+
if validation_result.credentials:
|
|
122
|
+
sub_type = (
|
|
123
|
+
validation_result.credentials.claude_ai_oauth.subscription_type
|
|
124
|
+
or "Unknown"
|
|
125
|
+
)
|
|
126
|
+
table.add_row("Subscription", f"[bold]{sub_type}[/bold]")
|
|
127
|
+
|
|
128
|
+
# Expiration
|
|
129
|
+
oauth_token = validation_result.credentials.claude_ai_oauth
|
|
130
|
+
exp_dt = oauth_token.expires_at_datetime
|
|
131
|
+
now = datetime.now(UTC)
|
|
132
|
+
time_diff = exp_dt - now
|
|
133
|
+
|
|
134
|
+
if time_diff.total_seconds() > 0:
|
|
135
|
+
days = time_diff.days
|
|
136
|
+
hours = time_diff.seconds // 3600
|
|
137
|
+
exp_str = f"{exp_dt.strftime('%Y-%m-%d %H:%M:%S UTC')} ({days}d {hours}h remaining)"
|
|
138
|
+
else:
|
|
139
|
+
exp_str = f"{exp_dt.strftime('%Y-%m-%d %H:%M:%S UTC')} [red](Expired)[/red]"
|
|
140
|
+
|
|
141
|
+
table.add_row("Expires", exp_str)
|
|
142
|
+
|
|
143
|
+
# Scopes
|
|
144
|
+
scopes = oauth_token.scopes
|
|
145
|
+
if scopes:
|
|
146
|
+
table.add_row("Scopes", ", ".join(str(s) for s in scopes))
|
|
147
|
+
|
|
148
|
+
console.print(table)
|
|
149
|
+
|
|
150
|
+
# Success message
|
|
151
|
+
if not validation_result.expired:
|
|
152
|
+
toolkit.print(
|
|
153
|
+
"[green]✓[/green] Valid Claude credentials found", tag="success"
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
toolkit.print(
|
|
157
|
+
"[yellow]![/yellow] Claude credentials found but expired",
|
|
158
|
+
tag="warning",
|
|
159
|
+
)
|
|
160
|
+
toolkit.print(
|
|
161
|
+
"\nPlease refresh your credentials by logging into Claude CLI",
|
|
162
|
+
tag="info",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
else:
|
|
166
|
+
# No valid credentials
|
|
167
|
+
toolkit.print("[red]✗[/red] No credentials file found", tag="error")
|
|
168
|
+
|
|
169
|
+
console.print("\n[dim]To authenticate with Claude CLI, run:[/dim]")
|
|
170
|
+
console.print("[cyan]claude login[/cyan]")
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
toolkit.print(f"Error validating credentials: {e}", tag="error")
|
|
174
|
+
raise typer.Exit(1) from e
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command(name="info")
|
|
178
|
+
def credential_info(
|
|
179
|
+
docker: bool = typer.Option(
|
|
180
|
+
False,
|
|
181
|
+
"--docker",
|
|
182
|
+
help="Use Docker credential paths (from get_claude_docker_home_dir())",
|
|
183
|
+
),
|
|
184
|
+
credential_file: str | None = typer.Option(
|
|
185
|
+
None,
|
|
186
|
+
"--credential-file",
|
|
187
|
+
help="Path to specific credential file to display info for",
|
|
188
|
+
),
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Display detailed credential information.
|
|
191
|
+
|
|
192
|
+
Shows all available information about Claude credentials including
|
|
193
|
+
file location, token details, and subscription information.
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
ccproxy auth info
|
|
197
|
+
ccproxy auth info --docker
|
|
198
|
+
ccproxy auth info --credential-file /path/to/credentials.json
|
|
199
|
+
"""
|
|
200
|
+
toolkit = get_rich_toolkit()
|
|
201
|
+
toolkit.print("[bold cyan]Claude Credential Information[/bold cyan]", centered=True)
|
|
202
|
+
toolkit.print_line()
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Get credential paths based on options
|
|
206
|
+
custom_paths = None
|
|
207
|
+
if credential_file:
|
|
208
|
+
custom_paths = [Path(credential_file)]
|
|
209
|
+
elif docker:
|
|
210
|
+
custom_paths = get_docker_credential_paths()
|
|
211
|
+
|
|
212
|
+
# Get credentials manager and try to load credentials
|
|
213
|
+
manager = get_credentials_manager(custom_paths)
|
|
214
|
+
credentials = asyncio.run(manager.load())
|
|
215
|
+
|
|
216
|
+
if not credentials:
|
|
217
|
+
toolkit.print("No credential file found", tag="error")
|
|
218
|
+
console.print("\n[dim]Expected locations:[/dim]")
|
|
219
|
+
for path in manager.config.storage.storage_paths:
|
|
220
|
+
console.print(f" - {path}")
|
|
221
|
+
raise typer.Exit(1)
|
|
222
|
+
|
|
223
|
+
# Display account section
|
|
224
|
+
console.print("\n[bold]Account[/bold]")
|
|
225
|
+
oauth = credentials.claude_ai_oauth
|
|
226
|
+
|
|
227
|
+
# Login method based on subscription type
|
|
228
|
+
login_method = "Claude Account"
|
|
229
|
+
if oauth.subscription_type:
|
|
230
|
+
login_method = f"Claude {oauth.subscription_type.title()} Account"
|
|
231
|
+
console.print(f" L Login Method: {login_method}")
|
|
232
|
+
|
|
233
|
+
# Try to load saved account profile first
|
|
234
|
+
profile = asyncio.run(manager.get_account_profile())
|
|
235
|
+
|
|
236
|
+
if profile:
|
|
237
|
+
# Display saved account data
|
|
238
|
+
if profile.organization:
|
|
239
|
+
console.print(f" L Organization: {profile.organization.name}")
|
|
240
|
+
if profile.organization.organization_type:
|
|
241
|
+
console.print(
|
|
242
|
+
f" L Organization Type: {profile.organization.organization_type}"
|
|
243
|
+
)
|
|
244
|
+
if profile.organization.billing_type:
|
|
245
|
+
console.print(
|
|
246
|
+
f" L Billing Type: {profile.organization.billing_type}"
|
|
247
|
+
)
|
|
248
|
+
if profile.organization.rate_limit_tier:
|
|
249
|
+
console.print(
|
|
250
|
+
f" L Rate Limit Tier: {profile.organization.rate_limit_tier}"
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
console.print(" L Organization: [dim]Not available[/dim]")
|
|
254
|
+
|
|
255
|
+
if profile.account:
|
|
256
|
+
console.print(f" L Email: {profile.account.email}")
|
|
257
|
+
if profile.account.full_name:
|
|
258
|
+
console.print(f" L Full Name: {profile.account.full_name}")
|
|
259
|
+
if profile.account.display_name:
|
|
260
|
+
console.print(f" L Display Name: {profile.account.display_name}")
|
|
261
|
+
console.print(
|
|
262
|
+
f" L Has Claude Pro: {'Yes' if profile.account.has_claude_pro else 'No'}"
|
|
263
|
+
)
|
|
264
|
+
console.print(
|
|
265
|
+
f" L Has Claude Max: {'Yes' if profile.account.has_claude_max else 'No'}"
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
console.print(" L Email: [dim]Not available[/dim]")
|
|
269
|
+
else:
|
|
270
|
+
# No saved profile, try to fetch fresh data
|
|
271
|
+
try:
|
|
272
|
+
# First try to get a valid access token (with refresh if needed)
|
|
273
|
+
valid_token = asyncio.run(manager.get_access_token())
|
|
274
|
+
if valid_token:
|
|
275
|
+
profile = asyncio.run(manager.fetch_user_profile())
|
|
276
|
+
if profile:
|
|
277
|
+
# Save the profile for future use
|
|
278
|
+
asyncio.run(manager._save_account_profile(profile))
|
|
279
|
+
|
|
280
|
+
if profile.organization:
|
|
281
|
+
console.print(
|
|
282
|
+
f" L Organization: {profile.organization.name}"
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
console.print(
|
|
286
|
+
" L Organization: [dim]Unable to fetch[/dim]"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if profile.account:
|
|
290
|
+
console.print(f" L Email: {profile.account.email}")
|
|
291
|
+
else:
|
|
292
|
+
console.print(" L Email: [dim]Unable to fetch[/dim]")
|
|
293
|
+
else:
|
|
294
|
+
console.print(" L Organization: [dim]Unable to fetch[/dim]")
|
|
295
|
+
console.print(" L Email: [dim]Unable to fetch[/dim]")
|
|
296
|
+
|
|
297
|
+
# Reload credentials after potential refresh to show updated token info
|
|
298
|
+
credentials = asyncio.run(manager.load())
|
|
299
|
+
if credentials:
|
|
300
|
+
oauth = credentials.claude_ai_oauth
|
|
301
|
+
else:
|
|
302
|
+
console.print(" L Organization: [dim]Token refresh failed[/dim]")
|
|
303
|
+
console.print(" L Email: [dim]Token refresh failed[/dim]")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.debug(f"Could not fetch user profile: {e}")
|
|
306
|
+
console.print(" L Organization: [dim]Unable to fetch[/dim]")
|
|
307
|
+
console.print(" L Email: [dim]Unable to fetch[/dim]")
|
|
308
|
+
|
|
309
|
+
# Create details table
|
|
310
|
+
console.print()
|
|
311
|
+
table = Table(
|
|
312
|
+
show_header=True,
|
|
313
|
+
header_style="bold cyan",
|
|
314
|
+
box=box.ROUNDED,
|
|
315
|
+
title="Credential Details",
|
|
316
|
+
title_style="bold white",
|
|
317
|
+
)
|
|
318
|
+
table.add_column("Property", style="cyan")
|
|
319
|
+
table.add_column("Value", style="white")
|
|
320
|
+
|
|
321
|
+
# File location - check if there's a credentials file or if using keyring
|
|
322
|
+
cred_file = asyncio.run(manager.find_credentials_file())
|
|
323
|
+
if cred_file:
|
|
324
|
+
table.add_row("File Location", str(cred_file))
|
|
325
|
+
else:
|
|
326
|
+
table.add_row("File Location", "Keyring storage")
|
|
327
|
+
|
|
328
|
+
# Token info
|
|
329
|
+
table.add_row("Subscription Type", oauth.subscription_type or "Unknown")
|
|
330
|
+
table.add_row(
|
|
331
|
+
"Token Expired",
|
|
332
|
+
"[red]Yes[/red]" if oauth.is_expired else "[green]No[/green]",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Expiration details
|
|
336
|
+
exp_dt = oauth.expires_at_datetime
|
|
337
|
+
table.add_row("Expires At", exp_dt.strftime("%Y-%m-%d %H:%M:%S UTC"))
|
|
338
|
+
|
|
339
|
+
# Time until expiration
|
|
340
|
+
now = datetime.now(UTC)
|
|
341
|
+
time_diff = exp_dt - now
|
|
342
|
+
if time_diff.total_seconds() > 0:
|
|
343
|
+
days = time_diff.days
|
|
344
|
+
hours = (time_diff.seconds % 86400) // 3600
|
|
345
|
+
minutes = (time_diff.seconds % 3600) // 60
|
|
346
|
+
table.add_row(
|
|
347
|
+
"Time Remaining", f"{days} days, {hours} hours, {minutes} minutes"
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
table.add_row("Time Remaining", "[red]Expired[/red]")
|
|
351
|
+
|
|
352
|
+
# Scopes
|
|
353
|
+
if oauth.scopes:
|
|
354
|
+
table.add_row("OAuth Scopes", ", ".join(oauth.scopes))
|
|
355
|
+
|
|
356
|
+
# Token preview (first and last 8 chars)
|
|
357
|
+
if oauth.access_token:
|
|
358
|
+
token_preview = f"{oauth.access_token[:8]}...{oauth.access_token[-8:]}"
|
|
359
|
+
table.add_row("Access Token", f"[dim]{token_preview}[/dim]")
|
|
360
|
+
|
|
361
|
+
# Account profile status
|
|
362
|
+
account_profile_exists = profile is not None
|
|
363
|
+
table.add_row(
|
|
364
|
+
"Account Profile",
|
|
365
|
+
"[green]Available[/green]"
|
|
366
|
+
if account_profile_exists
|
|
367
|
+
else "[yellow]Not saved[/yellow]",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
console.print(table)
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
toolkit.print(f"Error getting credential info: {e}", tag="error")
|
|
374
|
+
raise typer.Exit(1) from e
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@app.command(name="login")
|
|
378
|
+
def login_command(
|
|
379
|
+
docker: bool = typer.Option(
|
|
380
|
+
False,
|
|
381
|
+
"--docker",
|
|
382
|
+
help="Use Docker credential paths (from get_claude_docker_home_dir())",
|
|
383
|
+
),
|
|
384
|
+
credential_file: str | None = typer.Option(
|
|
385
|
+
None,
|
|
386
|
+
"--credential-file",
|
|
387
|
+
help="Path to specific credential file to save to",
|
|
388
|
+
),
|
|
389
|
+
) -> None:
|
|
390
|
+
"""Login to Claude using OAuth authentication.
|
|
391
|
+
|
|
392
|
+
This command will open your web browser to authenticate with Claude
|
|
393
|
+
and save the credentials locally.
|
|
394
|
+
|
|
395
|
+
Examples:
|
|
396
|
+
ccproxy auth login
|
|
397
|
+
ccproxy auth login --docker
|
|
398
|
+
ccproxy auth login --credential-file /path/to/credentials.json
|
|
399
|
+
"""
|
|
400
|
+
toolkit = get_rich_toolkit()
|
|
401
|
+
toolkit.print("[bold cyan]Claude OAuth Login[/bold cyan]", centered=True)
|
|
402
|
+
toolkit.print_line()
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
# Get credential paths based on options
|
|
406
|
+
custom_paths = None
|
|
407
|
+
if credential_file:
|
|
408
|
+
custom_paths = [Path(credential_file)]
|
|
409
|
+
elif docker:
|
|
410
|
+
custom_paths = get_docker_credential_paths()
|
|
411
|
+
|
|
412
|
+
# Check if already logged in
|
|
413
|
+
manager = get_credentials_manager(custom_paths)
|
|
414
|
+
validation_result = asyncio.run(manager.validate())
|
|
415
|
+
if validation_result.valid and not validation_result.expired:
|
|
416
|
+
console.print(
|
|
417
|
+
"[yellow]You are already logged in with valid credentials.[/yellow]"
|
|
418
|
+
)
|
|
419
|
+
console.print(
|
|
420
|
+
"Use [cyan]ccproxy auth info[/cyan] to view current credentials."
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
overwrite = typer.confirm(
|
|
424
|
+
"Do you want to login again and overwrite existing credentials?"
|
|
425
|
+
)
|
|
426
|
+
if not overwrite:
|
|
427
|
+
console.print("Login cancelled.")
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
# Perform OAuth login
|
|
431
|
+
console.print("Starting OAuth login process...")
|
|
432
|
+
console.print("Your browser will open for authentication.")
|
|
433
|
+
console.print(
|
|
434
|
+
"A temporary server will start on port 54545 for the OAuth callback..."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
asyncio.run(manager.login())
|
|
439
|
+
success = True
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.error(f"Login failed: {e}")
|
|
442
|
+
success = False
|
|
443
|
+
|
|
444
|
+
if success:
|
|
445
|
+
toolkit.print("Successfully logged in to Claude!", tag="success")
|
|
446
|
+
|
|
447
|
+
# Show credential info
|
|
448
|
+
console.print("\n[dim]Credential information:[/dim]")
|
|
449
|
+
updated_validation = asyncio.run(manager.validate())
|
|
450
|
+
if updated_validation.valid and updated_validation.credentials:
|
|
451
|
+
oauth_token = updated_validation.credentials.claude_ai_oauth
|
|
452
|
+
console.print(
|
|
453
|
+
f" Subscription: {oauth_token.subscription_type or 'Unknown'}"
|
|
454
|
+
)
|
|
455
|
+
if oauth_token.scopes:
|
|
456
|
+
console.print(f" Scopes: {', '.join(oauth_token.scopes)}")
|
|
457
|
+
exp_dt = oauth_token.expires_at_datetime
|
|
458
|
+
console.print(f" Expires: {exp_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
459
|
+
else:
|
|
460
|
+
toolkit.print("Login failed. Please try again.", tag="error")
|
|
461
|
+
raise typer.Exit(1)
|
|
462
|
+
|
|
463
|
+
except KeyboardInterrupt:
|
|
464
|
+
console.print("\n[yellow]Login cancelled by user.[/yellow]")
|
|
465
|
+
raise typer.Exit(1) from None
|
|
466
|
+
except Exception as e:
|
|
467
|
+
toolkit.print(f"Error during login: {e}", tag="error")
|
|
468
|
+
raise typer.Exit(1) from e
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@app.command()
|
|
472
|
+
def renew(
|
|
473
|
+
docker: bool = typer.Option(
|
|
474
|
+
False,
|
|
475
|
+
"--docker",
|
|
476
|
+
"-d",
|
|
477
|
+
help="Renew credentials for Docker environment",
|
|
478
|
+
),
|
|
479
|
+
credential_file: Path | None = typer.Option(
|
|
480
|
+
None,
|
|
481
|
+
"--credential-file",
|
|
482
|
+
"-f",
|
|
483
|
+
help="Path to custom credential file",
|
|
484
|
+
),
|
|
485
|
+
) -> None:
|
|
486
|
+
"""Force renew Claude credentials without checking expiration.
|
|
487
|
+
|
|
488
|
+
This command will refresh your access token regardless of whether it's expired.
|
|
489
|
+
Useful for testing or when you want to ensure you have the latest token.
|
|
490
|
+
|
|
491
|
+
Examples:
|
|
492
|
+
ccproxy auth renew
|
|
493
|
+
ccproxy auth renew --docker
|
|
494
|
+
ccproxy auth renew --credential-file /path/to/credentials.json
|
|
495
|
+
"""
|
|
496
|
+
toolkit = get_rich_toolkit()
|
|
497
|
+
toolkit.print("[bold cyan]Claude Credentials Renewal[/bold cyan]", centered=True)
|
|
498
|
+
toolkit.print_line()
|
|
499
|
+
|
|
500
|
+
console = Console()
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
# Get credential paths based on options
|
|
504
|
+
custom_paths = None
|
|
505
|
+
if credential_file:
|
|
506
|
+
custom_paths = [Path(credential_file)]
|
|
507
|
+
elif docker:
|
|
508
|
+
custom_paths = get_docker_credential_paths()
|
|
509
|
+
|
|
510
|
+
# Create credentials manager
|
|
511
|
+
manager = get_credentials_manager(custom_paths)
|
|
512
|
+
|
|
513
|
+
# Check if credentials exist
|
|
514
|
+
validation_result = asyncio.run(manager.validate())
|
|
515
|
+
if not validation_result.valid:
|
|
516
|
+
toolkit.print("[red]✗[/red] No credentials found to renew", tag="error")
|
|
517
|
+
console.print("\n[dim]Please login first:[/dim]")
|
|
518
|
+
console.print("[cyan]ccproxy auth login[/cyan]")
|
|
519
|
+
raise typer.Exit(1)
|
|
520
|
+
|
|
521
|
+
# Force refresh the token
|
|
522
|
+
console.print("[yellow]Refreshing access token...[/yellow]")
|
|
523
|
+
refreshed_credentials = asyncio.run(manager.refresh_token())
|
|
524
|
+
|
|
525
|
+
if refreshed_credentials:
|
|
526
|
+
toolkit.print(
|
|
527
|
+
"[green]✓[/green] Successfully renewed credentials!", tag="success"
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Show updated credential info
|
|
531
|
+
oauth_token = refreshed_credentials.claude_ai_oauth
|
|
532
|
+
console.print("\n[dim]Updated credential information:[/dim]")
|
|
533
|
+
console.print(
|
|
534
|
+
f" Subscription: {oauth_token.subscription_type or 'Unknown'}"
|
|
535
|
+
)
|
|
536
|
+
if oauth_token.scopes:
|
|
537
|
+
console.print(f" Scopes: {', '.join(oauth_token.scopes)}")
|
|
538
|
+
exp_dt = oauth_token.expires_at_datetime
|
|
539
|
+
console.print(f" Expires: {exp_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
540
|
+
else:
|
|
541
|
+
toolkit.print("[red]✗[/red] Failed to renew credentials", tag="error")
|
|
542
|
+
raise typer.Exit(1)
|
|
543
|
+
|
|
544
|
+
except KeyboardInterrupt:
|
|
545
|
+
console.print("\n[yellow]Renewal cancelled by user.[/yellow]")
|
|
546
|
+
raise typer.Exit(1) from None
|
|
547
|
+
except Exception as e:
|
|
548
|
+
toolkit.print(f"Error during renewal: {e}", tag="error")
|
|
549
|
+
raise typer.Exit(1) from e
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
if __name__ == "__main__":
|
|
553
|
+
app()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Config command module for CCProxy API."""
|
|
2
|
+
|
|
3
|
+
from ccproxy.cli.commands.config.commands import app, config_list
|
|
4
|
+
from ccproxy.cli.commands.config.schema_commands import (
|
|
5
|
+
config_schema,
|
|
6
|
+
config_validate,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Register schema commands with the app
|
|
11
|
+
app.command(name="schema")(config_schema)
|
|
12
|
+
app.command(name="validate")(config_validate)
|
|
13
|
+
|
|
14
|
+
__all__ = ["app", "config_list"]
|