|
|
import fs from 'fs'; |
|
|
import path from 'path'; |
|
|
import os from 'os'; |
|
|
import fetch from 'node-fetch'; |
|
|
import { logDebug, logError, logInfo } from './logger.js'; |
|
|
|
|
|
|
|
|
let currentApiKey = null; |
|
|
let currentRefreshToken = null; |
|
|
let lastRefreshTime = null; |
|
|
let clientId = null; |
|
|
let authSource = null; |
|
|
let authFilePath = null; |
|
|
let factoryApiKey = null; |
|
|
|
|
|
const REFRESH_URL = 'https://api.workos.com/user_management/authenticate'; |
|
|
const REFRESH_INTERVAL_HOURS = 6; |
|
|
const TOKEN_VALID_HOURS = 8; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateULID() { |
|
|
|
|
|
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; |
|
|
|
|
|
|
|
|
const timestamp = Date.now(); |
|
|
|
|
|
|
|
|
let time = ''; |
|
|
let ts = timestamp; |
|
|
for (let i = 9; i >= 0; i--) { |
|
|
const mod = ts % 32; |
|
|
time = ENCODING[mod] + time; |
|
|
ts = Math.floor(ts / 32); |
|
|
} |
|
|
|
|
|
|
|
|
let randomPart = ''; |
|
|
for (let i = 0; i < 16; i++) { |
|
|
const rand = Math.floor(Math.random() * 32); |
|
|
randomPart += ENCODING[rand]; |
|
|
} |
|
|
|
|
|
return time + randomPart; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateClientId() { |
|
|
const ulid = generateULID(); |
|
|
return `client_01${ulid}`; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadAuthConfig() { |
|
|
|
|
|
const factoryKey = process.env.FACTORY_API_KEY; |
|
|
if (factoryKey && factoryKey.trim() !== '') { |
|
|
logInfo('Using fixed API key from FACTORY_API_KEY environment variable'); |
|
|
factoryApiKey = factoryKey.trim(); |
|
|
authSource = 'factory_key'; |
|
|
return { type: 'factory_key', value: factoryKey.trim() }; |
|
|
} |
|
|
|
|
|
|
|
|
const envRefreshKey = process.env.DROID_REFRESH_KEY; |
|
|
if (envRefreshKey && envRefreshKey.trim() !== '') { |
|
|
logInfo('Using refresh token from DROID_REFRESH_KEY environment variable'); |
|
|
authSource = 'env'; |
|
|
authFilePath = path.join(process.cwd(), 'auth.json'); |
|
|
return { type: 'refresh', value: envRefreshKey.trim() }; |
|
|
} |
|
|
|
|
|
|
|
|
const homeDir = os.homedir(); |
|
|
const factoryAuthPath = path.join(homeDir, '.factory', 'auth.json'); |
|
|
|
|
|
try { |
|
|
if (fs.existsSync(factoryAuthPath)) { |
|
|
const authContent = fs.readFileSync(factoryAuthPath, 'utf-8'); |
|
|
const authData = JSON.parse(authContent); |
|
|
|
|
|
if (authData.refresh_token && authData.refresh_token.trim() !== '') { |
|
|
logInfo('Using refresh token from ~/.factory/auth.json'); |
|
|
authSource = 'file'; |
|
|
authFilePath = factoryAuthPath; |
|
|
|
|
|
|
|
|
if (authData.access_token) { |
|
|
currentApiKey = authData.access_token.trim(); |
|
|
} |
|
|
|
|
|
return { type: 'refresh', value: authData.refresh_token.trim() }; |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
logError('Error reading ~/.factory/auth.json', error); |
|
|
} |
|
|
|
|
|
|
|
|
logInfo('No auth configuration found, will use client authorization headers'); |
|
|
authSource = 'client'; |
|
|
return { type: 'client', value: null }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshApiKey() { |
|
|
if (!currentRefreshToken) { |
|
|
throw new Error('No refresh token available'); |
|
|
} |
|
|
|
|
|
if (!clientId) { |
|
|
clientId = 'client_01HNM792M5G5G1A2THWPXKFMXB'; |
|
|
logDebug(`Using fixed client ID: ${clientId}`); |
|
|
} |
|
|
|
|
|
logInfo('Refreshing API key...'); |
|
|
|
|
|
try { |
|
|
|
|
|
const formData = new URLSearchParams(); |
|
|
formData.append('grant_type', 'refresh_token'); |
|
|
formData.append('refresh_token', currentRefreshToken); |
|
|
formData.append('client_id', clientId); |
|
|
|
|
|
const response = await fetch(REFRESH_URL, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/x-www-form-urlencoded' |
|
|
}, |
|
|
body: formData.toString() |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
const errorText = await response.text(); |
|
|
throw new Error(`Failed to refresh token: ${response.status} ${errorText}`); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
|
|
|
currentApiKey = data.access_token; |
|
|
currentRefreshToken = data.refresh_token; |
|
|
lastRefreshTime = Date.now(); |
|
|
|
|
|
|
|
|
if (data.user) { |
|
|
logInfo(`Authenticated as: ${data.user.email} (${data.user.first_name} ${data.user.last_name})`); |
|
|
logInfo(`User ID: ${data.user.id}`); |
|
|
logInfo(`Organization ID: ${data.organization_id}`); |
|
|
} |
|
|
|
|
|
|
|
|
saveTokens(data.access_token, data.refresh_token); |
|
|
|
|
|
logInfo(`New Refresh-Key: ${currentRefreshToken}`); |
|
|
logInfo('API key refreshed successfully'); |
|
|
return data.access_token; |
|
|
|
|
|
} catch (error) { |
|
|
logError('Failed to refresh API key', error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function saveTokens(accessToken, refreshToken) { |
|
|
try { |
|
|
const authData = { |
|
|
access_token: accessToken, |
|
|
refresh_token: refreshToken, |
|
|
last_updated: new Date().toISOString() |
|
|
}; |
|
|
|
|
|
|
|
|
const dir = path.dirname(authFilePath); |
|
|
if (!fs.existsSync(dir)) { |
|
|
fs.mkdirSync(dir, { recursive: true }); |
|
|
} |
|
|
|
|
|
|
|
|
if (authSource === 'file' && fs.existsSync(authFilePath)) { |
|
|
try { |
|
|
const existingData = JSON.parse(fs.readFileSync(authFilePath, 'utf-8')); |
|
|
Object.assign(authData, existingData, { |
|
|
access_token: accessToken, |
|
|
refresh_token: refreshToken, |
|
|
last_updated: authData.last_updated |
|
|
}); |
|
|
} catch (error) { |
|
|
logError('Error reading existing auth file, will overwrite', error); |
|
|
} |
|
|
} |
|
|
|
|
|
fs.writeFileSync(authFilePath, JSON.stringify(authData, null, 2), 'utf-8'); |
|
|
logDebug(`Tokens saved to ${authFilePath}`); |
|
|
|
|
|
} catch (error) { |
|
|
logError('Failed to save tokens', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function shouldRefresh() { |
|
|
if (!lastRefreshTime) { |
|
|
return true; |
|
|
} |
|
|
|
|
|
const hoursSinceRefresh = (Date.now() - lastRefreshTime) / (1000 * 60 * 60); |
|
|
return hoursSinceRefresh >= REFRESH_INTERVAL_HOURS; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function initializeAuth() { |
|
|
try { |
|
|
const authConfig = loadAuthConfig(); |
|
|
|
|
|
if (authConfig.type === 'factory_key') { |
|
|
|
|
|
logInfo('Auth system initialized with fixed API key'); |
|
|
} else if (authConfig.type === 'refresh') { |
|
|
|
|
|
currentRefreshToken = authConfig.value; |
|
|
|
|
|
|
|
|
await refreshApiKey(); |
|
|
logInfo('Auth system initialized with refresh token mechanism'); |
|
|
} else { |
|
|
|
|
|
logInfo('Auth system initialized for client authorization mode'); |
|
|
} |
|
|
|
|
|
logInfo('Auth system initialized successfully'); |
|
|
} catch (error) { |
|
|
logError('Failed to initialize auth system', error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function getApiKey(clientAuthorization = null) { |
|
|
|
|
|
if (authSource === 'factory_key' && factoryApiKey) { |
|
|
return `Bearer ${factoryApiKey}`; |
|
|
} |
|
|
|
|
|
|
|
|
if (authSource === 'env' || authSource === 'file') { |
|
|
|
|
|
if (shouldRefresh()) { |
|
|
logInfo('API key needs refresh (6+ hours old)'); |
|
|
await refreshApiKey(); |
|
|
} |
|
|
|
|
|
if (!currentApiKey) { |
|
|
throw new Error('No API key available from refresh token mechanism.'); |
|
|
} |
|
|
|
|
|
return `Bearer ${currentApiKey}`; |
|
|
} |
|
|
|
|
|
|
|
|
if (clientAuthorization) { |
|
|
logDebug('Using client authorization header'); |
|
|
return clientAuthorization; |
|
|
} |
|
|
|
|
|
|
|
|
throw new Error('No authorization available. Please configure FACTORY_API_KEY, refresh token, or provide client authorization.'); |
|
|
} |
|
|
|