UNPKG

@teamwork/get-bearer-token

Version:

CLI tool to obtain bearer tokens for Teamwork API using OAuth flow

176 lines (151 loc) 4.09 kB
import axios from "axios"; import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { getConfig } from "./config.js"; function getOAuthDefaults() { const { teamworkUrl, clientId, clientSecret } = getConfig(); return { clientId, clientSecret, redirectUri: 'http://127.0.0.1:8123', launchpadUrl: `${teamworkUrl}/launchpad/login`, tokenUrl: `${teamworkUrl}/launchpad/v1/token.json` }; } // Maximum auth data age: 24 hours in milliseconds const MAX_AUTH_AGE = 24 * 60 * 60 * 1000; // File-based persistent storage function getConfigDir() { const base = process.platform === 'win32' ? (process.env.APPDATA || join(homedir(), 'AppData', 'Roaming')) : (process.env.XDG_CONFIG_HOME || join(homedir(), '.config')); return join(base, 'teamwork-bearer-token'); } function getStorePath() { return join(getConfigDir(), 'auth.json'); } function readStore() { try { return JSON.parse(readFileSync(getStorePath(), 'utf8')); } catch { return null; } } function writeStore(data) { const dir = getConfigDir(); mkdirSync(dir, { recursive: true }); writeFileSync(getStorePath(), JSON.stringify(data, null, 2)); } function deleteStore() { try { unlinkSync(getStorePath()); } catch { // ignore if file doesn't exist } } /** * Get OAuth configuration */ export function getOAuthConfig() { return getOAuthDefaults(); } /** * Build the Teamwork Launchpad authentication URL * @param {string} clientId - Optional custom client ID (defaults to OAuth config) */ export function buildAuthUrl(clientId = null) { const config = getOAuthDefaults(); const params = new URLSearchParams({ redirect_uri: config.redirectUri, client_id: clientId || config.clientId, }); return `${config.launchpadUrl}?${params.toString()}`; } /** * Exchange authorization code for access token * @param {string} code - Authorization code from OAuth callback * @returns {Promise<Object>} Token response including access_token, installation, and user info */ export async function exchangeCodeForToken(code) { const config = getOAuthDefaults(); try { const response = await axios.post( config.tokenUrl, { code, client_id: config.clientId, client_secret: config.clientSecret, redirect_uri: config.redirectUri, }, { headers: { "Content-Type": "application/json", Accept: "application/json", }, } ); return response.data; } catch (error) { console.error("Token exchange error:", error.response?.data || error.message); throw new Error( error.response?.data?.error || "Failed to exchange code for token" ); } } /** * In-memory cache of authentication data */ let authData = null; /** * Store authentication data with timestamp */ export function setAuthData(data) { const authRecord = { data, timestamp: Date.now() }; // Store in memory authData = data; // Persist to disk writeStore(authRecord); } /** * Get stored authentication data * Returns null if data is older than 24 hours */ export function getAuthData() { // Return from memory if available if (authData) { return authData; } // Try to load from persistent storage const authRecord = readStore(); if (!authRecord || !authRecord.data) { return null; } // Check if data is still valid (less than 24 hours old) const age = Date.now() - authRecord.timestamp; if (age > MAX_AUTH_AGE) { // Data is too old, clear it clearAuthData(); return null; } // Cache in memory and return authData = authRecord.data; return authData; } /** * Clear authentication data from memory and persistent storage */ export function clearAuthData() { authData = null; deleteStore(); } /** * Check if user is authenticated and token is not expired */ export function isAuthenticated() { const data = getAuthData(); return data !== null && data.access_token !== undefined; }