import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import fetch from 'node-fetch'; const TOKEN_URL = 'https://webexapis.com/v1/access_token'; const AUTHORIZE_URL = 'https://webexapis.com/v1/authorize'; const REFRESH_BUFFER_MS = 5 * 60 * 1000; export function isTokenExpiringSoon(expiresAt, bufferMs = REFRESH_BUFFER_MS) { if (!expiresAt) { return true; } return Date.parse(expiresAt) <= Date.now() + bufferMs; } function defaultTokenData() { return { accessToken: null, refreshToken: null, expiresAt: null, refreshExpiresAt: null, personEmail: null, webhookIds: {}, }; } function computeExpiry(expiresInSeconds) { if (!expiresInSeconds) { return null; } return new Date(Date.now() + Number(expiresInSeconds) * 1000).toISOString(); } export function createWebexOAuthService(config, log) { const integration = config.webex.integration; const tokenFile = integration.tokenFile; let tokenData = loadTokenData(); function loadTokenData() { try { if (!fs.existsSync(tokenFile)) { return defaultTokenData(); } const parsed = JSON.parse(fs.readFileSync(tokenFile, 'utf8')); return { ...defaultTokenData(), ...parsed }; } catch (error) { log.logError('webexOAuth', `Failed to load token file ${tokenFile}`, error); return defaultTokenData(); } } function saveTokenData() { const dir = path.dirname(tokenFile); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(tokenFile, JSON.stringify(tokenData, null, 2)); } function applyTokenResponse(responseData) { tokenData = { ...tokenData, accessToken: responseData.access_token, refreshToken: responseData.refresh_token || tokenData.refreshToken, expiresAt: computeExpiry(responseData.expires_in), refreshExpiresAt: computeExpiry(responseData.refresh_token_expires_in) || tokenData.refreshExpiresAt, }; saveTokenData(); return tokenData; } async function postTokenRequest(body) { const response = await fetch(TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(body).toString(), }); const responseText = await response.text(); let data; try { data = responseText ? JSON.parse(responseText) : {}; } catch { throw new Error(`Invalid token response: ${responseText}`); } if (!response.ok) { const error = new Error(data.message || data.error_description || responseText || 'Token request failed'); error.status = response.status; error.details = data; throw error; } return data; } function buildAuthorizeUrl(state) { const params = new URLSearchParams({ client_id: integration.clientId, response_type: 'code', redirect_uri: integration.redirectUri, scope: integration.scopes, state, }); return `${AUTHORIZE_URL}?${params.toString()}`; } function createOAuthState() { return crypto.randomBytes(24).toString('hex'); } async function exchangeCode(code) { const data = await postTokenRequest({ grant_type: 'authorization_code', client_id: integration.clientId, client_secret: integration.clientSecret, code, redirect_uri: integration.redirectUri, }); applyTokenResponse(data); log.logger('webexOAuth', 'OAuth tokens stored after authorization'); return tokenData; } async function refreshIfNeeded() { if (!tokenData.refreshToken) { return false; } if (!isTokenExpiringSoon(tokenData.expiresAt)) { return false; } const data = await postTokenRequest({ grant_type: 'refresh_token', client_id: integration.clientId, client_secret: integration.clientSecret, refresh_token: tokenData.refreshToken, }); applyTokenResponse(data); log.logger('webexOAuth', 'OAuth access token refreshed'); return true; } async function getAccessToken() { if (!tokenData.accessToken && !tokenData.refreshToken) { return null; } await refreshIfNeeded(); if (!tokenData.accessToken) { return null; } return tokenData.accessToken; } function isAuthenticated() { return Boolean(tokenData.refreshToken || tokenData.accessToken); } function getStatus() { return { authenticated: isAuthenticated(), expiresAt: tokenData.expiresAt, refreshExpiresAt: tokenData.refreshExpiresAt, personEmail: tokenData.personEmail || null, webhookIds: { ...tokenData.webhookIds }, tokenFile, }; } function setPersonEmail(email) { tokenData.personEmail = email; saveTokenData(); } function setWebhookId(roomId, webhookId) { tokenData.webhookIds = { ...tokenData.webhookIds, [roomId]: webhookId, }; saveTokenData(); } function getWebhookIds() { return { ...tokenData.webhookIds }; } return { buildAuthorizeUrl, createOAuthState, exchangeCode, refreshIfNeeded, getAccessToken, isAuthenticated, getStatus, setPersonEmail, setWebhookId, getWebhookIds, loadTokenData, }; }