xenfra 0.4.3__tar.gz → 0.4.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.
- xenfra-0.4.4/PKG-INFO +113 -0
- xenfra-0.4.4/README.md +78 -0
- {xenfra-0.4.3 → xenfra-0.4.4}/pyproject.toml +54 -56
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/commands/__init__.py +3 -3
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/commands/auth.py +144 -144
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/commands/auth_device.py +164 -164
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/commands/deployments.py +1133 -973
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/commands/intelligence.py +503 -412
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/commands/projects.py +204 -204
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/commands/security_cmd.py +233 -233
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/main.py +76 -75
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/utils/__init__.py +3 -3
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/utils/auth.py +374 -374
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/utils/codebase.py +169 -169
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/utils/config.py +459 -436
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/utils/errors.py +116 -116
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/utils/file_sync.py +286 -286
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/utils/security.py +336 -336
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/utils/validation.py +234 -234
- xenfra-0.4.3/PKG-INFO +0 -118
- xenfra-0.4.3/README.md +0 -83
- {xenfra-0.4.3 → xenfra-0.4.4}/src/xenfra/__init__.py +0 -0
xenfra-0.4.4/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: xenfra
|
|
3
|
+
Version: 0.4.4
|
|
4
|
+
Summary: Xenfra CLI: Hands for AI to deploy on DigitalOcean.
|
|
5
|
+
Author: xenfra-cloud
|
|
6
|
+
Author-email: xenfra-cloud <xenfracloud@gmail.com>
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Topic :: System :: Systems Administration
|
|
14
|
+
Requires-Dist: click>=8.1.7
|
|
15
|
+
Requires-Dist: rich>=14.2.0
|
|
16
|
+
Requires-Dist: sqlmodel>=0.0.16
|
|
17
|
+
Requires-Dist: python-digitalocean>=1.17.0
|
|
18
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
19
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
20
|
+
Requires-Dist: fabric>=3.2.2
|
|
21
|
+
Requires-Dist: xenfra-sdk>=0.2.2
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: keyring>=25.7.0
|
|
24
|
+
Requires-Dist: keyrings-alt>=5.0.2
|
|
25
|
+
Requires-Dist: tenacity>=8.2.3
|
|
26
|
+
Requires-Dist: cryptography>=43.0.0
|
|
27
|
+
Requires-Dist: toml>=0.10.2
|
|
28
|
+
Requires-Dist: pytest>=8.0.0 ; extra == 'test'
|
|
29
|
+
Requires-Dist: pytest-mock>=3.12.0 ; extra == 'test'
|
|
30
|
+
Requires-Python: >=3.11
|
|
31
|
+
Project-URL: Homepage, https://github.com/xenfra-cloud/xenfra-cli
|
|
32
|
+
Project-URL: Issues, https://github.com/xenfra-cloud/xenfra-cli/issues
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Xenfra CLI (The Interface) 🖥️
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/xenfra/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
41
|
+
The official command-line interface for **Xenfra** (The Sovereign Cloud OS). It empowers developers to deploy, monitor, and manage applications on their own infrastructure (DigitalOcean) with the ease of Heroku.
|
|
42
|
+
|
|
43
|
+
## 🚀 Features
|
|
44
|
+
|
|
45
|
+
- **Zero-Config Deployment**: `xenfra deploy` detects your stack (Python, Node.js) and ships it.
|
|
46
|
+
- **Sovereign Auth**: `xenfra auth login` connects securely to your cloud provider.
|
|
47
|
+
- **Live Logs**: `xenfra logs` streams colorized, PII-scrubbed logs from your servers.
|
|
48
|
+
- **Doctor**: `xenfra doctor` runs a battery of health checks on your deployment environment.
|
|
49
|
+
- **Zen Mode**: Automatically applies fix patches when deployments fail.
|
|
50
|
+
|
|
51
|
+
## 📦 Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Recommended: Install via uv
|
|
55
|
+
uv tool install xenfra
|
|
56
|
+
|
|
57
|
+
# Or via pip
|
|
58
|
+
pip install xenfra
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 🛠️ Quick Start
|
|
62
|
+
|
|
63
|
+
### 1. Login
|
|
64
|
+
|
|
65
|
+
Authenticate with your cloud provider (DigitalOcean via Xenfra Platform).
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
xenfra auth login
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Deploy Your App
|
|
72
|
+
|
|
73
|
+
Navigate to your project directory and blast off.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd ~/my-projects/awesome-api
|
|
77
|
+
xenfra deploy
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
_That's it._ Xenfra handles Dockerfile generation, server provisioning, SSL (Caddy), and database connections.
|
|
81
|
+
|
|
82
|
+
### 3. Check Status
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
xenfra status
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 🎛️ Command Reference
|
|
89
|
+
|
|
90
|
+
| Command | Description |
|
|
91
|
+
| :------------------ | :-------------------------------------- |
|
|
92
|
+
| `xenfra auth login` | Start the OAuth flow |
|
|
93
|
+
| `xenfra deploy` | Deploy current directory |
|
|
94
|
+
| `xenfra logs` | Tail logs (Ctrl+C to stop) |
|
|
95
|
+
| `xenfra status` | Show health metrics (CPU/RAM) |
|
|
96
|
+
| `xenfra list` | List all your projects |
|
|
97
|
+
| `xenfra init` | Generate config files without deploying |
|
|
98
|
+
|
|
99
|
+
## 🔗 The Xenfra Ecosystem
|
|
100
|
+
|
|
101
|
+
This CLI is the "Interface" of the Xenfra Open Core architecture:
|
|
102
|
+
|
|
103
|
+
- **[xenfra-sdk](https://github.com/xenfracloud/xenfra-sdk)**: The Core Engine (Used by this CLI).
|
|
104
|
+
- **[xenfra-mcp](https://github.com/xenfracloud/xenfra-mcp)**: The AI Agent Interface.
|
|
105
|
+
- **xenfra-platform**: The Private SaaS Backend.
|
|
106
|
+
|
|
107
|
+
## 🤝 Contributing
|
|
108
|
+
|
|
109
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
110
|
+
|
|
111
|
+
## 📄 License
|
|
112
|
+
|
|
113
|
+
MIT © [Xenfra Cloud](https://xenfra.tech)
|
xenfra-0.4.4/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Xenfra CLI (The Interface) 🖥️
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/xenfra/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
The official command-line interface for **Xenfra** (The Sovereign Cloud OS). It empowers developers to deploy, monitor, and manage applications on their own infrastructure (DigitalOcean) with the ease of Heroku.
|
|
7
|
+
|
|
8
|
+
## 🚀 Features
|
|
9
|
+
|
|
10
|
+
- **Zero-Config Deployment**: `xenfra deploy` detects your stack (Python, Node.js) and ships it.
|
|
11
|
+
- **Sovereign Auth**: `xenfra auth login` connects securely to your cloud provider.
|
|
12
|
+
- **Live Logs**: `xenfra logs` streams colorized, PII-scrubbed logs from your servers.
|
|
13
|
+
- **Doctor**: `xenfra doctor` runs a battery of health checks on your deployment environment.
|
|
14
|
+
- **Zen Mode**: Automatically applies fix patches when deployments fail.
|
|
15
|
+
|
|
16
|
+
## 📦 Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Recommended: Install via uv
|
|
20
|
+
uv tool install xenfra
|
|
21
|
+
|
|
22
|
+
# Or via pip
|
|
23
|
+
pip install xenfra
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 🛠️ Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Login
|
|
29
|
+
|
|
30
|
+
Authenticate with your cloud provider (DigitalOcean via Xenfra Platform).
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
xenfra auth login
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Deploy Your App
|
|
37
|
+
|
|
38
|
+
Navigate to your project directory and blast off.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd ~/my-projects/awesome-api
|
|
42
|
+
xenfra deploy
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
_That's it._ Xenfra handles Dockerfile generation, server provisioning, SSL (Caddy), and database connections.
|
|
46
|
+
|
|
47
|
+
### 3. Check Status
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
xenfra status
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 🎛️ Command Reference
|
|
54
|
+
|
|
55
|
+
| Command | Description |
|
|
56
|
+
| :------------------ | :-------------------------------------- |
|
|
57
|
+
| `xenfra auth login` | Start the OAuth flow |
|
|
58
|
+
| `xenfra deploy` | Deploy current directory |
|
|
59
|
+
| `xenfra logs` | Tail logs (Ctrl+C to stop) |
|
|
60
|
+
| `xenfra status` | Show health metrics (CPU/RAM) |
|
|
61
|
+
| `xenfra list` | List all your projects |
|
|
62
|
+
| `xenfra init` | Generate config files without deploying |
|
|
63
|
+
|
|
64
|
+
## 🔗 The Xenfra Ecosystem
|
|
65
|
+
|
|
66
|
+
This CLI is the "Interface" of the Xenfra Open Core architecture:
|
|
67
|
+
|
|
68
|
+
- **[xenfra-sdk](https://github.com/xenfracloud/xenfra-sdk)**: The Core Engine (Used by this CLI).
|
|
69
|
+
- **[xenfra-mcp](https://github.com/xenfracloud/xenfra-mcp)**: The AI Agent Interface.
|
|
70
|
+
- **xenfra-platform**: The Private SaaS Backend.
|
|
71
|
+
|
|
72
|
+
## 🤝 Contributing
|
|
73
|
+
|
|
74
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
75
|
+
|
|
76
|
+
## 📄 License
|
|
77
|
+
|
|
78
|
+
MIT © [Xenfra Cloud](https://xenfra.tech)
|
|
@@ -1,56 +1,54 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "xenfra"
|
|
3
|
-
version = "0.4.
|
|
4
|
-
description = "
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
authors = [
|
|
7
|
-
{ name = "xenfra-cloud", email = "xenfracloud@gmail.com" }
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
classifiers = [
|
|
11
|
-
"Programming Language :: Python :: 3",
|
|
12
|
-
"License :: OSI Approved :: MIT License",
|
|
13
|
-
"Operating System :: OS Independent",
|
|
14
|
-
"Development Status :: 3 - Alpha",
|
|
15
|
-
"Intended Audience :: Developers",
|
|
16
|
-
"Topic :: Software Development :: Build Tools",
|
|
17
|
-
"Topic :: System :: Systems Administration",
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
dependencies = [
|
|
21
|
-
"click>=8.1.7",
|
|
22
|
-
"rich>=14.2.0",
|
|
23
|
-
"sqlmodel>=0.0.16",
|
|
24
|
-
"python-digitalocean>=1.17.0",
|
|
25
|
-
"python-dotenv>=1.2.1",
|
|
26
|
-
"pyyaml>=6.0.1",
|
|
27
|
-
"fabric>=3.2.2",
|
|
28
|
-
"xenfra-sdk",
|
|
29
|
-
"httpx>=0.27.0",
|
|
30
|
-
"keyring>=25.7.0",
|
|
31
|
-
"keyrings.alt>=5.0.2",
|
|
32
|
-
"tenacity>=8.2.3", # For retry logic
|
|
33
|
-
"cryptography>=43.0.0", # For encrypted file-based token storage
|
|
34
|
-
"toml>=0.10.2",
|
|
35
|
-
]
|
|
36
|
-
requires-python = ">=3.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
56
|
-
build-backend = "uv_build"
|
|
1
|
+
[project]
|
|
2
|
+
name = "xenfra"
|
|
3
|
+
version = "0.4.4"
|
|
4
|
+
description = "Xenfra CLI: Hands for AI to deploy on DigitalOcean."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "xenfra-cloud", email = "xenfracloud@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Topic :: Software Development :: Build Tools",
|
|
17
|
+
"Topic :: System :: Systems Administration",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"click>=8.1.7",
|
|
22
|
+
"rich>=14.2.0",
|
|
23
|
+
"sqlmodel>=0.0.16",
|
|
24
|
+
"python-digitalocean>=1.17.0",
|
|
25
|
+
"python-dotenv>=1.2.1",
|
|
26
|
+
"pyyaml>=6.0.1",
|
|
27
|
+
"fabric>=3.2.2",
|
|
28
|
+
"xenfra-sdk>=0.2.2",
|
|
29
|
+
"httpx>=0.27.0",
|
|
30
|
+
"keyring>=25.7.0",
|
|
31
|
+
"keyrings.alt>=5.0.2",
|
|
32
|
+
"tenacity>=8.2.3", # For retry logic
|
|
33
|
+
"cryptography>=43.0.0", # For encrypted file-based token storage
|
|
34
|
+
"toml>=0.10.2",
|
|
35
|
+
]
|
|
36
|
+
requires-python = ">=3.11"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/xenfra-cloud/xenfra-cli"
|
|
41
|
+
Issues = "https://github.com/xenfra-cloud/xenfra-cli/issues"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
test = [
|
|
45
|
+
"pytest>=8.0.0",
|
|
46
|
+
"pytest-mock>=3.12.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
xenfra = "xenfra.main:main"
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
54
|
+
build-backend = "uv_build"
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
CLI command modules for Xenfra.
|
|
3
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
CLI command modules for Xenfra.
|
|
3
|
+
"""
|
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Authentication commands for Xenfra CLI.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import base64
|
|
6
|
-
import hashlib
|
|
7
|
-
import secrets
|
|
8
|
-
import urllib.parse
|
|
9
|
-
import webbrowser
|
|
10
|
-
from http.server import HTTPServer
|
|
11
|
-
|
|
12
|
-
import click
|
|
13
|
-
import httpx
|
|
14
|
-
import keyring
|
|
15
|
-
from rich.console import Console
|
|
16
|
-
from tenacity import (
|
|
17
|
-
retry,
|
|
18
|
-
retry_if_exception_type,
|
|
19
|
-
stop_after_attempt,
|
|
20
|
-
wait_exponential,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
from ..utils.auth import (
|
|
24
|
-
API_BASE_URL,
|
|
25
|
-
CLI_CLIENT_ID,
|
|
26
|
-
CLI_LOCAL_SERVER_END_PORT,
|
|
27
|
-
CLI_LOCAL_SERVER_START_PORT,
|
|
28
|
-
CLI_REDIRECT_PATH,
|
|
29
|
-
SERVICE_ID,
|
|
30
|
-
AuthCallbackHandler,
|
|
31
|
-
clear_tokens,
|
|
32
|
-
get_auth_token,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
console = Console()
|
|
36
|
-
|
|
37
|
-
# HTTP request timeout (30 seconds)
|
|
38
|
-
HTTP_TIMEOUT = 30.0
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@click.group()
|
|
42
|
-
def auth():
|
|
43
|
-
"""Authentication commands (login, logout, whoami)."""
|
|
44
|
-
pass
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@retry(
|
|
48
|
-
stop=stop_after_attempt(3),
|
|
49
|
-
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
50
|
-
retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
|
|
51
|
-
reraise=True,
|
|
52
|
-
)
|
|
53
|
-
def _exchange_code_for_tokens_with_retry(code: str, code_verifier: str, redirect_uri: str) -> dict:
|
|
54
|
-
"""
|
|
55
|
-
Exchange authorization code for tokens with retry logic.
|
|
56
|
-
|
|
57
|
-
Returns token data dictionary.
|
|
58
|
-
"""
|
|
59
|
-
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
60
|
-
response = client.post(
|
|
61
|
-
f"{API_BASE_URL}/auth/token",
|
|
62
|
-
data={
|
|
63
|
-
"grant_type": "authorization_code",
|
|
64
|
-
"client_id": CLI_CLIENT_ID,
|
|
65
|
-
"code": code,
|
|
66
|
-
"code_verifier": code_verifier,
|
|
67
|
-
"redirect_uri": redirect_uri,
|
|
68
|
-
},
|
|
69
|
-
headers={"Accept": "application/json"},
|
|
70
|
-
)
|
|
71
|
-
response.raise_for_status()
|
|
72
|
-
|
|
73
|
-
# Safe JSON parsing with content-type check
|
|
74
|
-
content_type = response.headers.get("content-type", "")
|
|
75
|
-
if "application/json" not in content_type:
|
|
76
|
-
raise ValueError(f"Expected JSON response, got {content_type}")
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
token_data = response.json()
|
|
80
|
-
except (ValueError, TypeError) as e:
|
|
81
|
-
raise ValueError(f"Failed to parse JSON response: {e}")
|
|
82
|
-
|
|
83
|
-
return token_data
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@auth.command()
|
|
87
|
-
def login():
|
|
88
|
-
"""Login to Xenfra using Device Authorization Flow (like GitHub CLI, Claude Code)."""
|
|
89
|
-
from .auth_device import device_login
|
|
90
|
-
device_login()
|
|
91
|
-
|
|
92
|
-
# Removed old PKCE flow - now using Device Authorization Flow
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@auth.command()
|
|
96
|
-
def logout():
|
|
97
|
-
"""Logout and clear stored tokens."""
|
|
98
|
-
try:
|
|
99
|
-
clear_tokens()
|
|
100
|
-
console.print("[bold green]Logged out successfully.[/bold green]")
|
|
101
|
-
except Exception as e:
|
|
102
|
-
console.print(f"[yellow]Warning: Error during logout: {e}[/yellow]")
|
|
103
|
-
console.print("[dim]Tokens may still be stored in keyring.[/dim]")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@auth.command()
|
|
107
|
-
@click.option("--token", is_flag=True, help="Show access token")
|
|
108
|
-
def whoami(token):
|
|
109
|
-
"""Show current authenticated user."""
|
|
110
|
-
access_token = get_auth_token()
|
|
111
|
-
|
|
112
|
-
if not access_token:
|
|
113
|
-
console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
try:
|
|
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)
|
|
136
|
-
|
|
137
|
-
console.print("[bold green]Logged in as:[/bold green]")
|
|
138
|
-
console.print(f" Email: {claims.get('sub', 'N/A')}")
|
|
139
|
-
console.print(f" User ID: {claims.get('user_id', 'N/A')}")
|
|
140
|
-
|
|
141
|
-
if token:
|
|
142
|
-
console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
|
|
143
|
-
except Exception as e:
|
|
144
|
-
console.print(f"[bold red]Failed to decode token: {e}[/bold red]")
|
|
1
|
+
"""
|
|
2
|
+
Authentication commands for Xenfra CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import secrets
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import webbrowser
|
|
10
|
+
from http.server import HTTPServer
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import httpx
|
|
14
|
+
import keyring
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from tenacity import (
|
|
17
|
+
retry,
|
|
18
|
+
retry_if_exception_type,
|
|
19
|
+
stop_after_attempt,
|
|
20
|
+
wait_exponential,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ..utils.auth import (
|
|
24
|
+
API_BASE_URL,
|
|
25
|
+
CLI_CLIENT_ID,
|
|
26
|
+
CLI_LOCAL_SERVER_END_PORT,
|
|
27
|
+
CLI_LOCAL_SERVER_START_PORT,
|
|
28
|
+
CLI_REDIRECT_PATH,
|
|
29
|
+
SERVICE_ID,
|
|
30
|
+
AuthCallbackHandler,
|
|
31
|
+
clear_tokens,
|
|
32
|
+
get_auth_token,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
# HTTP request timeout (30 seconds)
|
|
38
|
+
HTTP_TIMEOUT = 30.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.group()
|
|
42
|
+
def auth():
|
|
43
|
+
"""Authentication commands (login, logout, whoami)."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@retry(
|
|
48
|
+
stop=stop_after_attempt(3),
|
|
49
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
50
|
+
retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
|
|
51
|
+
reraise=True,
|
|
52
|
+
)
|
|
53
|
+
def _exchange_code_for_tokens_with_retry(code: str, code_verifier: str, redirect_uri: str) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
Exchange authorization code for tokens with retry logic.
|
|
56
|
+
|
|
57
|
+
Returns token data dictionary.
|
|
58
|
+
"""
|
|
59
|
+
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
|
|
60
|
+
response = client.post(
|
|
61
|
+
f"{API_BASE_URL}/auth/token",
|
|
62
|
+
data={
|
|
63
|
+
"grant_type": "authorization_code",
|
|
64
|
+
"client_id": CLI_CLIENT_ID,
|
|
65
|
+
"code": code,
|
|
66
|
+
"code_verifier": code_verifier,
|
|
67
|
+
"redirect_uri": redirect_uri,
|
|
68
|
+
},
|
|
69
|
+
headers={"Accept": "application/json"},
|
|
70
|
+
)
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
|
|
73
|
+
# Safe JSON parsing with content-type check
|
|
74
|
+
content_type = response.headers.get("content-type", "")
|
|
75
|
+
if "application/json" not in content_type:
|
|
76
|
+
raise ValueError(f"Expected JSON response, got {content_type}")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
token_data = response.json()
|
|
80
|
+
except (ValueError, TypeError) as e:
|
|
81
|
+
raise ValueError(f"Failed to parse JSON response: {e}")
|
|
82
|
+
|
|
83
|
+
return token_data
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@auth.command()
|
|
87
|
+
def login():
|
|
88
|
+
"""Login to Xenfra using Device Authorization Flow (like GitHub CLI, Claude Code)."""
|
|
89
|
+
from .auth_device import device_login
|
|
90
|
+
device_login()
|
|
91
|
+
|
|
92
|
+
# Removed old PKCE flow - now using Device Authorization Flow
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@auth.command()
|
|
96
|
+
def logout():
|
|
97
|
+
"""Logout and clear stored tokens."""
|
|
98
|
+
try:
|
|
99
|
+
clear_tokens()
|
|
100
|
+
console.print("[bold green]Logged out successfully.[/bold green]")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
console.print(f"[yellow]Warning: Error during logout: {e}[/yellow]")
|
|
103
|
+
console.print("[dim]Tokens may still be stored in keyring.[/dim]")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@auth.command()
|
|
107
|
+
@click.option("--token", is_flag=True, help="Show access token")
|
|
108
|
+
def whoami(token):
|
|
109
|
+
"""Show current authenticated user."""
|
|
110
|
+
access_token = get_auth_token()
|
|
111
|
+
|
|
112
|
+
if not access_token:
|
|
113
|
+
console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
try:
|
|
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)
|
|
136
|
+
|
|
137
|
+
console.print("[bold green]Logged in as:[/bold green]")
|
|
138
|
+
console.print(f" Email: {claims.get('sub', 'N/A')}")
|
|
139
|
+
console.print(f" User ID: {claims.get('user_id', 'N/A')}")
|
|
140
|
+
|
|
141
|
+
if token:
|
|
142
|
+
console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
console.print(f"[bold red]Failed to decode token: {e}[/bold red]")
|