feat: add oauth with remote server (#1178)
This commit is contained in:
163
packages/tm-core/src/auth/auth-manager.ts
Normal file
163
packages/tm-core/src/auth/auth-manager.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Authentication manager for Task Master CLI
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthCredentials,
|
||||
OAuthFlowOptions,
|
||||
AuthenticationError,
|
||||
AuthConfig
|
||||
} from './types';
|
||||
import { CredentialStore } from './credential-store';
|
||||
import { OAuthService } from './oauth-service';
|
||||
import { SupabaseAuthClient } from '../clients/supabase-client';
|
||||
|
||||
/**
|
||||
* Authentication manager class
|
||||
*/
|
||||
export class AuthManager {
|
||||
private static instance: AuthManager;
|
||||
private credentialStore: CredentialStore;
|
||||
private oauthService: OAuthService;
|
||||
private supabaseClient: SupabaseAuthClient;
|
||||
|
||||
private constructor(config?: Partial<AuthConfig>) {
|
||||
this.credentialStore = new CredentialStore(config);
|
||||
this.supabaseClient = new SupabaseAuthClient();
|
||||
this.oauthService = new OAuthService(this.credentialStore, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
static getInstance(config?: Partial<AuthConfig>): AuthManager {
|
||||
if (!AuthManager.instance) {
|
||||
AuthManager.instance = new AuthManager(config);
|
||||
}
|
||||
return AuthManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored authentication credentials
|
||||
*/
|
||||
getCredentials(): AuthCredentials | null {
|
||||
return this.credentialStore.getCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start OAuth 2.0 Authorization Code Flow with browser handling
|
||||
*/
|
||||
async authenticateWithOAuth(
|
||||
options: OAuthFlowOptions = {}
|
||||
): Promise<AuthCredentials> {
|
||||
return this.oauthService.authenticate(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authorization URL (for browser opening)
|
||||
*/
|
||||
getAuthorizationUrl(): string | null {
|
||||
return this.oauthService.getAuthorizationUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with API key
|
||||
* Note: This would require a custom implementation or Supabase RLS policies
|
||||
*/
|
||||
async authenticateWithApiKey(apiKey: string): Promise<AuthCredentials> {
|
||||
const token = apiKey.trim();
|
||||
if (!token || token.length < 10) {
|
||||
throw new AuthenticationError('Invalid API key', 'INVALID_API_KEY');
|
||||
}
|
||||
|
||||
const authData: AuthCredentials = {
|
||||
token,
|
||||
tokenType: 'api_key',
|
||||
userId: 'api-user',
|
||||
email: undefined,
|
||||
expiresAt: undefined, // API keys don't expire
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.credentialStore.saveCredentials(authData);
|
||||
return authData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh authentication token
|
||||
*/
|
||||
async refreshToken(): Promise<AuthCredentials> {
|
||||
const authData = this.credentialStore.getCredentials({
|
||||
allowExpired: true
|
||||
});
|
||||
|
||||
if (!authData || !authData.refreshToken) {
|
||||
throw new AuthenticationError(
|
||||
'No refresh token available',
|
||||
'NO_REFRESH_TOKEN'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Use Supabase client to refresh the token
|
||||
const response = await this.supabaseClient.refreshSession(
|
||||
authData.refreshToken
|
||||
);
|
||||
|
||||
// Update authentication data
|
||||
const newAuthData: AuthCredentials = {
|
||||
...authData,
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
expiresAt: response.expiresAt,
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.credentialStore.saveCredentials(newAuthData);
|
||||
return newAuthData;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout and clear credentials
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
// First try to sign out from Supabase to revoke tokens
|
||||
await this.supabaseClient.signOut();
|
||||
} catch (error) {
|
||||
// Log but don't throw - we still want to clear local credentials
|
||||
console.warn('Failed to sign out from Supabase:', error);
|
||||
}
|
||||
|
||||
// Always clear local credentials (removes auth.json file)
|
||||
this.credentialStore.clearCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if authenticated
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.credentialStore.hasValidCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization headers
|
||||
*/
|
||||
getAuthHeaders(): Record<string, string> {
|
||||
const authData = this.getCredentials();
|
||||
|
||||
if (!authData) {
|
||||
throw new AuthenticationError(
|
||||
'Not authenticated. Please authenticate first.',
|
||||
'NOT_AUTHENTICATED'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${authData.token}`
|
||||
};
|
||||
}
|
||||
}
|
||||
37
packages/tm-core/src/auth/config.ts
Normal file
37
packages/tm-core/src/auth/config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Centralized authentication configuration
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { AuthConfig } from './types';
|
||||
|
||||
// Single base domain for all URLs
|
||||
// Build-time: process.env.TM_PUBLIC_BASE_DOMAIN gets replaced by tsup's env option
|
||||
// Default: https://tryhamster.com for production
|
||||
const BASE_DOMAIN =
|
||||
process.env.TM_PUBLIC_BASE_DOMAIN || // This gets replaced at build time by tsup
|
||||
'https://tryhamster.com';
|
||||
|
||||
/**
|
||||
* Default authentication configuration
|
||||
* All URL configuration is derived from the single BASE_DOMAIN
|
||||
*/
|
||||
export const DEFAULT_AUTH_CONFIG: AuthConfig = {
|
||||
// Base domain for all services
|
||||
baseUrl: BASE_DOMAIN,
|
||||
|
||||
// Configuration directory and file paths
|
||||
configDir: path.join(os.homedir(), '.taskmaster'),
|
||||
configFile: path.join(os.homedir(), '.taskmaster', 'auth.json')
|
||||
};
|
||||
|
||||
/**
|
||||
* Get merged configuration with optional overrides
|
||||
*/
|
||||
export function getAuthConfig(overrides?: Partial<AuthConfig>): AuthConfig {
|
||||
return {
|
||||
...DEFAULT_AUTH_CONFIG,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
109
packages/tm-core/src/auth/credential-store.ts
Normal file
109
packages/tm-core/src/auth/credential-store.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Credential storage and management
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import { AuthCredentials, AuthenticationError, AuthConfig } from './types';
|
||||
import { getAuthConfig } from './config';
|
||||
import { getLogger } from '../logger';
|
||||
|
||||
export class CredentialStore {
|
||||
private logger = getLogger('CredentialStore');
|
||||
private config: AuthConfig;
|
||||
|
||||
constructor(config?: Partial<AuthConfig>) {
|
||||
this.config = getAuthConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored authentication credentials
|
||||
*/
|
||||
getCredentials(options?: { allowExpired?: boolean }): AuthCredentials | null {
|
||||
try {
|
||||
if (!fs.existsSync(this.config.configFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authData = JSON.parse(
|
||||
fs.readFileSync(this.config.configFile, 'utf-8')
|
||||
) as AuthCredentials;
|
||||
|
||||
// Check if token is expired
|
||||
if (
|
||||
authData.expiresAt &&
|
||||
new Date(authData.expiresAt) < new Date() &&
|
||||
!options?.allowExpired
|
||||
) {
|
||||
this.logger.warn('Authentication token has expired');
|
||||
return null;
|
||||
}
|
||||
|
||||
return authData;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to read auth credentials: ${(error as Error).message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save authentication credentials
|
||||
*/
|
||||
saveCredentials(authData: AuthCredentials): void {
|
||||
try {
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(this.config.configDir)) {
|
||||
fs.mkdirSync(this.config.configDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
authData.savedAt = new Date().toISOString();
|
||||
|
||||
// Save credentials atomically with secure permissions
|
||||
const tempFile = `${this.config.configFile}.tmp`;
|
||||
fs.writeFileSync(tempFile, JSON.stringify(authData, null, 2), {
|
||||
mode: 0o600
|
||||
});
|
||||
fs.renameSync(tempFile, this.config.configFile);
|
||||
} catch (error) {
|
||||
throw new AuthenticationError(
|
||||
`Failed to save auth credentials: ${(error as Error).message}`,
|
||||
'SAVE_FAILED',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear stored credentials
|
||||
*/
|
||||
clearCredentials(): void {
|
||||
try {
|
||||
if (fs.existsSync(this.config.configFile)) {
|
||||
fs.unlinkSync(this.config.configFile);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new AuthenticationError(
|
||||
`Failed to clear credentials: ${(error as Error).message}`,
|
||||
'CLEAR_FAILED',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if credentials exist and are valid
|
||||
*/
|
||||
hasValidCredentials(): boolean {
|
||||
const credentials = this.getCredentials({ allowExpired: false });
|
||||
return credentials !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration
|
||||
*/
|
||||
getConfig(): AuthConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
}
|
||||
21
packages/tm-core/src/auth/index.ts
Normal file
21
packages/tm-core/src/auth/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Authentication module exports
|
||||
*/
|
||||
|
||||
export { AuthManager } from './auth-manager';
|
||||
export { CredentialStore } from './credential-store';
|
||||
export { OAuthService } from './oauth-service';
|
||||
|
||||
export type {
|
||||
AuthCredentials,
|
||||
OAuthFlowOptions,
|
||||
AuthConfig,
|
||||
CliData
|
||||
} from './types';
|
||||
|
||||
export { AuthenticationError } from './types';
|
||||
|
||||
export {
|
||||
DEFAULT_AUTH_CONFIG,
|
||||
getAuthConfig
|
||||
} from './config';
|
||||
346
packages/tm-core/src/auth/oauth-service.ts
Normal file
346
packages/tm-core/src/auth/oauth-service.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* OAuth 2.0 Authorization Code Flow service
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
import crypto from 'crypto';
|
||||
import os from 'os';
|
||||
import {
|
||||
AuthCredentials,
|
||||
AuthenticationError,
|
||||
OAuthFlowOptions,
|
||||
AuthConfig,
|
||||
CliData
|
||||
} from './types';
|
||||
import { CredentialStore } from './credential-store';
|
||||
import { SupabaseAuthClient } from '../clients/supabase-client';
|
||||
import { getAuthConfig } from './config';
|
||||
import { getLogger } from '../logger';
|
||||
import packageJson from '../../../../package.json' with { type: 'json' };
|
||||
|
||||
export class OAuthService {
|
||||
private logger = getLogger('OAuthService');
|
||||
private credentialStore: CredentialStore;
|
||||
private supabaseClient: SupabaseAuthClient;
|
||||
private baseUrl: string;
|
||||
private authorizationUrl: string | null = null;
|
||||
private originalState: string | null = null;
|
||||
private authorizationReady: Promise<void> | null = null;
|
||||
private resolveAuthorizationReady: (() => void) | null = null;
|
||||
|
||||
constructor(
|
||||
credentialStore: CredentialStore,
|
||||
config: Partial<AuthConfig> = {}
|
||||
) {
|
||||
this.credentialStore = credentialStore;
|
||||
this.supabaseClient = new SupabaseAuthClient();
|
||||
const authConfig = getAuthConfig(config);
|
||||
this.baseUrl = authConfig.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start OAuth 2.0 Authorization Code Flow with browser handling
|
||||
*/
|
||||
async authenticate(options: OAuthFlowOptions = {}): Promise<AuthCredentials> {
|
||||
const {
|
||||
openBrowser,
|
||||
timeout = 300000, // 5 minutes default
|
||||
onAuthUrl,
|
||||
onWaitingForAuth,
|
||||
onSuccess,
|
||||
onError
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// Start the OAuth flow (starts local server)
|
||||
const authPromise = this.startFlow(timeout);
|
||||
|
||||
// Wait for server to be ready and URL to be generated
|
||||
if (this.authorizationReady) {
|
||||
await this.authorizationReady;
|
||||
}
|
||||
|
||||
// Get the authorization URL
|
||||
const authUrl = this.getAuthorizationUrl();
|
||||
|
||||
if (!authUrl) {
|
||||
throw new AuthenticationError(
|
||||
'Failed to generate authorization URL',
|
||||
'URL_GENERATION_FAILED'
|
||||
);
|
||||
}
|
||||
|
||||
// Notify about the auth URL
|
||||
if (onAuthUrl) {
|
||||
onAuthUrl(authUrl);
|
||||
}
|
||||
|
||||
// Open browser if callback provided
|
||||
if (openBrowser) {
|
||||
try {
|
||||
await openBrowser(authUrl);
|
||||
this.logger.debug('Browser opened successfully with URL:', authUrl);
|
||||
} catch (error) {
|
||||
// Log the error but don't throw - user can still manually open the URL
|
||||
this.logger.warn('Failed to open browser automatically:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify that we're waiting for authentication
|
||||
if (onWaitingForAuth) {
|
||||
onWaitingForAuth();
|
||||
}
|
||||
|
||||
// Wait for authentication to complete
|
||||
const credentials = await authPromise;
|
||||
|
||||
// Notify success
|
||||
if (onSuccess) {
|
||||
onSuccess(credentials);
|
||||
}
|
||||
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
const authError =
|
||||
error instanceof AuthenticationError
|
||||
? error
|
||||
: new AuthenticationError(
|
||||
`OAuth authentication failed: ${(error as Error).message}`,
|
||||
'OAUTH_FAILED',
|
||||
error
|
||||
);
|
||||
|
||||
// Notify error
|
||||
if (onError) {
|
||||
onError(authError);
|
||||
}
|
||||
|
||||
throw authError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the OAuth flow (internal implementation)
|
||||
*/
|
||||
private async startFlow(timeout: number = 300000): Promise<AuthCredentials> {
|
||||
const state = this.generateState();
|
||||
|
||||
// Store the original state for verification
|
||||
this.originalState = state;
|
||||
|
||||
// Create a promise that will resolve when the server is ready
|
||||
this.authorizationReady = new Promise<void>((resolve) => {
|
||||
this.resolveAuthorizationReady = resolve;
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
// Create local HTTP server for OAuth callback
|
||||
const server = http.createServer();
|
||||
|
||||
// Start server on localhost only, bind to port 0 for automatic port assignment
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
reject(new Error('Failed to get server address'));
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
const callbackUrl = `http://localhost:${port}/callback`;
|
||||
|
||||
// Set up request handler after we know the port
|
||||
server.on('request', async (req, res) => {
|
||||
const url = new URL(req.url!, `http://127.0.0.1:${port}`);
|
||||
|
||||
if (url.pathname === '/callback') {
|
||||
await this.handleCallback(
|
||||
url,
|
||||
res,
|
||||
server,
|
||||
resolve,
|
||||
reject,
|
||||
timeoutId
|
||||
);
|
||||
} else {
|
||||
// Handle other paths (favicon, etc.)
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Prepare CLI data object (server handles OAuth/PKCE)
|
||||
const cliData: CliData = {
|
||||
callback: callbackUrl,
|
||||
state: state,
|
||||
name: 'Task Master CLI',
|
||||
version: this.getCliVersion(),
|
||||
device: os.hostname(),
|
||||
user: os.userInfo().username,
|
||||
platform: os.platform(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Build authorization URL for web app sign-in page
|
||||
const authUrl = new URL(`${this.baseUrl}/auth/sign-in`);
|
||||
|
||||
// Encode CLI data as base64
|
||||
const cliParam = Buffer.from(JSON.stringify(cliData)).toString(
|
||||
'base64'
|
||||
);
|
||||
|
||||
// Set the single CLI parameter with all encoded data
|
||||
authUrl.searchParams.append('cli', cliParam);
|
||||
|
||||
// Store auth URL for browser opening
|
||||
this.authorizationUrl = authUrl.toString();
|
||||
|
||||
this.logger.info(
|
||||
`OAuth session started - ${cliData.name} v${cliData.version} on port ${port}`
|
||||
);
|
||||
this.logger.debug('CLI data:', cliData);
|
||||
|
||||
// Signal that the server is ready and URL is available
|
||||
if (this.resolveAuthorizationReady) {
|
||||
this.resolveAuthorizationReady();
|
||||
this.resolveAuthorizationReady = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Set timeout for authentication
|
||||
timeoutId = setTimeout(() => {
|
||||
if (server.listening) {
|
||||
server.close();
|
||||
// Clean up the readiness promise if still pending
|
||||
if (this.resolveAuthorizationReady) {
|
||||
this.resolveAuthorizationReady();
|
||||
this.resolveAuthorizationReady = null;
|
||||
}
|
||||
reject(
|
||||
new AuthenticationError('Authentication timeout', 'AUTH_TIMEOUT')
|
||||
);
|
||||
}
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle OAuth callback
|
||||
*/
|
||||
private async handleCallback(
|
||||
url: URL,
|
||||
res: http.ServerResponse,
|
||||
server: http.Server,
|
||||
resolve: (value: AuthCredentials) => void,
|
||||
reject: (error: any) => void,
|
||||
timeoutId?: NodeJS.Timeout
|
||||
): Promise<void> {
|
||||
// Server now returns tokens directly instead of code
|
||||
const type = url.searchParams.get('type');
|
||||
const returnedState = url.searchParams.get('state');
|
||||
const accessToken = url.searchParams.get('access_token');
|
||||
const refreshToken = url.searchParams.get('refresh_token');
|
||||
const expiresIn = url.searchParams.get('expires_in');
|
||||
const error = url.searchParams.get('error');
|
||||
const errorDescription = url.searchParams.get('error_description');
|
||||
|
||||
// Server handles displaying success/failure, just close connection
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
|
||||
if (error) {
|
||||
if (server.listening) {
|
||||
server.close();
|
||||
}
|
||||
reject(
|
||||
new AuthenticationError(
|
||||
errorDescription || error || 'Authentication failed',
|
||||
'OAUTH_ERROR'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify state parameter for CSRF protection
|
||||
if (returnedState !== this.originalState) {
|
||||
if (server.listening) {
|
||||
server.close();
|
||||
}
|
||||
reject(
|
||||
new AuthenticationError('Invalid state parameter', 'INVALID_STATE')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle direct token response from server
|
||||
if (
|
||||
accessToken &&
|
||||
(type === 'oauth_success' || type === 'session_transfer')
|
||||
) {
|
||||
try {
|
||||
this.logger.info(`Received tokens via ${type}`);
|
||||
|
||||
// Get user info using the access token if possible
|
||||
const user = await this.supabaseClient.getUser(accessToken);
|
||||
|
||||
// Calculate expiration time
|
||||
const expiresAt = expiresIn
|
||||
? new Date(Date.now() + parseInt(expiresIn) * 1000).toISOString()
|
||||
: undefined;
|
||||
|
||||
// Save authentication data
|
||||
const authData: AuthCredentials = {
|
||||
token: accessToken,
|
||||
refreshToken: refreshToken || undefined,
|
||||
userId: user?.id || 'unknown',
|
||||
email: user?.email,
|
||||
expiresAt: expiresAt,
|
||||
tokenType: 'standard',
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.credentialStore.saveCredentials(authData);
|
||||
|
||||
if (server.listening) {
|
||||
server.close();
|
||||
}
|
||||
// Clear timeout since authentication succeeded
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
resolve(authData);
|
||||
} catch (error) {
|
||||
if (server.listening) {
|
||||
server.close();
|
||||
}
|
||||
reject(error);
|
||||
}
|
||||
} else {
|
||||
if (server.listening) {
|
||||
server.close();
|
||||
}
|
||||
reject(new AuthenticationError('No access token received', 'NO_TOKEN'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate state for OAuth flow
|
||||
*/
|
||||
private generateState(): string {
|
||||
return crypto.randomBytes(32).toString('base64url');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CLI version from package.json if available
|
||||
*/
|
||||
private getCliVersion(): string {
|
||||
return packageJson.version || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authorization URL (for browser opening)
|
||||
*/
|
||||
getAuthorizationUrl(): string | null {
|
||||
return this.authorizationUrl;
|
||||
}
|
||||
}
|
||||
85
packages/tm-core/src/auth/types.ts
Normal file
85
packages/tm-core/src/auth/types.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Authentication types and interfaces
|
||||
*/
|
||||
|
||||
export interface AuthCredentials {
|
||||
token: string;
|
||||
refreshToken?: string;
|
||||
userId: string;
|
||||
email?: string;
|
||||
expiresAt?: string;
|
||||
tokenType?: 'standard' | 'api_key';
|
||||
savedAt: string;
|
||||
}
|
||||
|
||||
export interface OAuthFlowOptions {
|
||||
/** Callback to open the browser with the auth URL. If not provided, browser won't be opened */
|
||||
openBrowser?: (url: string) => Promise<void>;
|
||||
/** Timeout for the OAuth flow in milliseconds. Default: 300000 (5 minutes) */
|
||||
timeout?: number;
|
||||
/** Callback to be invoked with the authorization URL */
|
||||
onAuthUrl?: (url: string) => void;
|
||||
/** Callback to be invoked when waiting for authentication */
|
||||
onWaitingForAuth?: () => void;
|
||||
/** Callback to be invoked on successful authentication */
|
||||
onSuccess?: (credentials: AuthCredentials) => void;
|
||||
/** Callback to be invoked on authentication error */
|
||||
onError?: (error: AuthenticationError) => void;
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
baseUrl: string;
|
||||
configDir: string;
|
||||
configFile: string;
|
||||
}
|
||||
|
||||
export interface CliData {
|
||||
callback: string;
|
||||
state: string;
|
||||
name: string;
|
||||
version: string;
|
||||
device?: string;
|
||||
user?: string;
|
||||
platform?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication error codes
|
||||
*/
|
||||
export type AuthErrorCode =
|
||||
| 'AUTH_TIMEOUT'
|
||||
| 'AUTH_EXPIRED'
|
||||
| 'OAUTH_FAILED'
|
||||
| 'OAUTH_ERROR'
|
||||
| 'OAUTH_CANCELED'
|
||||
| 'URL_GENERATION_FAILED'
|
||||
| 'INVALID_STATE'
|
||||
| 'NO_TOKEN'
|
||||
| 'TOKEN_EXCHANGE_FAILED'
|
||||
| 'INVALID_API_KEY'
|
||||
| 'INVALID_CREDENTIALS'
|
||||
| 'NO_REFRESH_TOKEN'
|
||||
| 'NOT_AUTHENTICATED'
|
||||
| 'NETWORK_ERROR'
|
||||
| 'CONFIG_MISSING'
|
||||
| 'SAVE_FAILED'
|
||||
| 'CLEAR_FAILED'
|
||||
| 'STORAGE_ERROR';
|
||||
|
||||
/**
|
||||
* Authentication error class
|
||||
*/
|
||||
export class AuthenticationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: AuthErrorCode,
|
||||
public cause?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AuthenticationError';
|
||||
if (cause && cause instanceof Error) {
|
||||
this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user