xenfra 0.3.3__tar.gz → 0.3.4__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.3.3
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "xenfra"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "A 'Zen Mode' infrastructure engine for Python developers."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -30,6 +30,7 @@ dependencies = [
30
30
  "keyring>=25.7.0",
31
31
  "keyrings.alt>=5.0.2",
32
32
  "tenacity>=8.2.3", # For retry logic
33
+ "cryptography>=43.0.0", # For encrypted file-based token storage
33
34
  ]
34
35
  requires-python = ">=3.13"
35
36
 
@@ -34,40 +34,42 @@ 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: Show token details
38
- import base64
39
- import json
40
- try:
41
- parts = token.split(".")
42
- if len(parts) == 3:
43
- # Decode payload
44
- payload_b64 = parts[1]
45
- padding = 4 - len(payload_b64) % 4
46
- if padding != 4:
47
- payload_b64 += "=" * padding
48
- payload_bytes = base64.urlsafe_b64decode(payload_b64)
49
- claims = json.loads(payload_bytes)
50
-
51
- console.print("[dim]━━━ DEBUG: Token Info ━━━[/dim]")
52
- console.print(f"[dim] API URL: {API_BASE_URL}[/dim]")
53
- console.print(f"[dim] Token prefix: {token[:20]}...[/dim]")
54
- console.print(f"[dim] sub (email): {claims.get('sub', 'MISSING')}[/dim]")
55
- console.print(f"[dim] user_id: {claims.get('user_id', 'MISSING')}[/dim]")
56
- console.print(f"[dim] iss (issuer): {claims.get('iss', 'MISSING')}[/dim]")
57
- console.print(f"[dim] aud (audience): {claims.get('aud', 'MISSING')}[/dim]")
58
-
59
- # Check if token is expired
60
- exp = claims.get('exp')
61
- if exp:
62
- import time
63
- is_expired = time.time() >= exp
64
- from datetime import datetime, timezone
65
- exp_time = datetime.fromtimestamp(exp, tz=timezone.utc)
66
- console.print(f"[dim] expires_at: {exp_time}[/dim]")
67
- console.print(f"[dim] expired: {is_expired}[/dim]")
68
- console.print("[dim]━━━━━━━━━━━━━━━━━━━━━[/dim]\n")
69
- except Exception as e:
70
- console.print(f"[dim]DEBUG: Could not decode token: {e}[/dim]\n")
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")
71
73
 
72
74
  return XenfraClient(token=token, api_url=API_BASE_URL)
73
75
 
@@ -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,9 +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")
198
- console.print("[dim]DEBUG: Token retrieved from keyring[/dim]")
250
+ if os.getenv("XENFRA_DEBUG") == "1":
251
+ console.print("[dim]DEBUG: Token retrieved from keyring[/dim]")
199
252
  except keyring.errors.KeyringError as e:
200
- console.print(f"[dim]DEBUG: 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]")
201
255
  use_file_fallback = True
202
256
  access_token = _get_token_from_file("access_token")
203
257
  refresh_token = _get_token_from_file("refresh_token")
File without changes
File without changes
File without changes