Skip to content

feat(ENG-13681): add credential-helper domains with repository-scoped custom domains - #337

Draft
cloudsmith-iduffy wants to merge 9 commits into
credential-plumbingfrom
custom-domains
Draft

feat(ENG-13681): add credential-helper domains with repository-scoped custom domains#337
cloudsmith-iduffy wants to merge 9 commits into
credential-plumbingfrom
custom-domains

Conversation

@cloudsmith-iduffy

@cloudsmith-iduffy cloudsmith-iduffy commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

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 JSON
document listing the hosts Cloudsmith can authenticate — built-in
*.cloudsmith.io service hosts plus an organisation's custom domains — with the
package format each serves and what each host is for (download, upload,
native_api), so a consumer picks the right host without pattern-matching on
hostnames.

Alongside it:

  • A replaceable domain table. A dedicated or on-premise deployment can
    replace the built-in hosts via a [domains] section in config.ini. This is
    honoured only from trusted configuration — your config directory,
    ~/.cloudsmith, or an explicit --config-file. A [domains] section in a
    directory-relative config.ini is ignored with a warning, because that file
    can be committed to a repository and this list decides which hosts may receive
    a credential.
  • Repository-scoped custom domains. Records carry the organisation they were
    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.
  • A seven-day on-disk cache, with --refresh to bypass it. Failures that
    could 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.
  • With no organisation configured, the command lists whatever custom domains an
    earlier run already cached, at no API cost, rather than enumerating every
    organisation the caller belongs to.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Other (please describe)

cloudsmith-iduffy and others added 9 commits July 31, 2026 23:16
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>
Comment thread cloudsmith_cli/cli/tests/commands/test_credential_helper_domains.py Dismissed
Comment thread cloudsmith_cli/cli/tests/commands/test_credential_helper_domains.py Dismissed
Comment thread cloudsmith_cli/cli/tests/commands/test_credential_helper_domains.py Dismissed
@cloudsmith-iduffy cloudsmith-iduffy changed the title custom domains feat(ENG-13681): add credential-helper domains with repository-scoped custom domains Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants