LoadStore reads the credentials file and returns a usable CredentialStore. Returns an empty (but non-nil) store if the file doesn't exist — that's not an error, just "nothing logged in anywhere." Migrates v1 single-blob format to v2 in memory; v1 files stay v1 on disk until the first Save (which alw
()
| 94 | // callers that need a single server's entry should call |
| 95 | // LoadStore().Get(url). |
| 96 | func LoadStore() (*CredentialStore, error) { |
| 97 | path, err := credentialsPath() |
| 98 | if err != nil { |
| 99 | return nil, err |
| 100 | } |
| 101 | |
| 102 | data, err := os.ReadFile(path) |
| 103 | if err != nil { |
| 104 | if os.IsNotExist(err) { |
| 105 | return newEmptyStore(), nil |
| 106 | } |
| 107 | return nil, fmt.Errorf("read credentials: %w", err) |
| 108 | } |
| 109 | |
| 110 | if len(strings.TrimSpace(string(data))) == 0 { |
| 111 | return newEmptyStore(), nil |
| 112 | } |
| 113 | |
| 114 | // Detect format. v1 has a top-level `token` string; v2 has a |
| 115 | // top-level `credentials` object. Probe with a generic map so a |
| 116 | // garbage file fails loudly here rather than silently parsing as |
| 117 | // "empty v2." |
| 118 | var probe map[string]json.RawMessage |
| 119 | if err := json.Unmarshal(data, &probe); err != nil { |
| 120 | return nil, fmt.Errorf("parse credentials: %w", err) |
| 121 | } |
| 122 | if _, hasToken := probe["token"]; hasToken { |
| 123 | return migrateV1(data) |
| 124 | } |
| 125 | return parseV2(data) |
| 126 | } |
| 127 | |
| 128 | // migrateV1 reads a v1 single-blob file and returns an in-memory v2 store |
| 129 | // keyed by the legacy ServerURL. A v1 file with an empty server_url is |