data-api: follow auth context via /.well-known/entire-api.json#1377
data-api: follow auth context via /.well-known/entire-api.json#1377toothbrush wants to merge 4 commits into
Conversation
activity/search/trail/dispatch now discover which login server the data API host trusts (and which audience to exchange for) from its /.well-known/entire-api.json, then pick the matching auth context and exchange that context's token — the same cluster semantics git already uses. So `ENTIRE_API_BASE_URL=https://partial.to entire activity` authenticates as the partial.to login without also setting ENTIRE_AUTH_BASE_URL. Falls back to today's static token resolution when the host doesn't advertise discovery (404/unreachable/503), so nothing breaks pre-deploy. - clusterdiscovery: generalized to serve both entire-cluster.json and entire-api.json (shared selectContext); DiscoverAPI/ResolveContextForAPI - auth: NewRefreshingResourceProvider (per-context, audience-aware exchange) + ResolveDataAPIToken (discovery + fallback) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 42efb4b17beb
There was a problem hiding this comment.
Pull request overview
This PR adds data-API “auth context discovery” so commands that target a data host (activity/search/trail/dispatch + the authenticated API client) can automatically pick the correct login context by fetching /.well-known/entire-api.json from the data host, then exchanging for the advertised audience. It preserves existing behavior by falling back to the current static token resolution when discovery is unavailable.
Changes:
- Generalize cluster discovery selection logic to also support API-host discovery (
/.well-known/entire-api.json) and reuse the same context-selection semantics. - Introduce data-API token resolution (
auth.ResolveDataAPIToken) and a per-context, audience-aware token exchange provider (auth.NewRefreshingResourceProvider), wiring it into search/dispatch and the authenticated API client. - Update architecture documentation to reflect the new discovery mechanism and audience semantics.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/entireclient/clusterdiscovery/resolve.go | Generalizes context selection messaging to work for both clusters and API hosts. |
| internal/entireclient/clusterdiscovery/discovery.go | Extracts shared well-known JSON fetch helper and refactors cluster discovery to use it. |
| internal/entireclient/clusterdiscovery/api_discovery.go | Adds API-host discovery parsing and context resolution using trusted_issuers + audience. |
| internal/entireclient/clusterdiscovery/api_discovery_test.go | Adds tests for API discovery behavior, error folding, and context selection semantics. |
| docs/architecture/upstream-host-resolution.md | Documents the new /.well-known/entire-api.json flow and audience exchange behavior. |
| cmd/entire/cli/search_cmd.go | Switches search token resolution to the new discovery-aware resolver. |
| cmd/entire/cli/dispatch/mode_local.go | Switches dispatch token lookup wiring to use the discovery-aware resolver. |
| cmd/entire/cli/auth/refresh.go | Adds NewRefreshingResourceProvider to exchange a context’s login JWT for a resource audience. |
| cmd/entire/cli/auth/data_api.go | Implements ResolveDataAPIToken (discovery + selection + exchange + fallback). |
| cmd/entire/cli/auth/data_api_test.go | Adds tests covering fallback behavior, surfacing selection errors, and exchange parameters. |
| cmd/entire/cli/api_client.go | Updates the authenticated API client to use ResolveDataAPIToken instead of static resolution. |
NewRefreshingLoginProvider and NewRefreshingResourceProvider shared ~30 near-identical lines (validation, tokenmanager.New, the reauth error switch). Extract newContextTokenManager + contextReauthError; the two providers now differ only in Refresh() vs Token(req) and the residual error wording. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 6f9136693ab0
The "where do I log in" hints now just say `entire login` (plus `entire auth use` to switch between existing logins) instead of `ENTIRE_AUTH_BASE_URL=<url> entire login`. The env-var override stays a power-user mechanism; fully sunsetting it (+ `entire login --server`) is tracked in COR-393. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 93d30f60bf97
…very redirects Two reviewer findings: - ErrNotLoggedIn lost after discovery (cursor + Copilot): contextReauthError returned a plain string, so callers that branch on errors.Is(err, ErrNotLoggedIn) (NewAuthenticatedAPIClient/search/dispatch) fell through to their generic error — a regression vs the pre-discovery TokenForResource path. Wrap the sentinel via a reauthError type that keeps the friendly context-named message while unwrapping to the tokenmanager sentinel. - Redirect-following in fetchWellKnownJSON (Copilot): a trust-root fetch must not follow a 3xx to another origin/plaintext. Refuse redirects on a shallow-copied client (so the caller's redirect policy is untouched). Low real exploitability — the token is never sent to the redirect target — but cheap hardening that covers the cluster path too. Tests: provider error unwraps to ErrNotLoggedIn; cross-origin redirect (to a server serving a valid doc) is refused rather than followed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: d24466d6862b
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f883714. Configure here.
| fmt.Fprint(&b, "\nLog in with `entire login`, then re-run your command.\n"+ | ||
| "If you already have a login on one of those servers, switch to it with `entire auth use <context>`.") |
There was a problem hiding this comment.
Intentional: ENTIRE_AUTH_BASE_URL is being phased out this week in favour of entire login --server (COR-393), so the hint stays as plain entire login for now.
| case errors.Is(err, tokenmanager.ErrReauthRequired): | ||
| return &reauthError{ | ||
| msg: fmt.Sprintf("login session for %q (%s) expired; run `entire login` to re-authenticate", c.Name, coreURL), | ||
| sentinel: tokenmanager.ErrReauthRequired, | ||
| } |
There was a problem hiding this comment.
Intentional — same as above: env-var form is going away in favour of entire login --server (COR-393).
| case errors.Is(err, tokenmanager.ErrNotLoggedIn): | ||
| return &reauthError{ | ||
| msg: fmt.Sprintf("no usable login for %q (%s); run `entire login`", c.Name, coreURL), | ||
| sentinel: tokenmanager.ErrNotLoggedIn, | ||
| } |
There was a problem hiding this comment.
Intentional — same as above: entire login --server will make this per-core actionable (COR-393).
activity/search/trail/dispatchnow pick the right login automatically when you point only at a data-API host:The CLI reads the host's
/.well-known/entire-api.jsonto learn which login servers it trusts (and which audience to exchange for), then selects the matching auth context — same cluster semanticsgit clonealready uses (active-wins-if-eligible → sole → explicit choice). This fixes the cross-core case: active context is a prodentire.iologin, but the command targetspartial.to.Falls back to today's static token resolution when the host doesn't advertise discovery (404 / unreachable / 503), so nothing breaks before the server side ships.
Server side: entirehq/entire.io#2277.
Changes
clusterdiscovery: generalized to serve bothentire-cluster.jsonandentire-api.json(sharedselectContext);DiscoverAPI/ResolveContextForAPI.auth:NewRefreshingResourceProvider(per-context, audience-aware RFC 8693 exchange) +ResolveDataAPIToken(discovery + fallback). Wired intoNewAuthenticatedAPIClient, dispatchlookupResourceToken, searchresolveSearchToken.docs/architecture/upstream-host-resolution.md.Note: audience = the data host origin (
https://entire.io/https://partial.to), not an opaque string — confirmed against entire.io'sENTIRE_CORE_JWT_AUDIENCEand entiredb's api-access exchange.🤖 Generated with Claude Code
Note
High Risk
Changes authentication for all data-API CLI entry points (token exchange, context selection, and discovery fallback), which are security-critical and affect multi-core staging vs prod workflows.
Overview
Data API auth now follows the target host, not only the active context and
ENTIRE_AUTH_BASE_URL. Commands that dialENTIRE_API_BASE_URL(activity,search,trail,dispatch, andNewAuthenticatedAPIClient) callauth.ResolveDataAPITokeninstead ofTokenForResourceon an origin-only URL.That path fetches
/.well-known/entire-api.json, selects a local context with the same rules as git cluster discovery (active-if-eligible → sole eligible → explicit choice), and mints a bearer viaNewRefreshingResourceProvider(per-contextc.CoreURL, RFC 8693 exchange for the document’saudience). If discovery is missing or unusable (ErrDiscoveryUnavailable), behavior falls back to the old singletonTokenForResourcepath; real selection failures still surface to the user.clusterdiscoveryaddsDiscoverAPI/ResolveContextForAPI, sharesselectContextand a hardenedfetchWellKnownJSON(HTTPS-only, no redirects) with cluster discovery, and updates login hints.refresh.gocentralizesnewContextTokenManagerandreauthErrorsoErrNotLoggedInstill unwraps on the discovery path. Architecture docs mark the web/data API resolution slice as done.Reviewed by Cursor Bugbot for commit f883714. Configure here.