Skip to content

Commit 5f3a54a

Browse files
committed
Redact signup secrets from human output
1 parent c5e95ee commit 5f3a54a

2 files changed

Lines changed: 103 additions & 3 deletions

File tree

internal/commands/signup.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ func runSignupStart(cmd *cobra.Command, args []string) error {
400400
data["code"] = code
401401
}
402402

403-
printMutation(data, "Magic link sent. Check your email for a 6-digit code.", breadcrumbs)
403+
printMutation(signupHumanData(data), "Magic link sent. Check your email for a 6-digit code.", breadcrumbs)
404404
return nil
405405
}
406406

@@ -445,7 +445,7 @@ func runSignupVerify(cmd *cobra.Command, args []string) error {
445445
}
446446
}
447447

448-
printDetail(result, "", nil)
448+
printDetail(signupHumanData(result, "requires_signup_completion", "accounts"), "", nil)
449449
return nil
450450
}
451451

@@ -551,10 +551,26 @@ func runSignupComplete(cmd *cobra.Command, args []string) error {
551551
breadcrumb("setup", "fizzy setup", "Full interactive setup"),
552552
}
553553

554-
printMutation(result, summary, breadcrumbs)
554+
printMutation(signupHumanData(result, "account", "is_new_user"), summary, breadcrumbs)
555555
return nil
556556
}
557557

558+
// signupHumanData removes machine-only secrets from human-facing output while
559+
// preserving the full payload for JSON/quiet/agent workflows.
560+
func signupHumanData(data map[string]any, fields ...string) map[string]any {
561+
if !isHumanOutput() {
562+
return data
563+
}
564+
565+
result := make(map[string]any, len(fields))
566+
for _, field := range fields {
567+
if value, ok := data[field]; ok {
568+
result[field] = value
569+
}
570+
}
571+
return result
572+
}
573+
558574
// signupAPIURL returns the effective API URL for signup commands, normalized without trailing slash.
559575
func signupAPIURL() string {
560576
if cfg != nil && cfg.APIURL != "" {

internal/commands/signup_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"testing"
1111

12+
"github.com/basecamp/cli/output"
1213
"github.com/basecamp/fizzy-cli/internal/config"
1314
"gopkg.in/yaml.v3"
1415
)
@@ -754,6 +755,89 @@ func TestSignupCompleteRejectsEmptyToken(t *testing.T) {
754755
})
755756
}
756757

758+
func TestSignupHumanOutputRedactsSecrets(t *testing.T) {
759+
t.Run("signup start hides pending token in styled output", func(t *testing.T) {
760+
resetSignupFlags()
761+
server := newTestSignupServer(t, testSignupServerOpts{accessToken: "fizzy_test"})
762+
defer server.Close()
763+
764+
mock := NewMockClient()
765+
SetTestMode(mock)
766+
SetTestFormat(output.FormatStyled)
767+
SetTestConfig("", "", server.URL)
768+
defer ResetTestMode()
769+
770+
signupStartCmd.Flags().Set("email", "test@example.com")
771+
err := signupStartCmd.RunE(signupStartCmd, []string{})
772+
assertExitCode(t, err, 0)
773+
774+
raw := TestOutput()
775+
if strings.Contains(raw, "signed-pending-token-value") {
776+
t.Fatalf("expected styled output to hide pending token, got:\n%s", raw)
777+
}
778+
})
779+
780+
t.Run("signup verify hides session token in styled output", func(t *testing.T) {
781+
resetSignupFlags()
782+
server := newTestSignupServer(t, testSignupServerOpts{
783+
requiresCompletion: false,
784+
accessToken: "fizzy_test",
785+
})
786+
defer server.Close()
787+
788+
mock := NewMockClient()
789+
SetTestMode(mock)
790+
SetTestFormat(output.FormatStyled)
791+
SetTestConfig("", "", server.URL)
792+
defer ResetTestMode()
793+
794+
signupVerifyCmd.Flags().Set("code", "VALID1")
795+
signupVerifyCmd.Flags().Set("pending-token", "signed-pending-token-value")
796+
err := signupVerifyCmd.RunE(signupVerifyCmd, []string{})
797+
assertExitCode(t, err, 0)
798+
799+
raw := TestOutput()
800+
if strings.Contains(raw, "signed-session-token-value") {
801+
t.Fatalf("expected styled output to hide session token, got:\n%s", raw)
802+
}
803+
if !strings.Contains(raw, "requires_signup_completion") {
804+
t.Fatalf("expected styled output to retain non-secret fields, got:\n%s", raw)
805+
}
806+
})
807+
808+
t.Run("signup complete hides access token in styled output", func(t *testing.T) {
809+
resetSignupFlags()
810+
server := newTestSignupServer(t, testSignupServerOpts{
811+
accessToken: "fizzy_generated_token",
812+
})
813+
defer server.Close()
814+
815+
config.SetTestConfigDir(t.TempDir())
816+
defer config.ResetTestConfigDir()
817+
818+
mock := NewMockClient()
819+
SetTestMode(mock)
820+
SetTestFormat(output.FormatStyled)
821+
SetTestConfig("", "", server.URL)
822+
defer ResetTestMode()
823+
824+
restoreStdin := pipeSessionToken("signed-session-token-value")
825+
defer restoreStdin()
826+
827+
signupCompleteCmd.Flags().Set("account", "123456")
828+
err := signupCompleteCmd.RunE(signupCompleteCmd, []string{})
829+
assertExitCode(t, err, 0)
830+
831+
raw := TestOutput()
832+
if strings.Contains(raw, "fizzy_generated_token") {
833+
t.Fatalf("expected styled output to hide generated token, got:\n%s", raw)
834+
}
835+
if !strings.Contains(raw, "123456") {
836+
t.Fatalf("expected styled output to retain account, got:\n%s", raw)
837+
}
838+
})
839+
}
840+
757841
func TestReadSessionTokenFromStdin(t *testing.T) {
758842
t.Run("reads token from piped stdin", func(t *testing.T) {
759843
restoreStdin := pipeSessionToken("eyJ-session-token-value")

0 commit comments

Comments
 (0)