[object Object]

← back to Cli Printing Press

fix(cli): kind-aware auth env-vars and promoted-command --limit (#645)

0cf04f73a03c16f87ca3c504bce7cceb3d8ed0e3 · 2026-05-05 23:11:12 -0700 · Trevin Chow

* fix(cli): stop treating OAuth setup inputs as request credentials

The generator treated all auth env vars equally, so Client ID and
Client Secret (auth_flow_input) and harvested session cookies flowed
into bearer-header fallbacks, the verifier's --api-key fan-out, and
auth-status hint output as if they were per-call credentials.

Add IsRequestCredential() / EffectiveKind() on AuthEnvVar and thread
the predicate through:

- spec: CanonicalEnvVar, IsAuthEnvVarORCase, independentAuthORGroupsExample
- pipeline/runtime: --api-key fan-out and mock-token injection use only
  request-credential names; summarizeAuthEnvVars classifies by predicate
- templates/config.go.tmpl: drop the canonical-env-var-as-Bearer fallback
  under client_credentials (Client ID is not a usable bearer)
- templates/auth_simple.go.tmpl: status hints suggest only request creds
- templates/client.go.tmpl: under verify mode, plant a mock access token
  before attempting to mint via the unreachable token endpoint

Goldens regenerated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): apply --limit truncation to promoted commands

Promoted commands lacked the truncateJSONArray block that endpoint
commands already had, so calling the typed subcommand `app endpoint-list
--limit 10` truncated client-side, but the promoted alias `app list
--limit 10` returned every row.

Add the same block in command_promoted.go.tmpl. Shrink the verbose
explanatory comment in command_endpoint.go.tmpl to one line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 0cf04f73a03c16f87ca3c504bce7cceb3d8ed0e3
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Tue May 5 23:11:12 2026 -0700

    fix(cli): kind-aware auth env-vars and promoted-command --limit (#645)
    
    * fix(cli): stop treating OAuth setup inputs as request credentials
    
    The generator treated all auth env vars equally, so Client ID and
    Client Secret (auth_flow_input) and harvested session cookies flowed
    into bearer-header fallbacks, the verifier's --api-key fan-out, and
    auth-status hint output as if they were per-call credentials.
    
    Add IsRequestCredential() / EffectiveKind() on AuthEnvVar and thread
    the predicate through:
    
    - spec: CanonicalEnvVar, IsAuthEnvVarORCase, independentAuthORGroupsExample
    - pipeline/runtime: --api-key fan-out and mock-token injection use only
      request-credential names; summarizeAuthEnvVars classifies by predicate
    - templates/config.go.tmpl: drop the canonical-env-var-as-Bearer fallback
      under client_credentials (Client ID is not a usable bearer)
    - templates/auth_simple.go.tmpl: status hints suggest only request creds
    - templates/client.go.tmpl: under verify mode, plant a mock access token
      before attempting to mint via the unreachable token endpoint
    
    Goldens regenerated.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * fix(cli): apply --limit truncation to promoted commands
    
    Promoted commands lacked the truncateJSONArray block that endpoint
    commands already had, so calling the typed subcommand `app endpoint-list
    --limit 10` truncated client-side, but the promoted alias `app list
    --limit 10` returned every row.
    
    Add the same block in command_promoted.go.tmpl. Shrink the verbose
    explanatory comment in command_endpoint.go.tmpl to one line.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 internal/generator/auth_env_precedence_test.go     | 37 ++++++++++++-------
 internal/generator/generator.go                    |  5 +++
 internal/generator/limit_truncation_test.go        | 40 ++++++++++++++++++++
 internal/generator/templates/auth_simple.go.tmpl   |  8 ++++
 internal/generator/templates/client.go.tmpl        |  4 ++
 .../generator/templates/command_endpoint.go.tmpl   |  7 +---
 .../generator/templates/command_promoted.go.tmpl   |  4 ++
 internal/generator/templates/config.go.tmpl        | 14 -------
 internal/generator/templates_test.go               | 36 ++++++++++++++++++
 internal/pipeline/runtime.go                       | 43 ++++++++++++++--------
 internal/pipeline/runtime_test.go                  | 20 +++++++++-
 internal/spec/spec.go                              | 19 ++++++++--
 .../internal/client/client.go                      |  4 ++
 .../internal/config/config.go                      |  4 --
 .../printing-press-rich-auth/internal/cli/auth.go  |  3 --
 15 files changed, 187 insertions(+), 61 deletions(-)

diff --git a/internal/generator/auth_env_precedence_test.go b/internal/generator/auth_env_precedence_test.go
index 381fba78..2b8921d6 100644
--- a/internal/generator/auth_env_precedence_test.go
+++ b/internal/generator/auth_env_precedence_test.go
@@ -11,17 +11,20 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TestAuthHeader_ClientCredentialsAccessTokenWinsOverEnv pins that under
-// OAuth2 client_credentials the cached AccessToken wins over the env-var
-// fallback. The env var is the Client ID, not a usable bearer JWT.
-func TestAuthHeader_ClientCredentialsAccessTokenWinsOverEnv(t *testing.T) {
+// TestAuthHeader_ClientCredentialsDoesNotUseSetupEnvVars pins that under
+// OAuth2 client_credentials the setup inputs are never emitted as bearer
+// headers. Only a minted AccessToken is usable for API requests.
+func TestAuthHeader_ClientCredentialsDoesNotUseSetupEnvVars(t *testing.T) {
 	t.Parallel()
 
 	apiSpec := minimalSpec("cc-precedence")
 	apiSpec.Auth = spec.AuthConfig{
-		Type:        "bearer_token",
-		Header:      "Authorization",
-		EnvVars:     []string{"CC_AUTH_TEST_CLIENT_ID"},
+		Type:   "bearer_token",
+		Header: "Authorization",
+		EnvVarSpecs: []spec.AuthEnvVar{
+			{Name: "CC_AUTH_TEST_CLIENT_ID", Kind: spec.AuthEnvVarKindAuthFlowInput, Required: false, Sensitive: false},
+			{Name: "CC_AUTH_TEST_CLIENT_SECRET", Kind: spec.AuthEnvVarKindAuthFlowInput, Required: false, Sensitive: true},
+		},
 		OAuth2Grant: spec.OAuth2GrantClientCredentials,
 		TokenURL:    "https://example.com/token",
 	}
@@ -34,18 +37,24 @@ func TestAuthHeader_ClientCredentialsAccessTokenWinsOverEnv(t *testing.T) {
 	content := string(cfgSrc)
 
 	envCheck := "if c." + resolveEnvVarField("CC_AUTH_TEST_CLIENT_ID") + ` != ""`
+	envSecretCheck := "if c." + resolveEnvVarField("CC_AUTH_TEST_CLIENT_SECRET") + ` != ""`
 	tokenCheck := `if c.AccessToken != ""`
 
-	require.Contains(t, content, envCheck, "AuthHeader must keep the env-var fallback")
 	require.Contains(t, content, tokenCheck, "AuthHeader must check AccessToken")
 
 	body := authHeaderBody(t, content)
-	envIdx := strings.Index(body, envCheck)
-	tokenIdx := strings.Index(body, tokenCheck)
-	require.NotEqual(t, -1, envIdx)
-	require.NotEqual(t, -1, tokenIdx)
-	assert.Less(t, tokenIdx, envIdx,
-		"AccessToken check must appear BEFORE env-var fallback under OAuth2 client_credentials")
+	require.Contains(t, body, tokenCheck)
+	require.NotContains(t, body, envCheck, "client ID must not be used as a bearer token")
+	require.NotContains(t, body, envSecretCheck, "client secret must not be used as a bearer token")
+
+	clientSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
+	require.NoError(t, err)
+	clientContent := string(clientSrc)
+	verifyIdx := strings.Index(clientContent, `cliutil.IsVerifyEnv()`)
+	mintIdx := strings.Index(clientContent, `c.mintClientCredentials(clientID, clientSecret)`)
+	require.NotEqual(t, -1, verifyIdx, "mock verification should short-circuit before token minting")
+	require.NotEqual(t, -1, mintIdx, "client_credentials mint path should still be emitted")
+	assert.Less(t, verifyIdx, mintIdx, "mock verification must not dial the real token endpoint")
 }
 
 func TestAuthHeader_OAuth2AuthorizationCodeUsesToken(t *testing.T) {
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index ada9d804..3fa5ebf8 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -215,6 +215,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"authCommandShort":       authCommandShort,
 		"authHarvestedEnvHint":   authHarvestedEnvHint,
 		"hasAuthEnvVarKind":      hasAuthEnvVarKind,
+		"isRequestAuthEnvVar":    isRequestAuthEnvVar,
 		"effectiveTier":          effectiveTier,
 		"effectiveSubTier":       effectiveSubTier,
 		"add":                    func(a, b int) int { return a + b },
@@ -834,6 +835,10 @@ func hasAuthEnvVarKind(envVarSpecs []spec.AuthEnvVar, kind string) bool {
 	return false
 }
 
+func isRequestAuthEnvVar(envVar spec.AuthEnvVar) bool {
+	return envVar.IsRequestCredential()
+}
+
 func effectiveTier(api *spec.APISpec, resource spec.Resource, endpoint spec.Endpoint) string {
 	if api == nil {
 		return ""
diff --git a/internal/generator/limit_truncation_test.go b/internal/generator/limit_truncation_test.go
index 4b1ea04d..220d7dac 100644
--- a/internal/generator/limit_truncation_test.go
+++ b/internal/generator/limit_truncation_test.go
@@ -1,10 +1,13 @@
 package generator
 
 import (
+	"os"
+	"path/filepath"
 	"testing"
 
 	"github.com/mvanhorn/cli-printing-press/v3/internal/spec"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 // TestEndpointNeedsClientLimit verifies the gate that controls when
@@ -96,3 +99,40 @@ func TestEndpointNeedsClientLimit(t *testing.T) {
 		})
 	}
 }
+
+func TestClientLimitGenerationEmitsHelperAndBuilds(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("limit-truncate")
+	apiSpec.Resources = map[string]spec.Resource{
+		"orders": {
+			Description: "Manage orders",
+			Endpoints: map[string]spec.Endpoint{
+				"list": {
+					Method:      "GET",
+					Path:        "/orders",
+					Description: "List orders",
+					Params: []spec.Param{{
+						Name:    "limit",
+						Type:    "integer",
+						Default: 5,
+					}},
+					Response: spec.ResponseDef{Type: "array", Item: "Order"},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "limit-truncate-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	helpersSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "helpers.go"))
+	require.NoError(t, err)
+	require.Contains(t, string(helpersSrc), "func truncateJSONArray(")
+
+	commandSrc := readGeneratedFile(t, outputDir, "internal", "cli", "promoted_orders.go")
+	require.Contains(t, commandSrc, "truncateJSONArray(data,",
+		"generated command should call truncateJSONArray for non-paginated limit endpoint")
+
+	runGoCommand(t, outputDir, "build", "./internal/cli")
+}
diff --git a/internal/generator/templates/auth_simple.go.tmpl b/internal/generator/templates/auth_simple.go.tmpl
index c415e494..73d923ed 100644
--- a/internal/generator/templates/auth_simple.go.tmpl
+++ b/internal/generator/templates/auth_simple.go.tmpl
@@ -61,8 +61,16 @@ func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
 				fmt.Fprintln(w, red("Not authenticated"))
 				fmt.Fprintln(w, "")
 				fmt.Fprintln(w, "Set your token:")
+{{- if .Auth.EnvVarSpecs}}
+{{- range .Auth.EnvVarSpecs}}
+{{- if isRequestAuthEnvVar .}}
+				fmt.Fprintln(w, "  export {{.Name}}=\"your-token-here\"")
+{{- end}}
+{{- end}}
+{{- else}}
 {{- range .Auth.EnvVars}}
 				fmt.Fprintln(w, "  export {{.}}=\"your-token-here\"")
+{{- end}}
 {{- end}}
 				fmt.Fprintf(w, "  {{.Name}}-pp-cli auth set-token <token>\n")
 				return authErr(fmt.Errorf("no credentials configured"))
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 80c5abb7..d134ab75 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -997,6 +997,10 @@ func (c *Client) authHeader() (string, error) {
 		return "", nil
 	}
 {{- if eq .Auth.EffectiveOAuth2Grant "client_credentials"}}
+	if c.Config.AccessToken == "" && cliutil.IsVerifyEnv() {
+		c.Config.AccessToken = "mock-token-for-testing"
+		return c.Config.AuthHeader(), nil
+	}
 	// 60s window avoids in-flight requests racing the expiry boundary.
 	// Double-checked lock so only one goroutine mints under contention.
 	if needsClientCredentialsMint(c.Config) {
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 3dce55e0..61ec96f6 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -328,12 +328,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 			}
 {{- end}}
 {{- if endpointNeedsClientLimit .Endpoint}}
-			// The API doesn't declare a paginator but accepts a limit
-			// query param. Some APIs (Firebase, file-backed JSON dumps,
-			// RSS feeds) silently ignore ?limit=N and return the full
-			// collection — truncate client-side so --limit N is
-			// honored regardless. Idempotent when the API already
-			// returned <= N items.
+			// Honor --limit when the API accepts but ignores ?limit=N.
 			data = truncateJSONArray(data, flagLimit)
 {{- end}}
 
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index 9cc6a463..d362fd3c 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -193,6 +193,10 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 			if err != nil {
 				return classifyAPIError(err, flags)
 			}
+{{- if endpointNeedsClientLimit .Endpoint}}
+			// Honor --limit when the API accepts but ignores ?limit=N.
+			data = truncateJSONArray(data, flagLimit)
+{{- end}}
 
 {{- if .Endpoint.UsesHTMLResponse}}
 			if !flags.dryRun {
diff --git a/internal/generator/templates/config.go.tmpl b/internal/generator/templates/config.go.tmpl
index e990394b..8bc7f9ea 100644
--- a/internal/generator/templates/config.go.tmpl
+++ b/internal/generator/templates/config.go.tmpl
@@ -229,20 +229,6 @@ func (c *Config) AuthHeader() string {
 		return "Bearer " + c.AccessToken
 		{{- end}}
 	}
-	{{- with $canonicalEnvVar}}
-	if c.{{resolveEnvVarField .Name}} != "" {
-		c.AuthSource = "env:{{.Name}}"
-		{{- if $.Auth.Format}}
-		return applyAuthFormat("{{$.Auth.Format}}", map[string]string{
-			"{{envVarPlaceholder .Name}}": c.{{resolveEnvVarField .Name}},
-			"{{.Name}}": c.{{resolveEnvVarField .Name}},
-			"token": c.{{resolveEnvVarField .Name}},
-		})
-		{{- else}}
-		return "Bearer " + c.{{resolveEnvVarField .Name}}
-		{{- end}}
-	}
-	{{- end}}
 	return ""
 {{- else}}
 {{- if .BearerRefresh.Enabled}}
diff --git a/internal/generator/templates_test.go b/internal/generator/templates_test.go
index fbce37b3..b90e63b5 100644
--- a/internal/generator/templates_test.go
+++ b/internal/generator/templates_test.go
@@ -43,6 +43,42 @@ func TestDoctorTemplateRendersKindAwareAuthEnvPresence(t *testing.T) {
 	require.Contains(t, content, `report["env_vars"] = "INFO set one of: " + strings.Join(authEnvOptionalNames, " or ")`)
 }
 
+func TestAuthStatusHintsOnlyRequestCredentialEnvVars(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("auth-status-rich-auth")
+	apiSpec.Auth = spec.AuthConfig{
+		Type: "api_key",
+		EnvVarSpecs: []spec.AuthEnvVar{
+			{Name: "STATUS_AUTH_TOKEN", Kind: spec.AuthEnvVarKindPerCall, Required: true, Sensitive: true},
+			{Name: "STATUS_AUTH_CLIENT_ID", Kind: spec.AuthEnvVarKindAuthFlowInput, Required: false, Sensitive: false},
+			{Name: "STATUS_AUTH_CLIENT_SECRET", Kind: spec.AuthEnvVarKindAuthFlowInput, Required: false, Sensitive: true},
+			{Name: "STATUS_AUTH_SESSION_COOKIE", Kind: spec.AuthEnvVarKindHarvested, Required: false, Sensitive: true},
+			{Name: "STATUS_AUTH_OPTIONAL_TOKEN", Kind: spec.AuthEnvVarKindPerCall, Required: false, Sensitive: true},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "auth-status-rich-auth-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	authSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "auth.go"))
+	require.NoError(t, err)
+	content := string(authSrc)
+
+	start := strings.Index(content, `fmt.Fprintln(w, "Set your token:")`)
+	require.NotEqual(t, -1, start, "auth status hint block should be emitted:\n%s", content)
+	hintBlock := content[start:]
+	end := strings.Index(hintBlock, `auth set-token <token>`)
+	require.NotEqual(t, -1, end, "auth set-token fallback should terminate status hint block:\n%s", hintBlock)
+	hintBlock = hintBlock[:end]
+
+	require.Contains(t, hintBlock, `export STATUS_AUTH_TOKEN=\"your-token-here\"`)
+	require.Contains(t, hintBlock, `export STATUS_AUTH_OPTIONAL_TOKEN=\"your-token-here\"`)
+	require.NotContains(t, hintBlock, `STATUS_AUTH_CLIENT_ID`)
+	require.NotContains(t, hintBlock, `STATUS_AUTH_CLIENT_SECRET`)
+	require.NotContains(t, hintBlock, `STATUS_AUTH_SESSION_COOKIE`)
+}
+
 func TestMCPContextOmitsHarvestedAuthEnvVars(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/pipeline/runtime.go b/internal/pipeline/runtime.go
index 165606db..a4284982 100644
--- a/internal/pipeline/runtime.go
+++ b/internal/pipeline/runtime.go
@@ -200,6 +200,7 @@ func RunVerify(cfg VerifyConfig) (*VerifyReport, error) {
 		}
 	}
 	authEnvVars := authEnvVarSpecNames(authEnvVarSpecs)
+	requestAuthEnvVars := requestAuthEnvVarNames(authEnvVarSpecs)
 	authEnvVarStatuses := summarizeAuthEnvVars(authEnvVarSpecs, cfg.APIKey, cfg.EnvVar, report.Mode)
 	if shouldReportAuthEnvVarStatuses(authEnvVarStatuses) {
 		report.AuthEnvVars = authEnvVarStatuses
@@ -231,16 +232,17 @@ func RunVerify(cfg VerifyConfig) (*VerifyReport, error) {
 					env = append(env, ev+"="+val)
 				}
 			}
-			// Also pass the explicit --api-key under ALL auth env var names so the
-			// generated CLI finds it regardless of which env var it reads.
+			// Also pass the explicit --api-key under request credential env var
+			// names so the generated CLI finds it without rewriting setup-only
+			// OAuth inputs such as client IDs and client secrets.
 			if cfg.APIKey != "" {
-				for _, ev := range authEnvVars {
+				for _, ev := range requestAuthEnvVars {
 					env = append(env, ev+"="+cfg.APIKey)
 				}
 			}
 		} else {
 			env = append(env, baseURLEnvVar+"="+baseURLOverride)
-			for _, ev := range authEnvVars {
+			for _, ev := range requestAuthEnvVars {
 				env = append(env, ev+"=mock-token-for-testing")
 			}
 			// Templated URLs (e.g. /admin/api/{api_version}/graphql.json)
@@ -305,9 +307,24 @@ func RunVerify(cfg VerifyConfig) (*VerifyReport, error) {
 }
 
 func authEnvVarSpecNames(envVarSpecs []apispec.AuthEnvVar) []string {
+	return authEnvVarNamesMatching(envVarSpecs, func(apispec.AuthEnvVar) bool {
+		return true
+	})
+}
+
+func requestAuthEnvVarNames(envVarSpecs []apispec.AuthEnvVar) []string {
+	return authEnvVarNamesMatching(envVarSpecs, func(envVar apispec.AuthEnvVar) bool {
+		return envVar.IsRequestCredential()
+	})
+}
+
+func authEnvVarNamesMatching(envVarSpecs []apispec.AuthEnvVar, matches func(apispec.AuthEnvVar) bool) []string {
 	names := make([]string, 0, len(envVarSpecs))
 	seen := make(map[string]struct{}, len(envVarSpecs))
 	for _, envVar := range envVarSpecs {
+		if !matches(envVar) {
+			continue
+		}
 		name := strings.TrimSpace(envVar.Name)
 		if name == "" {
 			continue
@@ -326,15 +343,14 @@ func summarizeAuthEnvVars(envVarSpecs []apispec.AuthEnvVar, apiKey, apiKeyEnvVar
 	if apiKey != "" && apiKeyEnvVar == "" {
 		for _, envVar := range envVarSpecs {
 			name := strings.TrimSpace(envVar.Name)
-			if name == "" {
+			if name == "" || !envVar.IsRequestCredential() {
 				continue
 			}
-			apiKeyEnvVar = name
-			kind := envVar.Kind
-			if kind == "" {
-				kind = apispec.AuthEnvVarKindPerCall
+			if apiKeyEnvVar == "" {
+				apiKeyEnvVar = name
 			}
-			if kind == apispec.AuthEnvVarKindPerCall && envVar.Required {
+			if envVar.Required {
+				apiKeyEnvVar = name
 				break
 			}
 		}
@@ -344,10 +360,7 @@ func summarizeAuthEnvVars(envVarSpecs []apispec.AuthEnvVar, apiKey, apiKeyEnvVar
 		if name == "" {
 			continue
 		}
-		kind := envVar.Kind
-		if kind == "" {
-			kind = apispec.AuthEnvVarKindPerCall
-		}
+		kind := envVar.EffectiveKind()
 		status := AuthEnvVarStatus{
 			Name:     name,
 			Kind:     string(kind),
@@ -358,7 +371,7 @@ func summarizeAuthEnvVars(envVarSpecs []apispec.AuthEnvVar, apiKey, apiKeyEnvVar
 			statuses = append(statuses, status)
 			continue
 		}
-		if kind == apispec.AuthEnvVarKindPerCall && envVar.Required {
+		if envVar.IsRequestCredential() && envVar.Required {
 			status.Status = AuthEnvVarStatusMissingRequired
 			status.Detail = "required per-call auth env var is not set"
 		} else {
diff --git a/internal/pipeline/runtime_test.go b/internal/pipeline/runtime_test.go
index 48a73870..ad87b4e0 100644
--- a/internal/pipeline/runtime_test.go
+++ b/internal/pipeline/runtime_test.go
@@ -952,6 +952,18 @@ func TestSummarizeAuthEnvVarsKindAware(t *testing.T) {
 	assert.Equal(t, AuthEnvVarStatusOK, mock[0].Status)
 }
 
+func TestRequestAuthEnvVarNamesOnlyIncludesPerCallCredentials(t *testing.T) {
+	got := requestAuthEnvVarNames([]apispec.AuthEnvVar{
+		{Name: "REQUIRED_TOKEN", Kind: apispec.AuthEnvVarKindPerCall, Required: true},
+		{Name: "LEGACY_EMPTY_KIND"},
+		{Name: "SETUP_CLIENT_ID", Kind: apispec.AuthEnvVarKindAuthFlowInput},
+		{Name: "SESSION_COOKIE", Kind: apispec.AuthEnvVarKindHarvested},
+		{Name: "REQUIRED_TOKEN", Kind: apispec.AuthEnvVarKindPerCall, Required: true},
+	})
+
+	assert.Equal(t, []string{"REQUIRED_TOKEN", "LEGACY_EMPTY_KIND"}, got)
+}
+
 func TestSummarizeAuthEnvVarsAPIKeyOnlySatisfiesConfiguredEnvVar(t *testing.T) {
 	t.Setenv("PRIMARY_TOKEN", "")
 	t.Setenv("SECONDARY_TOKEN", "")
@@ -971,11 +983,15 @@ func TestSummarizeAuthEnvVarsApiKeyWithoutCanonicalEnvVar(t *testing.T) {
 	t.Setenv("PRIMARY_TOKEN", "")
 
 	got := summarizeAuthEnvVars([]apispec.AuthEnvVar{
+		{Name: "SETUP_CLIENT_ID", Kind: apispec.AuthEnvVarKindAuthFlowInput},
 		{Name: "PRIMARY_TOKEN", Kind: apispec.AuthEnvVarKindPerCall, Required: true},
+		{Name: "SESSION_COOKIE", Kind: apispec.AuthEnvVarKindHarvested},
 	}, "secret", "", "live")
 
-	require.Len(t, got, 1)
-	assert.Equal(t, AuthEnvVarStatusOK, got[0].Status)
+	require.Len(t, got, 3)
+	assert.Equal(t, AuthEnvVarStatusMissingInfo, got[0].Status)
+	assert.Equal(t, AuthEnvVarStatusOK, got[1].Status)
+	assert.Equal(t, AuthEnvVarStatusMissingInfo, got[2].Status)
 	assert.Empty(t, missingRequiredAuthEnvVars(got))
 }
 
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index f4440798..7990bd19 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -481,6 +481,19 @@ const (
 	AuthEnvVarKindHarvested     AuthEnvVarKind = "harvested"
 )
 
+// EffectiveKind treats legacy empty kinds as per-call credentials.
+func (v AuthEnvVar) EffectiveKind() AuthEnvVarKind {
+	if v.Kind == "" {
+		return AuthEnvVarKindPerCall
+	}
+	return v.Kind
+}
+
+// IsRequestCredential reports whether this env var can satisfy request auth.
+func (v AuthEnvVar) IsRequestCredential() bool {
+	return v.EffectiveKind() == AuthEnvVarKindPerCall
+}
+
 func (k AuthEnvVarKind) SensitivePlaceholder() string {
 	switch k {
 	case AuthEnvVarKindPerCall:
@@ -511,7 +524,7 @@ func (c *AuthConfig) CanonicalEnvVar() *AuthEnvVar {
 	}
 	c.NormalizeEnvVarSpecs("")
 	for i := range c.EnvVarSpecs {
-		if c.EnvVarSpecs[i].Kind == AuthEnvVarKindPerCall && c.EnvVarSpecs[i].Required {
+		if c.EnvVarSpecs[i].IsRequestCredential() && c.EnvVarSpecs[i].Required {
 			return &c.EnvVarSpecs[i]
 		}
 	}
@@ -530,7 +543,7 @@ func (c *AuthConfig) IsAuthEnvVarORCase() bool {
 		return false
 	}
 	for _, ev := range c.EnvVarSpecs {
-		if ev.Kind != AuthEnvVarKindPerCall || ev.Required {
+		if !ev.IsRequestCredential() || ev.Required {
 			return false
 		}
 	}
@@ -1516,7 +1529,7 @@ func independentAuthORGroupsExample(envVarSpecs []AuthEnvVar) (string, bool) {
 	members := make([]AuthEnvVar, 0, len(envVarSpecs))
 	for _, envVar := range envVarSpecs {
 		name := strings.TrimSpace(envVar.Name)
-		if name == "" || envVar.Kind != AuthEnvVarKindPerCall || envVar.Required || !strings.Contains(envVar.Description, " OR ") {
+		if name == "" || !envVar.IsRequestCredential() || envVar.Required || !strings.Contains(envVar.Description, " OR ") {
 			continue
 		}
 		referencesSibling := false
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
index 9483f84a..f3297a7f 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
@@ -339,6 +339,10 @@ func (c *Client) authHeader() (string, error) {
 	if c.Config == nil {
 		return "", nil
 	}
+	if c.Config.AccessToken == "" && cliutil.IsVerifyEnv() {
+		c.Config.AccessToken = "mock-token-for-testing"
+		return c.Config.AuthHeader(), nil
+	}
 	// 60s window avoids in-flight requests racing the expiry boundary.
 	// Double-checked lock so only one goroutine mints under contention.
 	if needsClientCredentialsMint(c.Config) {
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/config/config.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/config/config.go
index 02432df0..9a5ddc07 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/config/config.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/config/config.go
@@ -78,10 +78,6 @@ func (c *Config) AuthHeader() string {
 		c.AuthSource = "oauth2"
 		return "Bearer " + c.AccessToken
 	}
-	if c.PrintingPressOauth2ClientId != "" {
-		c.AuthSource = "env:PRINTING_PRESS_OAUTH2_CLIENT_ID"
-		return "Bearer " + c.PrintingPressOauth2ClientId
-	}
 	return ""
 }
 
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
index 523863c7..209f4628 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
@@ -60,9 +60,6 @@ func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
 				fmt.Fprintln(w, "")
 				fmt.Fprintln(w, "Set your token:")
 				fmt.Fprintln(w, "  export RICH_AUTH_API_KEY=\"your-token-here\"")
-				fmt.Fprintln(w, "  export RICH_AUTH_CLIENT_ID=\"your-token-here\"")
-				fmt.Fprintln(w, "  export RICH_AUTH_CLIENT_SECRET=\"your-token-here\"")
-				fmt.Fprintln(w, "  export RICH_AUTH_SESSION_COOKIE=\"your-token-here\"")
 				fmt.Fprintln(w, "  export RICH_AUTH_OPTIONAL_TOKEN=\"your-token-here\"")
 				fmt.Fprintln(w, "  export RICH_AUTH_BOT_TOKEN=\"your-token-here\"")
 				fmt.Fprintln(w, "  export RICH_AUTH_USER_TOKEN=\"your-token-here\"")

← 3bc1a23a feat(cli): widen auth env-var model with kind/required/sensi  ·  back to Cli Printing Press  ·  chore(main): release 3.10.0 (#616) f486e4d2 →