Introduces integration-based webhook registration, message parsing, dry-run monitoring, JSM ticket creation, and OAuth token refresh for DC Ops spaces. Co-authored-by: Cursor <cursoragent@cursor.com>
206 lines
5.8 KiB
JavaScript
206 lines
5.8 KiB
JavaScript
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,
|
|
};
|
|
}
|