UNPKG

@crowdin/app-project-module

Version:

Module that generates for you all common endpoints for serving standalone Crowdin App

467 lines (466 loc) 25.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.prepareCrowdinClient = prepareCrowdinClient; exports.resolveIntegrationAccess = resolveIntegrationAccess; exports.prepareIntegrationCredentials = prepareIntegrationCredentials; const crowdin_api_client_1 = __importDefault(require("@crowdin/crowdin-api-client")); const axios_1 = __importDefault(require("axios")); const os = __importStar(require("os")); const _1 = require("."); const defaults_1 = require("../modules/integration/util/defaults"); const storage_1 = require("../storage"); const types_1 = require("../types"); const token_1 = require("./app-functions/token"); const axios_2 = require("./axios"); const logger_1 = require("./logger"); const axiosCustom = new axios_2.AxiosProvider().axios; function prepareCrowdinClient(_a) { return __awaiter(this, arguments, void 0, function* ({ config, credentials, autoRenew = false, context, }) { var _b, _c, _d, _e, _f, _g, _h, _j; let userAgent; if (config.crowdinApiUserAgent) { userAgent = config.crowdinApiUserAgent; } else { userAgent = `app/${config.name} node/v${process.versions.node} ${os.type()}/${os.platform()}/${os.release()}`; } //2 min as an extra buffer const isExpired = +credentials.expire - 120 < Date.now() / 1000; const organization = credentials.type === types_1.AccountType.ENTERPRISE ? credentials.id : undefined; if (config.authenticationType === types_1.AuthenticationType.APP_WITH_CODE) { const code = (_b = context === null || context === void 0 ? void 0 : context.jwtPayload) === null || _b === void 0 ? void 0 : _b.code; if (!code) { throw new Error(`Missing code in JWT payload for ${types_1.AuthenticationType.APP_WITH_CODE} authentication`); } const freshToken = yield (0, token_1.fetchAppWithCodeToken)({ appId: config.identifier, appSecret: credentials.appSecret, clientId: config.clientId, clientSecret: config.clientSecret, domain: credentials.domain || '', userId: credentials.userId, code, url: (_c = config.crowdinUrls) === null || _c === void 0 ? void 0 : _c.accountUrl, }); const token = freshToken.accessToken; return { client: new crowdin_api_client_1.default({ token, organization, apiDomain: (_d = config.crowdinUrls) === null || _d === void 0 ? void 0 : _d.apiDomain, baseUrl: (_e = config.crowdinUrls) === null || _e === void 0 ? void 0 : _e.apiUrl, }, { userAgent }), token, }; } let token; let crowdinCreds; if (!isExpired) { token = (0, _1.decryptData)(config, credentials.accessToken); crowdinCreds = credentials; } else { const res = yield refreshCrowdinCreds({ config, credentials }); token = res.token; crowdinCreds = res.credentials; } if (!autoRenew) { return { client: new crowdin_api_client_1.default({ token, organization, apiDomain: (_f = config.crowdinUrls) === null || _f === void 0 ? void 0 : _f.apiDomain, baseUrl: (_g = config.crowdinUrls) === null || _g === void 0 ? void 0 : _g.apiUrl, }, { userAgent }), token, }; } const getHttpConfig = (config) => { if (!config) { return; } const headers = Object.assign(Object.assign({}, config.headers), { Authorization: `Bearer ${token}` }); if (userAgent) { headers['User-Agent'] = userAgent; } return { headers }; }; const renewToken = () => __awaiter(this, void 0, void 0, function* () { const res = yield refreshCrowdinCreds({ config, credentials: crowdinCreds }); token = res.token; crowdinCreds = res.credentials; }); const isExpiredCause = (error) => { var _a, _b; const errorObj = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error; return (errorObj === null || errorObj === void 0 ? void 0 : errorObj.code) === 401 && (errorObj === null || errorObj === void 0 ? void 0 : errorObj.message) === 'Unauthorized'; }; //providing custom http client to handle expirations and renew token on the fly //needed for long running jobs with single Crowdin client instance return { client: new crowdin_api_client_1.default({ token, organization, apiDomain: (_h = config.crowdinUrls) === null || _h === void 0 ? void 0 : _h.apiDomain, baseUrl: (_j = config.crowdinUrls) === null || _j === void 0 ? void 0 : _j.apiUrl, }, { httpClient: { get(url, httpConfig) { return __awaiter(this, void 0, void 0, function* () { try { const res = yield axiosCustom.get(url, getHttpConfig(httpConfig)); return res.data; } catch (e) { if (isExpiredCause(e)) { yield renewToken(); const res = yield axiosCustom.get(url, getHttpConfig(httpConfig)); return res.data; } (0, logger_1.logError)(e, context); throw e; } }); }, delete(url, httpConfig) { return __awaiter(this, void 0, void 0, function* () { try { const res = yield axiosCustom.delete(url, getHttpConfig(httpConfig)); return res.data; } catch (e) { if (isExpiredCause(e)) { yield renewToken(); const res = yield axiosCustom.delete(url, getHttpConfig(httpConfig)); return res.data; } (0, logger_1.logError)(e, context); throw e; } }); }, head(url, httpConfig) { return __awaiter(this, void 0, void 0, function* () { try { const res = yield axiosCustom.head(url, getHttpConfig(httpConfig)); return res.data; } catch (e) { if (isExpiredCause(e)) { yield renewToken(); const res = yield axiosCustom.head(url, getHttpConfig(httpConfig)); return res.data; } (0, logger_1.logError)(e, context); throw e; } }); }, patch(url, data, httpConfig) { return __awaiter(this, void 0, void 0, function* () { try { const res = yield axiosCustom.patch(url, data, getHttpConfig(httpConfig)); return res.data; } catch (e) { if (isExpiredCause(e)) { yield renewToken(); const res = yield axiosCustom.patch(url, data, getHttpConfig(httpConfig)); return res.data; } (0, logger_1.logError)(e, context); throw e; } }); }, post(url, data, httpConfig) { return __awaiter(this, void 0, void 0, function* () { try { const res = yield axiosCustom.post(url, data, getHttpConfig(httpConfig)); return res.data; } catch (e) { if (isExpiredCause(e)) { yield renewToken(); const res = yield axiosCustom.post(url, data, getHttpConfig(httpConfig)); return res.data; } (0, logger_1.logError)(e, context); throw e; } }); }, put(url, data, httpConfig) { return __awaiter(this, void 0, void 0, function* () { try { const res = yield axiosCustom.put(url, data, getHttpConfig(httpConfig)); return res.data; } catch (e) { if (isExpiredCause(e)) { yield renewToken(); const res = yield axiosCustom.put(url, data, getHttpConfig(httpConfig)); return res.data; } (0, logger_1.logError)(e, context); throw e; } }); }, }, userAgent, }), token, }; }); } function resolveIntegrationAccess(clientId) { return __awaiter(this, void 0, void 0, function* () { const { organization, projectId, userId } = (0, token_1.parseCrowdinId)(clientId); let integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(clientId); const ownerIds = []; let projectIntegrationCredentials = []; if (!integrationCredentials) { projectIntegrationCredentials = (yield (0, storage_1.getStorage)().getAllIntegrationCredentials(organization)).filter((item) => { const { organization: itemOrganization, projectId: itemProjectId } = (0, token_1.parseCrowdinId)(item.id); return itemOrganization === organization && itemProjectId === projectId; }); for (const credentials of projectIntegrationCredentials) { ownerIds.push((0, token_1.parseCrowdinId)(credentials.id).userId); if (credentials.managers) { const managers = JSON.parse(credentials.managers) || []; if (managers.includes(`${userId}`)) { integrationCredentials = credentials; clientId = credentials.id; break; } } } } return { integrationCredentials: integrationCredentials || null, projectIntegrationCredentials, projectId, clientId, ownerIds, }; }); } function prepareIntegrationCredentials(config, integration, integrationCredentials) { return __awaiter(this, void 0, void 0, function* () { const credentials = JSON.parse((0, _1.decryptData)(config, integrationCredentials.credentials)); credentials.ownerId = (0, token_1.parseCrowdinId)(integrationCredentials.id).userId; const oauthLogin = integration.oauthLogin; const integrationLogin = integration.loginForm; // Add tokenProvider function to credentials for dynamic token refresh if ((oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.refresh) || (integrationLogin === null || integrationLogin === void 0 ? void 0 : integrationLogin.refresh)) { const performRefreshTokenRequest = (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.performRefreshTokenRequest) || (integrationLogin === null || integrationLogin === void 0 ? void 0 : integrationLogin.performRefreshTokenRequest); credentials.tokenProvider = { getToken: (...args_1) => __awaiter(this, [...args_1], void 0, function* (force = false) { var _a, _b, _c, _d; const { expireIn } = credentials; //2 min as an extra buffer const isExpired = !expireIn || expireIn - 120 < Date.now() / 1000; if (!force && !isExpired) { return credentials.accessToken; } (0, logger_1.log)(force ? 'Force refreshing integration credentials' : 'Integration credentials have expired during operation. Requesting a new credentials'); const doRefresh = (currentCredentials) => __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; if (performRefreshTokenRequest) { const loginForm = yield (0, storage_1.getStorage)().getMetadata((0, defaults_1.getOAuthLoginFormId)(integrationCredentials.id)); return performRefreshTokenRequest({ credentials: currentCredentials, loginForm }); } else if (oauthLogin) { const url = oauthLogin.refreshTokenUrl || oauthLogin.accessTokenUrl; const request = {}; request[((_a = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _a === void 0 ? void 0 : _a.clientId) || 'client_id'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientId; request[((_b = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _b === void 0 ? void 0 : _b.clientSecret) || 'client_secret'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientSecret; request[((_c = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _c === void 0 ? void 0 : _c.refreshToken) || 'refresh_token'] = currentCredentials.refreshToken; if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraRefreshTokenParameters) { Object.entries(oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraRefreshTokenParameters).forEach(([key, value]) => (request[key] = value)); } return (yield axios_1.default.post(url || '', request, { headers: { Accept: 'application/json' }, })).data; } return null; }); let newCredentials; try { newCredentials = yield doRefresh(credentials); } catch (e) { // In-memory refresh token may be stale (rotated by another process/worker, // or the token itself expired). Try fetching fresh credentials from DB. (0, logger_1.log)('Token refresh failed. Attempting to fetch fresh credentials from the database'); const freshStoredCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(integrationCredentials.id); if (freshStoredCredentials) { const freshCredentials = JSON.parse((0, _1.decryptData)(config, freshStoredCredentials.credentials)); if (freshCredentials.refreshToken && freshCredentials.refreshToken !== credentials.refreshToken) { (0, logger_1.log)('Found a newer refresh token in the database, retrying token refresh'); try { newCredentials = yield doRefresh(freshCredentials); credentials.refreshToken = freshCredentials.refreshToken; } catch (retryError) { (0, logger_1.logError)(retryError); throw retryError; } } else { throw e; } } else { throw e; } } if (!newCredentials) { return credentials.accessToken; } credentials.accessToken = newCredentials[((_a = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _a === void 0 ? void 0 : _a.accessToken) || 'access_token']; credentials.expireIn = Number(newCredentials[((_b = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _b === void 0 ? void 0 : _b.expiresIn) || 'expires_in']) + Date.now() / 1000; if (newCredentials[((_c = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _c === void 0 ? void 0 : _c.refreshToken) || 'refresh_token']) { credentials.refreshToken = newCredentials[((_d = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _d === void 0 ? void 0 : _d.refreshToken) || 'refresh_token']; } (0, logger_1.log)('Saving updated integration credentials in the database'); yield (0, storage_1.getStorage)().updateIntegrationCredentials(integrationCredentials.id, (0, _1.encryptData)(config, JSON.stringify(credentials))); return credentials.accessToken; }), refreshToken: () => { return credentials.tokenProvider.getToken(true); }, }; (0, logger_1.log)('Checking if integration credentials need to be refreshed'); yield credentials.tokenProvider.getToken(); } return credentials; }); } function refreshCrowdinCreds(_a) { return __awaiter(this, arguments, void 0, function* ({ config, credentials }) { (0, logger_1.log)('Crowdin credentials have expired. Requesting a new credentials'); const newCredentials = yield refreshToken(config, credentials); (0, logger_1.log)('Saving updated crowdin credentials in the database'); const newCrowdinCredentials = { id: credentials.id, appSecret: credentials.appSecret, domain: credentials.domain, userId: credentials.userId, agentId: credentials.agentId, organizationId: credentials.organizationId, baseUrl: credentials.baseUrl, refreshToken: newCredentials.refreshToken, accessToken: newCredentials.accessToken, expire: (new Date().getTime() / 1000 + newCredentials.expiresIn).toString(), type: credentials.type, }; yield (0, storage_1.getStorage)().updateCrowdinCredentials(newCrowdinCredentials); const token = (0, _1.decryptData)(config, newCredentials.accessToken); return { credentials: newCrowdinCredentials, token, }; }); } function refreshToken(config, credentials) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; if (config.authenticationType === types_1.AuthenticationType.CODE) { const token = yield (0, token_1.refreshOAuthToken)({ clientId: config.clientId, clientSecret: config.clientSecret, refreshToken: (0, _1.decryptData)(config, credentials.refreshToken), url: (_a = config.crowdinUrls) === null || _a === void 0 ? void 0 : _a.accountUrl, }); return { accessToken: (0, _1.encryptData)(config, token.accessToken), refreshToken: (0, _1.encryptData)(config, token.refreshToken), expiresIn: token.expiresIn, }; } if (config.authenticationType === types_1.AuthenticationType.AGENT) { const token = yield (0, token_1.fetchAgentToken)({ appId: config.identifier, appSecret: credentials.appSecret, clientId: config.clientId, clientSecret: config.clientSecret, domain: credentials.domain || '', userId: credentials.userId, agentId: credentials.agentId, url: (_b = config.crowdinUrls) === null || _b === void 0 ? void 0 : _b.accountUrl, }); return { accessToken: (0, _1.encryptData)(config, token.accessToken), expiresIn: token.expiresIn, refreshToken: '', }; } const token = yield (0, token_1.fetchAppToken)({ appId: config.identifier, appSecret: credentials.appSecret, clientId: config.clientId, clientSecret: config.clientSecret, domain: credentials.domain || '', userId: credentials.userId, url: (_c = config.crowdinUrls) === null || _c === void 0 ? void 0 : _c.accountUrl, }); return { accessToken: (0, _1.encryptData)(config, token.accessToken), expiresIn: token.expiresIn, refreshToken: '', }; }); }