@teamwork/get-bearer-token
Version:
CLI tool to obtain bearer tokens for Teamwork API using OAuth flow
176 lines (151 loc) • 4.09 kB
JavaScript
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;
}