xenfra 0.2.4__tar.gz → 0.2.5__tar.gz
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-0.2.4 → xenfra-0.2.5}/PKG-INFO +26 -25
- {xenfra-0.2.4 → xenfra-0.2.5}/README.md +24 -24
- {xenfra-0.2.4 → xenfra-0.2.5}/pyproject.toml +3 -2
- xenfra-0.2.5/src/xenfra/commands/auth.py +289 -0
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/commands/deployments.py +42 -1
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/commands/intelligence.py +59 -28
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/commands/projects.py +54 -13
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/commands/security_cmd.py +16 -18
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/main.py +1 -0
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/utils/auth.py +99 -21
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/utils/codebase.py +53 -15
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/utils/config.py +144 -66
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/utils/security.py +27 -21
- xenfra-0.2.5/src/xenfra/utils/validation.py +229 -0
- xenfra-0.2.4/src/xenfra/commands/auth.py +0 -186
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/__init__.py +0 -0
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/commands/__init__.py +0 -0
- {xenfra-0.2.4 → xenfra-0.2.5}/src/xenfra/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: xenfra
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: A 'Zen Mode' infrastructure engine for Python developers.
|
|
5
5
|
Author: xenfra-cloud
|
|
6
6
|
Author-email: xenfra-cloud <xenfracloud@gmail.com>
|
|
@@ -22,6 +22,7 @@ Requires-Dist: xenfra-sdk
|
|
|
22
22
|
Requires-Dist: httpx>=0.27.0
|
|
23
23
|
Requires-Dist: keyring>=25.7.0
|
|
24
24
|
Requires-Dist: keyrings-alt>=5.0.2
|
|
25
|
+
Requires-Dist: tenacity>=8.2.3
|
|
25
26
|
Requires-Dist: pytest>=8.0.0 ; extra == 'test'
|
|
26
27
|
Requires-Dist: pytest-mock>=3.12.0 ; extra == 'test'
|
|
27
28
|
Requires-Python: >=3.13
|
|
@@ -38,11 +39,11 @@ The Xenfra CLI is a powerful and intuitive command-line interface designed to st
|
|
|
38
39
|
|
|
39
40
|
### ✨ Key Features
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
- **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
|
|
43
|
+
- **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
|
|
44
|
+
- **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
|
|
45
|
+
- **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
|
|
46
|
+
- **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
|
|
46
47
|
|
|
47
48
|
### 🚀 Quickstart
|
|
48
49
|
|
|
@@ -83,28 +84,28 @@ xenfra deploy
|
|
|
83
84
|
|
|
84
85
|
### 📋 Usage Examples
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
87
|
+
- **Monitor Deployment Status:**
|
|
88
|
+
```bash
|
|
89
|
+
xenfra status <deployment-id>
|
|
90
|
+
```
|
|
91
|
+
- **Stream Application Logs:**
|
|
92
|
+
```bash
|
|
93
|
+
xenfra logs <deployment-id>
|
|
94
|
+
```
|
|
95
|
+
- **List Deployed Projects:**
|
|
96
|
+
```bash
|
|
97
|
+
xenfra projects list
|
|
98
|
+
```
|
|
99
|
+
- **Diagnose a Failed Deployment (AI-Powered):**
|
|
100
|
+
```bash
|
|
101
|
+
xenfra diagnose <deployment-id>
|
|
102
|
+
# Or to diagnose from a log file:
|
|
103
|
+
xenfra diagnose --logs error.log
|
|
104
|
+
```
|
|
104
105
|
|
|
105
106
|
### 📚 Documentation
|
|
106
107
|
|
|
107
|
-
For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.
|
|
108
|
+
For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.tech/cli) (Link will be updated upon final deployment).
|
|
108
109
|
|
|
109
110
|
### 🤝 Contributing
|
|
110
111
|
|
|
@@ -6,11 +6,11 @@ The Xenfra CLI is a powerful and intuitive command-line interface designed to st
|
|
|
6
6
|
|
|
7
7
|
### ✨ Key Features
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
- **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
|
|
10
|
+
- **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
|
|
11
|
+
- **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
|
|
12
|
+
- **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
|
|
13
|
+
- **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
|
|
14
14
|
|
|
15
15
|
### 🚀 Quickstart
|
|
16
16
|
|
|
@@ -51,28 +51,28 @@ xenfra deploy
|
|
|
51
51
|
|
|
52
52
|
### 📋 Usage Examples
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
54
|
+
- **Monitor Deployment Status:**
|
|
55
|
+
```bash
|
|
56
|
+
xenfra status <deployment-id>
|
|
57
|
+
```
|
|
58
|
+
- **Stream Application Logs:**
|
|
59
|
+
```bash
|
|
60
|
+
xenfra logs <deployment-id>
|
|
61
|
+
```
|
|
62
|
+
- **List Deployed Projects:**
|
|
63
|
+
```bash
|
|
64
|
+
xenfra projects list
|
|
65
|
+
```
|
|
66
|
+
- **Diagnose a Failed Deployment (AI-Powered):**
|
|
67
|
+
```bash
|
|
68
|
+
xenfra diagnose <deployment-id>
|
|
69
|
+
# Or to diagnose from a log file:
|
|
70
|
+
xenfra diagnose --logs error.log
|
|
71
|
+
```
|
|
72
72
|
|
|
73
73
|
### 📚 Documentation
|
|
74
74
|
|
|
75
|
-
For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.
|
|
75
|
+
For more detailed information, advanced configurations, and API references, please refer to the [official Xenfra Documentation](https://docs.xenfra.tech/cli) (Link will be updated upon final deployment).
|
|
76
76
|
|
|
77
77
|
### 🤝 Contributing
|
|
78
78
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xenfra"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.5"
|
|
4
4
|
description = "A 'Zen Mode' infrastructure engine for Python developers."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
"httpx>=0.27.0",
|
|
30
30
|
"keyring>=25.7.0",
|
|
31
31
|
"keyrings.alt>=5.0.2",
|
|
32
|
+
"tenacity>=8.2.3", # For retry logic
|
|
32
33
|
]
|
|
33
34
|
requires-python = ">=3.13"
|
|
34
35
|
|
|
@@ -50,4 +51,4 @@ xenfra = "xenfra.main:main"
|
|
|
50
51
|
|
|
51
52
|
[build-system]
|
|
52
53
|
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
53
|
-
build-backend = "uv_build"
|
|
54
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
stop_after_attempt,
|
|
19
|
+
wait_exponential,
|
|
20
|
+
retry_if_exception_type,
|
|
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
|
+
@retry(
|
|
42
|
+
stop=stop_after_attempt(3),
|
|
43
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
44
|
+
retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
|
|
45
|
+
reraise=True,
|
|
46
|
+
)
|
|
47
|
+
def _exchange_code_for_tokens_with_retry(
|
|
48
|
+
code: str, code_verifier: str, redirect_uri: str
|
|
49
|
+
) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
Exchange authorization code for tokens with retry logic.
|
|
52
|
+
|
|
53
|
+
Returns token data dictionary.
|
|
54
|
+
"""
|
|
55
|
+
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
56
|
+
response = client.post(
|
|
57
|
+
f"{API_BASE_URL}/auth/token",
|
|
58
|
+
data={
|
|
59
|
+
"grant_type": "authorization_code",
|
|
60
|
+
"client_id": CLI_CLIENT_ID,
|
|
61
|
+
"code": code,
|
|
62
|
+
"code_verifier": code_verifier,
|
|
63
|
+
"redirect_uri": redirect_uri,
|
|
64
|
+
},
|
|
65
|
+
headers={"Accept": "application/json"},
|
|
66
|
+
)
|
|
67
|
+
response.raise_for_status()
|
|
68
|
+
|
|
69
|
+
# Safe JSON parsing with content-type check
|
|
70
|
+
content_type = response.headers.get("content-type", "")
|
|
71
|
+
if "application/json" not in content_type:
|
|
72
|
+
raise ValueError(f"Expected JSON response, got {content_type}")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
token_data = response.json()
|
|
76
|
+
except (ValueError, TypeError) as e:
|
|
77
|
+
raise ValueError(f"Failed to parse JSON response: {e}")
|
|
78
|
+
|
|
79
|
+
return token_data
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@auth.command()
|
|
83
|
+
def login():
|
|
84
|
+
"""Login to Xenfra using OAuth2 PKCE flow."""
|
|
85
|
+
global oauth_data
|
|
86
|
+
oauth_data = {"code": None, "state": None, "error": None}
|
|
87
|
+
|
|
88
|
+
# 1. Generate PKCE parameters
|
|
89
|
+
code_verifier = secrets.token_urlsafe(96)
|
|
90
|
+
code_challenge = (
|
|
91
|
+
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
|
|
92
|
+
.decode()
|
|
93
|
+
.rstrip("=")
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# 2. Generate state for CSRF protection
|
|
97
|
+
state = secrets.token_urlsafe(32)
|
|
98
|
+
|
|
99
|
+
# 3. Start local HTTP server
|
|
100
|
+
server_port = None
|
|
101
|
+
httpd_instance = None
|
|
102
|
+
try:
|
|
103
|
+
for port in range(CLI_LOCAL_SERVER_START_PORT, CLI_LOCAL_SERVER_END_PORT + 1):
|
|
104
|
+
try:
|
|
105
|
+
server_address = ("127.0.0.1", port)
|
|
106
|
+
httpd_instance = HTTPServer(server_address, AuthCallbackHandler)
|
|
107
|
+
server_port = port
|
|
108
|
+
break
|
|
109
|
+
except OSError:
|
|
110
|
+
continue
|
|
111
|
+
except Exception as e:
|
|
112
|
+
console.print(f"[yellow]Warning: Failed to bind to port {port}: {e}[/yellow]")
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if not server_port:
|
|
116
|
+
console.print(
|
|
117
|
+
f"[bold red]Error: No available ports in range {CLI_LOCAL_SERVER_START_PORT}-{CLI_LOCAL_SERVER_END_PORT}[/bold red]"
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
redirect_uri = f"http://localhost:{server_port}{CLI_REDIRECT_PATH}"
|
|
122
|
+
|
|
123
|
+
# 4. Construct Authorization URL
|
|
124
|
+
auth_url = (
|
|
125
|
+
f"{API_BASE_URL}/auth/authorize?"
|
|
126
|
+
f"client_id={CLI_CLIENT_ID}&"
|
|
127
|
+
f"redirect_uri={urllib.parse.quote(redirect_uri)}&"
|
|
128
|
+
f"response_type=code&"
|
|
129
|
+
f"scope={urllib.parse.quote('openid profile')}&"
|
|
130
|
+
f"state={state}&"
|
|
131
|
+
f"code_challenge={code_challenge}&"
|
|
132
|
+
f"code_challenge_method=S256"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
console.print("[bold blue]Opening browser for login...[/bold blue]")
|
|
136
|
+
console.print(
|
|
137
|
+
f"[dim]If browser doesn't open, navigate to:[/dim]\n[link={auth_url}]{auth_url}[/link]"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Try to open browser, handle errors gracefully
|
|
141
|
+
try:
|
|
142
|
+
webbrowser.open(auth_url)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
console.print(
|
|
145
|
+
f"[yellow]Warning: Could not open browser automatically: {e}[/yellow]"
|
|
146
|
+
)
|
|
147
|
+
console.print(f"[dim]Please open the URL manually: {auth_url}[/dim]")
|
|
148
|
+
|
|
149
|
+
# 5. Run local server to capture redirect
|
|
150
|
+
try:
|
|
151
|
+
AuthCallbackHandler.server = httpd_instance # type: ignore
|
|
152
|
+
httpd_instance.handle_request() # type: ignore
|
|
153
|
+
console.print("[dim]Local OAuth server shut down.[/dim]")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
console.print(f"[bold red]Error running OAuth server: {e}[/bold red]")
|
|
156
|
+
return
|
|
157
|
+
finally:
|
|
158
|
+
# Ensure server is closed
|
|
159
|
+
if httpd_instance:
|
|
160
|
+
try:
|
|
161
|
+
httpd_instance.server_close()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
if oauth_data["error"]:
|
|
166
|
+
console.print(f"[bold red]Login failed: {oauth_data['error']}[/bold red]")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if not oauth_data["code"]:
|
|
170
|
+
console.print("[bold red]Login failed: No authorization code received.[/bold red]")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# 6. Verify state (CSRF protection)
|
|
174
|
+
if not oauth_data.get("state"):
|
|
175
|
+
console.print(
|
|
176
|
+
"[bold red]Login failed: State parameter missing in callback (possible CSRF attack)[/bold red]"
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
if oauth_data["state"] != state:
|
|
181
|
+
console.print(
|
|
182
|
+
"[bold red]Login failed: State mismatch (possible CSRF attack)[/bold red]"
|
|
183
|
+
)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# 7. Exchange code for tokens with retry logic
|
|
187
|
+
console.print("[bold cyan]Exchanging authorization code for tokens...[/bold cyan]")
|
|
188
|
+
try:
|
|
189
|
+
token_data = _exchange_code_for_tokens_with_retry(
|
|
190
|
+
oauth_data["code"], code_verifier, redirect_uri
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
access_token = token_data.get("access_token")
|
|
194
|
+
refresh_token = token_data.get("refresh_token")
|
|
195
|
+
|
|
196
|
+
if access_token and refresh_token:
|
|
197
|
+
try:
|
|
198
|
+
keyring.set_password(SERVICE_ID, "access_token", access_token)
|
|
199
|
+
keyring.set_password(SERVICE_ID, "refresh_token", refresh_token)
|
|
200
|
+
console.print("[bold green]Login successful! Tokens saved securely.[/bold green]")
|
|
201
|
+
except keyring.errors.KeyringError as e:
|
|
202
|
+
console.print(
|
|
203
|
+
f"[bold red]Failed to save tokens to keyring: {e}[/bold red]"
|
|
204
|
+
)
|
|
205
|
+
console.print("[yellow]Tokens were received but not saved.[/yellow]")
|
|
206
|
+
else:
|
|
207
|
+
console.print("[bold red]Login failed: No tokens received.[/bold red]")
|
|
208
|
+
|
|
209
|
+
except httpx.TimeoutException:
|
|
210
|
+
console.print(
|
|
211
|
+
"[bold red]Token exchange failed: Request timed out. Please try again.[/bold red]"
|
|
212
|
+
)
|
|
213
|
+
except httpx.NetworkError as e:
|
|
214
|
+
console.print(
|
|
215
|
+
f"[bold red]Token exchange failed: Network error - {type(e).__name__}[/bold red]"
|
|
216
|
+
)
|
|
217
|
+
except httpx.HTTPStatusError as exc:
|
|
218
|
+
error_detail = "Unknown error"
|
|
219
|
+
try:
|
|
220
|
+
if exc.response.content:
|
|
221
|
+
content_type = exc.response.headers.get("content-type", "")
|
|
222
|
+
if "application/json" in content_type:
|
|
223
|
+
error_data = exc.response.json()
|
|
224
|
+
error_detail = error_data.get("detail", str(error_data))
|
|
225
|
+
except Exception:
|
|
226
|
+
error_detail = exc.response.text[:200] if exc.response.text else "Unknown error"
|
|
227
|
+
|
|
228
|
+
console.print(
|
|
229
|
+
f"[bold red]Token exchange failed: {exc.response.status_code} - {error_detail}[/bold red]"
|
|
230
|
+
)
|
|
231
|
+
except ValueError as e:
|
|
232
|
+
console.print(f"[bold red]Token exchange failed: {e}[/bold red]")
|
|
233
|
+
except Exception as e:
|
|
234
|
+
console.print(
|
|
235
|
+
f"[bold red]Token exchange failed: Unexpected error - {type(e).__name__}[/bold red]"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
console.print(f"[bold red]Login failed: {type(e).__name__} - {e}[/bold red]")
|
|
240
|
+
if httpd_instance:
|
|
241
|
+
try:
|
|
242
|
+
httpd_instance.server_close()
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@auth.command()
|
|
248
|
+
def logout():
|
|
249
|
+
"""Logout and clear stored tokens."""
|
|
250
|
+
try:
|
|
251
|
+
clear_tokens()
|
|
252
|
+
console.print("[bold green]Logged out successfully.[/bold green]")
|
|
253
|
+
except Exception as e:
|
|
254
|
+
console.print(f"[yellow]Warning: Error during logout: {e}[/yellow]")
|
|
255
|
+
console.print("[dim]Tokens may still be stored in keyring.[/dim]")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@auth.command()
|
|
259
|
+
@click.option("--token", is_flag=True, help="Show access token")
|
|
260
|
+
def whoami(token):
|
|
261
|
+
"""Show current authenticated user."""
|
|
262
|
+
access_token = get_auth_token()
|
|
263
|
+
|
|
264
|
+
if not access_token:
|
|
265
|
+
console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
from jose import jwt
|
|
270
|
+
|
|
271
|
+
# For display purposes only, in a CLI context where the token has just
|
|
272
|
+
# been retrieved from a secure source (keyring), we can disable
|
|
273
|
+
# signature verification.
|
|
274
|
+
#
|
|
275
|
+
# SECURITY BEST PRACTICE: In a real application, especially a server,
|
|
276
|
+
# you would fetch the public key from the SSO's JWKS endpoint and
|
|
277
|
+
# fully verify the token's signature to ensure its integrity.
|
|
278
|
+
claims = jwt.decode(
|
|
279
|
+
access_token, options={"verify_signature": False} # OK for local display
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
console.print("[bold green]Logged in as:[/bold green]")
|
|
283
|
+
console.print(f" User ID: {claims.get('sub')}")
|
|
284
|
+
console.print(f" Email: {claims.get('email', 'N/A')}")
|
|
285
|
+
|
|
286
|
+
if token:
|
|
287
|
+
console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
|
|
288
|
+
except Exception as e:
|
|
289
|
+
console.print(f"[bold red]Failed to decode token: {e}[/bold red]")
|
|
@@ -15,6 +15,13 @@ from xenfra_sdk.privacy import scrub_logs
|
|
|
15
15
|
from ..utils.auth import API_BASE_URL, get_auth_token
|
|
16
16
|
from ..utils.codebase import has_xenfra_config
|
|
17
17
|
from ..utils.config import apply_patch
|
|
18
|
+
from ..utils.validation import (
|
|
19
|
+
validate_branch_name,
|
|
20
|
+
validate_deployment_id,
|
|
21
|
+
validate_framework,
|
|
22
|
+
validate_git_repo_url,
|
|
23
|
+
validate_project_name,
|
|
24
|
+
)
|
|
18
25
|
|
|
19
26
|
console = Console()
|
|
20
27
|
|
|
@@ -147,8 +154,18 @@ def deploy(project_name, git_repo, branch, framework, no_heal):
|
|
|
147
154
|
if not project_name:
|
|
148
155
|
project_name = os.path.basename(os.getcwd())
|
|
149
156
|
|
|
150
|
-
#
|
|
157
|
+
# Validate project name
|
|
158
|
+
is_valid, error_msg = validate_project_name(project_name)
|
|
159
|
+
if not is_valid:
|
|
160
|
+
console.print(f"[bold red]Invalid project name: {error_msg}[/bold red]")
|
|
161
|
+
raise click.Abort()
|
|
162
|
+
|
|
163
|
+
# Validate git repo if provided
|
|
151
164
|
if git_repo:
|
|
165
|
+
is_valid, error_msg = validate_git_repo_url(git_repo)
|
|
166
|
+
if not is_valid:
|
|
167
|
+
console.print(f"[bold red]Invalid git repository URL: {error_msg}[/bold red]")
|
|
168
|
+
raise click.Abort()
|
|
152
169
|
console.print(f"[cyan]Deploying {project_name} from git repository...[/cyan]")
|
|
153
170
|
else:
|
|
154
171
|
console.print(f"[cyan]Deploying {project_name} from local directory...[/cyan]")
|
|
@@ -158,6 +175,19 @@ def deploy(project_name, git_repo, branch, framework, no_heal):
|
|
|
158
175
|
console.print("[dim]Please use --git-repo for now.[/dim]")
|
|
159
176
|
return
|
|
160
177
|
|
|
178
|
+
# Validate branch name
|
|
179
|
+
is_valid, error_msg = validate_branch_name(branch)
|
|
180
|
+
if not is_valid:
|
|
181
|
+
console.print(f"[bold red]Invalid branch name: {error_msg}[/bold red]")
|
|
182
|
+
raise click.Abort()
|
|
183
|
+
|
|
184
|
+
# Validate framework if provided
|
|
185
|
+
if framework:
|
|
186
|
+
is_valid, error_msg = validate_framework(framework)
|
|
187
|
+
if not is_valid:
|
|
188
|
+
console.print(f"[bold red]Invalid framework: {error_msg}[/bold red]")
|
|
189
|
+
raise click.Abort()
|
|
190
|
+
|
|
161
191
|
# Retry loop for auto-healing
|
|
162
192
|
attempt = 0
|
|
163
193
|
deployment_id = None
|
|
@@ -288,6 +318,11 @@ def deploy(project_name, git_repo, branch, framework, no_heal):
|
|
|
288
318
|
@click.option("--follow", "-f", is_flag=True, help="Follow log output (stream)")
|
|
289
319
|
@click.option("--tail", type=int, help="Show last N lines")
|
|
290
320
|
def logs(deployment_id, follow, tail):
|
|
321
|
+
# Validate deployment ID
|
|
322
|
+
is_valid, error_msg = validate_deployment_id(deployment_id)
|
|
323
|
+
if not is_valid:
|
|
324
|
+
console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
|
|
325
|
+
raise click.Abort()
|
|
291
326
|
"""
|
|
292
327
|
Stream deployment logs.
|
|
293
328
|
|
|
@@ -357,6 +392,12 @@ def status(deployment_id, watch):
|
|
|
357
392
|
console.print("[dim]Usage: xenfra status <deployment-id>[/dim]")
|
|
358
393
|
return
|
|
359
394
|
|
|
395
|
+
# Validate deployment ID
|
|
396
|
+
is_valid, error_msg = validate_deployment_id(deployment_id)
|
|
397
|
+
if not is_valid:
|
|
398
|
+
console.print(f"[bold red]Invalid deployment ID: {error_msg}[/bold red]")
|
|
399
|
+
raise click.Abort()
|
|
400
|
+
|
|
360
401
|
with get_client() as client:
|
|
361
402
|
console.print(f"[cyan]Fetching status for deployment {deployment_id}...[/cyan]")
|
|
362
403
|
|