11import { chmod , mkdir , readFile , rm , stat , writeFile } from 'node:fs/promises' ;
22import { join } from 'node:path' ;
3- import { xdg } from '../utils/xdg' ;
43import { AUTH_ENV_VARS , getAuth0Config } from './config' ;
54import { AuthError } from './errors' ;
6- import { type Credentials , createCredentials , validateCredentials } from './schema' ;
5+ import {
6+ type ClixCredentials ,
7+ CREDENTIALS_VERSION ,
8+ type Credentials ,
9+ createClixCredentials ,
10+ type FirebaseTokens ,
11+ validateCredentials ,
12+ } from './schema' ;
713import type { TokenResponse } from './types' ;
814
915/**
@@ -15,9 +21,13 @@ const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
1521/**
1622 * CredentialsManager handles storing, loading, and refreshing auth credentials.
1723 *
18- * Storage location: $XDG_STATE_HOME/ clix/credentials.json
24+ * Storage location: project/. clix/credentials.json
1925 * File permissions: 0600 (owner read/write only)
2026 *
27+ * Unified credentials file structure:
28+ * - clix: Auth0/Clix authentication tokens
29+ * - firebase: Firebase OAuth tokens
30+ *
2131 * @example
2232 * ```typescript
2333 * const manager = getCredentialsManager();
@@ -33,7 +43,7 @@ export class CredentialsManager {
3343 private credentialsFilePath : string ;
3444
3545 constructor ( customStateDir ?: string ) {
36- this . stateDirPath = customStateDir ?? xdg . state ( ) ;
46+ this . stateDirPath = customStateDir ?? join ( process . cwd ( ) , '.clix' ) ;
3747 this . credentialsFilePath = join ( this . stateDirPath , 'credentials.json' ) ;
3848 }
3949
@@ -130,14 +140,53 @@ export class CredentialsManager {
130140 }
131141 }
132142
143+ // ============================================
144+ // Clix (Auth0) Credentials Methods
145+ // ============================================
146+
147+ /**
148+ * Get Clix credentials from unified store.
149+ */
150+ async getClixCredentials ( ) : Promise < ClixCredentials | null > {
151+ const credentials = await this . load ( ) ;
152+ return credentials ?. clix ?? null ;
153+ }
154+
133155 /**
134- * Check if access token is expired.
156+ * Save Clix credentials to unified store.
157+ */
158+ async saveClixCredentials ( clixCredentials : ClixCredentials ) : Promise < void > {
159+ const current = ( await this . load ( ) ) ?? { version : CREDENTIALS_VERSION } ;
160+ await this . save ( {
161+ ...current ,
162+ version : CREDENTIALS_VERSION ,
163+ clix : clixCredentials ,
164+ } ) ;
165+ }
166+
167+ /**
168+ * Clear only Clix credentials (keep Firebase tokens).
169+ */
170+ async clearClixCredentials ( ) : Promise < void > {
171+ const current = await this . load ( ) ;
172+ if ( current ) {
173+ const { clix : _ , ...rest } = current ;
174+ if ( rest . firebase ) {
175+ await this . save ( { ...rest , version : CREDENTIALS_VERSION } ) ;
176+ } else {
177+ await this . delete ( ) ;
178+ }
179+ }
180+ }
181+
182+ /**
183+ * Check if Clix access token is expired.
135184 *
136- * @param credentials - Credentials to check
185+ * @param clixCredentials - Clix credentials to check
137186 * @returns true if expired or about to expire
138187 */
139- isExpired ( credentials : Credentials ) : boolean {
140- const expiresAtMs = Date . parse ( credentials . expiresAt ) ;
188+ isClixExpired ( clixCredentials : ClixCredentials ) : boolean {
189+ const expiresAtMs = Date . parse ( clixCredentials . expiresAt ) ;
141190 // Treat invalid dates as expired (secure default)
142191 if ( ! Number . isFinite ( expiresAtMs ) ) {
143192 return true ;
@@ -146,15 +195,23 @@ export class CredentialsManager {
146195 return expiresAtMs - EXPIRY_BUFFER_MS <= Date . now ( ) ;
147196 }
148197
198+ /**
199+ * @deprecated Use isClixExpired instead.
200+ */
201+ isExpired ( credentials : Credentials ) : boolean {
202+ if ( ! credentials . clix ) return true ;
203+ return this . isClixExpired ( credentials . clix ) ;
204+ }
205+
149206 /**
150207 * Refresh access token using refresh token.
151208 *
152- * @param credentials - Current credentials with refresh token
153- * @returns New credentials with fresh access token
209+ * @param clixCredentials - Current Clix credentials with refresh token
210+ * @returns New Clix credentials with fresh access token
154211 * @throws AuthError if refresh fails
155212 */
156- async refreshAccessToken ( credentials : Credentials ) : Promise < Credentials > {
157- if ( ! credentials . refreshToken ) {
213+ async refreshAccessToken ( clixCredentials : ClixCredentials ) : Promise < ClixCredentials > {
214+ if ( ! clixCredentials . refreshToken ) {
158215 throw AuthError . refreshFailed ( 'No refresh token available' ) ;
159216 }
160217
@@ -170,7 +227,7 @@ export class CredentialsManager {
170227 body : new URLSearchParams ( {
171228 grant_type : 'refresh_token' ,
172229 client_id : config . clientId ,
173- refresh_token : credentials . refreshToken ,
230+ refresh_token : clixCredentials . refreshToken ,
174231 } ) ,
175232 signal : AbortSignal . timeout ( 30_000 ) ,
176233 } ) ;
@@ -188,20 +245,20 @@ export class CredentialsManager {
188245 // Preserve existing refresh token if response omits it (RFC 6749 compliant)
189246 const mergedTokenResponse : TokenResponse = {
190247 ...tokenResponse ,
191- refresh_token : tokenResponse . refresh_token ?? credentials . refreshToken ,
248+ refresh_token : tokenResponse . refresh_token ?? clixCredentials . refreshToken ,
192249 } ;
193250
194251 // Create new credentials with refreshed tokens
195- const newCredentials = createCredentials (
252+ const newClixCredentials = createClixCredentials (
196253 mergedTokenResponse ,
197- credentials . issuer ,
198- credentials . audience ,
254+ clixCredentials . issuer ,
255+ clixCredentials . audience ,
199256 ) ;
200257
201258 // Save updated credentials
202- await this . save ( newCredentials ) ;
259+ await this . saveClixCredentials ( newClixCredentials ) ;
203260
204- return newCredentials ;
261+ return newClixCredentials ;
205262 } catch ( error ) {
206263 if ( error instanceof AuthError ) {
207264 throw error ;
@@ -233,21 +290,21 @@ export class CredentialsManager {
233290 return envToken ;
234291 }
235292
236- // 2. Load stored credentials
237- const credentials = await this . load ( ) ;
238- if ( ! credentials ) {
293+ // 2. Load stored Clix credentials
294+ const clixCredentials = await this . getClixCredentials ( ) ;
295+ if ( ! clixCredentials ) {
239296 return null ;
240297 }
241298
242299 // 3. Check if access token is expired
243- if ( ! this . isExpired ( credentials ) ) {
244- return credentials . accessToken ;
300+ if ( ! this . isClixExpired ( clixCredentials ) ) {
301+ return clixCredentials . accessToken ;
245302 }
246303
247304 // 4. Try to refresh if we have a refresh token
248- if ( credentials . refreshToken ) {
305+ if ( clixCredentials . refreshToken ) {
249306 try {
250- const refreshed = await this . refreshAccessToken ( credentials ) ;
307+ const refreshed = await this . refreshAccessToken ( clixCredentials ) ;
251308 return refreshed . accessToken ;
252309 } catch {
253310 // Refresh failed - return null to indicate re-login needed
@@ -278,6 +335,81 @@ export class CredentialsManager {
278335 return ! ! process . env [ AUTH_ENV_VARS . ACCESS_TOKEN ] ;
279336 }
280337
338+ // ============================================
339+ // Firebase Token Methods
340+ // ============================================
341+
342+ /**
343+ * Get Firebase tokens from unified store.
344+ */
345+ async getFirebaseTokens ( ) : Promise < FirebaseTokens | null > {
346+ const credentials = await this . load ( ) ;
347+ return credentials ?. firebase ?? null ;
348+ }
349+
350+ /**
351+ * Save Firebase tokens to unified store.
352+ */
353+ async saveFirebaseTokens ( firebaseTokens : FirebaseTokens ) : Promise < void > {
354+ const current = ( await this . load ( ) ) ?? { version : CREDENTIALS_VERSION } ;
355+ await this . save ( {
356+ ...current ,
357+ version : CREDENTIALS_VERSION ,
358+ firebase : firebaseTokens ,
359+ } ) ;
360+ }
361+
362+ /**
363+ * Clear only Firebase tokens (keep Clix credentials).
364+ */
365+ async clearFirebaseTokens ( ) : Promise < void > {
366+ const current = await this . load ( ) ;
367+ if ( current ) {
368+ const { firebase : _ , ...rest } = current ;
369+ if ( rest . clix ) {
370+ await this . save ( { ...rest , version : CREDENTIALS_VERSION } ) ;
371+ } else {
372+ await this . delete ( ) ;
373+ }
374+ }
375+ }
376+
377+ /**
378+ * Check if Firebase tokens exist.
379+ */
380+ async hasFirebaseTokens ( ) : Promise < boolean > {
381+ const tokens = await this . getFirebaseTokens ( ) ;
382+ return tokens !== null ;
383+ }
384+
385+ /**
386+ * Check if Firebase tokens are expired.
387+ *
388+ * @param tokens - Firebase tokens to check
389+ * @returns true if tokens are expired or will expire within 5 minutes
390+ */
391+ isFirebaseExpired ( tokens : FirebaseTokens ) : boolean {
392+ if ( ! tokens . expiry_date ) {
393+ return false ; // No expiry info, assume valid
394+ }
395+ // Consider expired if less than 5 minutes remaining
396+ return Date . now ( ) >= tokens . expiry_date - EXPIRY_BUFFER_MS ;
397+ }
398+
399+ /**
400+ * Check if Firebase tokens have a valid refresh token.
401+ *
402+ * @param tokens - Firebase tokens to check
403+ * @returns true if refresh token exists
404+ */
405+ hasFirebaseRefreshToken ( tokens : FirebaseTokens ) : boolean {
406+ return ! ! tokens . refresh_token ;
407+ }
408+
409+ // ============================================
410+ // Utility Methods
411+ // ============================================
412+
281413 /**
282414 * Clear the cached credentials (useful for testing).
283415 */
0 commit comments