@tamyla/clodo-framework 2.0.20 → 3.0.3

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 (54) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/bin/clodo-service.js +1 -1
  3. package/bin/database/README.md +33 -0
  4. package/bin/database/deployment-db-manager.js +527 -0
  5. package/bin/database/enterprise-db-manager.js +736 -0
  6. package/bin/database/wrangler-d1-manager.js +775 -0
  7. package/bin/shared/cloudflare/domain-discovery.js +636 -0
  8. package/bin/shared/cloudflare/domain-manager.js +952 -0
  9. package/bin/shared/cloudflare/index.js +8 -0
  10. package/bin/shared/cloudflare/ops.js +359 -0
  11. package/bin/shared/config/index.js +1 -1
  12. package/bin/shared/database/connection-manager.js +374 -0
  13. package/bin/shared/database/index.js +7 -0
  14. package/bin/shared/database/orchestrator.js +726 -0
  15. package/bin/shared/deployment/auditor.js +969 -0
  16. package/bin/shared/deployment/index.js +10 -0
  17. package/bin/shared/deployment/rollback-manager.js +570 -0
  18. package/bin/shared/deployment/validator.js +779 -0
  19. package/bin/shared/index.js +32 -0
  20. package/bin/shared/monitoring/health-checker.js +484 -0
  21. package/bin/shared/monitoring/index.js +8 -0
  22. package/bin/shared/monitoring/memory-manager.js +387 -0
  23. package/bin/shared/monitoring/production-monitor.js +391 -0
  24. package/bin/shared/production-tester/api-tester.js +82 -0
  25. package/bin/shared/production-tester/auth-tester.js +132 -0
  26. package/bin/shared/production-tester/core.js +197 -0
  27. package/bin/shared/production-tester/database-tester.js +109 -0
  28. package/bin/shared/production-tester/index.js +77 -0
  29. package/bin/shared/production-tester/load-tester.js +131 -0
  30. package/bin/shared/production-tester/performance-tester.js +103 -0
  31. package/bin/shared/security/api-token-manager.js +312 -0
  32. package/bin/shared/security/index.js +8 -0
  33. package/bin/shared/security/secret-generator.js +937 -0
  34. package/bin/shared/security/secure-token-manager.js +398 -0
  35. package/bin/shared/utils/error-recovery.js +225 -0
  36. package/bin/shared/utils/graceful-shutdown-manager.js +390 -0
  37. package/bin/shared/utils/index.js +9 -0
  38. package/bin/shared/utils/interactive-prompts.js +146 -0
  39. package/bin/shared/utils/interactive-utils.js +530 -0
  40. package/bin/shared/utils/rate-limiter.js +246 -0
  41. package/dist/database/database-orchestrator.js +34 -12
  42. package/dist/deployment/index.js +2 -2
  43. package/dist/orchestration/multi-domain-orchestrator.js +26 -10
  44. package/dist/service-management/GenerationEngine.js +76 -28
  45. package/dist/service-management/ServiceInitializer.js +5 -3
  46. package/dist/shared/cloudflare/domain-manager.js +1 -1
  47. package/dist/shared/cloudflare/ops.js +27 -12
  48. package/dist/shared/config/index.js +1 -1
  49. package/dist/shared/deployment/index.js +2 -2
  50. package/dist/shared/security/secret-generator.js +4 -2
  51. package/dist/shared/utils/error-recovery.js +1 -1
  52. package/dist/shared/utils/graceful-shutdown-manager.js +4 -3
  53. package/dist/utils/deployment/secret-generator.js +19 -6
  54. package/package.json +4 -2
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Secure Token Manager
3
+ * Implements secure token storage, rotation, and access control
4
+ */
5
+
6
+ import { readFile, writeFile, access, mkdir } from 'fs/promises';
7
+ import { join } from 'path';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import crypto from 'crypto';
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ export class SecureTokenManager {
15
+ constructor(options = {}) {
16
+ this.frameworkConfig = null; // Will be loaded in initialize()
17
+ this.config = {
18
+ tokenDir: options.tokenDir, // Will be set from framework config if not provided
19
+ encryptionKey: options.encryptionKey || this.generateEncryptionKey(),
20
+ tokenRotationInterval: options.tokenRotationInterval || 24 * 60 * 60 * 1000, // 24 hours
21
+ maxTokensPerService: options.maxTokensPerService || 3,
22
+ enableAudit: options.enableAudit !== false,
23
+ auditLogPath: options.auditLogPath, // Will be set from framework config if not provided
24
+ ...options
25
+ };
26
+
27
+ this.tokens = new Map(); // service -> token data
28
+ this.auditLog = [];
29
+ }
30
+
31
+ /**
32
+ * Initialize the token manager
33
+ */
34
+ async initialize() {
35
+ try {
36
+ // Load framework configuration
37
+ const { frameworkConfig } = await import('../../../src/utils/framework-config.js');
38
+ this.frameworkConfig = frameworkConfig;
39
+
40
+ // Update paths with framework config
41
+ const configPaths = this.frameworkConfig.getPaths();
42
+
43
+ this.config.tokenDir = this.config.tokenDir || configPaths.secureTokens;
44
+ this.config.auditLogPath = this.config.auditLogPath || join(configPaths.auditLogs, 'token-audit.log');
45
+
46
+ console.log(`🔐 Token directory configured: ${this.config.tokenDir}`);
47
+ console.log(`📋 Token audit log: ${this.config.auditLogPath}`);
48
+
49
+ } catch (error) {
50
+ console.warn(`⚠️ Could not load framework config: ${error.message}. Using default paths.`);
51
+ // Use fallback defaults
52
+ this.config.tokenDir = this.config.tokenDir || '.secure-tokens';
53
+ this.config.auditLogPath = this.config.auditLogPath || 'token-audit.log';
54
+ }
55
+
56
+ await this.ensureSecureDirectory();
57
+ await this.loadTokens();
58
+ await this.rotateExpiredTokens();
59
+
60
+ if (this.config.enableAudit) {
61
+ this.logAuditEvent('TOKEN_MANAGER_INITIALIZED', { timestamp: new Date() });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Store a token securely
67
+ */
68
+ async storeToken(service, token, metadata = {}) {
69
+ const tokenData = {
70
+ service,
71
+ token: this.encrypt(token),
72
+ created: new Date(),
73
+ expires: new Date(Date.now() + this.config.tokenRotationInterval),
74
+ metadata: {
75
+ ...metadata,
76
+ permissions: metadata.permissions || ['read'],
77
+ environment: metadata.environment || 'production'
78
+ },
79
+ fingerprint: this.generateFingerprint(token)
80
+ };
81
+
82
+ // Check token limits
83
+ const serviceTokens = this.getServiceTokens(service);
84
+ if (serviceTokens.length >= this.config.maxTokensPerService) {
85
+ // Remove oldest token
86
+ const oldestToken = serviceTokens.sort((a, b) => a.created - b.created)[0];
87
+ await this.revokeToken(service, oldestToken.fingerprint);
88
+ }
89
+
90
+ this.tokens.set(`${service}_${tokenData.fingerprint}`, tokenData);
91
+ await this.saveTokens();
92
+
93
+ if (this.config.enableAudit) {
94
+ this.logAuditEvent('TOKEN_STORED', {
95
+ service,
96
+ fingerprint: tokenData.fingerprint,
97
+ permissions: tokenData.metadata.permissions
98
+ });
99
+ }
100
+
101
+ return tokenData.fingerprint;
102
+ }
103
+
104
+ /**
105
+ * Retrieve a token securely
106
+ */
107
+ async retrieveToken(service, fingerprint, requiredPermissions = []) {
108
+ const tokenKey = `${service}_${fingerprint}`;
109
+ const tokenData = this.tokens.get(tokenKey);
110
+
111
+ if (!tokenData) {
112
+ throw new Error(`Token not found for service: ${service}`);
113
+ }
114
+
115
+ // Check expiration
116
+ if (new Date() > tokenData.expires) {
117
+ await this.revokeToken(service, fingerprint);
118
+ throw new Error(`Token expired for service: ${service}`);
119
+ }
120
+
121
+ // Check permissions
122
+ if (requiredPermissions.length > 0) {
123
+ const hasPermissions = requiredPermissions.every(perm =>
124
+ tokenData.metadata.permissions.includes(perm)
125
+ );
126
+ if (!hasPermissions) {
127
+ if (this.config.enableAudit) {
128
+ this.logAuditEvent('TOKEN_ACCESS_DENIED', {
129
+ service,
130
+ fingerprint,
131
+ requiredPermissions,
132
+ tokenPermissions: tokenData.metadata.permissions
133
+ });
134
+ }
135
+ throw new Error(`Insufficient permissions for token access`);
136
+ }
137
+ }
138
+
139
+ if (this.config.enableAudit) {
140
+ this.logAuditEvent('TOKEN_RETRIEVED', {
141
+ service,
142
+ fingerprint,
143
+ permissions: tokenData.metadata.permissions
144
+ });
145
+ }
146
+
147
+ return this.decrypt(tokenData.token);
148
+ }
149
+
150
+ /**
151
+ * Rotate a token
152
+ */
153
+ async rotateToken(service, fingerprint, newToken) {
154
+ const tokenKey = `${service}_${fingerprint}`;
155
+ const tokenData = this.tokens.get(tokenKey);
156
+
157
+ if (!tokenData) {
158
+ throw new Error(`Token not found for rotation: ${service}`);
159
+ }
160
+
161
+ const newFingerprint = this.generateFingerprint(newToken);
162
+ const rotatedTokenData = {
163
+ ...tokenData,
164
+ token: this.encrypt(newToken),
165
+ created: new Date(),
166
+ expires: new Date(Date.now() + this.config.tokenRotationInterval),
167
+ fingerprint: newFingerprint,
168
+ rotatedFrom: fingerprint
169
+ };
170
+
171
+ // Remove old token
172
+ this.tokens.delete(tokenKey);
173
+
174
+ // Store new token
175
+ this.tokens.set(`${service}_${newFingerprint}`, rotatedTokenData);
176
+ await this.saveTokens();
177
+
178
+ if (this.config.enableAudit) {
179
+ this.logAuditEvent('TOKEN_ROTATED', {
180
+ service,
181
+ oldFingerprint: fingerprint,
182
+ newFingerprint: newFingerprint
183
+ });
184
+ }
185
+
186
+ return newFingerprint;
187
+ }
188
+
189
+ /**
190
+ * Revoke a token
191
+ */
192
+ async revokeToken(service, fingerprint) {
193
+ const tokenKey = `${service}_${fingerprint}`;
194
+ const tokenData = this.tokens.get(tokenKey);
195
+
196
+ if (tokenData) {
197
+ this.tokens.delete(tokenKey);
198
+ await this.saveTokens();
199
+
200
+ if (this.config.enableAudit) {
201
+ this.logAuditEvent('TOKEN_REVOKED', {
202
+ service,
203
+ fingerprint,
204
+ reason: 'manual_revoke'
205
+ });
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * List tokens for a service
212
+ */
213
+ listTokens(service) {
214
+ const serviceTokens = [];
215
+ for (const [key, tokenData] of this.tokens) {
216
+ if (key.startsWith(`${service}_`)) {
217
+ serviceTokens.push({
218
+ fingerprint: tokenData.fingerprint,
219
+ created: tokenData.created,
220
+ expires: tokenData.expires,
221
+ permissions: tokenData.metadata.permissions,
222
+ environment: tokenData.metadata.environment
223
+ });
224
+ }
225
+ }
226
+ return serviceTokens;
227
+ }
228
+
229
+ /**
230
+ * Get service tokens
231
+ */
232
+ getServiceTokens(service) {
233
+ const serviceTokens = [];
234
+ for (const [key, tokenData] of this.tokens) {
235
+ if (key.startsWith(`${service}_`)) {
236
+ serviceTokens.push(tokenData);
237
+ }
238
+ }
239
+ return serviceTokens;
240
+ }
241
+
242
+ /**
243
+ * Rotate expired tokens
244
+ */
245
+ async rotateExpiredTokens() {
246
+ const now = new Date();
247
+ const expiredTokens = [];
248
+
249
+ for (const [key, tokenData] of this.tokens) {
250
+ if (now > tokenData.expires) {
251
+ expiredTokens.push({ key, tokenData });
252
+ }
253
+ }
254
+
255
+ for (const { key, tokenData } of expiredTokens) {
256
+ this.tokens.delete(key);
257
+ if (this.config.enableAudit) {
258
+ this.logAuditEvent('TOKEN_EXPIRED', {
259
+ service: tokenData.service,
260
+ fingerprint: tokenData.fingerprint
261
+ });
262
+ }
263
+ }
264
+
265
+ if (expiredTokens.length > 0) {
266
+ await this.saveTokens();
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Encrypt token data
272
+ */
273
+ encrypt(data) {
274
+ const iv = crypto.randomBytes(16);
275
+ const cipher = crypto.createCipher('aes-256-gcm', this.config.encryptionKey);
276
+ cipher.setIV(iv);
277
+
278
+ let encrypted = cipher.update(data, 'utf8', 'hex');
279
+ encrypted += cipher.final('hex');
280
+
281
+ const authTag = cipher.getAuthTag();
282
+
283
+ return {
284
+ encrypted,
285
+ iv: iv.toString('hex'),
286
+ authTag: authTag.toString('hex')
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Decrypt token data
292
+ */
293
+ decrypt(encryptedData) {
294
+ const decipher = crypto.createDecipher('aes-256-gcm', this.config.encryptionKey);
295
+ decipher.setIV(Buffer.from(encryptedData.iv, 'hex'));
296
+ decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
297
+
298
+ let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
299
+ decrypted += decipher.final('utf8');
300
+
301
+ return decrypted;
302
+ }
303
+
304
+ /**
305
+ * Generate encryption key
306
+ */
307
+ generateEncryptionKey() {
308
+ return crypto.randomBytes(32).toString('hex');
309
+ }
310
+
311
+ /**
312
+ * Generate token fingerprint
313
+ */
314
+ generateFingerprint(token) {
315
+ return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
316
+ }
317
+
318
+ /**
319
+ * Ensure secure directory exists
320
+ */
321
+ async ensureSecureDirectory() {
322
+ try {
323
+ await access(this.config.tokenDir);
324
+ } catch {
325
+ await mkdir(this.config.tokenDir, { mode: 0o700 }); // Secure permissions
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Save tokens to disk
331
+ */
332
+ async saveTokens() {
333
+ const tokenFile = join(this.config.tokenDir, 'tokens.json');
334
+ const tokenData = {};
335
+
336
+ for (const [key, token] of this.tokens) {
337
+ tokenData[key] = token;
338
+ }
339
+
340
+ await writeFile(tokenFile, JSON.stringify(tokenData, null, 2));
341
+ }
342
+
343
+ /**
344
+ * Load tokens from disk
345
+ */
346
+ async loadTokens() {
347
+ try {
348
+ const tokenFile = join(this.config.tokenDir, 'tokens.json');
349
+ const data = await readFile(tokenFile, 'utf8');
350
+ const tokenData = JSON.parse(data);
351
+
352
+ for (const [key, token] of Object.entries(tokenData)) {
353
+ // Convert date strings back to Date objects
354
+ token.created = new Date(token.created);
355
+ token.expires = new Date(token.expires);
356
+ this.tokens.set(key, token);
357
+ }
358
+ } catch (error) {
359
+ // File doesn't exist or is corrupted, start fresh
360
+ this.tokens.clear();
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Log audit event
366
+ */
367
+ logAuditEvent(event, details) {
368
+ const auditEntry = {
369
+ timestamp: new Date(),
370
+ event,
371
+ details
372
+ };
373
+
374
+ this.auditLog.push(auditEntry);
375
+
376
+ // Keep only last 1000 entries in memory
377
+ if (this.auditLog.length > 1000) {
378
+ this.auditLog.shift();
379
+ }
380
+
381
+ // In a real implementation, you'd write to a secure audit log
382
+ console.log(`[TOKEN_AUDIT] ${event}:`, details);
383
+ }
384
+
385
+ /**
386
+ * Get audit log
387
+ */
388
+ getAuditLog(limit = 100) {
389
+ return this.auditLog.slice(-limit);
390
+ }
391
+
392
+ /**
393
+ * Validate token permissions
394
+ */
395
+ validatePermissions(tokenPermissions, requiredPermissions) {
396
+ return requiredPermissions.every(perm => tokenPermissions.includes(perm));
397
+ }
398
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Error Recovery Module
3
+ * Implements circuit breakers, retries, and graceful degradation
4
+ */
5
+
6
+ export class ErrorRecoveryManager {
7
+ constructor(options = {}) {
8
+ this.options = options;
9
+ this.config = null;
10
+ this.circuitStates = new Map(); // service -> { failures, lastFailure, state }
11
+ this.retryStates = new Map(); // operation -> retry count
12
+ }
13
+
14
+ /**
15
+ * Initialize with framework configuration
16
+ */
17
+ async initialize() {
18
+ // Import framework config for consistent timing and retry settings
19
+ const { frameworkConfig } = await import('../../../dist/utils/framework-config.js');
20
+ const timing = frameworkConfig.getTiming();
21
+
22
+ this.config = {
23
+ maxRetries: this.options.maxRetries || timing.retryAttempts,
24
+ retryDelay: this.options.retryDelay || timing.retryDelay,
25
+ circuitBreakerThreshold: this.options.circuitBreakerThreshold || timing.circuitBreakerThreshold,
26
+ circuitBreakerTimeout: this.options.circuitBreakerTimeout || timing.circuitBreakerTimeout,
27
+ gracefulDegradation: this.options.gracefulDegradation !== false,
28
+ ...this.options
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Execute operation with error recovery
34
+ */
35
+ async executeWithRecovery(operation, options = {}) {
36
+ const config = { ...this.config, ...options };
37
+ const operationId = this.getOperationId(operation);
38
+
39
+ // Check circuit breaker
40
+ if (this.isCircuitOpen(operationId)) {
41
+ if (config.gracefulDegradation) {
42
+ return this.executeGracefulFallback(operation, config);
43
+ }
44
+ throw new Error(`Circuit breaker open for operation: ${operationId}`);
45
+ }
46
+
47
+ let lastError;
48
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
49
+ try {
50
+ const result = await operation();
51
+ this.recordSuccess(operationId);
52
+ return result;
53
+ } catch (error) {
54
+ lastError = error;
55
+ this.recordFailure(operationId, error);
56
+
57
+ if (attempt < config.maxRetries) {
58
+ const delay = this.calculateRetryDelay(attempt, config.retryDelay);
59
+ await this.delay(delay);
60
+ }
61
+ }
62
+ }
63
+
64
+ // All retries exhausted
65
+ if (config.gracefulDegradation) {
66
+ return this.executeGracefulFallback(operation, config);
67
+ }
68
+
69
+ throw lastError;
70
+ }
71
+
72
+ /**
73
+ * Check if circuit breaker is open
74
+ */
75
+ isCircuitOpen(operationId) {
76
+ const state = this.circuitStates.get(operationId);
77
+ if (!state) return false;
78
+
79
+ if (state.state === 'open') {
80
+ // Check if timeout has passed
81
+ if (Date.now() - state.lastFailure > this.config.circuitBreakerTimeout) {
82
+ state.state = 'half-open';
83
+ state.failures = 0;
84
+ return false;
85
+ }
86
+ return true;
87
+ }
88
+
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * Record operation success
94
+ */
95
+ recordSuccess(operationId) {
96
+ const state = this.circuitStates.get(operationId);
97
+ if (state) {
98
+ if (state.state === 'half-open') {
99
+ state.state = 'closed';
100
+ state.failures = 0;
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Record operation failure
107
+ */
108
+ recordFailure(operationId, error) {
109
+ let state = this.circuitStates.get(operationId);
110
+ if (!state) {
111
+ state = { failures: 0, lastFailure: 0, state: 'closed' };
112
+ this.circuitStates.set(operationId, state);
113
+ }
114
+
115
+ state.failures++;
116
+ state.lastFailure = Date.now();
117
+
118
+ if (state.failures >= this.config.circuitBreakerThreshold) {
119
+ state.state = 'open';
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Calculate retry delay with exponential backoff
125
+ */
126
+ calculateRetryDelay(attempt, baseDelay) {
127
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
128
+ const jitter = Math.random() * 0.1 * exponentialDelay;
129
+ return Math.min(exponentialDelay + jitter, 30000); // Max 30 seconds
130
+ }
131
+
132
+ /**
133
+ * Execute graceful fallback
134
+ */
135
+ async executeGracefulFallback(operation, config) {
136
+ console.warn(`Executing graceful fallback for operation`);
137
+
138
+ // Try to execute with reduced functionality
139
+ try {
140
+ // For deployment operations, try a simplified version
141
+ if (operation.name && operation.name.includes('deploy')) {
142
+ return { success: false, degraded: true, message: 'Operation executed in degraded mode' };
143
+ }
144
+
145
+ // For data operations, return cached or default data
146
+ if (operation.name && operation.name.includes('fetch')) {
147
+ return { data: [], cached: true, degraded: true };
148
+ }
149
+
150
+ // Default fallback
151
+ return { success: false, degraded: true, fallback: true };
152
+
153
+ } catch (fallbackError) {
154
+ console.error('Graceful fallback also failed:', fallbackError);
155
+ throw fallbackError;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Get unique operation ID
161
+ */
162
+ getOperationId(operation) {
163
+ if (typeof operation === 'function' && operation.name) {
164
+ return operation.name;
165
+ }
166
+ return `operation_${Date.now()}_${Math.random()}`;
167
+ }
168
+
169
+ /**
170
+ * Utility delay function
171
+ */
172
+ delay(ms) {
173
+ return new Promise(resolve => setTimeout(resolve, ms));
174
+ }
175
+
176
+ /**
177
+ * Get circuit breaker status
178
+ */
179
+ getCircuitStatus(operationId) {
180
+ const state = this.circuitStates.get(operationId);
181
+ if (!state) {
182
+ return { state: 'closed', failures: 0 };
183
+ }
184
+ return {
185
+ state: state.state,
186
+ failures: state.failures,
187
+ lastFailure: state.lastFailure,
188
+ timeSinceLastFailure: Date.now() - state.lastFailure
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Reset circuit breaker
194
+ */
195
+ resetCircuit(operationId) {
196
+ this.circuitStates.delete(operationId);
197
+ }
198
+
199
+ /**
200
+ * Get all circuit statuses
201
+ */
202
+ getAllCircuitStatuses() {
203
+ const statuses = {};
204
+ for (const [operationId, state] of this.circuitStates) {
205
+ statuses[operationId] = this.getCircuitStatus(operationId);
206
+ }
207
+ return statuses;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Retry wrapper for functions
213
+ */
214
+ export function withRetry(fn, options = {}) {
215
+ const recovery = new ErrorRecoveryManager(options);
216
+ return (...args) => recovery.executeWithRecovery(() => fn(...args), options);
217
+ }
218
+
219
+ /**
220
+ * Circuit breaker wrapper for functions
221
+ */
222
+ export function withCircuitBreaker(fn, options = {}) {
223
+ const recovery = new ErrorRecoveryManager(options);
224
+ return (...args) => recovery.executeWithRecovery(() => fn(...args), options);
225
+ }