/** * Copyright (c) 2023 MERCENARIES.AI PTE. LTD. * All rights reserved. */ /** * * This file composes the runnable server from services and integrations * **/ import { OmniLogLevels, registerOmnilogGlobal, type IServiceConfig } from 'omni-shared'; import Server from './core/Server.js'; import { loadServerConfig, type IServerConfig } from './loadConfig.js'; import { exec } from 'child_process'; import os from 'os'; import fs from 'node:fs'; // Services import { APIServerService, type IAPIServerServiceConfig } from './services/APIService.js'; import { AmqpService } from './services/AmqpService.js'; import { ChatService } from './services/ChatService.js'; import { CredentialService } from './services/CredentialsService/CredentialService.js'; import { LocalFileCredentialStore } from './services/CredentialsService/Store/BaseCredentialStore.js'; import { VaultWardenCredentialStore } from './services/CredentialsService/Store/VaultWardenCredentialStore.js'; import { DBService } from './services/DBService.js'; import { FastifyServerService, type FastifyServerServiceConfig } from './services/FastifyServerService.js'; import { HttpClientService } from './services/HttpClientService.js'; import { JobControllerService, type IJobControllerServiceConfig } from './services/JobController/JobControllerService.js'; import { MessagingServerService, type IMessagingServerServiceConfig } from './services/MessagingService.js'; import { RESTConsumerService, type RESTConsumerServiceConfig } from './services/RestConsumerService/RESTConsumerService.js'; // Integrations import { AuthIntegration, type IAuthIntegrationConfig } from './integrations/Authentication/AuthIntegration.js'; import { LocalCdnIntegration, type ILocalCdnIntegrationConfig } from './integrations/CdnIntegrations/LocalCdnIntegration.js'; import { ChatIntegration, type IChatIntegrationConfig } from './integrations/Chat/ChatIntegration.js'; import { MercsDefaultIntegration, type MercsDefaultIntegrationConfig } from './integrations/Mercenaries/MercsDefaultIntegration.js'; import { WorkflowIntegration, type IWorkflowIntegrationConfig } from './integrations/WorkflowIntegration/WorkflowIntegration.js'; import { Command, type OptionValues } from 'commander'; import path from 'path'; import { ServerExtensionManager } from './core/ServerExtensionsManager.js'; // ----------------------------------------- Globals ---------------------------------------- registerOmnilogGlobal(); omnilog.wrapConsoleLogger(); // ----------------------------------------- Server ----------------------------------------- const config: IServerConfig = loadServerConfig('../../.mercs.yaml') as IServerConfig; const packagejson = JSON.parse( fs.readFileSync('package.json', { encoding: 'utf-8' })); const serverConfig = config.server; serverConfig.version = packagejson.version; const server_config = serverConfig; process.on('unhandledRejection', (reason, promise) => { omnilog.trace(); omnilog.error('Uncaught error in', promise, reason); process.exit(1); }); // ----------------------------------------- CLI ----------------------------------------- const bootstrap = async (): Promise => { const program: Command = new Command(); // common options program .option('-u, --updateExtensions', 'Update all extensions') .option('-rb, --refreshBlocks', 'Refresh block definitions') .option('-px, --pruneExtensions', 'Prune deprecated extensions') .option('-R, --resetDB ', 'Reset the database on startup. Valid scopes: blocks,settings') .option('--chown ', 'Reparent all unowned files in CDN storage to this user') .option('-ll, --loglevel ', 'Set logging level', serverConfig.logger.level.toString()) .option('--emittery', 'Enable emittery debug logs. Always disabled on log level silent(0).') .option('--verbose', 'Max logging level') .option( '-purl, --publicUrl ', 'Set the external address for services that requires it', server_config.network.public_url ) .option( '--fastifyopt ', 'Advanced Fastify options - JSON Object', JSON.stringify({ bodyLimit: 32 * 1024 * 1024 }) ) .option('-p, --port ', 'Overwrite the listening port', '1688') .option('--openBrowser') .option('-nx, --noExtensions', 'Disable all (non core) extensions') .option('-s, --secure ', 'Enforce secure connection', false) .option('--dburl ', 'Connection URL to the DB') .option('--dbuser ', 'DB admin user', 'admin@local.host') .option('--viteProxy ', 'Specify vite debugger URL') .option('--autologin', 'Autologin user') .option('--uncensored', 'Disable NSFW protections') .option('--flushLogs', 'Flush logs to DB') .requiredOption('-l, --listen ', 'Sets the interface the host listens on'); program.action((options) => { // apply option overwrites omnilog.setCustomLevel('emittery', options.emittery ? OmniLogLevels.verbose : OmniLogLevels.silent); omnilog.level = options.verbose ? OmniLogLevels.verbose : Number.parseInt(options.loglevel); const isLocalStack = options.listen === '127.0.0.1'; // set defaults for Autologin if not present if (options.autologin === undefined) { options.autologin = isLocalStack; } // Default to true: set --flushLogs false to disable if (options.flushLogs === undefined) { options.flushLogs = true; } // set defaults for DB if not present, rewrites if (!options.dburl) { server_config.services.db.pocketbaseDbUrl = isLocalStack ? serverConfig.services.db.pocketbase.local.dbUrl : serverConfig.services.db.pocketbase.development.dbUrl; } else { server_config.services.db.pocketbaseDbUrl = options.dburl; } server_config.services.db.pocketbaseDbAdmin = options.dbuser; server_config.services.db.flushLogs = options.flushLogs; // public URL const publicURL = new URL(options.publicUrl); server_config.network.public_url = options.publicUrl; // Cookie security server_config.session.cookie.secure = options.secure; // CDN overwrites const currentCDNLocalRoute = new URL(server_config.integrations.cdn.localRoute); server_config.integrations.cdn.local.url = publicURL.host; currentCDNLocalRoute.protocol = publicURL.protocol; currentCDNLocalRoute.hostname = publicURL.hostname; currentCDNLocalRoute.port = publicURL.port; server_config.integrations.cdn.localRoute = currentCDNLocalRoute.href; // finally boot void boot(options); }); program.parse(); }; // ----------------------------------------- Boot ----------------------------------------- const boot = async (options: OptionValues) => { const server = new Server('mercs', serverConfig, options); // Initialize global settings before everything starts await server.initGlobalSettings(); const extensionPath = path.join(process.cwd(), 'extensions'); omnilog.status_start('--- Ensuring core extensions -----'); await ServerExtensionManager.ensureCoreExtensions(extensionPath); omnilog.status_success('OK'); omnilog.status_start('--- Updating extensions -----'); await ServerExtensionManager.updateExtensions(extensionPath, options); omnilog.status_success('OK'); if (options.pruneExtensions) { omnilog.status_start('--- Pruning extensions -----'); await ServerExtensionManager.pruneExtensions(extensionPath); omnilog.status_success('OK'); } omnilog.status_start('Booting Server'); // ----------------------------------------- Services ----------------------------------------- const dbConfig = Object.assign({ id: 'db' }, server_config.services?.db); server.use(DBService, dbConfig, 'service'); const messagingConfig: IMessagingServerServiceConfig = Object.assign( { id: 'messaging' }, serverConfig.services?.messaging ); server.use(MessagingServerService, messagingConfig, 'service'); // Amqp Service const amqpConfig = Object.assign({ id: 'amqp' }, serverConfig.services?.amqp); server.use(AmqpService, amqpConfig); if (!serverConfig.services?.credentials?.disabled) { if (serverConfig.services?.credentials?.type === 'local') { const store = new LocalFileCredentialStore(serverConfig.services?.credentials?.storeConfig); server.use( CredentialService, Object.assign({ id: 'credentials' }, serverConfig.services?.credentials, { store }) ); } else if (serverConfig.services?.credentials?.type === 'vaultWarden') { const store = new VaultWardenCredentialStore(serverConfig.services?.credentials?.storeConfig); server.use( CredentialService, Object.assign({ id: 'credentials' }, serverConfig.services?.credentials, { store }) ); } else { server.debug('⚠️Default to KV storage'); server.use(CredentialService, Object.assign({ id: 'credentials' }, serverConfig.services?.credentials)); } } else { server.warn('⚠️CredentialService is disabled in config.'); } // RestConsumerService if (!serverConfig.services?.rest_consumer?.disabled) { const consumerConfig: RESTConsumerServiceConfig = Object.assign( { id: 'rest_consumer' }, serverConfig.services?.rest_consumer ); server.use(RESTConsumerService, consumerConfig); } else { server.warn('⚠️RestConsumerService is disabled in config.'); } // Axios wrapper server.use(HttpClientService, { id: 'http_client' }); // TODO: Currently this is using the client path rather than directly invoking the functions on the server. That all // needs to be reworked. But for now this allows us to run code on both client and server. const apiConfig: IAPIServerServiceConfig = { id: 'api', host: 'http://127.0.0.1:1688', // remote API is disabled? integrationsUrl: '/api/v1/mercenaries/integrations' }; server.use(APIServerService, apiConfig); // if (config.server.integrations.cdn.type == 'local') { const cdnConfig: ILocalCdnIntegrationConfig = Object.assign({ id: 'cdn' }, serverConfig.integrations?.cdn); server.use(LocalCdnIntegration, cdnConfig, 'integration'); // JobControllerService const jobControllerServiceConfig: IJobControllerServiceConfig = Object.assign( { id: 'jobs' }, serverConfig.services?.jobs ); server.use(JobControllerService, jobControllerServiceConfig); // Chat Service const chatServiceConfig: IServiceConfig = Object.assign({ id: 'chat' }); server.use(ChatService, chatServiceConfig); const listenOn = new URL('http://0.0.0.0:1688'); listenOn.hostname = options.listen; listenOn.protocol = options.secure ? 'https' : 'http'; listenOn.port = options.port; const fastifyOptions = JSON.parse(options.fastifyopt); const corsOrigin = [listenOn.origin]; if (options.viteProxy !== undefined) { corsOrigin.push(options.viteProxy); } const fastifyConfig: FastifyServerServiceConfig = { id: 'httpd', listen: { host: listenOn.hostname, port: Number.parseInt(listenOn.port) }, cors: { origin: corsOrigin, credentials: true }, autologin: options.autologin, proxy: { enabled: options.viteProxy !== undefined, viteDebugger: options.viteProxy }, plugins: {}, opts: fastifyOptions, session: { secret: serverConfig.session.secret, cookie: serverConfig.session.cookie, kvStorage: serverConfig.kvStorage }, rateLimit: { global: serverConfig.network.rateLimit.global, max: serverConfig.network.rateLimit.max, timeWindow: serverConfig.network.rateLimit.timeWindow } }; server.use(FastifyServerService, fastifyConfig); // ----------------------------------------- Integrations ----------------------------------------- const mercsIntegrationConfig: MercsDefaultIntegrationConfig = Object.assign( { id: 'mercenaries' }, serverConfig.integrations?.mercenaries ); server.use(MercsDefaultIntegration, mercsIntegrationConfig); const workflowConfig: IWorkflowIntegrationConfig = Object.assign( { id: 'workflow' }, serverConfig.integrations?.workflow ); server.use(WorkflowIntegration, workflowConfig); const authConfig: IAuthIntegrationConfig = Object.assign({ id: 'auth' }, serverConfig.integrations?.auth); server.use(AuthIntegration, authConfig); const chatConfig: IChatIntegrationConfig = Object.assign({ id: 'chat' }, serverConfig.integrations?.chat); server.use(ChatIntegration, chatConfig); // ----------------------------------------- Extensions ----------------------------------------- await server.init(); await server.load(); await server.start(); omnilog.status_success(`Server has started and is ready to accept connections on ${listenOn.origin}`); omnilog.status_success('Ctrl-C to quit.'); // open default browser if (options.openBrowser) { switch (os.platform()) { case 'win32': exec(`start ${options.publicUrl}`); break; case 'darwin': exec(`open ${options.publicUrl}`); break; } } }; bootstrap().catch((err) => { omnilog.trace(); omnilog.error('Caught unhandled exception during bootstrap: ', err); process.exit(1); });