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