xenfra 0.4.3__py3-none-any.whl → 0.4.5__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/blueprints/__init__.py +55 -0
- xenfra/blueprints/base.py +85 -0
- xenfra/blueprints/cache.py +286 -0
- xenfra/blueprints/dry_run.py +251 -0
- xenfra/blueprints/e2b.py +101 -0
- xenfra/blueprints/factory.py +113 -0
- xenfra/blueprints/railpack.py +319 -0
- xenfra/blueprints/validation.py +182 -0
- xenfra/commands/__init__.py +3 -3
- xenfra/commands/auth.py +144 -144
- xenfra/commands/auth_device.py +164 -164
- xenfra/commands/deployments.py +1358 -973
- xenfra/commands/intelligence.py +503 -412
- xenfra/commands/projects.py +204 -204
- xenfra/commands/security_cmd.py +233 -233
- xenfra/main.py +79 -75
- xenfra/utils/__init__.py +3 -3
- xenfra/utils/auth.py +374 -374
- xenfra/utils/codebase.py +169 -169
- xenfra/utils/config.py +459 -436
- xenfra/utils/errors.py +116 -116
- xenfra/utils/file_sync.py +286 -286
- xenfra/utils/security.py +336 -336
- xenfra/utils/validation.py +234 -234
- xenfra-0.4.5.dist-info/METADATA +113 -0
- xenfra-0.4.5.dist-info/RECORD +29 -0
- {xenfra-0.4.3.dist-info → xenfra-0.4.5.dist-info}/WHEEL +1 -1
- xenfra-0.4.3.dist-info/METADATA +0 -118
- xenfra-0.4.3.dist-info/RECORD +0 -21
- {xenfra-0.4.3.dist-info → xenfra-0.4.5.dist-info}/entry_points.txt +0 -0
xenfra/commands/auth_device.py
CHANGED
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Device Authorization Flow for Xenfra CLI.
|
|
3
|
-
Modern OAuth flow used by GitHub CLI, AWS CLI, Claude Code, etc.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import time
|
|
7
|
-
import webbrowser
|
|
8
|
-
from urllib.parse import urlencode
|
|
9
|
-
|
|
10
|
-
import click
|
|
11
|
-
import httpx
|
|
12
|
-
import keyring
|
|
13
|
-
from rich.console import Console
|
|
14
|
-
from rich.panel import Panel
|
|
15
|
-
|
|
16
|
-
from ..utils.auth import API_BASE_URL, CLI_CLIENT_ID, HTTP_TIMEOUT, SERVICE_ID
|
|
17
|
-
|
|
18
|
-
console = Console()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def device_login():
|
|
22
|
-
"""
|
|
23
|
-
Device Authorization Flow (OAuth 2.0 Device Grant).
|
|
24
|
-
|
|
25
|
-
Flow:
|
|
26
|
-
1. CLI calls /auth/device/authorize to get device_code and user_code
|
|
27
|
-
2. User visits https://www.xenfra.tech/activate and enters user_code
|
|
28
|
-
3. CLI polls /auth/device/token until user authorizes
|
|
29
|
-
4. CLI receives access_token and stores it
|
|
30
|
-
"""
|
|
31
|
-
try:
|
|
32
|
-
# Step 1: Request device code
|
|
33
|
-
console.print("[cyan]Initiating device authorization...[/cyan]")
|
|
34
|
-
|
|
35
|
-
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
36
|
-
response = client.post(
|
|
37
|
-
f"{API_BASE_URL}/auth/device/authorize",
|
|
38
|
-
data={
|
|
39
|
-
"client_id": CLI_CLIENT_ID,
|
|
40
|
-
"scope": "openid profile",
|
|
41
|
-
},
|
|
42
|
-
)
|
|
43
|
-
response.raise_for_status()
|
|
44
|
-
device_data = response.json()
|
|
45
|
-
|
|
46
|
-
device_code = device_data["device_code"]
|
|
47
|
-
user_code = device_data["user_code"]
|
|
48
|
-
verification_uri = device_data["verification_uri"]
|
|
49
|
-
verification_uri_complete = device_data.get("verification_uri_complete")
|
|
50
|
-
expires_in = device_data["expires_in"]
|
|
51
|
-
interval = device_data.get("interval", 5)
|
|
52
|
-
|
|
53
|
-
# Step 2: Show user code and open browser
|
|
54
|
-
console.print()
|
|
55
|
-
console.print(
|
|
56
|
-
Panel.fit(
|
|
57
|
-
f"[bold white]{user_code}[/bold white]",
|
|
58
|
-
title="[bold green]Your Activation Code[/bold green]",
|
|
59
|
-
border_style="green",
|
|
60
|
-
)
|
|
61
|
-
)
|
|
62
|
-
console.print()
|
|
63
|
-
console.print(f"[bold]Visit:[/bold] [link]{verification_uri}[/link]")
|
|
64
|
-
console.print(f"[bold]Enter code:[/bold] [cyan]{user_code}[/cyan]")
|
|
65
|
-
console.print()
|
|
66
|
-
|
|
67
|
-
# Open browser automatically
|
|
68
|
-
try:
|
|
69
|
-
url_to_open = verification_uri_complete or verification_uri
|
|
70
|
-
webbrowser.open(url_to_open)
|
|
71
|
-
console.print("[dim]Opening browser...[/dim]")
|
|
72
|
-
except Exception:
|
|
73
|
-
console.print("[yellow]Could not open browser automatically. Please visit the URL above.[/yellow]")
|
|
74
|
-
|
|
75
|
-
# Step 3: Poll for authorization
|
|
76
|
-
console.print()
|
|
77
|
-
console.print("[cyan]Waiting for authorization...[/cyan]")
|
|
78
|
-
console.print("[dim](Press Ctrl+C to cancel)[/dim]")
|
|
79
|
-
console.print()
|
|
80
|
-
|
|
81
|
-
start_time = time.time()
|
|
82
|
-
poll_count = 0
|
|
83
|
-
|
|
84
|
-
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
85
|
-
while True:
|
|
86
|
-
# Check timeout
|
|
87
|
-
if time.time() - start_time > expires_in:
|
|
88
|
-
console.print("[bold red]✗ Authorization timed out. Please try again.[/bold red]")
|
|
89
|
-
return False
|
|
90
|
-
|
|
91
|
-
# Poll the token endpoint
|
|
92
|
-
try:
|
|
93
|
-
response = client.post(
|
|
94
|
-
f"{API_BASE_URL}/auth/device/token",
|
|
95
|
-
data={
|
|
96
|
-
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
97
|
-
"device_code": device_code,
|
|
98
|
-
"client_id": CLI_CLIENT_ID,
|
|
99
|
-
},
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
if response.status_code == 200:
|
|
103
|
-
# Success! User authorized
|
|
104
|
-
token_data = response.json()
|
|
105
|
-
access_token = token_data["access_token"]
|
|
106
|
-
refresh_token = token_data.get("refresh_token")
|
|
107
|
-
|
|
108
|
-
# Store tokens (keyring or file fallback)
|
|
109
|
-
try:
|
|
110
|
-
keyring.set_password(SERVICE_ID, "access_token", access_token)
|
|
111
|
-
if refresh_token:
|
|
112
|
-
keyring.set_password(SERVICE_ID, "refresh_token", refresh_token)
|
|
113
|
-
except keyring.errors.KeyringError as e:
|
|
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)
|
|
120
|
-
|
|
121
|
-
console.print()
|
|
122
|
-
console.print("[bold green]✓ Successfully authenticated![/bold green]")
|
|
123
|
-
console.print()
|
|
124
|
-
return True
|
|
125
|
-
|
|
126
|
-
elif response.status_code == 400:
|
|
127
|
-
error_data = response.json()
|
|
128
|
-
error = error_data.get("error", "unknown_error")
|
|
129
|
-
|
|
130
|
-
if error == "authorization_pending":
|
|
131
|
-
# Still waiting for user to authorize
|
|
132
|
-
poll_count += 1
|
|
133
|
-
if poll_count % 6 == 0: # Every 30 seconds
|
|
134
|
-
console.print("[dim]Still waiting...[/dim]")
|
|
135
|
-
time.sleep(interval)
|
|
136
|
-
continue
|
|
137
|
-
|
|
138
|
-
elif error == "slow_down":
|
|
139
|
-
# We're polling too fast
|
|
140
|
-
interval += 5
|
|
141
|
-
time.sleep(interval)
|
|
142
|
-
continue
|
|
143
|
-
|
|
144
|
-
else:
|
|
145
|
-
# Other error
|
|
146
|
-
error_desc = error_data.get("error_description", error)
|
|
147
|
-
console.print(f"[bold red]✗ Authorization failed: {error_desc}[/bold red]")
|
|
148
|
-
return False
|
|
149
|
-
|
|
150
|
-
else:
|
|
151
|
-
console.print(f"[bold red]✗ Unexpected response: {response.status_code}[/bold red]")
|
|
152
|
-
return False
|
|
153
|
-
|
|
154
|
-
except httpx.HTTPError as e:
|
|
155
|
-
console.print(f"[bold red]✗ Network error: {e}[/bold red]")
|
|
156
|
-
return False
|
|
157
|
-
|
|
158
|
-
except KeyboardInterrupt:
|
|
159
|
-
console.print()
|
|
160
|
-
console.print("[yellow]Authorization cancelled.[/yellow]")
|
|
161
|
-
return False
|
|
162
|
-
except Exception as e:
|
|
163
|
-
console.print(f"[bold red]✗ Error: {e}[/bold red]")
|
|
164
|
-
return False
|
|
1
|
+
"""
|
|
2
|
+
Device Authorization Flow for Xenfra CLI.
|
|
3
|
+
Modern OAuth flow used by GitHub CLI, AWS CLI, Claude Code, etc.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import webbrowser
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import httpx
|
|
12
|
+
import keyring
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
|
|
16
|
+
from ..utils.auth import API_BASE_URL, CLI_CLIENT_ID, HTTP_TIMEOUT, SERVICE_ID
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def device_login():
|
|
22
|
+
"""
|
|
23
|
+
Device Authorization Flow (OAuth 2.0 Device Grant).
|
|
24
|
+
|
|
25
|
+
Flow:
|
|
26
|
+
1. CLI calls /auth/device/authorize to get device_code and user_code
|
|
27
|
+
2. User visits https://www.xenfra.tech/activate and enters user_code
|
|
28
|
+
3. CLI polls /auth/device/token until user authorizes
|
|
29
|
+
4. CLI receives access_token and stores it
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
# Step 1: Request device code
|
|
33
|
+
console.print("[cyan]Initiating device authorization...[/cyan]")
|
|
34
|
+
|
|
35
|
+
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
36
|
+
response = client.post(
|
|
37
|
+
f"{API_BASE_URL}/auth/device/authorize",
|
|
38
|
+
data={
|
|
39
|
+
"client_id": CLI_CLIENT_ID,
|
|
40
|
+
"scope": "openid profile",
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
device_data = response.json()
|
|
45
|
+
|
|
46
|
+
device_code = device_data["device_code"]
|
|
47
|
+
user_code = device_data["user_code"]
|
|
48
|
+
verification_uri = device_data["verification_uri"]
|
|
49
|
+
verification_uri_complete = device_data.get("verification_uri_complete")
|
|
50
|
+
expires_in = device_data["expires_in"]
|
|
51
|
+
interval = device_data.get("interval", 5)
|
|
52
|
+
|
|
53
|
+
# Step 2: Show user code and open browser
|
|
54
|
+
console.print()
|
|
55
|
+
console.print(
|
|
56
|
+
Panel.fit(
|
|
57
|
+
f"[bold white]{user_code}[/bold white]",
|
|
58
|
+
title="[bold green]Your Activation Code[/bold green]",
|
|
59
|
+
border_style="green",
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
console.print()
|
|
63
|
+
console.print(f"[bold]Visit:[/bold] [link]{verification_uri}[/link]")
|
|
64
|
+
console.print(f"[bold]Enter code:[/bold] [cyan]{user_code}[/cyan]")
|
|
65
|
+
console.print()
|
|
66
|
+
|
|
67
|
+
# Open browser automatically
|
|
68
|
+
try:
|
|
69
|
+
url_to_open = verification_uri_complete or verification_uri
|
|
70
|
+
webbrowser.open(url_to_open)
|
|
71
|
+
console.print("[dim]Opening browser...[/dim]")
|
|
72
|
+
except Exception:
|
|
73
|
+
console.print("[yellow]Could not open browser automatically. Please visit the URL above.[/yellow]")
|
|
74
|
+
|
|
75
|
+
# Step 3: Poll for authorization
|
|
76
|
+
console.print()
|
|
77
|
+
console.print("[cyan]Waiting for authorization...[/cyan]")
|
|
78
|
+
console.print("[dim](Press Ctrl+C to cancel)[/dim]")
|
|
79
|
+
console.print()
|
|
80
|
+
|
|
81
|
+
start_time = time.time()
|
|
82
|
+
poll_count = 0
|
|
83
|
+
|
|
84
|
+
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
85
|
+
while True:
|
|
86
|
+
# Check timeout
|
|
87
|
+
if time.time() - start_time > expires_in:
|
|
88
|
+
console.print("[bold red]✗ Authorization timed out. Please try again.[/bold red]")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
# Poll the token endpoint
|
|
92
|
+
try:
|
|
93
|
+
response = client.post(
|
|
94
|
+
f"{API_BASE_URL}/auth/device/token",
|
|
95
|
+
data={
|
|
96
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
97
|
+
"device_code": device_code,
|
|
98
|
+
"client_id": CLI_CLIENT_ID,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if response.status_code == 200:
|
|
103
|
+
# Success! User authorized
|
|
104
|
+
token_data = response.json()
|
|
105
|
+
access_token = token_data["access_token"]
|
|
106
|
+
refresh_token = token_data.get("refresh_token")
|
|
107
|
+
|
|
108
|
+
# Store tokens (keyring or file fallback)
|
|
109
|
+
try:
|
|
110
|
+
keyring.set_password(SERVICE_ID, "access_token", access_token)
|
|
111
|
+
if refresh_token:
|
|
112
|
+
keyring.set_password(SERVICE_ID, "refresh_token", refresh_token)
|
|
113
|
+
except keyring.errors.KeyringError as e:
|
|
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)
|
|
120
|
+
|
|
121
|
+
console.print()
|
|
122
|
+
console.print("[bold green]✓ Successfully authenticated![/bold green]")
|
|
123
|
+
console.print()
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
elif response.status_code == 400:
|
|
127
|
+
error_data = response.json()
|
|
128
|
+
error = error_data.get("error", "unknown_error")
|
|
129
|
+
|
|
130
|
+
if error == "authorization_pending":
|
|
131
|
+
# Still waiting for user to authorize
|
|
132
|
+
poll_count += 1
|
|
133
|
+
if poll_count % 6 == 0: # Every 30 seconds
|
|
134
|
+
console.print("[dim]Still waiting...[/dim]")
|
|
135
|
+
time.sleep(interval)
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
elif error == "slow_down":
|
|
139
|
+
# We're polling too fast
|
|
140
|
+
interval += 5
|
|
141
|
+
time.sleep(interval)
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
else:
|
|
145
|
+
# Other error
|
|
146
|
+
error_desc = error_data.get("error_description", error)
|
|
147
|
+
console.print(f"[bold red]✗ Authorization failed: {error_desc}[/bold red]")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
else:
|
|
151
|
+
console.print(f"[bold red]✗ Unexpected response: {response.status_code}[/bold red]")
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
except httpx.HTTPError as e:
|
|
155
|
+
console.print(f"[bold red]✗ Network error: {e}[/bold red]")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
except KeyboardInterrupt:
|
|
159
|
+
console.print()
|
|
160
|
+
console.print("[yellow]Authorization cancelled.[/yellow]")
|
|
161
|
+
return False
|
|
162
|
+
except Exception as e:
|
|
163
|
+
console.print(f"[bold red]✗ Error: {e}[/bold red]")
|
|
164
|
+
return False
|