Two halves of v0.3 roadmap item 3-2.
(1) `weknora auth refresh` — explicit token renewal:
Reads the stored refresh_token, spends it via POST /api/v1/auth/refresh
(OAuth refresh-token grant), and persists both new tokens. API-key
contexts rejected with input.invalid_argument (no refresh semantic).
NOTE: gh CLI has `gh auth refresh` but with different semantics —
gh's variant is an OAuth scope expansion / re-prompt via the browser
(verified against the gh manual). The two share a name but solve
different problems; there's no direct gh parallel for refresh-token
grant because gh's PAT/OAuth-app model doesn't expose a short-lived
access_token + refresh_token pair to clients.
Error mapping:
- no current context → auth.unauthenticated
- --name unknown → local.context_not_found
- missing refresh in keyring → auth.token_expired (hint: re-login)
- server Success=false → auth.token_expired
- network → network.error
Envelope omits the token values (would leak into agent transcripts).
(2) AuthRetryTransport — transparent retry:
Wraps the SDK http.Client. On a 401 from a non-/auth/* endpoint:
- JWT context: read refresh token, hit /auth/refresh, persist new pair,
replay original request with new bearer.
- API-key context: pass through (no refresh semantic).
- Non-replayable body (req.GetBody == nil): pass through.
- /auth/login or /auth/refresh: pass through (no recursion).
Concurrent 401s are singleflight-coalesced via sync.Mutex — 5 parallel
calls trigger exactly 1 refresh.
SDK additions (additive, non-breaking):
- WithTransport(rt http.RoundTripper) ClientOption.
- PathAuthLogin / PathAuthRefresh constants (cli/internal/cmdutil/authretry
imports them so the CLI and SDK can't drift on path strings).
Refactor surfaced by the post-commit reviewer round:
- cmdutil.RefreshAndPersist(ctx, store, refresher, ctxName) — the
load-refresh → call-SDK → persist-pair sequence was duplicated between
the standalone `auth refresh` and the transport's refresh closure;
collapsed to one canonical implementation.
- refreshFn signature takes context.Context so Ctrl+C during a
transparent refresh cancels.
- AuthRetryTransport.CurrentToken() removed — never called.
8 + 8 + 8 unit tests cover happy path / refresh-fail / auth-endpoint
skip / api-key passthrough / singleflight under concurrency / non-
replayable-body fallback.
Roadmap: 3-2.
Five additive SDK changes that PR-3 (CLI Factory adapter) and PR-4
(auth login / status) consume.
WithToken split (Breaking + alias):
- WithAPIKey(key) sets X-API-Key header (long-lived API keys)
- WithBearerToken(token) sets Authorization: Bearer header (JWTs)
- WithToken kept as deprecated alias for WithAPIKey for two minor versions
- doRequest now sets X-API-Key and Authorization headers independently;
callers can carry both kinds of credentials simultaneously
applyAuthHeaders extraction:
- Header injection (X-API-Key, Authorization, X-Request-ID, X-Tenant-ID)
is now a Client method shared between doRequest and the multipart path
in CreateKnowledgeFromFile. Eliminates ~30 lines of drift risk and
fixes the latent missing X-Tenant-ID on file uploads.
- Drops the dead second tenant-header switch in doRequest.
Raw escape hatch:
- Client.Raw(ctx, method, path, body) is the public passthrough used by
the future \`weknora api\` command. godoc marks it experimental.
Auth methods (auth.go, new):
- Login(ctx, LoginRequest) -> POST /auth/login (existing server endpoint)
- GetCurrentUser(ctx) -> GET /auth/me, returns *CurrentUserResponse
- RefreshToken(ctx, refreshToken) -> POST /auth/refresh
- Types: LoginRequest, LoginResponse, AuthUser, AuthTenant,
CurrentUserResponse, RefreshTokenResponse
No server changes. The auth methods consume endpoints that already exist;
PR-3/PR-4 wire them through the CLI.