lightnow-cli 1.5.0__tar.gz → 1.6.0__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.
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/.github/workflows/ci.yml +6 -6
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/.github/workflows/release-please.yml +1 -1
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/.github/workflows/release.yml +11 -11
- lightnow_cli-1.6.0/.release-please-manifest.json +3 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/CHANGELOG.md +12 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/PKG-INFO +43 -2
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/README.md +41 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/__init__.py +1 -1
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/auth.py +70 -24
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/config.py +217 -7
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/main.py +4 -4
- lightnow_cli-1.6.0/lightnow_cli/target.py +102 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/pyproject.toml +1 -1
- lightnow_cli-1.6.0/renovate.json +47 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_auth_flow.py +4 -2
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_cli.py +29 -28
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_config.py +4 -2
- lightnow_cli-1.6.0/tests/test_connection_target.py +535 -0
- lightnow_cli-1.5.0/.release-please-manifest.json +0 -3
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/.gitignore +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/CONTRIBUTING.md +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/LICENSE +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/Makefile +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/SECURITY.md +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/docs/development.md +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/docs/release.md +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/examples/README.md +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/examples/docs.md +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/examples/openapi.json +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/examples/server.json +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/authenticated_http.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/client.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/__init__.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/context.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/integrations.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/publish.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/query.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/runner.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/updates.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/commands/validate.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/tls.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/updates.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/lightnow_cli/validation.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/release-please-config.json +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/__init__.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_authenticated_http.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_client.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_context.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_integrations.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_tls.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_updates.py +0 -0
- {lightnow_cli-1.5.0 → lightnow_cli-1.6.0}/tests/test_validation.py +0 -0
|
@@ -23,10 +23,10 @@ jobs:
|
|
|
23
23
|
|
|
24
24
|
steps:
|
|
25
25
|
- name: Checkout
|
|
26
|
-
uses: actions/checkout@
|
|
26
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
27
27
|
|
|
28
28
|
- name: Set up Python
|
|
29
|
-
uses: actions/setup-python@
|
|
29
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
30
30
|
with:
|
|
31
31
|
python-version: ${{ matrix.python-version }}
|
|
32
32
|
cache: pip
|
|
@@ -59,12 +59,12 @@ jobs:
|
|
|
59
59
|
|
|
60
60
|
steps:
|
|
61
61
|
- name: Checkout
|
|
62
|
-
uses: actions/checkout@
|
|
62
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
63
63
|
|
|
64
64
|
- name: Set up Python
|
|
65
|
-
uses: actions/setup-python@
|
|
65
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
66
66
|
with:
|
|
67
|
-
python-version: "3.
|
|
67
|
+
python-version: "3.12"
|
|
68
68
|
cache: pip
|
|
69
69
|
|
|
70
70
|
- name: Install build tools
|
|
@@ -74,7 +74,7 @@ jobs:
|
|
|
74
74
|
run: make package
|
|
75
75
|
|
|
76
76
|
- name: Upload distributions
|
|
77
|
-
uses: actions/upload-artifact@
|
|
77
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
78
78
|
with:
|
|
79
79
|
name: python-distributions
|
|
80
80
|
path: dist/*
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
|
|
18
18
|
steps:
|
|
19
19
|
- name: Run Release Please
|
|
20
|
-
uses: googleapis/release-please-action@
|
|
20
|
+
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
|
|
21
21
|
with:
|
|
22
22
|
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
23
23
|
config-file: release-please-config.json
|
|
@@ -15,12 +15,12 @@ jobs:
|
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
17
|
- name: Checkout
|
|
18
|
-
uses: actions/checkout@
|
|
18
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
19
19
|
|
|
20
20
|
- name: Set up Python
|
|
21
|
-
uses: actions/setup-python@
|
|
21
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
22
22
|
with:
|
|
23
|
-
python-version: "3.
|
|
23
|
+
python-version: "3.12"
|
|
24
24
|
cache: pip
|
|
25
25
|
|
|
26
26
|
- name: Install package
|
|
@@ -36,12 +36,12 @@ jobs:
|
|
|
36
36
|
|
|
37
37
|
steps:
|
|
38
38
|
- name: Checkout
|
|
39
|
-
uses: actions/checkout@
|
|
39
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
40
40
|
|
|
41
41
|
- name: Set up Python
|
|
42
|
-
uses: actions/setup-python@
|
|
42
|
+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
43
43
|
with:
|
|
44
|
-
python-version: "3.
|
|
44
|
+
python-version: "3.12"
|
|
45
45
|
cache: pip
|
|
46
46
|
|
|
47
47
|
- name: Install build tools
|
|
@@ -51,7 +51,7 @@ jobs:
|
|
|
51
51
|
run: make package
|
|
52
52
|
|
|
53
53
|
- name: Upload distributions
|
|
54
|
-
uses: actions/upload-artifact@
|
|
54
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
55
55
|
with:
|
|
56
56
|
name: python-distributions
|
|
57
57
|
path: dist/*
|
|
@@ -66,13 +66,13 @@ jobs:
|
|
|
66
66
|
|
|
67
67
|
steps:
|
|
68
68
|
- name: Download distributions
|
|
69
|
-
uses: actions/download-artifact@
|
|
69
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
70
70
|
with:
|
|
71
71
|
name: python-distributions
|
|
72
72
|
path: dist
|
|
73
73
|
|
|
74
74
|
- name: Publish GitHub release notes
|
|
75
|
-
uses: softprops/action-gh-release@
|
|
75
|
+
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3
|
|
76
76
|
with:
|
|
77
77
|
generate_release_notes: true
|
|
78
78
|
files: dist/*
|
|
@@ -89,13 +89,13 @@ jobs:
|
|
|
89
89
|
|
|
90
90
|
steps:
|
|
91
91
|
- name: Download distributions
|
|
92
|
-
uses: actions/download-artifact@
|
|
92
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
93
93
|
with:
|
|
94
94
|
name: python-distributions
|
|
95
95
|
path: dist
|
|
96
96
|
|
|
97
97
|
- name: Publish distributions to PyPI
|
|
98
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
98
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
99
99
|
|
|
100
100
|
update-homebrew:
|
|
101
101
|
name: Update Homebrew formula
|
|
@@ -4,6 +4,18 @@ All notable changes to the LightNow CLI are documented here.
|
|
|
4
4
|
|
|
5
5
|
This project follows semantic versioning.
|
|
6
6
|
|
|
7
|
+
## [1.6.0](https://github.com/lightnow-ai/lightnow-cli/compare/v1.5.0...v1.6.0) (2026-10-09)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **auth:** support complete connection targets ([#39](https://github.com/lightnow-ai/lightnow-cli/issues/39)) ([699b09e](https://github.com/lightnow-ai/lightnow-cli/commit/699b09e70d1131a82667bb14a828d0234d19a75d))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **renovate:** constrain Python dependency names ([#22](https://github.com/lightnow-ai/lightnow-cli/issues/22)) ([a48bb79](https://github.com/lightnow-ai/lightnow-cli/commit/a48bb7935184d56f169c850d1fc478cde0c1667e))
|
|
18
|
+
|
|
7
19
|
## [1.5.0](https://github.com/lightnow-ai/lightnow-cli/compare/v1.4.0...v1.5.0) (2026-07-18)
|
|
8
20
|
|
|
9
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: lightnow-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/lightnow-ai/lightnow-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/lightnow-ai/lightnow-cli
|
|
@@ -121,6 +121,47 @@ your session with:
|
|
|
121
121
|
lightnow status
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
For an isolated LightNow environment, obtain a credential-free connection target
|
|
125
|
+
from its operator and select it explicitly:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
lightnow login --target ./connection-target.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The complete JSON schema is:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"schemaVersion": 1,
|
|
136
|
+
"issuer": "https://auth.example.test/realms/example",
|
|
137
|
+
"clientId": "lightnow-cli",
|
|
138
|
+
"registryApiUrl": "https://registry.example.test/v0.1",
|
|
139
|
+
"adminApiUrl": "https://admin.example.test/v0/portal"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
All five fields are required; additional fields and embedded credentials are
|
|
144
|
+
rejected. URLs must use HTTPS without credentials, query strings or fragments.
|
|
145
|
+
The environment operator provisions the public OAuth device-flow client; the CLI
|
|
146
|
+
does not register clients. Authentication remains at the product issuer, which
|
|
147
|
+
may broker an external identity provider through its browser flow.
|
|
148
|
+
The device request includes `openid profile email organization:*`; the operator
|
|
149
|
+
must assign the standard `organization` client scope so existing organization
|
|
150
|
+
memberships are available to the scoped APIs.
|
|
151
|
+
|
|
152
|
+
`--target` cannot be combined with `--local`, `--issuer` or `--client-id`. Custom
|
|
153
|
+
issuers require a complete target and cannot silently use production API defaults.
|
|
154
|
+
Normal `lightnow login` and `lightnow login --local` remain available.
|
|
155
|
+
|
|
156
|
+
The selected target and named session are saved only after device authorization
|
|
157
|
+
and userinfo verification succeed. OIDC discovery must identify the selected
|
|
158
|
+
issuer and expose credential-free HTTPS endpoints. Failed login preserves the previous connection.
|
|
159
|
+
A different environment or account returns to personal context; logging into the
|
|
160
|
+
same target and account preserves the selected organization. Organization selection,
|
|
161
|
+
profile requests and generated Local Proxy connections use the selected API URLs.
|
|
162
|
+
For acceptance runs, use a separate container user/home and explicit client and
|
|
163
|
+
proxy configuration paths so existing desktop connections are not modified.
|
|
164
|
+
|
|
124
165
|
## Find MCP Servers
|
|
125
166
|
|
|
126
167
|
Search the LightNow Registry:
|
|
@@ -73,6 +73,47 @@ your session with:
|
|
|
73
73
|
lightnow status
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
For an isolated LightNow environment, obtain a credential-free connection target
|
|
77
|
+
from its operator and select it explicitly:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
lightnow login --target ./connection-target.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The complete JSON schema is:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"schemaVersion": 1,
|
|
88
|
+
"issuer": "https://auth.example.test/realms/example",
|
|
89
|
+
"clientId": "lightnow-cli",
|
|
90
|
+
"registryApiUrl": "https://registry.example.test/v0.1",
|
|
91
|
+
"adminApiUrl": "https://admin.example.test/v0/portal"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
All five fields are required; additional fields and embedded credentials are
|
|
96
|
+
rejected. URLs must use HTTPS without credentials, query strings or fragments.
|
|
97
|
+
The environment operator provisions the public OAuth device-flow client; the CLI
|
|
98
|
+
does not register clients. Authentication remains at the product issuer, which
|
|
99
|
+
may broker an external identity provider through its browser flow.
|
|
100
|
+
The device request includes `openid profile email organization:*`; the operator
|
|
101
|
+
must assign the standard `organization` client scope so existing organization
|
|
102
|
+
memberships are available to the scoped APIs.
|
|
103
|
+
|
|
104
|
+
`--target` cannot be combined with `--local`, `--issuer` or `--client-id`. Custom
|
|
105
|
+
issuers require a complete target and cannot silently use production API defaults.
|
|
106
|
+
Normal `lightnow login` and `lightnow login --local` remain available.
|
|
107
|
+
|
|
108
|
+
The selected target and named session are saved only after device authorization
|
|
109
|
+
and userinfo verification succeed. OIDC discovery must identify the selected
|
|
110
|
+
issuer and expose credential-free HTTPS endpoints. Failed login preserves the previous connection.
|
|
111
|
+
A different environment or account returns to personal context; logging into the
|
|
112
|
+
same target and account preserves the selected organization. Organization selection,
|
|
113
|
+
profile requests and generated Local Proxy connections use the selected API URLs.
|
|
114
|
+
For acceptance runs, use a separate container user/home and explicit client and
|
|
115
|
+
proxy configuration paths so existing desktop connections are not modified.
|
|
116
|
+
|
|
76
117
|
## Find MCP Servers
|
|
77
118
|
|
|
78
119
|
Search the LightNow Registry:
|
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
import secrets
|
|
8
8
|
import time
|
|
9
9
|
import webbrowser
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
from typing import Any, Dict, Optional, cast
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
@@ -16,13 +17,16 @@ from rich.console import Console
|
|
|
16
17
|
from typing_extensions import Annotated
|
|
17
18
|
|
|
18
19
|
from ..config import (
|
|
20
|
+
DEFAULT_ADMIN_API_URL,
|
|
19
21
|
DEFAULT_CLIENT_ID,
|
|
20
22
|
DEFAULT_ISSUER,
|
|
23
|
+
DEFAULT_REGISTRY_API_URL,
|
|
21
24
|
LOCAL_ADMIN_API_URL,
|
|
22
25
|
LOCAL_ISSUER,
|
|
23
26
|
LOCAL_REGISTRY_API_URL,
|
|
24
27
|
config_manager,
|
|
25
28
|
)
|
|
29
|
+
from ..target import ConnectionTarget, load_target, validate_https_url
|
|
26
30
|
|
|
27
31
|
console = Console()
|
|
28
32
|
app = typer.Typer(help="Authentication commands")
|
|
@@ -70,6 +74,10 @@ def pkce_challenge(verifier: str) -> str:
|
|
|
70
74
|
|
|
71
75
|
async def discover_oidc_endpoints(issuer: str) -> Dict[str, str]:
|
|
72
76
|
"""Discover the OIDC endpoints required by the CLI."""
|
|
77
|
+
try:
|
|
78
|
+
issuer = validate_https_url(issuer)
|
|
79
|
+
except ValueError:
|
|
80
|
+
raise AuthError("OIDC issuer must be a credential-free HTTPS URL.") from None
|
|
73
81
|
async with httpx.AsyncClient(timeout=AUTH_HTTP_TIMEOUT) as client:
|
|
74
82
|
try:
|
|
75
83
|
discovery_response = await client.get(
|
|
@@ -79,6 +87,10 @@ async def discover_oidc_endpoints(issuer: str) -> Dict[str, str]:
|
|
|
79
87
|
discovery = discovery_response.json()
|
|
80
88
|
if not isinstance(discovery, dict):
|
|
81
89
|
raise AuthError("OIDC discovery response is not a JSON object")
|
|
90
|
+
if discovery.get("issuer") != issuer:
|
|
91
|
+
raise AuthError(
|
|
92
|
+
"OIDC discovery issuer does not match the selected issuer."
|
|
93
|
+
)
|
|
82
94
|
except httpx.HTTPError as e:
|
|
83
95
|
raise AuthError(
|
|
84
96
|
f"Failed to discover OIDC endpoints: {describe_http_error(e)}"
|
|
@@ -91,12 +103,19 @@ async def discover_oidc_endpoints(issuer: str) -> Dict[str, str]:
|
|
|
91
103
|
if not device_authorization_endpoint or not token_endpoint:
|
|
92
104
|
raise AuthError("OIDC provider does not support device code flow")
|
|
93
105
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
try:
|
|
107
|
+
endpoints = {
|
|
108
|
+
"device_authorization_endpoint": validate_https_url(
|
|
109
|
+
device_authorization_endpoint
|
|
110
|
+
),
|
|
111
|
+
"token_endpoint": validate_https_url(token_endpoint),
|
|
112
|
+
}
|
|
113
|
+
if userinfo_endpoint is not None:
|
|
114
|
+
endpoints["userinfo_endpoint"] = validate_https_url(userinfo_endpoint)
|
|
115
|
+
except ValueError:
|
|
116
|
+
raise AuthError(
|
|
117
|
+
"OIDC discovery endpoints must be credential-free HTTPS URLs."
|
|
118
|
+
) from None
|
|
100
119
|
return endpoints
|
|
101
120
|
|
|
102
121
|
|
|
@@ -111,7 +130,7 @@ async def device_code_flow(issuer: str, client_id: str) -> Dict[str, Any]:
|
|
|
111
130
|
endpoints["device_authorization_endpoint"],
|
|
112
131
|
data={
|
|
113
132
|
"client_id": client_id,
|
|
114
|
-
"scope": "openid profile email",
|
|
133
|
+
"scope": "openid profile email organization:*",
|
|
115
134
|
"code_challenge": pkce_challenge(code_verifier),
|
|
116
135
|
"code_challenge_method": "S256",
|
|
117
136
|
},
|
|
@@ -295,6 +314,7 @@ def persist_refreshed_token(config: Any, token_data: Dict[str, Any]) -> str:
|
|
|
295
314
|
else config.refresh_token
|
|
296
315
|
),
|
|
297
316
|
None,
|
|
317
|
+
expected_binding=config_manager.connection_binding(config),
|
|
298
318
|
)
|
|
299
319
|
return refreshed_token
|
|
300
320
|
|
|
@@ -376,21 +396,43 @@ def login(
|
|
|
376
396
|
hidden=True,
|
|
377
397
|
),
|
|
378
398
|
] = None,
|
|
399
|
+
target: Annotated[
|
|
400
|
+
Optional[Path],
|
|
401
|
+
typer.Option(
|
|
402
|
+
"--target",
|
|
403
|
+
help="Credential-free JSON connection target for an isolated environment.",
|
|
404
|
+
),
|
|
405
|
+
] = None,
|
|
379
406
|
) -> None:
|
|
380
407
|
"""Authenticate with LightNow."""
|
|
381
408
|
try:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
409
|
+
if target is not None:
|
|
410
|
+
if local or issuer is not None or client_id is not None:
|
|
411
|
+
raise AuthError(
|
|
412
|
+
"--target cannot be combined with --local, --issuer or --client-id."
|
|
413
|
+
)
|
|
414
|
+
try:
|
|
415
|
+
connection = load_target(target)
|
|
416
|
+
except ValueError as exc:
|
|
417
|
+
raise AuthError(str(exc)) from None
|
|
418
|
+
else:
|
|
419
|
+
resolved_issuer = LOCAL_ISSUER if local else DEFAULT_ISSUER
|
|
420
|
+
if issuer is not None and issuer.rstrip("/") != resolved_issuer:
|
|
421
|
+
raise AuthError("A custom issuer requires --target with both API URLs.")
|
|
422
|
+
if client_id is not None and (
|
|
423
|
+
not client_id
|
|
424
|
+
or any(
|
|
425
|
+
character.isspace() or ord(character) < 32
|
|
426
|
+
for character in client_id
|
|
427
|
+
)
|
|
428
|
+
):
|
|
429
|
+
raise AuthError("Client ID must be a nonempty identifier.")
|
|
430
|
+
connection = ConnectionTarget(
|
|
431
|
+
resolved_issuer,
|
|
432
|
+
client_id or DEFAULT_CLIENT_ID,
|
|
433
|
+
LOCAL_REGISTRY_API_URL if local else DEFAULT_REGISTRY_API_URL,
|
|
434
|
+
LOCAL_ADMIN_API_URL if local else DEFAULT_ADMIN_API_URL,
|
|
435
|
+
)
|
|
394
436
|
|
|
395
437
|
# Perform device code flow
|
|
396
438
|
if local:
|
|
@@ -399,23 +441,27 @@ def login(
|
|
|
399
441
|
)
|
|
400
442
|
else:
|
|
401
443
|
console.print("[bold blue]Starting LightNow authentication...[/bold blue]")
|
|
402
|
-
token_data = asyncio.run(
|
|
444
|
+
token_data = asyncio.run(
|
|
445
|
+
device_code_flow(connection.issuer, connection.client_id)
|
|
446
|
+
)
|
|
403
447
|
token = token_data.get("access_token")
|
|
404
448
|
if not isinstance(token, str) or token == "":
|
|
405
449
|
raise AuthError("OIDC token response did not include an access token.")
|
|
406
450
|
refresh_token = token_data.get("refresh_token")
|
|
407
451
|
|
|
408
452
|
# Fetch user info from the issuer instead of trusting local JWT claims.
|
|
409
|
-
user_info = asyncio.run(fetch_user_info(
|
|
453
|
+
user_info = asyncio.run(fetch_user_info(connection.issuer, token))
|
|
454
|
+
subject = user_info.get("sub")
|
|
455
|
+
if not isinstance(subject, str) or not subject.strip():
|
|
456
|
+
raise AuthError("OIDC userinfo response did not include a valid subject.")
|
|
410
457
|
|
|
411
458
|
# Store token and user info
|
|
412
|
-
config_manager.
|
|
459
|
+
config_manager.commit_login(
|
|
460
|
+
connection,
|
|
413
461
|
token,
|
|
414
462
|
refresh_token if isinstance(refresh_token, str) else None,
|
|
415
463
|
user_info,
|
|
416
|
-
update_active_session=False,
|
|
417
464
|
)
|
|
418
|
-
config_manager.persist_current_session(user_info)
|
|
419
465
|
|
|
420
466
|
console.print("[bold green]✓ Authentication successful![/bold green]")
|
|
421
467
|
if user_info:
|