Skip to main content
Glama
utils.ts6.27 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { concatUrls } from '@medplum/core'; import { generateKeyPairSync, randomUUID } from 'node:crypto'; import { getLogger } from '../logger'; import type { MedplumServerConfig } from './types'; const DEFAULT_AWS_REGION = 'us-east-1'; export type ServerConfig = MedplumServerConfig & Required<Pick<MedplumServerConfig, DefaultConfigKeys>>; /** * Adds default values to the config. * @param config - The input config as loaded from the config file. * @returns The config with default values added. */ export function addDefaults(config: MedplumServerConfig): ServerConfig { config.port ||= 8103; config.issuer ||= config.baseUrl; config.jwksUrl ||= concatUrls(config.baseUrl, '/.well-known/jwks.json'); config.authorizeUrl ||= concatUrls(config.baseUrl, '/oauth2/authorize'); config.tokenUrl ||= concatUrls(config.baseUrl, '/oauth2/token'); config.userInfoUrl ||= concatUrls(config.baseUrl, '/oauth2/userinfo'); config.introspectUrl ||= concatUrls(config.baseUrl, '/oauth2/introspect'); config.registerUrl ||= concatUrls(config.baseUrl, '/oauth2/register'); config.storageBaseUrl ||= concatUrls(config.baseUrl, '/storage'); config.maxJsonSize ||= '1mb'; config.maxBatchSize ||= '50mb'; config.awsRegion ||= DEFAULT_AWS_REGION; config.botLambdaLayerName ||= 'medplum-bot-layer'; config.bcryptHashSalt ||= 10; config.bullmq = { concurrency: 20, removeOnComplete: { count: 1 }, removeOnFail: { count: 1 }, ...config.bullmq }; config.shutdownTimeoutMilliseconds ??= 30_000; config.accurateCountThreshold ??= 1_000_000; config.maxSearchOffset ??= 10_000; config.defaultBotRuntimeVersion ??= 'awslambda'; config.defaultProjectFeatures ??= []; config.defaultProjectSystemSetting ??= []; config.emailProvider ||= config.smtp ? 'smtp' : 'awsses'; config.autoDownloadEnabled ??= true; // History: // Before, the default "auth rate limit" was 600 per 15 minutes, but used "MemoryStore" rather than "RedisStore" // That meant that the rate limit was per server instance, rather than per server cluster // The value was primarily tuned for one particular cluster with 6 server instances // Therefore, to maintain parity, the new default "auth rate limit" is 1200 per 15 minutes config.defaultRateLimit ??= 60_000; config.defaultAuthRateLimit ??= 160; config.defaultFhirQuota ??= 50_000; // Automatically generate a signing key if using built-in storage and no signing key is provided if (config.storageBaseUrl.startsWith(config.baseUrl) && !config.signingKey) { getLogger().warn( 'Generating temporary signing key. Storage URLs will not work in cluster environments, and will be invalid after server restart.' ); const passphrase = randomUUID(); const signingKey = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem', }, privateKeyEncoding: { type: 'pkcs1', format: 'pem', cipher: 'aes-256-cbc', passphrase, }, }); config.signingKeyId = 'medplum-generated-key'; config.signingKey = signingKey.privateKey; config.signingKeyPassphrase = passphrase; } return config as ServerConfig; } type DefaultConfigKeys = | 'port' | 'issuer' | 'jwksUrl' | 'authorizeUrl' | 'tokenUrl' | 'userInfoUrl' | 'introspectUrl' | 'storageBaseUrl' | 'maxJsonSize' | 'maxBatchSize' | 'awsRegion' | 'botLambdaLayerName' | 'bcryptHashSalt' | 'bullmq' | 'shutdownTimeoutMilliseconds' | 'accurateCountThreshold' | 'maxSearchOffset' | 'defaultBotRuntimeVersion' | 'defaultProjectFeatures' | 'defaultProjectSystemSetting' | 'emailProvider' | 'defaultRateLimit' | 'defaultAuthRateLimit' | 'defaultFhirQuota'; const integerKeys = new Set([ 'accurateCountThreshold', 'bcryptHashSalt', 'defaultAuthRateLimit', 'defaultFhirQuota', 'defaultRateLimit', 'heartbeatMilliseconds', 'keepAliveTimeout', 'maxBotLogLengthForLogs', 'maxBotLogLengthForResource', 'maxSearchOffset', 'mfaAuthenticatorWindow', 'port', 'shutdownTimeoutMilliseconds', 'transactionAttempts', 'transactionExpBackoffBaseDelayMs', 'fhirSearchMinLimit', 'database.maxConnections', 'database.port', 'database.queryTimeout', 'readonlyDatabase.maxConnections', 'readonlyDatabase.port', 'readonlyDatabase.queryTimeout', 'redis.db', 'redis.port', 'smtp.port', 'bullmq.concurrency', 'fission.routerPort', ]); export function isIntegerConfig(key: string): boolean { return integerKeys.has(key); } export function isFloatConfig(_key: string): boolean { return false; } const booleanKeys = new Set([ 'botCustomFunctionsEnabled', 'database.ssl.rejectUnauthorized', 'database.ssl.require', 'database.disableConnectionConfiguration', 'database.disableRunPostDeployMigrations', 'database.runMigrations', 'readonlyDatabase.ssl.rejectUnauthorized', 'readonlyDatabase.ssl.require', 'readonlyDatabase.disableConnectionConfiguration', 'logRequests', 'logAuditEvents', 'mcpEnabled', 'registerEnabled', 'require', 'rejectUnauthorized', 'fhirSearchDiscourageSeqScan', 'redactAuditEvents', ]); export function isBooleanConfig(key: string): boolean { return booleanKeys.has(key); } const objectKeys = new Set([ 'tls', 'ssl', 'defaultProjectSystemSetting', 'defaultOAuthClients', 'smtp', 'arrayColumnPadding', ]); export function isObjectConfig(key: string): boolean { return objectKeys.has(key); } export function setValue(config: Record<string, unknown>, key: string, value: string): void { const keySegments = key.split('.'); let obj = config; while (keySegments.length > 1) { const segment = keySegments.shift() as string; if (!obj[segment]) { obj[segment] = {}; } obj = obj[segment] as Record<string, unknown>; } let parsedValue: any = value; if (isIntegerConfig(key)) { parsedValue = Number.parseInt(value, 10); } else if (isBooleanConfig(key)) { parsedValue = value === 'true'; } else if (isObjectConfig(key)) { parsedValue = JSON.parse(value); } obj[keySegments[0]] = parsedValue; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server