cloudsmith-cli 1.20.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.
Files changed (130) hide show
  1. cloudsmith_cli/__init__.py +10 -0
  2. cloudsmith_cli/__main__.py +8 -0
  3. cloudsmith_cli/cli/__init__.py +1 -0
  4. cloudsmith_cli/cli/command.py +160 -0
  5. cloudsmith_cli/cli/commands/__init__.py +33 -0
  6. cloudsmith_cli/cli/commands/auth.py +173 -0
  7. cloudsmith_cli/cli/commands/check.py +129 -0
  8. cloudsmith_cli/cli/commands/copy.py +98 -0
  9. cloudsmith_cli/cli/commands/credential_helper/__init__.py +39 -0
  10. cloudsmith_cli/cli/commands/credential_helper/docker.py +66 -0
  11. cloudsmith_cli/cli/commands/credential_helper/manage.py +299 -0
  12. cloudsmith_cli/cli/commands/delete.py +67 -0
  13. cloudsmith_cli/cli/commands/dependencies.py +108 -0
  14. cloudsmith_cli/cli/commands/docs.py +16 -0
  15. cloudsmith_cli/cli/commands/download.py +620 -0
  16. cloudsmith_cli/cli/commands/entitlements.py +819 -0
  17. cloudsmith_cli/cli/commands/help_.py +12 -0
  18. cloudsmith_cli/cli/commands/list_.py +317 -0
  19. cloudsmith_cli/cli/commands/login.py +99 -0
  20. cloudsmith_cli/cli/commands/logout.py +151 -0
  21. cloudsmith_cli/cli/commands/main.py +73 -0
  22. cloudsmith_cli/cli/commands/mcp.py +523 -0
  23. cloudsmith_cli/cli/commands/metadata.py +503 -0
  24. cloudsmith_cli/cli/commands/metrics/__init__.py +2 -0
  25. cloudsmith_cli/cli/commands/metrics/command.py +20 -0
  26. cloudsmith_cli/cli/commands/metrics/entitlements.py +148 -0
  27. cloudsmith_cli/cli/commands/metrics/packages.py +134 -0
  28. cloudsmith_cli/cli/commands/move.py +111 -0
  29. cloudsmith_cli/cli/commands/policy/__init__.py +3 -0
  30. cloudsmith_cli/cli/commands/policy/command.py +20 -0
  31. cloudsmith_cli/cli/commands/policy/deny.py +248 -0
  32. cloudsmith_cli/cli/commands/policy/license.py +335 -0
  33. cloudsmith_cli/cli/commands/policy/vulnerability.py +322 -0
  34. cloudsmith_cli/cli/commands/push.py +1323 -0
  35. cloudsmith_cli/cli/commands/quarantine.py +148 -0
  36. cloudsmith_cli/cli/commands/quota/__init__.py +2 -0
  37. cloudsmith_cli/cli/commands/quota/command.py +20 -0
  38. cloudsmith_cli/cli/commands/quota/history.py +122 -0
  39. cloudsmith_cli/cli/commands/quota/quota.py +107 -0
  40. cloudsmith_cli/cli/commands/repos.py +330 -0
  41. cloudsmith_cli/cli/commands/resync.py +90 -0
  42. cloudsmith_cli/cli/commands/status.py +92 -0
  43. cloudsmith_cli/cli/commands/tags.py +375 -0
  44. cloudsmith_cli/cli/commands/tokens.py +318 -0
  45. cloudsmith_cli/cli/commands/upstream.py +479 -0
  46. cloudsmith_cli/cli/commands/vulnerabilities.py +141 -0
  47. cloudsmith_cli/cli/commands/whoami.py +188 -0
  48. cloudsmith_cli/cli/config.py +635 -0
  49. cloudsmith_cli/cli/decorators.py +624 -0
  50. cloudsmith_cli/cli/exceptions.py +215 -0
  51. cloudsmith_cli/cli/metadata_common.py +146 -0
  52. cloudsmith_cli/cli/saml.py +109 -0
  53. cloudsmith_cli/cli/table.py +59 -0
  54. cloudsmith_cli/cli/types.py +14 -0
  55. cloudsmith_cli/cli/utils.py +267 -0
  56. cloudsmith_cli/cli/validators.py +378 -0
  57. cloudsmith_cli/cli/webserver.py +263 -0
  58. cloudsmith_cli/core/__init__.py +1 -0
  59. cloudsmith_cli/core/api/__init__.py +1 -0
  60. cloudsmith_cli/core/api/distros.py +31 -0
  61. cloudsmith_cli/core/api/entitlements.py +130 -0
  62. cloudsmith_cli/core/api/exceptions.py +57 -0
  63. cloudsmith_cli/core/api/files.py +131 -0
  64. cloudsmith_cli/core/api/init.py +109 -0
  65. cloudsmith_cli/core/api/metadata.py +217 -0
  66. cloudsmith_cli/core/api/metrics.py +78 -0
  67. cloudsmith_cli/core/api/orgs.py +201 -0
  68. cloudsmith_cli/core/api/packages.py +309 -0
  69. cloudsmith_cli/core/api/quota.py +64 -0
  70. cloudsmith_cli/core/api/rates.py +28 -0
  71. cloudsmith_cli/core/api/repos.py +81 -0
  72. cloudsmith_cli/core/api/status.py +27 -0
  73. cloudsmith_cli/core/api/upstreams.py +72 -0
  74. cloudsmith_cli/core/api/user.py +109 -0
  75. cloudsmith_cli/core/api/version.py +15 -0
  76. cloudsmith_cli/core/api/vulnerabilities.py +230 -0
  77. cloudsmith_cli/core/cache_utils.py +160 -0
  78. cloudsmith_cli/core/config.py +140 -0
  79. cloudsmith_cli/core/credentials/__init__.py +0 -0
  80. cloudsmith_cli/core/credentials/chain.py +69 -0
  81. cloudsmith_cli/core/credentials/models.py +44 -0
  82. cloudsmith_cli/core/credentials/oidc/__init__.py +6 -0
  83. cloudsmith_cli/core/credentials/oidc/cache.py +220 -0
  84. cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +122 -0
  85. cloudsmith_cli/core/credentials/oidc/detectors/aws.py +85 -0
  86. cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +70 -0
  87. cloudsmith_cli/core/credentials/oidc/detectors/base.py +26 -0
  88. cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +35 -0
  89. cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +40 -0
  90. cloudsmith_cli/core/credentials/oidc/detectors/generic.py +42 -0
  91. cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +64 -0
  92. cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +49 -0
  93. cloudsmith_cli/core/credentials/oidc/exchange.py +87 -0
  94. cloudsmith_cli/core/credentials/provider.py +17 -0
  95. cloudsmith_cli/core/credentials/providers/__init__.py +15 -0
  96. cloudsmith_cli/core/credentials/providers/cli_flag.py +24 -0
  97. cloudsmith_cli/core/credentials/providers/credentials_file.py +24 -0
  98. cloudsmith_cli/core/credentials/providers/env_var.py +24 -0
  99. cloudsmith_cli/core/credentials/providers/keyring_provider.py +59 -0
  100. cloudsmith_cli/core/credentials/providers/oidc_provider.py +115 -0
  101. cloudsmith_cli/core/download.py +594 -0
  102. cloudsmith_cli/core/keyring.py +171 -0
  103. cloudsmith_cli/core/mcp/__init__.py +0 -0
  104. cloudsmith_cli/core/mcp/data.py +17 -0
  105. cloudsmith_cli/core/mcp/server.py +786 -0
  106. cloudsmith_cli/core/pagination.py +131 -0
  107. cloudsmith_cli/core/ratelimits.py +88 -0
  108. cloudsmith_cli/core/rest.py +255 -0
  109. cloudsmith_cli/core/utils.py +95 -0
  110. cloudsmith_cli/core/version.py +20 -0
  111. cloudsmith_cli/credential_helpers/__init__.py +7 -0
  112. cloudsmith_cli/credential_helpers/backends.py +41 -0
  113. cloudsmith_cli/credential_helpers/common.py +111 -0
  114. cloudsmith_cli/credential_helpers/custom_domains.py +281 -0
  115. cloudsmith_cli/credential_helpers/docker/__init__.py +4 -0
  116. cloudsmith_cli/credential_helpers/docker/installer.py +349 -0
  117. cloudsmith_cli/credential_helpers/docker/runtime.py +117 -0
  118. cloudsmith_cli/credential_helpers/launchers.py +175 -0
  119. cloudsmith_cli/data/VERSION +1 -0
  120. cloudsmith_cli/data/config.ini +23 -0
  121. cloudsmith_cli/data/credentials.ini +14 -0
  122. cloudsmith_cli/templates/__init__.py +3 -0
  123. cloudsmith_cli/templates/auth_error.html +45 -0
  124. cloudsmith_cli/templates/auth_success.html +37 -0
  125. cloudsmith_cli-1.20.0.dist-info/METADATA +610 -0
  126. cloudsmith_cli-1.20.0.dist-info/RECORD +130 -0
  127. cloudsmith_cli-1.20.0.dist-info/WHEEL +5 -0
  128. cloudsmith_cli-1.20.0.dist-info/entry_points.txt +2 -0
  129. cloudsmith_cli-1.20.0.dist-info/licenses/LICENSE +201 -0
  130. cloudsmith_cli-1.20.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,64 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ """GitHub Actions OIDC detector.
3
+
4
+ Fetches an OIDC token via the Actions runtime HTTP endpoint, using the
5
+ ``ACTIONS_ID_TOKEN_REQUEST_URL`` and ``ACTIONS_ID_TOKEN_REQUEST_TOKEN``
6
+ variables exposed when a workflow requests ``id-token: write`` permission.
7
+
8
+ References:
9
+ https://docs.github.com/en/actions/reference/security/oidc
10
+ https://docs.cloudsmith.com/authentication/setup-cloudsmith-to-authenticate-with-oidc-in-github-actions
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from urllib.parse import quote
17
+
18
+ from ....rest import create_requests_session as create_session
19
+ from .base import EnvironmentDetector
20
+
21
+ DEFAULT_AUDIENCE = "cloudsmith"
22
+
23
+
24
+ class GitHubActionsDetector(EnvironmentDetector):
25
+ """Detects GitHub Actions and fetches an OIDC token via HTTP request."""
26
+
27
+ name = "GitHub Actions"
28
+ id = "github"
29
+
30
+ def detect(self) -> bool:
31
+ return (
32
+ os.environ.get("GITHUB_ACTIONS") == "true"
33
+ and bool(os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL"))
34
+ and bool(os.environ.get("ACTIONS_ID_TOKEN_REQUEST_TOKEN"))
35
+ )
36
+
37
+ def get_token(self) -> str:
38
+ request_url = os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"]
39
+ request_token = os.environ["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
40
+
41
+ audience = self.context.oidc_audience or DEFAULT_AUDIENCE
42
+ separator = "&" if "?" in request_url else "?"
43
+ url = f"{request_url}{separator}audience={quote(audience, safe='')}"
44
+
45
+ session = self.context.session or create_session()
46
+ try:
47
+ response = session.get(
48
+ url,
49
+ headers={
50
+ "Authorization": f"Bearer {request_token}",
51
+ "Accept": "application/json; api-version=2.0",
52
+ },
53
+ timeout=30,
54
+ )
55
+ response.raise_for_status()
56
+
57
+ data = response.json()
58
+ token = data.get("value")
59
+ if not token:
60
+ raise ValueError("GitHub Actions OIDC response did not contain a token")
61
+ return token
62
+ finally:
63
+ if not self.context.session:
64
+ session.close()
@@ -0,0 +1,49 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ """GitLab CI OIDC detector.
3
+
4
+ Reads an OIDC token from environment variables populated by GitLab's
5
+ ``id_tokens`` configuration in ``.gitlab-ci.yml``.
6
+
7
+ References:
8
+ https://docs.gitlab.com/ci/cloud_services/
9
+ https://docs.cloudsmith.com/integrations/integrating-with-gitlab-cicd
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+
16
+ from .base import EnvironmentDetector
17
+
18
+
19
+ class GitLabCIDetector(EnvironmentDetector):
20
+ """Detects GitLab CI and reads an OIDC token from an environment variable.
21
+
22
+ GitLab requires users to configure ``id_tokens`` in ``.gitlab-ci.yml``,
23
+ minting a token with ``aud`` set to the Cloudsmith OIDC endpoint and
24
+ exposing it as ``CLOUDSMITH_OIDC_TOKEN``. The legacy ``CI_JOB_JWT``/
25
+ ``CI_JOB_JWT_V2`` variables are deliberately not consulted: they were
26
+ removed in GitLab 17.0, carry the GitLab instance URL as their audience
27
+ (not the Cloudsmith audience the token exchange validates), and were
28
+ auto-injected into every job on older instances.
29
+ """
30
+
31
+ name = "GitLab CI"
32
+ id = "gitlab"
33
+
34
+ TOKEN_ENV_VAR = "CLOUDSMITH_OIDC_TOKEN"
35
+
36
+ def detect(self) -> bool:
37
+ if os.environ.get("GITLAB_CI") != "true":
38
+ return False
39
+ return bool(os.environ.get(self.TOKEN_ENV_VAR))
40
+
41
+ def get_token(self) -> str:
42
+ token = os.environ.get(self.TOKEN_ENV_VAR)
43
+ if token:
44
+ return token
45
+ raise ValueError(
46
+ "GitLab CI detected but no OIDC token found. "
47
+ "Configure id_tokens in .gitlab-ci.yml and expose it as "
48
+ + self.TOKEN_ENV_VAR
49
+ )
@@ -0,0 +1,87 @@
1
+ """Cloudsmith OIDC token exchange.
2
+
3
+ Exchanges a vendor OIDC JWT for a short-lived Cloudsmith API token
4
+ via the POST /openid/{org}/ endpoint.
5
+
6
+ References:
7
+ https://help.cloudsmith.io/docs/openid-connect
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import TYPE_CHECKING
14
+
15
+ import requests
16
+
17
+ from ...rest import create_requests_session as create_session
18
+
19
+ if TYPE_CHECKING:
20
+ from ... import CredentialContext
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def exchange_oidc_token(
26
+ context: CredentialContext,
27
+ org: str,
28
+ service_slug: str,
29
+ oidc_token: str,
30
+ ) -> str:
31
+ """Exchange a vendor OIDC JWT for a Cloudsmith API token.
32
+
33
+ Raises:
34
+ OidcExchangeError: If the exchange fails.
35
+ """
36
+ host = context.api_host.rstrip("/")
37
+ if not host.startswith("http"):
38
+ host = f"https://{host}"
39
+
40
+ url = f"{host}/openid/{org}/"
41
+ payload = {
42
+ "oidc_token": oidc_token,
43
+ "service_slug": service_slug,
44
+ }
45
+
46
+ session = context.session or create_session()
47
+
48
+ try:
49
+ try:
50
+ response = session.post(
51
+ url,
52
+ json=payload,
53
+ timeout=30,
54
+ )
55
+ except requests.exceptions.RequestException as exc:
56
+ raise OidcExchangeError(
57
+ f"OIDC token exchange request failed: {exc}"
58
+ ) from exc
59
+
60
+ if response.status_code in (200, 201):
61
+ data = response.json()
62
+ token = data.get("token")
63
+ if not token or not isinstance(token, str) or not token.strip():
64
+ raise OidcExchangeError(
65
+ "Cloudsmith OIDC exchange returned an empty or invalid token"
66
+ )
67
+ return token
68
+
69
+ try:
70
+ error_json = response.json()
71
+ error_detail = error_json.get(
72
+ "detail", error_json.get("error", str(error_json))
73
+ )
74
+ except Exception: # pylint: disable=broad-exception-caught
75
+ error_detail = response.text[:200]
76
+
77
+ raise OidcExchangeError(
78
+ f"OIDC token exchange failed with {response.status_code}: "
79
+ f"{error_detail}"
80
+ )
81
+ finally:
82
+ if not context.session:
83
+ session.close()
84
+
85
+
86
+ class OidcExchangeError(Exception):
87
+ """Raised when the OIDC token exchange with Cloudsmith fails."""
@@ -0,0 +1,17 @@
1
+ """Base credential provider interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from .models import CredentialContext, CredentialResult
8
+
9
+
10
+ class CredentialProvider(ABC):
11
+ """Base class for credential providers."""
12
+
13
+ name: str = "base"
14
+
15
+ @abstractmethod
16
+ def resolve(self, context: CredentialContext) -> CredentialResult | None:
17
+ """Attempt to resolve credentials. Return CredentialResult or None."""
@@ -0,0 +1,15 @@
1
+ """Credential providers for the Cloudsmith CLI."""
2
+
3
+ from .cli_flag import CLIFlagProvider
4
+ from .credentials_file import CredentialsFileProvider
5
+ from .env_var import EnvVarProvider
6
+ from .keyring_provider import KeyringProvider
7
+ from .oidc_provider import OidcProvider
8
+
9
+ __all__ = [
10
+ "CLIFlagProvider",
11
+ "CredentialsFileProvider",
12
+ "EnvVarProvider",
13
+ "KeyringProvider",
14
+ "OidcProvider",
15
+ ]
@@ -0,0 +1,24 @@
1
+ """CLI flag credential provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..models import CredentialContext, CredentialResult
6
+ from ..provider import CredentialProvider
7
+
8
+
9
+ class CLIFlagProvider(CredentialProvider):
10
+ """Resolves credentials from the --api-key CLI flag."""
11
+
12
+ name = "cli_flag"
13
+
14
+ def resolve(self, context: CredentialContext) -> CredentialResult | None:
15
+ api_key = context.api_key_from_flag
16
+ if api_key and api_key.strip():
17
+ api_key = api_key.strip()
18
+ suffix = api_key[-4:]
19
+ return CredentialResult(
20
+ api_key=api_key,
21
+ source_name="cli_flag",
22
+ source_detail=f"--api-key flag (ends with ...{suffix})",
23
+ )
24
+ return None
@@ -0,0 +1,24 @@
1
+ """Credentials file provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..models import CredentialContext, CredentialResult
6
+ from ..provider import CredentialProvider
7
+
8
+
9
+ class CredentialsFileProvider(CredentialProvider):
10
+ """Resolves credentials from the api_key stored in credentials.ini."""
11
+
12
+ name = "credentials_file"
13
+
14
+ def resolve(self, context: CredentialContext) -> CredentialResult | None:
15
+ api_key = context.api_key_from_file
16
+ if api_key and api_key.strip():
17
+ api_key = api_key.strip()
18
+ suffix = api_key[-4:]
19
+ return CredentialResult(
20
+ api_key=api_key,
21
+ source_name="credentials_file",
22
+ source_detail=f"credentials.ini (ends with ...{suffix})",
23
+ )
24
+ return None
@@ -0,0 +1,24 @@
1
+ """Environment variable credential provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..models import CredentialContext, CredentialResult
6
+ from ..provider import CredentialProvider
7
+
8
+
9
+ class EnvVarProvider(CredentialProvider):
10
+ """Resolves credentials from the CLOUDSMITH_API_KEY environment variable."""
11
+
12
+ name = "env_var"
13
+
14
+ def resolve(self, context: CredentialContext) -> CredentialResult | None:
15
+ api_key = context.api_key_from_env
16
+ if api_key and api_key.strip():
17
+ api_key = api_key.strip()
18
+ suffix = api_key[-4:]
19
+ return CredentialResult(
20
+ api_key=api_key,
21
+ source_name="env_var",
22
+ source_detail=f"CLOUDSMITH_API_KEY env var (ends with ...{suffix})",
23
+ )
24
+ return None
@@ -0,0 +1,59 @@
1
+ """Keyring credential provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from ....cli.saml import refresh_access_token
8
+ from ....core import keyring
9
+ from ..models import CredentialContext, CredentialResult
10
+ from ..provider import CredentialProvider
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class KeyringProvider(CredentialProvider):
16
+ """Resolves credentials from SAML tokens stored in the system keyring."""
17
+
18
+ name = "keyring"
19
+
20
+ def resolve(self, context: CredentialContext) -> CredentialResult | None:
21
+ if not keyring.should_use_keyring():
22
+ return None
23
+
24
+ api_host = context.api_host
25
+ access_token = keyring.get_access_token(api_host)
26
+
27
+ if not access_token:
28
+ return None
29
+
30
+ try:
31
+ if keyring.should_refresh_access_token(api_host):
32
+ if not context.session:
33
+ logger.debug(
34
+ "Session unavailable; skipping token refresh, using existing token"
35
+ )
36
+ else:
37
+ refresh_token = keyring.get_refresh_token(api_host)
38
+ new_access_token, new_refresh_token = refresh_access_token(
39
+ api_host,
40
+ access_token,
41
+ refresh_token,
42
+ session=context.session,
43
+ )
44
+ keyring.store_sso_tokens(
45
+ api_host, new_access_token, new_refresh_token
46
+ )
47
+ access_token = new_access_token
48
+ except Exception: # pylint: disable=broad-exception-caught
49
+ keyring.update_refresh_attempted_at(api_host)
50
+ context.keyring_refresh_failed = True
51
+ logger.debug("Failed to refresh SAML token", exc_info=True)
52
+ return None
53
+
54
+ return CredentialResult(
55
+ api_key=access_token,
56
+ source_name="keyring",
57
+ source_detail="SAML token from system keyring",
58
+ auth_type="bearer",
59
+ )
@@ -0,0 +1,115 @@
1
+ """OIDC credential provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from ..models import CredentialContext, CredentialResult
8
+ from ..provider import CredentialProvider
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class OidcProvider(CredentialProvider):
14
+ """Resolves credentials via OIDC auto-discovery.
15
+
16
+ Requires CLOUDSMITH_ORG and CLOUDSMITH_SERVICE_SLUG to be set (via env
17
+ vars or click options). Auto-detects the environment, fetches the vendor
18
+ OIDC JWT, and exchanges it for a short-lived Cloudsmith API token.
19
+ """
20
+
21
+ name = "oidc"
22
+
23
+ def resolve( # pylint: disable=too-many-return-statements
24
+ self, context: CredentialContext
25
+ ) -> CredentialResult | None:
26
+ if context.oidc_discovery_disabled:
27
+ if context.debug:
28
+ logger.debug(
29
+ "OidcProvider: OIDC auto-discovery disabled via "
30
+ "CLOUDSMITH_OIDC_DISCOVERY_DISABLED"
31
+ )
32
+ return None
33
+
34
+ org = context.oidc_org
35
+ service_slug = context.oidc_service_slug
36
+
37
+ if not org or not service_slug:
38
+ if context.debug:
39
+ logger.debug(
40
+ "OidcProvider: CLOUDSMITH_ORG and/or CLOUDSMITH_SERVICE_SLUG "
41
+ "not set, skipping OIDC auto-discovery"
42
+ )
43
+ return None
44
+
45
+ from ..oidc.cache import get_cached_token, store_cached_token
46
+
47
+ # Check cache BEFORE environment detection — detection can be expensive
48
+ # (e.g. boto3 credential resolution, IMDS calls) and is unnecessary when
49
+ # we already hold a valid exchanged token.
50
+ cached = get_cached_token(context.api_host, org, service_slug)
51
+ if cached:
52
+ logger.debug("OidcProvider: Using cached OIDC token")
53
+ return CredentialResult(
54
+ api_key=cached,
55
+ source_name="oidc",
56
+ source_detail=f"OIDC [cached] (org: {org}, service: {service_slug})",
57
+ )
58
+
59
+ from ..oidc.detectors import detect_environment
60
+ from ..oidc.exchange import OidcExchangeError, exchange_oidc_token
61
+
62
+ detector = detect_environment(context=context)
63
+ if detector is None:
64
+ if context.debug:
65
+ logger.debug(
66
+ "OidcProvider: No supported OIDC environment detected, skipping"
67
+ )
68
+ return None
69
+
70
+ try:
71
+ vendor_token = detector.get_token()
72
+ except Exception: # pylint: disable=broad-exception-caught
73
+ logger.warning(
74
+ "OIDC: Failed to retrieve identity token from %s. "
75
+ "Use --debug for details.",
76
+ detector.name,
77
+ )
78
+ logger.debug(
79
+ "OidcProvider: %s token retrieval error",
80
+ detector.name,
81
+ exc_info=True,
82
+ )
83
+ return None
84
+
85
+ if not vendor_token:
86
+ logger.warning("OIDC: %s detector returned an empty token.", detector.name)
87
+ return None
88
+
89
+ try:
90
+ cloudsmith_token = exchange_oidc_token(
91
+ context=context,
92
+ org=org,
93
+ service_slug=service_slug,
94
+ oidc_token=vendor_token,
95
+ )
96
+ except OidcExchangeError as exc:
97
+ logger.warning("OIDC: Token exchange failed: %s", exc)
98
+ return None
99
+ except Exception: # pylint: disable=broad-exception-caught
100
+ logger.warning(
101
+ "OIDC: Token exchange failed unexpectedly. Use --debug for details."
102
+ )
103
+ logger.debug("OidcProvider: OIDC token exchange error", exc_info=True)
104
+ return None
105
+
106
+ if not cloudsmith_token:
107
+ return None
108
+
109
+ store_cached_token(context.api_host, org, service_slug, cloudsmith_token)
110
+
111
+ return CredentialResult(
112
+ api_key=cloudsmith_token,
113
+ source_name="oidc",
114
+ source_detail=f"OIDC via {detector.name} (org: {org}, service: {service_slug})",
115
+ )