diff --git a/AGENTS.md b/AGENTS.md index 5551a46..13dddb9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -259,5 +259,7 @@ When adding new OAuth flows, use port 9005, path `/auth/callback`, the shared `O ## Security -Do not commit API keys or user data. Local config lives in `$XDG_CONFIG_HOME/clix/config.json` (default: `~/.config/clix/config.json`). -Sessions are stored in `$XDG_STATE_HOME/clix/sessions/` (default: `~/.local/state/clix/sessions/`). +Do not commit API keys or user data. Global config lives in `$XDG_CONFIG_HOME/clix/config.json` (default: `~/.config/clix/config.json`). +Project-local data is stored in `project/.clix/` directory: +- Sessions: `project/.clix/sessions/` +- Credentials: `project/.clix/credentials.json` (unified: Clix Auth + Firebase tokens) diff --git a/README.md b/README.md index 138a028..ac12624 100644 --- a/README.md +++ b/README.md @@ -341,10 +341,10 @@ Transfer your conversation to continue in the native agent CLI: ``` > /transfer claude -✅ Session saved to ~/.local/state/clix/session-1234567890.md +✅ Session saved to .clix/sessions/session-1234567890.md To continue in Claude Code: -claude "$(cat ~/.local/state/clix/session-1234567890.md)" +claude "$(cat .clix/sessions/session-1234567890.md)" ``` This preserves your entire conversation history and allows you to continue seamlessly in the agent's native interface. diff --git a/llms.txt b/llms.txt index 891a04b..7ab8cf0 100644 --- a/llms.txt +++ b/llms.txt @@ -61,7 +61,7 @@ clix ``` **What happens:** -1. Loads configuration from `~/.clix/config.json` +1. Loads configuration from `~/.config/clix/config.json` 2. Detects available AI agents (Claude Code, Codex) 3. Prompts for agent selection if not configured or multiple available 4. Initializes executor with selected agent @@ -701,7 +701,7 @@ Switching to Claude... - `/transfer gemini` - Transfers to Gemini **Process:** -1. Conversation history saved to `~/.local/state/clix/sessions/session-{timestamp}.md` +1. Conversation history saved to `.clix/sessions/session-{timestamp}.md` 2. Markdown formatted with full history 3. Command generated to continue in agent CLI 4. User exits and runs provided command @@ -711,11 +711,11 @@ Switching to Claude... > /transfer claude ✅ Session saved! -File: ~/.local/state/clix/sessions/session-1704735000000.md +File: .clix/sessions/session-1704735000000.md To continue in Claude Code: -claude "$(cat ~/.local/state/clix/sessions/session-1704735000000.md)" +claude "$(cat .clix/sessions/session-1704735000000.md)" ``` #### `/resume` - Resume Session @@ -1003,13 +1003,13 @@ Switching to Claude... clix [Having a long conversation] > /transfer claude -✅ Session saved to ~/.local/state/clix/sessions/session-1704735000.md +✅ Session saved to .clix/sessions/session-1704735000.md To continue in Claude Code: -claude "$(cat ~/.local/state/clix/sessions/session-1704735000.md)" +claude "$(cat .clix/sessions/session-1704735000.md)" # Exit and run provided command -$ claude "$(cat ~/.local/state/clix/sessions/session-1704735000.md)" +$ claude "$(cat .clix/sessions/session-1704735000.md)" [Conversation continues in native Claude CLI] ``` @@ -1118,7 +1118,7 @@ interface AgentExecutor { ### Session Files -**Location:** `~/.local/state/clix/sessions/` (or `$XDG_STATE_HOME/clix/sessions/`) +**Location:** `.clix/sessions/` (project-local) **Format:** Markdown with conversation history @@ -1279,11 +1279,11 @@ When helping users with Clix CLI, keep these points in mind: 5. **Autonomous vs Interactive** - Autonomous commands (`/install`, `/doctor`, `/debug`, `/ios-setup`) can run from CLI, Interactive skills (`/integration`, `/event-tracking`, etc.) require chat mode 6. **Skills from package** - Interactive skills from @clix-so/clix-agent-skills package, Autonomous commands are local 7. **/install vs /integration** - `/install` makes changes autonomously, `/integration` provides guided steps -8. **Session transfer** - Saves to `~/.local/state/clix/sessions/`, provides command for native CLI +8. **Session transfer** - Saves to `.clix/sessions/`, provides command for native CLI 9. **Agent switching** - Preserves history when switching between any agents 10. **Context management** - 200K tokens (Claude), auto-compact at 90% 11. **MCP integration** - Built-in installer for Clix MCP Server supporting all agents -12. **XDG paths** - Config in `~/.config/clix/`, sessions in `~/.local/state/clix/sessions/` +12. **Storage paths** - Global config in `~/.config/clix/`, project-local data in `.clix/` (sessions, credentials) **When users ask about:** - "How to use" → Emphasize `clix` command starts chat, use `/help` for commands diff --git a/src/lib/auth/credentials.ts b/src/lib/auth/credentials.ts index 99098cb..1dd842e 100644 --- a/src/lib/auth/credentials.ts +++ b/src/lib/auth/credentials.ts @@ -1,9 +1,16 @@ import { chmod, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { xdg } from '../utils/xdg'; +import { findProjectRoot } from '../utils/path'; import { AUTH_ENV_VARS, getAuth0Config } from './config'; import { AuthError } from './errors'; -import { type Credentials, createCredentials, validateCredentials } from './schema'; +import { + type ClixCredentials, + CREDENTIALS_VERSION, + type Credentials, + createClixCredentials, + type FirebaseTokens, + validateCredentials, +} from './schema'; import type { TokenResponse } from './types'; /** @@ -15,9 +22,13 @@ const EXPIRY_BUFFER_MS = 5 * 60 * 1000; /** * CredentialsManager handles storing, loading, and refreshing auth credentials. * - * Storage location: $XDG_STATE_HOME/clix/credentials.json + * Storage location: project/.clix/credentials.json * File permissions: 0600 (owner read/write only) * + * Unified credentials file structure: + * - clix: Auth0/Clix authentication tokens + * - firebase: Firebase OAuth tokens + * * @example * ```typescript * const manager = getCredentialsManager(); @@ -33,7 +44,7 @@ export class CredentialsManager { private credentialsFilePath: string; constructor(customStateDir?: string) { - this.stateDirPath = customStateDir ?? xdg.state(); + this.stateDirPath = customStateDir ?? join(findProjectRoot(), '.clix'); this.credentialsFilePath = join(this.stateDirPath, 'credentials.json'); } @@ -130,14 +141,53 @@ export class CredentialsManager { } } + // ============================================ + // Clix (Auth0) Credentials Methods + // ============================================ + + /** + * Get Clix credentials from unified store. + */ + async getClixCredentials(): Promise { + const credentials = await this.load(); + return credentials?.clix ?? null; + } + + /** + * Save Clix credentials to unified store. + */ + async saveClixCredentials(clixCredentials: ClixCredentials): Promise { + const current = (await this.load()) ?? { version: CREDENTIALS_VERSION }; + await this.save({ + ...current, + version: CREDENTIALS_VERSION, + clix: clixCredentials, + }); + } + + /** + * Clear only Clix credentials (keep Firebase tokens). + */ + async clearClixCredentials(): Promise { + const current = await this.load(); + if (current) { + const { clix: _, ...rest } = current; + if (rest.firebase) { + await this.save({ ...rest, version: CREDENTIALS_VERSION }); + } else { + await this.delete(); + } + } + } + /** - * Check if access token is expired. + * Check if Clix access token is expired. * - * @param credentials - Credentials to check + * @param clixCredentials - Clix credentials to check * @returns true if expired or about to expire */ - isExpired(credentials: Credentials): boolean { - const expiresAtMs = Date.parse(credentials.expiresAt); + isClixExpired(clixCredentials: ClixCredentials): boolean { + const expiresAtMs = Date.parse(clixCredentials.expiresAt); // Treat invalid dates as expired (secure default) if (!Number.isFinite(expiresAtMs)) { return true; @@ -146,15 +196,23 @@ export class CredentialsManager { return expiresAtMs - EXPIRY_BUFFER_MS <= Date.now(); } + /** + * @deprecated Use isClixExpired instead. + */ + isExpired(credentials: Credentials): boolean { + if (!credentials.clix) return true; + return this.isClixExpired(credentials.clix); + } + /** * Refresh access token using refresh token. * - * @param credentials - Current credentials with refresh token - * @returns New credentials with fresh access token + * @param clixCredentials - Current Clix credentials with refresh token + * @returns New Clix credentials with fresh access token * @throws AuthError if refresh fails */ - async refreshAccessToken(credentials: Credentials): Promise { - if (!credentials.refreshToken) { + async refreshAccessToken(clixCredentials: ClixCredentials): Promise { + if (!clixCredentials.refreshToken) { throw AuthError.refreshFailed('No refresh token available'); } @@ -170,7 +228,7 @@ export class CredentialsManager { body: new URLSearchParams({ grant_type: 'refresh_token', client_id: config.clientId, - refresh_token: credentials.refreshToken, + refresh_token: clixCredentials.refreshToken, }), signal: AbortSignal.timeout(30_000), }); @@ -188,20 +246,20 @@ export class CredentialsManager { // Preserve existing refresh token if response omits it (RFC 6749 compliant) const mergedTokenResponse: TokenResponse = { ...tokenResponse, - refresh_token: tokenResponse.refresh_token ?? credentials.refreshToken, + refresh_token: tokenResponse.refresh_token ?? clixCredentials.refreshToken, }; // Create new credentials with refreshed tokens - const newCredentials = createCredentials( + const newClixCredentials = createClixCredentials( mergedTokenResponse, - credentials.issuer, - credentials.audience, + clixCredentials.issuer, + clixCredentials.audience, ); // Save updated credentials - await this.save(newCredentials); + await this.saveClixCredentials(newClixCredentials); - return newCredentials; + return newClixCredentials; } catch (error) { if (error instanceof AuthError) { throw error; @@ -233,21 +291,21 @@ export class CredentialsManager { return envToken; } - // 2. Load stored credentials - const credentials = await this.load(); - if (!credentials) { + // 2. Load stored Clix credentials + const clixCredentials = await this.getClixCredentials(); + if (!clixCredentials) { return null; } // 3. Check if access token is expired - if (!this.isExpired(credentials)) { - return credentials.accessToken; + if (!this.isClixExpired(clixCredentials)) { + return clixCredentials.accessToken; } // 4. Try to refresh if we have a refresh token - if (credentials.refreshToken) { + if (clixCredentials.refreshToken) { try { - const refreshed = await this.refreshAccessToken(credentials); + const refreshed = await this.refreshAccessToken(clixCredentials); return refreshed.accessToken; } catch { // Refresh failed - return null to indicate re-login needed @@ -278,6 +336,84 @@ export class CredentialsManager { return !!process.env[AUTH_ENV_VARS.ACCESS_TOKEN]; } + // ============================================ + // Firebase Token Methods + // ============================================ + + /** + * Get Firebase tokens from unified store. + */ + async getFirebaseTokens(): Promise { + const credentials = await this.load(); + return credentials?.firebase ?? null; + } + + /** + * Save Firebase tokens to unified store. + */ + async saveFirebaseTokens(firebaseTokens: FirebaseTokens): Promise { + const current = (await this.load()) ?? { version: CREDENTIALS_VERSION }; + await this.save({ + ...current, + version: CREDENTIALS_VERSION, + firebase: firebaseTokens, + }); + } + + /** + * Clear only Firebase tokens (keep Clix credentials). + */ + async clearFirebaseTokens(): Promise { + const current = await this.load(); + if (current) { + const { firebase: _, ...rest } = current; + if (rest.clix) { + await this.save({ ...rest, version: CREDENTIALS_VERSION }); + } else { + await this.delete(); + } + } + } + + /** + * Check if Firebase tokens exist and have usable content. + */ + async hasFirebaseTokens(): Promise { + const tokens = await this.getFirebaseTokens(); + return !!(tokens?.access_token || tokens?.refresh_token); + } + + /** + * Check if Firebase tokens are expired. + * + * @param tokens - Firebase tokens to check + * @returns true if tokens are expired or will expire within 5 minutes + */ + isFirebaseExpired(tokens: FirebaseTokens): boolean { + if (!tokens.access_token) { + return true; // No access token, force refresh + } + if (!tokens.expiry_date) { + return true; // Missing expiry info, treat as expired + } + // Consider expired if less than 5 minutes remaining + return Date.now() >= tokens.expiry_date - EXPIRY_BUFFER_MS; + } + + /** + * Check if Firebase tokens have a valid refresh token. + * + * @param tokens - Firebase tokens to check + * @returns true if refresh token exists + */ + hasFirebaseRefreshToken(tokens: FirebaseTokens): boolean { + return !!tokens.refresh_token; + } + + // ============================================ + // Utility Methods + // ============================================ + /** * Clear the cached credentials (useful for testing). */ diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index bf05522..7ad47f3 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -14,12 +14,15 @@ export type { AuthErrorCode } from './errors'; // Errors export { AUTH_ERROR_CODES, AuthError } from './errors'; export { PKCEFlowService } from './pkce-flow'; -export type { Credentials } from './schema'; +export type { ClixCredentials, Credentials, FirebaseTokens } from './schema'; // Schema export { + ClixCredentialsSchema, CREDENTIALS_VERSION, CredentialsSchema, + createClixCredentials, createCredentials, + FirebaseTokensSchema, validateCredentials, } from './schema'; export type { Auth0Config, RefreshTokenRequest, TokenResponse, UserInfo } from './types'; diff --git a/src/lib/auth/schema.ts b/src/lib/auth/schema.ts index 398d003..f3704c1 100644 --- a/src/lib/auth/schema.ts +++ b/src/lib/auth/schema.ts @@ -2,16 +2,13 @@ import { z } from 'zod'; /** * Current credentials schema version. - * Increment when making breaking changes to structure. */ export const CREDENTIALS_VERSION = 1; /** - * Zod schema for stored credentials. + * Zod schema for Clix (Auth0) credentials. */ -export const CredentialsSchema = z.object({ - /** Schema version for migrations */ - version: z.number().int().min(1), +export const ClixCredentialsSchema = z.object({ /** Auth0 access token */ accessToken: z.string().min(1), /** Auth0 refresh token (for session persistence) */ @@ -29,8 +26,38 @@ export const CredentialsSchema = z.object({ }); /** - * Inferred type from CredentialsSchema. + * Zod schema for Firebase OAuth tokens. + */ +export const FirebaseTokensSchema = z.object({ + /** Firebase access token */ + access_token: z.string().nullish(), + /** Firebase refresh token */ + refresh_token: z.string().nullish(), + /** OAuth scope */ + scope: z.string().optional(), + /** Token type (e.g., "Bearer") */ + token_type: z.string().nullish(), + /** Token expiration timestamp (ms) */ + expiry_date: z.number().nullish(), +}); + +/** + * Zod schema for unified credentials file. */ +export const CredentialsSchema = z.object({ + /** Schema version */ + version: z.number().int().min(1), + /** Clix (Auth0) credentials */ + clix: ClixCredentialsSchema.optional(), + /** Firebase OAuth tokens */ + firebase: FirebaseTokensSchema.optional(), +}); + +/** + * Inferred types from schemas. + */ +export type ClixCredentials = z.infer; +export type FirebaseTokens = z.infer; export type Credentials = z.infer; /** @@ -45,14 +72,14 @@ export function validateCredentials(data: unknown): Credentials | null { } /** - * Create credentials object from token response. + * Create Clix credentials object from token response. * * @param tokenResponse - Auth0 token response * @param issuer - Auth0 issuer URL * @param audience - API audience - * @returns Credentials object ready for storage + * @returns ClixCredentials object ready for storage */ -export function createCredentials( +export function createClixCredentials( tokenResponse: { access_token: string; refresh_token?: string; @@ -61,7 +88,7 @@ export function createCredentials( }, issuer: string, audience: string, -): Credentials { +): ClixCredentials { const now = new Date(); const expiresInMs = tokenResponse.expires_in * 1000; @@ -73,7 +100,6 @@ export function createCredentials( const expiresAt = new Date(now.getTime() + expiresInMs); return { - version: CREDENTIALS_VERSION, accessToken: tokenResponse.access_token, refreshToken: tokenResponse.refresh_token, idToken: tokenResponse.id_token, @@ -83,3 +109,9 @@ export function createCredentials( audience, }; } + +/** + * @deprecated Use createClixCredentials instead. + * Kept for backward compatibility during transition. + */ +export const createCredentials = createClixCredentials; diff --git a/src/lib/services/firebase/oauth/token-store.ts b/src/lib/services/firebase/oauth/token-store.ts index 27de74f..a56a4f2 100644 --- a/src/lib/services/firebase/oauth/token-store.ts +++ b/src/lib/services/firebase/oauth/token-store.ts @@ -1,40 +1,29 @@ /** * Token storage for OAuth credentials. * - * Stores OAuth tokens in the XDG config directory (~/.config/clix/). + * Delegates to CredentialsManager for unified credential storage. * * @module services/firebase/oauth/token-store */ -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { xdg } from '@/lib/utils/xdg'; +import { getCredentialsManager } from '@/lib/auth/credentials'; import type { OAuthTokens } from './types'; -const TOKEN_FILE_NAME = 'firebase-tokens.json'; - /** * Token store for persisting OAuth tokens. + * + * Uses CredentialsManager to store Firebase tokens in the unified + * credentials.json file at project/.clix/credentials.json */ export class TokenStore { - private tokenPath: string; - - constructor() { - this.tokenPath = path.join(xdg.config(), TOKEN_FILE_NAME); - } - /** * Load tokens from storage. * * @returns Stored tokens or null if not found */ async load(): Promise { - try { - const data = await fs.readFile(this.tokenPath, 'utf-8'); - return JSON.parse(data) as OAuthTokens; - } catch { - return null; - } + const manager = getCredentialsManager(); + return manager.getFirebaseTokens(); } /** @@ -43,25 +32,16 @@ export class TokenStore { * @param tokens - OAuth tokens to save */ async save(tokens: OAuthTokens): Promise { - const dir = path.dirname(this.tokenPath); - // Create directory with restricted permissions (owner only) - await fs.mkdir(dir, { recursive: true, mode: 0o700 }); - // Write token file with restricted permissions (owner read/write only) - await fs.writeFile(this.tokenPath, JSON.stringify(tokens, null, 2), { - encoding: 'utf-8', - mode: 0o600, - }); + const manager = getCredentialsManager(); + await manager.saveFirebaseTokens(tokens); } /** * Clear stored tokens. */ async clear(): Promise { - try { - await fs.unlink(this.tokenPath); - } catch { - // Ignore if file doesn't exist - } + const manager = getCredentialsManager(); + await manager.clearFirebaseTokens(); } /** @@ -70,12 +50,8 @@ export class TokenStore { * @returns True if tokens file exists */ async exists(): Promise { - try { - await fs.access(this.tokenPath); - return true; - } catch { - return false; - } + const manager = getCredentialsManager(); + return manager.hasFirebaseTokens(); } /** @@ -85,12 +61,8 @@ export class TokenStore { * @returns True if tokens are expired or will expire within 5 minutes */ isExpired(tokens: OAuthTokens): boolean { - if (!tokens.expiry_date) { - return false; // No expiry info, assume valid - } - // Consider expired if less than 5 minutes remaining - const bufferMs = 5 * 60 * 1000; - return Date.now() >= tokens.expiry_date - bufferMs; + const manager = getCredentialsManager(); + return manager.isFirebaseExpired(tokens); } /** @@ -100,6 +72,7 @@ export class TokenStore { * @returns True if refresh token exists */ hasRefreshToken(tokens: OAuthTokens): boolean { - return !!tokens.refresh_token; + const manager = getCredentialsManager(); + return manager.hasFirebaseRefreshToken(tokens); } } diff --git a/src/lib/services/session-store.ts b/src/lib/services/session-store.ts index e94699d..44ee2bb 100644 --- a/src/lib/services/session-store.ts +++ b/src/lib/services/session-store.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; -import { xdg } from '@/lib/utils/xdg'; import type { ChatMessage } from '@/ui/chat/context/ChatContext'; +import { findProjectRoot } from '../utils/path'; export const CHAT_SESSION_SCHEMA_VERSION = 1; @@ -45,7 +45,7 @@ function resolveSessionsDir(): string { if (process.env.CLIX_SESSION_DIR) { return process.env.CLIX_SESSION_DIR; } - return path.join(xdg.state(), 'sessions'); + return path.join(findProjectRoot(), '.clix', 'sessions'); } async function ensureSessionsDir(): Promise { diff --git a/src/lib/services/transfer-service.ts b/src/lib/services/transfer-service.ts index 5a10c8a..777cca8 100644 --- a/src/lib/services/transfer-service.ts +++ b/src/lib/services/transfer-service.ts @@ -2,8 +2,7 @@ import { spawn } from 'node:child_process'; import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import type { ConversationMessage } from '../executor'; -import { formatPath } from '../utils/path'; -import { xdg } from '../utils/xdg'; +import { findProjectRoot, formatPath } from '../utils/path'; export type TransferAgent = 'claude' | 'codex'; @@ -40,7 +39,7 @@ function formatHistoryAsMarkdown(history: ConversationMessage[]): string { * Returns the path to the saved file. */ async function saveSessionHistory(history: ConversationMessage[]): Promise { - const clixDir = xdg.state(); + const clixDir = join(findProjectRoot(), '.clix'); // Ensure .clix directory exists try { diff --git a/src/lib/utils/path.ts b/src/lib/utils/path.ts index 0c2c10a..dce2225 100644 --- a/src/lib/utils/path.ts +++ b/src/lib/utils/path.ts @@ -1,4 +1,45 @@ +import { existsSync } from 'node:fs'; import { homedir } from 'node:os'; +import { dirname, join, parse } from 'node:path'; + +/** + * Project root markers in order of priority. + */ +const PROJECT_MARKERS = [ + '.clix', // Clix config directory (highest priority - already initialized) + 'package.json', // Node.js project + '.git', // Git repository + 'Podfile', // iOS project + 'Package.swift', // Swift package + 'pubspec.yaml', // Flutter project + 'build.gradle', // Android/Gradle project +]; + +/** + * Find the project root by walking up directories looking for project markers. + * + * @param startDir - Starting directory (defaults to process.cwd()) + * @returns Project root path, or the starting directory if no markers found + */ +export function findProjectRoot(startDir?: string): string { + let currentDir = startDir ?? process.cwd(); + const { root } = parse(currentDir); + + while (currentDir !== root) { + // Check for any project markers + for (const marker of PROJECT_MARKERS) { + const markerPath = join(currentDir, marker); + if (existsSync(markerPath)) { + return currentDir; + } + } + // Move up one directory + currentDir = dirname(currentDir); + } + + // No markers found, return the original starting directory + return startDir ?? process.cwd(); +} /** * Formats a file path by replacing the home directory with ~ diff --git a/src/ui/LoginUI.tsx b/src/ui/LoginUI.tsx index 97ccbb0..f30b1d8 100644 --- a/src/ui/LoginUI.tsx +++ b/src/ui/LoginUI.tsx @@ -4,8 +4,8 @@ import type React from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { getInternalApiClient, type Member, type Organization, type Project } from '@/lib/api'; import { - type Credentials, - createCredentials, + type ClixCredentials, + createClixCredentials, getAuth0Config, getCredentialsManager, getIssuerUrl, @@ -40,7 +40,7 @@ interface OrgWithProjects { interface LoginUIProps { /** Called when login completes successfully */ - onComplete?: (credentials: Credentials) => void; + onComplete?: (credentials: ClixCredentials) => void; /** Called on error */ onError?: (error: Error) => void; } @@ -113,7 +113,7 @@ export const LoginUI: React.FC = ({ onComplete, onError }) => { const [savedConfig, setSavedConfig] = useState(null); const [workspacePath] = useState(() => process.cwd()); const pkceServiceRef = useRef(null); - const credentialsRef = useRef(null); + const credentialsRef = useRef(null); const memberRef = useRef(null); const handleProjectSelect = useCallback( @@ -209,7 +209,7 @@ export const LoginUI: React.FC = ({ onComplete, onError }) => { setPhase('complete'); setTimeout(() => { - const creds = credentialsManager.credentials; + const creds = credentialsManager.credentials?.clix; if (onComplete && creds) { onComplete(creds); } else { @@ -238,8 +238,8 @@ export const LoginUI: React.FC = ({ onComplete, onError }) => { // Save credentials const issuer = getIssuerUrl(config); - const credentials = createCredentials(tokenResponse, issuer, config.audience); - await credentialsManager.save(credentials); + const credentials = createClixCredentials(tokenResponse, issuer, config.audience); + await credentialsManager.saveClixCredentials(credentials); // Verify login setPhase('verifying'); diff --git a/src/ui/LogoutUI.tsx b/src/ui/LogoutUI.tsx index f417e93..de02a35 100644 --- a/src/ui/LogoutUI.tsx +++ b/src/ui/LogoutUI.tsx @@ -26,7 +26,7 @@ export const LogoutUI: React.FC = ({ onComplete }) => { setPhase('checking'); const credentials = await credentialsManager.load(); - if (!credentials) { + if (!credentials?.clix) { setPhase('not_logged_in'); setTimeout(() => { if (onComplete) { @@ -38,9 +38,9 @@ export const LogoutUI: React.FC = ({ onComplete }) => { return; } - // Delete credentials + // Clear Clix credentials only (preserve Firebase tokens) setPhase('deleting'); - await credentialsManager.delete(); + await credentialsManager.clearClixCredentials(); setPhase('complete'); setTimeout(() => { diff --git a/src/ui/SetupUI.tsx b/src/ui/SetupUI.tsx index 5b93e1a..005b773 100644 --- a/src/ui/SetupUI.tsx +++ b/src/ui/SetupUI.tsx @@ -4,8 +4,7 @@ import type React from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { getInternalApiClient, type Member, type Organization, type Project } from '@/lib/api'; import { - type Credentials, - createCredentials, + createClixCredentials, getAuth0Config, getCredentialsManager, getIssuerUrl, @@ -229,8 +228,8 @@ export const SetupUI: React.FC = ({ onComplete, onError, projectPa // Save credentials const issuer = getIssuerUrl(config); - const credentials: Credentials = createCredentials(tokenResponse, issuer, config.audience); - await credentialsManager.save(credentials); + const credentials = createClixCredentials(tokenResponse, issuer, config.audience); + await credentialsManager.saveClixCredentials(credentials); // Fetch user data setPhase('fetching_data');