claude-autopm 1.31.0 → 2.1.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.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Circuit Breaker Implementation
3
+ *
4
+ * Implements the circuit breaker pattern to prevent cascading failures
5
+ * and allow systems to recover from transient faults.
6
+ *
7
+ * States:
8
+ * - CLOSED: Normal operation, requests pass through
9
+ * - OPEN: Too many failures, reject requests immediately
10
+ * - HALF_OPEN: Testing recovery, allow limited requests
11
+ *
12
+ * State Transitions:
13
+ * CLOSED → OPEN: After failureThreshold consecutive failures
14
+ * OPEN → HALF_OPEN: After timeout expires
15
+ * HALF_OPEN → CLOSED: After successThreshold consecutive successes
16
+ * HALF_OPEN → OPEN: On any failure
17
+ *
18
+ * @example
19
+ * const breaker = new CircuitBreaker({
20
+ * failureThreshold: 5, // Open after 5 consecutive failures
21
+ * successThreshold: 2, // Close after 2 consecutive successes
22
+ * timeout: 60000, // Wait 60s before trying again
23
+ * halfOpenTimeout: 30000 // (unused, for future extensions)
24
+ * });
25
+ *
26
+ * try {
27
+ * const result = await breaker.execute(async () => {
28
+ * return await apiCall();
29
+ * });
30
+ * } catch (error) {
31
+ * if (error.message === 'Circuit breaker is OPEN') {
32
+ * // Circuit is open, service unavailable
33
+ * }
34
+ * }
35
+ */
36
+
37
+ /**
38
+ * Circuit breaker states
39
+ * @enum {string}
40
+ */
41
+ const States = {
42
+ CLOSED: 'CLOSED', // Normal operation
43
+ OPEN: 'OPEN', // Failing, reject immediately
44
+ HALF_OPEN: 'HALF_OPEN' // Testing if recovered
45
+ };
46
+
47
+ /**
48
+ * Circuit Breaker class
49
+ */
50
+ class CircuitBreaker {
51
+ /**
52
+ * Create a circuit breaker
53
+ *
54
+ * @param {Object} [options={}] - Configuration options
55
+ * @param {number} [options.failureThreshold=5] - Number of consecutive failures before opening
56
+ * @param {number} [options.successThreshold=2] - Number of consecutive successes to close from half-open
57
+ * @param {number} [options.timeout=60000] - Time in ms to wait before transitioning from OPEN to HALF_OPEN
58
+ * @param {number} [options.halfOpenTimeout=30000] - Reserved for future use
59
+ */
60
+ constructor(options = {}) {
61
+ // Configuration with defaults
62
+ this.failureThreshold = options.failureThreshold || 5;
63
+ this.successThreshold = options.successThreshold || 2;
64
+ this.timeout = options.timeout !== undefined ? options.timeout : 60000;
65
+ this.halfOpenTimeout = options.halfOpenTimeout || 30000;
66
+
67
+ // State tracking
68
+ this.state = States.CLOSED;
69
+ this.failureCount = 0;
70
+ this.successCount = 0;
71
+ this.nextAttempt = Date.now(); // Time when we can retry after OPEN
72
+ }
73
+
74
+ /**
75
+ * Execute a function with circuit breaker protection
76
+ *
77
+ * @param {Function} fn - Async function to execute
78
+ * @returns {Promise<*>} Result of the function
79
+ * @throws {Error} Circuit breaker error or function error
80
+ */
81
+ async execute(fn) {
82
+ // Check if circuit is OPEN
83
+ if (this.state === States.OPEN) {
84
+ if (Date.now() < this.nextAttempt) {
85
+ // Still within timeout period, reject immediately
86
+ throw new Error('Circuit breaker is OPEN');
87
+ }
88
+ // Timeout expired, transition to HALF_OPEN
89
+ this.state = States.HALF_OPEN;
90
+ }
91
+
92
+ // Execute the function
93
+ try {
94
+ const result = await fn();
95
+ this._onSuccess();
96
+ return result;
97
+ } catch (error) {
98
+ this._onFailure();
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Handle successful execution
105
+ * @private
106
+ */
107
+ _onSuccess() {
108
+ // Reset failure count on any success
109
+ this.failureCount = 0;
110
+
111
+ if (this.state === States.HALF_OPEN) {
112
+ // In HALF_OPEN state, count successes
113
+ this.successCount++;
114
+
115
+ // If we've reached the success threshold, close the circuit
116
+ if (this.successCount >= this.successThreshold) {
117
+ this.state = States.CLOSED;
118
+ this.successCount = 0;
119
+ }
120
+ }
121
+ // In CLOSED state, success count is not tracked (stays 0)
122
+ }
123
+
124
+ /**
125
+ * Handle failed execution
126
+ * @private
127
+ */
128
+ _onFailure() {
129
+ // Reset success count on any failure
130
+ this.successCount = 0;
131
+
132
+ // Increment failure count (only in CLOSED or HALF_OPEN)
133
+ if (this.state !== States.OPEN) {
134
+ this.failureCount++;
135
+ }
136
+
137
+ // Check if we should open the circuit
138
+ if (this.failureCount >= this.failureThreshold) {
139
+ this.state = States.OPEN;
140
+ this.nextAttempt = Date.now() + this.timeout;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Reset circuit breaker to initial state
146
+ * Useful for manual recovery or testing
147
+ */
148
+ reset() {
149
+ this.state = States.CLOSED;
150
+ this.failureCount = 0;
151
+ this.successCount = 0;
152
+ this.nextAttempt = Date.now();
153
+ }
154
+
155
+ /**
156
+ * Get current circuit breaker state
157
+ *
158
+ * @returns {string} Current state (CLOSED, OPEN, or HALF_OPEN)
159
+ */
160
+ getState() {
161
+ return this.state;
162
+ }
163
+ }
164
+
165
+ module.exports = { CircuitBreaker, States };
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @fileoverview Encryption utility for secure API key storage
3
+ * Implements AES-256-CBC encryption with PBKDF2 key derivation
4
+ *
5
+ * Based on Context7 documentation patterns:
6
+ * - node-config: Hierarchical configuration patterns
7
+ * - Node.js crypto: AES-256-CBC, PBKDF2, scrypt for encryption
8
+ *
9
+ * @example
10
+ * const encryption = new Encryption();
11
+ * const encrypted = encryption.encrypt('my-api-key', 'password');
12
+ * const decrypted = encryption.decrypt(encrypted, 'password');
13
+ */
14
+
15
+ const crypto = require('crypto');
16
+
17
+ /**
18
+ * Encryption utility class for API keys and sensitive data
19
+ */
20
+ class Encryption {
21
+ constructor() {
22
+ // Algorithm configuration
23
+ this.algorithm = 'aes-256-cbc';
24
+ this.keyLength = 32; // 256 bits
25
+ this.ivLength = 16; // 128 bits
26
+ this.saltLength = 32; // 256 bits
27
+ this.iterations = 100000; // PBKDF2 iterations
28
+ this.digest = 'sha512';
29
+ }
30
+
31
+ /**
32
+ * Generate cryptographically secure random salt
33
+ *
34
+ * @returns {Buffer} Random salt buffer (32 bytes)
35
+ */
36
+ generateSalt() {
37
+ return crypto.randomBytes(this.saltLength);
38
+ }
39
+
40
+ /**
41
+ * Generate cryptographically secure random IV
42
+ *
43
+ * @returns {Buffer} Random IV buffer (16 bytes)
44
+ */
45
+ generateIV() {
46
+ return crypto.randomBytes(this.ivLength);
47
+ }
48
+
49
+ /**
50
+ * Derive encryption key from password using PBKDF2
51
+ *
52
+ * @param {string} password - Master password
53
+ * @param {Buffer} salt - Salt for key derivation
54
+ * @returns {Buffer} Derived key (32 bytes)
55
+ * @throws {Error} If password or salt is invalid
56
+ */
57
+ deriveKey(password, salt) {
58
+ // Validate inputs
59
+ if (!password || typeof password !== 'string' || password.length === 0) {
60
+ throw new Error('Password is required');
61
+ }
62
+
63
+ if (!Buffer.isBuffer(salt)) {
64
+ throw new Error('Salt must be a Buffer');
65
+ }
66
+
67
+ // Derive key using PBKDF2
68
+ return crypto.pbkdf2Sync(
69
+ password,
70
+ salt,
71
+ this.iterations,
72
+ this.keyLength,
73
+ this.digest
74
+ );
75
+ }
76
+
77
+ /**
78
+ * Encrypt plaintext using AES-256-CBC
79
+ *
80
+ * @param {string} plaintext - Data to encrypt
81
+ * @param {string} password - Master password
82
+ * @returns {Object} Encrypted data object
83
+ * @returns {string} returns.encrypted - Base64-encoded encrypted data
84
+ * @returns {string} returns.iv - Base64-encoded IV
85
+ * @returns {string} returns.salt - Base64-encoded salt
86
+ * @returns {string} returns.algorithm - Encryption algorithm used
87
+ * @throws {Error} If plaintext or password is invalid
88
+ *
89
+ * @example
90
+ * const result = encryption.encrypt('my-api-key', 'password');
91
+ * // {
92
+ * // encrypted: 'base64string',
93
+ * // iv: 'base64string',
94
+ * // salt: 'base64string',
95
+ * // algorithm: 'aes-256-cbc'
96
+ * // }
97
+ */
98
+ encrypt(plaintext, password) {
99
+ // Validate inputs
100
+ if (plaintext === null || plaintext === undefined) {
101
+ throw new Error('Plaintext is required');
102
+ }
103
+
104
+ if (!password || typeof password !== 'string' || password.length === 0) {
105
+ throw new Error('Password is required');
106
+ }
107
+
108
+ // Convert plaintext to string if needed
109
+ const plaintextStr = String(plaintext);
110
+
111
+ // Generate random salt and IV
112
+ const salt = this.generateSalt();
113
+ const iv = this.generateIV();
114
+
115
+ // Derive key from password
116
+ const key = this.deriveKey(password, salt);
117
+
118
+ // Create cipher
119
+ const cipher = crypto.createCipheriv(this.algorithm, key, iv);
120
+
121
+ // Encrypt data
122
+ let encrypted = cipher.update(plaintextStr, 'utf8', 'base64');
123
+ encrypted += cipher.final('base64');
124
+
125
+ // Return encrypted data object
126
+ return {
127
+ encrypted,
128
+ iv: iv.toString('base64'),
129
+ salt: salt.toString('base64'),
130
+ algorithm: this.algorithm
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Decrypt encrypted data using AES-256-CBC
136
+ *
137
+ * @param {Object} encryptedData - Encrypted data object from encrypt()
138
+ * @param {string} encryptedData.encrypted - Base64-encoded encrypted data
139
+ * @param {string} encryptedData.iv - Base64-encoded IV
140
+ * @param {string} encryptedData.salt - Base64-encoded salt
141
+ * @param {string} encryptedData.algorithm - Encryption algorithm
142
+ * @param {string} password - Master password
143
+ * @returns {string} Decrypted plaintext
144
+ * @throws {Error} If data is invalid or password is wrong
145
+ *
146
+ * @example
147
+ * const decrypted = encryption.decrypt(encryptedData, 'password');
148
+ */
149
+ decrypt(encryptedData, password) {
150
+ // Validate inputs
151
+ if (!encryptedData || typeof encryptedData !== 'object') {
152
+ throw new Error('Encrypted data must be an object');
153
+ }
154
+
155
+ if (!encryptedData.encrypted) {
156
+ throw new Error('Encrypted data is required');
157
+ }
158
+
159
+ if (!encryptedData.iv) {
160
+ throw new Error('IV is required');
161
+ }
162
+
163
+ if (!encryptedData.salt) {
164
+ throw new Error('Salt is required');
165
+ }
166
+
167
+ if (!encryptedData.algorithm || encryptedData.algorithm !== this.algorithm) {
168
+ throw new Error('Invalid algorithm');
169
+ }
170
+
171
+ if (!password || typeof password !== 'string' || password.length === 0) {
172
+ throw new Error('Password is required');
173
+ }
174
+
175
+ try {
176
+ // Parse base64-encoded values
177
+ const salt = Buffer.from(encryptedData.salt, 'base64');
178
+ const iv = Buffer.from(encryptedData.iv, 'base64');
179
+
180
+ // Derive key from password
181
+ const key = this.deriveKey(password, salt);
182
+
183
+ // Create decipher
184
+ const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
185
+
186
+ // Decrypt data
187
+ let decrypted = decipher.update(encryptedData.encrypted, 'base64', 'utf8');
188
+ decrypted += decipher.final('utf8');
189
+
190
+ return decrypted;
191
+ } catch (error) {
192
+ // Don't leak password in error messages
193
+ if (error.message.includes('bad decrypt')) {
194
+ throw new Error('Decryption failed - wrong password or corrupted data');
195
+ }
196
+ throw error;
197
+ }
198
+ }
199
+ }
200
+
201
+ module.exports = Encryption;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * RateLimiter
3
+ *
4
+ * Token Bucket Algorithm implementation for rate limiting.
5
+ *
6
+ * Features:
7
+ * - Token bucket with configurable capacity
8
+ * - Multiple time windows (second, minute, hour, day)
9
+ * - Burst support (bucket size > rate)
10
+ * - Async wait or immediate failure modes
11
+ * - In-memory storage (no dependencies)
12
+ * - High precision (millisecond accuracy)
13
+ *
14
+ * Usage:
15
+ * const limiter = new RateLimiter({
16
+ * tokensPerInterval: 60,
17
+ * interval: 'minute',
18
+ * bucketSize: 100 // Allow burst
19
+ * });
20
+ *
21
+ * // Async - waits if needed
22
+ * await limiter.removeTokens(1);
23
+ *
24
+ * // Sync - immediate response
25
+ * if (limiter.tryRemoveTokens(1)) {
26
+ * // proceed
27
+ * }
28
+ *
29
+ * // fireImmediately mode for HTTP 429
30
+ * const limiter2 = new RateLimiter({
31
+ * tokensPerInterval: 100,
32
+ * interval: 'hour',
33
+ * fireImmediately: true
34
+ * });
35
+ *
36
+ * const remaining = await limiter2.removeTokens(1);
37
+ * if (remaining < 0) {
38
+ * res.status(429).send('Rate limit exceeded');
39
+ * }
40
+ */
41
+
42
+ class RateLimiter {
43
+ /**
44
+ * Create a new RateLimiter
45
+ *
46
+ * @param {Object} options - Configuration options
47
+ * @param {number} options.tokensPerInterval - Number of tokens to add per interval (default: 60)
48
+ * @param {string|number} options.interval - Time interval ('second', 'minute', 'hour', 'day') or milliseconds (default: 'minute')
49
+ * @param {number} options.bucketSize - Maximum tokens in bucket (default: tokensPerInterval)
50
+ * @param {boolean} options.fireImmediately - Don't wait, return negative on exceeded (default: false)
51
+ */
52
+ constructor(options = {}) {
53
+ // Validate and set tokensPerInterval
54
+ this.tokensPerInterval = options.tokensPerInterval !== undefined
55
+ ? options.tokensPerInterval
56
+ : 60;
57
+
58
+ if (typeof this.tokensPerInterval !== 'number' || this.tokensPerInterval <= 0) {
59
+ throw new Error('tokensPerInterval must be greater than 0');
60
+ }
61
+
62
+ // Parse and validate interval
63
+ this.interval = this._parseInterval(
64
+ options.interval !== undefined ? options.interval : 'minute'
65
+ );
66
+
67
+ if (typeof this.interval !== 'number' || this.interval <= 0) {
68
+ throw new Error('interval must be greater than 0');
69
+ }
70
+
71
+ // Validate and set bucket size
72
+ this.bucketSize = options.bucketSize !== undefined
73
+ ? options.bucketSize
74
+ : this.tokensPerInterval;
75
+
76
+ if (typeof this.bucketSize !== 'number' || this.bucketSize <= 0) {
77
+ throw new Error('bucketSize must be greater than 0');
78
+ }
79
+
80
+ // Set fireImmediately mode
81
+ this.fireImmediately = options.fireImmediately === true;
82
+
83
+ // Initialize token bucket
84
+ this.tokens = this.bucketSize;
85
+ this.lastRefill = Date.now();
86
+ }
87
+
88
+ /**
89
+ * Remove tokens from the bucket (async)
90
+ *
91
+ * Waits if insufficient tokens (unless fireImmediately=true)
92
+ *
93
+ * @param {number} count - Number of tokens to remove (default: 1)
94
+ * @returns {Promise<number>} Remaining tokens after removal
95
+ * @throws {Error} If count exceeds bucket size
96
+ */
97
+ async removeTokens(count = 1) {
98
+ // fireImmediately mode: allow any count, can go negative
99
+ if (!this.fireImmediately && count > this.bucketSize) {
100
+ throw new Error(`Requested ${count} tokens exceeds bucket size ${this.bucketSize}`);
101
+ }
102
+
103
+ // Refill tokens based on time passed
104
+ this._refill();
105
+
106
+ // fireImmediately mode: return immediately, even if negative
107
+ if (this.fireImmediately) {
108
+ this.tokens -= count;
109
+ return this.tokens;
110
+ }
111
+
112
+ // Wait mode: wait until sufficient tokens available
113
+ while (this.tokens < count) {
114
+ const tokensNeeded = count - this.tokens;
115
+ const timeToWait = (tokensNeeded / this.tokensPerInterval) * this.interval;
116
+
117
+ await this._delay(timeToWait);
118
+ this._refill();
119
+ }
120
+
121
+ this.tokens -= count;
122
+ return this.tokens;
123
+ }
124
+
125
+ /**
126
+ * Try to remove tokens (sync, non-blocking)
127
+ *
128
+ * Returns immediately without waiting
129
+ *
130
+ * @param {number} count - Number of tokens to remove (default: 1)
131
+ * @returns {boolean} True if tokens were removed, false if insufficient
132
+ */
133
+ tryRemoveTokens(count = 1) {
134
+ this._refill();
135
+
136
+ if (this.tokens >= count) {
137
+ this.tokens -= count;
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * Get current number of tokens available
146
+ *
147
+ * @returns {number} Available tokens
148
+ */
149
+ getTokensRemaining() {
150
+ return this.tokens;
151
+ }
152
+
153
+ /**
154
+ * Reset the rate limiter to initial state
155
+ *
156
+ * Refills bucket to capacity and resets timer
157
+ */
158
+ reset() {
159
+ this.tokens = this.bucketSize;
160
+ this.lastRefill = Date.now();
161
+ }
162
+
163
+ /**
164
+ * Refill tokens based on time passed
165
+ *
166
+ * Uses token bucket algorithm:
167
+ * - Calculate time since last refill
168
+ * - Add tokens proportional to time passed
169
+ * - Cap at bucket size
170
+ *
171
+ * @private
172
+ */
173
+ _refill() {
174
+ const now = Date.now();
175
+ const timePassed = now - this.lastRefill;
176
+
177
+ if (timePassed <= 0) {
178
+ return; // No time passed, no refill
179
+ }
180
+
181
+ // Calculate tokens to add based on time passed
182
+ const tokensToAdd = (timePassed / this.interval) * this.tokensPerInterval;
183
+
184
+ // Add tokens up to bucket size
185
+ this.tokens = Math.min(this.bucketSize, this.tokens + tokensToAdd);
186
+
187
+ // Update last refill time
188
+ this.lastRefill = now;
189
+ }
190
+
191
+ /**
192
+ * Parse interval string to milliseconds
193
+ *
194
+ * Supports: 'second', 'minute', 'hour', 'day' (case insensitive, with/without 's')
195
+ * Also accepts numeric milliseconds
196
+ *
197
+ * @param {string|number} interval - Interval to parse
198
+ * @returns {number} Interval in milliseconds
199
+ * @throws {Error} If interval is invalid
200
+ * @private
201
+ */
202
+ _parseInterval(interval) {
203
+ // If already a number, return as-is
204
+ if (typeof interval === 'number') {
205
+ return interval;
206
+ }
207
+
208
+ // String parsing (case insensitive)
209
+ if (typeof interval === 'string') {
210
+ const normalized = interval.toLowerCase().replace(/s$/, ''); // Remove trailing 's'
211
+
212
+ switch (normalized) {
213
+ case 'second':
214
+ return 1000;
215
+ case 'minute':
216
+ return 60000;
217
+ case 'hour':
218
+ return 3600000;
219
+ case 'day':
220
+ return 86400000;
221
+ default:
222
+ throw new Error(`Invalid interval: ${interval}`);
223
+ }
224
+ }
225
+
226
+ throw new Error(`Invalid interval: ${interval}`);
227
+ }
228
+
229
+ /**
230
+ * Delay execution for specified milliseconds
231
+ *
232
+ * @param {number} ms - Milliseconds to delay
233
+ * @returns {Promise<void>}
234
+ * @private
235
+ */
236
+ _delay(ms) {
237
+ return new Promise(resolve => setTimeout(resolve, ms));
238
+ }
239
+ }
240
+
241
+ module.exports = RateLimiter;