@sanskari27/aws-rate-limiter 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 +8 -0
- package/LICENSE +21 -0
- package/README.md +1027 -0
- package/dist/adapters/express.d.ts +122 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +190 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fastify.d.ts +112 -0
- package/dist/adapters/fastify.d.ts.map +1 -0
- package/dist/adapters/fastify.js +178 -0
- package/dist/adapters/fastify.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +22 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/lambda/decorator.d.ts +120 -0
- package/dist/adapters/lambda/decorator.d.ts.map +1 -0
- package/dist/adapters/lambda/decorator.js +281 -0
- package/dist/adapters/lambda/decorator.js.map +1 -0
- package/dist/adapters/lambda/extension.d.ts +178 -0
- package/dist/adapters/lambda/extension.d.ts.map +1 -0
- package/dist/adapters/lambda/extension.js +445 -0
- package/dist/adapters/lambda/extension.js.map +1 -0
- package/dist/adapters/lambda/index.d.ts +9 -0
- package/dist/adapters/lambda/index.d.ts.map +1 -0
- package/dist/adapters/lambda/index.js +16 -0
- package/dist/adapters/lambda/index.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +11 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +68 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +280 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/ssm-watcher.d.ts +103 -0
- package/dist/config/ssm-watcher.d.ts.map +1 -0
- package/dist/config/ssm-watcher.js +264 -0
- package/dist/config/ssm-watcher.js.map +1 -0
- package/dist/core/algorithm.d.ts +98 -0
- package/dist/core/algorithm.d.ts.map +1 -0
- package/dist/core/algorithm.js +127 -0
- package/dist/core/algorithm.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +24 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/key-builder.d.ts +103 -0
- package/dist/core/key-builder.d.ts.map +1 -0
- package/dist/core/key-builder.js +232 -0
- package/dist/core/key-builder.js.map +1 -0
- package/dist/core/types.d.ts +253 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +72 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/index.d.ts +5 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +12 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/logger.d.ts +136 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +167 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +129 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +137 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/rate-limiter.d.ts +171 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +702 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/redis/circuit-breaker.d.ts +84 -0
- package/dist/redis/circuit-breaker.d.ts.map +1 -0
- package/dist/redis/circuit-breaker.js +131 -0
- package/dist/redis/circuit-breaker.js.map +1 -0
- package/dist/redis/client.d.ts +98 -0
- package/dist/redis/client.d.ts.map +1 -0
- package/dist/redis/client.js +223 -0
- package/dist/redis/client.js.map +1 -0
- package/dist/redis/index.d.ts +8 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +16 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/script-loader.d.ts +111 -0
- package/dist/redis/script-loader.d.ts.map +1 -0
- package/dist/redis/script-loader.js +204 -0
- package/dist/redis/script-loader.js.map +1 -0
- package/dist/reservoir/index.d.ts +6 -0
- package/dist/reservoir/index.d.ts.map +1 -0
- package/dist/reservoir/index.js +9 -0
- package/dist/reservoir/index.js.map +1 -0
- package/dist/reservoir/local-reservoir.d.ts +98 -0
- package/dist/reservoir/local-reservoir.d.ts.map +1 -0
- package/dist/reservoir/local-reservoir.js +148 -0
- package/dist/reservoir/local-reservoir.js.map +1 -0
- package/package.json +101 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview YAML + environment variable config loader for the AWS Rate Limiter.
|
|
4
|
+
*
|
|
5
|
+
* Provides three loading strategies:
|
|
6
|
+
* 1. {@link loadConfigFromFile} — parse a YAML file with ${ENV_VAR} substitution.
|
|
7
|
+
* 2. {@link loadConfigFromEnv} — build a minimal config from RATE_LIMITER_* env vars.
|
|
8
|
+
* 3. {@link loadConfig} — try file first (or RATE_LIMITER_CONFIG), fall back to env.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.validateConfig = validateConfig;
|
|
45
|
+
exports.loadConfigFromFile = loadConfigFromFile;
|
|
46
|
+
exports.loadConfigFromEnv = loadConfigFromEnv;
|
|
47
|
+
exports.loadConfig = loadConfig;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const yaml = __importStar(require("js-yaml"));
|
|
51
|
+
const types_1 = require("../core/types");
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Env-var substitution
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/**
|
|
56
|
+
* Replace all `${VAR_NAME}` placeholders in `content` with the corresponding
|
|
57
|
+
* `process.env` value. Missing variables are replaced with an empty string.
|
|
58
|
+
*
|
|
59
|
+
* @param content Raw YAML string potentially containing `${...}` placeholders.
|
|
60
|
+
* @returns String with all placeholders resolved.
|
|
61
|
+
*/
|
|
62
|
+
function substituteEnvVars(content) {
|
|
63
|
+
return content.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] ?? '');
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Validation
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/**
|
|
69
|
+
* Validate a config object. Throws {@link ConfigurationError} with a helpful
|
|
70
|
+
* message if any required field is absent or malformed.
|
|
71
|
+
*
|
|
72
|
+
* This is an *asserting* type-guard: after a successful call, TypeScript knows
|
|
73
|
+
* the value satisfies `RateLimiterConfig`.
|
|
74
|
+
*
|
|
75
|
+
* @param config The unknown value to validate.
|
|
76
|
+
* @throws {ConfigurationError} If the config is missing required fields.
|
|
77
|
+
*/
|
|
78
|
+
function validateConfig(config) {
|
|
79
|
+
if (config === null || typeof config !== 'object') {
|
|
80
|
+
throw new types_1.ConfigurationError('Config must be an object');
|
|
81
|
+
}
|
|
82
|
+
const obj = config;
|
|
83
|
+
// -- redis ------------------------------------------------------------------
|
|
84
|
+
if (!('redis' in obj) || obj['redis'] === null || typeof obj['redis'] !== 'object') {
|
|
85
|
+
throw new types_1.ConfigurationError('Config must contain a "redis" object with connection details (e.g. { url: "redis://..." })');
|
|
86
|
+
}
|
|
87
|
+
// -- rules ------------------------------------------------------------------
|
|
88
|
+
if (!('rules' in obj)) {
|
|
89
|
+
throw new types_1.ConfigurationError('Config must contain a "rules" array. Provide an empty array [] if no rules are needed.');
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(obj['rules'])) {
|
|
92
|
+
throw new types_1.ConfigurationError('"rules" must be an array');
|
|
93
|
+
}
|
|
94
|
+
const rules = obj['rules'];
|
|
95
|
+
for (let i = 0; i < rules.length; i++) {
|
|
96
|
+
const rule = rules[i];
|
|
97
|
+
if (rule === null || typeof rule !== 'object') {
|
|
98
|
+
throw new types_1.ConfigurationError(`rules[${i}] must be an object`);
|
|
99
|
+
}
|
|
100
|
+
const r = rule;
|
|
101
|
+
if (typeof r['name'] !== 'string' || r['name'].trim() === '') {
|
|
102
|
+
throw new types_1.ConfigurationError(`rules[${i}] must have a non-empty "name" string`);
|
|
103
|
+
}
|
|
104
|
+
if (!('limits' in r) || r['limits'] === null || typeof r['limits'] !== 'object') {
|
|
105
|
+
throw new types_1.ConfigurationError(`rules[${i}] ("${r['name']}") must have a "limits" object`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Validate redis has at least url or cluster
|
|
109
|
+
const redis = obj['redis'];
|
|
110
|
+
if (!redis['url'] && !redis['cluster']) {
|
|
111
|
+
throw new types_1.ConfigurationError('Config "redis" must contain at least "url" or "cluster" connection details');
|
|
112
|
+
}
|
|
113
|
+
// Validate each rule's limit specs have valid values
|
|
114
|
+
for (let i = 0; i < rules.length; i++) {
|
|
115
|
+
const r = rules[i];
|
|
116
|
+
const limits = r['limits'];
|
|
117
|
+
for (const dim of ['ip', 'route', 'user', 'userRoute']) {
|
|
118
|
+
const spec = limits[dim];
|
|
119
|
+
if (spec !== undefined && spec !== null) {
|
|
120
|
+
const s = spec;
|
|
121
|
+
if (typeof s['limit'] !== 'number' || s['limit'] <= 0) {
|
|
122
|
+
throw new types_1.ConfigurationError(`rules[${i}].limits.${dim}.limit must be a positive number`);
|
|
123
|
+
}
|
|
124
|
+
if (typeof s['window'] !== 'number' || s['window'] <= 0) {
|
|
125
|
+
throw new types_1.ConfigurationError(`rules[${i}].limits.${dim}.window must be a positive number`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// loadConfigFromFile
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* Load rate limiter configuration from a YAML file.
|
|
136
|
+
*
|
|
137
|
+
* Performs `${ENV_VAR}` substitution before parsing so that secrets can be
|
|
138
|
+
* injected via environment variables without being stored in the YAML file.
|
|
139
|
+
*
|
|
140
|
+
* @param filePath Absolute or relative path to the YAML configuration file.
|
|
141
|
+
* @returns Parsed and validated {@link RateLimiterConfig}.
|
|
142
|
+
* @throws {ConfigurationError} If the file does not exist, cannot be read, or
|
|
143
|
+
* contains invalid YAML / missing required fields.
|
|
144
|
+
*/
|
|
145
|
+
function loadConfigFromFile(filePath) {
|
|
146
|
+
const resolved = path.resolve(filePath);
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
raw = fs.readFileSync(resolved, 'utf-8');
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
+
throw new types_1.ConfigurationError(`Cannot read config file "${resolved}": ${msg}`);
|
|
154
|
+
}
|
|
155
|
+
const substituted = substituteEnvVars(raw);
|
|
156
|
+
let parsed;
|
|
157
|
+
try {
|
|
158
|
+
parsed = yaml.load(substituted);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
162
|
+
throw new types_1.ConfigurationError(`Invalid YAML in config file "${resolved}": ${msg}`);
|
|
163
|
+
}
|
|
164
|
+
validateConfig(parsed);
|
|
165
|
+
return parsed;
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// loadConfigFromEnv
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Build a {@link RateLimiterConfig} entirely from `RATE_LIMITER_*` environment
|
|
172
|
+
* variables. Sensible defaults are applied when variables are absent.
|
|
173
|
+
*
|
|
174
|
+
* | Environment variable | Config path | Default |
|
|
175
|
+
* |------------------------------------|-------------------------------------|-----------|
|
|
176
|
+
* | `RATE_LIMITER_REDIS_URL` | `redis.url` | (none) |
|
|
177
|
+
* | `RATE_LIMITER_REDIS_AUTH` | `redis.password` | (none) |
|
|
178
|
+
* | `RATE_LIMITER_DEFAULT_LIMIT` | default rule ip.limit | `60` |
|
|
179
|
+
* | `RATE_LIMITER_DEFAULT_WINDOW` | default rule ip.window (seconds) | `60` |
|
|
180
|
+
* | `RATE_LIMITER_FAILURE_POLICY` | `failure.default` | `open` |
|
|
181
|
+
* | `RATE_LIMITER_RESERVOIR_BATCH_SIZE`| `reservoir.batchSize` | `10` |
|
|
182
|
+
* | `RATE_LIMITER_RESERVOIR_SYNC_INTERVAL`| `reservoir.syncInterval` (ms) | `1000` |
|
|
183
|
+
* | `RATE_LIMITER_CIRCUIT_BREAKER_ENABLED`| circuit breaker enabled | `false` |
|
|
184
|
+
* | `RATE_LIMITER_RESERVOIR_ENABLED` | `reservoir.enabled` | `false` |
|
|
185
|
+
* | `RATE_LIMITER_LOG_LEVEL` | `observability.logLevel` | `info` |
|
|
186
|
+
* | `RATE_LIMITER_LOG_SAMPLE_RATE` | `observability.logSampleRate` | `1` |
|
|
187
|
+
* | `RATE_LIMITER_METRICS_BACKEND` | `observability.metrics` | `none` |
|
|
188
|
+
* | `RATE_LIMITER_METRICS_NAMESPACE` | `observability.namespace` | (none) |
|
|
189
|
+
*
|
|
190
|
+
* @returns A valid {@link RateLimiterConfig} derived from the current environment.
|
|
191
|
+
*/
|
|
192
|
+
function loadConfigFromEnv() {
|
|
193
|
+
const env = process.env;
|
|
194
|
+
const defaultLimit = parseInt(env['RATE_LIMITER_DEFAULT_LIMIT'] ?? '60', 10);
|
|
195
|
+
const defaultWindow = parseInt(env['RATE_LIMITER_DEFAULT_WINDOW'] ?? '60', 10);
|
|
196
|
+
const failurePolicyRaw = env['RATE_LIMITER_FAILURE_POLICY'];
|
|
197
|
+
const failurePolicy = failurePolicyRaw === 'closed' || failurePolicyRaw === 'local'
|
|
198
|
+
? failurePolicyRaw
|
|
199
|
+
: 'open';
|
|
200
|
+
const reservoirEnabled = env['RATE_LIMITER_RESERVOIR_ENABLED'] === 'true';
|
|
201
|
+
const batchSize = parseInt(env['RATE_LIMITER_RESERVOIR_BATCH_SIZE'] ?? '10', 10);
|
|
202
|
+
const syncInterval = parseInt(env['RATE_LIMITER_RESERVOIR_SYNC_INTERVAL'] ?? '1000', 10);
|
|
203
|
+
const circuitBreakerEnabled = env['RATE_LIMITER_CIRCUIT_BREAKER_ENABLED'] === 'true';
|
|
204
|
+
const logLevelRaw = env['RATE_LIMITER_LOG_LEVEL'];
|
|
205
|
+
const logLevel = logLevelRaw === 'debug' || logLevelRaw === 'warn' || logLevelRaw === 'error'
|
|
206
|
+
? logLevelRaw
|
|
207
|
+
: 'info';
|
|
208
|
+
const logSampleRate = parseFloat(env['RATE_LIMITER_LOG_SAMPLE_RATE'] ?? '1');
|
|
209
|
+
const metricsBackendRaw = env['RATE_LIMITER_METRICS_BACKEND'];
|
|
210
|
+
const metricsBackend = metricsBackendRaw === 'cloudwatch' ||
|
|
211
|
+
metricsBackendRaw === 'prometheus' ||
|
|
212
|
+
metricsBackendRaw === 'statsd'
|
|
213
|
+
? metricsBackendRaw
|
|
214
|
+
: 'none';
|
|
215
|
+
const defaultRule = {
|
|
216
|
+
name: 'default',
|
|
217
|
+
limits: {
|
|
218
|
+
ip: { limit: defaultLimit, window: defaultWindow },
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const redisUrl = env['RATE_LIMITER_REDIS_URL'];
|
|
222
|
+
const config = {
|
|
223
|
+
redis: {
|
|
224
|
+
...(redisUrl !== undefined ? { url: redisUrl } : { url: 'redis://localhost:6379' }),
|
|
225
|
+
...(env['RATE_LIMITER_REDIS_AUTH'] !== undefined
|
|
226
|
+
? { password: env['RATE_LIMITER_REDIS_AUTH'] }
|
|
227
|
+
: {}),
|
|
228
|
+
},
|
|
229
|
+
rules: [defaultRule],
|
|
230
|
+
reservoir: {
|
|
231
|
+
enabled: reservoirEnabled,
|
|
232
|
+
batchSize,
|
|
233
|
+
syncInterval,
|
|
234
|
+
},
|
|
235
|
+
failure: {
|
|
236
|
+
default: failurePolicy,
|
|
237
|
+
...(circuitBreakerEnabled
|
|
238
|
+
? {
|
|
239
|
+
circuitBreaker: {
|
|
240
|
+
enabled: true,
|
|
241
|
+
threshold: 5,
|
|
242
|
+
recoveryTimeout: 30000,
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
: {}),
|
|
246
|
+
},
|
|
247
|
+
observability: {
|
|
248
|
+
logLevel,
|
|
249
|
+
logSampleRate: isNaN(logSampleRate) ? 1 : logSampleRate,
|
|
250
|
+
metrics: metricsBackend,
|
|
251
|
+
...(env['RATE_LIMITER_METRICS_NAMESPACE'] !== undefined
|
|
252
|
+
? { namespace: env['RATE_LIMITER_METRICS_NAMESPACE'] }
|
|
253
|
+
: {}),
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
validateConfig(config);
|
|
257
|
+
return config;
|
|
258
|
+
}
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// loadConfig
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
/**
|
|
263
|
+
* Load configuration using the following resolution order:
|
|
264
|
+
* 1. `filePath` argument (if provided).
|
|
265
|
+
* 2. `RATE_LIMITER_CONFIG` environment variable (path to YAML file).
|
|
266
|
+
* 3. Fall back to {@link loadConfigFromEnv}.
|
|
267
|
+
*
|
|
268
|
+
* @param filePath Optional explicit path to a YAML configuration file.
|
|
269
|
+
* @returns Parsed and validated {@link RateLimiterConfig}.
|
|
270
|
+
* @throws {ConfigurationError} If a file path is resolved but the file is
|
|
271
|
+
* missing or invalid.
|
|
272
|
+
*/
|
|
273
|
+
function loadConfig(filePath) {
|
|
274
|
+
const resolvedPath = filePath ?? process.env['RATE_LIMITER_CONFIG'];
|
|
275
|
+
if (resolvedPath !== undefined && resolvedPath !== '') {
|
|
276
|
+
return loadConfigFromFile(resolvedPath);
|
|
277
|
+
}
|
|
278
|
+
return loadConfigFromEnv();
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SSM Parameter Store hot-reload watcher.
|
|
3
|
+
*
|
|
4
|
+
* Polls AWS SSM Parameter Store at a configurable interval and calls
|
|
5
|
+
* `onUpdate` whenever parameter values change. This allows rate limit
|
|
6
|
+
* configuration to be updated at runtime without restarting the process.
|
|
7
|
+
*
|
|
8
|
+
* Uses dynamic import of `@aws-sdk/client-ssm` so that the module can be
|
|
9
|
+
* loaded without the AWS SDK being available (e.g. in unit tests).
|
|
10
|
+
*/
|
|
11
|
+
import { RateLimiterConfig } from '../core/types';
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for the {@link SSMWatcher}.
|
|
14
|
+
*/
|
|
15
|
+
export interface SSMWatcherConfig {
|
|
16
|
+
/**
|
|
17
|
+
* SSM parameter path prefix to poll, e.g. `/rate-limiter/prod/limits`.
|
|
18
|
+
* All parameters under this path hierarchy will be fetched.
|
|
19
|
+
*/
|
|
20
|
+
parameterPath: string;
|
|
21
|
+
/** AWS region where the SSM parameters live. */
|
|
22
|
+
region: string;
|
|
23
|
+
/** Polling interval in milliseconds. Defaults to 60 000 ms (1 minute). */
|
|
24
|
+
refreshInterval?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Callback invoked whenever one or more parameter values change.
|
|
27
|
+
* Receives a partial config built from the changed parameters.
|
|
28
|
+
*/
|
|
29
|
+
onUpdate?: (newConfig: Partial<RateLimiterConfig>) => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Polls AWS SSM Parameter Store for configuration changes and notifies the
|
|
33
|
+
* application via the `onUpdate` callback.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const watcher = new SSMWatcher({
|
|
38
|
+
* parameterPath: '/rate-limiter/prod',
|
|
39
|
+
* region: 'us-east-1',
|
|
40
|
+
* refreshInterval: 30_000,
|
|
41
|
+
* onUpdate: (cfg) => limiter.applyConfig(cfg),
|
|
42
|
+
* })
|
|
43
|
+
* watcher.start()
|
|
44
|
+
* // … later …
|
|
45
|
+
* watcher.stop()
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare class SSMWatcher {
|
|
49
|
+
private intervalId;
|
|
50
|
+
private readonly config;
|
|
51
|
+
private lastParams;
|
|
52
|
+
/**
|
|
53
|
+
* @param config Watcher configuration.
|
|
54
|
+
* @throws {ConfigurationError} If `parameterPath` or `region` are empty.
|
|
55
|
+
*/
|
|
56
|
+
constructor(config: SSMWatcherConfig);
|
|
57
|
+
/**
|
|
58
|
+
* Start polling SSM for parameter changes at the configured interval.
|
|
59
|
+
* If the watcher is already running this is a no-op.
|
|
60
|
+
*/
|
|
61
|
+
start(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Stop polling. The in-flight poll (if any) may still complete.
|
|
64
|
+
*/
|
|
65
|
+
stop(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Returns `true` when the watcher is actively polling.
|
|
68
|
+
*/
|
|
69
|
+
isRunning(): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Fetch all SSM parameters under the configured path once.
|
|
72
|
+
* Exposed for testing and manual refresh.
|
|
73
|
+
*
|
|
74
|
+
* @returns Map from parameter name to its string value.
|
|
75
|
+
* @throws {ConfigurationError} If the AWS SDK cannot be loaded.
|
|
76
|
+
*/
|
|
77
|
+
fetchParams(): Promise<Map<string, string>>;
|
|
78
|
+
/**
|
|
79
|
+
* Internal poll: fetch params, diff against last snapshot, call onUpdate if changed.
|
|
80
|
+
*/
|
|
81
|
+
private poll;
|
|
82
|
+
/**
|
|
83
|
+
* Compute which parameters changed between `prev` and `next`.
|
|
84
|
+
*
|
|
85
|
+
* @param prev Previous snapshot.
|
|
86
|
+
* @param next Current snapshot.
|
|
87
|
+
* @returns Map containing only the changed entries from `next`.
|
|
88
|
+
*/
|
|
89
|
+
private diff;
|
|
90
|
+
/**
|
|
91
|
+
* Build a partial {@link RateLimiterConfig} from the changed SSM parameters.
|
|
92
|
+
*
|
|
93
|
+
* Parameter name conventions (relative to `parameterPath`):
|
|
94
|
+
* - `/ip/limit` → first rule ip.limit
|
|
95
|
+
* - `/ip/window` → first rule ip.window
|
|
96
|
+
* - `/log/level` → observability.logLevel
|
|
97
|
+
*
|
|
98
|
+
* @param changed Map of changed parameter names → new values.
|
|
99
|
+
* @returns Partial config derived from the changed parameters.
|
|
100
|
+
*/
|
|
101
|
+
private buildPartialConfig;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=ssm-watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssm-watcher.d.ts","sourceRoot":"","sources":["../../src/config/ssm-watcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,iBAAiB,EAAsB,MAAM,eAAe,CAAC;AAMtE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;CAC5D;AAiCD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,UAAU,CAAkC;IAEpD;;;OAGG;gBACS,MAAM,EAAE,gBAAgB;IAoBpC;;;OAGG;IACH,KAAK,IAAI,IAAI;IAgBb;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;;;;;OAMG;IACG,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IA4CjD;;OAEG;YACW,IAAI;IAgBlB;;;;;;OAMG;IACH,OAAO,CAAC,IAAI;IAmBZ;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kBAAkB;CA+B3B"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview SSM Parameter Store hot-reload watcher.
|
|
4
|
+
*
|
|
5
|
+
* Polls AWS SSM Parameter Store at a configurable interval and calls
|
|
6
|
+
* `onUpdate` whenever parameter values change. This allows rate limit
|
|
7
|
+
* configuration to be updated at runtime without restarting the process.
|
|
8
|
+
*
|
|
9
|
+
* Uses dynamic import of `@aws-sdk/client-ssm` so that the module can be
|
|
10
|
+
* loaded without the AWS SDK being available (e.g. in unit tests).
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.SSMWatcher = void 0;
|
|
47
|
+
const types_1 = require("../core/types");
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// SSM client factory (lazy dynamic import avoids hard dep at load time)
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
/**
|
|
52
|
+
* Lazily create an SSM client using dynamic import.
|
|
53
|
+
*
|
|
54
|
+
* @param region AWS region.
|
|
55
|
+
* @returns Configured SSM client.
|
|
56
|
+
*/
|
|
57
|
+
async function getSSMClient(region) {
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic SDK import
|
|
59
|
+
const mod = await Promise.resolve().then(() => __importStar(require('@aws-sdk/client-ssm')));
|
|
60
|
+
const SSM = mod.SSM ?? mod.default?.SSM;
|
|
61
|
+
if (typeof SSM !== 'function') {
|
|
62
|
+
throw new types_1.ConfigurationError('Cannot load @aws-sdk/client-ssm');
|
|
63
|
+
}
|
|
64
|
+
return new SSM({ region });
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// SSMWatcher
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Polls AWS SSM Parameter Store for configuration changes and notifies the
|
|
71
|
+
* application via the `onUpdate` callback.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const watcher = new SSMWatcher({
|
|
76
|
+
* parameterPath: '/rate-limiter/prod',
|
|
77
|
+
* region: 'us-east-1',
|
|
78
|
+
* refreshInterval: 30_000,
|
|
79
|
+
* onUpdate: (cfg) => limiter.applyConfig(cfg),
|
|
80
|
+
* })
|
|
81
|
+
* watcher.start()
|
|
82
|
+
* // … later …
|
|
83
|
+
* watcher.stop()
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
class SSMWatcher {
|
|
87
|
+
intervalId = null;
|
|
88
|
+
config;
|
|
89
|
+
lastParams = new Map();
|
|
90
|
+
/**
|
|
91
|
+
* @param config Watcher configuration.
|
|
92
|
+
* @throws {ConfigurationError} If `parameterPath` or `region` are empty.
|
|
93
|
+
*/
|
|
94
|
+
constructor(config) {
|
|
95
|
+
if (!config.parameterPath || config.parameterPath.trim() === '') {
|
|
96
|
+
throw new types_1.ConfigurationError('SSMWatcher requires a non-empty parameterPath');
|
|
97
|
+
}
|
|
98
|
+
if (!config.region || config.region.trim() === '') {
|
|
99
|
+
throw new types_1.ConfigurationError('SSMWatcher requires a non-empty region');
|
|
100
|
+
}
|
|
101
|
+
this.config = {
|
|
102
|
+
parameterPath: config.parameterPath,
|
|
103
|
+
region: config.region,
|
|
104
|
+
refreshInterval: config.refreshInterval ?? 60_000,
|
|
105
|
+
onUpdate: config.onUpdate ?? (() => undefined),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// -------------------------------------------------------------------------
|
|
109
|
+
// Public API
|
|
110
|
+
// -------------------------------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Start polling SSM for parameter changes at the configured interval.
|
|
113
|
+
* If the watcher is already running this is a no-op.
|
|
114
|
+
*/
|
|
115
|
+
start() {
|
|
116
|
+
if (this.intervalId !== null)
|
|
117
|
+
return;
|
|
118
|
+
// Fire immediately on first start, then on each interval tick.
|
|
119
|
+
void this.poll();
|
|
120
|
+
this.intervalId = setInterval(() => {
|
|
121
|
+
void this.poll();
|
|
122
|
+
}, this.config.refreshInterval);
|
|
123
|
+
// Prevent the interval from keeping the Node process alive.
|
|
124
|
+
if (this.intervalId.unref) {
|
|
125
|
+
this.intervalId.unref();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Stop polling. The in-flight poll (if any) may still complete.
|
|
130
|
+
*/
|
|
131
|
+
stop() {
|
|
132
|
+
if (this.intervalId !== null) {
|
|
133
|
+
clearInterval(this.intervalId);
|
|
134
|
+
this.intervalId = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Returns `true` when the watcher is actively polling.
|
|
139
|
+
*/
|
|
140
|
+
isRunning() {
|
|
141
|
+
return this.intervalId !== null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Fetch all SSM parameters under the configured path once.
|
|
145
|
+
* Exposed for testing and manual refresh.
|
|
146
|
+
*
|
|
147
|
+
* @returns Map from parameter name to its string value.
|
|
148
|
+
* @throws {ConfigurationError} If the AWS SDK cannot be loaded.
|
|
149
|
+
*/
|
|
150
|
+
async fetchParams() {
|
|
151
|
+
const client = await getSSMClient(this.config.region);
|
|
152
|
+
const result = new Map();
|
|
153
|
+
let nextToken;
|
|
154
|
+
do {
|
|
155
|
+
const params = {
|
|
156
|
+
Path: this.config.parameterPath,
|
|
157
|
+
Recursive: true,
|
|
158
|
+
WithDecryption: true,
|
|
159
|
+
...(nextToken !== undefined ? { NextToken: nextToken } : {}),
|
|
160
|
+
};
|
|
161
|
+
// Support both SDK v2 (.promise()) and SDK v3 (direct promise)
|
|
162
|
+
const response = await (async () => {
|
|
163
|
+
const call = client.getParametersByPath(params);
|
|
164
|
+
if (call && typeof call.promise === 'function') {
|
|
165
|
+
return call.promise();
|
|
166
|
+
}
|
|
167
|
+
return call;
|
|
168
|
+
})();
|
|
169
|
+
const resp = response;
|
|
170
|
+
for (const p of resp.Parameters ?? []) {
|
|
171
|
+
if (p.Name !== undefined && p.Value !== undefined) {
|
|
172
|
+
result.set(p.Name, p.Value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
nextToken = resp.NextToken;
|
|
176
|
+
} while (nextToken !== undefined);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
// Private helpers
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
/**
|
|
183
|
+
* Internal poll: fetch params, diff against last snapshot, call onUpdate if changed.
|
|
184
|
+
*/
|
|
185
|
+
async poll() {
|
|
186
|
+
try {
|
|
187
|
+
const current = await this.fetchParams();
|
|
188
|
+
const changed = this.diff(this.lastParams, current);
|
|
189
|
+
if (changed.size > 0) {
|
|
190
|
+
this.lastParams = current;
|
|
191
|
+
const partial = this.buildPartialConfig(changed);
|
|
192
|
+
this.config.onUpdate(partial);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
197
|
+
process.stderr.write(`[SSMWatcher] poll error: ${message}\n`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Compute which parameters changed between `prev` and `next`.
|
|
202
|
+
*
|
|
203
|
+
* @param prev Previous snapshot.
|
|
204
|
+
* @param next Current snapshot.
|
|
205
|
+
* @returns Map containing only the changed entries from `next`.
|
|
206
|
+
*/
|
|
207
|
+
diff(prev, next) {
|
|
208
|
+
const changed = new Map();
|
|
209
|
+
for (const [key, value] of next) {
|
|
210
|
+
if (prev.get(key) !== value) {
|
|
211
|
+
changed.set(key, value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Also detect deleted params (value disappears).
|
|
215
|
+
for (const key of prev.keys()) {
|
|
216
|
+
if (!next.has(key)) {
|
|
217
|
+
changed.set(key, '');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return changed;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Build a partial {@link RateLimiterConfig} from the changed SSM parameters.
|
|
224
|
+
*
|
|
225
|
+
* Parameter name conventions (relative to `parameterPath`):
|
|
226
|
+
* - `/ip/limit` → first rule ip.limit
|
|
227
|
+
* - `/ip/window` → first rule ip.window
|
|
228
|
+
* - `/log/level` → observability.logLevel
|
|
229
|
+
*
|
|
230
|
+
* @param changed Map of changed parameter names → new values.
|
|
231
|
+
* @returns Partial config derived from the changed parameters.
|
|
232
|
+
*/
|
|
233
|
+
buildPartialConfig(changed) {
|
|
234
|
+
const partial = {};
|
|
235
|
+
for (const [name, value] of changed) {
|
|
236
|
+
const suffix = name.replace(this.config.parameterPath, '');
|
|
237
|
+
if (suffix === '/ip/limit' || suffix === '/ip/window') {
|
|
238
|
+
const parsed = parseInt(value, 10);
|
|
239
|
+
if (!isNaN(parsed)) {
|
|
240
|
+
if (!partial.rules) {
|
|
241
|
+
partial.rules = [{ name: 'default', limits: {} }];
|
|
242
|
+
}
|
|
243
|
+
const rule = partial.rules[0];
|
|
244
|
+
if (!rule.limits.ip) {
|
|
245
|
+
rule.limits.ip = { limit: 60, window: 60 };
|
|
246
|
+
}
|
|
247
|
+
if (suffix === '/ip/limit')
|
|
248
|
+
rule.limits.ip.limit = parsed;
|
|
249
|
+
else
|
|
250
|
+
rule.limits.ip.window = parsed;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (suffix === '/log/level') {
|
|
254
|
+
const level = value;
|
|
255
|
+
if (['debug', 'info', 'warn', 'error'].includes(level)) {
|
|
256
|
+
partial.observability = { ...partial.observability, logLevel: level };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return partial;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
exports.SSMWatcher = SSMWatcher;
|
|
264
|
+
//# sourceMappingURL=ssm-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssm-watcher.js","sourceRoot":"","sources":["../../src/config/ssm-watcher.ts"],"names":[],"mappings":"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"}
|