corclient 0.1.0__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.
@@ -0,0 +1,393 @@
1
+ Metadata-Version: 2.4
2
+ Name: corclient
3
+ Version: 0.1.0
4
+ Summary: Internal CLI tool for microservices management, infrastructure administration and monitoring with AWS Cognito authentication
5
+ Author: Carlos Ferrer
6
+ Author-email: Carlos Ferrer <cferrer@projectcor.com>
7
+ Maintainer-email: Carlos Ferrer <cferrer@projectcor.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/ProjectCORTeam/corcli
10
+ Project-URL: Documentation, https://github.com/ProjectCORTeam/corcli#readme
11
+ Project-URL: Repository, https://github.com/ProjectCORTeam/corcli.git
12
+ Project-URL: Bug Tracker, https://github.com/ProjectCORTeam/corcli/issues
13
+ Project-URL: Changelog, https://github.com/ProjectCORTeam/corcli/releases
14
+ Keywords: cli,microservices,infrastructure,devops,monitoring,aws,internal-tools
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: System Administrators
18
+ Classifier: Intended Audience :: Information Technology
19
+ Classifier: Topic :: System :: Monitoring
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Classifier: Topic :: Software Development :: Build Tools
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
23
+ Classifier: Topic :: Utilities
24
+ Classifier: License :: OSI Approved :: MIT License
25
+ Classifier: Programming Language :: Python :: 3
26
+ Classifier: Programming Language :: Python :: 3.9
27
+ Classifier: Programming Language :: Python :: 3.10
28
+ Classifier: Programming Language :: Python :: 3.11
29
+ Classifier: Programming Language :: Python :: 3.12
30
+ Classifier: Operating System :: OS Independent
31
+ Classifier: Environment :: Console
32
+ Classifier: Natural Language :: English
33
+ Requires-Python: >=3.10
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: typer>=0.12
37
+ Requires-Dist: rich>=13
38
+ Requires-Dist: flask>=2.3
39
+ Requires-Dist: requests>=2.31
40
+ Provides-Extra: dev
41
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
42
+ Requires-Dist: black>=23.0.0; extra == "dev"
43
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
44
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
45
+ Requires-Dist: types-requests>=2.31.0; extra == "dev"
46
+ Dynamic: author
47
+ Dynamic: license-file
48
+ Dynamic: requires-python
49
+
50
+ # CoreCLI
51
+
52
+ [![PyPI version](https://img.shields.io/pypi/v/corclient.svg)](https://pypi.org/project/corclient/)
53
+ [![Python versions](https://img.shields.io/pypi/pyversions/corclient.svg)](https://pypi.org/project/corclient/)
54
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
55
+
56
+ Internal CLI tool for microservices management, infrastructure administration and monitoring with AWS Cognito authentication.
57
+
58
+ ## Features
59
+
60
+ - 🔐 **AWS Cognito Authentication** - Secure authentication using PKCE (Proof Key for Code Exchange) flow
61
+ - 🚀 **Microservices Management** - Interact with internal microservices infrastructure
62
+ - 📊 **Basic Monitoring** - Monitor and manage microservices health
63
+ - 🔄 **Token Management** - Automatic token refresh and session handling
64
+ - 💻 **CLI-First Design** - Built with modern CLI best practices using Typer
65
+
66
+ ## Requirements
67
+
68
+ - Python 3.9 or higher
69
+ - AWS Cognito user pool configured with Hosted UI
70
+ - Valid AWS Cognito credentials
71
+
72
+ ## Installation
73
+
74
+ ### Via pip (Recommended)
75
+
76
+ ```bash
77
+ pip install corclient
78
+ ```
79
+
80
+ ### Via Homebrew (macOS/Linux)
81
+
82
+ ```bash
83
+ # Add the tap
84
+ brew tap ProjectCORTeam/corcli https://github.com/ProjectCORTeam/corcli.git
85
+
86
+ # Install
87
+ brew install corclient
88
+ ```
89
+
90
+ To update:
91
+ ```bash
92
+ brew update && brew upgrade corclient
93
+ ```
94
+
95
+ ### Development Installation
96
+
97
+ ```bash
98
+ git clone https://github.com/ProjectCORTeam/corcli.git
99
+ cd corcli
100
+ pip install -e .
101
+ ```
102
+
103
+ ## Quick Start
104
+
105
+ ### 1. Login
106
+
107
+ Authenticate with AWS Cognito using the Hosted UI:
108
+
109
+ ```bash
110
+ cor login
111
+ ```
112
+
113
+ This will:
114
+ - Open your browser to the Cognito Hosted UI
115
+ - Start a local callback server
116
+ - Complete the PKCE flow
117
+ - Store tokens securely
118
+
119
+ #### Custom Redirect URI
120
+
121
+ If you need to redirect to a custom app scheme:
122
+
123
+ ```bash
124
+ cor login --app-redirect myapp://callback
125
+ ```
126
+
127
+ ### 2. Check Authentication Status
128
+
129
+ View your current authentication details:
130
+
131
+ ```bash
132
+ cor whoami
133
+ ```
134
+
135
+ This displays:
136
+ - User information from the ID token
137
+ - Token expiration time
138
+ - Active session details
139
+
140
+ ### 3. Refresh Tokens
141
+
142
+ Manually refresh your access tokens:
143
+
144
+ ```bash
145
+ cor refresh
146
+ ```
147
+
148
+ The CLI automatically refreshes tokens when needed, but you can manually trigger a refresh if required.
149
+
150
+ ### 4. Logout
151
+
152
+ Clear local session and revoke tokens:
153
+
154
+ ```bash
155
+ cor logout
156
+ ```
157
+
158
+ This will:
159
+ - Revoke active tokens with Cognito
160
+ - Clear local token storage
161
+ - End your authentication session
162
+
163
+ ## Configuration
164
+
165
+ CoreCLI requires the following environment variables or configuration:
166
+
167
+ ```bash
168
+ # AWS Cognito Configuration
169
+ COGNITO_DOMAIN=your-domain.auth.region.amazoncognito.com
170
+ COGNITO_CLIENT_ID=your-client-id
171
+ COGNITO_REDIRECT_URI=http://localhost:8080/callback
172
+ ```
173
+
174
+ Configuration can be set via:
175
+ - Environment variables
176
+ - Configuration file (`.corecli/config`)
177
+ - Command-line arguments
178
+
179
+ ## Usage Examples
180
+
181
+ ### Basic Authentication Flow
182
+
183
+ ```bash
184
+ # Login to Cognito
185
+ cor login
186
+
187
+ # Check who is logged in
188
+ cor whoami
189
+
190
+ # When done, logout
191
+ cor logout
192
+ ```
193
+
194
+ ### Using with Custom Applications
195
+
196
+ ```bash
197
+ # Login and redirect to custom app
198
+ cor login --app-redirect myapp://authenticated
199
+
200
+ # The CLI will handle authentication and redirect to your app
201
+ ```
202
+
203
+ ## Command Reference
204
+
205
+ | Command | Description | Options |
206
+ |---------|-------------|---------|
207
+ | `cor login` | Authenticate with Cognito | `--app-redirect, -r` - Custom redirect URI |
208
+ | `cor whoami` | Show authenticated user info | None |
209
+ | `cor refresh` | Refresh authentication tokens | None |
210
+ | `cor logout` | End session and revoke tokens | None |
211
+
212
+ ## Development
213
+
214
+ ### Setup Development Environment
215
+
216
+ ```bash
217
+ # Clone repository
218
+ git clone https://github.com/ProjectCORTeam/corcli.git
219
+ cd corcli
220
+
221
+ # Install in development mode with dev dependencies
222
+ pip install -e ".[dev]"
223
+
224
+ # Install pre-commit hooks (optional but recommended)
225
+ pip install pre-commit
226
+ pre-commit install
227
+
228
+ # Run the CLI
229
+ cor --help
230
+ ```
231
+
232
+ ### Development Tools
233
+
234
+ The project uses modern Python development tools:
235
+
236
+ - **Ruff** - Fast Python linter (replaces flake8, isort, and more)
237
+ - **Black** - Code formatter
238
+ - **MyPy** - Static type checker
239
+ - **Pre-commit** - Git hooks for automated checks
240
+
241
+ ### Running Checks Locally
242
+
243
+ ```bash
244
+ # Run linting
245
+ make lint
246
+
247
+ # Fix linting issues automatically
248
+ make lint-fix
249
+
250
+ # Format code
251
+ make format
252
+
253
+ # Check formatting (without modifying)
254
+ make format-check
255
+
256
+ # Run type checking
257
+ make type-check
258
+
259
+ # Build and verify package
260
+ make build
261
+
262
+ # Run all checks
263
+ make all
264
+
265
+ # Clean build artifacts
266
+ make clean
267
+ ```
268
+
269
+ Or manually:
270
+
271
+ ```bash
272
+ # Linting
273
+ ruff check corecli/
274
+
275
+ # Formatting
276
+ black corecli/
277
+
278
+ # Type checking
279
+ mypy corecli/
280
+
281
+ # Build
282
+ python -m build
283
+ twine check dist/*
284
+ ```
285
+
286
+ ### Project Structure
287
+
288
+ ```
289
+ corecli/
290
+ ├── corecli/
291
+ │ ├── __init__.py # Package initialization
292
+ │ ├── cli.py # CLI commands and interface
293
+ │ ├── auth.py # Authentication logic
294
+ │ ├── pkce.py # PKCE flow implementation
295
+ │ └── utils.py # Utility functions
296
+ ├── pyproject.toml # Project metadata
297
+ ├── setup.py # Package setup
298
+ └── README.md # This file
299
+ ```
300
+
301
+ ## Security Considerations
302
+
303
+ - **PKCE Flow**: Uses the industry-standard PKCE flow for secure OAuth 2.0 authentication
304
+ - **Token Storage**: Tokens are stored securely in the local user directory
305
+ - **Automatic Refresh**: Access tokens are automatically refreshed before expiration
306
+ - **Session Management**: Proper logout flow revokes tokens server-side
307
+
308
+ ## Troubleshooting
309
+
310
+ ### Authentication Fails
311
+
312
+ If authentication fails, check:
313
+ 1. Cognito domain and client ID are correct
314
+ 2. Redirect URI is whitelisted in Cognito
315
+ 3. User pool has Hosted UI enabled
316
+ 4. Network connectivity to Cognito endpoints
317
+
318
+ ### Token Refresh Issues
319
+
320
+ If token refresh fails:
321
+ 1. Check that refresh token hasn't expired
322
+ 2. Verify Cognito client settings allow refresh tokens
323
+ 3. Try logging out and logging in again
324
+
325
+ ### Port Already in Use
326
+
327
+ If the callback server can't start:
328
+ ```bash
329
+ # The default port (8080) might be in use
330
+ # Kill the process or configure a different port
331
+ ```
332
+
333
+ ## Contributing
334
+
335
+ This is an internal tool for ProjectCOR Team. If you're a team member:
336
+
337
+ 1. Fork the repository
338
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
339
+ 3. Make your changes and ensure they pass all checks:
340
+ ```bash
341
+ make lint
342
+ make format
343
+ make type-check
344
+ make build
345
+ ```
346
+ 4. Commit your changes using [Conventional Commits](https://www.conventionalcommits.org/):
347
+ ```bash
348
+ git commit -m 'feat: add amazing feature'
349
+ git commit -m 'fix: resolve authentication issue'
350
+ git commit -m 'docs: update README'
351
+ ```
352
+ 5. Push to the branch (`git push origin feature/amazing-feature`)
353
+ 6. Open a Pull Request
354
+
355
+ ### Quality Checks
356
+
357
+ All pull requests must pass:
358
+ - ✅ **Ruff linting** - Code quality and style checks
359
+ - ✅ **Black formatting** - Consistent code formatting
360
+ - ✅ **MyPy type checking** - Static type validation (warnings allowed)
361
+ - ✅ **Package build** - Successful package creation
362
+ - ✅ **Twine validation** - PyPI metadata verification
363
+
364
+ These checks run automatically on every release.
365
+
366
+ ## Versioning
367
+
368
+ We use [Semantic Versioning](https://semver.org/) via semantic-release. Versions are automatically generated based on commit messages following [Conventional Commits](https://www.conventionalcommits.org/).
369
+
370
+ ### Commit Message Format
371
+
372
+ - `feat:` - New feature (minor version bump)
373
+ - `fix:` - Bug fix (patch version bump)
374
+ - `feat!:` or `BREAKING CHANGE:` - Breaking change (major version bump)
375
+
376
+ ## License
377
+
378
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
379
+
380
+ ## Support
381
+
382
+ For internal support:
383
+ - Report issues: [GitHub Issues](https://github.com/ProjectCORTeam/corcli/issues)
384
+ - Documentation: [GitHub Wiki](https://github.com/ProjectCORTeam/corcli#readme)
385
+ - Releases: [GitHub Releases](https://github.com/ProjectCORTeam/corcli/releases)
386
+
387
+ ## Authors
388
+
389
+ - **Carlos Ferrer** - *Initial work* - [ProjectCOR Team](https://github.com/ProjectCORTeam)
390
+
391
+ ---
392
+
393
+ Built with ❤️ by the ProjectCOR Team
@@ -0,0 +1,12 @@
1
+ corclient-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ corecli/__init__.py,sha256=YahZFfcGjK3dKwjCDZ9v5ZZ9R6-G3Nr28_3djZUh2kY,138
3
+ corecli/_version.py,sha256=KOV2vc17OypCoLGU5NvcmuqpYxasPtk2grDJBCoULbY,264
4
+ corecli/auth.py,sha256=F7Hq1TiFhsQ0Ai_e6TzlTEyXutbda-aX9_Q7QA6nzv4,13501
5
+ corecli/cli.py,sha256=mwA7pOqr96MtXL0ROIQh2Wy66_3XB_GvtzadXzGX3V0,955
6
+ corecli/pkce.py,sha256=eZkbryW2A74wFSr9ark-fU-wQeC6Qh1LXhHiBj5Wo7g,435
7
+ corecli/utils.py,sha256=9NDqjMEM5rxyZQR7FK3ivg4NLa581AeUEJN8lTBNG_I,1136
8
+ corclient-0.1.0.dist-info/METADATA,sha256=qBQKJST1Id8I3aTxMxFplKVoPRoeLkrlEz5aG-CxY7c,10187
9
+ corclient-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ corclient-0.1.0.dist-info/entry_points.txt,sha256=kVyCipMY9yXvgIMJnwONt8v-_ZfMbUPlRO7MzpuwbfQ,40
11
+ corclient-0.1.0.dist-info/top_level.txt,sha256=JoPv54lgmqVztIG_JrKHt7hDhK8KTGidli2_NfpK-4o,8
12
+ corclient-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cor = corecli.cli:app
File without changes
@@ -0,0 +1 @@
1
+ corecli
corecli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """CoreCLI - CLI para autenticación con AWS Cognito usando PKCE."""
2
+
3
+ from corecli._version import __version__
4
+
5
+ __all__ = ["__version__"]
corecli/_version.py ADDED
@@ -0,0 +1,9 @@
1
+ # File generated by setuptools-scm
2
+ # DO NOT EDIT - version is managed by Git tags
3
+
4
+ try:
5
+ from setuptools_scm import get_version
6
+
7
+ __version__ = get_version(root="..", relative_to=__file__)
8
+ except (ImportError, LookupError):
9
+ __version__ = "0.0.0+unknown"
corecli/auth.py ADDED
@@ -0,0 +1,440 @@
1
+ import json
2
+ import sys
3
+ import webbrowser
4
+ from http.server import BaseHTTPRequestHandler, HTTPServer
5
+ from typing import Optional
6
+ from urllib.parse import parse_qs, urlparse
7
+
8
+ import requests
9
+ from rich import box
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from corecli.pkce import generate_pkce_pair
15
+ from corecli.utils import clear_tokens, decode_jwt, load_tokens, save_tokens
16
+
17
+ console = Console()
18
+
19
+
20
+ # =====================
21
+ # CONFIG FIJA (NO SE MODIFICA)
22
+ # =====================
23
+ CLIENT_ID = "1gd8tf85qs07ra4b38vcm4mih5" # <-- tu Client ID
24
+ COGNITO_DOMAIN = "https://corcli.projectcor.com"
25
+ REDIRECT_URI = "http://localhost:8000/callback"
26
+ SCOPES = ["openid", "email", "profile"]
27
+ LOGOUT_REDIRECT_URI = "http://localhost:8000/logout"
28
+
29
+
30
+ # =====================
31
+ # HTTP HANDLER PARA CALLBACK
32
+ # =====================
33
+ class CallbackHandler(BaseHTTPRequestHandler):
34
+ def log_message(self, format, *args):
35
+ # Silencia logs de acceso/informativos
36
+ return
37
+
38
+ def log_error(self, format, *args):
39
+ # Muestra únicamente errores a stderr
40
+ try:
41
+ sys.stderr.write(
42
+ f"{self.address_string()} - - [{self.log_date_time_string()}] ERROR: {format % args}\n"
43
+ )
44
+ except Exception:
45
+ pass
46
+
47
+ def do_GET(self):
48
+ if self.path.startswith("/callback"):
49
+ query = parse_qs(urlparse(self.path).query)
50
+ self.server.auth_code = query.get("code", [None])[0]
51
+ app_redirect = getattr(self.server, "app_redirect", None)
52
+ js_app_redirect = json.dumps(app_redirect) if app_redirect else "null"
53
+
54
+ self.send_response(200)
55
+ self.send_header("Content-Type", "text/html; charset=utf-8")
56
+ self.end_headers()
57
+
58
+ html = f"""
59
+ <!doctype html>
60
+ <html lang=\"es\">
61
+ <head>
62
+ <meta charset=\"utf-8\" />
63
+ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
64
+ <title>CoreCLI · Login exitoso</title>
65
+ <style>
66
+ :root {{
67
+ --bg: #0b1020;
68
+ --card: #121a33;
69
+ --accent: #8ab4f8;
70
+ --ok: #34d399;
71
+ --text: #e5e7eb;
72
+ --muted: #94a3b8;
73
+ }}
74
+ * {{ box-sizing: border-box; }}
75
+ body {{
76
+ margin: 0;
77
+ min-height: 100vh;
78
+ display: grid;
79
+ place-items: center;
80
+ background: radial-gradient(1200px 600px at 20% -10%, #1f2a52 0%, #0b1020 70%);
81
+ color: var(--text);
82
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial, sans-serif;
83
+ }}
84
+ .card {{
85
+ width: min(680px, 92vw);
86
+ background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
87
+ border: 1px solid rgba(255,255,255,0.08);
88
+ border-radius: 16px;
89
+ padding: 28px 24px;
90
+ box-shadow: 0 20px 60px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06);
91
+ backdrop-filter: blur(6px);
92
+ }}
93
+ h1 {{
94
+ margin: 0 0 8px;
95
+ font-size: 24px;
96
+ letter-spacing: 0.2px;
97
+ }}
98
+ p {{
99
+ margin: 6px 0;
100
+ color: var(--muted);
101
+ line-height: 1.6;
102
+ }}
103
+ .ok {{ color: var(--ok); font-weight: 600; }}
104
+ .meta {{
105
+ margin-top: 16px;
106
+ display: grid;
107
+ grid-template-columns: 1fr 1fr;
108
+ gap: 8px 16px;
109
+ font-size: 14px;
110
+ color: #c7d2fe;
111
+ }}
112
+ .countdown {{
113
+ margin-top: 14px;
114
+ padding: 10px 12px;
115
+ border: 1px dashed rgba(138,180,248,0.4);
116
+ border-radius: 10px;
117
+ background: rgba(26, 35, 75, 0.35);
118
+ color: var(--accent);
119
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
120
+ }}
121
+ .small {{ font-size: 13px; color: var(--muted); }}
122
+ a.btn {{
123
+ display: inline-block;
124
+ margin-top: 16px;
125
+ padding: 10px 14px;
126
+ border-radius: 10px;
127
+ background: #22315f;
128
+ color: #e8eeff;
129
+ text-decoration: none;
130
+ border: 1px solid rgba(138,180,248,0.25);
131
+ }}
132
+ a.btn:hover {{ background: #2b3c70; }}
133
+ </style>
134
+ <script>
135
+ const appRedirect = {js_app_redirect};
136
+ if (appRedirect) {{
137
+ // Intenta volver a la app que abrió el login
138
+ setTimeout(() => {{ window.location.href = appRedirect; }}, 0);
139
+ }}
140
+ let secs = 10;
141
+ function tick() {{
142
+ const el = document.getElementById('secs');
143
+ if (el) el.textContent = secs.toString();
144
+ secs -= 1;
145
+ if (secs < 0) {{
146
+ try {{
147
+ window.open('', '_self');
148
+ window.close();
149
+ }} catch (e) {{}}
150
+ }} else {{
151
+ setTimeout(tick, 1000);
152
+ }}
153
+ }}
154
+ window.addEventListener('DOMContentLoaded', tick);
155
+ </script>
156
+ <meta http-equiv=\"refresh\" content=\"35;url=about:blank\">
157
+ <!-- meta de respaldo: si window.close falla, al menos salimos de la página -->
158
+ <link rel=\"icon\" href=\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><circle cx='32' cy='32' r='30' fill='%238ab4f8'/><path d='M20 34l7 7 17-17' stroke='white' stroke-width='6' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>\" />
159
+
160
+ </head>
161
+ <body>
162
+ <div class=\"card\">
163
+ <h1>✅ Login exitoso</h1>
164
+ <p>Ya puedes volver a la terminal. Esta pestaña se cerrará automáticamente.</p>
165
+ <div class=\"countdown\">Cerrando en <span id=\"secs\">10</span> segundos…</div>
166
+ <p class=\"small\">Si no se cierra por políticas del navegador, puedes cerrarla manualmente.</p>
167
+ <a class=\"btn\" href=\"about:blank\">Cerrar ahora</a>
168
+ </div>
169
+ </body>
170
+ </html>
171
+ """
172
+ self.wfile.write(html.encode("utf-8"))
173
+ elif self.path.startswith("/logout"):
174
+ self.send_response(200)
175
+ self.send_header("Content-Type", "text/html; charset=utf-8")
176
+ self.end_headers()
177
+
178
+ html = """
179
+ <!doctype html>
180
+ <html lang=\"es\">
181
+ <head>
182
+ <meta charset=\"utf-8\" />
183
+ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
184
+ <title>CoreCLI · Logout</title>
185
+ <style>
186
+ :root {
187
+ --bg: #0b1020;
188
+ --card: #121a33;
189
+ --accent: #fca5a5;
190
+ --ok: #34d399;
191
+ --text: #e5e7eb;
192
+ --muted: #94a3b8;
193
+ }
194
+ * { box-sizing: border-box; }
195
+ body {
196
+ margin: 0;
197
+ min-height: 100vh;
198
+ display: grid;
199
+ place-items: center;
200
+ background: radial-gradient(1200px 600px at 20% -10%, #1f2a52 0%, #0b1020 70%);
201
+ color: var(--text);
202
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial, sans-serif;
203
+ }
204
+ .card {
205
+ width: min(680px, 92vw);
206
+ background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
207
+ border: 1px solid rgba(255,255,255,0.08);
208
+ border-radius: 16px;
209
+ padding: 28px 24px;
210
+ box-shadow: 0 20px 60px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06);
211
+ backdrop-filter: blur(6px);
212
+ }
213
+ h1 {
214
+ margin: 0 0 8px;
215
+ font-size: 24px;
216
+ letter-spacing: 0.2px;
217
+ }
218
+ p {
219
+ margin: 6px 0;
220
+ color: var(--muted);
221
+ line-height: 1.6;
222
+ }
223
+ .countdown {
224
+ margin-top: 14px;
225
+ padding: 10px 12px;
226
+ border: 1px dashed rgba(252,165,165,0.35);
227
+ border-radius: 10px;
228
+ background: rgba(75, 26, 35, 0.25);
229
+ color: var(--accent);
230
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
231
+ }
232
+ .small { font-size: 13px; color: var(--muted); }
233
+ a.btn {
234
+ display: inline-block;
235
+ margin-top: 16px;
236
+ padding: 10px 14px;
237
+ border-radius: 10px;
238
+ background: #5f2222;
239
+ color: #ffe8e8;
240
+ text-decoration: none;
241
+ border: 1px solid rgba(252,165,165,0.35);
242
+ }
243
+ a.btn:hover { background: #703030; }
244
+ </style>
245
+ <script>
246
+ let secs = 5;
247
+ function tick() {
248
+ const el = document.getElementById('secs');
249
+ if (el) el.textContent = secs.toString();
250
+ secs -= 1;
251
+ if (secs < 0) {
252
+ try {
253
+ window.open('', '_self');
254
+ window.close();
255
+ } catch (e) {}
256
+ } else {
257
+ setTimeout(tick, 1000);
258
+ }
259
+ }
260
+ window.addEventListener('DOMContentLoaded', tick);
261
+ </script>
262
+ <meta http-equiv=\"refresh\" content=\"10;url=about:blank\">
263
+ </head>
264
+ <body>
265
+ <div class=\"card\">
266
+ <h1>👋 Sesión cerrada</h1>
267
+ <p>Ya puedes volver a la terminal. Esta pestaña se cerrará automáticamente.</p>
268
+ <div class=\"countdown\">Cerrando en <span id=\"secs\">5</span> segundos…</div>
269
+ <a class=\"btn\" href=\"about:blank\">Cerrar ahora</a>
270
+ </div>
271
+ </body>
272
+ </html>
273
+ """
274
+ self.wfile.write(html.encode("utf-8"))
275
+
276
+
277
+ def start_local_server(app_redirect: Optional[str] = None):
278
+ server = HTTPServer(("localhost", 8000), CallbackHandler)
279
+ # adjunta destino para que la página callback pueda redirigir
280
+ server.app_redirect = app_redirect # type: ignore[attr-defined]
281
+ server.handle_request()
282
+ return server.auth_code # type: ignore[attr-defined]
283
+
284
+
285
+ # =====================
286
+ # AUTH FLOW
287
+ # =====================
288
+ def login(app_redirect: Optional[str] = None):
289
+ code_verifier, code_challenge = generate_pkce_pair()
290
+
291
+ auth_url = (
292
+ f"{COGNITO_DOMAIN}/oauth2/authorize?"
293
+ f"response_type=code&client_id={CLIENT_ID}"
294
+ f"&redirect_uri={REDIRECT_URI}"
295
+ f"&scope={' '.join(SCOPES)}"
296
+ f"&code_challenge={code_challenge}&code_challenge_method=S256"
297
+ )
298
+
299
+ console.print(
300
+ Panel.fit(
301
+ "Abriendo navegador para autenticación…\n\n"
302
+ "Si no se abre automáticamente, copia y pega esta URL:\n"
303
+ f"[cyan]{auth_url}[/cyan]",
304
+ title="[bold cyan]CoreCLI · Login",
305
+ border_style="cyan",
306
+ )
307
+ )
308
+ webbrowser.open(auth_url)
309
+
310
+ auth_code = start_local_server(app_redirect=app_redirect)
311
+ if not auth_code:
312
+ console.print(
313
+ Panel(
314
+ "No se recibió el código de autorización.", title="[red]Error", border_style="red"
315
+ )
316
+ )
317
+ raise Exception("No se recibió el código de autorización.")
318
+
319
+ token_url = f"{COGNITO_DOMAIN}/oauth2/token"
320
+ data = {
321
+ "grant_type": "authorization_code",
322
+ "client_id": CLIENT_ID,
323
+ "redirect_uri": REDIRECT_URI,
324
+ "code": auth_code,
325
+ "code_verifier": code_verifier,
326
+ }
327
+
328
+ resp = requests.post(
329
+ token_url, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}
330
+ )
331
+ resp.raise_for_status()
332
+ tokens = resp.json()
333
+
334
+ save_tokens(tokens)
335
+ console.print(
336
+ Panel.fit(
337
+ "✅ Autenticación exitosa, tokens guardados.", border_style="green", title="[green]OK"
338
+ )
339
+ )
340
+
341
+
342
+ def refresh_tokens():
343
+ tokens = load_tokens()
344
+ if not tokens or "refresh_token" not in tokens:
345
+ console.print(
346
+ Panel.fit(
347
+ "No hay refresh token guardado. Ejecuta `core-login login` para autenticarse de nuevo.",
348
+ title="[yellow]Aviso",
349
+ border_style="yellow",
350
+ )
351
+ )
352
+ return
353
+
354
+ token_url = f"{COGNITO_DOMAIN}/oauth2/token"
355
+ data = {
356
+ "grant_type": "refresh_token",
357
+ "client_id": CLIENT_ID,
358
+ "refresh_token": tokens["refresh_token"],
359
+ }
360
+
361
+ resp = requests.post(
362
+ token_url, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}
363
+ )
364
+ resp.raise_for_status()
365
+ new_tokens = resp.json()
366
+
367
+ if "refresh_token" not in new_tokens:
368
+ new_tokens["refresh_token"] = tokens["refresh_token"]
369
+
370
+ save_tokens(new_tokens)
371
+ console.print(
372
+ Panel.fit("🔄 Tokens refrescados correctamente.", border_style="green", title="[green]OK")
373
+ )
374
+
375
+
376
+ def whoami():
377
+ tokens = load_tokens()
378
+ if not tokens or "id_token" not in tokens:
379
+ console.print(
380
+ Panel.fit(
381
+ "No hay sesión activa. Ejecuta `core-login login` primero.",
382
+ title="[yellow]Aviso",
383
+ border_style="yellow",
384
+ )
385
+ )
386
+ return
387
+
388
+ id_token = tokens["id_token"]
389
+ claims = decode_jwt(id_token)
390
+
391
+ table = Table(
392
+ title="👤 Usuario autenticado",
393
+ show_header=False,
394
+ box=box.SIMPLE,
395
+ padding=(0, 1),
396
+ border_style="cyan",
397
+ )
398
+ table.add_row("sub", f"[white]{claims.get('sub')}[/white]")
399
+ table.add_row("email", f"[white]{claims.get('email')}[/white]")
400
+ table.add_row("username", f"[white]{claims.get('cognito:username')}[/white]")
401
+ console.print(table)
402
+
403
+
404
+ def logout(local_only: bool = True):
405
+ """Cierra sesión local: revoca refresh token (best-effort) y borra tokens locales."""
406
+ tokens = load_tokens()
407
+
408
+ # Revocación opcional de refresh_token (si el pool lo permite)
409
+ try:
410
+ if tokens and tokens.get("refresh_token"):
411
+ revoke_url = f"{COGNITO_DOMAIN}/oauth2/revoke"
412
+ data = {
413
+ "token": tokens["refresh_token"],
414
+ "client_id": CLIENT_ID,
415
+ }
416
+ # best-effort
417
+ requests.post(
418
+ revoke_url,
419
+ data=data,
420
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
421
+ timeout=5,
422
+ )
423
+ except Exception:
424
+ pass
425
+
426
+ # Borra tokens locales
427
+ cleared = clear_tokens()
428
+
429
+ # Ya no se abre Hosted UI: logout 100% local
430
+
431
+ if cleared:
432
+ console.print(
433
+ Panel.fit("🧹 Tokens locales eliminados.", border_style="green", title="[green]OK")
434
+ )
435
+ else:
436
+ console.print(
437
+ Panel.fit(
438
+ "No se pudieron borrar los tokens locales.", border_style="red", title="[red]Aviso"
439
+ )
440
+ )
corecli/cli.py ADDED
@@ -0,0 +1,43 @@
1
+ from typing import Optional
2
+
3
+ import typer
4
+
5
+ from corecli import auth
6
+
7
+ app = typer.Typer(help="CoreCLI - CLI de ejemplo con login en Cognito usando PKCE")
8
+
9
+
10
+ @app.command()
11
+ def login(
12
+ app_redirect: Optional[str] = typer.Option(
13
+ None,
14
+ "--app-redirect",
15
+ "-r",
16
+ help="URL o esquema (p.ej. myapp://callback) al que redirigir tras login",
17
+ ),
18
+ ):
19
+ """Inicia el flujo de autenticación con Cognito (Hosted UI + PKCE)."""
20
+ auth.login(app_redirect=app_redirect)
21
+
22
+
23
+ @app.command()
24
+ def refresh():
25
+ """Refresca los tokens usando refresh_token guardado."""
26
+ auth.refresh_tokens()
27
+
28
+
29
+ @app.command()
30
+ def whoami():
31
+ """Muestra información del usuario autenticado (id_token)."""
32
+ auth.whoami()
33
+
34
+
35
+ @app.command()
36
+ def logout():
37
+ """Cierra sesión local (revoca/borrado de tokens)."""
38
+ auth.logout()
39
+
40
+
41
+ def main():
42
+ """Compatibilidad: permite ejecutar como 'python -m corecli.cli' o entrypoint antiguo."""
43
+ app()
corecli/pkce.py ADDED
@@ -0,0 +1,14 @@
1
+ import base64
2
+ import hashlib
3
+ import secrets
4
+
5
+
6
+ def generate_pkce_pair():
7
+ """Genera (code_verifier, code_challenge) para PKCE."""
8
+ code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("utf-8").rstrip("=")
9
+ code_challenge = (
10
+ base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("utf-8")).digest())
11
+ .decode("utf-8")
12
+ .rstrip("=")
13
+ )
14
+ return code_verifier, code_challenge
corecli/utils.py ADDED
@@ -0,0 +1,42 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ from typing import Any
5
+
6
+ CONFIG_DIR = os.path.expanduser("~/.corecli")
7
+ TOKENS_FILE = os.path.join(CONFIG_DIR, "tokens.json")
8
+
9
+
10
+ def save_tokens(tokens: dict):
11
+ os.makedirs(CONFIG_DIR, exist_ok=True)
12
+ with open(TOKENS_FILE, "w") as f:
13
+ json.dump(tokens, f, indent=2)
14
+
15
+
16
+ def load_tokens():
17
+ if os.path.exists(TOKENS_FILE):
18
+ with open(TOKENS_FILE) as f:
19
+ return json.load(f)
20
+ return None
21
+
22
+
23
+ def decode_jwt(token: str) -> dict[str, Any]:
24
+ """Decodifica un JWT (solo payload, sin validar firma)."""
25
+ parts = token.split(".")
26
+ if len(parts) != 3:
27
+ raise ValueError("Token JWT inválido")
28
+
29
+ payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4) # padding
30
+ payload_json = base64.urlsafe_b64decode(payload_b64.encode()).decode()
31
+ return json.loads(payload_json) # type: ignore[no-any-return]
32
+
33
+
34
+ def clear_tokens() -> bool:
35
+ """Elimina el archivo de tokens local si existe."""
36
+ if os.path.exists(TOKENS_FILE):
37
+ try:
38
+ os.remove(TOKENS_FILE)
39
+ return True
40
+ except OSError:
41
+ return False
42
+ return True