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.
Files changed (59) hide show
  1. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/PKG-INFO +1 -1
  2. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/pyproject.toml +1 -1
  3. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/auth.py +7 -1
  4. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/config.py +21 -0
  5. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_login.py +39 -0
  6. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/.gitignore +0 -0
  7. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/README.md +0 -0
  8. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/docs/commands.md +0 -0
  9. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/scripts/release.sh +0 -0
  10. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/__init__.py +0 -0
  11. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/__main__.py +0 -0
  12. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/api.py +0 -0
  13. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/build.py +0 -0
  14. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_adopt.py +0 -0
  15. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_auth.py +0 -0
  16. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_config.py +0 -0
  17. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_deploy.py +0 -0
  18. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_doctor.py +0 -0
  19. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_env.py +0 -0
  20. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_projects.py +0 -0
  21. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_publish.py +0 -0
  22. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_registry.py +0 -0
  23. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_secrets.py +0 -0
  24. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_shared.py +0 -0
  25. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_token.py +0 -0
  26. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/compose.py +0 -0
  27. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/link.py +0 -0
  28. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/local_build.py +0 -0
  29. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/main.py +0 -0
  30. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/runtime_secrets.py +0 -0
  31. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/session.py +0 -0
  32. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/conftest.py +0 -0
  33. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_adopt.py +0 -0
  34. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_build.py +0 -0
  35. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_ci_token_auth.py +0 -0
  36. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_compose.py +0 -0
  37. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_config.py +0 -0
  38. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_deploy.py +0 -0
  39. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_deploy_preflight.py +0 -0
  40. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_diff.py +0 -0
  41. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_doctor.py +0 -0
  42. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_env.py +0 -0
  43. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_link.py +0 -0
  44. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_local_build.py +0 -0
  45. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_login_cli.py +0 -0
  46. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_logout.py +0 -0
  47. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_logs.py +0 -0
  48. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_publish.py +0 -0
  49. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_publish_local.py +0 -0
  50. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_publish_multi_component.py +0 -0
  51. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_refresh.py +0 -0
  52. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_registry.py +0 -0
  53. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_rollback.py +0 -0
  54. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_runtime_secrets.py +0 -0
  55. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_secrets.py +0 -0
  56. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_session_permissions.py +0 -0
  57. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_status.py +0 -0
  58. {mozbridge_cli-0.7.0 → mozbridge_cli-0.8.0}/tests/test_token.py +0 -0
  59. {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.7.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.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={"client_id": config.LOGTO_CLIENT_ID, "scope": config.DEVICE_SCOPE},
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