# auth: route RepoScopedToken through cluster discovery (COR-395)

`821be3e`→[main](/content/gh/entireio/cli/commits/main/index.html)·

toothbrush·1mo ago·8 files·+190 added/-128 removed

RepoScopedToken minted repo-scoped tokens by exchanging the active context's login JWT at the hard-coded api.AuthBaseURL() core, never asking which core the target cluster trusts. Multi-core this ships the token to the wrong core's STS (exchange fails, or worse authenticates the wrong identity); the same gap COR-389, PR #1377, and git-remote-entire each fixed for their surface.

Resolve the login context the way git-remote-entire does — the cluster's /.well-known/entire-cluster.json names its trusted core(s), clusterdiscovery picks the eligible local context (active if eligible, sole, or explicit-choice error) — then exchange through repocreds with a refreshing login provider. This also closes the documented stale login-JWT gap (expired JWTs now re-mint from the refresh token) and makes the wire form exactly what git-remote-entire sends, client_id via Basic auth included.

httputil.OAuthError gains a Code field (RFC 6749 `error`, parsed best-effort) so the suspended-mirror sentinel ErrRepoTargetUnknown keeps working over the repocreds path.

The second COR-395 item, deleting resolveAuthHostToken, was already done; this removes the last stale comment reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

## Sessions

### Changes

8

- cmd/entire/cli

- auth

- Mdata_api.go+6/-6

- Mdata_api_test.go+1/-1

- Mrepo_token.go+66/-83

- Mrepo_token_test.go+78/-34

- Mrepo_mirror.go+1/-1

- Mrepo_mirror_probe.go+1/-1

- internal/entireclient/httputil

- Moauth.go+8/-2

- Moauth_test.go+29

```go
// resolution, so a slow or absent endpoint must not stall the command.
const dataAPIDiscoveryTimeout = 8 * time.Second

// resolveContextForAPIFunc is the shape of the discovery seam: it mirrors
// clusterdiscovery.ResolveContextForAPI (ctx, configDir, cacheDir, apiHost,
// httpClient, debugf).
type resolveContextForAPIFunc func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error)
// resolveContextFunc is the shape of a context-discovery seam: it mirrors
// clusterdiscovery.ResolveContextForAPI / ResolveContextForCluster
// (ctx, configDir, cacheDir, host, httpClient, debugf).
type resolveContextFunc func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error)

// resolveContextForAPI is the discovery seam, swapped in tests so they don't
// reach the network. See SetResolveContextForAPIForTest for cross-package tests.
var resolveContextForAPI resolveContextForAPIFunc = clusterdiscovery.ResolveContextForAPI
var resolveContextForAPI resolveContextFunc = clusterdiscovery.ResolveContextForAPI

// SetResolveContextForAPIForTest overrides the /.well-known/entire-api.json
// discovery seam and returns a cleanup func. Tests in other packages that
// configured data host and bypasses any SetManagerForTest fallback seam. Pass
// a func returning clusterdiscovery.ErrDiscoveryUnavailable to force the static
// fallback path. Test-only.
func SetResolveContextForAPIForTest(t interface{ Helper() }, fn resolveContextForAPIFunc) func() {
func SetResolveContextForAPIForTest(t interface{ Helper() }, fn resolveContextFunc) func() {
    t.Helper()
    prev := resolveContextForAPI
    resolveContextForAPI = fn
}
```

```go
func RepoScopedToken(ctx context.Context, clusterBaseURL, repoSlug, action string) (string, error) {
    provider := CurrentProvider()
    if strings.TrimSpace(provider.STSPath) == "" {
        return "", errors.New("repo-scoped token exchange requires a v2 auth host (set ENTIRE_AUTH_BASE_URL to a login server that exposes /oauth/token)")
    }

loginJWT, err := LookupCurrentToken()
    clusterCtx, err := resolveContextForCluster(ctx, contexts.DefaultConfigDir(), discovery.DefaultCacheDir(), clusterHost, httpClient, nil)
    if err != nil {
        return "", fmt.Errorf("read login token: %w", err)
    }
    if loginJWT == "" {
        return "", ErrNotLoggedIn
    }

// Exchange logic here
}
```

- `ErrRepoTargetUnknown reports that the cluster's STS refused the exchange`

- `repoExchangeTransportForTest, when non-nil, is used as the sts.Client`

- `TestRepoScopedToken_InvalidTarget asserts that a 400 invalid_target STS`

### Additional Information
The above content includes various comments and code snippets that pertain to the implementation and testing of the `RepoScopedToken` function. The code contains significant detail about error handling, testing scenarios, and the overall workings of the token exchange process defined in the given package.
