@superdangerous/app-framework 4.9.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.
- package/LICENSE +21 -0
- package/README.md +652 -0
- package/dist/api/logsRouter.d.ts +20 -0
- package/dist/api/logsRouter.d.ts.map +1 -0
- package/dist/api/logsRouter.js +515 -0
- package/dist/api/logsRouter.js.map +1 -0
- package/dist/cli/dev-server.d.ts +7 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +640 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/StandardServer.d.ts +129 -0
- package/dist/core/StandardServer.d.ts.map +1 -0
- package/dist/core/StandardServer.js +453 -0
- package/dist/core/StandardServer.js.map +1 -0
- package/dist/core/apiResponse.d.ts +69 -0
- package/dist/core/apiResponse.d.ts.map +1 -0
- package/dist/core/apiResponse.js +127 -0
- package/dist/core/apiResponse.js.map +1 -0
- package/dist/core/healthCheck.d.ts +160 -0
- package/dist/core/healthCheck.d.ts.map +1 -0
- package/dist/core/healthCheck.js +398 -0
- package/dist/core/healthCheck.js.map +1 -0
- package/dist/core/index.d.ts +40 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +40 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +117 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +826 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/portUtils.d.ts +71 -0
- package/dist/core/portUtils.d.ts.map +1 -0
- package/dist/core/portUtils.js +240 -0
- package/dist/core/portUtils.js.map +1 -0
- package/dist/core/storageService.d.ts +119 -0
- package/dist/core/storageService.d.ts.map +1 -0
- package/dist/core/storageService.js +405 -0
- package/dist/core/storageService.js.map +1 -0
- package/dist/desktop/bundler.d.ts +40 -0
- package/dist/desktop/bundler.d.ts.map +1 -0
- package/dist/desktop/bundler.js +176 -0
- package/dist/desktop/bundler.js.map +1 -0
- package/dist/desktop/index.d.ts +25 -0
- package/dist/desktop/index.d.ts.map +1 -0
- package/dist/desktop/index.js +15 -0
- package/dist/desktop/index.js.map +1 -0
- package/dist/desktop/native-modules.d.ts +66 -0
- package/dist/desktop/native-modules.d.ts.map +1 -0
- package/dist/desktop/native-modules.js +200 -0
- package/dist/desktop/native-modules.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/LogCategories.d.ts +87 -0
- package/dist/logging/LogCategories.d.ts.map +1 -0
- package/dist/logging/LogCategories.js +205 -0
- package/dist/logging/LogCategories.js.map +1 -0
- package/dist/middleware/aiErrorHandler.d.ts +31 -0
- package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
- package/dist/middleware/aiErrorHandler.js +181 -0
- package/dist/middleware/aiErrorHandler.js.map +1 -0
- package/dist/middleware/auth.d.ts +101 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +230 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +56 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +123 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +13 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +85 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/fileUpload.d.ts +62 -0
- package/dist/middleware/fileUpload.d.ts.map +1 -0
- package/dist/middleware/fileUpload.js +175 -0
- package/dist/middleware/fileUpload.js.map +1 -0
- package/dist/middleware/health.d.ts +48 -0
- package/dist/middleware/health.d.ts.map +1 -0
- package/dist/middleware/health.js +143 -0
- package/dist/middleware/health.js.map +1 -0
- package/dist/middleware/index.d.ts +20 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/openapi.d.ts +64 -0
- package/dist/middleware/openapi.d.ts.map +1 -0
- package/dist/middleware/openapi.js +258 -0
- package/dist/middleware/openapi.js.map +1 -0
- package/dist/middleware/requestLogging.d.ts +22 -0
- package/dist/middleware/requestLogging.d.ts.map +1 -0
- package/dist/middleware/requestLogging.js +61 -0
- package/dist/middleware/requestLogging.js.map +1 -0
- package/dist/middleware/session.d.ts +84 -0
- package/dist/middleware/session.d.ts.map +1 -0
- package/dist/middleware/session.js +189 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/validation.d.ts +1337 -0
- package/dist/middleware/validation.d.ts.map +1 -0
- package/dist/middleware/validation.js +483 -0
- package/dist/middleware/validation.js.map +1 -0
- package/dist/services/aiService.d.ts +180 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +547 -0
- package/dist/services/aiService.js.map +1 -0
- package/dist/services/conversationStorage.d.ts +38 -0
- package/dist/services/conversationStorage.d.ts.map +1 -0
- package/dist/services/conversationStorage.js +158 -0
- package/dist/services/conversationStorage.js.map +1 -0
- package/dist/services/crossPlatformBuffer.d.ts +84 -0
- package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
- package/dist/services/crossPlatformBuffer.js +246 -0
- package/dist/services/crossPlatformBuffer.js.map +1 -0
- package/dist/services/index.d.ts +17 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +18 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/networkService.d.ts +81 -0
- package/dist/services/networkService.d.ts.map +1 -0
- package/dist/services/networkService.js +268 -0
- package/dist/services/networkService.js.map +1 -0
- package/dist/services/queueService.d.ts +112 -0
- package/dist/services/queueService.d.ts.map +1 -0
- package/dist/services/queueService.js +338 -0
- package/dist/services/queueService.js.map +1 -0
- package/dist/services/settingsService.d.ts +135 -0
- package/dist/services/settingsService.d.ts.map +1 -0
- package/dist/services/settingsService.js +425 -0
- package/dist/services/settingsService.js.map +1 -0
- package/dist/services/systemMonitor.d.ts +208 -0
- package/dist/services/systemMonitor.d.ts.map +1 -0
- package/dist/services/systemMonitor.js +693 -0
- package/dist/services/systemMonitor.js.map +1 -0
- package/dist/services/updateService.d.ts +78 -0
- package/dist/services/updateService.d.ts.map +1 -0
- package/dist/services/updateService.js +252 -0
- package/dist/services/updateService.js.map +1 -0
- package/dist/services/websocketEvents.d.ts +372 -0
- package/dist/services/websocketEvents.d.ts.map +1 -0
- package/dist/services/websocketEvents.js +338 -0
- package/dist/services/websocketEvents.js.map +1 -0
- package/dist/services/websocketServer.d.ts +80 -0
- package/dist/services/websocketServer.d.ts.map +1 -0
- package/dist/services/websocketServer.js +299 -0
- package/dist/services/websocketServer.js.map +1 -0
- package/dist/settings/SettingsSchema.d.ts +151 -0
- package/dist/settings/SettingsSchema.d.ts.map +1 -0
- package/dist/settings/SettingsSchema.js +424 -0
- package/dist/settings/SettingsSchema.js.map +1 -0
- package/dist/testing/TestServer.d.ts +69 -0
- package/dist/testing/TestServer.d.ts.map +1 -0
- package/dist/testing/TestServer.js +250 -0
- package/dist/testing/TestServer.js.map +1 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/appPaths.d.ts +74 -0
- package/dist/utils/appPaths.d.ts.map +1 -0
- package/dist/utils/appPaths.js +162 -0
- package/dist/utils/appPaths.js.map +1 -0
- package/dist/utils/fs-utils.d.ts +50 -0
- package/dist/utils/fs-utils.d.ts.map +1 -0
- package/dist/utils/fs-utils.js +114 -0
- package/dist/utils/fs-utils.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/standardConfig.d.ts +61 -0
- package/dist/utils/standardConfig.d.ts.map +1 -0
- package/dist/utils/standardConfig.js +109 -0
- package/dist/utils/standardConfig.js.map +1 -0
- package/dist/utils/startupBanner.d.ts +34 -0
- package/dist/utils/startupBanner.d.ts.map +1 -0
- package/dist/utils/startupBanner.js +169 -0
- package/dist/utils/startupBanner.js.map +1 -0
- package/dist/utils/startupLogger.d.ts +45 -0
- package/dist/utils/startupLogger.d.ts.map +1 -0
- package/dist/utils/startupLogger.js +200 -0
- package/dist/utils/startupLogger.js.map +1 -0
- package/package.json +151 -0
- package/src/api/logsRouter.ts +600 -0
- package/src/cli/dev-server.ts +803 -0
- package/src/cli/index.ts +31 -0
- package/src/core/StandardServer.ts +587 -0
- package/src/core/apiResponse.ts +202 -0
- package/src/core/healthCheck.ts +565 -0
- package/src/core/index.ts +80 -0
- package/src/core/logger.ts +1092 -0
- package/src/core/portUtils.ts +319 -0
- package/src/core/storageService.ts +595 -0
- package/src/desktop/bundler.ts +271 -0
- package/src/desktop/index.ts +18 -0
- package/src/desktop/native-modules.ts +289 -0
- package/src/index.ts +142 -0
- package/src/logging/LogCategories.ts +302 -0
- package/src/middleware/aiErrorHandler.ts +278 -0
- package/src/middleware/auth.ts +329 -0
- package/src/middleware/cors.ts +187 -0
- package/src/middleware/errorHandler.ts +103 -0
- package/src/middleware/fileUpload.ts +252 -0
- package/src/middleware/health.ts +206 -0
- package/src/middleware/index.ts +71 -0
- package/src/middleware/openapi.ts +305 -0
- package/src/middleware/requestLogging.ts +92 -0
- package/src/middleware/session.ts +238 -0
- package/src/middleware/validation.ts +603 -0
- package/src/services/aiService.ts +789 -0
- package/src/services/conversationStorage.ts +232 -0
- package/src/services/crossPlatformBuffer.ts +341 -0
- package/src/services/index.ts +47 -0
- package/src/services/networkService.ts +351 -0
- package/src/services/queueService.ts +446 -0
- package/src/services/settingsService.ts +549 -0
- package/src/services/systemMonitor.ts +936 -0
- package/src/services/updateService.ts +334 -0
- package/src/services/websocketEvents.ts +409 -0
- package/src/services/websocketServer.ts +394 -0
- package/src/settings/SettingsSchema.ts +664 -0
- package/src/testing/TestServer.ts +312 -0
- package/src/types/index.ts +154 -0
- package/src/utils/appPaths.ts +196 -0
- package/src/utils/fs-utils.ts +130 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/standardConfig.ts +178 -0
- package/src/utils/startupBanner.ts +287 -0
- package/src/utils/startupLogger.ts +268 -0
- package/ui/dist/index.d.mts +1221 -0
- package/ui/dist/index.d.ts +1221 -0
- package/ui/dist/index.js +73 -0
- package/ui/dist/index.js.map +1 -0
- package/ui/dist/index.mjs +73 -0
- package/ui/dist/index.mjs.map +1 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Service
|
|
3
|
+
* Manages application settings with persistence, validation, and real-time updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import { ZodSchema } from "zod";
|
|
10
|
+
import { createLogger } from "../core/index.js";
|
|
11
|
+
|
|
12
|
+
let logger: any; // Will be initialized when needed
|
|
13
|
+
|
|
14
|
+
function ensureLogger() {
|
|
15
|
+
if (!logger) {
|
|
16
|
+
logger = createLogger("SettingsService");
|
|
17
|
+
}
|
|
18
|
+
return logger;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SettingsCategory {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
icon?: string;
|
|
26
|
+
schema?: ZodSchema;
|
|
27
|
+
fields: SettingsField[];
|
|
28
|
+
order?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SettingsField {
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
type:
|
|
35
|
+
| "text"
|
|
36
|
+
| "number"
|
|
37
|
+
| "boolean"
|
|
38
|
+
| "select"
|
|
39
|
+
| "multiselect"
|
|
40
|
+
| "json"
|
|
41
|
+
| "password"
|
|
42
|
+
| "email"
|
|
43
|
+
| "url"
|
|
44
|
+
| "color"
|
|
45
|
+
| "date"
|
|
46
|
+
| "time"
|
|
47
|
+
| "datetime";
|
|
48
|
+
description?: string;
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
defaultValue?: any;
|
|
51
|
+
required?: boolean;
|
|
52
|
+
validation?: ZodSchema;
|
|
53
|
+
options?: Array<{ value: any; label: string; description?: string }>;
|
|
54
|
+
min?: number;
|
|
55
|
+
max?: number;
|
|
56
|
+
step?: number;
|
|
57
|
+
pattern?: string;
|
|
58
|
+
readonly?: boolean;
|
|
59
|
+
hidden?: boolean;
|
|
60
|
+
dependsOn?: {
|
|
61
|
+
field: string;
|
|
62
|
+
value: any;
|
|
63
|
+
};
|
|
64
|
+
transform?: (value: any) => any;
|
|
65
|
+
format?: (value: any) => string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SettingsOptions {
|
|
69
|
+
storagePath?: string;
|
|
70
|
+
autoSave?: boolean;
|
|
71
|
+
saveDebounce?: number;
|
|
72
|
+
encryptSensitive?: boolean;
|
|
73
|
+
sensitiveFields?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class SettingsService extends EventEmitter {
|
|
77
|
+
private categories: Map<string, SettingsCategory> = new Map();
|
|
78
|
+
private settings: Record<string, any> = {};
|
|
79
|
+
private storagePath?: string;
|
|
80
|
+
private autoSave: boolean;
|
|
81
|
+
private saveDebounce: number;
|
|
82
|
+
private saveTimeout?: NodeJS.Timeout;
|
|
83
|
+
private sensitiveFields: Set<string>;
|
|
84
|
+
|
|
85
|
+
constructor(private options: SettingsOptions = {}) {
|
|
86
|
+
super();
|
|
87
|
+
this.storagePath = options.storagePath;
|
|
88
|
+
this.autoSave = options.autoSave ?? true;
|
|
89
|
+
this.saveDebounce = options.saveDebounce ?? 1000;
|
|
90
|
+
this.sensitiveFields = new Set(options.sensitiveFields || []);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register a settings category
|
|
95
|
+
*/
|
|
96
|
+
registerCategory(category: SettingsCategory): void {
|
|
97
|
+
this.categories.set(category.id, category);
|
|
98
|
+
|
|
99
|
+
// Initialize default values
|
|
100
|
+
for (const field of category.fields) {
|
|
101
|
+
if (field.defaultValue !== undefined && !(field.key in this.settings)) {
|
|
102
|
+
this.settings[field.key] = field.defaultValue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ensureLogger().info(`Registered settings category: ${category.id}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Load settings from storage
|
|
111
|
+
*/
|
|
112
|
+
async load(): Promise<void> {
|
|
113
|
+
if (!this.storagePath) {
|
|
114
|
+
ensureLogger().info("No storage path configured, using defaults");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const fullPath = path.resolve(this.storagePath);
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(fullPath)) {
|
|
122
|
+
ensureLogger().info("Settings file not found, using defaults");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const content = await fs.promises.readFile(fullPath, "utf-8");
|
|
127
|
+
const loaded = JSON.parse(content);
|
|
128
|
+
|
|
129
|
+
// Merge with defaults
|
|
130
|
+
this.settings = { ...this.settings, ...loaded };
|
|
131
|
+
|
|
132
|
+
// Decrypt sensitive fields if needed
|
|
133
|
+
if (this.options.encryptSensitive) {
|
|
134
|
+
this.decryptSensitiveFields();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ensureLogger().info("Settings loaded successfully");
|
|
138
|
+
this.emit("loaded", this.settings);
|
|
139
|
+
} catch (_error: any) {
|
|
140
|
+
ensureLogger().error("Failed to load settings:", _error);
|
|
141
|
+
throw new Error(`Failed to load settings: ${_error.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Save settings to storage
|
|
147
|
+
*/
|
|
148
|
+
async save(): Promise<void> {
|
|
149
|
+
if (!this.storagePath) {
|
|
150
|
+
ensureLogger().info("No storage path configured, skipping save");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const fullPath = path.resolve(this.storagePath);
|
|
156
|
+
const dir = path.dirname(fullPath);
|
|
157
|
+
|
|
158
|
+
// Ensure directory exists
|
|
159
|
+
if (!fs.existsSync(dir)) {
|
|
160
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Prepare settings for saving
|
|
164
|
+
let toSave = { ...this.settings };
|
|
165
|
+
|
|
166
|
+
// Encrypt sensitive fields if needed
|
|
167
|
+
if (this.options.encryptSensitive) {
|
|
168
|
+
toSave = this.encryptSensitiveFields(toSave);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Save to file
|
|
172
|
+
const content = JSON.stringify(toSave, null, 2);
|
|
173
|
+
await fs.promises.writeFile(fullPath, content, "utf-8");
|
|
174
|
+
|
|
175
|
+
ensureLogger().info("Settings saved successfully");
|
|
176
|
+
this.emit("saved", this.settings);
|
|
177
|
+
} catch (_error: any) {
|
|
178
|
+
ensureLogger().error("Failed to save settings:", _error);
|
|
179
|
+
throw new Error(`Failed to save settings: ${_error.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get a setting value
|
|
185
|
+
*/
|
|
186
|
+
get<T = any>(key: string, defaultValue?: T): T {
|
|
187
|
+
const value = this.getNestedValue(this.settings, key);
|
|
188
|
+
return value !== undefined ? value : (defaultValue as T);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Set a setting value
|
|
193
|
+
*/
|
|
194
|
+
async set(key: string, value: any): Promise<void> {
|
|
195
|
+
const previous = this.get(key);
|
|
196
|
+
|
|
197
|
+
// Find field definition
|
|
198
|
+
const field = this.findField(key);
|
|
199
|
+
|
|
200
|
+
// Validate if field has validation
|
|
201
|
+
if (field?.validation) {
|
|
202
|
+
const result = field.validation.safeParse(value);
|
|
203
|
+
if (!result.success) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Validation failed for ${key}: ${result.error.message}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
value = result.data;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Apply transform if defined
|
|
212
|
+
if (field?.transform) {
|
|
213
|
+
value = field.transform(value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Set the value
|
|
217
|
+
this.setNestedValue(this.settings, key, value);
|
|
218
|
+
|
|
219
|
+
// Emit change event
|
|
220
|
+
this.emit("change", { key, value, previous });
|
|
221
|
+
|
|
222
|
+
// Auto-save if enabled
|
|
223
|
+
if (this.autoSave) {
|
|
224
|
+
this.scheduleSave();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Update multiple settings
|
|
230
|
+
*/
|
|
231
|
+
async update(updates: Record<string, any>): Promise<void> {
|
|
232
|
+
const changes: Array<{ key: string; value: any; previous: any }> = [];
|
|
233
|
+
|
|
234
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
235
|
+
const previous = this.get(key);
|
|
236
|
+
await this.set(key, value);
|
|
237
|
+
changes.push({ key, value, previous });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.emit("bulk-change", changes);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get all settings
|
|
245
|
+
*/
|
|
246
|
+
getAll(): Record<string, any> {
|
|
247
|
+
return { ...this.settings };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get settings by category
|
|
252
|
+
*/
|
|
253
|
+
getByCategory(categoryId: string): Record<string, any> {
|
|
254
|
+
const category = this.categories.get(categoryId);
|
|
255
|
+
if (!category) {
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result: Record<string, any> = {};
|
|
260
|
+
for (const field of category.fields) {
|
|
261
|
+
result[field.key] = this.get(field.key, field.defaultValue);
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get all categories
|
|
268
|
+
*/
|
|
269
|
+
getCategories(): SettingsCategory[] {
|
|
270
|
+
return Array.from(this.categories.values()).sort(
|
|
271
|
+
(a, b) => (a.order || 0) - (b.order || 0),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get category by ID
|
|
277
|
+
*/
|
|
278
|
+
getCategory(id: string): SettingsCategory | undefined {
|
|
279
|
+
return this.categories.get(id);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Validate all settings
|
|
284
|
+
*/
|
|
285
|
+
validate(): { valid: boolean; errors: Record<string, string> } {
|
|
286
|
+
const errors: Record<string, string> = {};
|
|
287
|
+
|
|
288
|
+
for (const category of this.categories.values()) {
|
|
289
|
+
for (const field of category.fields) {
|
|
290
|
+
const value = this.get(field.key);
|
|
291
|
+
|
|
292
|
+
// Check required fields
|
|
293
|
+
if (
|
|
294
|
+
field.required &&
|
|
295
|
+
(value === undefined || value === null || value === "")
|
|
296
|
+
) {
|
|
297
|
+
errors[field.key] = `${field.label} is required`;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Validate with schema
|
|
302
|
+
if (field.validation && value !== undefined) {
|
|
303
|
+
const result = field.validation.safeParse(value);
|
|
304
|
+
if (!result.success) {
|
|
305
|
+
errors[field.key] =
|
|
306
|
+
result.error.issues[0]?.message || "Validation failed";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
valid: Object.keys(errors).length === 0,
|
|
314
|
+
errors,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Reset settings to defaults
|
|
320
|
+
*/
|
|
321
|
+
reset(categoryId?: string): void {
|
|
322
|
+
if (categoryId) {
|
|
323
|
+
const category = this.categories.get(categoryId);
|
|
324
|
+
if (category) {
|
|
325
|
+
for (const field of category.fields) {
|
|
326
|
+
if (field.defaultValue !== undefined) {
|
|
327
|
+
delete this.settings[field.key];
|
|
328
|
+
this.setNestedValue(this.settings, field.key, field.defaultValue);
|
|
329
|
+
} else {
|
|
330
|
+
delete this.settings[field.key];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
// Reset all settings
|
|
336
|
+
this.settings = {};
|
|
337
|
+
for (const category of this.categories.values()) {
|
|
338
|
+
for (const field of category.fields) {
|
|
339
|
+
if (field.defaultValue !== undefined) {
|
|
340
|
+
this.settings[field.key] = field.defaultValue;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.emit("reset", categoryId);
|
|
347
|
+
|
|
348
|
+
if (this.autoSave) {
|
|
349
|
+
this.scheduleSave();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Export settings
|
|
355
|
+
*/
|
|
356
|
+
export(categoryId?: string): string {
|
|
357
|
+
const data = categoryId ? this.getByCategory(categoryId) : this.settings;
|
|
358
|
+
return JSON.stringify(data, null, 2);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Import settings
|
|
363
|
+
*/
|
|
364
|
+
async import(data: string): Promise<void> {
|
|
365
|
+
try {
|
|
366
|
+
const imported = JSON.parse(data);
|
|
367
|
+
await this.update(imported);
|
|
368
|
+
ensureLogger().info("Settings imported successfully");
|
|
369
|
+
} catch (_error: any) {
|
|
370
|
+
ensureLogger().error("Failed to import settings:", _error);
|
|
371
|
+
throw new Error(`Failed to import settings: ${_error.message}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Private methods
|
|
376
|
+
|
|
377
|
+
private findField(key: string): SettingsField | undefined {
|
|
378
|
+
for (const category of this.categories.values()) {
|
|
379
|
+
const field = category.fields.find((f) => f.key === key);
|
|
380
|
+
if (field) return field;
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private scheduleSave(): void {
|
|
386
|
+
if (this.saveTimeout) {
|
|
387
|
+
clearTimeout(this.saveTimeout);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.saveTimeout = setTimeout(async () => {
|
|
391
|
+
try {
|
|
392
|
+
await this.save();
|
|
393
|
+
} catch (_error: any) {
|
|
394
|
+
ensureLogger().error("Auto-save failed:", _error);
|
|
395
|
+
this.emit("save-error", _error);
|
|
396
|
+
}
|
|
397
|
+
}, this.saveDebounce);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private encryptSensitiveFields(
|
|
401
|
+
settings: Record<string, any>,
|
|
402
|
+
): Record<string, any> {
|
|
403
|
+
const result = { ...settings };
|
|
404
|
+
|
|
405
|
+
for (const field of this.sensitiveFields) {
|
|
406
|
+
const value = this.getNestedValue(result, field);
|
|
407
|
+
if (value !== undefined && typeof value === "string") {
|
|
408
|
+
// Simple base64 encoding for demonstration
|
|
409
|
+
// In production, use proper encryption
|
|
410
|
+
this.setNestedValue(
|
|
411
|
+
result,
|
|
412
|
+
field,
|
|
413
|
+
Buffer.from(value).toString("base64"),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private decryptSensitiveFields(): void {
|
|
422
|
+
for (const field of this.sensitiveFields) {
|
|
423
|
+
const value = this.getNestedValue(this.settings, field);
|
|
424
|
+
if (value !== undefined && typeof value === "string") {
|
|
425
|
+
try {
|
|
426
|
+
// Simple base64 decoding for demonstration
|
|
427
|
+
// In production, use proper decryption
|
|
428
|
+
this.setNestedValue(
|
|
429
|
+
this.settings,
|
|
430
|
+
field,
|
|
431
|
+
Buffer.from(value, "base64").toString(),
|
|
432
|
+
);
|
|
433
|
+
} catch {
|
|
434
|
+
// Value might not be encrypted, leave as is
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private getNestedValue(obj: any, path: string): any {
|
|
441
|
+
// Support both nested objects and flat keys containing dots
|
|
442
|
+
if (Object.prototype.hasOwnProperty.call(obj, path)) {
|
|
443
|
+
return (obj as any)[path];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private setNestedValue(obj: any, path: string, value: any): void {
|
|
450
|
+
const keys = path.split(".");
|
|
451
|
+
const lastKey = keys.pop()!;
|
|
452
|
+
const target = keys.reduce((current, key) => {
|
|
453
|
+
if (!current[key]) {
|
|
454
|
+
current[key] = {};
|
|
455
|
+
}
|
|
456
|
+
return current[key];
|
|
457
|
+
}, obj);
|
|
458
|
+
target[lastKey] = value;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Create common settings categories
|
|
464
|
+
*/
|
|
465
|
+
export const CommonSettingsCategories = {
|
|
466
|
+
general: (): SettingsCategory => ({
|
|
467
|
+
id: "general",
|
|
468
|
+
label: "General",
|
|
469
|
+
description: "General application settings",
|
|
470
|
+
icon: "Settings",
|
|
471
|
+
order: 1,
|
|
472
|
+
fields: [
|
|
473
|
+
{
|
|
474
|
+
key: "app.name",
|
|
475
|
+
label: "Application Name",
|
|
476
|
+
type: "text",
|
|
477
|
+
defaultValue: "My Application",
|
|
478
|
+
required: true,
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
key: "app.description",
|
|
482
|
+
label: "Description",
|
|
483
|
+
type: "text",
|
|
484
|
+
placeholder: "Enter application description",
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
}),
|
|
488
|
+
|
|
489
|
+
server: (): SettingsCategory => ({
|
|
490
|
+
id: "server",
|
|
491
|
+
label: "Server",
|
|
492
|
+
description: "Server configuration",
|
|
493
|
+
icon: "Server",
|
|
494
|
+
order: 2,
|
|
495
|
+
fields: [
|
|
496
|
+
{
|
|
497
|
+
key: "server.port",
|
|
498
|
+
label: "Port",
|
|
499
|
+
type: "number",
|
|
500
|
+
defaultValue: 3000,
|
|
501
|
+
min: 1,
|
|
502
|
+
max: 65535,
|
|
503
|
+
required: true,
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
key: "server.host",
|
|
507
|
+
label: "Host",
|
|
508
|
+
type: "text",
|
|
509
|
+
defaultValue: "localhost",
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
key: "server.https.enabled",
|
|
513
|
+
label: "Enable HTTPS",
|
|
514
|
+
type: "boolean",
|
|
515
|
+
defaultValue: false,
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
}),
|
|
519
|
+
|
|
520
|
+
logging: (): SettingsCategory => ({
|
|
521
|
+
id: "logging",
|
|
522
|
+
label: "Logging",
|
|
523
|
+
description: "Logging configuration",
|
|
524
|
+
icon: "FileText",
|
|
525
|
+
order: 3,
|
|
526
|
+
fields: [
|
|
527
|
+
{
|
|
528
|
+
key: "logging.level",
|
|
529
|
+
label: "Log Level",
|
|
530
|
+
type: "select",
|
|
531
|
+
defaultValue: "info",
|
|
532
|
+
options: [
|
|
533
|
+
{ value: "debug", label: "Debug" },
|
|
534
|
+
{ value: "info", label: "Info" },
|
|
535
|
+
{ value: "warn", label: "Warning" },
|
|
536
|
+
{ value: "error", label: "Error" },
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
key: "logging.file",
|
|
541
|
+
label: "Log File Path",
|
|
542
|
+
type: "text",
|
|
543
|
+
placeholder: "./logs/app.log",
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
}),
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
export default SettingsService;
|