fix(cli): send api-keys via X-API-Key in client.New + help surface
passmgr.GetToken returns an api-key in padded-cell mode (provisioned by
scripts/provision-hf-accounts.sh via 'hf user reset-apikey'), but every
call site funneled that through client.New which sent it as a
'Authorization: Bearer <hex>'. The HF backend's HTTPBearer middleware
expects JWT shape there and rejects hex strings as 'Could not validate
credentials'. The d2b83ad backend fix added a Bearer-fallback that tries
the value as an api-key, which masked the issue against current prod;
older backends or any future change in that fallback still 401.
Two changes:
- client.New auto-detects shape: 'eyJ'-prefix + two dots == JWT (Bearer),
anything else == api-key (X-API-Key). Empty token sets neither header.
- internal/help/surface.go's loadPermissionState (called by hf --help
introspection) switches to client.NewWithAPIKey explicitly so the
command-discovery path doesn't depend on the heuristic at all. When
that path failed silently (Known:false), agents would see only the
always-permitted commands ('user.*', 'agent.status', 'config',
'health', 'version') and conclude they had no project permission.
Adds internal/client/client_test.go covering both header paths plus
empty-token, isLikelyJWT cases, and NewWithAPIKey precedence.
Verified end-to-end in sim against a rebuilt hf-backend matching prod
(commit d2b83ad): cli with --token <api-key> sends X-Api-Key header,
backend returns 200 on /projects + /auth/me/permissions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,14 +19,34 @@ type Client struct {
|
||||
}
|
||||
|
||||
// New creates a Client with the given base URL and optional auth token.
|
||||
//
|
||||
// The token is sent as Authorization: Bearer when it looks like a JWT
|
||||
// (eyJ-prefixed, three dot-separated segments). Anything else is treated as
|
||||
// an API key and sent via X-API-Key. This lets call sites that historically
|
||||
// passed an api-key as a "token" (e.g. the value returned by passmgr.GetToken
|
||||
// in padded-cell mode) authenticate correctly without per-callsite churn.
|
||||
func New(baseURL, token string) *Client {
|
||||
return &Client{
|
||||
c := &Client{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
Token: token,
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
if isLikelyJWT(token) {
|
||||
c.Token = token
|
||||
} else if token != "" {
|
||||
c.APIKey = token
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// isLikelyJWT returns true for tokens that look like a JSON Web Token:
|
||||
// "eyJ"-prefixed (base64-encoded JSON header opening with `{"`) and exactly
|
||||
// two dots separating header.payload.signature. API keys minted by the HF
|
||||
// backend are hex (`/users/{id}/apikey` returns a 64-hex-char `key`); fabric
|
||||
// keys use a `fak_` prefix. None of those match this shape.
|
||||
func isLikelyJWT(token string) bool {
|
||||
return strings.HasPrefix(token, "eyJ") && strings.Count(token, ".") == 2
|
||||
}
|
||||
|
||||
// NewWithAPIKey creates a Client that authenticates using X-API-Key.
|
||||
|
||||
Reference in New Issue
Block a user