mozbridge-cli 0.7.0__tar.gz → 0.8.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.
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/PKG-INFO +1 -1
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/pyproject.toml +1 -1
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/auth.py +7 -1
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/config.py +21 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_login.py +39 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/.gitignore +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/README.md +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/docs/commands.md +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/scripts/release.sh +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/__init__.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/__main__.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/api.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/build.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_adopt.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_auth.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_config.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_deploy.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_doctor.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_env.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_projects.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_publish.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_registry.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_secrets.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_shared.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_token.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/compose.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/link.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/local_build.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/main.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/runtime_secrets.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/session.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/conftest.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_adopt.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_build.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_ci_token_auth.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_compose.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_config.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_deploy.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_deploy_preflight.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_diff.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_doctor.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_env.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_link.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_local_build.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_login_cli.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_logout.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_logs.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_publish.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_publish_local.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_publish_multi_component.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_refresh.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_registry.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_rollback.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_runtime_secrets.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_secrets.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_session_permissions.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_status.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_token.py +0 -0
- {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_whoami.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mozbridge-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Command-line client for the Mozbridge deployment platform: SSO login, build, publish, status, and rollback for any project hosted on Mozbridge.
|
|
5
5
|
Project-URL: Homepage, https://mozbridge.com
|
|
6
6
|
Project-URL: Documentation, https://github.com/jessin01/mozbridge/blob/main/cli/docs/commands.md
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mozbridge-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Command-line client for the Mozbridge deployment platform: SSO login, build, publish, status, and rollback for any project hosted on Mozbridge."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -64,7 +64,11 @@ class DeviceAuthorization:
|
|
|
64
64
|
def start_device_authorization(client: httpx.Client) -> DeviceAuthorization:
|
|
65
65
|
resp = client.post(
|
|
66
66
|
config.DEVICE_AUTH_URL,
|
|
67
|
-
data={
|
|
67
|
+
data={
|
|
68
|
+
"client_id": config.LOGTO_CLIENT_ID,
|
|
69
|
+
"scope": config.DEVICE_SCOPE,
|
|
70
|
+
"resource": config.LOGTO_RESOURCE,
|
|
71
|
+
},
|
|
68
72
|
)
|
|
69
73
|
resp.raise_for_status()
|
|
70
74
|
data = resp.json()
|
|
@@ -89,6 +93,7 @@ def _poll_once(client: httpx.Client, device_code: str) -> dict | None:
|
|
|
89
93
|
"client_id": config.LOGTO_CLIENT_ID,
|
|
90
94
|
"grant_type": GRANT_TYPE_DEVICE_CODE,
|
|
91
95
|
"device_code": device_code,
|
|
96
|
+
"resource": config.LOGTO_RESOURCE,
|
|
92
97
|
},
|
|
93
98
|
)
|
|
94
99
|
if resp.status_code == 200:
|
|
@@ -166,6 +171,7 @@ def refresh_access_token(client: httpx.Client, refresh_token: str) -> dict:
|
|
|
166
171
|
"grant_type": "refresh_token",
|
|
167
172
|
"refresh_token": refresh_token,
|
|
168
173
|
"client_id": config.LOGTO_CLIENT_ID,
|
|
174
|
+
"resource": config.LOGTO_RESOURCE,
|
|
169
175
|
},
|
|
170
176
|
)
|
|
171
177
|
if resp.status_code == 200:
|
|
@@ -24,6 +24,27 @@ from pathlib import Path
|
|
|
24
24
|
LOGTO_ENDPOINT = os.environ.get("MOZBRIDGE_LOGTO_ENDPOINT", "https://auth.mozbridge.com").rstrip("/")
|
|
25
25
|
LOGTO_CLIENT_ID = os.environ.get("MOZBRIDGE_LOGTO_CLIENT_ID", "7zkurxlp5x98iw217wrhw")
|
|
26
26
|
|
|
27
|
+
# Resource indicator (RFC 8707) the token request must carry so Logto stamps
|
|
28
|
+
# the issued access token's `aud` claim with something the backend actually
|
|
29
|
+
# accepts. Found live, 2026-09-14, immediately after the client_id fix above:
|
|
30
|
+
# device-flow login succeeded (a real session.json got written) but every
|
|
31
|
+
# subsequent API call 401'd with "Session is no longer valid" — decoding the
|
|
32
|
+
# actual issued access_token showed it was a bare OPAQUE string, not a JWT at
|
|
33
|
+
# all (no dots) — exactly what Logto issues when no `resource` is requested.
|
|
34
|
+
# The backend (`backend/app/features/identity/auth.py`'s `verify_token`)
|
|
35
|
+
# needs a real JWT with an `aud` claim matching its own `LOGTO_RESOURCE` env
|
|
36
|
+
# var to accept a token at all.
|
|
37
|
+
#
|
|
38
|
+
# `https://api.mozbridge.com` (guessed from mcp/mozbridge_mcp/oauth.py's own
|
|
39
|
+
# fallback default) was WRONG — confirmed via a live GET .../api/resources:
|
|
40
|
+
# the real registered resource is named "Mozbridge API" with indicator
|
|
41
|
+
# `https://mozbridge.com` (no `api.` subdomain). Cross-checked directly
|
|
42
|
+
# against production's actual `LOGTO_RESOURCE` env var
|
|
43
|
+
# (`https://mozbridge.com,https://mcp.mozbridge.com/mcp`) — this value is
|
|
44
|
+
# the first of the two, confirmed to be in the backend's own accepted list,
|
|
45
|
+
# not inferred from adjacent code a second time.
|
|
46
|
+
LOGTO_RESOURCE = os.environ.get("MOZBRIDGE_LOGTO_RESOURCE", "https://mozbridge.com")
|
|
47
|
+
|
|
27
48
|
DEVICE_AUTH_URL = f"{LOGTO_ENDPOINT}/oidc/device/auth"
|
|
28
49
|
TOKEN_URL = f"{LOGTO_ENDPOINT}/oidc/token"
|
|
29
50
|
|
|
@@ -38,6 +38,45 @@ def test_start_device_authorization_uses_server_interval():
|
|
|
38
38
|
assert result.interval == 7
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def test_start_device_authorization_sends_resource_indicator():
|
|
42
|
+
# Regression test: without `resource` in this request, Logto issues a
|
|
43
|
+
# bare opaque access_token (no dots — not a JWT at all) instead of a
|
|
44
|
+
# signed JWT with an `aud` claim, and the backend's verify_token 401s
|
|
45
|
+
# every API call even after a successful login. Found live, 2026-09-14,
|
|
46
|
+
# by decoding an actual issued access_token and seeing it had zero dots.
|
|
47
|
+
with respx.mock:
|
|
48
|
+
route = _device_auth_route()
|
|
49
|
+
with httpx.Client() as client:
|
|
50
|
+
auth.start_device_authorization(client)
|
|
51
|
+
|
|
52
|
+
sent = route.calls.last.request.content.decode()
|
|
53
|
+
assert "resource=" in sent
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_poll_once_sends_resource_indicator():
|
|
57
|
+
with respx.mock:
|
|
58
|
+
route = respx.post(config.TOKEN_URL).mock(
|
|
59
|
+
return_value=httpx.Response(200, json={"access_token": "tok", "token_type": "Bearer"})
|
|
60
|
+
)
|
|
61
|
+
with httpx.Client() as client:
|
|
62
|
+
auth._poll_once(client, "devcode-123")
|
|
63
|
+
|
|
64
|
+
sent = route.calls.last.request.content.decode()
|
|
65
|
+
assert "resource=" in sent
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_refresh_access_token_sends_resource_indicator():
|
|
69
|
+
with respx.mock:
|
|
70
|
+
route = respx.post(config.TOKEN_URL).mock(
|
|
71
|
+
return_value=httpx.Response(200, json={"access_token": "tok", "token_type": "Bearer"})
|
|
72
|
+
)
|
|
73
|
+
with httpx.Client() as client:
|
|
74
|
+
auth.refresh_access_token(client, "refresh-abc")
|
|
75
|
+
|
|
76
|
+
sent = route.calls.last.request.content.decode()
|
|
77
|
+
assert "resource=" in sent
|
|
78
|
+
|
|
79
|
+
|
|
41
80
|
def test_happy_path_login_pending_then_success():
|
|
42
81
|
"""authorization_pending a couple of times, then a successful token response."""
|
|
43
82
|
responses = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|