xenfra 0.3.1__py3-none-any.whl → 0.3.2__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/commands/auth.py CHANGED
@@ -128,8 +128,8 @@ def whoami(token):
128
128
  )
129
129
 
130
130
  console.print("[bold green]Logged in as:[/bold green]")
131
- console.print(f" User ID: {claims.get('sub')}")
132
- console.print(f" Email: {claims.get('email', 'N/A')}")
131
+ console.print(f" Email: {claims.get('sub', 'N/A')}")
132
+ console.print(f" User ID: {claims.get('user_id', 'N/A')}")
133
133
 
134
134
  if token:
135
135
  console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
@@ -105,13 +105,18 @@ def device_login():
105
105
  access_token = token_data["access_token"]
106
106
  refresh_token = token_data.get("refresh_token")
107
107
 
108
- # Store tokens in keyring
108
+ # Store tokens (keyring or file fallback)
109
109
  try:
110
110
  keyring.set_password(SERVICE_ID, "access_token", access_token)
111
111
  if refresh_token:
112
112
  keyring.set_password(SERVICE_ID, "refresh_token", refresh_token)
113
113
  except keyring.errors.KeyringError as e:
114
- console.print(f"[yellow]Warning: Could not save tokens to keyring: {e}[/yellow]")
114
+ console.print(f"[dim]Keyring unavailable, using file storage: {e}[/dim]")
115
+ # Fallback to file storage
116
+ from ..utils.auth import _set_token_to_file
117
+ _set_token_to_file("access_token", access_token)
118
+ if refresh_token:
119
+ _set_token_to_file("refresh_token", refresh_token)
115
120
 
116
121
  console.print()
117
122
  console.print("[bold green]✓ Successfully authenticated![/bold green]")
xenfra/utils/auth.py CHANGED
@@ -3,7 +3,10 @@ Authentication utilities for Xenfra CLI.
3
3
  Handles OAuth2 PKCE flow and token management.
4
4
  """
5
5
 
6
+ import json
7
+ import os
6
8
  from http.server import BaseHTTPRequestHandler, HTTPServer
9
+ from pathlib import Path
7
10
  from urllib.parse import parse_qs, urlparse
8
11
 
9
12
  import httpx
@@ -24,6 +27,9 @@ console = Console()
24
27
  API_BASE_URL = validate_and_get_api_url()
25
28
  SERVICE_ID = "xenfra"
26
29
 
30
+ # Fallback file-based token storage (for Windows if keyring fails)
31
+ TOKEN_FILE = Path.home() / ".xenfra" / "tokens.json"
32
+
27
33
  # CLI OAuth2 Configuration
28
34
  CLI_CLIENT_ID = "xenfra-cli"
29
35
  CLI_REDIRECT_PATH = "/auth/callback"
@@ -118,6 +124,62 @@ def _refresh_token_with_retry(refresh_token: str) -> dict:
118
124
  return token_data
119
125
 
120
126
 
127
+ def _get_token_from_file(key: str) -> str | None:
128
+ """Fallback: Get token from file storage (for Windows if keyring fails)."""
129
+ try:
130
+ if TOKEN_FILE.exists():
131
+ with open(TOKEN_FILE, "r") as f:
132
+ tokens = json.load(f)
133
+ return tokens.get(key)
134
+ except Exception:
135
+ pass
136
+ return None
137
+
138
+
139
+ def _set_token_to_file(key: str, value: str):
140
+ """Fallback: Save token to file storage (for Windows if keyring fails)."""
141
+ try:
142
+ TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
143
+
144
+ # Load existing tokens
145
+ tokens = {}
146
+ if TOKEN_FILE.exists():
147
+ with open(TOKEN_FILE, "r") as f:
148
+ tokens = json.load(f)
149
+
150
+ # Update token
151
+ tokens[key] = value
152
+
153
+ # Save (with restrictive permissions on Unix-like systems)
154
+ with open(TOKEN_FILE, "w") as f:
155
+ json.dump(tokens, f)
156
+
157
+ # Set file permissions (owner read/write only)
158
+ if os.name != "nt": # Not Windows
159
+ TOKEN_FILE.chmod(0o600)
160
+ except Exception as e:
161
+ console.print(f"[yellow]Warning: Could not save token to file: {e}[/yellow]")
162
+
163
+
164
+ def _delete_token_from_file(key: str):
165
+ """Fallback: Delete token from file storage."""
166
+ try:
167
+ if TOKEN_FILE.exists():
168
+ with open(TOKEN_FILE, "r") as f:
169
+ tokens = json.load(f)
170
+
171
+ if key in tokens:
172
+ del tokens[key]
173
+
174
+ if tokens:
175
+ with open(TOKEN_FILE, "w") as f:
176
+ json.dump(tokens, f)
177
+ else:
178
+ TOKEN_FILE.unlink() # Delete file if empty
179
+ except Exception:
180
+ pass
181
+
182
+
121
183
  def get_auth_token() -> str | None:
122
184
  """
123
185
  Retrieve a valid access token, refreshing it if necessary.
@@ -125,12 +187,19 @@ def get_auth_token() -> str | None:
125
187
  Returns:
126
188
  Valid access token or None if not authenticated
127
189
  """
190
+ # Try keyring first
191
+ access_token = None
192
+ refresh_token = None
193
+ use_file_fallback = False
194
+
128
195
  try:
129
196
  access_token = keyring.get_password(SERVICE_ID, "access_token")
130
197
  refresh_token = keyring.get_password(SERVICE_ID, "refresh_token")
131
198
  except keyring.errors.KeyringError as e:
132
- console.print(f"[yellow]Warning: Could not access keyring: {e}[/yellow]")
133
- return None
199
+ console.print(f"[dim]Keyring unavailable, using file storage: {e}[/dim]")
200
+ use_file_fallback = True
201
+ access_token = _get_token_from_file("access_token")
202
+ refresh_token = _get_token_from_file("refresh_token")
134
203
 
135
204
  if not access_token:
136
205
  return None
@@ -173,18 +242,24 @@ def get_auth_token() -> str | None:
173
242
  new_refresh_token = token_data.get("refresh_token")
174
243
 
175
244
  if new_access_token:
176
- try:
177
- keyring.set_password(SERVICE_ID, "access_token", new_access_token)
245
+ # Save tokens (keyring or file fallback)
246
+ if use_file_fallback:
247
+ _set_token_to_file("access_token", new_access_token)
178
248
  if new_refresh_token:
179
- keyring.set_password(SERVICE_ID, "refresh_token", new_refresh_token)
180
- console.print("[bold green]Token refreshed successfully.[/bold green]")
181
- return new_access_token
182
- except keyring.errors.KeyringError as e:
183
- console.print(
184
- f"[yellow]Warning: Could not save refreshed token to keyring: {e}[/yellow]"
185
- )
186
- # Return the token anyway, but warn user
187
- return new_access_token
249
+ _set_token_to_file("refresh_token", new_refresh_token)
250
+ else:
251
+ try:
252
+ keyring.set_password(SERVICE_ID, "access_token", new_access_token)
253
+ if new_refresh_token:
254
+ keyring.set_password(SERVICE_ID, "refresh_token", new_refresh_token)
255
+ except keyring.errors.KeyringError as e:
256
+ console.print(f"[dim]Keyring failed, falling back to file storage: {e}[/dim]")
257
+ _set_token_to_file("access_token", new_access_token)
258
+ if new_refresh_token:
259
+ _set_token_to_file("refresh_token", new_refresh_token)
260
+
261
+ console.print("[bold green]Token refreshed successfully.[/bold green]")
262
+ return new_access_token
188
263
  else:
189
264
  console.print("[bold red]Failed to get new access token.[/bold red]")
190
265
  return None
@@ -214,11 +289,7 @@ def get_auth_token() -> str | None:
214
289
  )
215
290
 
216
291
  # Clear tokens on refresh failure
217
- try:
218
- keyring.delete_password(SERVICE_ID, "access_token")
219
- keyring.delete_password(SERVICE_ID, "refresh_token")
220
- except keyring.errors.KeyringError:
221
- pass # Ignore errors when clearing
292
+ clear_tokens()
222
293
  return None
223
294
  except ValueError as e:
224
295
  console.print(f"[bold red]Token refresh failed: {e}[/bold red]")
@@ -233,11 +304,16 @@ def get_auth_token() -> str | None:
233
304
 
234
305
 
235
306
  def clear_tokens():
236
- """Clear stored access and refresh tokens."""
307
+ """Clear stored access and refresh tokens (from both keyring and file)."""
308
+ # Clear from keyring
237
309
  try:
238
310
  keyring.delete_password(SERVICE_ID, "access_token")
239
311
  keyring.delete_password(SERVICE_ID, "refresh_token")
240
312
  except keyring.errors.PasswordDeleteError:
241
313
  pass # Tokens already cleared
242
- except keyring.errors.KeyringError as e:
243
- console.print(f"[yellow]Warning: Could not clear tokens from keyring: {e}[/yellow]")
314
+ except keyring.errors.KeyringError:
315
+ pass # Keyring not available, that's OK
316
+
317
+ # Clear from file storage (fallback)
318
+ _delete_token_from_file("access_token")
319
+ _delete_token_from_file("refresh_token")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: xenfra
3
- Version: 0.3.1
3
+ Version: 0.3.2
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>
@@ -1,19 +1,19 @@
1
1
  xenfra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  xenfra/commands/__init__.py,sha256=kTTwVnTvoxikyPUhQiyTAbnw4PYafktuE1----TqQoA,43
3
- xenfra/commands/auth.py,sha256=UMtaIPsBqrVoYDmXkbQZoeEQrPyf6vHQmgPFS4PGwYQ,4109
4
- xenfra/commands/auth_device.py,sha256=BfSjyoOHgvfPH69QdD4fF_VdCktS99z2k7UGFPClWls,6364
3
+ xenfra/commands/auth.py,sha256=V6azS3ohhuEufHS1j34W2If08cCfYUUeDYApGSfGedg,4118
4
+ xenfra/commands/auth_device.py,sha256=caD2UdveEZtAFjgjmnA-l5bjbbPONFjXJXgeJN7mhbk,6710
5
5
  xenfra/commands/deployments.py,sha256=bI6d9lVYPhCDoQE3nke7s80MUQ75ILPqiEGhcbXDExo,28609
6
6
  xenfra/commands/intelligence.py,sha256=1lw1Pw6deyC52JmpK8YAVFuydAC7cvg_2qiRnw8B8Yc,13884
7
7
  xenfra/commands/projects.py,sha256=SAxF_pOr95K6uz35U-zENptKndKxJNZn6bcD45PHcpI,6696
8
8
  xenfra/commands/security_cmd.py,sha256=EI5sjX2lcMxgMH-LCFmPVkc9YqadOrcoSgTiKknkVRY,7327
9
9
  xenfra/main.py,sha256=2EPPuIdxjhW-I-e-Mc0i2ayeLaSJdmzddNThkXq7B7c,2033
10
10
  xenfra/utils/__init__.py,sha256=4ZRYkrb--vzoXjBHG8zRxz2jCXNGtAoKNtkyu2WRI2A,45
11
- xenfra/utils/auth.py,sha256=L6fDwYbvDhZ7gky68GxNEMHGxdi-k1kzFvjDSe-uqI4,9092
11
+ xenfra/utils/auth.py,sha256=4sjcuFwiqnSTBBiE9JAX36qLpZsrn6H-v_633Pj8ia8,11534
12
12
  xenfra/utils/codebase.py,sha256=57GthXOvOQnUHiDwIHqxK6hNUGWlWf6Nfs3T8647Wrc,4144
13
13
  xenfra/utils/config.py,sha256=F2zedd3JXP7TBdul0u8b4NVx-C1N6Hq4sH5szyWim6M,11947
14
14
  xenfra/utils/security.py,sha256=EA8CIPLt8Y-QP5uZ7c5NuC6ZLRV1aZS8NapS9ix_vok,11479
15
15
  xenfra/utils/validation.py,sha256=cvuL_AEFJ2oCoP0abCqoOIABOwz79Gkf-jh_dcFIQlM,6912
16
- xenfra-0.3.1.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
17
- xenfra-0.3.1.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
18
- xenfra-0.3.1.dist-info/METADATA,sha256=nGWJ17J9a4dKTy_nT3Qu_0elfTlM3vhB-u_ArQ7U_uE,3834
19
- xenfra-0.3.1.dist-info/RECORD,,
16
+ xenfra-0.3.2.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
17
+ xenfra-0.3.2.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
18
+ xenfra-0.3.2.dist-info/METADATA,sha256=5u2CxbFjkXvlMqnf06WIN5cr-7iBBDDf-3aCu6zjVcU,3834
19
+ xenfra-0.3.2.dist-info/RECORD,,
File without changes