UNPKG

@syngrisi/syngrisi

Version:
251 lines (215 loc) 8.68 kB
/** * Syngrisi Plugin System * * Main entry point for the plugin system. */ import { Request, Response, NextFunction } from 'express'; import { config } from '@config'; import { env } from '@/server/envConfig'; import log from '@logger'; import { loadPlugins, pluginManager, hookRegistry } from './core'; import { PluginSettings } from '@models'; import { buildPluginContext } from './sdk/context'; import { PluginContext, AuthResult, CheckCompareContext, CheckOverrideResult, } from './sdk/types'; import { CompareResult } from '@services/comparison.service'; const logOpts = { scope: 'PluginSystem', msgType: 'PLUGIN', }; // Re-export SDK types for external use export * from './sdk'; export * from './core'; /** * Initialize the plugin system */ export async function initPlugins(): Promise<void> { log.info('Initializing plugin system...', logOpts); // Parse enabled plugins from environment (prefer live process.env for test overrides) const enabledPluginsRaw = process.env.SYNGRISI_PLUGINS_ENABLED ?? env.SYNGRISI_PLUGINS_ENABLED ?? ''; const enabledPlugins = enabledPluginsRaw ? enabledPluginsRaw.split(',').map(s => s.trim()).filter(Boolean) : []; // Build plugin configurations from environment const jwtPluginConfig: Record<string, unknown> = { jwksUrl: process.env.SYNGRISI_PLUGIN_JWT_AUTH_JWKS_URL, issuer: process.env.SYNGRISI_PLUGIN_JWT_AUTH_ISSUER, serviceUserRole: process.env.SYNGRISI_PLUGIN_JWT_AUTH_SERVICE_USER_ROLE || 'user', headerName: process.env.SYNGRISI_PLUGIN_JWT_AUTH_HEADER_NAME || 'Authorization', }; const headerPrefix = process.env.SYNGRISI_PLUGIN_JWT_AUTH_HEADER_PREFIX; if (headerPrefix !== undefined) { jwtPluginConfig.headerPrefix = headerPrefix; } const audience = process.env.SYNGRISI_PLUGIN_JWT_AUTH_AUDIENCE; if (audience !== undefined) { jwtPluginConfig.audience = audience; } const requiredScopes = process.env.SYNGRISI_PLUGIN_JWT_AUTH_REQUIRED_SCOPES; if (requiredScopes !== undefined) { jwtPluginConfig.requiredScopes = requiredScopes; } const issuerMatch = process.env.SYNGRISI_PLUGIN_JWT_AUTH_ISSUER_MATCH; if (issuerMatch !== undefined) { jwtPluginConfig.issuerMatch = issuerMatch; } const autoProvisionRaw = process.env.SYNGRISI_PLUGIN_JWT_AUTH_AUTO_PROVISION; if (autoProvisionRaw !== undefined) { jwtPluginConfig.autoProvisionUsers = autoProvisionRaw.toLowerCase() === 'true'; } const jwksCacheTtlRaw = process.env.SYNGRISI_PLUGIN_JWT_AUTH_JWKS_CACHE_TTL; if (jwksCacheTtlRaw !== undefined) { const parsedTtl = parseInt(jwksCacheTtlRaw, 10); if (!Number.isNaN(parsedTtl)) { jwtPluginConfig.jwksCacheTtl = parsedTtl; } } const pluginConfigs: Record<string, Record<string, unknown>> = { 'jwt-auth': jwtPluginConfig, 'custom-check-validator': { mismatchThreshold: parseFloat(env.CHECK_MISMATCH_THRESHOLD || '0'), scriptPath: env.CHECK_VALIDATOR_SCRIPT, }, }; let jwtDbSettings: Record<string, unknown> | null = null; let jwtDbEnabled: boolean | undefined; try { const dbSettings = await PluginSettings.findOne({ pluginName: 'jwt-auth' }).lean(); jwtDbSettings = dbSettings?.settings as Record<string, unknown> | undefined || null; jwtDbEnabled = dbSettings?.enabled; } catch (error) { log.debug(`Failed to read DB config for jwt-auth during startup validation: ${error}`, logOpts); } // If DB explicitly enables jwt-auth and it has required settings, ensure it's in the enabled list if (jwtDbEnabled === true && !enabledPlugins.includes('jwt-auth')) { const dbJwksUrl = (jwtDbSettings as Record<string, unknown> | null)?.jwksUrl as string | undefined; const dbIssuer = (jwtDbSettings as Record<string, unknown> | null)?.issuer as string | undefined; if (dbJwksUrl && dbIssuer) { enabledPlugins.push('jwt-auth'); } else { log.warn('jwt-auth enabled in DB without required settings; skipping auto-enable', logOpts); } } // Validate configuration for enabled plugins if (enabledPlugins.includes('jwt-auth')) { if (jwtDbEnabled === false) { log.info('jwt-auth is disabled in DB; skipping startup validation', logOpts); } else { let jwksUrl = pluginConfigs['jwt-auth']?.jwksUrl as string | undefined; let issuer = pluginConfigs['jwt-auth']?.issuer as string | undefined; if (!jwksUrl || !issuer) { const dbConfig = jwtDbSettings || {}; const hasEnvJwks = Object.prototype.hasOwnProperty.call(process.env, 'SYNGRISI_PLUGIN_JWT_AUTH_JWKS_URL'); const hasEnvIssuer = Object.prototype.hasOwnProperty.call(process.env, 'SYNGRISI_PLUGIN_JWT_AUTH_ISSUER'); if (!jwksUrl && !hasEnvJwks) { jwksUrl = (dbConfig as Record<string, unknown>).jwksUrl as string | undefined; } if (!issuer && !hasEnvIssuer) { issuer = (dbConfig as Record<string, unknown>).issuer as string | undefined; } } if (!jwksUrl || !issuer) { const errMsg = 'Missing required configuration for "jwt-auth" plugin. ' + 'Please set SYNGRISI_PLUGIN_JWT_AUTH_JWKS_URL and SYNGRISI_PLUGIN_JWT_AUTH_ISSUER environment variables.'; console.error(errMsg); throw new Error(errMsg); } // Validate JWKS URL format try { new URL(jwksUrl); } catch (error) { const errMsg = `Invalid JWKS URL for "jwt-auth" plugin: "${jwksUrl}". ` + 'SYNGRISI_PLUGIN_JWT_AUTH_JWKS_URL must be a valid URL.'; console.error(errMsg); throw new Error(errMsg); } } } await loadPlugins({ pluginsDir: process.env.SYNGRISI_PLUGINS_DIR || env.SYNGRISI_PLUGINS_DIR, enabledPlugins, pluginConfigs, }); log.info(`Plugin system initialized. Enabled plugins: [${enabledPlugins.join(', ') || 'none'}]`, logOpts); } /** * Get Express middleware for request:before hooks */ export function getPluginMiddleware() { return async (req: Request, res: Response, next: NextFunction): Promise<void> => { const hooks = hookRegistry.getHooks('request:before', req.path); if (hooks.length === 0) { return next(); } const context = buildPluginContext(); for (const hook of hooks) { try { await hook.handler!(req, res, next, context); // Check if response was sent if (res.headersSent) { return; } } catch (error) { log.error(`Error in request:before hook from '${hook.pluginName}': ${error}`, logOpts); } } next(); }; } /** * Execute auth:validate hook * Returns AuthResult if a plugin handled auth, null otherwise */ export async function executeAuthHook(req: Request, res: Response): Promise<AuthResult | null> { if (!hookRegistry.hasHooks('auth:validate')) { return null; } const context = buildPluginContext(); return hookRegistry.executeAuthValidate(req, res, context); } /** * Execute check:beforeCompare hook */ export async function executeBeforeCompareHook( checkContext: CheckCompareContext ): Promise<CheckCompareContext | { skip: true; result: CheckOverrideResult }> { if (!hookRegistry.hasHooks('check:beforeCompare')) { return checkContext; } const pluginContext = buildPluginContext(); return hookRegistry.executeCheckBeforeCompare(checkContext, pluginContext); } /** * Execute check:afterCompare hook */ export async function executeAfterCompareHook( checkContext: CheckCompareContext, compareResult: CompareResult ): Promise<CompareResult> { if (!hookRegistry.hasHooks('check:afterCompare')) { return compareResult; } const pluginContext = buildPluginContext(); return hookRegistry.executeCheckAfterCompare(checkContext, compareResult, pluginContext); } /** * Check if any plugins are loaded */ export function hasPlugins(): boolean { return pluginManager.getPluginCount() > 0; } /** * Get loaded plugin names */ export function getLoadedPluginNames(): string[] { return pluginManager.getPluginNames(); }