@@ -9,31 +9,47 @@ export class CredentialStorage {
99 private readonly serviceName = 'deploystack-gateway' ;
1010 private readonly fallbackDir = join ( homedir ( ) , '.deploystack' ) ;
1111 private readonly fallbackFile = join ( this . fallbackDir , 'credentials.enc' ) ;
12+ private readonly accountsFile = join ( this . fallbackDir , 'accounts.json' ) ;
1213 private readonly encryptionKey = 'deploystack-gateway-key' ;
1314
1415 /**
1516 * Store credentials securely using OS keychain with encrypted file fallback
1617 * @param credentials Credentials to store
1718 */
1819 async storeCredentials ( credentials : StoredCredentials ) : Promise < void > {
20+ let keychainError : Error | null = null ;
21+
1922 try {
2023 // Try OS keychain first
2124 await keyring . setPassword (
2225 this . serviceName ,
2326 credentials . userEmail ,
2427 JSON . stringify ( credentials )
2528 ) ;
29+
30+ // Also maintain a list of accounts for retrieval
31+ await this . addToAccountsList ( credentials . userEmail ) ;
32+
33+ console . log ( '✓ Credentials stored in OS keychain' ) ;
34+ return ; // Success, no need for fallback
2635 } catch ( error ) {
27- // Fallback to encrypted file storage
28- try {
29- await this . storeEncrypted ( credentials ) ;
30- } catch ( fallbackError ) {
31- throw new AuthenticationError (
32- AuthError . STORAGE_ERROR ,
33- 'Failed to store credentials securely' ,
34- fallbackError as Error
35- ) ;
36- }
36+ keychainError = error as Error ;
37+ console . log ( '⚠ Keychain storage failed, trying encrypted file fallback...' ) ;
38+ }
39+
40+ // Fallback to encrypted file storage
41+ try {
42+ await this . storeEncrypted ( credentials ) ;
43+ console . log ( '✓ Credentials stored in encrypted file' ) ;
44+ } catch ( fallbackError ) {
45+ console . error ( '❌ Both keychain and file storage failed:' ) ;
46+ console . error ( 'Keychain error:' , keychainError ?. message ) ;
47+ console . error ( 'File error:' , ( fallbackError as Error ) ?. message ) ;
48+ throw new AuthenticationError (
49+ AuthError . STORAGE_ERROR ,
50+ 'Failed to store credentials securely' ,
51+ fallbackError as Error
52+ ) ;
3753 }
3854 }
3955
@@ -42,21 +58,52 @@ export class CredentialStorage {
4258 * @returns Stored credentials or null if not found
4359 */
4460 async getCredentials ( ) : Promise < StoredCredentials | null > {
61+ console . log ( '🔍 Attempting to retrieve stored credentials...' ) ;
62+
63+ // First try encrypted file (more reliable for single-user scenario)
4564 try {
46- // Try to get all stored credentials and find the most recent one
47- const accounts = await this . getStoredAccounts ( ) ;
48- if ( accounts . length === 0 ) {
49- return await this . retrieveEncrypted ( ) ;
65+ console . log ( '📁 Checking encrypted file:' , this . fallbackFile ) ;
66+ const encryptedCredentials = await this . retrieveEncrypted ( ) ;
67+ if ( encryptedCredentials ) {
68+ console . log ( '✓ Found credentials in encrypted file' ) ;
69+ return encryptedCredentials ;
70+ } else {
71+ console . log ( '⚠ No credentials found in encrypted file' ) ;
5072 }
73+ } catch ( error ) {
74+ console . log ( '❌ Error reading encrypted file:' , ( error as Error ) ?. message ) ;
75+ }
5176
52- // Get the most recently stored credentials
53- const mostRecent = accounts [ 0 ] ;
54- const stored = await keyring . getPassword ( this . serviceName , mostRecent ) ;
55- return stored ? JSON . parse ( stored ) : await this . retrieveEncrypted ( ) ;
77+ // Fallback to keychain
78+ try {
79+ console . log ( '🔑 Checking OS keychain...' ) ;
80+ const accounts = await this . getStoredAccounts ( ) ;
81+ console . log ( '📋 Found accounts:' , accounts ) ;
82+
83+ if ( accounts . length > 0 ) {
84+ // Try each account until we find valid credentials
85+ for ( const account of accounts ) {
86+ try {
87+ const stored = await keyring . getPassword ( this . serviceName , account ) ;
88+ if ( stored ) {
89+ const credentials = JSON . parse ( stored ) ;
90+ console . log ( '✓ Found credentials in OS keychain for:' , account ) ;
91+ return credentials ;
92+ }
93+ } catch ( error ) {
94+ console . log ( '⚠ Failed to retrieve credentials for account:' , account ) ;
95+ continue ;
96+ }
97+ }
98+ } else {
99+ console . log ( '⚠ No accounts found in keychain' ) ;
100+ }
56101 } catch ( error ) {
57- // Fallback to encrypted file
58- return await this . retrieveEncrypted ( ) ;
102+ console . log ( '❌ Error accessing keychain:' , ( error as Error ) ?. message ) ;
59103 }
104+
105+ console . log ( '❌ No credentials found in any storage method' ) ;
106+ return null ;
60107 }
61108
62109 /**
@@ -67,6 +114,7 @@ export class CredentialStorage {
67114 try {
68115 if ( userEmail ) {
69116 await keyring . deletePassword ( this . serviceName , userEmail ) ;
117+ await this . removeFromAccountsList ( userEmail ) ;
70118 } else {
71119 // Clear all stored accounts
72120 const accounts = await this . getStoredAccounts ( ) ;
@@ -77,6 +125,7 @@ export class CredentialStorage {
77125 // Continue clearing other accounts even if one fails
78126 }
79127 }
128+ await this . clearAccountsList ( ) ;
80129 }
81130 } catch ( error ) {
82131 // Continue to clear encrypted file even if keychain fails
@@ -114,11 +163,68 @@ export class CredentialStorage {
114163 */
115164 private async getStoredAccounts ( ) : Promise < string [ ] > {
116165 try {
117- // This is a simplified approach - in a real implementation,
118- // you might want to maintain a separate list of accounts
119- return [ ] ;
166+ if ( existsSync ( this . accountsFile ) ) {
167+ const data = readFileSync ( this . accountsFile , 'utf8' ) ;
168+ const accounts = JSON . parse ( data ) ;
169+ return Array . isArray ( accounts ) ? accounts : [ ] ;
170+ }
171+ } catch ( error ) {
172+ // If we can't read the accounts file, return empty array
173+ }
174+ return [ ] ;
175+ }
176+
177+ /**
178+ * Add account to the accounts list
179+ * @param email User email to add
180+ */
181+ private async addToAccountsList ( email : string ) : Promise < void > {
182+ try {
183+ // Ensure directory exists
184+ const { mkdirSync } = await import ( 'fs' ) ;
185+ try {
186+ mkdirSync ( this . fallbackDir , { recursive : true } ) ;
187+ } catch ( error ) {
188+ // Directory might already exist
189+ }
190+
191+ const accounts = await this . getStoredAccounts ( ) ;
192+ if ( ! accounts . includes ( email ) ) {
193+ accounts . unshift ( email ) ; // Add to beginning (most recent first)
194+ writeFileSync ( this . accountsFile , JSON . stringify ( accounts , null , 2 ) ) ;
195+ }
196+ } catch ( error ) {
197+ // Non-critical error, don't throw
198+ console . log ( '⚠ Failed to update accounts list:' , ( error as Error ) ?. message ) ;
199+ }
200+ }
201+
202+ /**
203+ * Remove account from the accounts list
204+ * @param email User email to remove
205+ */
206+ private async removeFromAccountsList ( email : string ) : Promise < void > {
207+ try {
208+ const accounts = await this . getStoredAccounts ( ) ;
209+ const filtered = accounts . filter ( account => account !== email ) ;
210+ if ( filtered . length !== accounts . length ) {
211+ writeFileSync ( this . accountsFile , JSON . stringify ( filtered , null , 2 ) ) ;
212+ }
213+ } catch ( error ) {
214+ // Non-critical error, don't throw
215+ }
216+ }
217+
218+ /**
219+ * Clear the accounts list
220+ */
221+ private async clearAccountsList ( ) : Promise < void > {
222+ try {
223+ if ( existsSync ( this . accountsFile ) ) {
224+ unlinkSync ( this . accountsFile ) ;
225+ }
120226 } catch ( error ) {
121- return [ ] ;
227+ // Non-critical error, don't throw
122228 }
123229 }
124230
0 commit comments