@qwickapps/server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +321 -0
  3. package/dist/core/control-panel.d.ts +21 -0
  4. package/dist/core/control-panel.d.ts.map +1 -0
  5. package/dist/core/control-panel.js +416 -0
  6. package/dist/core/control-panel.js.map +1 -0
  7. package/dist/core/gateway.d.ts +133 -0
  8. package/dist/core/gateway.d.ts.map +1 -0
  9. package/dist/core/gateway.js +270 -0
  10. package/dist/core/gateway.js.map +1 -0
  11. package/dist/core/health-manager.d.ts +52 -0
  12. package/dist/core/health-manager.d.ts.map +1 -0
  13. package/dist/core/health-manager.js +192 -0
  14. package/dist/core/health-manager.js.map +1 -0
  15. package/dist/core/index.d.ts +10 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +8 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/core/logging.d.ts +83 -0
  20. package/dist/core/logging.d.ts.map +1 -0
  21. package/dist/core/logging.js +191 -0
  22. package/dist/core/logging.js.map +1 -0
  23. package/dist/core/types.d.ts +195 -0
  24. package/dist/core/types.d.ts.map +1 -0
  25. package/dist/core/types.js +7 -0
  26. package/dist/core/types.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +17 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/plugins/config-plugin.d.ts +15 -0
  32. package/dist/plugins/config-plugin.d.ts.map +1 -0
  33. package/dist/plugins/config-plugin.js +96 -0
  34. package/dist/plugins/config-plugin.js.map +1 -0
  35. package/dist/plugins/diagnostics-plugin.d.ts +29 -0
  36. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -0
  37. package/dist/plugins/diagnostics-plugin.js +142 -0
  38. package/dist/plugins/diagnostics-plugin.js.map +1 -0
  39. package/dist/plugins/health-plugin.d.ts +17 -0
  40. package/dist/plugins/health-plugin.d.ts.map +1 -0
  41. package/dist/plugins/health-plugin.js +25 -0
  42. package/dist/plugins/health-plugin.js.map +1 -0
  43. package/dist/plugins/index.d.ts +14 -0
  44. package/dist/plugins/index.d.ts.map +1 -0
  45. package/dist/plugins/index.js +10 -0
  46. package/dist/plugins/index.js.map +1 -0
  47. package/dist/plugins/logs-plugin.d.ts +22 -0
  48. package/dist/plugins/logs-plugin.d.ts.map +1 -0
  49. package/dist/plugins/logs-plugin.js +242 -0
  50. package/dist/plugins/logs-plugin.js.map +1 -0
  51. package/dist-ui/assets/index-Bk7ypbI4.js +465 -0
  52. package/dist-ui/assets/index-Bk7ypbI4.js.map +1 -0
  53. package/dist-ui/assets/index-CiizQQnb.css +1 -0
  54. package/dist-ui/index.html +13 -0
  55. package/package.json +98 -0
  56. package/src/core/control-panel.ts +493 -0
  57. package/src/core/gateway.ts +421 -0
  58. package/src/core/health-manager.ts +227 -0
  59. package/src/core/index.ts +25 -0
  60. package/src/core/logging.ts +234 -0
  61. package/src/core/types.ts +218 -0
  62. package/src/index.ts +55 -0
  63. package/src/plugins/config-plugin.ts +117 -0
  64. package/src/plugins/diagnostics-plugin.ts +178 -0
  65. package/src/plugins/health-plugin.ts +35 -0
  66. package/src/plugins/index.ts +17 -0
  67. package/src/plugins/logs-plugin.ts +314 -0
  68. package/ui/index.html +12 -0
  69. package/ui/src/App.tsx +65 -0
  70. package/ui/src/api/controlPanelApi.ts +148 -0
  71. package/ui/src/config/AppConfig.ts +18 -0
  72. package/ui/src/index.css +29 -0
  73. package/ui/src/index.tsx +11 -0
  74. package/ui/src/pages/ConfigPage.tsx +199 -0
  75. package/ui/src/pages/DashboardPage.tsx +264 -0
  76. package/ui/src/pages/DiagnosticsPage.tsx +315 -0
  77. package/ui/src/pages/HealthPage.tsx +204 -0
  78. package/ui/src/pages/LogsPage.tsx +267 -0
  79. package/ui/src/pages/NotFoundPage.tsx +41 -0
  80. package/ui/tsconfig.json +19 -0
  81. package/ui/vite.config.ts +21 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Control Panel Logging Subsystem
3
+ *
4
+ * Provides centralized logging for the control panel and all plugins.
5
+ * Configures @qwickapps/logging to write to files that the LogsPage can display.
6
+ *
7
+ * Environment Variables:
8
+ * LOG_LEVEL - Minimum log level (debug, info, warn, error). Default: debug in dev, info in prod
9
+ * LOG_DIR - Directory for log files. Default: ./logs
10
+ * LOG_FILE - Enable file logging. Default: true
11
+ * LOG_FILE_PATH - Path to log file. Default: ./logs/app.log (used by pino if available)
12
+ * LOG_CONSOLE - Enable console output. Default: true
13
+ *
14
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
15
+ */
16
+
17
+ import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
18
+ import { resolve, dirname } from 'node:path';
19
+ import { Logger as QwickLogger, getLogger, LogLevel, LogTransport } from '@qwickapps/logging';
20
+ import type { Logger } from './types.js';
21
+
22
+ export interface LoggingConfig {
23
+ /** Product namespace for log entries */
24
+ namespace?: string;
25
+ /** Minimum log level */
26
+ level?: LogLevel;
27
+ /** Directory for log files */
28
+ logDir?: string;
29
+ /** Enable file logging */
30
+ fileLogging?: boolean;
31
+ /** Enable console output */
32
+ consoleOutput?: boolean;
33
+ }
34
+
35
+ // Default configuration
36
+ const DEFAULT_CONFIG: Required<LoggingConfig> = {
37
+ namespace: 'ControlPanel',
38
+ level: (process.env.LOG_LEVEL as LogLevel) || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
39
+ logDir: process.env.LOG_DIR || './logs',
40
+ fileLogging: process.env.LOG_FILE !== 'false',
41
+ consoleOutput: process.env.LOG_CONSOLE !== 'false',
42
+ };
43
+
44
+ /**
45
+ * File transport for writing logs to disk
46
+ * Used when pino is not available or as additional transport
47
+ */
48
+ class FileLogTransport implements LogTransport {
49
+ private logPath: string;
50
+ private errorLogPath: string;
51
+
52
+ constructor(logDir: string) {
53
+ this.logPath = resolve(logDir, 'app.log');
54
+ this.errorLogPath = resolve(logDir, 'error.log');
55
+ this.ensureLogDir(logDir);
56
+ }
57
+
58
+ private ensureLogDir(logDir: string): void {
59
+ const resolvedDir = resolve(logDir);
60
+ if (!existsSync(resolvedDir)) {
61
+ mkdirSync(resolvedDir, { recursive: true });
62
+ }
63
+ }
64
+
65
+ handle(level: LogLevel, namespace: string, message: string, context?: Record<string, any>): void {
66
+ const entry = {
67
+ timestamp: new Date().toISOString(),
68
+ level,
69
+ ns: namespace,
70
+ msg: message,
71
+ ...context,
72
+ };
73
+
74
+ const line = JSON.stringify(entry) + '\n';
75
+
76
+ try {
77
+ // Write to app.log
78
+ appendFileSync(this.logPath, line);
79
+
80
+ // Also write errors to error.log
81
+ if (level === 'error') {
82
+ appendFileSync(this.errorLogPath, line);
83
+ }
84
+ } catch {
85
+ // Silently fail - don't let logging errors break the application
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Logging subsystem instance
92
+ */
93
+ class LoggingSubsystem {
94
+ private config: Required<LoggingConfig>;
95
+ private rootLogger: QwickLogger;
96
+ private fileTransport: FileLogTransport | null = null;
97
+ private initialized = false;
98
+
99
+ constructor() {
100
+ this.config = { ...DEFAULT_CONFIG };
101
+ this.rootLogger = getLogger(this.config.namespace);
102
+ }
103
+
104
+ /**
105
+ * Initialize the logging subsystem with configuration
106
+ */
107
+ initialize(config: LoggingConfig = {}): void {
108
+ if (this.initialized) {
109
+ return;
110
+ }
111
+
112
+ this.config = { ...DEFAULT_CONFIG, ...config };
113
+
114
+ // Set up file transport if enabled
115
+ if (this.config.fileLogging) {
116
+ this.fileTransport = new FileLogTransport(this.config.logDir);
117
+
118
+ // Configure the root logger with file transport
119
+ this.rootLogger.setConfig({
120
+ level: this.config.level,
121
+ disableConsole: !this.config.consoleOutput,
122
+ transports: [this.fileTransport],
123
+ });
124
+
125
+ // Set LOG_FILE_PATH for pino (if available)
126
+ if (!process.env.LOG_FILE_PATH) {
127
+ process.env.LOG_FILE = 'true';
128
+ process.env.LOG_FILE_PATH = resolve(this.config.logDir, 'app.log');
129
+ }
130
+ } else {
131
+ this.rootLogger.setConfig({
132
+ level: this.config.level,
133
+ disableConsole: !this.config.consoleOutput,
134
+ });
135
+ }
136
+
137
+ this.initialized = true;
138
+ this.rootLogger.info('Logging subsystem initialized', {
139
+ logDir: this.config.logDir,
140
+ level: this.config.level,
141
+ fileLogging: this.config.fileLogging,
142
+ consoleOutput: this.config.consoleOutput,
143
+ usingPino: this.rootLogger.isUsingPino(),
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Get a logger for a specific component/plugin
149
+ */
150
+ getLogger(namespace: string): Logger {
151
+ const childLogger = this.rootLogger.child(namespace);
152
+
153
+ // Return a Logger interface compatible with control-panel types
154
+ return {
155
+ debug: (message: string, data?: Record<string, unknown>) => {
156
+ childLogger.debug(message, data || {});
157
+ },
158
+ info: (message: string, data?: Record<string, unknown>) => {
159
+ childLogger.info(message, data || {});
160
+ },
161
+ warn: (message: string, data?: Record<string, unknown>) => {
162
+ childLogger.warn(message, data || {});
163
+ },
164
+ error: (message: string, data?: Record<string, unknown>) => {
165
+ childLogger.error(message, data || {});
166
+ },
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Get the root logger
172
+ */
173
+ getRootLogger(): Logger {
174
+ return this.getLogger('Core');
175
+ }
176
+
177
+ /**
178
+ * Check if file logging is enabled and working
179
+ */
180
+ isFileLoggingEnabled(): boolean {
181
+ return this.config.fileLogging && this.fileTransport !== null;
182
+ }
183
+
184
+ /**
185
+ * Get the log directory path
186
+ */
187
+ getLogDir(): string {
188
+ return resolve(this.config.logDir);
189
+ }
190
+
191
+ /**
192
+ * Get the default log file paths
193
+ */
194
+ getLogPaths(): { appLog: string; errorLog: string } {
195
+ return {
196
+ appLog: resolve(this.config.logDir, 'app.log'),
197
+ errorLog: resolve(this.config.logDir, 'error.log'),
198
+ };
199
+ }
200
+ }
201
+
202
+ // Singleton instance
203
+ let loggingSubsystem: LoggingSubsystem | null = null;
204
+
205
+ /**
206
+ * Get or create the logging subsystem
207
+ */
208
+ export function getLoggingSubsystem(): LoggingSubsystem {
209
+ if (!loggingSubsystem) {
210
+ loggingSubsystem = new LoggingSubsystem();
211
+ }
212
+ return loggingSubsystem;
213
+ }
214
+
215
+ /**
216
+ * Initialize the logging subsystem with configuration
217
+ */
218
+ export function initializeLogging(config: LoggingConfig = {}): LoggingSubsystem {
219
+ const subsystem = getLoggingSubsystem();
220
+ subsystem.initialize(config);
221
+ return subsystem;
222
+ }
223
+
224
+ /**
225
+ * Get a logger for a specific namespace
226
+ */
227
+ export function getControlPanelLogger(namespace: string): Logger {
228
+ return getLoggingSubsystem().getLogger(namespace);
229
+ }
230
+
231
+ /**
232
+ * Export for convenience
233
+ */
234
+ export { LoggingSubsystem };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Type definitions for @qwickapps/server
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+
7
+ import type { Application, RequestHandler, Router } from 'express';
8
+
9
+ /**
10
+ * Control Panel Configuration
11
+ */
12
+ export interface ControlPanelConfig {
13
+ /** Product name displayed in the control panel */
14
+ productName: string;
15
+
16
+ /** Port to run the control panel on */
17
+ port: number;
18
+
19
+ /** Optional: Product version */
20
+ version?: string;
21
+
22
+ /** Optional: Branding configuration */
23
+ branding?: {
24
+ logo?: string;
25
+ primaryColor?: string;
26
+ favicon?: string;
27
+ };
28
+
29
+ /** Optional: Authentication configuration */
30
+ auth?: {
31
+ enabled: boolean;
32
+ provider: 'basic' | 'jwt' | 'custom';
33
+ users?: Array<{ username: string; password: string }>;
34
+ jwtSecret?: string;
35
+ customMiddleware?: RequestHandler;
36
+ };
37
+
38
+ /** Optional: CORS configuration */
39
+ cors?: {
40
+ origins: string[];
41
+ };
42
+
43
+ /** Optional: Quick links to display */
44
+ links?: Array<{
45
+ label: string;
46
+ url: string;
47
+ icon?: string;
48
+ external?: boolean;
49
+ requiresHealth?: string; // Only show when this health check passes
50
+ }>;
51
+
52
+ /** Optional: Paths to skip body parsing for (useful for proxy middleware) */
53
+ skipBodyParserPaths?: string[];
54
+
55
+ /** Optional: Disable the built-in dashboard HTML (set true when serving a custom React UI) */
56
+ disableDashboard?: boolean;
57
+
58
+ /** Optional: Use rich React UI instead of basic HTML (default: true if dist-ui exists) */
59
+ useRichUI?: boolean;
60
+
61
+ /** Optional: Custom path to a dist-ui folder for serving a custom React UI */
62
+ customUiPath?: string;
63
+ }
64
+
65
+ /**
66
+ * Plugin Context - passed to plugins during initialization
67
+ */
68
+ export interface PluginContext {
69
+ config: ControlPanelConfig;
70
+ app: Application;
71
+ router: Router;
72
+ logger: Logger;
73
+ registerHealthCheck: (check: HealthCheck) => void;
74
+ }
75
+
76
+ /**
77
+ * Control Panel Plugin interface
78
+ */
79
+ export interface ControlPanelPlugin {
80
+ /** Unique plugin name */
81
+ name: string;
82
+
83
+ /** Order for tab display (lower = first) */
84
+ order?: number;
85
+
86
+ /** API routes provided by this plugin */
87
+ routes?: Array<{
88
+ method: 'get' | 'post' | 'put' | 'delete';
89
+ path: string;
90
+ handler: RequestHandler;
91
+ }>;
92
+
93
+ /** Lifecycle: Initialize plugin */
94
+ onInit?: (context: PluginContext) => Promise<void>;
95
+
96
+ /** Lifecycle: Shutdown plugin */
97
+ onShutdown?: () => Promise<void>;
98
+ }
99
+
100
+ /**
101
+ * Health Check types
102
+ */
103
+ export type HealthCheckType = 'http' | 'tcp' | 'custom';
104
+ export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
105
+
106
+ export interface HealthCheckResult {
107
+ status: HealthStatus;
108
+ latency?: number;
109
+ message?: string;
110
+ lastChecked: Date;
111
+ details?: Record<string, unknown>;
112
+ }
113
+
114
+ export interface HealthCheck {
115
+ name: string;
116
+ type: HealthCheckType;
117
+ /** For http checks */
118
+ url?: string;
119
+ /** For tcp checks */
120
+ host?: string;
121
+ port?: number;
122
+ /** Check interval in ms */
123
+ interval?: number;
124
+ /** Timeout in ms */
125
+ timeout?: number;
126
+ /** For custom checks */
127
+ check?: () => Promise<{ healthy: boolean; latency?: number; details?: Record<string, unknown> }>;
128
+ }
129
+
130
+ /**
131
+ * Log Source Configuration
132
+ */
133
+ export interface LogSource {
134
+ name: string;
135
+ type: 'file' | 'api';
136
+ path?: string; // For file sources
137
+ url?: string; // For API sources
138
+ }
139
+
140
+ /**
141
+ * Config Display Configuration
142
+ */
143
+ export interface ConfigDisplayOptions {
144
+ /** Environment variables to show */
145
+ show: string[];
146
+ /** Environment variables to mask */
147
+ mask: string[];
148
+ /** Validation rules */
149
+ validate?: Array<{
150
+ key: string;
151
+ required?: boolean;
152
+ pattern?: RegExp;
153
+ minLength?: number;
154
+ }>;
155
+ }
156
+
157
+ /**
158
+ * Simple logger interface
159
+ */
160
+ export interface Logger {
161
+ debug: (message: string, data?: Record<string, unknown>) => void;
162
+ info: (message: string, data?: Record<string, unknown>) => void;
163
+ warn: (message: string, data?: Record<string, unknown>) => void;
164
+ error: (message: string, data?: Record<string, unknown>) => void;
165
+ }
166
+
167
+ /**
168
+ * Control Panel Instance
169
+ */
170
+ export interface ControlPanelInstance {
171
+ /** Express application */
172
+ app: Application;
173
+
174
+ /** Start the control panel server */
175
+ start: () => Promise<void>;
176
+
177
+ /** Stop the control panel server */
178
+ stop: () => Promise<void>;
179
+
180
+ /** Register a plugin */
181
+ registerPlugin: (plugin: ControlPanelPlugin) => Promise<void>;
182
+
183
+ /** Get health check results */
184
+ getHealthStatus: () => Record<string, HealthCheckResult>;
185
+
186
+ /** Get diagnostics for AI agents */
187
+ getDiagnostics: () => DiagnosticsReport;
188
+ }
189
+
190
+ /**
191
+ * Diagnostics Report for AI agents
192
+ */
193
+ export interface DiagnosticsReport {
194
+ timestamp: string;
195
+ product: string;
196
+ version?: string;
197
+ uptime: number;
198
+ health: Record<string, HealthCheckResult>;
199
+ system: {
200
+ nodeVersion: string;
201
+ platform: string;
202
+ arch: string;
203
+ memory: {
204
+ total: number;
205
+ used: number;
206
+ free: number;
207
+ };
208
+ cpu: {
209
+ usage: number;
210
+ };
211
+ };
212
+ config?: Record<string, string>;
213
+ logs?: {
214
+ startup: string[];
215
+ recent: string[];
216
+ errors: string[];
217
+ };
218
+ }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @qwickapps/server
3
+ *
4
+ * Independent control panel and management UI for QwickApps services
5
+ * Runs on a separate port, provides health checks, logs, config, and diagnostics
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ // Core exports
11
+ export { createControlPanel } from './core/control-panel.js';
12
+ export { createGateway } from './core/gateway.js';
13
+ export { HealthManager } from './core/health-manager.js';
14
+
15
+ // Logging exports
16
+ export {
17
+ initializeLogging,
18
+ getControlPanelLogger,
19
+ getLoggingSubsystem,
20
+ } from './core/logging.js';
21
+ export type { LoggingConfig } from './core/logging.js';
22
+
23
+ export type {
24
+ ControlPanelConfig,
25
+ ControlPanelPlugin,
26
+ ControlPanelInstance,
27
+ PluginContext,
28
+ HealthCheck,
29
+ HealthCheckType,
30
+ HealthCheckResult,
31
+ HealthStatus,
32
+ LogSource,
33
+ ConfigDisplayOptions,
34
+ Logger,
35
+ DiagnosticsReport,
36
+ } from './core/types.js';
37
+ export type {
38
+ GatewayConfig,
39
+ GatewayInstance,
40
+ ServiceFactory,
41
+ } from './core/gateway.js';
42
+
43
+ // Built-in plugins
44
+ export {
45
+ createHealthPlugin,
46
+ createLogsPlugin,
47
+ createConfigPlugin,
48
+ createDiagnosticsPlugin,
49
+ } from './plugins/index.js';
50
+ export type {
51
+ HealthPluginConfig,
52
+ LogsPluginConfig,
53
+ ConfigPluginConfig,
54
+ DiagnosticsPluginConfig,
55
+ } from './plugins/index.js';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Config Plugin
3
+ *
4
+ * Displays configuration and environment variables with secret masking
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ import type { Request, Response } from 'express';
10
+ import type { ControlPanelPlugin, ConfigDisplayOptions, PluginContext } from '../core/types.js';
11
+
12
+ export interface ConfigPluginConfig extends ConfigDisplayOptions {}
13
+
14
+ interface ConfigValidationResult {
15
+ key: string;
16
+ valid: boolean;
17
+ message?: string;
18
+ }
19
+
20
+ /**
21
+ * Create a config display plugin
22
+ */
23
+ export function createConfigPlugin(config: ConfigPluginConfig): ControlPanelPlugin {
24
+ return {
25
+ name: 'config',
26
+ order: 30,
27
+
28
+ routes: [
29
+ {
30
+ method: 'get',
31
+ path: '/config',
32
+ handler: (_req: Request, res: Response) => {
33
+ const envVars: Record<string, string> = {};
34
+
35
+ // Get visible env vars
36
+ for (const key of config.show) {
37
+ const value = process.env[key];
38
+ if (value !== undefined) {
39
+ // Mask if in mask list
40
+ if (config.mask.some((m) => key.toLowerCase().includes(m.toLowerCase()))) {
41
+ envVars[key] = maskValue(value);
42
+ } else {
43
+ envVars[key] = value;
44
+ }
45
+ } else {
46
+ envVars[key] = '<not set>';
47
+ }
48
+ }
49
+
50
+ res.json({
51
+ environment: process.env.NODE_ENV || 'development',
52
+ config: envVars,
53
+ });
54
+ },
55
+ },
56
+ {
57
+ method: 'get',
58
+ path: '/config/validate',
59
+ handler: (_req: Request, res: Response) => {
60
+ const results: ConfigValidationResult[] = [];
61
+ let allValid = true;
62
+
63
+ if (config.validate) {
64
+ for (const rule of config.validate) {
65
+ const value = process.env[rule.key];
66
+ let valid = true;
67
+ let message: string | undefined;
68
+
69
+ // Required check
70
+ if (rule.required && !value) {
71
+ valid = false;
72
+ message = `Required environment variable "${rule.key}" is not set`;
73
+ }
74
+
75
+ // Pattern check
76
+ if (valid && value && rule.pattern && !rule.pattern.test(value)) {
77
+ valid = false;
78
+ message = `Environment variable "${rule.key}" does not match expected pattern`;
79
+ }
80
+
81
+ // Min length check
82
+ if (valid && value && rule.minLength && value.length < rule.minLength) {
83
+ valid = false;
84
+ message = `Environment variable "${rule.key}" is too short (min ${rule.minLength} chars)`;
85
+ }
86
+
87
+ if (!valid) {
88
+ allValid = false;
89
+ }
90
+
91
+ results.push({ key: rule.key, valid, message });
92
+ }
93
+ }
94
+
95
+ res.json({
96
+ valid: allValid,
97
+ results,
98
+ });
99
+ },
100
+ },
101
+ ],
102
+
103
+ async onInit(context: PluginContext): Promise<void> {
104
+ context.logger.info(`[ConfigPlugin] Initialized with ${config.show.length} visible vars`);
105
+ },
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Mask a sensitive value
111
+ */
112
+ function maskValue(value: string): string {
113
+ if (value.length <= 4) {
114
+ return '****';
115
+ }
116
+ return value.substring(0, 2) + '*'.repeat(value.length - 4) + value.substring(value.length - 2);
117
+ }