Skip to content

[COR-174] auth: shareable auth library + split-host config + RFC 8693 token exchange#1153

Open
khaong wants to merge 18 commits intomainfrom
alex/cli-auth-consolidation
Open

[COR-174] auth: shareable auth library + split-host config + RFC 8693 token exchange#1153
khaong wants to merge 18 commits intomainfrom
alex/cli-auth-consolidation

Conversation

@khaong
Copy link
Copy Markdown
Contributor

@khaong khaong commented May 8, 2026

https://entire.io/gh/entireio/cli/trails/327

Summary

Linear: COR-174

Note

STS endpoint will be moving soon. v2 currently exposes the RFC 8693 token-exchange endpoint at /api/authz/sts/token (encoded in provider.go's v2 surface). Server-side work is in flight to relocate it; once that lands, provider.stsPath flips to the new path and the CLI side is a one-line change. No CLI release is needed in the interim — v1 is single-host and unaffected, and v2 staging deployments can stay pinned until the move completes.

Stands up a shareable auth library at the repo root (auth/...) and migrates the CLI's auth wiring onto it. Also enables the partial.to staging deployment where the auth issuer (us.auth.partial.to) and the data API (partial.to) live on different origins by introducing per-resource RFC 8693 token exchange.

  • auth/ library (new + reorganised): deviceflow (RFC 8628), sts (RFC 8693), tokens (TokenSet + JWT claim parsing), tokenstore (pluggable persistence + Keyring impl), and the new tokenmanager (orchestration: cache, JWT-aud shortcut, exchange dispatch). The library is provider-agnostic — every endpoint, identifier, and default value comes from Config. No env-var reads, no globals, no implicit URLs. Ready to share with other internal CLIs.
  • Split-host config: new ENTIRE_AUTH_BASE_URL env var; AuthBaseURL() falls back to BaseURL() so single-host deployments are unchanged. Tokens are now keyed in the keyring by the auth issuer (the host that minted them). Auth-management commands (auth list/revoke/status/logout) routed to the auth host since their endpoints live there.
  • NewAuthenticatedAPIClient and entire search go through the manager. Data-API calls obtain a resource-audience bearer via RFC 8693 exchange when the core token's audience doesn't already match. All 7 callers thread ctx. Search now defaults serviceURL to api.BaseURL() (was hardcoded entire.io) and normalizes path-bearing URLs to scheme+host before token resolution.
  • v2 client_id aligned to entire-cli (matched v1).
  • Provider table carries stsPath per surface; v2's STS lives at /api/authz/sts/token. v1 is single-host, so its stsPath is empty and tokenmanager.Config.STSPath is optional — the same-host shortcut wins. Misconfigured split-host setups fail loudly via ErrNoSTSPath.

Behaviour fixes surfaced during review

  • Store.LoadTokens legacy bare-string fallback was unreachable — it only fired on ErrNotFound, but a pre-shim raw-token entry produced an unmarshal error. Added tokenstore.ErrMalformed so callers can distinguish "no entry" from "entry exists but malformed" and route the legacy path correctly. Store.GetToken was over-permissive in the opposite direction (any error → fallback, masking real keyring failures); now also gated on ErrMalformed.
  • tokenmanager.runExchange was silently dropping req.Resourcests.ExchangeRequest.Resource (RFC 8693 §2.1) was never sent to the AS. Fixed and tested.
  • JWT-aud shortcut in tokenmanager.Token now gated on empty Audience so an explicit per-call Audience always forces an exchange (was silently downgraded if the core token's aud happened to include the resource).
  • tokenmanager.DeleteCoreToken order swapped: keyring delete first, in-memory cache clear only on success — pre-emptive clear created a window where the CLI thought it was logged out but the keyring still held the token.
  • api.bearerTransport rejects empty bearer at first request rather than putting Authorization: Bearer<space> on the wire (which produced confusing 401s).

Test plan

  • mise run fmt && mise run lint clean
  • go test ./auth/... — all pass
  • go test ./cmd/entire/cli/auth/... ./cmd/entire/cli/api/... — all pass
  • Tested end-to-end against the partial.to staging deployment — entire login, entire trail list, entire search all working through the device flow → STS exchange → resource-scoped bearer chain
  • go test ./cmd/entire/cli/... — only the two pre-existing failures (TestGroupCommitsByDay_SortsNewestFirst, TestExplainCmd_PositionalArgConflictsWithFlags) remain, unchanged from main

Notes for reviewers

  • The branch picks up earlier commits already merged on prior PRs (the auth/sts package, RFC 8628 §3.5 error_description support, the v1/v2 provider switch, etc.). Easiest review path is by commit — the meaningful new work starts at c492a54b1:
    • c492a54b1 — split-host + tokenmanager (largest commit)
    • d9322bc2a — STS path on provider, STSPath optional
    • ead027cf9 — search routing
    • 16746fd66 — round-1 review fixes (legacy fallback bug, doc rot, v2 path coverage, empty-bearer guard)
    • 5173d30fc — round-2 review fixes (DeleteCoreToken ordering, additional coverage, deprecation tags)
    • f33b79dfc — codex review fixes (Audience-gates-aud-shortcut, Resource on STS, search URL normalization)
    • d8ccd264a — test isolation against provider env
  • Deferred to a follow-up PR (separate, focused refactor): constructor parity for auth/deviceflow.Client + auth/sts.Client, moving tokenmanager.Config.Now/Exchange off the public Config to test-only seams, TokenRequest.ResourceResourceURL rename, tokens.ParseClaims value-return, typed-error replacement of descriptionFromSentinel, expose tokenstore.Keyring.Service() as a getter.

🤖 Generated with Claude Code


Note

High Risk
High risk because it rewires core authentication/token storage and introduces RFC 8693 token exchange plus new base-URL routing, which can cause login failures or 401s across CLI commands if misconfigured.

Overview
Adds a new shareable auth/ Go library implementing OAuth device flow (RFC 8628), token exchange (RFC 8693), token persistence, and a tokenmanager that caches resource-scoped bearers and short-circuits when exchange isn’t needed.

Updates the CLI to support split auth/data origins via ENTIRE_AUTH_BASE_URL and provider-version routing via ENTIRE_AUTH_PROVIDER_VERSION, moving authenticated calls (e.g. NewAuthenticatedAPIClient, search, dispatch) to resolve per-resource tokens through the manager rather than reading the keyring directly.

Refactors keyring storage to JSON-encoded token bundles with explicit ErrMalformed handling and legacy bare-string fallback, routes auth-token management endpoints to the auth host, and tightens request safety (reject empty bearer, enforce secure URL checks for both auth and data hosts).

Reviewed by Cursor Bugbot for commit 9c2b070. Configure here.

khaong and others added 15 commits May 8, 2026 09:50
Introduces github.com/entireio/cli/auth as a shared OAuth client library
for the Entire CLI. Three subpackages ship in this commit:

* auth/tokens     — TokenSet bundle plus unverified JWT claim parsing
* auth/tokenstore — Store interface plus an OS-keyring reference impl
* auth/deviceflow — RFC 8628 OAuth Device Authorization Grant client

The packages are deliberately provider-agnostic: every server-specific
value (endpoint paths, client_id, scope) is supplied at construction.
The library has no global state, no implicit URLs, and no provider
detection. It is intended to be importable by any RFC 8628 / RFC 8693
caller.

No existing callers are wired up in this commit; the cmd/entire/cli
shim swap follows separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the bespoke device-flow client and keyring store in
cmd/entire/cli/auth with thin wrappers over auth/deviceflow and
auth/tokenstore. The package's exported API (NewClient, NewStore,
DeviceAuthStart, DeviceAuthPoll, LookupCurrentToken, etc.) is preserved
field-for-field so login.go / logout.go / auth.go don't need to change.

Two wrapper concerns worth noting:

1. PollDeviceAuth maps the shared library's RFC 8628 §3.5 sentinel
   errors back to the wire-side error code in DeviceAuthPoll.Error.
   This keeps the existing polling loop in login.go (which switches on
   result.Error) working unchanged.

2. Store.GetToken keeps a backward-compatibility fallback for keyring
   entries written before this commit, which stored bare access-token
   strings rather than JSON-encoded TokenSets. SaveToken always writes
   the new shape; GetToken transparently handles both.

The legacy decodeJSON / decodeJSONStrict tests are removed; equivalent
coverage now lives in auth/deviceflow tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a transition-period env-var switch that picks between two
device-flow configurations:

  v1 (default): /oauth/device/code + /oauth/token, client_id="entire-cli"
  v2          : /api/auth/oauth/device/code + /api/auth/token, client_id="cli"

Both surfaces speak the same RFC 8628 protocol; only the paths and
client_id differ. Default behaviour is unchanged. Setting
ENTIRE_AUTH_PROVIDER_VERSION=v2 (alongside an appropriate
ENTIRE_API_BASE_URL) opts a user into the next-generation surface early.

Unrecognised values fall back to v1 so old binaries stay safe if a
future v3 ever ships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the fourth subpackage of the auth/ library: a small, provider-
agnostic client for RFC 8693 token exchange.

Caller supplies BaseURL, Path, and per-call ExchangeRequest fields
(SubjectToken, SubjectTokenType, RequestedTokenType, plus optional
Audience/Resource/Scope and an Extra url.Values for any non-standard
form fields the server expects). The package defines constants only
for RFC 8693's standard token-type URIs and the token-exchange
grant_type — the requested-token-type URI is always caller-supplied.

Returns *tokens.TokenSet on success with absolute ExpiresAt; wraps
RFC 6749 / 8693 error responses with both code and description.

Tests cover happy path, optional-field omission, Extra forwarding,
standard-fields-override-Extra precedence, missing required fields,
JSON and non-JSON server errors, missing access_token, and the no-
expiry case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captive portals, corporate proxies, and VPN firewalls (Cloudflare WARP,
etc.) commonly intercept the OAuth endpoint and return a 200 OK with an
HTML error page. Today the JSON decoder produces an opaque error like:

  start login: decode device auth start response: decode JSON response:
    invalid character '<' looking for beginning of value

That tells the user nothing actionable. Now both auth/deviceflow and
auth/sts surface:

  could not reach authentication server: server returned non-JSON
    response (check VPN, proxy, or firewall — e.g. Cloudflare WARP)

Implementation lives in a new internal package auth/internal/oauthhttp.
Both deviceflow and sts now run their successful-response bodies
through oauthhttp.ReadAndDecodeJSON, which sniffs for a leading '<'
(after trimming whitespace) and returns a typed ErrNonJSONResponse
sentinel — callers can errors.Is when they want to branch, or just let
the message bubble up.

Tests cover the helper in isolation plus end-to-end paths through both
StartDeviceAuth, PollDeviceAuth, and Exchange.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The token endpoint's error response carries an optional human-readable
error_description alongside the standard error code (RFC 6749 §5.2,
inherited by RFC 8628 §3.5). The lib was decoding only the code, which
collapsed several distinct invalid_grant flavours — "device_code unknown"
vs "client_id does not match grant" vs already-consumed replay — into a
single opaque "device authorization failed: invalid_grant" at the CLI.

Pull through:

* auth/deviceflow now decodes both fields on a non-2xx response and
  wraps the sentinel error as fmt.Errorf("%w: %s", sentinel, desc) so
  errors.Is(err, ErrInvalidGrant) keeps matching while the message
  retains the description.
* cmd/entire/cli/auth.DeviceAuthPoll grows an ErrorDescription field;
  the shim extracts it from the wrapped sentinel.
* cmd/entire/cli/login.go appends ": <description>" to the user-facing
  failure message when the server provided one.

Two new deviceflow tests cover the description-present and
description-absent paths (no trailing colon-space when absent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…version

The auth-tokens endpoint family lives at different paths on the two
backends — historical /api/v1/auth/tokens vs the consolidated
/api/auth/tokens. ENTIRE_AUTH_PROVIDER_VERSION already gates the
device-flow path split; auth_tokens.go now reads the same env var to
pick its base path.

ListTokens, RevokeToken, and RevokeCurrentToken all flow through one
authTokensBasePath() helper so future paths land in one place.

The env-var name is duplicated as a constant rather than imported from
cmd/entire/cli/auth: api/ is a leaf package and shouldn't take a
dependency on auth/ for routing. Both reads must stay in sync; flagged
in a comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Across the auth/ library and customer-CLI shim, golangci-lint flagged a
fistful of routine findings that the existing files inherited or that
my recent commits introduced. None are correctness bugs; just noise
that the repo's strict configuration wants explicit suppression for.

* auth/deviceflow/deviceflow_test.go and auth/sts/sts_test.go grow a
  shared writeBody(t, w, body) helper, replacing every `_, _ = io.WriteString`
  in test fixtures. errcheck-clean without per-callsite nolints.
  newTestClient drops its unused *httptest.Server return (unparam).

* auth/sts/sts.go suppresses gosec G101 on the three RFC 8693 standard
  URI constants (GrantTypeTokenExchange, SubjectTokenType*) and
  errcheck on the best-effort body read in readAPIError.

* auth/tokenstore/keyring.go suppresses gosec G117 on the json.Marshal
  call that intentionally serialises the access token into the
  OS-keyring entry (encrypted at rest by the OS).

* cmd/entire/cli/api/auth_tokens.go suppresses G101 on
  authTokensProviderVersionEnvVar — env-var name, not a credential.

* cmd/entire/cli/auth/provider.go suppresses G101 on the v1/v2 entries
  in the providers map (OAuth client_id and endpoint paths, not
  credentials).

* cmd/entire/cli/auth/provider_test.go extracts wantClientIDV1 /
  wantClientIDV2 test-local constants to satisfy goconst, then uses
  them in every comparison.

cmd/entire/cli/auth/{client,store}.go also need nolint:wrapcheck
comments on the four shim returns — those changes sit in the working
tree alongside the in-progress AuthBaseURL refactor and will go in
together with that commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets the CLI talk to deployments where the auth issuer and data API
live on different origins (e.g. us.console.partial.to mints tokens that
are then exchanged for partial.to-scoped tokens before each data-API
call).

Split-host plumbing:
- New ENTIRE_AUTH_BASE_URL env var; AuthBaseURL() falls back to
  BaseURL() so single-host deployments are unchanged.
- Tokens are keyed in the keyring by the auth issuer (the host that
  minted them), not by the data API URL.
- Auth-management commands (auth list/revoke/status/logout) hit the
  auth host via NewClientWithBaseURL since their endpoints live there.
- Align v2 client_id to "entire-cli" to match v1.

New shareable library auth/tokenmanager:
- Provider-agnostic orchestration over auth/sts: cache, JWT-aud
  shortcut, exchange dispatch.
- Config struct takes Issuer, ClientID, STSPath, Store, plus defaults
  and test hooks. No globals, no env-var reads, no implicit URLs —
  ready to share with other internal CLIs.
- TokenForResource/Token resolve to:
  1) ErrNotLoggedIn when the store is empty,
  2) core token verbatim when issuer == resource,
  3) core token verbatim when its aud claim already includes the
     resource (multi-audience tokens skip exchange),
  4) RFC 8693 exchange otherwise, cached per (core, resource,
     audience, requested-token-type, scope) until expiry.

CLI wiring:
- NewAuthenticatedAPIClient now takes ctx and routes through
  tokenmanager so data-API calls carry the right-audience bearer.
  All 7 callers updated to pass ctx.
- cmd/entire/cli/auth/exchange.go is a thin shim that builds a
  package-level Manager from the active provider + NewStore() and
  exposes TokenForResource / Token / ErrNotLoggedIn.
- *Store now implements tokenstore.Store so it can be passed to the
  Manager, preserving the legacy bare-string keyring fallback.

Fix discovered along the way:
- search defaulted to a hardcoded entire.io serviceURL; now defaults
  to api.BaseURL() when ENTIRE_SEARCH_URL is unset.

Misc gofmt/lint autofixes in auth/deviceflow, auth/sts, auth/tokens
that the linter applied while iterating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nager

Two fixes that came out of getting `entire trail list` working against
partial.to's split-host deployment:

- Provider config now carries an stsPath alongside the OAuth token
  endpoint. v2's STS lives at /api/authz/sts/token, distinct from the
  /api/auth/token OAuth endpoint that rejects token-exchange grants
  with unsupported_grant_type. cmd/entire/cli/auth/exchange.go now
  passes provider.stsPath (rather than provider.tokenPath) into the
  tokenmanager.
- v1 is the legacy single-host surface (entire.io for both auth and
  data API), so the same-host shortcut in tokenmanager.Token always
  wins and STS is never invoked. v1.stsPath is left empty.
- tokenmanager.Config.STSPath is now optional. New() no longer
  rejects empty STSPath; runExchange() returns the new ErrNoSTSPath
  sentinel if an exchange is actually attempted with no path
  configured. Single-host setups (incl. v1) need no STS endpoint;
  split-host misconfigurations fail loudly at the right layer.

Tests updated to cover empty-STSPath construction, the ErrNoSTSPath
path, and v1's empty stsPath contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`entire search` was sending the raw core token to the search service,
which on split-host deployments has the wrong audience (auth host
issuer, not the data API). Switch to auth.TokenForResource(ctx,
serviceURL) so the bearer is exchange-resolved against the search
service URL: same-host shortcut keeps single-host setups unchanged,
split-host setups now get an exchanged token with aud=entire-api.

Also moves the auth lookup after the git/repo plumbing so the
resource URL (which can come from ENTIRE_SEARCH_URL or api.BaseURL())
is known at the time we resolve the bearer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review surfaced a real correctness bug plus a handful of
clarity/coverage gaps. This commit fixes them in one pass.

Critical fix — Store.LoadTokens legacy bare-string fallback was dead
code:
- tokenstore.Keyring.LoadTokens returned "unmarshal TokenSet: ..." for
  pre-shim bare-string entries, not ErrNotFound. The cmd-side shim's
  fallback only fired on ErrNotFound, so users with pre-shim keyring
  entries appeared logged out after upgrading to the manager-backed
  code path (entire trail/search/etc.). The legacy GetToken path was
  separately over-permissive: it fell back on any error, masking real
  keyring errors.
- Add tokenstore.ErrMalformed sentinel returned (wrapped) by
  decodeTokenSet on JSON unmarshal or expires_at parse failures.
- Update Store.LoadTokens / Store.GetToken to fall back precisely on
  ErrMalformed (legacy path) and surface ErrNotFound + real keyring
  errors verbatim. Regression tests pre-seed bare-string keyring
  entries and assert the round-trip.

api.bearerTransport: reject empty bearer at first request rather than
sending Authorization: Bearer<space> on the wire (which produces a
confusing 401). New errEmptyBearerToken sentinel.

api/auth_tokens: add table-driven test that pins the
ENTIRE_AUTH_PROVIDER_VERSION → path mapping (v1/v2/unrecognised/
whitespace) plus an end-to-end ListTokens routing check. The path
switch is the whole point of the version env var; it had no test.

Doc fixes (review found these stale or misleading):
- auth/doc.go: list tokenmanager subpackage (was missing).
- auth/tokenstore/tokenstore.go: drop the "File impl" claim — only
  Keyring ships today.
- auth/tokenstore/keyring.go: collapse the duplicated keyringTokenSet
  comment paragraph, drop the dangling G117 reference.
- cmd/entire/cli/auth/exchange.go: defaultManager rationale corrected
  (sync.Once means later env-var changes are ignored, not honoured);
  TokenForResource doc points at Manager.Token (the rules live there,
  not on TokenForResource).
- cmd/entire/cli/api_client.go: cache-key undercount — list all
  wire-affecting fields rather than just (core-token, resource).
- cmd/entire/cli/search_cmd.go: rewrite the misleading "fall back to
  search.DefaultServiceURL" comment (the fallback is api.BaseURL()).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ions)

Behaviour:
- tokenmanager.DeleteCoreToken now deletes the keyring entry first and
  only clears the in-memory exchange cache on success. Pre-emptively
  clearing would leave a window where the CLI thinks it's logged out
  but the keyring still hands out the core token to the next process.
  Surfaces the store error wrapped as "delete core token: ...".

Coverage:
- tokenmanager: regression tests for the cache-clear (and its
  inverse — cache survives a failed delete), cache-key independence
  for RequestedTokenType and Scope (matching the existing Audience
  test), malformed-JWT fallthrough on the audience shortcut (security
  contract — corrupt cores must not be returned verbatim), and
  surface-don't-collapse for non-ErrNotFound store errors. Adds an
  erroringStore test helper for failure-path tests.
- tokenstore.Keyring: pin the ErrMalformed contract — malformed JSON,
  legacy bare-string entries, and bad expires_at all surface as
  ErrMalformed (wrapped), not ErrNotFound. cmd-side legacy fallback
  depends on this distinction.

Deprecations / docs:
- Mark cmd/entire/cli/auth.Store.SaveToken/GetToken/DeleteToken as
  // Deprecated so godoc and IDE hover steer new callers to the
  tokenstore.Store interface methods. Legacy direct-bearer call sites
  (login, logout, auth status/list/revoke) keep using them; login.go
  carries a //nolint:staticcheck with a pointer to the doc.
- Document keyringService = "entire-cli" as immutable — renaming would
  orphan every existing user's stored credentials.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 8, 2026 08:20
@khaong khaong requested a review from a team as a code owner May 8, 2026 08:20
Comment thread cmd/entire/cli/auth/client.go
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a provider-agnostic, shareable auth library (auth/…) and migrates the CLI to use it, enabling split-host deployments (separate auth issuer and data API origins) via RFC 8693 token exchange.

Changes:

  • Added a new root-level auth library implementing RFC 8628 device flow, RFC 8693 token exchange, token persistence, JWT claim parsing, and a token manager with caching/exchange orchestration.
  • Added split-host configuration (ENTIRE_AUTH_BASE_URL) and updated CLI auth/token storage and auth-management endpoints to route to the auth origin.
  • Updated CLI API client creation and entire search to resolve resource-scoped tokens (including URL-origin normalization) and thread context.Context through authenticated client creation.

Reviewed changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cmd/entire/cli/trail_cmd.go Passes ctx into authenticated API client creation.
cmd/entire/cli/search/search.go Clarifies GitHubToken field meaning for backwards compatibility.
cmd/entire/cli/search_cmd.go Routes search via token manager; defaults service URL to api.BaseURL(); normalizes resource origin.
cmd/entire/cli/search_cmd_test.go Adds coverage for search service URL → origin normalization.
cmd/entire/cli/recap.go Passes ctx into authenticated API client creation.
cmd/entire/cli/logout.go Routes logout revocation calls to auth base URL.
cmd/entire/cli/login.go Uses legacy token save shim; surfaces RFC 8628 error_description on failures.
cmd/entire/cli/integration_test/login_test.go Sets auth base URL + provider version for login integration tests.
cmd/entire/cli/dispatch_wizard.go Passes ctx into authenticated API client creation.
cmd/entire/cli/auth/store.go Wraps shared tokenstore.Keyring while preserving legacy bare-token fallback reads; keys tokens by auth issuer.
cmd/entire/cli/auth/store_test.go Adds tests for legacy bare-string fallback behavior.
cmd/entire/cli/auth/provider.go Introduces provider version switch (v1/v2) including per-provider STS path.
cmd/entire/cli/auth/provider_test.go Tests provider selection and client wiring.
cmd/entire/cli/auth/exchange.go Adds CLI shim over tokenmanager.Manager (singleton + test injection).
cmd/entire/cli/auth/exchange_test.go Tests shim delegation and ErrNotLoggedIn aliasing.
cmd/entire/cli/auth/client.go Replaces bespoke device-flow client logic with shared auth/deviceflow client + compatibility shim types.
cmd/entire/cli/auth/client_test.go Removes tests for old JSON decoding helpers (now in shared oauthhttp helpers).
cmd/entire/cli/auth.go Validates both API and auth origins for HTTPS; routes auth-management commands to auth base URL.
cmd/entire/cli/api/client.go Adds NewClientWithBaseURL; fails early on empty bearer tokens at first request.
cmd/entire/cli/api/client_test.go Adds test ensuring empty bearer tokens are rejected.
cmd/entire/cli/api/base_url.go Adds ENTIRE_AUTH_BASE_URL and AuthBaseURL() fallback to BaseURL().
cmd/entire/cli/api/base_url_test.go Adds tests for AuthBaseURL() fallback/override behavior.
cmd/entire/cli/api/auth_tokens.go Routes auth-token endpoints by provider version; documents v1/v2 differences.
cmd/entire/cli/api/auth_tokens_test.go Adds tests for provider-version routing and v2 path selection.
cmd/entire/cli/api_client.go Reworks authenticated API client creation to use token manager + RFC 8693 exchange when needed.
cmd/entire/cli/activity_cmd.go Passes ctx into authenticated API client creation.
auth/tokenstore/tokenstore.go Defines persistence interface and sentinel errors (ErrNotFound, ErrMalformed).
auth/tokenstore/keyring.go Implements keyring-backed tokenstore.Store with JSON TokenSet encoding and malformed-entry signaling.
auth/tokenstore/keyring_test.go Adds tests for keyring round-trips and malformed-entry behavior.
auth/tokens/tokens.go Introduces TokenSet + unverified JWT claims parsing helpers.
auth/tokens/tokens_test.go Adds tests for expiry helpers and JWT claim parsing.
auth/tokenmanager/tokenmanager.go Adds manager orchestration: core token lookup, same-host / aud shortcuts, RFC 8693 exchange, caching.
auth/tokenmanager/tokenmanager_test.go Adds comprehensive tests for exchange dispatch, caching, shortcuts, and failure modes.
auth/sts/sts.go Adds RFC 8693 token-exchange client.
auth/sts/sts_test.go Adds tests for form construction, error surfacing, and expiry handling.
auth/internal/oauthhttp/jsonresp.go Adds shared JSON response decoding with HTML/non-JSON detection.
auth/internal/oauthhttp/jsonresp_test.go Tests strict/tolerant decoding and HTML detection behavior.
auth/doc.go Documents the new auth library package structure and goals.
auth/deviceflow/deviceflow.go Adds RFC 8628 device-flow client with sentinel error mapping and strict/tolerant decoding rules.
auth/deviceflow/deviceflow_test.go Adds tests for device-flow success/error behaviors, including error_description handling.

Comment thread cmd/entire/cli/auth/store.go Outdated
Comment thread cmd/entire/cli/auth/client.go
Comment thread cmd/entire/cli/api/auth_tokens.go
khaong and others added 2 commits May 8, 2026 18:31
`entire dispatch` was sending the raw core token (audience = auth host)
to the data API and getting back a 401 that cloud.go mapped to "dispatch
requires login — run \`entire login\`" — misleading on split-host
deployments where the user IS logged in but with the wrong-audience
bearer. Same trap search hit before; same fix.

- mode_cloud.go now resolves the bearer via auth.TokenForResource so
  the tokenmanager's same-host shortcut / JWT-aud shortcut / RFC 8693
  exchange all apply. ErrNotLoggedIn is mapped to the friendly
  "dispatch requires login" message; other errors surface verbatim.
- mode_local.go grows a lookupResourceToken seam (defaulted to
  auth.TokenForResource) for test injection; the existing
  lookupCurrentToken seam is retained for back-compat with tests that
  haven't migrated.
- Test stubs (stubCloudDispatchAuth + per-test cleanups) updated to
  swap both seams so the assertions still cover what they used to.

Sweep confirmed no other data-API caller bypasses the manager: search,
trail, recap, dispatch_wizard, and activity all flow through
NewAuthenticatedAPIClient → tokenmanager. Auth-host commands
(auth list/revoke/status, logout) correctly retain LookupCurrentToken
since they need the auth-audience bearer.

Docs:
- New CLAUDE.md "Auth and token resolution" section flags the two
  blessed entry points (NewAuthenticatedAPIClient, TokenForResource),
  the resolution rules, and that LookupCurrentToken is for auth-host
  callers only.
- New auth/README.md positions the library as shareable across
  internal CLIs: subpackage map, embedding checklist, design
  principles (no globals, no env-var reads, provider-agnostic),
  non-goals (OIDC discovery, server-side, code-flow PKCE), quick-
  start snippets for login / data-API call / logout.
- search.Config.GitHubToken doc now points at TokenForResource (was
  LookupCurrentToken).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PollDeviceAuth: unknown OAuth error codes (invalid_request,
  invalid_client, server_error, unsupported_grant_type, etc.) used to
  fall through to login.go's transient-retry path, burning ~25-150s on
  permanent server failures before producing a confusing "after N
  consecutive failures" message. Replace oauthErrorCode +
  descriptionFromSentinel with a single oauthErrorParts that also
  matches deviceflow's generic "oauth error: <code>" wrapper. Unknown
  codes now land in DeviceAuthPoll.Error so the polling loop's default
  switch arm fails fast with "device authorization failed: <code>".
  Tests cover known sentinels, sentinel-with-description, unknown-
  passthrough, unknown-with-description, and non-OAuth (transient)
  errors.
- Store.GetToken: doc said "only ErrNotFound and ErrMalformed trigger
  the fallback" but ErrNotFound short-circuits to the empty-string
  return without a keyring read; only ErrMalformed actually triggers
  the bare-string fallback. Fixed the doc to match.
- api.RevokeCurrentToken: dropped the stale comment about v2 not
  exposing /current — server-side fix is incoming.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaong
Copy link
Copy Markdown
Contributor Author

khaong commented May 8, 2026

bugbot run

Comment thread auth/deviceflow/deviceflow_test.go
Comment thread auth/tokenmanager/tokenmanager.go
Two cursor bug-bot findings, both Low severity but worth closing.

- auth/deviceflow + auth/sts: TestPollDeviceAuth_Success and
  TestExchange_Success call freezeClock, which mutates the package-
  level nowFunc. Both tests were marked t.Parallel(), creating a
  latent race against any future parallel test that reads nowFunc
  through a real Exchange/PollDeviceAuth call. Drop the t.Parallel()
  on those two tests with a comment explaining why; the rest of the
  package keeps parallelism. -race confirms no race remains.
- auth/tokenmanager: cacheKey was a delimiter-joined string, structurally
  vulnerable to collisions if any field embedded the "|" separator
  (none do today, but no guarantee for future callers). Replace with a
  struct map key — Go's map can use comparable structs directly, so
  there's no string encoding to misbehave.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaong
Copy link
Copy Markdown
Contributor Author

khaong commented May 8, 2026

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 9c2b070. Configure here.

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