xenfra 0.4.3__py3-none-any.whl → 0.4.5__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.
- xenfra/blueprints/__init__.py +55 -0
- xenfra/blueprints/base.py +85 -0
- xenfra/blueprints/cache.py +286 -0
- xenfra/blueprints/dry_run.py +251 -0
- xenfra/blueprints/e2b.py +101 -0
- xenfra/blueprints/factory.py +113 -0
- xenfra/blueprints/railpack.py +319 -0
- xenfra/blueprints/validation.py +182 -0
- xenfra/commands/__init__.py +3 -3
- xenfra/commands/auth.py +144 -144
- xenfra/commands/auth_device.py +164 -164
- xenfra/commands/deployments.py +1358 -973
- xenfra/commands/intelligence.py +503 -412
- xenfra/commands/projects.py +204 -204
- xenfra/commands/security_cmd.py +233 -233
- xenfra/main.py +79 -75
- xenfra/utils/__init__.py +3 -3
- xenfra/utils/auth.py +374 -374
- xenfra/utils/codebase.py +169 -169
- xenfra/utils/config.py +459 -436
- xenfra/utils/errors.py +116 -116
- xenfra/utils/file_sync.py +286 -286
- xenfra/utils/security.py +336 -336
- xenfra/utils/validation.py +234 -234
- xenfra-0.4.5.dist-info/METADATA +113 -0
- xenfra-0.4.5.dist-info/RECORD +29 -0
- {xenfra-0.4.3.dist-info → xenfra-0.4.5.dist-info}/WHEEL +1 -1
- xenfra-0.4.3.dist-info/METADATA +0 -118
- xenfra-0.4.3.dist-info/RECORD +0 -21
- {xenfra-0.4.3.dist-info → xenfra-0.4.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tier 1: Local validation - Fast syntax and import checking.
|
|
3
|
+
|
|
4
|
+
This module provides lightning-fast (~100ms) validation of project files
|
|
5
|
+
before any expensive operations. It checks:
|
|
6
|
+
- Python syntax errors
|
|
7
|
+
- Import errors (missing dependencies)
|
|
8
|
+
- Config file validity
|
|
9
|
+
- Basic project structure
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ValidationResult:
|
|
22
|
+
"""Result of Tier 1 validation."""
|
|
23
|
+
success: bool
|
|
24
|
+
errors: list[str] = field(default_factory=list)
|
|
25
|
+
warnings: list[str] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
def add_error(self, message: str):
|
|
28
|
+
self.errors.append(message)
|
|
29
|
+
self.success = False
|
|
30
|
+
|
|
31
|
+
def add_warning(self, message: str):
|
|
32
|
+
self.warnings.append(message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LocalValidator:
|
|
36
|
+
"""Fast local validation for common project issues."""
|
|
37
|
+
|
|
38
|
+
def validate(self, project_path: Path) -> ValidationResult:
|
|
39
|
+
"""
|
|
40
|
+
Run all Tier 1 validations.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
project_path: Path to the project directory
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
ValidationResult with any errors or warnings
|
|
47
|
+
"""
|
|
48
|
+
result = ValidationResult(success=True)
|
|
49
|
+
|
|
50
|
+
# Check Python files for syntax errors
|
|
51
|
+
self._validate_python_syntax(project_path, result)
|
|
52
|
+
|
|
53
|
+
# Validate config files
|
|
54
|
+
self._validate_configs(project_path, result)
|
|
55
|
+
|
|
56
|
+
# Check for required files
|
|
57
|
+
self._validate_required_files(project_path, result)
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
def _validate_python_syntax(self, project_path: Path, result: ValidationResult):
|
|
62
|
+
"""Check all Python files for syntax errors."""
|
|
63
|
+
python_files = list(project_path.rglob("*.py"))
|
|
64
|
+
|
|
65
|
+
for py_file in python_files:
|
|
66
|
+
# Skip common virtual environment directories
|
|
67
|
+
if any(part.startswith(".") or part in ["venv", ".venv", "env", ".env", "__pycache__"]
|
|
68
|
+
for part in py_file.parts):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
content = py_file.read_text(encoding="utf-8")
|
|
73
|
+
ast.parse(content)
|
|
74
|
+
except SyntaxError as e:
|
|
75
|
+
result.add_error(
|
|
76
|
+
f"Syntax error in {py_file.relative_to(project_path)}:{e.lineno}: {e.msg}"
|
|
77
|
+
)
|
|
78
|
+
except UnicodeDecodeError:
|
|
79
|
+
result.add_warning(f"Could not read {py_file.relative_to(project_path)} (encoding issue)")
|
|
80
|
+
|
|
81
|
+
def _validate_configs(self, project_path: Path, result: ValidationResult):
|
|
82
|
+
"""Validate configuration files."""
|
|
83
|
+
# requirements.txt - check format
|
|
84
|
+
req_file = project_path / "requirements.txt"
|
|
85
|
+
if req_file.exists():
|
|
86
|
+
try:
|
|
87
|
+
content = req_file.read_text(encoding="utf-8")
|
|
88
|
+
for line_num, line in enumerate(content.split("\n"), 1):
|
|
89
|
+
line = line.strip()
|
|
90
|
+
if not line or line.startswith("#"):
|
|
91
|
+
continue
|
|
92
|
+
# Basic format check
|
|
93
|
+
if ";" in line and "sys_platform" not in line and "python_version" not in line:
|
|
94
|
+
# Might be an error, but could also be valid
|
|
95
|
+
pass
|
|
96
|
+
except Exception as e:
|
|
97
|
+
result.add_error(f"Error reading requirements.txt: {e}")
|
|
98
|
+
|
|
99
|
+
# package.json - validate JSON
|
|
100
|
+
pkg_file = project_path / "package.json"
|
|
101
|
+
if pkg_file.exists():
|
|
102
|
+
try:
|
|
103
|
+
content = pkg_file.read_text(encoding="utf-8")
|
|
104
|
+
json.loads(content)
|
|
105
|
+
except json.JSONDecodeError as e:
|
|
106
|
+
result.add_error(f"Invalid package.json: {e}")
|
|
107
|
+
|
|
108
|
+
# pyproject.toml - basic check
|
|
109
|
+
pyproject = project_path / "pyproject.toml"
|
|
110
|
+
if pyproject.exists():
|
|
111
|
+
try:
|
|
112
|
+
import tomllib
|
|
113
|
+
content = pyproject.read_bytes()
|
|
114
|
+
tomllib.loads(content.decode("utf-8"))
|
|
115
|
+
except ImportError:
|
|
116
|
+
# Python < 3.11, try toml library
|
|
117
|
+
try:
|
|
118
|
+
import toml
|
|
119
|
+
toml.load(pyproject)
|
|
120
|
+
except ImportError:
|
|
121
|
+
pass # Skip if no toml library
|
|
122
|
+
except Exception as e:
|
|
123
|
+
result.add_error(f"Invalid pyproject.toml: {e}")
|
|
124
|
+
except Exception as e:
|
|
125
|
+
result.add_error(f"Invalid pyproject.toml: {e}")
|
|
126
|
+
|
|
127
|
+
def _validate_required_files(self, project_path: Path, result: ValidationResult):
|
|
128
|
+
"""Check for required files based on detected project type."""
|
|
129
|
+
# Detect project type
|
|
130
|
+
has_requirements = (project_path / "requirements.txt").exists()
|
|
131
|
+
has_pyproject = (project_path / "pyproject.toml").exists()
|
|
132
|
+
has_package_json = (project_path / "package.json").exists()
|
|
133
|
+
has_go_mod = (project_path / "go.mod").exists()
|
|
134
|
+
has_cargo = (project_path / "Cargo.toml").exists()
|
|
135
|
+
|
|
136
|
+
# Check for main entry point (Python)
|
|
137
|
+
if has_requirements or has_pyproject:
|
|
138
|
+
main_files = [
|
|
139
|
+
project_path / "main.py",
|
|
140
|
+
project_path / "app.py",
|
|
141
|
+
project_path / "manage.py", # Django
|
|
142
|
+
]
|
|
143
|
+
app_dirs = [
|
|
144
|
+
project_path / "app",
|
|
145
|
+
project_path / project_path.name.lower().replace("-", "_").replace(" ", "_"),
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if not any(f.exists() for f in main_files):
|
|
149
|
+
# Check if there's a package directory with __main__.py or similar
|
|
150
|
+
has_entry = False
|
|
151
|
+
for app_dir in app_dirs:
|
|
152
|
+
if app_dir.is_dir():
|
|
153
|
+
if (app_dir / "__main__.py").exists() or (app_dir / "main.py").exists():
|
|
154
|
+
has_entry = True
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
if not has_entry:
|
|
158
|
+
result.add_warning(
|
|
159
|
+
"No clear Python entry point found (main.py, app.py, or package/__main__.py)"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Check for main entry point (Node.js)
|
|
163
|
+
if has_package_json:
|
|
164
|
+
pkg_json = json.loads((project_path / "package.json").read_text())
|
|
165
|
+
if "main" not in pkg_json and "start" not in str(pkg_json.get("scripts", {})):
|
|
166
|
+
result.add_warning(
|
|
167
|
+
"No 'main' field or 'start' script found in package.json"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def validate_project(project_path: Path) -> ValidationResult:
|
|
172
|
+
"""
|
|
173
|
+
Convenience function for Tier 1 validation.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
project_path: Path to the project directory
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
ValidationResult with errors and warnings
|
|
180
|
+
"""
|
|
181
|
+
validator = LocalValidator()
|
|
182
|
+
return validator.validate(project_path)
|
xenfra/commands/__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
CLI command modules for Xenfra.
|
|
3
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
CLI command modules for Xenfra.
|
|
3
|
+
"""
|
xenfra/commands/auth.py
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Authentication commands for Xenfra CLI.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import base64
|
|
6
|
-
import hashlib
|
|
7
|
-
import secrets
|
|
8
|
-
import urllib.parse
|
|
9
|
-
import webbrowser
|
|
10
|
-
from http.server import HTTPServer
|
|
11
|
-
|
|
12
|
-
import click
|
|
13
|
-
import httpx
|
|
14
|
-
import keyring
|
|
15
|
-
from rich.console import Console
|
|
16
|
-
from tenacity import (
|
|
17
|
-
retry,
|
|
18
|
-
retry_if_exception_type,
|
|
19
|
-
stop_after_attempt,
|
|
20
|
-
wait_exponential,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
from ..utils.auth import (
|
|
24
|
-
API_BASE_URL,
|
|
25
|
-
CLI_CLIENT_ID,
|
|
26
|
-
CLI_LOCAL_SERVER_END_PORT,
|
|
27
|
-
CLI_LOCAL_SERVER_START_PORT,
|
|
28
|
-
CLI_REDIRECT_PATH,
|
|
29
|
-
SERVICE_ID,
|
|
30
|
-
AuthCallbackHandler,
|
|
31
|
-
clear_tokens,
|
|
32
|
-
get_auth_token,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
console = Console()
|
|
36
|
-
|
|
37
|
-
# HTTP request timeout (30 seconds)
|
|
38
|
-
HTTP_TIMEOUT = 30.0
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@click.group()
|
|
42
|
-
def auth():
|
|
43
|
-
"""Authentication commands (login, logout, whoami)."""
|
|
44
|
-
pass
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@retry(
|
|
48
|
-
stop=stop_after_attempt(3),
|
|
49
|
-
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
50
|
-
retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
|
|
51
|
-
reraise=True,
|
|
52
|
-
)
|
|
53
|
-
def _exchange_code_for_tokens_with_retry(code: str, code_verifier: str, redirect_uri: str) -> dict:
|
|
54
|
-
"""
|
|
55
|
-
Exchange authorization code for tokens with retry logic.
|
|
56
|
-
|
|
57
|
-
Returns token data dictionary.
|
|
58
|
-
"""
|
|
59
|
-
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
60
|
-
response = client.post(
|
|
61
|
-
f"{API_BASE_URL}/auth/token",
|
|
62
|
-
data={
|
|
63
|
-
"grant_type": "authorization_code",
|
|
64
|
-
"client_id": CLI_CLIENT_ID,
|
|
65
|
-
"code": code,
|
|
66
|
-
"code_verifier": code_verifier,
|
|
67
|
-
"redirect_uri": redirect_uri,
|
|
68
|
-
},
|
|
69
|
-
headers={"Accept": "application/json"},
|
|
70
|
-
)
|
|
71
|
-
response.raise_for_status()
|
|
72
|
-
|
|
73
|
-
# Safe JSON parsing with content-type check
|
|
74
|
-
content_type = response.headers.get("content-type", "")
|
|
75
|
-
if "application/json" not in content_type:
|
|
76
|
-
raise ValueError(f"Expected JSON response, got {content_type}")
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
token_data = response.json()
|
|
80
|
-
except (ValueError, TypeError) as e:
|
|
81
|
-
raise ValueError(f"Failed to parse JSON response: {e}")
|
|
82
|
-
|
|
83
|
-
return token_data
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@auth.command()
|
|
87
|
-
def login():
|
|
88
|
-
"""Login to Xenfra using Device Authorization Flow (like GitHub CLI, Claude Code)."""
|
|
89
|
-
from .auth_device import device_login
|
|
90
|
-
device_login()
|
|
91
|
-
|
|
92
|
-
# Removed old PKCE flow - now using Device Authorization Flow
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@auth.command()
|
|
96
|
-
def logout():
|
|
97
|
-
"""Logout and clear stored tokens."""
|
|
98
|
-
try:
|
|
99
|
-
clear_tokens()
|
|
100
|
-
console.print("[bold green]Logged out successfully.[/bold green]")
|
|
101
|
-
except Exception as e:
|
|
102
|
-
console.print(f"[yellow]Warning: Error during logout: {e}[/yellow]")
|
|
103
|
-
console.print("[dim]Tokens may still be stored in keyring.[/dim]")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@auth.command()
|
|
107
|
-
@click.option("--token", is_flag=True, help="Show access token")
|
|
108
|
-
def whoami(token):
|
|
109
|
-
"""Show current authenticated user."""
|
|
110
|
-
access_token = get_auth_token()
|
|
111
|
-
|
|
112
|
-
if not access_token:
|
|
113
|
-
console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
import base64
|
|
118
|
-
import json
|
|
119
|
-
|
|
120
|
-
# Manually decode JWT payload without verification
|
|
121
|
-
# JWT format: header.payload.signature
|
|
122
|
-
parts = access_token.split(".")
|
|
123
|
-
if len(parts) != 3:
|
|
124
|
-
console.print("[bold red]Invalid token format[/bold red]")
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
# Decode payload (second part)
|
|
128
|
-
payload_b64 = parts[1]
|
|
129
|
-
# Add padding if needed
|
|
130
|
-
padding = 4 - len(payload_b64) % 4
|
|
131
|
-
if padding != 4:
|
|
132
|
-
payload_b64 += "=" * padding
|
|
133
|
-
|
|
134
|
-
payload_bytes = base64.urlsafe_b64decode(payload_b64)
|
|
135
|
-
claims = json.loads(payload_bytes)
|
|
136
|
-
|
|
137
|
-
console.print("[bold green]Logged in as:[/bold green]")
|
|
138
|
-
console.print(f" Email: {claims.get('sub', 'N/A')}")
|
|
139
|
-
console.print(f" User ID: {claims.get('user_id', 'N/A')}")
|
|
140
|
-
|
|
141
|
-
if token:
|
|
142
|
-
console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
|
|
143
|
-
except Exception as e:
|
|
144
|
-
console.print(f"[bold red]Failed to decode token: {e}[/bold red]")
|
|
1
|
+
"""
|
|
2
|
+
Authentication commands for Xenfra CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import secrets
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import webbrowser
|
|
10
|
+
from http.server import HTTPServer
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import httpx
|
|
14
|
+
import keyring
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from tenacity import (
|
|
17
|
+
retry,
|
|
18
|
+
retry_if_exception_type,
|
|
19
|
+
stop_after_attempt,
|
|
20
|
+
wait_exponential,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ..utils.auth import (
|
|
24
|
+
API_BASE_URL,
|
|
25
|
+
CLI_CLIENT_ID,
|
|
26
|
+
CLI_LOCAL_SERVER_END_PORT,
|
|
27
|
+
CLI_LOCAL_SERVER_START_PORT,
|
|
28
|
+
CLI_REDIRECT_PATH,
|
|
29
|
+
SERVICE_ID,
|
|
30
|
+
AuthCallbackHandler,
|
|
31
|
+
clear_tokens,
|
|
32
|
+
get_auth_token,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
# HTTP request timeout (30 seconds)
|
|
38
|
+
HTTP_TIMEOUT = 30.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.group()
|
|
42
|
+
def auth():
|
|
43
|
+
"""Authentication commands (login, logout, whoami)."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@retry(
|
|
48
|
+
stop=stop_after_attempt(3),
|
|
49
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
50
|
+
retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
|
|
51
|
+
reraise=True,
|
|
52
|
+
)
|
|
53
|
+
def _exchange_code_for_tokens_with_retry(code: str, code_verifier: str, redirect_uri: str) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
Exchange authorization code for tokens with retry logic.
|
|
56
|
+
|
|
57
|
+
Returns token data dictionary.
|
|
58
|
+
"""
|
|
59
|
+
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
60
|
+
response = client.post(
|
|
61
|
+
f"{API_BASE_URL}/auth/token",
|
|
62
|
+
data={
|
|
63
|
+
"grant_type": "authorization_code",
|
|
64
|
+
"client_id": CLI_CLIENT_ID,
|
|
65
|
+
"code": code,
|
|
66
|
+
"code_verifier": code_verifier,
|
|
67
|
+
"redirect_uri": redirect_uri,
|
|
68
|
+
},
|
|
69
|
+
headers={"Accept": "application/json"},
|
|
70
|
+
)
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
|
|
73
|
+
# Safe JSON parsing with content-type check
|
|
74
|
+
content_type = response.headers.get("content-type", "")
|
|
75
|
+
if "application/json" not in content_type:
|
|
76
|
+
raise ValueError(f"Expected JSON response, got {content_type}")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
token_data = response.json()
|
|
80
|
+
except (ValueError, TypeError) as e:
|
|
81
|
+
raise ValueError(f"Failed to parse JSON response: {e}")
|
|
82
|
+
|
|
83
|
+
return token_data
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@auth.command()
|
|
87
|
+
def login():
|
|
88
|
+
"""Login to Xenfra using Device Authorization Flow (like GitHub CLI, Claude Code)."""
|
|
89
|
+
from .auth_device import device_login
|
|
90
|
+
device_login()
|
|
91
|
+
|
|
92
|
+
# Removed old PKCE flow - now using Device Authorization Flow
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@auth.command()
|
|
96
|
+
def logout():
|
|
97
|
+
"""Logout and clear stored tokens."""
|
|
98
|
+
try:
|
|
99
|
+
clear_tokens()
|
|
100
|
+
console.print("[bold green]Logged out successfully.[/bold green]")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
console.print(f"[yellow]Warning: Error during logout: {e}[/yellow]")
|
|
103
|
+
console.print("[dim]Tokens may still be stored in keyring.[/dim]")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@auth.command()
|
|
107
|
+
@click.option("--token", is_flag=True, help="Show access token")
|
|
108
|
+
def whoami(token):
|
|
109
|
+
"""Show current authenticated user."""
|
|
110
|
+
access_token = get_auth_token()
|
|
111
|
+
|
|
112
|
+
if not access_token:
|
|
113
|
+
console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
import base64
|
|
118
|
+
import json
|
|
119
|
+
|
|
120
|
+
# Manually decode JWT payload without verification
|
|
121
|
+
# JWT format: header.payload.signature
|
|
122
|
+
parts = access_token.split(".")
|
|
123
|
+
if len(parts) != 3:
|
|
124
|
+
console.print("[bold red]Invalid token format[/bold red]")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Decode payload (second part)
|
|
128
|
+
payload_b64 = parts[1]
|
|
129
|
+
# Add padding if needed
|
|
130
|
+
padding = 4 - len(payload_b64) % 4
|
|
131
|
+
if padding != 4:
|
|
132
|
+
payload_b64 += "=" * padding
|
|
133
|
+
|
|
134
|
+
payload_bytes = base64.urlsafe_b64decode(payload_b64)
|
|
135
|
+
claims = json.loads(payload_bytes)
|
|
136
|
+
|
|
137
|
+
console.print("[bold green]Logged in as:[/bold green]")
|
|
138
|
+
console.print(f" Email: {claims.get('sub', 'N/A')}")
|
|
139
|
+
console.print(f" User ID: {claims.get('user_id', 'N/A')}")
|
|
140
|
+
|
|
141
|
+
if token:
|
|
142
|
+
console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
console.print(f"[bold red]Failed to decode token: {e}[/bold red]")
|