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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: xenfra
3
- Version: 0.2.4
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
- * **Zero-Configuration Deployment:** Automatically detects your project's framework and dependencies.
42
- * **AI-Powered Auto-Healing:** Diagnoses common deployment failures and suggests, or even applies, fixes automatically.
43
- * **Real-time Monitoring:** View deployment status and stream live application logs directly from your terminal.
44
- * **Integrated Project Management:** Easily list, view, and destroy your deployed projects.
45
- * **Secure Authentication:** Uses OAuth2 PKCE flow for secure, token-based authentication.
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
- * **Monitor Deployment Status:**
87
- ```bash
88
- xenfra status <deployment-id>
89
- ```
90
- * **Stream Application Logs:**
91
- ```bash
92
- xenfra logs <deployment-id>
93
- ```
94
- * **List Deployed Projects:**
95
- ```bash
96
- xenfra projects list
97
- ```
98
- * **Diagnose a Failed Deployment (AI-Powered):**
99
- ```bash
100
- xenfra diagnose <deployment-id>
101
- # Or to diagnose from a log file:
102
- xenfra diagnose --logs error.log
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.com/cli) (Link will be updated upon final deployment).
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
- * **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.
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
- * **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
- ```
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.com/cli) (Link will be updated upon final deployment).
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.4"
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
- # Determine deployment source
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