@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1027 -0
  4. package/dist/adapters/express.d.ts +122 -0
  5. package/dist/adapters/express.d.ts.map +1 -0
  6. package/dist/adapters/express.js +190 -0
  7. package/dist/adapters/express.js.map +1 -0
  8. package/dist/adapters/fastify.d.ts +112 -0
  9. package/dist/adapters/fastify.d.ts.map +1 -0
  10. package/dist/adapters/fastify.js +178 -0
  11. package/dist/adapters/fastify.js.map +1 -0
  12. package/dist/adapters/index.d.ts +13 -0
  13. package/dist/adapters/index.d.ts.map +1 -0
  14. package/dist/adapters/index.js +22 -0
  15. package/dist/adapters/index.js.map +1 -0
  16. package/dist/adapters/lambda/decorator.d.ts +120 -0
  17. package/dist/adapters/lambda/decorator.d.ts.map +1 -0
  18. package/dist/adapters/lambda/decorator.js +281 -0
  19. package/dist/adapters/lambda/decorator.js.map +1 -0
  20. package/dist/adapters/lambda/extension.d.ts +178 -0
  21. package/dist/adapters/lambda/extension.d.ts.map +1 -0
  22. package/dist/adapters/lambda/extension.js +445 -0
  23. package/dist/adapters/lambda/extension.js.map +1 -0
  24. package/dist/adapters/lambda/index.d.ts +9 -0
  25. package/dist/adapters/lambda/index.d.ts.map +1 -0
  26. package/dist/adapters/lambda/index.js +16 -0
  27. package/dist/adapters/lambda/index.js.map +1 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/index.d.ts.map +1 -0
  30. package/dist/config/index.js +11 -0
  31. package/dist/config/index.js.map +1 -0
  32. package/dist/config/loader.d.ts +68 -0
  33. package/dist/config/loader.d.ts.map +1 -0
  34. package/dist/config/loader.js +280 -0
  35. package/dist/config/loader.js.map +1 -0
  36. package/dist/config/ssm-watcher.d.ts +103 -0
  37. package/dist/config/ssm-watcher.d.ts.map +1 -0
  38. package/dist/config/ssm-watcher.js +264 -0
  39. package/dist/config/ssm-watcher.js.map +1 -0
  40. package/dist/core/algorithm.d.ts +98 -0
  41. package/dist/core/algorithm.d.ts.map +1 -0
  42. package/dist/core/algorithm.js +127 -0
  43. package/dist/core/algorithm.js.map +1 -0
  44. package/dist/core/index.d.ts +8 -0
  45. package/dist/core/index.d.ts.map +1 -0
  46. package/dist/core/index.js +24 -0
  47. package/dist/core/index.js.map +1 -0
  48. package/dist/core/key-builder.d.ts +103 -0
  49. package/dist/core/key-builder.d.ts.map +1 -0
  50. package/dist/core/key-builder.js +232 -0
  51. package/dist/core/key-builder.js.map +1 -0
  52. package/dist/core/types.d.ts +253 -0
  53. package/dist/core/types.d.ts.map +1 -0
  54. package/dist/core/types.js +72 -0
  55. package/dist/core/types.js.map +1 -0
  56. package/dist/index.d.ts +6 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +24 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/observability/index.d.ts +5 -0
  61. package/dist/observability/index.d.ts.map +1 -0
  62. package/dist/observability/index.js +12 -0
  63. package/dist/observability/index.js.map +1 -0
  64. package/dist/observability/logger.d.ts +136 -0
  65. package/dist/observability/logger.d.ts.map +1 -0
  66. package/dist/observability/logger.js +167 -0
  67. package/dist/observability/logger.js.map +1 -0
  68. package/dist/observability/metrics.d.ts +129 -0
  69. package/dist/observability/metrics.d.ts.map +1 -0
  70. package/dist/observability/metrics.js +137 -0
  71. package/dist/observability/metrics.js.map +1 -0
  72. package/dist/rate-limiter.d.ts +171 -0
  73. package/dist/rate-limiter.d.ts.map +1 -0
  74. package/dist/rate-limiter.js +702 -0
  75. package/dist/rate-limiter.js.map +1 -0
  76. package/dist/redis/circuit-breaker.d.ts +84 -0
  77. package/dist/redis/circuit-breaker.d.ts.map +1 -0
  78. package/dist/redis/circuit-breaker.js +131 -0
  79. package/dist/redis/circuit-breaker.js.map +1 -0
  80. package/dist/redis/client.d.ts +98 -0
  81. package/dist/redis/client.d.ts.map +1 -0
  82. package/dist/redis/client.js +223 -0
  83. package/dist/redis/client.js.map +1 -0
  84. package/dist/redis/index.d.ts +8 -0
  85. package/dist/redis/index.d.ts.map +1 -0
  86. package/dist/redis/index.js +16 -0
  87. package/dist/redis/index.js.map +1 -0
  88. package/dist/redis/script-loader.d.ts +111 -0
  89. package/dist/redis/script-loader.d.ts.map +1 -0
  90. package/dist/redis/script-loader.js +204 -0
  91. package/dist/redis/script-loader.js.map +1 -0
  92. package/dist/reservoir/index.d.ts +6 -0
  93. package/dist/reservoir/index.d.ts.map +1 -0
  94. package/dist/reservoir/index.js +9 -0
  95. package/dist/reservoir/index.js.map +1 -0
  96. package/dist/reservoir/local-reservoir.d.ts +98 -0
  97. package/dist/reservoir/local-reservoir.d.ts.map +1 -0
  98. package/dist/reservoir/local-reservoir.js +148 -0
  99. package/dist/reservoir/local-reservoir.js.map +1 -0
  100. 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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"}