Skip to content

Commit cba837a

Browse files
appleboyclaude
andauthored
feat(credentials): add secure credential storage for API keys (#263)
* feat(credentials): add secure credential storage for API keys - Introduce a secure credential store with OS keyring support and file fallback for managing API keys - Migrate sensitive API keys out of plaintext config files into the secure store during config initialization - Route config set operations for sensitive keys to the secure store and ensure they are removed from YAML - Enhance config list output to reflect where sensitive credentials are stored or if they remain unmigrated - Update all provider initializations to retrieve API keys from the secure store with fallback to existing config sources - Add comprehensive tests covering secure credential storage, retrieval, deletion, and missing-key behavior - Add and update dependencies required for secure credential storage and platform support Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> * fix(credentials): address PR review comments - Guard migration with viper.InConfig() to avoid persisting env vars - Handle os.UserHomeDir() error with TempDir fallback - Create fallback credential directory before constructing file store - Use viper.InConfig() in config list to detect YAML-stored keys - Surface GetCredential errors in config list instead of silent failure - Use filepath.Join in tests for cross-platform path safety Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 5aa0e69 commit cba837a

File tree

8 files changed

+325
-12
lines changed

8 files changed

+325
-12
lines changed

cmd/cmd.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"path"
99
"strings"
1010

11+
"github.com/appleboy/CodeGPT/util"
12+
1113
"github.com/appleboy/com/file"
1214
"github.com/spf13/cobra"
1315
"github.com/spf13/viper"
@@ -31,6 +33,46 @@ const (
3133
DRONE = "drone"
3234
)
3335

36+
// sensitiveConfigKeys lists the config keys that should be stored in the
37+
// secure credential store rather than in the plaintext YAML config file.
38+
var sensitiveConfigKeys = []string{"openai.api_key", "gemini.api_key"}
39+
40+
// migrateCredentialsToStore moves any plaintext API keys found in the YAML
41+
// config into the secure credential store and clears them from the config file.
42+
func migrateCredentialsToStore() {
43+
for _, key := range sensitiveConfigKeys {
44+
// Only migrate values that actually exist in the config file.
45+
// This prevents env vars (e.g. OPENAI_API_KEY) from being silently
46+
// persisted into the credential store.
47+
if !viper.InConfig(key) {
48+
continue
49+
}
50+
val := viper.GetString(key)
51+
if val == "" {
52+
continue
53+
}
54+
// Check if already in credstore; skip if already migrated.
55+
existing, err := util.GetCredential(key)
56+
if err != nil || existing != "" {
57+
continue
58+
}
59+
if err := util.SetCredential(key, val); err != nil {
60+
fmt.Fprintf(os.Stderr, "warning: could not migrate %s to secure store: %v\n", key, err)
61+
continue
62+
}
63+
// Remove from YAML.
64+
viper.Set(key, "")
65+
if err := viper.WriteConfig(); err != nil {
66+
fmt.Fprintf(
67+
os.Stderr,
68+
"warning: could not update config after migrating %s: %v\n",
69+
key,
70+
err,
71+
)
72+
}
73+
}
74+
}
75+
3476
func init() {
3577
cobra.OnInitialize(initConfig)
3678

@@ -124,6 +166,9 @@ func initConfig() {
124166
}
125167
}
126168

169+
// Auto-migrate plaintext API keys to secure store.
170+
migrateCredentialsToStore()
171+
127172
switch {
128173
case promptFolder != "":
129174
// If a prompt folder is specified by the promptFolder variable,

cmd/config_list.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package cmd
22

33
import (
4+
"slices"
45
"sort"
56

7+
"github.com/appleboy/CodeGPT/util"
8+
69
"github.com/fatih/color"
710
"github.com/rodaine/table"
811
"github.com/spf13/cobra"
@@ -73,9 +76,24 @@ var configListCmd = &cobra.Command{
7376

7477
// Add the key and value to the table
7578
for _, v := range keys {
76-
// Hide the api key
77-
if v == "openai.api_key" || v == "gemini.api_key" {
78-
tbl.AddRow(v, "****************")
79+
if slices.Contains(sensitiveConfigKeys, v) {
80+
cred, err := util.GetCredential(v)
81+
if err != nil {
82+
tbl.AddRow(v, "(error reading secure store)")
83+
continue
84+
}
85+
switch {
86+
case cred != "":
87+
if util.CredStoreIsKeyring() {
88+
tbl.AddRow(v, "(stored in keyring)")
89+
} else {
90+
tbl.AddRow(v, "(stored in secure file)")
91+
}
92+
case viper.InConfig(v):
93+
tbl.AddRow(v, "**************** (YAML — run config set to migrate)")
94+
default:
95+
tbl.AddRow(v, "(not set)")
96+
}
7997
continue
8098
}
8199
tbl.AddRow(v, viper.Get(v))

cmd/config_set.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package cmd
22

33
import (
44
"errors"
5+
"fmt"
56
"strings"
67

8+
"github.com/appleboy/CodeGPT/util"
9+
710
"github.com/fatih/color"
811
"github.com/spf13/cobra"
912
"github.com/spf13/viper"
@@ -103,6 +106,22 @@ var configSetCmd = &cobra.Command{
103106
)
104107
}
105108

109+
// Sensitive keys go to secure store, not YAML.
110+
for _, sensitiveKey := range sensitiveConfigKeys {
111+
if args[0] == sensitiveKey {
112+
if err := util.SetCredential(args[0], args[1]); err != nil {
113+
return fmt.Errorf("failed to store credential in secure store: %w", err)
114+
}
115+
// Ensure the key is cleared from YAML.
116+
viper.Set(args[0], "")
117+
if err := viper.WriteConfig(); err != nil {
118+
return err
119+
}
120+
color.Green("you can see the config file: %s", viper.ConfigFileUsed())
121+
return nil
122+
}
123+
}
124+
106125
// Set config value in viper
107126
if args[0] == "git.exclude_list" {
108127
viper.Set(args[0], strings.Split(args[1], ","))

cmd/provider.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ import (
1414
"github.com/spf13/viper"
1515
)
1616

17+
// getAPIKey retrieves an API key from the secure credential store first,
18+
// then falls back to viper (env vars or legacy YAML).
19+
func getAPIKey(viperKey string) (string, error) {
20+
val, err := util.GetCredential(viperKey)
21+
if err != nil {
22+
return "", err
23+
}
24+
if val != "" {
25+
return val, nil
26+
}
27+
// Fallback: env var or legacy YAML (not yet migrated).
28+
return viper.GetString(viperKey), nil
29+
}
30+
1731
func NewOpenAI(ctx context.Context) (*openai.Client, error) {
1832
var apiKey string
1933

@@ -35,7 +49,11 @@ func NewOpenAI(ctx context.Context) (*openai.Client, error) {
3549
}
3650
apiKey = key
3751
} else {
38-
apiKey = viper.GetString("openai.api_key")
52+
key, err := getAPIKey("openai.api_key")
53+
if err != nil {
54+
return nil, err
55+
}
56+
apiKey = key
3957
}
4058

4159
return openai.New(
@@ -81,7 +99,11 @@ func NewGemini(ctx context.Context) (*gemini.Client, error) {
8199
apiKey = key
82100
} else {
83101
// Fallback to static config: gemini.api_key -> openai.api_key
84-
apiKey = viper.GetString("gemini.api_key")
102+
key, err := getAPIKey("gemini.api_key")
103+
if err != nil {
104+
return nil, err
105+
}
106+
apiKey = key
85107
if apiKey == "" {
86108
// Try openai.api_key_helper as fallback
87109
if helper := viper.GetString("openai.api_key_helper"); helper != "" {
@@ -95,13 +117,17 @@ func NewGemini(ctx context.Context) (*gemini.Client, error) {
95117
// Not set, use default
96118
refreshInterval = util.DefaultRefreshInterval
97119
}
98-
key, err := util.GetAPIKeyFromHelperWithCache(ctx, helper, refreshInterval)
120+
helperKey, err := util.GetAPIKeyFromHelperWithCache(ctx, helper, refreshInterval)
99121
if err != nil {
100122
return nil, err
101123
}
102-
apiKey = key
124+
apiKey = helperKey
103125
} else {
104-
apiKey = viper.GetString("openai.api_key")
126+
openaiKey, err := getAPIKey("openai.api_key")
127+
if err != nil {
128+
return nil, err
129+
}
130+
apiKey = openaiKey
105131
}
106132
}
107133
}
@@ -150,7 +176,11 @@ func NewAnthropic(ctx context.Context) (*anthropic.Client, error) {
150176
}
151177
apiKey = key
152178
} else {
153-
apiKey = viper.GetString("openai.api_key")
179+
key, err := getAPIKey("openai.api_key")
180+
if err != nil {
181+
return nil, err
182+
}
183+
apiKey = key
154184
}
155185

156186
return anthropic.New(

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/charmbracelet/bubbletea v1.3.10
1010
github.com/erikgeiser/promptkit v0.9.0
1111
github.com/fatih/color v1.18.0
12+
github.com/go-authgate/sdk-go v0.2.0
1213
github.com/joho/godotenv v1.5.1
1314
github.com/liushuangls/go-anthropic/v2 v2.17.1
1415
github.com/rodaine/table v1.3.0
@@ -17,11 +18,12 @@ require (
1718
github.com/spf13/viper v1.21.0
1819
github.com/yassinebenaid/godump v0.11.1
1920
golang.org/x/net v0.51.0
20-
golang.org/x/sys v0.41.0
21+
golang.org/x/sys v0.42.0
2122
google.golang.org/genai v1.49.0
2223
)
2324

2425
require (
26+
al.essio.dev/pkg/shellescape v1.6.0 // indirect
2527
cloud.google.com/go v0.123.0 // indirect
2628
cloud.google.com/go/auth v0.18.2 // indirect
2729
cloud.google.com/go/compute/metadata v0.9.0 // indirect
@@ -35,12 +37,14 @@ require (
3537
github.com/charmbracelet/x/term v0.2.2 // indirect
3638
github.com/clipperhouse/displaywidth v0.11.0 // indirect
3739
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
40+
github.com/danieljoos/wincred v1.2.3 // indirect
3841
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
3942
github.com/felixge/httpsnoop v1.0.4 // indirect
4043
github.com/fsnotify/fsnotify v1.9.0 // indirect
4144
github.com/go-logr/logr v1.4.3 // indirect
4245
github.com/go-logr/stdr v1.2.2 // indirect
4346
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
47+
github.com/godbus/dbus/v5 v5.2.2 // indirect
4448
github.com/google/go-cmp v0.7.0 // indirect
4549
github.com/google/s2a-go v0.1.9 // indirect
4650
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
@@ -64,6 +68,7 @@ require (
6468
github.com/spf13/pflag v1.0.10 // indirect
6569
github.com/subosito/gotenv v1.6.0 // indirect
6670
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
71+
github.com/zalando/go-keyring v0.2.6 // indirect
6772
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
6873
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
6974
go.opentelemetry.io/otel v1.42.0 // indirect

go.sum

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
2+
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
13
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
24
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
35
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
@@ -37,6 +39,8 @@ github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3
3739
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
3840
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
3941
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
42+
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
43+
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
4044
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4145
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4246
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -52,20 +56,26 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
5256
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
5357
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
5458
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
59+
github.com/go-authgate/sdk-go v0.2.0 h1:w22f+sAg/YMqnLOcS/4SAuMZXTbPurzkSQBsjb1hcbw=
60+
github.com/go-authgate/sdk-go v0.2.0/go.mod h1:RGqvrFdrPnOumndoQQV8qzu8zP1KFUZPdhX0IkWduho=
5561
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
5662
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
5763
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
5864
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
5965
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
6066
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
6167
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
68+
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
69+
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
6270
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
6371
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
6472
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
6573
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
6674
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
6775
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
6876
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
77+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
78+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
6979
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
7080
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7181
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
@@ -135,6 +145,7 @@ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjb
135145
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
136146
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
137147
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
148+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
138149
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
139150
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
140151
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -148,6 +159,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
148159
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
149160
github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI=
150161
github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44=
162+
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
163+
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
151164
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
152165
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
153166
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
@@ -174,8 +187,8 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
174187
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
175188
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
176189
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
177-
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
178-
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
190+
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
191+
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
179192
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
180193
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
181194
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=

util/credstore.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package util
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/go-authgate/sdk-go/credstore"
8+
)
9+
10+
const credServiceName = "codegpt"
11+
12+
// credStore is the singleton SecureStore[string] instance.
13+
// Initialized once; uses OS keyring with file-based fallback.
14+
var credStore *credstore.SecureStore[string]
15+
16+
func init() {
17+
home, err := os.UserHomeDir()
18+
var fallbackPath string
19+
if err != nil || home == "" {
20+
fallbackPath = filepath.Join(os.TempDir(), "codegpt", "credentials.json")
21+
} else {
22+
fallbackPath = filepath.Join(home, ".config", "codegpt", ".cache", "credentials.json")
23+
}
24+
25+
// Ensure the directory for the fallback credential file exists.
26+
dir := filepath.Dir(fallbackPath)
27+
_ = os.MkdirAll(dir, 0o700)
28+
29+
keyring := credstore.NewStringKeyringStore(credServiceName)
30+
file := credstore.NewStringFileStore(fallbackPath)
31+
credStore = credstore.NewSecureStore(keyring, file)
32+
}
33+
34+
// GetCredential retrieves a stored credential by key.
35+
// Returns ("", nil) if not found.
36+
func GetCredential(key string) (string, error) {
37+
val, err := credStore.Load(key)
38+
if err == credstore.ErrNotFound {
39+
return "", nil
40+
}
41+
return val, err
42+
}
43+
44+
// SetCredential stores a credential by key.
45+
func SetCredential(key, value string) error {
46+
return credStore.Save(key, value)
47+
}
48+
49+
// DeleteCredential removes a credential by key.
50+
func DeleteCredential(key string) error {
51+
return credStore.Delete(key)
52+
}
53+
54+
// CredStoreIsKeyring reports whether the active backend is the OS keyring.
55+
func CredStoreIsKeyring() bool {
56+
return credStore.UseKeyring()
57+
}

0 commit comments

Comments
 (0)