feat(ENG-13681): add credential-helper domains with repository-scoped custom domains - #337
Draft
cloudsmith-iduffy wants to merge 9 commits into
Draft
feat(ENG-13681): add credential-helper domains with repository-scoped custom domains#337cloudsmith-iduffy wants to merge 9 commits into
cloudsmith-iduffy wants to merge 9 commits into
Conversation
Answers the question `credential-helper generic` deliberately does not: which hosts can Cloudsmith authenticate? A consumer needs both, and a token is org-wide, so the host check belongs in its own command. Emits a versioned JSON document listing the built-in service hosts alongside the organisation's custom domains, with the package format each one serves (null for hosts serving no single format) and a domain_type saying what the host is for. Custom domains are not knowable without an API call; shipping the built-in table here means consumers do not have to hardcode and re-sync a host list of their own. Disabled and unvalidated custom domains are listed with their state intact, so a domain that exists but does not work is visible rather than absent. The organisation comes from click context (CLOUDSMITH_ORG or oidc_org in config.ini); with none configured the built-in hosts are still listed, which needs no authentication. Like `generic`, this is for programmatic consumers only and has no human-readable output mode. The built-in hosts live in a shared default_domains table: which host serves which package format, and what each host is for (DomainType — the download CDN, the generic upload endpoint, or a format's native API). Consumers pick hosts by what they are for instead of matching hostnames. Format labels are derived from the backend kind, so a row cannot contradict itself, and classification compares backend_kind against None rather than truthiness because BackendKind.DEB is 0. BackendKind itself is synced with the server-side enum (NIX = 31 was missing; these are wire values, so nothing is renumbered). The table can be replaced wholesale by a [domains] section in config.ini for dedicated and on-premise deployments — honoured only from trusted locations (the user config dir, ~/.cloudsmith, or an explicit --config-file), never a directory-relative config.ini. That file is searched from the current working directory first, so a repository can ship one, and this list decides which hosts may receive a credential — the same reasoning behind the existing api_host guard in the decorators. The trusted/untrusted split is derived from ConfigReader's search path rather than restated. An untrusted section is ignored with a warning on stderr, suppressed when an explicit --config-file is passed: that file is honoured wherever it lives, and warning that it is ignored would claim the opposite of what happens. The custom-domain lookup runs strict: get_custom_domains() grows a strict flag that re-raises failures instead of degrading to an empty list, so a typo'd organisation, a missing permission or an unreachable API exits non-zero with a message on stderr instead of silently rendering as "no custom domains" (a 402 still returns [], since the feature being disabled genuinely means there are none). Best-effort callers — installer discovery — keep the default and degrade as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two findings from a review of the domain table: - `_trusted_config_path` returned the first trusted config.ini that merely existed, so a config holding only [default] api settings masked a [domains] section in a later candidate and silently restored the public *.cloudsmith.io table. Fall through to the first config that actually declares the section. - Resolve default hosts per call rather than freezing them from the built-in table at import, so a dedicated or on-premise deployment's [domains] override reaches every caller instead of only this command. Also stop caching a failed custom-domain lookup, so a strict lookup cannot be served a cached 404 as a successful "no custom domains". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ict caches Review flagged that the version gate called data.get() before confirming the top-level JSON was a dict, so a corrupted or hand-edited cache file (e.g. a bare list) raised AttributeError instead of degrading to a miss. Order the isinstance(data, dict) check first and add a regression test.
Review ruling: a 402 (product feature not enabled) is a stable, correct answer - not a failure - so it should be cached like any other result to avoid hitting the API on every invocation forever. A 404 (org not found, usually a typo) must keep failing loudly on every lookup, which is why it stays uncached.
A run with no CLOUDSMITH_ORG configured now lists whatever custom domains earlier runs already cached for other organisations, at no API cost, instead of omitting custom domains entirely. Each entry gains org and repository attribution sourced from the cached record itself (never the sanitised cache filename), and --refresh forwards through to bypass the cache for a configured organisation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
load_config prepends an explicit --config-file to ConfigReader.config_files as an absolute path, but _config_candidates deliberately skips absolute filenames (to keep them out of the untrusted directory-relative scan). That skip also hid the file from the trusted no-argument load_default_domains() path, so `-C <dir>` honoured a [domains] override while `-C <file>` silently fell back to the built-in *.cloudsmith.io table - contradicting the CHANGELOG's promise that an explicit --config-file is honoured. _explicit_config_candidate() now reads that absolute config_files entry directly, ahead of the trusted search-path candidates, while untrusted_config_declares_domains() keeps ignoring it exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sting Record on the `credential-helper domains` bullet: the new `org` and `repository` fields (null for built-in hosts), the seven-day on-disk cache with `--refresh` to bypass it, and that a run with no organisation configured lists whatever earlier runs already cached rather than querying every organisation the caller belongs to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Stack 2/4 — the custom-domain foundation the Maven helper (#339) builds on.
Base: #336.
Adds
cloudsmith credential-helper domains, which emits a versioned JSONdocument listing the hosts Cloudsmith can authenticate — built-in
*.cloudsmith.ioservice hosts plus an organisation's custom domains — with thepackage format each serves and what each host is for (
download,upload,native_api), so a consumer picks the right host without pattern-matching onhostnames.
Alongside it:
replace the built-in hosts via a
[domains]section inconfig.ini. This ishonoured only from trusted configuration — your config directory,
~/.cloudsmith, or an explicit--config-file. A[domains]section in adirectory-relative
config.iniis ignored with a warning, because that filecan be committed to a repository and this list decides which hosts may receive
a credential.
discovered under and, when the domain serves a single repository, which one.
Such a domain identifies the repository on its own, which changes the URL
shape — consumed by feat(ENG-13683): add Maven credential helper via shell plugin #339.
--refreshto bypass it. Failures thatcould later succeed are never cached, so a typo'd organisation fails loudly
every time rather than being replayed as a successful "no custom domains". A
402 is cached: "the custom-domains product is not enabled" is a settled
answer, not a failure, and not caching it would hit the API on every
credential-helper invocation for such an organisation.
earlier run already cached, at no API cost, rather than enumerating every
organisation the caller belongs to.
Type of Change