xenfra 0.4.2__py3-none-any.whl → 0.4.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/utils/security.py CHANGED
@@ -1,336 +1,336 @@
1
- """
2
- Security utilities for Xenfra CLI.
3
- Implements comprehensive URL validation, domain whitelisting, HTTPS enforcement, and certificate pinning.
4
- """
5
-
6
- import os
7
- import ssl
8
- from urllib.parse import urlparse
9
-
10
- import certifi
11
- import click
12
- import httpx
13
- from rich.console import Console
14
-
15
- console = Console()
16
-
17
- # Production API URL
18
- PRODUCTION_API_URL = "https://api.xenfra.tech"
19
-
20
- # Allowed domains (whitelist) - Solution 2
21
- ALLOWED_DOMAINS = [
22
- "api.xenfra.tech", # Production
23
- "api-staging.xenfra.tech", # Staging
24
- "localhost", # Local development
25
- "127.0.0.1", # Local development (IP)
26
- ]
27
-
28
- # Certificate fingerprints for pinning - Solution 4
29
- # These should be updated when certificates are rotated
30
- PINNED_CERTIFICATES = {
31
- "api.xenfra.tech": {
32
- # SHA256 fingerprint of the expected certificate
33
- # Example: "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
34
- # To get fingerprint, run:
35
- # openssl s_client -connect api.xenfra.tech:443 | openssl x509 -pubkey -noout \
36
- # | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
37
- "fingerprints": [], # Add actual fingerprints when you have production cert
38
- }
39
- }
40
-
41
-
42
- class SecurityConfig:
43
- """Security configuration for CLI."""
44
-
45
- def __init__(self):
46
- """Initialize security configuration from environment."""
47
- # PRODUCTION-ONLY: Default to production settings
48
- # Environment variable only used for self-hosted instances
49
- self.environment = "production"
50
-
51
- # Security settings - ALWAYS enforced for production safety
52
- self.enforce_https = True # Always require HTTPS
53
- self.enforce_whitelist = False # Allow self-hosted instances
54
- self.enable_cert_pinning = False # Disabled (see future-enhancements.md #3)
55
- self.warn_on_http = True # Always warn on HTTP
56
-
57
- def is_production(self) -> bool:
58
- """Check if running in production environment."""
59
- return self.environment == "production"
60
-
61
- def is_development(self) -> bool:
62
- """Check if running in development environment."""
63
- return self.environment in ["development", "dev", "local"]
64
-
65
-
66
- # Global security configuration
67
- security_config = SecurityConfig()
68
-
69
-
70
- def validate_url_format(url: str) -> dict:
71
- """
72
- Solution 1: Basic URL validation.
73
-
74
- Args:
75
- url: The URL to validate
76
-
77
- Returns:
78
- Parsed URL components
79
-
80
- Raises:
81
- ValueError: If URL format is invalid
82
- """
83
- try:
84
- parsed = urlparse(url)
85
-
86
- # Check scheme
87
- if parsed.scheme not in ["http", "https"]:
88
- raise ValueError(
89
- f"Invalid URL scheme '{parsed.scheme}'. " f"Only 'http' and 'https' are allowed."
90
- )
91
-
92
- # Check hostname exists
93
- if not parsed.hostname:
94
- raise ValueError("URL must include a hostname")
95
-
96
- # Check for malicious patterns
97
- if ".." in url or "@" in url:
98
- raise ValueError("URL contains suspicious characters")
99
-
100
- # Prevent URL with credentials (http://user:pass@host)
101
- if parsed.username or parsed.password:
102
- raise ValueError("URLs with embedded credentials are not allowed")
103
-
104
- return {
105
- "scheme": parsed.scheme,
106
- "hostname": parsed.hostname,
107
- "port": parsed.port,
108
- "url": url,
109
- }
110
-
111
- except Exception as e:
112
- raise ValueError(f"Invalid URL format: {e}")
113
-
114
-
115
- def check_domain_whitelist(hostname: str) -> bool:
116
- """
117
- Solution 2: Domain whitelist validation.
118
-
119
- Args:
120
- hostname: The hostname to check
121
-
122
- Returns:
123
- True if domain is whitelisted
124
-
125
- Raises:
126
- ValueError: If domain is not whitelisted and enforcement is enabled
127
- """
128
- is_whitelisted = hostname in ALLOWED_DOMAINS
129
-
130
- if not is_whitelisted:
131
- if security_config.enforce_whitelist:
132
- # Hard block in strict mode
133
- raise ValueError(
134
- f"Domain '{hostname}' is not in the whitelist.\n"
135
- f"Allowed domains: {', '.join(ALLOWED_DOMAINS)}\n\n"
136
- f"If you're using a self-hosted Xenfra instance:\n"
137
- f"1. Set XENFRA_ENFORCE_WHITELIST=false\n"
138
- f"2. Or contact support to whitelist your domain"
139
- )
140
- else:
141
- # Soft warning in permissive mode
142
- console.print(
143
- f"[yellow]⚠️ Warning: Domain '{hostname}' is not in the official whitelist.[/yellow]"
144
- )
145
- console.print(f"[dim]Whitelisted domains: {', '.join(ALLOWED_DOMAINS)}[/dim]")
146
- console.print("[yellow]Are you sure you want to connect to this API?[/yellow]")
147
-
148
- if not click.confirm("Continue?", default=False):
149
- raise click.Abort()
150
-
151
- return is_whitelisted
152
-
153
-
154
- def enforce_https(scheme: str, hostname: str) -> None:
155
- """
156
- Solution 3: HTTPS enforcement.
157
-
158
- Args:
159
- scheme: URL scheme (http/https)
160
- hostname: The hostname
161
-
162
- Raises:
163
- ValueError: If HTTPS is required but HTTP is used
164
- """
165
- # Development exception: localhost is OK with HTTP
166
- is_localhost = hostname in ["localhost", "127.0.0.1"]
167
-
168
- if scheme == "http" and not is_localhost:
169
- if security_config.enforce_https:
170
- # Hard block in production/strict mode
171
- raise ValueError(
172
- f"HTTPS is required (environment: {security_config.environment}).\n"
173
- f"Current URL uses insecure HTTP.\n\n"
174
- f"To fix:\n"
175
- f"1. Update XENFRA_API_URL to use https://\n"
176
- f"2. Or set XENFRA_ENFORCE_HTTPS=false (not recommended)"
177
- )
178
- elif security_config.warn_on_http:
179
- # Soft warning
180
- console.print("[bold yellow]⚠️ Security Warning: Using unencrypted HTTP![/bold yellow]")
181
- console.print(f"[yellow]Connecting to: {scheme}://{hostname}[/yellow]")
182
- console.print("[yellow]Your credentials and data will be sent in plain text.[/yellow]")
183
- console.print("[yellow]This should ONLY be used for local development.[/yellow]\n")
184
-
185
- if not click.confirm("Continue with insecure connection?", default=False):
186
- raise click.Abort()
187
-
188
-
189
- def create_secure_client(url: str, token: str = None) -> httpx.Client:
190
- """
191
- Solution 4: Create HTTP client with optional certificate pinning.
192
-
193
- Args:
194
- url: The base URL
195
- token: Optional authentication token
196
-
197
- Returns:
198
- Configured httpx.Client with security settings
199
- """
200
- parsed = urlparse(url)
201
- headers = {"Content-Type": "application/json"}
202
-
203
- if token:
204
- headers["Authorization"] = f"Bearer {token}"
205
-
206
- # Certificate pinning for production domains (if enabled)
207
- if security_config.enable_cert_pinning and parsed.hostname in PINNED_CERTIFICATES:
208
- console.print(f"[dim]Enabling certificate pinning for {parsed.hostname}[/dim]")
209
-
210
- # Create SSL context with certificate verification
211
- ssl_context = ssl.create_default_context(cafile=certifi.where())
212
- ssl_context.check_hostname = True
213
- ssl_context.verify_mode = ssl.CERT_REQUIRED
214
-
215
- # Note: Full certificate pinning implementation would require custom verification
216
- # For now, we use strict certificate validation with system CA bundle
217
- # Future enhancement: Certificate fingerprint pinning (see docs/future-enhancements.md #3)
218
-
219
- return httpx.Client(
220
- base_url=url,
221
- headers=headers,
222
- verify=ssl_context,
223
- timeout=30.0,
224
- )
225
- else:
226
- # Standard client with default certificate verification
227
- return httpx.Client(
228
- base_url=url,
229
- headers=headers,
230
- timeout=30.0,
231
- )
232
-
233
-
234
- def validate_and_get_api_url(url: str = None) -> str:
235
- """
236
- Comprehensive API URL validation (combines all 4 solutions).
237
-
238
- Args:
239
- url: Optional URL override (only for self-hosted instances)
240
-
241
- Returns:
242
- Validated API URL (defaults to https://api.xenfra.tech)
243
-
244
- Raises:
245
- ValueError: If URL fails validation
246
- click.Abort: If user cancels security prompts
247
- """
248
- # PRODUCTION DEFAULT: Use hardcoded production URL
249
- # Only check environment variable for self-hosted overrides
250
- if url is None:
251
- url = os.getenv("XENFRA_API_URL", PRODUCTION_API_URL)
252
-
253
- try:
254
- # Solution 1: Validate URL format
255
- parsed = validate_url_format(url)
256
-
257
- # Solution 2: Check domain whitelist
258
- check_domain_whitelist(parsed["hostname"])
259
-
260
- # Solution 3: Enforce HTTPS
261
- enforce_https(parsed["scheme"], parsed["hostname"])
262
-
263
- # Display security info ONLY in debug mode
264
- # Normal users shouldn't see this
265
- if os.getenv("DEBUG") or os.getenv("XENFRA_DEBUG"):
266
- console.print(f"[dim]🔒 Security: API URL validated: {url}[/dim]")
267
- console.print(f"[dim] Environment: {security_config.environment}[/dim]")
268
- console.print(f"[dim] HTTPS enforced: {security_config.enforce_https}[/dim]")
269
- console.print(f"[dim] Whitelist enforced: {security_config.enforce_whitelist}[/dim]")
270
- console.print(f"[dim] Cert pinning: {security_config.enable_cert_pinning}[/dim]")
271
-
272
- return url
273
-
274
- except ValueError as e:
275
- console.print("[bold red]🔒 Security Validation Failed:[/bold red]")
276
- console.print(f"[red]{e}[/red]")
277
- raise click.Abort()
278
-
279
-
280
- def display_security_info():
281
- """Display current security configuration."""
282
- console.print("\n[bold cyan]🔒 Security Configuration:[/bold cyan]")
283
-
284
- table_data = [
285
- ("Environment", security_config.environment),
286
- ("HTTPS Enforcement", "✅ Enabled" if security_config.enforce_https else "⚠️ Disabled"),
287
- (
288
- "Domain Whitelist",
289
- "✅ Enforced" if security_config.enforce_whitelist else "⚠️ Warning Only",
290
- ),
291
- ("HTTP Warning", "✅ Enabled" if security_config.warn_on_http else "❌ Disabled"),
292
- (
293
- "Certificate Pinning",
294
- "✅ Enabled" if security_config.enable_cert_pinning else "❌ Disabled",
295
- ),
296
- ]
297
-
298
- for key, value in table_data:
299
- console.print(f" {key}: {value}")
300
-
301
- console.print()
302
-
303
-
304
- # Environment variable documentation
305
- """
306
- PRODUCTION-FIRST DESIGN:
307
- The CLI defaults to production (api.xenfra.tech) with HTTPS enforcement.
308
- No configuration needed for normal users.
309
-
310
- Environment variables (for developers/self-hosted only):
311
-
312
- XENFRA_ENV=development
313
- - Enables local development mode
314
- - Allows HTTP, relaxes security
315
- - Default: production (safe by default)
316
-
317
- XENFRA_API_URL=https://your-instance.com
318
- - Override API URL for self-hosted instances
319
- - Default: https://api.xenfra.tech
320
-
321
- XENFRA_ENFORCE_HTTPS=true|false
322
- - Require HTTPS for all connections
323
- - Default: true (production), false (development)
324
-
325
- Example usage:
326
-
327
- # Production users (zero config):
328
- xenfra auth login
329
- xenfra deploy
330
-
331
- # Local development:
332
- XENFRA_ENV=development xenfra auth login
333
-
334
- # Self-hosted instance:
335
- XENFRA_API_URL=https://xenfra.mycompany.com xenfra login
336
- """
1
+ """
2
+ Security utilities for Xenfra CLI.
3
+ Implements comprehensive URL validation, domain whitelisting, HTTPS enforcement, and certificate pinning.
4
+ """
5
+
6
+ import os
7
+ import ssl
8
+ from urllib.parse import urlparse
9
+
10
+ import certifi
11
+ import click
12
+ import httpx
13
+ from rich.console import Console
14
+
15
+ console = Console()
16
+
17
+ # Production API URL
18
+ PRODUCTION_API_URL = "https://api.xenfra.tech"
19
+
20
+ # Allowed domains (whitelist) - Solution 2
21
+ ALLOWED_DOMAINS = [
22
+ "api.xenfra.tech", # Production
23
+ "api-staging.xenfra.tech", # Staging
24
+ "localhost", # Local development
25
+ "127.0.0.1", # Local development (IP)
26
+ ]
27
+
28
+ # Certificate fingerprints for pinning - Solution 4
29
+ # These should be updated when certificates are rotated
30
+ PINNED_CERTIFICATES = {
31
+ "api.xenfra.tech": {
32
+ # SHA256 fingerprint of the expected certificate
33
+ # Example: "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
34
+ # To get fingerprint, run:
35
+ # openssl s_client -connect api.xenfra.tech:443 | openssl x509 -pubkey -noout \
36
+ # | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
37
+ "fingerprints": [], # Add actual fingerprints when you have production cert
38
+ }
39
+ }
40
+
41
+
42
+ class SecurityConfig:
43
+ """Security configuration for CLI."""
44
+
45
+ def __init__(self):
46
+ """Initialize security configuration from environment."""
47
+ # PRODUCTION-ONLY: Default to production settings
48
+ # Environment variable only used for self-hosted instances
49
+ self.environment = "production"
50
+
51
+ # Security settings - ALWAYS enforced for production safety
52
+ self.enforce_https = True # Always require HTTPS
53
+ self.enforce_whitelist = False # Allow self-hosted instances
54
+ self.enable_cert_pinning = False # Disabled (see future-enhancements.md #3)
55
+ self.warn_on_http = True # Always warn on HTTP
56
+
57
+ def is_production(self) -> bool:
58
+ """Check if running in production environment."""
59
+ return self.environment == "production"
60
+
61
+ def is_development(self) -> bool:
62
+ """Check if running in development environment."""
63
+ return self.environment in ["development", "dev", "local"]
64
+
65
+
66
+ # Global security configuration
67
+ security_config = SecurityConfig()
68
+
69
+
70
+ def validate_url_format(url: str) -> dict:
71
+ """
72
+ Solution 1: Basic URL validation.
73
+
74
+ Args:
75
+ url: The URL to validate
76
+
77
+ Returns:
78
+ Parsed URL components
79
+
80
+ Raises:
81
+ ValueError: If URL format is invalid
82
+ """
83
+ try:
84
+ parsed = urlparse(url)
85
+
86
+ # Check scheme
87
+ if parsed.scheme not in ["http", "https"]:
88
+ raise ValueError(
89
+ f"Invalid URL scheme '{parsed.scheme}'. " f"Only 'http' and 'https' are allowed."
90
+ )
91
+
92
+ # Check hostname exists
93
+ if not parsed.hostname:
94
+ raise ValueError("URL must include a hostname")
95
+
96
+ # Check for malicious patterns
97
+ if ".." in url or "@" in url:
98
+ raise ValueError("URL contains suspicious characters")
99
+
100
+ # Prevent URL with credentials (http://user:pass@host)
101
+ if parsed.username or parsed.password:
102
+ raise ValueError("URLs with embedded credentials are not allowed")
103
+
104
+ return {
105
+ "scheme": parsed.scheme,
106
+ "hostname": parsed.hostname,
107
+ "port": parsed.port,
108
+ "url": url,
109
+ }
110
+
111
+ except Exception as e:
112
+ raise ValueError(f"Invalid URL format: {e}")
113
+
114
+
115
+ def check_domain_whitelist(hostname: str) -> bool:
116
+ """
117
+ Solution 2: Domain whitelist validation.
118
+
119
+ Args:
120
+ hostname: The hostname to check
121
+
122
+ Returns:
123
+ True if domain is whitelisted
124
+
125
+ Raises:
126
+ ValueError: If domain is not whitelisted and enforcement is enabled
127
+ """
128
+ is_whitelisted = hostname in ALLOWED_DOMAINS
129
+
130
+ if not is_whitelisted:
131
+ if security_config.enforce_whitelist:
132
+ # Hard block in strict mode
133
+ raise ValueError(
134
+ f"Domain '{hostname}' is not in the whitelist.\n"
135
+ f"Allowed domains: {', '.join(ALLOWED_DOMAINS)}\n\n"
136
+ f"If you're using a self-hosted Xenfra instance:\n"
137
+ f"1. Set XENFRA_ENFORCE_WHITELIST=false\n"
138
+ f"2. Or contact support to whitelist your domain"
139
+ )
140
+ else:
141
+ # Soft warning in permissive mode
142
+ console.print(
143
+ f"[yellow]⚠️ Warning: Domain '{hostname}' is not in the official whitelist.[/yellow]"
144
+ )
145
+ console.print(f"[dim]Whitelisted domains: {', '.join(ALLOWED_DOMAINS)}[/dim]")
146
+ console.print("[yellow]Are you sure you want to connect to this API?[/yellow]")
147
+
148
+ if not click.confirm("Continue?", default=False):
149
+ raise click.Abort()
150
+
151
+ return is_whitelisted
152
+
153
+
154
+ def enforce_https(scheme: str, hostname: str) -> None:
155
+ """
156
+ Solution 3: HTTPS enforcement.
157
+
158
+ Args:
159
+ scheme: URL scheme (http/https)
160
+ hostname: The hostname
161
+
162
+ Raises:
163
+ ValueError: If HTTPS is required but HTTP is used
164
+ """
165
+ # Development exception: localhost is OK with HTTP
166
+ is_localhost = hostname in ["localhost", "127.0.0.1"]
167
+
168
+ if scheme == "http" and not is_localhost:
169
+ if security_config.enforce_https:
170
+ # Hard block in production/strict mode
171
+ raise ValueError(
172
+ f"HTTPS is required (environment: {security_config.environment}).\n"
173
+ f"Current URL uses insecure HTTP.\n\n"
174
+ f"To fix:\n"
175
+ f"1. Update XENFRA_API_URL to use https://\n"
176
+ f"2. Or set XENFRA_ENFORCE_HTTPS=false (not recommended)"
177
+ )
178
+ elif security_config.warn_on_http:
179
+ # Soft warning
180
+ console.print("[bold yellow]⚠️ Security Warning: Using unencrypted HTTP![/bold yellow]")
181
+ console.print(f"[yellow]Connecting to: {scheme}://{hostname}[/yellow]")
182
+ console.print("[yellow]Your credentials and data will be sent in plain text.[/yellow]")
183
+ console.print("[yellow]This should ONLY be used for local development.[/yellow]\n")
184
+
185
+ if not click.confirm("Continue with insecure connection?", default=False):
186
+ raise click.Abort()
187
+
188
+
189
+ def create_secure_client(url: str, token: str = None) -> httpx.Client:
190
+ """
191
+ Solution 4: Create HTTP client with optional certificate pinning.
192
+
193
+ Args:
194
+ url: The base URL
195
+ token: Optional authentication token
196
+
197
+ Returns:
198
+ Configured httpx.Client with security settings
199
+ """
200
+ parsed = urlparse(url)
201
+ headers = {"Content-Type": "application/json"}
202
+
203
+ if token:
204
+ headers["Authorization"] = f"Bearer {token}"
205
+
206
+ # Certificate pinning for production domains (if enabled)
207
+ if security_config.enable_cert_pinning and parsed.hostname in PINNED_CERTIFICATES:
208
+ console.print(f"[dim]Enabling certificate pinning for {parsed.hostname}[/dim]")
209
+
210
+ # Create SSL context with certificate verification
211
+ ssl_context = ssl.create_default_context(cafile=certifi.where())
212
+ ssl_context.check_hostname = True
213
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
214
+
215
+ # Note: Full certificate pinning implementation would require custom verification
216
+ # For now, we use strict certificate validation with system CA bundle
217
+ # Future enhancement: Certificate fingerprint pinning (see docs/future-enhancements.md #3)
218
+
219
+ return httpx.Client(
220
+ base_url=url,
221
+ headers=headers,
222
+ verify=ssl_context,
223
+ timeout=30.0,
224
+ )
225
+ else:
226
+ # Standard client with default certificate verification
227
+ return httpx.Client(
228
+ base_url=url,
229
+ headers=headers,
230
+ timeout=30.0,
231
+ )
232
+
233
+
234
+ def validate_and_get_api_url(url: str = None) -> str:
235
+ """
236
+ Comprehensive API URL validation (combines all 4 solutions).
237
+
238
+ Args:
239
+ url: Optional URL override (only for self-hosted instances)
240
+
241
+ Returns:
242
+ Validated API URL (defaults to https://api.xenfra.tech)
243
+
244
+ Raises:
245
+ ValueError: If URL fails validation
246
+ click.Abort: If user cancels security prompts
247
+ """
248
+ # PRODUCTION DEFAULT: Use hardcoded production URL
249
+ # Only check environment variable for self-hosted overrides
250
+ if url is None:
251
+ url = os.getenv("XENFRA_API_URL", PRODUCTION_API_URL)
252
+
253
+ try:
254
+ # Solution 1: Validate URL format
255
+ parsed = validate_url_format(url)
256
+
257
+ # Solution 2: Check domain whitelist
258
+ check_domain_whitelist(parsed["hostname"])
259
+
260
+ # Solution 3: Enforce HTTPS
261
+ enforce_https(parsed["scheme"], parsed["hostname"])
262
+
263
+ # Display security info ONLY in debug mode
264
+ # Normal users shouldn't see this
265
+ if os.getenv("DEBUG") or os.getenv("XENFRA_DEBUG"):
266
+ console.print(f"[dim]🔒 Security: API URL validated: {url}[/dim]")
267
+ console.print(f"[dim] Environment: {security_config.environment}[/dim]")
268
+ console.print(f"[dim] HTTPS enforced: {security_config.enforce_https}[/dim]")
269
+ console.print(f"[dim] Whitelist enforced: {security_config.enforce_whitelist}[/dim]")
270
+ console.print(f"[dim] Cert pinning: {security_config.enable_cert_pinning}[/dim]")
271
+
272
+ return url
273
+
274
+ except ValueError as e:
275
+ console.print("[bold red]🔒 Security Validation Failed:[/bold red]")
276
+ console.print(f"[red]{e}[/red]")
277
+ raise click.Abort()
278
+
279
+
280
+ def display_security_info():
281
+ """Display current security configuration."""
282
+ console.print("\n[bold cyan]🔒 Security Configuration:[/bold cyan]")
283
+
284
+ table_data = [
285
+ ("Environment", security_config.environment),
286
+ ("HTTPS Enforcement", "✅ Enabled" if security_config.enforce_https else "⚠️ Disabled"),
287
+ (
288
+ "Domain Whitelist",
289
+ "✅ Enforced" if security_config.enforce_whitelist else "⚠️ Warning Only",
290
+ ),
291
+ ("HTTP Warning", "✅ Enabled" if security_config.warn_on_http else "❌ Disabled"),
292
+ (
293
+ "Certificate Pinning",
294
+ "✅ Enabled" if security_config.enable_cert_pinning else "❌ Disabled",
295
+ ),
296
+ ]
297
+
298
+ for key, value in table_data:
299
+ console.print(f" {key}: {value}")
300
+
301
+ console.print()
302
+
303
+
304
+ # Environment variable documentation
305
+ """
306
+ PRODUCTION-FIRST DESIGN:
307
+ The CLI defaults to production (api.xenfra.tech) with HTTPS enforcement.
308
+ No configuration needed for normal users.
309
+
310
+ Environment variables (for developers/self-hosted only):
311
+
312
+ XENFRA_ENV=development
313
+ - Enables local development mode
314
+ - Allows HTTP, relaxes security
315
+ - Default: production (safe by default)
316
+
317
+ XENFRA_API_URL=https://your-instance.com
318
+ - Override API URL for self-hosted instances
319
+ - Default: https://api.xenfra.tech
320
+
321
+ XENFRA_ENFORCE_HTTPS=true|false
322
+ - Require HTTPS for all connections
323
+ - Default: true (production), false (development)
324
+
325
+ Example usage:
326
+
327
+ # Production users (zero config):
328
+ xenfra auth login
329
+ xenfra deploy
330
+
331
+ # Local development:
332
+ XENFRA_ENV=development xenfra auth login
333
+
334
+ # Self-hosted instance:
335
+ XENFRA_API_URL=https://xenfra.mycompany.com xenfra login
336
+ """