← back to Cli Printing Press
fix(generator): scope caches by auth identity (#853)
1a8f68eeb91a433554c3ef781de4382ee3d82671 · 2026-05-09 14:41:39 -0700 · hnshah
Files touched
A internal/generator/client_cache_scope_test.goA internal/generator/doctor_auth_status_test.goM internal/generator/templates/client.go.tmplM internal/generator/templates/doctor.go.tmplM internal/generator/tier_routing_test.goM testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.goM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/doctor.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
Diff
commit 1a8f68eeb91a433554c3ef781de4382ee3d82671
Author: hnshah <hnshah@gmail.com>
Date: Sat May 9 14:41:39 2026 -0700
fix(generator): scope caches by auth identity (#853)
---
internal/generator/client_cache_scope_test.go | 46 ++++++++++++++++++++++
internal/generator/doctor_auth_status_test.go | 37 +++++++++++++++++
internal/generator/templates/client.go.tmpl | 24 ++++++++---
internal/generator/templates/doctor.go.tmpl | 27 ++++++++++++-
internal/generator/tier_routing_test.go | 3 +-
.../internal/client/client.go | 20 +++++++++-
.../internal/cli/doctor.go | 10 +++++
.../printing-press-golden/internal/cli/doctor.go | 10 +++++
.../internal/client/client.go | 20 +++++++++-
.../tier-routing-golden/internal/cli/doctor.go | 10 +++++
.../tier-routing-golden/internal/client/client.go | 22 +++++++++--
11 files changed, 214 insertions(+), 15 deletions(-)
diff --git a/internal/generator/client_cache_scope_test.go b/internal/generator/client_cache_scope_test.go
new file mode 100644
index 00000000..817ad17d
--- /dev/null
+++ b/internal/generator/client_cache_scope_test.go
@@ -0,0 +1,46 @@
+package generator
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+ "github.com/stretchr/testify/require"
+)
+
+func TestClientCacheKeyScopesByBaseURLAndAuthIdentity(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("cache-scope")
+ apiSpec.Auth = spec.AuthConfig{
+ Type: "bearer_token",
+ Header: "Authorization",
+ EnvVars: []string{"CACHE_SCOPE_TOKEN"},
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "cache-scope-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ clientSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "client", "client.go"))
+ require.NoError(t, err)
+ client := string(clientSrc)
+ body := clientCacheKeyBody(t, client)
+
+ require.Contains(t, body, `"|base_url=" + c.BaseURL`, "cache keys must isolate staging/prod or per-tenant base URLs")
+ require.Contains(t, body, `"|auth_source=" + c.Config.AuthSource`, "cache keys should distinguish env/config/profile auth sources")
+ require.Contains(t, body, `sha256.Sum256([]byte(c.Config.AuthHeader()))`, "cache keys should include an auth fingerprint without storing the raw token")
+ require.Contains(t, body, `sort.Strings(paramKeys)`, "cache keys should be deterministic for map params")
+}
+
+func clientCacheKeyBody(t *testing.T, content string) string {
+ t.Helper()
+ start := strings.Index(content, "func (c *Client) cacheKey(")
+ require.NotEqual(t, -1, start, "cacheKey function must be emitted")
+ body := content[start:]
+ if next := strings.Index(body[1:], "\nfunc "); next != -1 {
+ body = body[:next+1]
+ }
+ return body
+}
diff --git a/internal/generator/doctor_auth_status_test.go b/internal/generator/doctor_auth_status_test.go
new file mode 100644
index 00000000..b885a20a
--- /dev/null
+++ b/internal/generator/doctor_auth_status_test.go
@@ -0,0 +1,37 @@
+package generator
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDoctorReportsConfigAuthAsEnvVarsSatisfied(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("doctor-auth-status")
+ apiSpec.Auth = spec.AuthConfig{
+ Type: "bearer_token",
+ Header: "Authorization",
+ EnvVars: []string{"DOCTOR_AUTH_STATUS_TOKEN"},
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "doctor-auth-status-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ doctorSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "doctor.go"))
+ require.NoError(t, err)
+ doctor := string(doctorSrc)
+
+ require.Contains(t, doctor, "authConfigured := false", "doctor should remember when cfg.AuthHeader() satisfied auth")
+ require.Contains(t, doctor, "credentials available from", "doctor env-var check should explain config-file credentials")
+ require.Contains(t, doctor, `report["env_vars"] = "OK " + strings.Join(authEnvInfo, "; ")`, "config credentials must not degrade env_vars to INFO/WARN")
+ require.NotContains(t, doctor, `if os.Getenv("DOCTOR_AUTH_STATUS_TOKEN") != "" {
+ authEnvSet++
+ }
+
+ if authEnvSet == 0 {`, "legacy EnvVars branch must not report zero env vars when config auth is already valid")
+}
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 4099c4bf..79d40acd 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -20,9 +20,7 @@ import (
{{- end}}
"os"
"path/filepath"
-{{- if ne .ClientPattern "proxy-envelope"}}
"sort"
-{{- end}}
"strings"
{{- if eq .Auth.EffectiveOAuth2Grant "client_credentials"}}
"sync"
@@ -375,11 +373,27 @@ func (c *Client) ProbeGet(path string) (int, error) {
func (c *Client) cacheKey(path string, params map[string]string) string {
key := path
+ key += "|base_url=" + c.BaseURL
{{- if .HasTierRouting}}
- key += "|tier=" + c.requestTier + "|base_url=" + c.baseURLForRequest()
+ key += "|tier=" + c.requestTier + "|tier_base_url=" + c.baseURLForRequest()
{{- end}}
- for k, v := range params {
- key += k + "=" + v
+ if c.Config != nil {
+ key += "|auth_source=" + c.Config.AuthSource
+ if authHeader := c.Config.AuthHeader(); authHeader != "" {
+ authHash := sha256.Sum256([]byte(c.Config.AuthHeader()))
+ key += "|auth=" + hex.EncodeToString(authHash[:8])
+ }
+ if c.Config.Path != "" {
+ key += "|config_path=" + c.Config.Path
+ }
+ }
+ paramKeys := make([]string, 0, len(params))
+ for k := range params {
+ paramKeys = append(paramKeys, k)
+ }
+ sort.Strings(paramKeys)
+ for _, k := range paramKeys {
+ key += k + "=" + params[k]
}
{{- if .EndpointTemplateVars}}
// Include resolved template-var values in the cache identity so two
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index 47ac975d..99a88721 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -134,7 +134,9 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
// Check auth
{{- if or (eq .Auth.Type "") (eq .Auth.Type "none")}}
report["auth"] = "not required"
-{{- else if .Auth.Inferred}}
+{{- else}}
+ authConfigured := false
+{{- if .Auth.Inferred}}
// Auth was inferred from spec description — warn the user to verify
if cfg != nil {
header := cfg.AuthHeader()
@@ -144,6 +146,7 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
report["auth_hint"] = "export {{.Name}}=<your-key>"
{{- end}}
} else {
+ authConfigured = true
report["auth"] = "inferred (configured — verify header and env var are correct)"
report["auth_source"] = cfg.AuthSource
}
@@ -157,6 +160,7 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
report["auth"] = "not configured"
report["auth_hint"] = "{{.Name}}-pp-cli auth login --chrome"
} else {
+ authConfigured = true
report["auth"] = "configured (browser session)"
report["auth_source"] = cfg.AuthSource
report["auth_domain"] = "{{.Auth.CookieDomain}}"
@@ -203,10 +207,15 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
report["auth_key_url"] = "{{.Auth.KeyURL}}"
{{- end}}
} else {
+ authConfigured = true
report["auth"] = "configured"
report["auth_source"] = cfg.AuthSource
}
}
+{{- end}}
+{{- if not (or .Auth.EnvVarSpecs .Auth.EnvVars)}}
+ _ = authConfigured
+{{- end}}
{{- end}}
// Check auth environment variables
@@ -228,6 +237,12 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
{{- if .Required}}
if os.Getenv("{{.Name}}") != "" {
authEnvSet = append(authEnvSet, "{{.Name}}")
+ } else if authConfigured {
+ authSource, _ := report["auth_source"].(string)
+ if authSource == "" {
+ authSource = "config"
+ }
+ authEnvInfo = append(authEnvInfo, "credentials available from "+authSource)
} else {
authEnvRequiredMissing = append(authEnvRequiredMissing, "{{.Name}}")
}
@@ -263,6 +278,8 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
report["env_vars"] = "ERROR missing required: " + strings.Join(authEnvRequiredMissing, ", ")
case len(authEnvOptionalNames) > 1 && !authEnvOptionalSatisfied:
report["env_vars"] = "INFO set one of: " + strings.Join(authEnvOptionalNames, " or ")
+ case len(authEnvInfo) > 0 && authConfigured:
+ report["env_vars"] = "OK " + strings.Join(authEnvInfo, "; ")
case len(authEnvInfo) > 0:
report["env_vars"] = "INFO " + strings.Join(authEnvInfo, "; ")
default:
@@ -277,7 +294,13 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
authEnvSet++
}
{{- end}}
- if authEnvSet == 0 {
+ if authEnvSet == 0 && authConfigured {
+ authSource, _ := report["auth_source"].(string)
+ if authSource == "" {
+ authSource = "config"
+ }
+ report["env_vars"] = "OK credentials available from " + authSource
+ } else if authEnvSet == 0 {
report["env_vars"] = fmt.Sprintf("none set (checked %d)", authEnvChecked)
} else {
report["env_vars"] = fmt.Sprintf("%d/%d set", authEnvSet, authEnvChecked)
diff --git a/internal/generator/tier_routing_test.go b/internal/generator/tier_routing_test.go
index dae657a6..4d633f81 100644
--- a/internal/generator/tier_routing_test.go
+++ b/internal/generator/tier_routing_test.go
@@ -93,7 +93,8 @@ func TestTierRoutingEmitsTierAwareClientAndCommands(t *testing.T) {
require.Contains(t, clientSrc, `os.Getenv("TIERED_PAID_KEY")`)
require.Contains(t, clientSrc, `"access_token": tierValue0`)
require.Contains(t, clientSrc, `q.Set(authInfo.Name, authHeader)`)
- require.Contains(t, clientSrc, `key += "|tier=" + c.requestTier + "|base_url=" + c.baseURLForRequest()`)
+ require.Contains(t, clientSrc, `key += "|base_url=" + c.BaseURL`)
+ require.Contains(t, clientSrc, `key += "|tier=" + c.requestTier + "|tier_base_url=" + c.baseURLForRequest()`)
freeCmd := readGeneratedFile(t, outputDir, "internal", "cli", "items_list.go")
require.Contains(t, freeCmd, `c = c.WithTier("free")`)
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 901bd1fc..497d4973 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
@@ -99,8 +99,24 @@ func (c *Client) ProbeGet(path string) (int, error) {
func (c *Client) cacheKey(path string, params map[string]string) string {
key := path
- for k, v := range params {
- key += k + "=" + v
+ key += "|base_url=" + c.BaseURL
+ if c.Config != nil {
+ key += "|auth_source=" + c.Config.AuthSource
+ if authHeader := c.Config.AuthHeader(); authHeader != "" {
+ authHash := sha256.Sum256([]byte(c.Config.AuthHeader()))
+ key += "|auth=" + hex.EncodeToString(authHash[:8])
+ }
+ if c.Config.Path != "" {
+ key += "|config_path=" + c.Config.Path
+ }
+ }
+ paramKeys := make([]string, 0, len(params))
+ for k := range params {
+ paramKeys = append(paramKeys, k)
+ }
+ sort.Strings(paramKeys)
+ for _, k := range paramKeys {
+ key += k + "=" + params[k]
}
h := sha256.Sum256([]byte(key))
return hex.EncodeToString(h[:8])
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/doctor.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/doctor.go
index 6fb0f8e5..6e68e94d 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/doctor.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/doctor.go
@@ -85,12 +85,14 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
}
// Check auth
+ authConfigured := false
if cfg != nil {
header := cfg.AuthHeader()
if header == "" {
report["auth"] = "not configured"
report["auth_hint"] = "export RICH_AUTH_API_KEY=<your-key>"
} else {
+ authConfigured = true
report["auth"] = "configured"
report["auth_source"] = cfg.AuthSource
}
@@ -109,6 +111,12 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
}
if os.Getenv("RICH_AUTH_API_KEY") != "" {
authEnvSet = append(authEnvSet, "RICH_AUTH_API_KEY")
+ } else if authConfigured {
+ authSource, _ := report["auth_source"].(string)
+ if authSource == "" {
+ authSource = "config"
+ }
+ authEnvInfo = append(authEnvInfo, "credentials available from "+authSource)
} else {
authEnvRequiredMissing = append(authEnvRequiredMissing, "RICH_AUTH_API_KEY")
}
@@ -165,6 +173,8 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
report["env_vars"] = "ERROR missing required: " + strings.Join(authEnvRequiredMissing, ", ")
case len(authEnvOptionalNames) > 1 && !authEnvOptionalSatisfied:
report["env_vars"] = "INFO set one of: " + strings.Join(authEnvOptionalNames, " or ")
+ case len(authEnvInfo) > 0 && authConfigured:
+ report["env_vars"] = "OK " + strings.Join(authEnvInfo, "; ")
case len(authEnvInfo) > 0:
report["env_vars"] = "INFO " + strings.Join(authEnvInfo, "; ")
default:
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
index 09b86a28..6172b3fd 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
@@ -85,12 +85,14 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
}
// Check auth
+ authConfigured := false
if cfg != nil {
header := cfg.AuthHeader()
if header == "" {
report["auth"] = "not configured"
report["auth_hint"] = "export PRINTING_PRESS_GOLDEN_API_KEY=<your-key>"
} else {
+ authConfigured = true
report["auth"] = "configured"
report["auth_source"] = cfg.AuthSource
}
@@ -105,6 +107,12 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
authEnvOptionalSatisfied := false
if os.Getenv("PRINTING_PRESS_GOLDEN_API_KEY") != "" {
authEnvSet = append(authEnvSet, "PRINTING_PRESS_GOLDEN_API_KEY")
+ } else if authConfigured {
+ authSource, _ := report["auth_source"].(string)
+ if authSource == "" {
+ authSource = "config"
+ }
+ authEnvInfo = append(authEnvInfo, "credentials available from "+authSource)
} else {
authEnvRequiredMissing = append(authEnvRequiredMissing, "PRINTING_PRESS_GOLDEN_API_KEY")
}
@@ -113,6 +121,8 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
report["env_vars"] = "ERROR missing required: " + strings.Join(authEnvRequiredMissing, ", ")
case len(authEnvOptionalNames) > 1 && !authEnvOptionalSatisfied:
report["env_vars"] = "INFO set one of: " + strings.Join(authEnvOptionalNames, " or ")
+ case len(authEnvInfo) > 0 && authConfigured:
+ report["env_vars"] = "OK " + strings.Join(authEnvInfo, "; ")
case len(authEnvInfo) > 0:
report["env_vars"] = "INFO " + strings.Join(authEnvInfo, "; ")
default:
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
index 80eff7fb..331598ff 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
@@ -93,8 +93,24 @@ func (c *Client) ProbeGet(path string) (int, error) {
func (c *Client) cacheKey(path string, params map[string]string) string {
key := path
- for k, v := range params {
- key += k + "=" + v
+ key += "|base_url=" + c.BaseURL
+ if c.Config != nil {
+ key += "|auth_source=" + c.Config.AuthSource
+ if authHeader := c.Config.AuthHeader(); authHeader != "" {
+ authHash := sha256.Sum256([]byte(c.Config.AuthHeader()))
+ key += "|auth=" + hex.EncodeToString(authHash[:8])
+ }
+ if c.Config.Path != "" {
+ key += "|config_path=" + c.Config.Path
+ }
+ }
+ paramKeys := make([]string, 0, len(params))
+ for k := range params {
+ paramKeys = append(paramKeys, k)
+ }
+ sort.Strings(paramKeys)
+ for _, k := range paramKeys {
+ key += k + "=" + params[k]
}
h := sha256.Sum256([]byte(key))
return hex.EncodeToString(h[:8])
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.go
index 65c4ec8c..d57149cd 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/doctor.go
@@ -85,12 +85,14 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
}
// Check auth
+ authConfigured := false
if cfg != nil {
header := cfg.AuthHeader()
if header == "" {
report["auth"] = "not configured"
report["auth_hint"] = "export TIER_GLOBAL_TOKEN=<your-key>"
} else {
+ authConfigured = true
report["auth"] = "configured"
report["auth_source"] = cfg.AuthSource
}
@@ -105,6 +107,12 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
authEnvOptionalSatisfied := false
if os.Getenv("TIER_GLOBAL_TOKEN") != "" {
authEnvSet = append(authEnvSet, "TIER_GLOBAL_TOKEN")
+ } else if authConfigured {
+ authSource, _ := report["auth_source"].(string)
+ if authSource == "" {
+ authSource = "config"
+ }
+ authEnvInfo = append(authEnvInfo, "credentials available from "+authSource)
} else {
authEnvRequiredMissing = append(authEnvRequiredMissing, "TIER_GLOBAL_TOKEN")
}
@@ -113,6 +121,8 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
report["env_vars"] = "ERROR missing required: " + strings.Join(authEnvRequiredMissing, ", ")
case len(authEnvOptionalNames) > 1 && !authEnvOptionalSatisfied:
report["env_vars"] = "INFO set one of: " + strings.Join(authEnvOptionalNames, " or ")
+ case len(authEnvInfo) > 0 && authConfigured:
+ report["env_vars"] = "OK " + strings.Join(authEnvInfo, "; ")
case len(authEnvInfo) > 0:
report["env_vars"] = "INFO " + strings.Join(authEnvInfo, "; ")
default:
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
index 09787659..d271cd0b 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
@@ -188,9 +188,25 @@ func (c *Client) ProbeGet(path string) (int, error) {
func (c *Client) cacheKey(path string, params map[string]string) string {
key := path
- key += "|tier=" + c.requestTier + "|base_url=" + c.baseURLForRequest()
- for k, v := range params {
- key += k + "=" + v
+ key += "|base_url=" + c.BaseURL
+ key += "|tier=" + c.requestTier + "|tier_base_url=" + c.baseURLForRequest()
+ if c.Config != nil {
+ key += "|auth_source=" + c.Config.AuthSource
+ if authHeader := c.Config.AuthHeader(); authHeader != "" {
+ authHash := sha256.Sum256([]byte(c.Config.AuthHeader()))
+ key += "|auth=" + hex.EncodeToString(authHash[:8])
+ }
+ if c.Config.Path != "" {
+ key += "|config_path=" + c.Config.Path
+ }
+ }
+ paramKeys := make([]string, 0, len(params))
+ for k := range params {
+ paramKeys = append(paramKeys, k)
+ }
+ sort.Strings(paramKeys)
+ for _, k := range paramKeys {
+ key += k + "=" + params[k]
}
h := sha256.Sum256([]byte(key))
return hex.EncodeToString(h[:8])
← 9f2a4efe fix(cli): default Surf browser transport to h2 (#850)
·
back to Cli Printing Press
·
fix(skills): correct update preflight compatibility (#854) 4b29a634 →