mozbridge-cli 0.6.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.6.0 → mozbridge_cli-0.8.0}/PKG-INFO +2 -2
  2. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/README.md +1 -1
  3. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/pyproject.toml +1 -1
  4. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/auth.py +7 -1
  5. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/config.py +32 -1
  6. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_login.py +39 -0
  7. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/.gitignore +0 -0
  8. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/docs/commands.md +0 -0
  9. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/scripts/release.sh +0 -0
  10. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/__init__.py +0 -0
  11. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/__main__.py +0 -0
  12. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/api.py +0 -0
  13. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/build.py +0 -0
  14. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_adopt.py +0 -0
  15. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_auth.py +0 -0
  16. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_config.py +0 -0
  17. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_deploy.py +0 -0
  18. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_doctor.py +0 -0
  19. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_env.py +0 -0
  20. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_projects.py +0 -0
  21. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_publish.py +0 -0
  22. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_registry.py +0 -0
  23. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_secrets.py +0 -0
  24. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_shared.py +0 -0
  25. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/cmd_token.py +0 -0
  26. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/compose.py +0 -0
  27. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/link.py +0 -0
  28. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/local_build.py +0 -0
  29. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/main.py +0 -0
  30. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/runtime_secrets.py +0 -0
  31. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/src/mozbridge_cli/session.py +0 -0
  32. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/conftest.py +0 -0
  33. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_adopt.py +0 -0
  34. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_build.py +0 -0
  35. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_ci_token_auth.py +0 -0
  36. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_compose.py +0 -0
  37. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_config.py +0 -0
  38. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_deploy.py +0 -0
  39. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_deploy_preflight.py +0 -0
  40. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_diff.py +0 -0
  41. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_doctor.py +0 -0
  42. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_env.py +0 -0
  43. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_link.py +0 -0
  44. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_local_build.py +0 -0
  45. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_login_cli.py +0 -0
  46. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_logout.py +0 -0
  47. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_logs.py +0 -0
  48. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_publish.py +0 -0
  49. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_publish_local.py +0 -0
  50. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_publish_multi_component.py +0 -0
  51. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_refresh.py +0 -0
  52. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_registry.py +0 -0
  53. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_rollback.py +0 -0
  54. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_runtime_secrets.py +0 -0
  55. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_secrets.py +0 -0
  56. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_session_permissions.py +0 -0
  57. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_status.py +0 -0
  58. {mozbridge_cli-0.6.0 → mozbridge_cli-0.8.0}/tests/test_token.py +0 -0
  59. {mozbridge_cli-0.6.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.6.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
@@ -305,7 +305,7 @@ so it belongs in the same pipeline step, before `publish`.
305
305
  | Env var | Default | Purpose |
306
306
  |---|---|---|
307
307
  | `MOZBRIDGE_LOGTO_ENDPOINT` | `https://auth.mozbridge.com` | Logto tenant endpoint |
308
- | `MOZBRIDGE_LOGTO_CLIENT_ID` | `swdtsuiz6dgzack1g75x3` | Logto Native app client id (public client) |
308
+ | `MOZBRIDGE_LOGTO_CLIENT_ID` | `7zkurxlp5x98iw217wrhw` | Logto Native app client id (public client, device-flow-capable) |
309
309
  | `MOZBRIDGE_PUBLIC_API_URL` (falls back to `PUBLIC_API_URL`) | `https://api.mozbridge.com` | Mozbridge API base |
310
310
  | `MOZBRIDGE_GHCR_ORG` | `jessin01` | GHCR org used to compute default image names for multi-component publish |
311
311
  | `MOZBRIDGE_CONFIG_DIR` | `~/.config/mozbridge` | Where the session file lives (mainly for tests) |
@@ -277,7 +277,7 @@ so it belongs in the same pipeline step, before `publish`.
277
277
  | Env var | Default | Purpose |
278
278
  |---|---|---|
279
279
  | `MOZBRIDGE_LOGTO_ENDPOINT` | `https://auth.mozbridge.com` | Logto tenant endpoint |
280
- | `MOZBRIDGE_LOGTO_CLIENT_ID` | `swdtsuiz6dgzack1g75x3` | Logto Native app client id (public client) |
280
+ | `MOZBRIDGE_LOGTO_CLIENT_ID` | `7zkurxlp5x98iw217wrhw` | Logto Native app client id (public client, device-flow-capable) |
281
281
  | `MOZBRIDGE_PUBLIC_API_URL` (falls back to `PUBLIC_API_URL`) | `https://api.mozbridge.com` | Mozbridge API base |
282
282
  | `MOZBRIDGE_GHCR_ORG` | `jessin01` | GHCR org used to compute default image names for multi-component publish |
283
283
  | `MOZBRIDGE_CONFIG_DIR` | `~/.config/mozbridge` | Where the session file lives (mainly for tests) |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mozbridge-cli"
7
- version = "0.6.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:
@@ -11,8 +11,39 @@ import os
11
11
  from pathlib import Path
12
12
 
13
13
  # Logto tenant serving Mozbridge's real, provisioned Native (public) client.
14
+ #
15
+ # client_id history: the original app (swdtsuiz6dgzack1g75x3) was created
16
+ # without Logto's `customClientMetadata.isDeviceFlow` set, and that field can
17
+ # ONLY be set at application creation — confirmed live, 2026-09-14, via a 422
18
+ # `application.device_flow_not_changeable` on an attempted PATCH. It can
19
+ # never complete a device-flow login and there is no way to repair it in
20
+ # place. Every mozbridge-cli release through 0.6.0 shipped with that old,
21
+ # permanently-broken id — this new one (created with is_device_flow=True,
22
+ # verified live with a real 200 from POST /oidc/device/auth) is what actually
23
+ # works, starting with this release.
14
24
  LOGTO_ENDPOINT = os.environ.get("MOZBRIDGE_LOGTO_ENDPOINT", "https://auth.mozbridge.com").rstrip("/")
15
- LOGTO_CLIENT_ID = os.environ.get("MOZBRIDGE_LOGTO_CLIENT_ID", "swdtsuiz6dgzack1g75x3")
25
+ LOGTO_CLIENT_ID = os.environ.get("MOZBRIDGE_LOGTO_CLIENT_ID", "7zkurxlp5x98iw217wrhw")
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")
16
47
 
17
48
  DEVICE_AUTH_URL = f"{LOGTO_ENDPOINT}/oidc/device/auth"
18
49
  TOKEN_URL = f"{LOGTO_ENDPOINT}/oidc/token"
@@ -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