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 +19 -12
- xenfra/commands/intelligence.py +37 -0
- xenfra/utils/auth.py +66 -11
- {xenfra-0.3.2.dist-info → xenfra-0.3.4.dist-info}/METADATA +2 -1
- {xenfra-0.3.2.dist-info → xenfra-0.3.4.dist-info}/RECORD +7 -7
- {xenfra-0.3.2.dist-info → xenfra-0.3.4.dist-info}/WHEEL +0 -0
- {xenfra-0.3.2.dist-info → xenfra-0.3.4.dist-info}/entry_points.txt +0 -0
xenfra/commands/auth.py
CHANGED
|
@@ -114,18 +114,25 @@ def whoami(token):
|
|
|
114
114
|
return
|
|
115
115
|
|
|
116
116
|
try:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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')}")
|
xenfra/commands/intelligence.py
CHANGED
|
@@ -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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
#
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
17
|
-
xenfra-0.3.
|
|
18
|
-
xenfra-0.3.
|
|
19
|
-
xenfra-0.3.
|
|
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
|
|
File without changes
|