xenfra 0.3.2__py3-none-any.whl → 0.3.4__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
@@ -114,18 +114,25 @@ def whoami(token):
114
114
  return
115
115
 
116
116
  try:
117
- from jose import jwt
118
-
119
- # For display purposes only, in a CLI context where the token has just
120
- # been retrieved from a secure source (keyring), we can disable
121
- # signature verification.
122
- #
123
- # SECURITY BEST PRACTICE: In a real application, especially a server,
124
- # you would fetch the public key from the SSO's JWKS endpoint and
125
- # fully verify the token's signature to ensure its integrity.
126
- claims = jwt.decode(
127
- access_token, options={"verify_signature": False} # OK for local display
128
- )
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)
129
136
 
130
137
  console.print("[bold green]Logged in as:[/bold green]")
131
138
  console.print(f" Email: {claims.get('sub', 'N/A')}")
@@ -34,6 +34,43 @@ def get_client() -> XenfraClient:
34
34
  console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
35
35
  raise click.Abort()
36
36
 
37
+ # DEBUG: Only show token info if XENFRA_DEBUG environment variable is set
38
+ import os
39
+ if os.getenv("XENFRA_DEBUG") == "1":
40
+ import base64
41
+ import json
42
+ try:
43
+ parts = token.split(".")
44
+ if len(parts) == 3:
45
+ # Decode payload
46
+ payload_b64 = parts[1]
47
+ padding = 4 - len(payload_b64) % 4
48
+ if padding != 4:
49
+ payload_b64 += "=" * padding
50
+ payload_bytes = base64.urlsafe_b64decode(payload_b64)
51
+ claims = json.loads(payload_bytes)
52
+
53
+ console.print("[dim]━━━ DEBUG: Token Info ━━━[/dim]")
54
+ console.print(f"[dim] API URL: {API_BASE_URL}[/dim]")
55
+ console.print(f"[dim] Token prefix: {token[:20]}...[/dim]")
56
+ console.print(f"[dim] sub (email): {claims.get('sub', 'MISSING')}[/dim]")
57
+ console.print(f"[dim] user_id: {claims.get('user_id', 'MISSING')}[/dim]")
58
+ console.print(f"[dim] iss (issuer): {claims.get('iss', 'MISSING')}[/dim]")
59
+ console.print(f"[dim] aud (audience): {claims.get('aud', 'MISSING')}[/dim]")
60
+
61
+ # Check if token is expired
62
+ exp = claims.get('exp')
63
+ if exp:
64
+ import time
65
+ is_expired = time.time() >= exp
66
+ from datetime import datetime, timezone
67
+ exp_time = datetime.fromtimestamp(exp, tz=timezone.utc)
68
+ console.print(f"[dim] expires_at: {exp_time}[/dim]")
69
+ console.print(f"[dim] expired: {is_expired}[/dim]")
70
+ console.print("[dim]━━━━━━━━━━━━━━━━━━━━━[/dim]\n")
71
+ except Exception as e:
72
+ console.print(f"[dim]DEBUG: Could not decode token: {e}[/dim]\n")
73
+
37
74
  return XenfraClient(token=token, api_url=API_BASE_URL)
38
75
 
39
76
 
xenfra/utils/auth.py CHANGED
@@ -124,37 +124,89 @@ def _refresh_token_with_retry(refresh_token: str) -> dict:
124
124
  return token_data
125
125
 
126
126
 
127
+ def _get_encryption_key() -> bytes:
128
+ """
129
+ Get or create encryption key for file-based token storage.
130
+
131
+ Uses machine-specific identifier to generate key (not perfect but better than plaintext).
132
+ """
133
+ import platform
134
+ import hashlib
135
+
136
+ # Use machine ID + username as seed (available on Windows/Linux/Mac)
137
+ machine_id = platform.node() + os.getlogin()
138
+ key = hashlib.sha256(machine_id.encode()).digest()[:32] # 32 bytes for Fernet
139
+ return key
140
+
141
+
127
142
  def _get_token_from_file(key: str) -> str | None:
128
143
  """Fallback: Get token from file storage (for Windows if keyring fails)."""
129
144
  try:
130
145
  if TOKEN_FILE.exists():
146
+ with open(TOKEN_FILE, "rb") as f:
147
+ encrypted_data = f.read()
148
+
149
+ # Decrypt the file contents
150
+ from cryptography.fernet import Fernet
151
+ import base64
152
+
153
+ fernet_key = base64.urlsafe_b64encode(_get_encryption_key())
154
+ cipher = Fernet(fernet_key)
155
+ decrypted_data = cipher.decrypt(encrypted_data)
156
+ tokens = json.loads(decrypted_data.decode())
157
+ return tokens.get(key)
158
+ except Exception:
159
+ # If decryption fails, try reading as plaintext (for backward compatibility)
160
+ try:
131
161
  with open(TOKEN_FILE, "r") as f:
132
162
  tokens = json.load(f)
133
163
  return tokens.get(key)
134
- except Exception:
135
- pass
164
+ except Exception:
165
+ pass
136
166
  return None
137
167
 
138
168
 
139
169
  def _set_token_to_file(key: str, value: str):
140
- """Fallback: Save token to file storage (for Windows if keyring fails)."""
170
+ """Fallback: Save token to file storage (encrypted for Windows if keyring fails)."""
141
171
  try:
142
172
  TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
143
173
 
144
- # Load existing tokens
174
+ # Load existing tokens (try encrypted first, then plaintext)
145
175
  tokens = {}
146
176
  if TOKEN_FILE.exists():
147
- with open(TOKEN_FILE, "r") as f:
148
- tokens = json.load(f)
177
+ try:
178
+ with open(TOKEN_FILE, "rb") as f:
179
+ encrypted_data = f.read()
180
+ from cryptography.fernet import Fernet
181
+ import base64
182
+
183
+ fernet_key = base64.urlsafe_b64encode(_get_encryption_key())
184
+ cipher = Fernet(fernet_key)
185
+ decrypted_data = cipher.decrypt(encrypted_data)
186
+ tokens = json.loads(decrypted_data.decode())
187
+ except Exception:
188
+ # Fallback to plaintext for backward compatibility
189
+ try:
190
+ with open(TOKEN_FILE, "r") as f:
191
+ tokens = json.load(f)
192
+ except Exception:
193
+ tokens = {}
149
194
 
150
195
  # Update token
151
196
  tokens[key] = value
152
197
 
153
- # Save (with restrictive permissions on Unix-like systems)
154
- with open(TOKEN_FILE, "w") as f:
155
- json.dump(tokens, f)
198
+ # Encrypt and save
199
+ from cryptography.fernet import Fernet
200
+ import base64
201
+
202
+ fernet_key = base64.urlsafe_b64encode(_get_encryption_key())
203
+ cipher = Fernet(fernet_key)
204
+ encrypted_data = cipher.encrypt(json.dumps(tokens).encode())
205
+
206
+ with open(TOKEN_FILE, "wb") as f:
207
+ f.write(encrypted_data)
156
208
 
157
- # Set file permissions (owner read/write only)
209
+ # Set file permissions (owner read/write only) - works on Unix-like systems
158
210
  if os.name != "nt": # Not Windows
159
211
  TOKEN_FILE.chmod(0o600)
160
212
  except Exception as e:
@@ -195,8 +247,11 @@ def get_auth_token() -> str | None:
195
247
  try:
196
248
  access_token = keyring.get_password(SERVICE_ID, "access_token")
197
249
  refresh_token = keyring.get_password(SERVICE_ID, "refresh_token")
250
+ if os.getenv("XENFRA_DEBUG") == "1":
251
+ console.print("[dim]DEBUG: Token retrieved from keyring[/dim]")
198
252
  except keyring.errors.KeyringError as e:
199
- console.print(f"[dim]Keyring unavailable, using file storage: {e}[/dim]")
253
+ if os.getenv("XENFRA_DEBUG") == "1":
254
+ console.print(f"[dim]DEBUG: Keyring unavailable, using file storage: {e}[/dim]")
200
255
  use_file_fallback = True
201
256
  access_token = _get_token_from_file("access_token")
202
257
  refresh_token = _get_token_from_file("refresh_token")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: xenfra
3
- Version: 0.3.2
3
+ Version: 0.3.4
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>
@@ -23,6 +23,7 @@ Requires-Dist: httpx>=0.27.0
23
23
  Requires-Dist: keyring>=25.7.0
24
24
  Requires-Dist: keyrings-alt>=5.0.2
25
25
  Requires-Dist: tenacity>=8.2.3
26
+ Requires-Dist: cryptography>=43.0.0
26
27
  Requires-Dist: pytest>=8.0.0 ; extra == 'test'
27
28
  Requires-Dist: pytest-mock>=3.12.0 ; extra == 'test'
28
29
  Requires-Python: >=3.13
@@ -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=V6azS3ohhuEufHS1j34W2If08cCfYUUeDYApGSfGedg,4118
3
+ xenfra/commands/auth.py,sha256=ecReVCGl7Ys2d77mv_e4mCbs4ug6FLIb3S9dl2FUhr4,4178
4
4
  xenfra/commands/auth_device.py,sha256=caD2UdveEZtAFjgjmnA-l5bjbbPONFjXJXgeJN7mhbk,6710
5
5
  xenfra/commands/deployments.py,sha256=bI6d9lVYPhCDoQE3nke7s80MUQ75ILPqiEGhcbXDExo,28609
6
- xenfra/commands/intelligence.py,sha256=1lw1Pw6deyC52JmpK8YAVFuydAC7cvg_2qiRnw8B8Yc,13884
6
+ xenfra/commands/intelligence.py,sha256=U4nYh1aoJJ3Pxv_eliG3tmJUhCzX9iOiWg5yFALXtvE,15799
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=4sjcuFwiqnSTBBiE9JAX36qLpZsrn6H-v_633Pj8ia8,11534
11
+ xenfra/utils/auth.py,sha256=9JbFnv0-rdlJF-4hKD2uWd9h9ehqC1iIHee1O5e-3RM,13769
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.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,,
16
+ xenfra-0.3.4.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
17
+ xenfra-0.3.4.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
18
+ xenfra-0.3.4.dist-info/METADATA,sha256=0b3lQ-vh0J5Wyeaa7_UKA9HKfRRnw9CjkD068to65os,3870
19
+ xenfra-0.3.4.dist-info/RECORD,,
File without changes