oauth-connector
Version:
OAuth Connector SDK for automatic token management and refresh with multiple persistence strategies
1,158 lines (1,146 loc) • 36.4 kB
JavaScript
import { scrypt, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
import { promisify } from 'util';
import { promises } from 'fs';
import { dirname } from 'path';
import { request } from 'https';
import { request as request$1 } from 'http';
import { URL } from 'url';
// src/utils/logger.ts
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
return LogLevel2;
})(LogLevel || {});
var Logger = class {
constructor(level = 1 /* INFO */, prefix = "") {
this.level = level;
this.prefix = prefix;
}
/**
* Set log level
*/
setLevel(level) {
this.level = level;
}
/**
* Set prefix for log messages
*/
setPrefix(prefix) {
this.prefix = prefix;
}
/**
* Get formatted timestamp
*/
getTimestamp() {
return (/* @__PURE__ */ new Date()).toISOString();
}
/**
* Format log message with timestamp
*/
formatMessage(level, message) {
const timestamp = this.getTimestamp();
const prefixPart = this.prefix ? ` [${this.prefix}]` : "";
return `[${timestamp}] [${level}]${prefixPart} ${message}`;
}
/**
* Debug log
*/
debug(message, ...args) {
if (this.level <= 0 /* DEBUG */) {
console.debug(this.formatMessage("DEBUG", message), ...args);
}
}
/**
* Info log
*/
info(message, ...args) {
if (this.level <= 1 /* INFO */) {
console.info(this.formatMessage("INFO", message), ...args);
}
}
/**
* Warn log
*/
warn(message, ...args) {
if (this.level <= 2 /* WARN */) {
console.warn(this.formatMessage("WARN", message), ...args);
}
}
/**
* Error log
*/
error(message, ...args) {
if (this.level <= 3 /* ERROR */) {
console.error(this.formatMessage("ERROR", message), ...args);
}
}
};
var defaultLogger = new Logger();
// src/token-manager.ts
var TokenManager = class {
// In-memory cache
constructor(oauthService, storageStrategy, backgroundSyncIntervalInSecs, graceExpiryTimeInSecs, logger) {
// seconds before expiry to refresh
this.cachedTokenData = null;
this.oauthService = oauthService;
this.storageStrategy = storageStrategy;
this.backgroundSyncIntervalSecs = backgroundSyncIntervalInSecs;
this.graceExpiryTimeSecs = graceExpiryTimeInSecs || 0;
this.logger = logger || new Logger(1 /* INFO */);
}
/**
* Check if token is expired or within grace period
*/
isTokenExpired(tokenData) {
if (!tokenData.expiresAt) {
return true;
}
const now = Date.now();
const gracePeriodMs = this.graceExpiryTimeSecs * 1e3;
const effectiveExpiryTime = tokenData.expiresAt - gracePeriodMs;
return now >= effectiveExpiryTime;
}
/**
* Get valid access token, refreshing if necessary
* Checks in-memory cache first for performance
*/
async getAccessToken() {
try {
if (this.cachedTokenData && !this.isTokenExpired(this.cachedTokenData)) {
this.logger.debug("Returning token from cache");
return this.cachedTokenData.accessToken;
}
let tokenData = null;
if (this.storageStrategy) {
tokenData = await this.storageStrategy.loadToken();
} else {
tokenData = this.cachedTokenData;
}
if (!tokenData || this.isTokenExpired(tokenData)) {
this.logger.debug("Token expired or missing, refreshing...");
const refreshToken = tokenData?.refreshToken || this.oauthService.getRefreshToken();
if (!refreshToken) {
throw new Error("No refresh token available. Please re-authenticate.");
}
tokenData = await this.oauthService.refreshAccessToken(refreshToken);
if (this.storageStrategy) {
await this.storageStrategy.saveToken(tokenData);
}
this.logger.debug("Token refreshed and saved");
}
this.cachedTokenData = tokenData;
this.logger.debug("Token cached in memory");
return tokenData.accessToken;
} catch (error) {
this.cachedTokenData = null;
this.logger.error(
`Failed to get access token: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Manually refresh token
*/
async refreshToken() {
try {
let tokenData = null;
if (this.storageStrategy) {
tokenData = await this.storageStrategy.loadToken();
} else {
tokenData = this.cachedTokenData;
}
const refreshToken = tokenData?.refreshToken || this.oauthService.getRefreshToken();
if (!refreshToken) {
throw new Error("No refresh token available. Please re-authenticate.");
}
this.logger.debug("Manually refreshing token...");
const newTokenData = await this.oauthService.refreshAccessToken(refreshToken);
if (this.storageStrategy) {
await this.storageStrategy.saveToken(newTokenData);
}
this.cachedTokenData = newTokenData;
this.logger.debug("Token manually refreshed and saved");
return newTokenData;
} catch (error) {
this.cachedTokenData = null;
this.logger.error(
`Failed to refresh token: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Start background token sync
*/
startBackgroundSync() {
if (this.backgroundSyncInterval) {
this.logger.debug("Background sync already running");
return;
}
if (!this.backgroundSyncIntervalSecs) {
this.logger.debug("Background sync is not configured");
return;
}
this.logger.debug(
`Starting background sync (interval: ${this.backgroundSyncIntervalSecs} seconds)`
);
const intervalMs = this.backgroundSyncIntervalSecs * 1e3;
this.backgroundSyncInterval = setInterval(async () => {
try {
this.logger.debug("Background sync: checking token...");
let tokenData = this.cachedTokenData;
if (!tokenData && this.storageStrategy) {
tokenData = await this.storageStrategy.loadToken();
}
if (tokenData && this.isTokenExpired(tokenData)) {
this.logger.debug("Background sync: token expired, refreshing...");
const refreshToken = tokenData.refreshToken || this.oauthService.getRefreshToken();
if (refreshToken) {
const newTokenData = await this.oauthService.refreshAccessToken(refreshToken);
if (this.storageStrategy) {
await this.storageStrategy.saveToken(newTokenData);
}
this.cachedTokenData = newTokenData;
this.logger.debug("Background sync: token refreshed successfully");
} else {
this.logger.warn("Background sync: no refresh token available");
}
} else {
this.logger.debug("Background sync: token still valid");
}
} catch (error) {
this.cachedTokenData = null;
this.logger.error(
`Background sync error: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}, intervalMs);
}
/**
* Stop background token sync
*/
stopBackgroundSync() {
if (this.backgroundSyncInterval) {
clearInterval(this.backgroundSyncInterval);
this.backgroundSyncInterval = void 0;
this.logger.debug("Background sync stopped");
}
}
/**
* Get current token data (uses cache)
*/
async getTokenData() {
if (this.cachedTokenData) {
return this.cachedTokenData;
}
if (this.storageStrategy) {
const tokenData = await this.storageStrategy.loadToken();
if (tokenData) {
this.cachedTokenData = tokenData;
}
return tokenData;
}
return null;
}
/**
* Set cached token (used after exchangeCodeForTokens)
*/
setCachedToken(tokenData) {
this.cachedTokenData = tokenData;
this.logger.debug("Token cached in memory");
}
/**
* Clear cached token
*/
clearCache() {
this.cachedTokenData = null;
this.logger.debug("Token cache cleared");
}
/**
* Cleanup resources
*/
destroy() {
this.stopBackgroundSync();
this.clearCache();
}
};
// src/connector.ts
var Connector = class {
constructor(oauthService, storageStrategy, options = {}) {
this.oauthService = oauthService;
this.storageStrategy = storageStrategy;
const logLevel = options.debug ? 0 /* DEBUG */ : 1 /* INFO */;
this.logger = new Logger(logLevel, options.instanceId);
if (options.instanceId && storageStrategy && "setInstanceId" in storageStrategy) {
storageStrategy.setInstanceId(options.instanceId);
}
const backgroundSyncIntervalInSecs = options.backgroundSyncIntervalInSecs;
const graceExpiryTimeInSecs = options.graceExpiryTimeInSecs;
this.tokenManager = new TokenManager(
this.oauthService,
this.storageStrategy,
backgroundSyncIntervalInSecs,
graceExpiryTimeInSecs,
this.logger
);
if (backgroundSyncIntervalInSecs) {
this.tokenManager.startBackgroundSync();
}
this.logger.debug(`Connector initialized (instance: ${options.instanceId || "default"})`);
}
/**
* Get access token (auto-refreshes if expired)
*/
async getAccessToken() {
try {
if (this.onFetchingAccessToken) {
await this.onFetchingAccessToken();
}
const token = await this.tokenManager.getAccessToken();
const tokenData = await this.tokenManager.getTokenData();
if (tokenData) {
const timeUntilExpiry = tokenData.expiresAt - Date.now();
if (timeUntilExpiry > 25 * 60 * 1e3) {
if (this.onTokenRefreshed) {
await this.onTokenRefreshed(tokenData);
}
}
}
return token;
} catch (error) {
const err = error instanceof Error ? error : new Error("Unknown error");
if (this.onTokenError) {
await this.onTokenError(err);
}
this.logger.error(`Failed to get access token: ${err.message}`);
throw err;
}
}
/**
* Manually refresh token
*/
async refreshToken() {
try {
if (this.onFetchingAccessToken) {
await this.onFetchingAccessToken();
}
const tokenData = await this.tokenManager.refreshToken();
if (this.onTokenRefreshed) {
await this.onTokenRefreshed(tokenData);
}
return tokenData;
} catch (error) {
const err = error instanceof Error ? error : new Error("Unknown error");
if (this.onTokenError) {
await this.onTokenError(err);
}
this.logger.error(`Failed to refresh token: ${err.message}`);
throw err;
}
}
/**
* Get current token data
*/
async getTokenData() {
return await this.tokenManager.getTokenData();
}
/**
* Get authorization URL for initial OAuth flow
*/
getAuthorizationUrl(redirectUri, scopes) {
return this.oauthService.getAuthorizationUrl(redirectUri, scopes);
}
/**
* Exchange authorization code for tokens
*/
async exchangeCodeForTokens(code, redirectUri) {
try {
this.logger.debug("Exchanging authorization code for tokens");
const tokenData = await this.oauthService.exchangeCodeForTokens(code, redirectUri);
if (this.storageStrategy) {
await this.storageStrategy.saveToken(tokenData);
}
this.tokenManager.setCachedToken(tokenData);
this.logger.debug("Tokens obtained and saved");
return tokenData;
} catch (error) {
this.logger.error(
`Failed to exchange code for tokens: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Delete stored token
*/
async deleteToken() {
try {
if (this.storageStrategy) {
await this.storageStrategy.deleteToken();
}
this.tokenManager.clearCache();
this.logger.debug("Token deleted");
} catch (error) {
this.logger.error(
`Failed to delete token: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Start background sync
*/
startBackgroundSync() {
this.tokenManager.startBackgroundSync();
}
/**
* Stop background sync
*/
stopBackgroundSync() {
this.tokenManager.stopBackgroundSync();
}
/**
* Cleanup resources
*/
destroy() {
this.tokenManager.destroy();
this.logger.debug("Connector destroyed");
}
};
var scryptAsync = promisify(scrypt);
var EncryptionService = class {
constructor() {
this.algorithm = "aes-256-gcm";
this.keyLength = 32;
// 256 bits
this.ivLength = 16;
// 128 bits
this.saltLength = 16;
this.tagLength = 16;
}
/**
* Derive key from password using scrypt
*/
async deriveKey(password, salt) {
return await scryptAsync(password, salt, this.keyLength);
}
/**
* Encrypt data
*/
async encrypt(data, password) {
try {
const salt = randomBytes(this.saltLength);
const iv = randomBytes(this.ivLength);
const key = await this.deriveKey(password, salt);
const cipher = createCipheriv(this.algorithm, key, iv);
let encrypted = cipher.update(data, "utf8", "hex");
encrypted += cipher.final("hex");
const tag = cipher.getAuthTag();
const result = Buffer.concat([salt, iv, tag, Buffer.from(encrypted, "hex")]).toString(
"base64"
);
return result;
} catch (error) {
throw new Error(
`Encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
/**
* Decrypt data
*/
async decrypt(encryptedData, password) {
try {
const data = Buffer.from(encryptedData, "base64");
const salt = data.subarray(0, this.saltLength);
const iv = data.subarray(this.saltLength, this.saltLength + this.ivLength);
const tag = data.subarray(
this.saltLength + this.ivLength,
this.saltLength + this.ivLength + this.tagLength
);
const encrypted = data.subarray(this.saltLength + this.ivLength + this.tagLength);
const key = await this.deriveKey(password, salt);
const decipher = createDecipheriv(this.algorithm, key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted, void 0, "utf8");
decrypted += decipher.final("utf8");
return decrypted;
} catch (error) {
throw new Error(
`Decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
};
var defaultEncryptionService = new EncryptionService();
// src/strategies/storage-strategy.ts
var StorageStrategy = class {
constructor(encryptionKey, logger) {
this.encryptionKey = encryptionKey;
this.encryptionService = defaultEncryptionService;
this.logger = logger || defaultLogger;
}
/**
* Encrypt token data if encryption key is provided
*/
async encryptIfNeeded(data) {
if (this.encryptionKey) {
return await this.encryptionService.encrypt(data, this.encryptionKey);
}
return data;
}
/**
* Decrypt token data if encryption key is provided
*/
async decryptIfNeeded(data) {
if (this.encryptionKey) {
return await this.encryptionService.decrypt(data, this.encryptionKey);
}
return data;
}
};
var LocalStorageStrategy = class extends StorageStrategy {
constructor(config, logger) {
super(config.encryptionKey, logger);
this.filePath = config.filePath;
}
/**
* Save token to local file
*/
async saveToken(tokenData) {
try {
this.logger.debug(`Saving token to local file: ${this.filePath}`);
const data = JSON.stringify(tokenData);
const encrypted = await this.encryptIfNeeded(data);
const dir = dirname(this.filePath);
await promises.mkdir(dir, { recursive: true });
await promises.writeFile(this.filePath, encrypted, "utf8");
this.logger.debug("Token saved successfully");
} catch (error) {
this.logger.error(
`Failed to save token: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Load token from local file
*/
async loadToken() {
try {
this.logger.debug(`Loading token from local file: ${this.filePath}`);
const data = await promises.readFile(this.filePath, "utf8");
const decrypted = await this.decryptIfNeeded(data);
const tokenData = JSON.parse(decrypted);
this.logger.debug("Token loaded successfully");
return tokenData;
} catch (error) {
if (error.code === "ENOENT") {
this.logger.debug("Token file does not exist");
return null;
}
this.logger.error(
`Failed to load token: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Delete token file
*/
async deleteToken() {
try {
this.logger.debug(`Deleting token file: ${this.filePath}`);
await promises.unlink(this.filePath);
this.logger.debug("Token file deleted successfully");
} catch (error) {
if (error.code === "ENOENT") {
this.logger.debug("Token file does not exist, nothing to delete");
return;
}
this.logger.error(
`Failed to delete token: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
};
// src/strategies/remote-storage-strategy.ts
var RemoteStorageStrategy = class extends StorageStrategy {
constructor(config, logger) {
super(config.encryptionKey, logger);
this.onUpload = config.onUpload;
this.onDownload = config.onDownload;
}
/**
* Save token using remote upload callback
*/
async saveToken(tokenData) {
try {
this.logger.debug("Saving token to remote storage");
let dataToUpload = tokenData;
if (this.encryptionKey) {
const dataString = JSON.stringify(tokenData);
const encrypted = await this.encryptIfNeeded(dataString);
dataToUpload = {
_encrypted: true,
_data: encrypted
};
}
await this.onUpload(dataToUpload);
this.logger.debug("Token saved to remote storage successfully");
} catch (error) {
this.logger.error(
`Failed to save token to remote storage: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Load token using remote download callback
*/
async loadToken() {
try {
this.logger.debug("Loading token from remote storage");
const tokenData = await this.onDownload();
if (!tokenData) {
this.logger.debug("No token found in remote storage");
return null;
}
const encryptedData = tokenData;
if (this.encryptionKey && encryptedData._encrypted && encryptedData._data) {
try {
const decrypted = await this.decryptIfNeeded(encryptedData._data);
const parsed = JSON.parse(decrypted);
this.logger.debug("Token decrypted and loaded from remote storage successfully");
return parsed;
} catch (error) {
this.logger.error(
`Failed to decrypt token: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
this.logger.debug("Token loaded from remote storage successfully");
return tokenData;
} catch (error) {
this.logger.error(
`Failed to load token from remote storage: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Delete token from remote storage
* Note: This requires onUpload to handle deletion (upload null/empty)
*/
async deleteToken() {
try {
this.logger.debug("Deleting token from remote storage");
await this.onUpload(null);
this.logger.debug("Token deleted from remote storage successfully");
} catch (error) {
this.logger.error(
`Failed to delete token from remote storage: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
};
var HttpClient = class {
constructor(logger) {
this.logger = logger || defaultLogger;
}
/**
* Make HTTP/HTTPS request
*/
async request(url, options = {}) {
return new Promise((resolve, reject) => {
try {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === "https:";
const requestModule = isHttps ? request : request$1;
const requestOptions = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: options.method || "GET",
headers: {
...options.headers
}
};
if (options.body) {
requestOptions.headers = {
...requestOptions.headers,
"Content-Length": Buffer.byteLength(options.body)
};
}
if (options.timeout) {
requestOptions.timeout = options.timeout;
}
this.logger.debug(`Making ${requestOptions.method} request to: ${url}`);
const req = requestModule(requestOptions, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
const response = {
statusCode: res.statusCode || 500,
statusMessage: res.statusMessage || "Unknown",
headers: res.headers,
data: this.parseResponse(data, res.headers["content-type"])
};
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
this.logger.debug(`Request successful: ${res.statusCode}`);
resolve(response);
} else {
const error = new Error(
`HTTP ${res.statusCode}: ${res.statusMessage || "Unknown error"} - ${data}`
);
this.logger.error(`Request failed: ${error.message}`);
reject(error);
}
} catch (parseError) {
const error = new Error(
`Failed to parse response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`
);
this.logger.error(`Parse error: ${error.message}`);
reject(error);
}
});
});
req.on("error", (error) => {
this.logger.error(`Request error: ${error.message}`);
reject(error);
});
req.on("timeout", () => {
req.destroy();
const error = new Error(`Request timeout after ${options.timeout}ms`);
this.logger.error(`Request timeout: ${error.message}`);
reject(error);
});
if (options.body) {
req.write(options.body);
}
req.end();
} catch (error) {
const err = error instanceof Error ? error : new Error("Unknown error");
this.logger.error(`Request setup error: ${err.message}`);
reject(err);
}
});
}
/**
* Make POST request
*/
async post(url, body, headers) {
return this.request(url, {
method: "POST",
headers,
body
});
}
/**
* Make GET request
*/
async get(url, headers) {
return this.request(url, {
method: "GET",
headers
});
}
/**
* Parse response data based on content type
*/
parseResponse(data, contentType) {
if (!data) {
return {};
}
const contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType;
if (contentTypeStr && contentTypeStr.includes("application/json")) {
try {
return JSON.parse(data);
} catch (error) {
this.logger.warn("Failed to parse JSON response, returning raw string");
return data;
}
}
if (data.trim().startsWith("{") || data.trim().startsWith("[")) {
try {
return JSON.parse(data);
} catch {
return data;
}
}
return data;
}
};
// src/strategies/catalyst-cache-storage-strategy.ts
var CatalystCacheStorageStrategy = class extends StorageStrategy {
constructor(config, logger) {
super(config?.encryptionKey, logger);
this.key = "token";
this.httpReq = config?.httpReq;
this.key = config?.key ?? "token";
this.httpClient = new HttpClient(this.logger);
}
getApiUrl() {
const apiDomain = process.env.X_ZOHO_CATALYST_CONSOLE_URL;
const projectId = process.env.CATALYST_PROJECT_ID;
const apiPath = `/baas/v1/project/${projectId}/segment/Default/cache`;
return apiDomain + apiPath;
}
getReqHeaders() {
const oauthToken = this.httpReq?.headers["x-zc-user-cred-token"] || this.httpReq?.catalystHeaders?.["x-zc-user-cred-token"];
const projectKey = this.httpReq?.headers["x-zc-project-key"] || this.httpReq?.catalystHeaders?.["x-zc-project-key"];
return {
Authorization: `Bearer ${oauthToken}`,
PROJECT_ID: projectKey,
"Content-Type": "application/json"
};
}
async getCache(key) {
try {
const apiUrl = this.getApiUrl() + `?cacheKey=${key}`;
const reqHeaders = this.getReqHeaders();
const response = await this.httpClient.get(apiUrl, reqHeaders);
if (response.data && response.data.status === "success" && response.data.data) {
return response.data.data.cache_value;
}
return null;
} catch (error) {
this.logger.error(
`Failed to get cache: ${error instanceof Error ? error.message : "Unknown error"}`
);
return null;
}
}
async setCache(key, value) {
try {
const apiUrl = this.getApiUrl();
const reqHeaders = this.getReqHeaders();
const reqBody = {
cache_name: key,
cache_value: value,
expiry_in_hours: "1"
};
await this.httpClient.post(
apiUrl,
JSON.stringify(reqBody),
reqHeaders
);
} catch (error) {
this.logger.error(
`Failed to set cache: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Save token to Catalyst cache
*/
async saveToken(tokenData) {
if (!this.httpReq) {
this.logger.warn("HTTP request object is null and strategy is not supported");
return;
}
try {
this.logger.debug(`Saving token to Catalyst cache with key: ${this.key}`);
const dataString = JSON.stringify(tokenData);
const dataToStore = await this.encryptIfNeeded(dataString);
await this.setCache(this.key, dataToStore);
this.logger.debug("Token saved to Catalyst cache successfully");
} catch (error) {
this.logger.error(
`Failed to save token to Catalyst cache: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Load token from Catalyst cache
*/
async loadToken() {
if (!this.httpReq) {
this.logger.warn("HTTP request object is null and strategy is not supported");
return null;
}
try {
this.logger.debug(`Loading token from Catalyst cache with key: ${this.key}`);
const cachedValue = await this.getCache(this.key);
if (!cachedValue) {
this.logger.debug("No token found in Catalyst cache");
return null;
}
const decrypted = await this.decryptIfNeeded(cachedValue);
const tokenData = JSON.parse(decrypted);
this.logger.debug("Token loaded from Catalyst cache successfully");
return tokenData;
} catch (error) {
if (error instanceof SyntaxError) {
this.logger.error("Failed to parse token data from Catalyst cache");
return null;
}
this.logger.error(
`Failed to load token from Catalyst cache: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Delete token from Catalyst cache
*/
async deleteToken() {
if (!this.httpReq) {
this.logger.warn("HTTP request object is null and strategy is not supported");
return;
}
try {
this.logger.debug(`Deleting token from Catalyst cache with key: ${this.key}`);
await this.setCache(this.key, "");
this.logger.debug("Token deleted from Catalyst cache successfully");
} catch (error) {
this.logger.error(
`Failed to delete token from Catalyst cache: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
};
// src/services/oauth-service.ts
var OAuthService = class {
constructor(config, logger) {
this.config = config;
this.logger = logger || defaultLogger;
this.httpClient = new HttpClient(this.logger);
}
/**
* Get refresh token from config (if available)
*/
getRefreshToken() {
return this.config.refreshToken;
}
/**
* Make token refresh request
*/
async makeTokenRequest(url, params) {
try {
this.logger.debug(`Making token request to: ${url}`);
const formData = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
formData.append(key, value);
});
const response = await this.httpClient.post(url, formData.toString(), {
"Content-Type": "application/x-www-form-urlencoded"
});
this.logger.debug("Token request successful");
return response.data;
} catch (error) {
this.logger.error(
`Token request error: ${error instanceof Error ? error.message : "Unknown error"}`
);
throw error;
}
}
/**
* Convert OAuth response to TokenData
*/
convertToTokenData(response) {
const expiresIn = response.expires_in || 3600;
const expiresAt = Date.now() + expiresIn * 1e3;
return {
accessToken: response.access_token,
refreshToken: response.refresh_token,
expiresAt,
tokenType: response.token_type || "Bearer",
scope: response.scope
};
}
};
// src/services/zoho-oauth.ts
var ZohoOAuth = class extends OAuthService {
constructor(config, logger) {
const baseUrl = `https://${config.accountsDomain}`;
const connectorConfig = {
clientId: config.clientId,
clientSecret: config.clientSecret,
authUrl: `${baseUrl}/oauth/v2/auth`,
refreshUrl: `${baseUrl}/oauth/v2/token`,
refreshToken: config.refreshToken
};
super(connectorConfig, logger);
this.zohoConfig = config;
this.authUrl = connectorConfig.authUrl;
this.refreshUrl = connectorConfig.refreshUrl;
}
/**
* Refresh access token using Zoho's refresh token endpoint
*/
async refreshAccessToken(refreshToken) {
this.logger.debug("Refreshing Zoho access token");
const params = {
refresh_token: refreshToken,
client_id: this.zohoConfig.clientId,
client_secret: this.zohoConfig.clientSecret,
grant_type: "refresh_token"
};
const response = await this.makeTokenRequest(this.refreshUrl, params);
const tokenData = this.convertToTokenData(response);
if (!tokenData.refreshToken) {
tokenData.refreshToken = refreshToken;
}
this.logger.debug("Zoho access token refreshed successfully");
return tokenData;
}
/**
* Get Zoho authorization URL
*/
getAuthorizationUrl(redirectUri, scopes = ["ZohoCRM.modules.ALL"]) {
const params = new URLSearchParams({
response_type: "code",
client_id: this.zohoConfig.clientId,
scope: scopes.join(","),
redirect_uri: redirectUri,
access_type: "offline",
prompt: "consent"
});
return `${this.authUrl}?${params.toString()}`;
}
/**
* Exchange authorization code for tokens
*/
async exchangeCodeForTokens(code, redirectUri) {
this.logger.debug("Exchanging Zoho authorization code for tokens");
const params = {
grant_type: "authorization_code",
client_id: this.zohoConfig.clientId,
client_secret: this.zohoConfig.clientSecret,
redirect_uri: redirectUri,
code
};
const response = await this.makeTokenRequest(this.refreshUrl, params);
const tokenData = this.convertToTokenData(response);
this.logger.debug("Zoho tokens obtained successfully");
return tokenData;
}
};
// src/services/google-oauth.ts
var GoogleOAuth = class extends OAuthService {
constructor(config, logger) {
super(config, logger);
}
/**
* Refresh access token using Google's refresh token endpoint
*/
async refreshAccessToken(refreshToken) {
this.logger.debug("Refreshing Google access token");
const params = {
refresh_token: refreshToken,
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
grant_type: "refresh_token"
};
const response = await this.makeTokenRequest(this.config.refreshUrl, params);
const tokenData = this.convertToTokenData(response);
if (!tokenData.refreshToken) {
tokenData.refreshToken = refreshToken;
}
this.logger.debug("Google access token refreshed successfully");
return tokenData;
}
/**
* Get Google authorization URL
*/
getAuthorizationUrl(redirectUri, scopes = ["openid", "email", "profile"]) {
const params = new URLSearchParams({
response_type: "code",
client_id: this.config.clientId,
scope: scopes.join(" "),
redirect_uri: redirectUri,
access_type: "offline",
prompt: "consent"
});
return `${this.config.authUrl}?${params.toString()}`;
}
/**
* Exchange authorization code for tokens
*/
async exchangeCodeForTokens(code, redirectUri) {
this.logger.debug("Exchanging Google authorization code for tokens");
const params = {
grant_type: "authorization_code",
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
redirect_uri: redirectUri,
code
};
const response = await this.makeTokenRequest(this.config.refreshUrl, params);
const tokenData = this.convertToTokenData(response);
this.logger.debug("Google tokens obtained successfully");
return tokenData;
}
};
// src/services/oauth.ts
var OAuth = class extends OAuthService {
constructor(config, logger) {
super(config, logger);
}
/**
* Refresh access token using generic OAuth refresh flow
*/
async refreshAccessToken(refreshToken) {
this.logger.debug("Refreshing access token (generic OAuth)");
const params = {
refresh_token: refreshToken,
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
grant_type: "refresh_token"
};
const response = await this.makeTokenRequest(this.config.refreshUrl, params);
const tokenData = this.convertToTokenData(response);
if (!tokenData.refreshToken) {
tokenData.refreshToken = refreshToken;
}
this.logger.debug("Access token refreshed successfully (generic OAuth)");
return tokenData;
}
/**
* Get authorization URL (generic implementation)
*/
getAuthorizationUrl(redirectUri, scopes = []) {
const params = new URLSearchParams({
response_type: "code",
client_id: this.config.clientId,
redirect_uri: redirectUri,
access_type: "offline",
prompt: "consent"
});
if (scopes.length > 0) {
params.append("scope", scopes.join(" "));
}
return `${this.config.authUrl}?${params.toString()}`;
}
/**
* Exchange authorization code for tokens
*/
async exchangeCodeForTokens(code, redirectUri) {
this.logger.debug("Exchanging authorization code for tokens (generic OAuth)");
const params = {
grant_type: "authorization_code",
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
redirect_uri: redirectUri,
code
};
const response = await this.makeTokenRequest(this.config.refreshUrl, params);
const tokenData = this.convertToTokenData(response);
this.logger.debug("Tokens obtained successfully (generic OAuth)");
return tokenData;
}
};
export { CatalystCacheStorageStrategy, Connector, EncryptionService, GoogleOAuth, HttpClient, LocalStorageStrategy, LogLevel, Logger, OAuth, OAuthService, RemoteStorageStrategy, StorageStrategy, TokenManager, ZohoOAuth };