n8n-nodes-arubacentral
Version:
n8n community node for Aruba Central API integration with comprehensive monitoring, configuration, and management capabilities
526 lines (525 loc) • 23.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAccessToken = getAccessToken;
exports.apiRequest = apiRequest;
// helpers/apiRequest.ts
const n8n_workflow_1 = require("n8n-workflow");
const logger_1 = require("./logger");
const tokenCache = {};
let sessionToken;
let csrfToken;
/**
* Extract a token from cookies
*/
function extractTokenFromCookies(cookies, tokenName) {
if (!cookies || cookies.length === 0) {
throw new Error(`No cookies found when extracting ${tokenName}`);
}
for (const cookie of cookies) {
const match = new RegExp(`${tokenName}=([^;]+)`).exec(cookie);
if (match) {
return match[1];
}
}
throw new Error(`${tokenName} not found in cookies`);
}
/**
* Check if in rate limit backoff period
*/
function isRateLimited(cacheKey) {
var _a, _b;
if (((_a = tokenCache[cacheKey]) === null || _a === void 0 ? void 0 : _a.retryAfter) && ((_b = tokenCache[cacheKey]) === null || _b === void 0 ? void 0 : _b.retryTimestamp)) {
const now = Date.now();
const retryTime = tokenCache[cacheKey].retryTimestamp + tokenCache[cacheKey].retryAfter * 1000;
if (now < retryTime) {
return {
isLimited: true,
waitTime: Math.ceil((retryTime - now) / 1000),
};
}
}
return { isLimited: false, waitTime: 0 };
}
/**
* Handle rate limiting response
*/
function handleRateLimiting(cacheKey, response) {
// Parse retry time from response
let retryAfter = 60; // Default 60 seconds
try {
// Check if retryAfter was directly provided
if (response.retryAfter && typeof response.retryAfter === 'number') {
retryAfter = response.retryAfter;
}
// Check for retry-after header
else if (response.headers && response.headers['retry-after']) {
retryAfter = parseInt(response.headers['retry-after'], 10);
}
// Try to parse from error message
else {
const message = (response === null || response === void 0 ? void 0 : response.message) || (response === null || response === void 0 ? void 0 : response.error_description) || '';
const match = message.match(/retry after (\d+) seconds/i) || message.match(/(\d+) seconds/);
if (match && match[1]) {
retryAfter = parseInt(match[1], 10);
}
}
}
catch (e) {
logger_1.logger.error('auth:ratelimit', `Failed to parse retry time: ${e.message}`);
}
// Store rate limit info in cache
if (tokenCache[cacheKey]) {
tokenCache[cacheKey].retryAfter = retryAfter;
tokenCache[cacheKey].retryTimestamp = Date.now();
}
else {
tokenCache[cacheKey] = {
accessToken: '',
refreshToken: '',
expiresAt: 0,
retryAfter,
retryTimestamp: Date.now(),
};
}
logger_1.logger.warn('auth:ratelimit', `Rate limited. Will retry after ${retryAfter} seconds`);
}
/**
* Get token data from n8n storage
*/
function getTokenFromCredentials(credentials) {
logger_1.logger.debug('auth:storage', 'Checking for token in n8n credential storage');
try {
if (credentials.oauthTokenData) {
const tokenData = credentials.oauthTokenData;
logger_1.logger.debug('auth:storage', 'Token data found in credentials');
if (tokenData.access_token && tokenData.expires_at) {
logger_1.logger.debug('auth:storage', 'Found access token and expiration');
return {
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token || '',
expiresAt: tokenData.expires_at,
};
}
}
logger_1.logger.debug('auth:storage', 'No valid token data found in credential storage');
return null;
}
catch (error) {
logger_1.logger.error('auth:storage', `Error retrieving token from credentials: ${error.message}`);
return null;
}
}
/**
* Store OAuth token data in n8n credentials or workflow storage for persistence
*/
async function storeTokenData(tokenData, credentialId) {
try {
// Calculate expiration timestamp
const expiresAt = Date.now() + tokenData.expires_in * 1000;
// Format token data for storage
const oauthTokenData = {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
expires_in: tokenData.expires_in,
expires_at: expiresAt,
token_type: 'Bearer',
};
// Try different methods to store the token based on available n8n APIs
let stored = false;
// Method 1: Try using nodeHelpers if available (newer n8n versions)
try {
if (this.helpers.nodeHelpers &&
typeof this.helpers.nodeHelpers.updateCredentials === 'function') {
await this.helpers.nodeHelpers.updateCredentials(credentialId, {
oauthTokenData,
});
logger_1.logger.debug('auth:oauth2', 'Token stored using nodeHelpers.updateCredentials');
stored = true;
}
}
catch (error) {
logger_1.logger.debug('auth:oauth2', `nodeHelpers.updateCredentials failed: ${error.message}`);
}
// Method 2: Try direct updateCredentials if available (older n8n versions)
if (!stored && typeof this.helpers.updateCredentials === 'function') {
try {
await this.helpers.updateCredentials(credentialId, {
oauthTokenData,
});
logger_1.logger.debug('auth:oauth2', 'Token stored using helpers.updateCredentials');
stored = true;
}
catch (error) {
logger_1.logger.debug('auth:oauth2', `helpers.updateCredentials failed: ${error.message}`);
}
}
// Method 3: Fall back to workflow static data if credential update is not possible
if (!stored && typeof this.getWorkflowStaticData === 'function') {
const workflowStaticData = this.getWorkflowStaticData('node');
workflowStaticData.oauthTokenData = oauthTokenData;
logger_1.logger.debug('auth:oauth2', 'Token stored in workflow static data (fallback method)');
stored = true;
}
if (!stored) {
logger_1.logger.warn('auth:oauth2', 'Could not persist token data - no suitable storage method available');
}
}
catch (error) {
logger_1.logger.error('auth:oauth2', `Failed to persist token data: ${error.message}`);
// We continue without throwing as not persisting is better than failing completely
}
}
/**
* Check if a token is expired
*/
function isTokenExpired(expiresAt) {
// Consider token expired 60 seconds before actual expiration
return Date.now() > expiresAt - 60000;
}
/**
* Get an access token using the OAuth2 flow
*/
async function getAccessToken(credentials) {
var _a, _b, _c, _d;
logger_1.logger.debug('auth:oauth2', 'Getting access token...');
const cacheKey = `${credentials.baseUrl}_${credentials.clientId}`;
const credentialId = credentials.$credentialId;
// Check rate limiting first
const rateLimitStatus = isRateLimited(cacheKey);
if (rateLimitStatus.isLimited) {
throw new Error(`API rate limited. Please try again after ${rateLimitStatus.waitTime} seconds.`);
}
// First try workflow static data as a persistent token source
if (typeof this.getWorkflowStaticData === 'function') {
const workflowStaticData = this.getWorkflowStaticData('node');
if (workflowStaticData.oauthTokenData) {
const tokenData = workflowStaticData.oauthTokenData;
if (tokenData.access_token &&
tokenData.expires_at &&
Date.now() < tokenData.expires_at) {
logger_1.logger.debug('auth:oauth2', 'Using valid token from workflow static data');
return tokenData.access_token;
}
else {
logger_1.logger.debug('auth:oauth2', 'Token in workflow static data is expired or invalid');
}
}
}
// Second, try to get token from n8n credential storage
const storedToken = getTokenFromCredentials(credentials);
logger_1.logger.debug('auth:oauth2', storedToken ? 'Found token in credential storage' : 'No token in credential storage');
if (storedToken && !isTokenExpired(storedToken.expiresAt)) {
logger_1.logger.debug('auth:oauth2', 'Using valid access token from n8n credential storage');
// Update runtime cache for faster access next time
tokenCache[cacheKey] = {
accessToken: storedToken.accessToken,
refreshToken: storedToken.refreshToken,
expiresAt: storedToken.expiresAt,
};
return storedToken.accessToken;
}
// Third, check in-memory cache
if (tokenCache[cacheKey] && !isTokenExpired(tokenCache[cacheKey].expiresAt)) {
logger_1.logger.debug('auth:oauth2', 'Using valid token from in-memory cache');
return tokenCache[cacheKey].accessToken;
}
// Fourth, try to refresh the token if we have a refresh token
const refreshToken = (storedToken === null || storedToken === void 0 ? void 0 : storedToken.refreshToken) || ((_a = tokenCache[cacheKey]) === null || _a === void 0 ? void 0 : _a.refreshToken);
if (refreshToken) {
logger_1.logger.debug('auth:oauth2', 'Attempting to refresh token');
try {
const refreshResponse = await this.helpers.request({
method: 'POST',
uri: `${credentials.baseUrl}/oauth2/token`,
qs: {
client_id: credentials.clientId,
client_secret: credentials.clientSecret,
grant_type: 'refresh_token',
refresh_token: refreshToken,
},
json: true,
});
logger_1.logger.debug('auth:oauth2', 'Token refresh successful');
// Format and store the new token
const expiresAt = Date.now() + refreshResponse.expires_in * 1000;
// Update the runtime cache
tokenCache[cacheKey] = {
accessToken: refreshResponse.access_token,
refreshToken: refreshResponse.refresh_token,
expiresAt,
};
// Persist tokens
await storeTokenData.call(this, {
access_token: refreshResponse.access_token,
refresh_token: refreshResponse.refresh_token,
expires_in: refreshResponse.expires_in,
}, credentialId);
logger_1.logger.debug('auth:oauth2', 'New token stored after refresh');
return refreshResponse.access_token;
}
catch (error) {
logger_1.logger.error('auth:oauth2', `Token refresh failed: ${error.message}`);
logger_1.logger.debug('auth:oauth2', 'Proceeding to full authentication flow');
// Check for rate limiting in refresh error
if (error.statusCode === 429) {
handleRateLimiting(cacheKey, error.error);
throw new Error(`API rate limited during token refresh. Please try again later.`);
}
}
}
// Full OAuth flow
logger_1.logger.debug('auth:oauth2', 'Starting full OAuth flow...');
try {
// Step 1: Login to obtain session and CSRF tokens.
const loginUrl = `${credentials.baseUrl}/oauth2/authorize/central/api/login?client_id=${credentials.clientId}`;
logger_1.logger.debug('auth:oauth2', `Calling login endpoint: ${loginUrl}`);
const loginResponse = await this.helpers.request({
method: 'POST',
uri: loginUrl,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: {
username: credentials.username,
password: credentials.password,
},
json: true,
resolveWithFullResponse: true,
});
logger_1.logger.debug('auth:oauth2', 'Login response received');
// Extract tokens from response cookies.
try {
sessionToken = extractTokenFromCookies(loginResponse.headers['set-cookie'], 'session');
csrfToken = extractTokenFromCookies(loginResponse.headers['set-cookie'], 'csrftoken');
logger_1.logger.debug('auth:oauth2', 'Session and CSRF tokens extracted successfully');
}
catch (error) {
logger_1.logger.error('auth:oauth2', `Failed to extract tokens: ${error.message}`);
throw new Error(`Authentication failed: ${error.message}`);
}
// Step 2: Generate authorization code.
const authCodeUrl = `${credentials.baseUrl}/oauth2/authorize/central/api?client_id=${credentials.clientId}&response_type=code&scope=all`;
logger_1.logger.debug('auth:oauth2', `Calling authorization code endpoint: ${authCodeUrl}`);
const authCodeResponse = await this.helpers.request({
method: 'POST',
uri: authCodeUrl,
headers: {
'Content-Type': 'application/json',
Cookie: `session=${sessionToken}`,
'X-CSRF-Token': csrfToken,
},
body: {
customer_id: credentials.customerId,
},
json: true,
});
if (!authCodeResponse || !authCodeResponse.auth_code) {
logger_1.logger.error('auth:oauth2', 'No authorization code in response', authCodeResponse);
throw new Error('Failed to obtain authorization code');
}
const authorizationCode = authCodeResponse.auth_code;
logger_1.logger.debug('auth:oauth2', 'Authorization code obtained successfully');
// Step 3: Exchange the authorization code for an access token.
const tokenUrl = `${credentials.baseUrl}/oauth2/token`;
logger_1.logger.debug('auth:oauth2', `Exchanging authorization code for access token at: ${tokenUrl}`);
const tokenResponse = await this.helpers.request({
method: 'POST',
uri: tokenUrl,
headers: {
'Content-Type': 'application/json',
},
body: {
client_id: credentials.clientId,
client_secret: credentials.clientSecret,
grant_type: 'authorization_code',
code: authorizationCode,
},
json: true,
});
if (!tokenResponse || !tokenResponse.access_token) {
logger_1.logger.error('auth:oauth2', 'No access token in response', tokenResponse);
throw new Error('Failed to obtain access token');
}
// Calculate expiration
const expiresAt = Date.now() + tokenResponse.expires_in * 1000;
// Store in runtime cache
tokenCache[cacheKey] = {
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,
expiresAt,
};
// Store persistently
await storeTokenData.call(this, {
access_token: tokenResponse.access_token,
refresh_token: tokenResponse.refresh_token,
expires_in: tokenResponse.expires_in,
}, credentialId);
logger_1.logger.debug('auth:oauth2', 'Full OAuth flow completed successfully');
return tokenResponse.access_token;
}
catch (error) {
logger_1.logger.error('auth:oauth2', `OAuth authentication failed: ${error.message}`);
// Enhanced rate limiting detection
if (error.statusCode === 429 ||
(((_b = error.response) === null || _b === void 0 ? void 0 : _b.body) &&
typeof error.response.body === 'object' &&
error.response.body.message &&
error.response.body.message.includes('rate limit'))) {
// Extract retry time
let retryAfter = '60';
let errorMsg = '';
if (((_c = error.response) === null || _c === void 0 ? void 0 : _c.headers) && error.response.headers['retry-after']) {
retryAfter = error.response.headers['retry-after'];
}
else if (((_d = error.response) === null || _d === void 0 ? void 0 : _d.body) && typeof error.response.body === 'object') {
errorMsg = error.response.body.message || '';
const match = errorMsg.match(/(\d+) seconds/);
if (match && match[1]) {
retryAfter = match[1];
}
}
else if (typeof error.error === 'object' && error.error.message) {
errorMsg = error.error.message;
const match = errorMsg.match(/(\d+) seconds/);
if (match && match[1]) {
retryAfter = match[1];
}
}
const retrySeconds = parseInt(retryAfter, 10) || 60;
handleRateLimiting(cacheKey, {
message: errorMsg,
retryAfter: retrySeconds,
});
throw new Error(`API rate limited during authentication. Please try again after ${retrySeconds} seconds.`);
}
throw new Error(`Authentication failed: ${error.message}`);
}
}
/**
* Make an API request to Aruba Central with OAuth2 authentication
*/
async function apiRequest(method, endpoint, body = {}, qs = {}) {
var _a;
logger_1.logger.debug('api:request', `${method} ${endpoint} started`);
logger_1.logger.debug('api:request:params', JSON.stringify(qs, null, 2));
const credentials = await this.getCredentials('ArubaCentralOAuth2Api');
if (!credentials) {
logger_1.logger.error('api:request', 'No credentials provided');
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No credentials provided');
}
logger_1.logger.debug('api:request', 'Credentials loaded successfully');
const baseUrl = credentials.baseUrl;
// Print credential object structure (without sensitive values)
const credentialKeys = Object.keys(credentials);
logger_1.logger.debug('api:request:credentials', `Available credential keys: ${JSON.stringify(credentialKeys)}`);
if (credentials.oauthTokenData) {
const tokenDataKeys = Object.keys(credentials.oauthTokenData);
logger_1.logger.debug('api:request:credentials', `Token data keys: ${JSON.stringify(tokenDataKeys)}`);
}
else {
logger_1.logger.debug('api:request:credentials', 'No oauthTokenData found in credentials');
}
try {
// Get access token using the OAuth2 flow
logger_1.logger.debug('api:request', 'Getting access token');
const accessToken = await getAccessToken.call(this, credentials);
logger_1.logger.debug('api:request', 'Successfully obtained access token');
// Make the actual API request
logger_1.logger.debug('api:request', `Making ${method} request to ${endpoint}`);
const requestOptions = {
method,
url: `${baseUrl}${endpoint}`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body,
qs,
json: true,
resolveWithFullResponse: true,
};
logger_1.logger.debug('api:request:options', JSON.stringify({
method: requestOptions.method,
url: requestOptions.url,
headers: {
'Content-Type': (_a = requestOptions.headers) === null || _a === void 0 ? void 0 : _a['Content-Type'],
Authorization: '***',
},
qs: requestOptions.qs,
body: requestOptions.body,
}, null, 2));
// Use n8n's request helper
const response = await this.helpers.httpRequest(requestOptions);
logger_1.logger.debug('api:request:response', `Response received`);
return response.body || response;
}
catch (error) {
logger_1.logger.error('api:request:exception', error.message);
// Handle API-specific errors
if (error.response) {
logger_1.logger.error('api:request:error', `Status: ${error.statusCode}, Body: ${JSON.stringify(error.error)}`);
let message = 'Unknown error';
const errorBody = error.error || {};
// More detailed error message extraction
if (typeof errorBody === 'object') {
if (errorBody.description) {
message = errorBody.description;
}
else if (errorBody.error_description) {
message = errorBody.error_description;
}
else if (errorBody.message) {
message = errorBody.message;
}
else if (errorBody.error) {
message = errorBody.error;
}
}
else if (typeof errorBody === 'string') {
try {
const parsedBody = JSON.parse(errorBody);
if (parsedBody.message) {
message = parsedBody.message;
}
}
catch (e) {
// If parsing fails, use the string if it's not too long
if (errorBody.length < 300) {
message = errorBody;
}
}
}
logger_1.logger.error('api:request:error', `Formatted message: ${message}`);
// Handle rate limiting specifically
if (error.statusCode === 429) {
const cacheKey = `${credentials.baseUrl}_${credentials.clientId}`;
// Extract retry time
let retryAfter = '60';
if (error.response.headers && error.response.headers['retry-after']) {
retryAfter = error.response.headers['retry-after'];
}
else if (message.includes('seconds')) {
const match = message.match(/(\d+) seconds/);
if (match && match[1]) {
retryAfter = match[1];
}
}
// Use the extracted retry time or a default
const retrySeconds = parseInt(retryAfter, 10) || 60;
handleRateLimiting(cacheKey, {
message,
retryAfter: retrySeconds,
});
throw new Error(`API rate limited. Please try again after ${retrySeconds} seconds.`);
}
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { message });
}
// Re-throw with original error
logger_1.logger.error('api:request:error', 'Re-throwing original error');
throw error;
}
finally {
logger_1.logger.debug('api:request', 'Request completed');
}
}