diff --git a/internal/cli/auth.go b/internal/cli/auth.go
index 46bae71..8e8863d 100644
--- a/internal/cli/auth.go
+++ b/internal/cli/auth.go
@@ -53,7 +53,9 @@ func (a *App) login(noBrowser bool, region string, progress progressEmitter) (ma
}
timeout = time.Duration(parsed) * time.Millisecond
}
- callback, err := waitForOAuthCallback(state, timeout)
+ callback, err := waitForOAuthCallbackWithPage(state, timeout, callbackPageConfig{
+ Region: loginRegion,
+ })
if err != nil {
return nil, err
}
@@ -284,6 +286,10 @@ type callbackPayload struct {
}
func waitForOAuthCallback(expectedState string, timeout time.Duration) (*callbackServer, error) {
+ return waitForOAuthCallbackWithPage(expectedState, timeout, callbackPageConfig{})
+}
+
+func waitForOAuthCallbackWithPage(expectedState string, timeout time.Duration, page callbackPageConfig) (*callbackServer, error) {
wait := make(chan callbackPayload, 1)
errs := make(chan error, 1)
mux := http.NewServeMux()
@@ -313,18 +319,22 @@ func waitForOAuthCallback(expectedState string, timeout time.Duration) (*callbac
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
oauthErr := r.URL.Query().Get("error")
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
switch {
case oauthErr != "":
- http.Error(w, "Agora CLI login failed. Return to the terminal for details.", http.StatusBadRequest)
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = io.WriteString(w, renderOAuthCallbackErrorPage(page, "Agora CLI login failed", "Return to the terminal for details."))
errs <- fmt.Errorf("OAuth authorization failed: %s", oauthErr)
case code == "" || state == "":
- http.Error(w, "Agora CLI login callback was missing required fields.", http.StatusBadRequest)
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = io.WriteString(w, renderOAuthCallbackErrorPage(page, "Missing callback fields", "The OAuth callback did not include the required code and state fields."))
case state != expectedState:
- http.Error(w, "Agora CLI login state mismatch.", http.StatusBadRequest)
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = io.WriteString(w, renderOAuthCallbackErrorPage(page, "Login state mismatch", "Return to the terminal and restart login to protect this session."))
errs <- errors.New("OAuth state mismatch.")
default:
w.WriteHeader(http.StatusOK)
- _, _ = io.WriteString(w, `
Agora CLI Login CompleteAgora CLI login complete
You can close this browser window and return to your terminal.
`)
+ _, _ = io.WriteString(w, renderOAuthCallbackSuccessPage(page))
wait <- callbackPayload{Code: code, State: state}
}
})
diff --git a/internal/cli/oauth_pages.go b/internal/cli/oauth_pages.go
new file mode 100644
index 0000000..e60ea3d
--- /dev/null
+++ b/internal/cli/oauth_pages.go
@@ -0,0 +1,157 @@
+package cli
+
+import (
+ "html"
+ "strings"
+)
+
+// callbackPageConfig carries the login region used to brand the browser callback page.
+type callbackPageConfig struct {
+ Region string
+}
+
+// loginPageContent contains the region-specific copy rendered on the OAuth callback page.
+type loginPageContent struct {
+ PageClass string
+ Title string
+ Message string
+ ActionLabel string
+ PrimaryAction string
+ Safety string
+}
+
+// renderOAuthCallbackSuccessPage renders the browser page shown after a successful OAuth callback.
+func renderOAuthCallbackSuccessPage(config callbackPageConfig) string {
+ content := loginPageContentForRegion(config.Region)
+ return renderOAuthCallbackPage(content, false, "", "")
+}
+
+// renderOAuthCallbackErrorPage renders a branded browser page for OAuth callback errors.
+func renderOAuthCallbackErrorPage(config callbackPageConfig, title, message string) string {
+ content := loginPageContentForRegion(config.Region)
+ return renderOAuthCallbackPage(content, true, title, message)
+}
+
+// loginPageContentForRegion returns the final login-page copy for the active control-plane region.
+func loginPageContentForRegion(region string) loginPageContent {
+ if normalizeContextRegion(region) == regionCN {
+ return loginPageContent{
+ PageClass: "shengwang-page",
+ Title: "你已成功登录声网 CLI",
+ Message: "此浏览器步骤已完成。CLI 现在可以将此账号作为当前本地配置继续使用。",
+ ActionLabel: "回到终端后确认当前登录状态。",
+ PrimaryAction: "agora auth status",
+ Safety: "你可以关闭此页面,回到终端继续操作。",
+ }
+ }
+
+ return loginPageContent{
+ PageClass: "agora-page",
+ Title: "You are now authenticated with Agora CLI",
+ Message: "This browser step completed successfully. The CLI can now use this account as your active local configuration.",
+ ActionLabel: "Return to your terminal and confirm the active account.",
+ PrimaryAction: "agora auth status",
+ Safety: "You can close this window and return to your terminal.",
+ }
+}
+
+// renderOAuthCallbackPage builds the complete OAuth callback HTML document.
+func renderOAuthCallbackPage(content loginPageContent, isError bool, errorTitle, errorMessage string) string {
+ title := content.Title
+ message := content.Message
+ if isError {
+ title = valueOrDefault(errorTitle, "Login could not be completed")
+ message = valueOrDefault(errorMessage, "Return to your terminal for details.")
+ }
+
+ var b strings.Builder
+ b.WriteString(`Agora CLI Login`)
+ b.WriteString(brandLogoHTML(content.PageClass))
+ b.WriteString(`CLI
`)
+ b.WriteString(escapeText(title))
+ b.WriteString(`
`)
+ b.WriteString(escapeText(message))
+ b.WriteString(`
`)
+ if !isError {
+ b.WriteString(``)
+ b.WriteString(escapeText(content.ActionLabel))
+ b.WriteString(`
`)
+ b.WriteString(escapeText(content.PrimaryAction))
+ b.WriteString(` `)
+ }
+ b.WriteString(``)
+ b.WriteString(escapeText(content.Safety))
+ b.WriteString(`
`)
+
+ return b.String()
+}
+
+// loginPageCSS returns shared page CSS plus the theme for the active login region.
+func loginPageCSS(pageClass string) string {
+ var b strings.Builder
+ b.WriteString(`
+*{box-sizing:border-box}
+body{margin:0;min-height:100vh;font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;-webkit-font-smoothing:antialiased}
+.page{min-height:100vh;display:grid;place-items:center;padding:64px 24px;color:var(--ink);background:var(--bg)}
+.card{width:min(760px,100%);padding:40px;border:1px solid var(--line);border-radius:20px;background:var(--panel);box-shadow:var(--shadow)}
+.brand{display:inline-flex;align-items:center;gap:10px;color:var(--ink);font-size:22px;font-weight:800;letter-spacing:-.01em}
+.brand-logo{display:inline-flex;align-items:center;flex:0 0 auto}
+.brand-logo svg{display:block;width:100%;height:100%}
+.brand-logo img{display:block;width:100%;height:auto}
+.brand-logo-agora{width:82px;max-height:32px}
+.hero{margin-top:36px}
+h1{margin:0;max-width:700px;font-size:clamp(26px,3vw,31px);line-height:1.22;letter-spacing:-.03em}
+.message{max-width:680px;margin:18px 0 0;color:var(--muted);font-size:16px;line-height:1.65}
+.next{display:grid;grid-template-columns:1fr auto;gap:18px;align-items:center;margin-top:28px;padding:14px 16px;border:1px solid var(--next-line);border-radius:12px;background:var(--next-bg)}
+.next p{margin:0;color:var(--next-text);font-size:14px;font-weight:600}
+code{padding:5px 9px;border:1px solid var(--code-line);border-radius:8px;background:var(--code-bg);color:var(--primary);font:700 14px ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap}
+.safety{margin:20px 0 0;color:var(--muted);font-size:13px;line-height:1.6}
+.is-error .next{display:none}
+@media (max-width:900px){
+ .page{padding:32px 18px}
+ .card{padding:28px}
+ .next{grid-template-columns:1fr}
+ code{white-space:normal;word-break:break-word}
+}
+`)
+
+ if pageClass == "shengwang-page" {
+ b.WriteString(`
+.shengwang-page{--ink:#152033;--muted:#617083;--primary:#0b9dfd;--brand:#0b9dfd;--line:#dce7f5;--panel:#fff;--shadow:0 20px 55px rgba(21,45,85,.1);--next-line:#dce7f5;--next-bg:#f7fbff;--next-text:#46576c;--code-line:#cfe0f8;--code-bg:#eef6ff;--bg:linear-gradient(180deg,#f6faff 0%,#fff 58%,#f9fbfd 100%)}
+.brand-logo-cn{width:58px;height:auto;color:var(--brand)}
+.brand-logo-cn svg{width:58px;height:auto}
+`)
+ return b.String()
+ }
+
+ b.WriteString(`
+.agora-page{--ink:#172033;--muted:#647084;--primary:#09976f;--line:#dfe5ee;--panel:#fff;--shadow:0 20px 55px rgba(22,33,55,.1);--next-line:#dfe5ee;--next-bg:#f8fafc;--next-text:#4c596c;--code-line:#d8e2ee;--code-bg:#f3f6fa;--bg:linear-gradient(180deg,#f8fafc 0%,#fff 58%,#f6f8fb 100%)}
+`)
+ return b.String()
+}
+
+// brandLogoHTML returns the official region-specific brand mark used in the callback page.
+func brandLogoHTML(pageClass string) string {
+ if pageClass == "shengwang-page" {
+ return ``
+ }
+
+ return `
`
+}
+
+// escapeText escapes user-visible text before it is inserted into HTML.
+func escapeText(value string) string {
+ return html.EscapeString(value)
+}
+
+// escapeAttr escapes attribute values before they are inserted into HTML.
+func escapeAttr(value string) string {
+ return html.EscapeString(value)
+}
diff --git a/internal/cli/oauth_pages_test.go b/internal/cli/oauth_pages_test.go
new file mode 100644
index 0000000..ec7eab8
--- /dev/null
+++ b/internal/cli/oauth_pages_test.go
@@ -0,0 +1,90 @@
+package cli
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestOAuthCallbackSuccessPageUsesFinalGlobalDesign(t *testing.T) {
+ html := renderOAuthCallbackSuccessPage(callbackPageConfig{Region: regionGlobal})
+
+ for _, want := range []string{
+ "agora-page",
+ "Agora%20Logo%20Crisp.webp",
+ "You are now authenticated with Agora CLI",
+ "This browser step completed successfully. The CLI can now use this account as your active local configuration.",
+ "agora auth status",
+ "You can close this window and return to your terminal.",
+ } {
+ if !strings.Contains(html, want) {
+ t.Fatalf("expected global login page to contain %q", want)
+ }
+ }
+
+ for _, forbidden := range []string{"agora whoami", "global-minimal", "global-deck", "shengwang", "声网", "brand-logo-cn"} {
+ if strings.Contains(html, forbidden) {
+ t.Fatalf("global login page should not contain legacy variant content %q", forbidden)
+ }
+ }
+}
+
+func TestOAuthCallbackSuccessPageDefaultsToGlobalBranding(t *testing.T) {
+ html := renderOAuthCallbackSuccessPage(callbackPageConfig{Region: "test"})
+
+ for _, want := range []string{
+ "agora-page",
+ "Agora%20Logo%20Crisp.webp",
+ "You are now authenticated with Agora CLI",
+ } {
+ if !strings.Contains(html, want) {
+ t.Fatalf("expected fallback login page to contain %q", want)
+ }
+ }
+
+ for _, forbidden := range []string{"shengwang", "声网", "brand-logo-cn"} {
+ if strings.Contains(html, forbidden) {
+ t.Fatalf("fallback login page should not contain China branding %q", forbidden)
+ }
+ }
+}
+
+func TestOAuthCallbackSuccessPageUsesFinalChinaDesign(t *testing.T) {
+ html := renderOAuthCallbackSuccessPage(callbackPageConfig{Region: regionCN})
+
+ for _, want := range []string{
+ "shengwang-page",
+ "brand-logo-cn",
+ "你已成功登录声网 CLI",
+ "此浏览器步骤已完成。CLI 现在可以将此账号作为当前本地配置继续使用。",
+ "agora auth status",
+ "你可以关闭此页面,回到终端继续操作。",
+ } {
+ if !strings.Contains(html, want) {
+ t.Fatalf("expected China login page to contain %q", want)
+ }
+ }
+
+ for _, forbidden := range []string{"agora whoami", "cn-console", "cn-silk"} {
+ if strings.Contains(html, forbidden) {
+ t.Fatalf("China login page should not contain legacy variant content %q", forbidden)
+ }
+ }
+}
+
+func TestOAuthCallbackErrorPageKeepsRegionBranding(t *testing.T) {
+ html := renderOAuthCallbackErrorPage(callbackPageConfig{Region: regionCN}, "Login state mismatch", "Return to the terminal and restart login.")
+
+ for _, want := range []string{
+ "shengwang-page is-error",
+ "Login state mismatch",
+ "Return to the terminal and restart login.",
+ } {
+ if !strings.Contains(html, want) {
+ t.Fatalf("expected error login page to contain %q", want)
+ }
+ }
+
+ if strings.Contains(html, "agora auth status") {
+ t.Fatal("error login page should not show success action command")
+ }
+}