Files
WeKnora/cli/cmd/auth/auth.go
T
nullkey 4c26bc9ecc feat(cli): auth refresh + transparent 401 retry transport
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.
2026-05-14 10:57:17 +08:00

32 lines
1.0 KiB
Go

// Package auth holds the cobra commands for authentication
// (login / logout / list / refresh / status).
package auth
import (
"github.com/spf13/cobra"
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
)
// NewCmdAuth builds the `weknora auth` command tree and registers its
// subcommands. Called from cli/cmd/root.go.
func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Manage authentication credentials and contexts",
// NoArgs makes cobra emit its canonical `unknown command "X" for
// "weknora auth"` for any positional, which mapCobraError tags as
// FlagError → exit 2. Run (not RunE) is required: a parent with
// neither Run nor RunE short-circuits to help and skips Args
// validation entirely.
Args: cobra.NoArgs,
Run: func(c *cobra.Command, _ []string) { _ = c.Help() },
}
cmd.AddCommand(NewCmdLogin(f, nil))
cmd.AddCommand(NewCmdLogout(f))
cmd.AddCommand(NewCmdList(f))
cmd.AddCommand(NewCmdRefresh(f))
cmd.AddCommand(NewCmdStatus(f))
return cmd
}