@crowdin/app-project-module
Version:
Module that generates for you all common endpoints for serving standalone Crowdin App
378 lines (377 loc) • 19.7 kB
JavaScript
;
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
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.metadataStore = exports.express = exports.maskKey = exports.postRequestCredentialsMasker = exports.getRequestCredentialsMasker = void 0;
exports.createApp = createApp;
exports.addCrowdinEndpoints = addCrowdinEndpoints;
const logsFormatter = __importStar(require("@crowdin/logs-formatter"));
const express_rate_limit_1 = require("express-rate-limit");
const path_1 = require("path");
const crowdin_client_1 = __importStar(require("./middlewares/crowdin-client"));
const json_response_1 = __importDefault(require("./middlewares/json-response"));
const form_data_display_1 = __importDefault(require("./modules/form-data-display"));
const form_data_save_1 = __importDefault(require("./modules/form-data-save"));
const install_1 = __importDefault(require("./modules/install"));
const manifest_1 = __importDefault(require("./modules/manifest"));
const about_1 = __importDefault(require("./modules/about"));
const subscription_paid_1 = __importDefault(require("./modules/subscription-paid"));
const uninstall_1 = __importDefault(require("./modules/uninstall"));
const status_1 = __importDefault(require("./modules/status"));
const storage = __importStar(require("./storage"));
const d1_1 = require("./storage/d1");
const cron = __importStar(require("./util/cron"));
const types_1 = require("./types");
const util_1 = require("./util");
const form_schema_1 = require("./util/form-schema");
const connection_1 = require("./util/connection");
const logger = __importStar(require("./util/logger"));
const logger_1 = require("./util/logger");
const terminus_express_1 = __importDefault(require("./util/terminus-express"));
exports.express = terminus_express_1.default;
const credentials_masker_1 = require("./util/credentials-masker");
Object.defineProperty(exports, "getRequestCredentialsMasker", { enumerable: true, get: function () { return credentials_masker_1.getRequestCredentialsMasker; } });
Object.defineProperty(exports, "postRequestCredentialsMasker", { enumerable: true, get: function () { return credentials_masker_1.postRequestCredentialsMasker; } });
Object.defineProperty(exports, "maskKey", { enumerable: true, get: function () { return credentials_masker_1.maskKey; } });
const auto_credentials_masker_1 = require("./middlewares/auto-credentials-masker");
const static_files_1 = require("./util/static-files");
const util_2 = require("./util");
//apps
const apiApp = __importStar(require("./modules/api"));
const contextMenuApp = __importStar(require("./modules/context-menu"));
const customMTApp = __importStar(require("./modules/custom-mt"));
const spellCheckApp = __importStar(require("./modules/custom-spell-check"));
const aiProvider = __importStar(require("./modules/ai-provider"));
const aiPromptProvider = __importStar(require("./modules/ai-prompt-provider"));
const aiTools = __importStar(require("./modules/ai-tools"));
const editorRightPanelApp = __importStar(require("./modules/editor-right-panel"));
const externalQaCheck = __importStar(require("./modules/external-qa-check"));
const fileProcessingApps = __importStar(require("./modules/file-processing"));
const integrationApp = __importStar(require("./modules/integration"));
const chatApp = __importStar(require("./modules/chat"));
const modalApp = __importStar(require("./modules/modal"));
const organizationMenuApp = __importStar(require("./modules/organization-menu"));
const organizationSettingsMenuApp = __importStar(require("./modules/organization-settings-menu"));
const profileResourcesMenuApp = __importStar(require("./modules/profile-resources-menu"));
const profileSettingsMenuApp = __importStar(require("./modules/profile-settings-menu"));
const projectMenuApp = __importStar(require("./modules/project-menu"));
const projectMenuCrowdsourceApp = __importStar(require("./modules/project-menu-crowdsource"));
const projectReportsApp = __importStar(require("./modules/project-reports"));
const projectToolsApp = __importStar(require("./modules/project-tools"));
const webhooks = __importStar(require("./modules/webhooks"));
const workflowStepType = __importStar(require("./modules/workflow-step-type"));
const aiRequestProcessors = __importStar(require("./modules/ai-request-processors"));
const automationAction = __importStar(require("./modules/automation-action"));
const authGuard = __importStar(require("./modules/auth-guard"));
const subscription_1 = require("./util/subscription");
__exportStar(require("./types"), exports);
exports.metadataStore = {
getMetadata: (id) => {
return storage.getStorage().getMetadata(id);
},
saveMetadata: (_a) => __awaiter(void 0, [_a], void 0, function* ({ id, metadata, crowdinId }) {
if (!crowdinId) {
throw new Error('The crowdinId parameter is required.');
}
const crowdinCredentials = yield storage.getStorage().getCrowdinCredentials(crowdinId);
if (!crowdinCredentials) {
throw new Error('Invalid crowdinId parameter: You can get your crowdinId from the JWT payload.');
}
const existing = yield storage.getStorage().getMetadata(id);
if (existing) {
yield storage.getStorage().updateMetadata(id, metadata, crowdinId);
}
else {
yield storage.getStorage().saveMetadata(id, metadata, crowdinId);
}
}),
deleteMetadata: (id) => {
return storage.getStorage().deleteMetadata(id);
},
getUserSettings: (clientId) => __awaiter(void 0, void 0, void 0, function* () {
const integrationConfig = yield storage.getStorage().getIntegrationConfig(clientId);
if (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) {
return JSON.parse(integrationConfig.config);
}
}),
};
function createApp(clientConfig) {
const app = (0, terminus_express_1.default)();
const config = convertClientConfig(clientConfig);
addCrowdinEndpoints(app, config);
/* eslint no-console: "off" */
app.listen(config, () => console.log(`App started on port ${config.port}`));
}
function addCrowdinEndpoints(app, clientConfig) {
var _a, _b, _c;
const config = convertClientConfig(clientConfig);
if (!config.disableGlobalErrorHandling) {
handleUncaughtErrors();
}
logger.initialize(config);
storage.initialize(config);
cron.initialize(config);
// Middleware to ensure D1 migration before handling requests
app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
try {
const storageInstance = storage.getStorage();
if (storageInstance instanceof d1_1.D1Storage) {
yield storageInstance.ensureMigrated();
}
next();
}
catch (error) {
next(error);
}
}));
// Middleware to detect and cache baseUrl from request if not provided
app.use((req, res, next) => {
if (!config.baseUrl) {
config.baseUrl = (0, util_1.extractBaseUrlFromRequest)(req);
}
next();
});
app.use((req, res, next) => {
if (config.webhooks && req.path === '/webhooks') {
return terminus_express_1.default.raw({ type: '*/*', limit: '50mb' })(req, res, next);
}
else {
return terminus_express_1.default.json({ limit: '50mb' })(req, res, next);
}
});
if (!config.disableLogsFormatter) {
logsFormatter.setup();
app.use(logsFormatter.contextResolverMiddleware());
app.use(logsFormatter.expressMiddleware());
}
app.use('/assets', (0, static_files_1.serveStatic)(config, config.assetsPath || 'static'));
app.use('/', (0, util_2.serveLogo)(config));
app.get('/manifest.json', json_response_1.default, (0, manifest_1.default)(config));
app.get('/', (0, about_1.default)(config));
if (((_a = config === null || config === void 0 ? void 0 : config.enableStatusPage) === null || _a === void 0 ? void 0 : _a.database) || ((_b = config === null || config === void 0 ? void 0 : config.enableStatusPage) === null || _b === void 0 ? void 0 : _b.filesystem)) {
app.set('trust proxy', 1);
const limiter = (0, express_rate_limit_1.rateLimit)({
windowMs: 60 * 1000, // 1 minute
limit: ((_c = config.enableStatusPage) === null || _c === void 0 ? void 0 : _c.rateLimit) || 10,
standardHeaders: true,
legacyHeaders: false,
message: {
status: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
},
});
app.get('/status', limiter, (0, status_1.default)(config));
}
if (!(0, subscription_1.isAppFree)(config)) {
app.post('/subscription-paid', (0, subscription_paid_1.default)());
}
spellCheckApp.register({ config, app });
editorRightPanelApp.register({ config, app });
projectMenuApp.register({ config, app });
profileResourcesMenuApp.register({ config, app });
profileSettingsMenuApp.register({ config, app });
organizationMenuApp.register({ config, app });
organizationSettingsMenuApp.register({ config, app });
projectMenuCrowdsourceApp.register({ config, app });
projectToolsApp.register({ config, app });
projectReportsApp.register({ config, app });
modalApp.register({ config, app });
chatApp.register({ config, app });
contextMenuApp.register({ config, app });
//other apps only work in authorized context
if (!(0, util_1.isAuthorizedConfig)(config)) {
return Object.assign(Object.assign({}, exports.metadataStore), { cron: cron.getCron(), storage: storage.getStorage(), establishCrowdinConnection: () => {
throw new Error('Establishing Crowdin connection is not available in unauthorized config');
}, encryptCrowdinConnection: () => {
throw new Error('Encrypting Crowdin connection is not available in unauthorized config');
}, decryptCrowdinConnection: () => {
throw new Error('Decrypting Crowdin connection is not available in unauthorized config');
}, jwtMiddleware: () => {
throw new Error('JWT Middleware is not available in unauthorized config');
} });
}
app.post('/installed', (0, install_1.default)(config));
app.post('/uninstall', (0, uninstall_1.default)(config));
/**
* Module structure
* /modules
* /<module-name>
* index.ts -> registration
* types.ts -> types, interfaces, etc
* /util -> folder for utilities
* /handlers -> for http handlers
*/
integrationApp.register({ config, app });
customMTApp.register({ config, app });
fileProcessingApps.registerCustomFileFormat({ config, app });
fileProcessingApps.registerFilePreImport({ config, app });
fileProcessingApps.registerFilePostImport({ config, app });
fileProcessingApps.registerFilePreExport({ config, app });
fileProcessingApps.registerFilePostExport({ config, app });
fileProcessingApps.registerFileTranslationsAlignmentExport({ config, app });
apiApp.register({ config, app });
aiProvider.register({ config, app });
aiPromptProvider.register({ config, app });
aiTools.registerAiTools({ config, app });
aiTools.registerAiToolWidgets({ config, app });
externalQaCheck.register({ config, app });
webhooks.register({ config, app });
workflowStepType.register({ config, app });
aiRequestProcessors.register({ config, app });
automationAction.register({ config, app });
authGuard.register({ config, app });
(0, auto_credentials_masker_1.registerAutoCredentialsMasker)(app, config);
addFormSchema({ config, app });
return Object.assign(Object.assign({}, exports.metadataStore), { storage: storage.getStorage(), cron: cron.getCron(), establishCrowdinConnection: ({ authRequest, jwtToken: token, moduleKey }) => {
let jwtToken = '';
if (token) {
jwtToken = token;
}
else if (authRequest) {
jwtToken = (0, crowdin_client_1.getToken)(authRequest);
}
else {
throw new Error('Either authRequest or jwtToken must be provided');
}
return (0, crowdin_client_1.prepareCrowdinRequest)({
jwtToken,
config,
optional: false,
checkSubscriptionExpiration: false,
moduleKey,
});
}, encryptCrowdinConnection: (data) => (0, util_1.encryptData)(config, JSON.stringify(data)), decryptCrowdinConnection: (_a) => __awaiter(this, [_a], void 0, function* ({ hash, autoRenew }) {
const { crowdinId, extra } = JSON.parse((0, util_1.decryptData)(config, hash));
const credentials = yield storage.getStorage().getCrowdinCredentials(crowdinId);
if (!credentials) {
throw new Error('Failed to find Crowdin credentials');
}
const { client } = yield (0, connection_1.prepareCrowdinClient)({ config, credentials, autoRenew });
return { client, extra };
}), jwtMiddleware: (options) => {
var _a, _b;
return (0, crowdin_client_1.default)({
config,
optional: (_a = options.optional) !== null && _a !== void 0 ? _a : false,
checkSubscriptionExpiration: (_b = options.checkSubscriptionExpiration) !== null && _b !== void 0 ? _b : false,
moduleKey: options.moduleKey,
});
} });
}
function addFormSchema({ app, config }) {
const allModuleConfigs = [];
for (const moduleKey of Object.keys(config)) {
const value = config[moduleKey];
if (Array.isArray(value)) {
allModuleConfigs.push(...value);
}
else {
allModuleConfigs.push(value);
}
}
for (const moduleConfig of allModuleConfigs) {
if ((0, form_schema_1.hasFormSchema)(moduleConfig)) {
const moduleConfigWithForm = (0, form_schema_1.getLowCodeUiConfigFromModuleConfig)(moduleConfig);
if (moduleConfigWithForm) {
if (!moduleConfigWithForm.formGetDataUrl) {
app.get(`/api/${moduleConfigWithForm.key}/form-data`, json_response_1.default, (0, crowdin_client_1.default)({
config,
optional: false,
checkSubscriptionExpiration: true,
moduleKey: moduleConfigWithForm.key,
}), (0, credentials_masker_1.getRequestCredentialsMasker)({ moduleConfig: moduleConfigWithForm }), (0, form_data_display_1.default)(config));
}
if (!moduleConfigWithForm.formPostDataUrl) {
app.post(`/api/${moduleConfigWithForm.key}/form-data`, (0, crowdin_client_1.default)({
config,
optional: false,
checkSubscriptionExpiration: true,
moduleKey: moduleConfigWithForm.key,
}), (0, credentials_masker_1.postRequestCredentialsMasker)({
secret: config.clientSecret,
moduleConfig: moduleConfigWithForm,
}), (0, form_data_save_1.default)(config));
}
}
}
}
}
function convertClientConfig(clientConfig) {
const baseUrl = clientConfig.baseUrl || process.env.URL;
const clientId = clientConfig.clientId || process.env.CROWDIN_CLIENT_ID;
const clientSecret = clientConfig.clientSecret || process.env.CROWDIN_CLIENT_SECRET;
const port = clientConfig.port || process.env.PORT || 3000;
const { region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION, tmpBucketName = process.env.AWS_TMP_BUCKET_NAME, } = clientConfig.awsConfig || {};
if (clientConfig.authenticationType !== types_1.AuthenticationType.NONE) {
if (!clientId && !clientSecret) {
throw new Error('Missing [clientId, clientSecret] parameters');
}
}
if (clientConfig.projectIntegration) {
clientConfig.api = Object.assign({ default: true }, clientConfig.api);
}
clientConfig.enableStatusPage = Object.assign({ database: true, filesystem: true }, (clientConfig.enableStatusPage || {}));
return Object.assign(Object.assign({}, clientConfig), { baseUrl: baseUrl ? (baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl) : undefined, clientId,
clientSecret, awsConfig: {
tmpBucketName,
region,
}, port: Number(port), dbFolder: clientConfig.dbFolder || (0, path_1.join)(process.cwd(), 'db'), imagePath: clientConfig.imagePath || (0, path_1.join)(process.cwd(), 'logo.png') });
}
function handleUncaughtErrors() {
process
.on('unhandledRejection', (reason) => {
const error = `Unhandled Rejection. Reason: ${reason.stack || reason}`;
(0, logger_1.logError)(error);
})
.on('uncaughtException', (reason) => {
const error = `Uncaught Exception. Reason: ${reason.stack || reason}`;
(0, logger_1.logError)(error);
});
}