@gugananuvem/aws-local-simulator 1.0.15 → 1.0.16
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/README.md +789 -594
- package/bin/aws-local-simulator.js +63 -63
- package/package.json +2 -2
- package/src/config/config-loader.js +114 -114
- package/src/config/default-config.js +68 -68
- package/src/config/env-loader.js +68 -68
- package/src/index.js +146 -146
- package/src/index.mjs +123 -123
- package/src/server.js +227 -227
- package/src/services/apigateway/index.js +75 -73
- package/src/services/apigateway/server.js +570 -507
- package/src/services/apigateway/simulator.js +1261 -1261
- package/src/services/athena/index.js +75 -75
- package/src/services/athena/server.js +101 -101
- package/src/services/athena/simulador.js +998 -998
- package/src/services/athena/simulator.js +346 -346
- package/src/services/cloudformation/index.js +106 -106
- package/src/services/cloudformation/server.js +417 -417
- package/src/services/cloudformation/simulador.js +1045 -1045
- package/src/services/cloudtrail/index.js +84 -84
- package/src/services/cloudtrail/server.js +235 -235
- package/src/services/cloudtrail/simulador.js +719 -719
- package/src/services/cloudwatch/index.js +84 -84
- package/src/services/cloudwatch/server.js +366 -366
- package/src/services/cloudwatch/simulador.js +1173 -1173
- package/src/services/cognito/index.js +79 -79
- package/src/services/cognito/server.js +301 -301
- package/src/services/cognito/simulator.js +1655 -1655
- package/src/services/config/index.js +96 -96
- package/src/services/config/server.js +215 -215
- package/src/services/config/simulador.js +1260 -1260
- package/src/services/dynamodb/index.js +74 -74
- package/src/services/dynamodb/server.js +125 -125
- package/src/services/dynamodb/simulator.js +630 -630
- package/src/services/ecs/index.js +65 -65
- package/src/services/ecs/server.js +235 -235
- package/src/services/ecs/simulator.js +844 -844
- package/src/services/eventbridge/index.js +89 -89
- package/src/services/eventbridge/server.js +209 -209
- package/src/services/eventbridge/simulator.js +684 -684
- package/src/services/index.js +45 -45
- package/src/services/kms/index.js +75 -75
- package/src/services/kms/server.js +67 -67
- package/src/services/kms/simulator.js +324 -324
- package/src/services/lambda/handler-loader.js +183 -183
- package/src/services/lambda/index.js +78 -78
- package/src/services/lambda/route-registry.js +274 -274
- package/src/services/lambda/server.js +145 -145
- package/src/services/lambda/simulator.js +199 -199
- package/src/services/parameter-store/index.js +80 -80
- package/src/services/parameter-store/server.js +50 -50
- package/src/services/parameter-store/simulator.js +201 -201
- package/src/services/s3/index.js +73 -73
- package/src/services/s3/server.js +329 -329
- package/src/services/s3/simulator.js +565 -565
- package/src/services/secret-manager/index.js +80 -80
- package/src/services/secret-manager/server.js +50 -50
- package/src/services/secret-manager/simulator.js +171 -171
- package/src/services/sns/index.js +89 -89
- package/src/services/sns/server.js +580 -580
- package/src/services/sns/simulator.js +1482 -1482
- package/src/services/sqs/index.js +98 -93
- package/src/services/sqs/server.js +349 -349
- package/src/services/sqs/simulator.js +441 -441
- package/src/services/sts/index.js +37 -37
- package/src/services/sts/server.js +144 -144
- package/src/services/sts/simulator.js +69 -69
- package/src/services/xray/index.js +83 -83
- package/src/services/xray/server.js +308 -308
- package/src/services/xray/simulador.js +994 -994
- package/src/template/aws-config-template.js +87 -87
- package/src/template/aws-config-template.mjs +90 -90
- package/src/template/config-template.json +203 -203
- package/src/utils/aws-config.js +91 -91
- package/src/utils/cloudtrail-audit.js +129 -129
- package/src/utils/local-store.js +83 -83
- package/src/utils/logger.js +59 -59
|
@@ -1,684 +1,684 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview EventBridge Simulator
|
|
3
|
-
* Simula o Amazon EventBridge com event buses, rules e targets
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
const crypto = require('crypto');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* EventBridge Simulator
|
|
12
|
-
*/
|
|
13
|
-
class EventBridgeSimulator {
|
|
14
|
-
/**
|
|
15
|
-
* @param {Object} config - Service configuration
|
|
16
|
-
* @param {Object} store - LocalStore instance
|
|
17
|
-
* @param {Object} logger - Logger instance
|
|
18
|
-
*/
|
|
19
|
-
constructor(config, store, logger) {
|
|
20
|
-
this.config = config;
|
|
21
|
-
this.store = store;
|
|
22
|
-
this.logger = logger;
|
|
23
|
-
|
|
24
|
-
/** @type {Map<string, Object>} Event buses */
|
|
25
|
-
this.buses = new Map();
|
|
26
|
-
/** @type {Map<string, Object>} Rules by ruleArn */
|
|
27
|
-
this.rules = new Map();
|
|
28
|
-
/** @type {Map<string, Object[]>} Targets by ruleArn */
|
|
29
|
-
this.targets = new Map();
|
|
30
|
-
/** @type {Array} Event archive (recent events) */
|
|
31
|
-
this.eventArchive = [];
|
|
32
|
-
|
|
33
|
-
this.region = 'us-east-1';
|
|
34
|
-
this.accountId = '123456789012';
|
|
35
|
-
|
|
36
|
-
// Services for target delivery
|
|
37
|
-
this.lambdaService = null;
|
|
38
|
-
this.sqsService = null;
|
|
39
|
-
this.snsService = null;
|
|
40
|
-
|
|
41
|
-
// Create default event bus
|
|
42
|
-
this._createDefaultBus();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Create the default event bus
|
|
47
|
-
*/
|
|
48
|
-
_createDefaultBus() {
|
|
49
|
-
const defaultBus = {
|
|
50
|
-
Name: 'default',
|
|
51
|
-
Arn: `arn:aws:events:${this.region}:${this.accountId}:event-bus/default`,
|
|
52
|
-
State: 'ACTIVE',
|
|
53
|
-
CreationTime: new Date().toISOString()
|
|
54
|
-
};
|
|
55
|
-
this.buses.set('default', defaultBus);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** @param {Object} s */ setLambdaService(s) { this.lambdaService = s; }
|
|
59
|
-
/** @param {Object} s */ setSqsService(s) { this.sqsService = s; }
|
|
60
|
-
/** @param {Object} s */ setSnsService(s) { this.snsService = s; }
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Load persisted data
|
|
64
|
-
*/
|
|
65
|
-
async load() {
|
|
66
|
-
try {
|
|
67
|
-
const buses = await this.store.read('eventbridge/buses');
|
|
68
|
-
if (Array.isArray(buses)) {
|
|
69
|
-
buses.forEach(b => this.buses.set(b.Name, b));
|
|
70
|
-
}
|
|
71
|
-
const rules = await this.store.read('eventbridge/rules');
|
|
72
|
-
if (Array.isArray(rules)) {
|
|
73
|
-
rules.forEach(r => this.rules.set(r.Arn, r));
|
|
74
|
-
}
|
|
75
|
-
const targets = await this.store.read('eventbridge/targets');
|
|
76
|
-
if (Array.isArray(targets)) {
|
|
77
|
-
targets.forEach(({ ruleArn, list }) => this.targets.set(ruleArn, list));
|
|
78
|
-
}
|
|
79
|
-
this.logger.debug('EventBridge', `Loaded ${this.buses.size} buses, ${this.rules.size} rules`);
|
|
80
|
-
} catch {
|
|
81
|
-
this.logger.debug('EventBridge', 'No persisted data, starting fresh');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Save data
|
|
87
|
-
*/
|
|
88
|
-
async _save() {
|
|
89
|
-
await this.store.write('eventbridge/buses', null, Array.from(this.buses.values()));
|
|
90
|
-
await this.store.write('eventbridge/rules', null, Array.from(this.rules.values()));
|
|
91
|
-
const targetsList = Array.from(this.targets.entries())
|
|
92
|
-
.map(([ruleArn, list]) => ({ ruleArn, list }));
|
|
93
|
-
await this.store.write('eventbridge/targets', null, targetsList);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ==================== Event Bus Operations ====================
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* CreateEventBus
|
|
100
|
-
* @param {Object} params
|
|
101
|
-
* @returns {Object}
|
|
102
|
-
*/
|
|
103
|
-
async createEventBus(params) {
|
|
104
|
-
const { Name, EventSourceName, Tags } = params;
|
|
105
|
-
|
|
106
|
-
if (!Name) throw this._error('ValidationException', 'Name is required');
|
|
107
|
-
if (Name === 'default') throw this._error('ResourceAlreadyExistsException', 'Default event bus already exists');
|
|
108
|
-
if (this.buses.has(Name)) throw this._error('ResourceAlreadyExistsException', `Event bus ${Name} already exists`);
|
|
109
|
-
|
|
110
|
-
const bus = {
|
|
111
|
-
Name,
|
|
112
|
-
Arn: `arn:aws:events:${this.region}:${this.accountId}:event-bus/${Name}`,
|
|
113
|
-
State: 'ACTIVE',
|
|
114
|
-
EventSourceName: EventSourceName || null,
|
|
115
|
-
Tags: Tags || [],
|
|
116
|
-
CreationTime: new Date().toISOString()
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
this.buses.set(Name, bus);
|
|
120
|
-
await this._save();
|
|
121
|
-
|
|
122
|
-
this.logger.info('EventBridge', `Created event bus: ${Name}`);
|
|
123
|
-
return { EventBusArn: bus.Arn };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* DeleteEventBus
|
|
128
|
-
* @param {Object} params
|
|
129
|
-
*/
|
|
130
|
-
async deleteEventBus(params) {
|
|
131
|
-
const { Name } = params;
|
|
132
|
-
|
|
133
|
-
if (Name === 'default') throw this._error('ValidationException', 'Cannot delete default event bus');
|
|
134
|
-
if (!this.buses.has(Name)) throw this._error('ResourceNotFoundException', `Event bus ${Name} not found`);
|
|
135
|
-
|
|
136
|
-
// Delete all rules for this bus
|
|
137
|
-
for (const [arn, rule] of this.rules.entries()) {
|
|
138
|
-
if (rule.EventBusName === Name) {
|
|
139
|
-
this.rules.delete(arn);
|
|
140
|
-
this.targets.delete(arn);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this.buses.delete(Name);
|
|
145
|
-
await this._save();
|
|
146
|
-
|
|
147
|
-
this.logger.info('EventBridge', `Deleted event bus: ${Name}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* ListEventBuses
|
|
152
|
-
* @param {Object} params
|
|
153
|
-
* @returns {Object}
|
|
154
|
-
*/
|
|
155
|
-
listEventBuses(params = {}) {
|
|
156
|
-
const { Limit = 100, NamePrefix, NextToken } = params;
|
|
157
|
-
let buses = Array.from(this.buses.values());
|
|
158
|
-
|
|
159
|
-
if (NamePrefix) {
|
|
160
|
-
buses = buses.filter(b => b.Name.startsWith(NamePrefix));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
EventBuses: buses.slice(0, Limit).map(b => ({
|
|
165
|
-
Name: b.Name,
|
|
166
|
-
Arn: b.Arn,
|
|
167
|
-
State: b.State
|
|
168
|
-
}))
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* DescribeEventBus
|
|
174
|
-
* @param {Object} params
|
|
175
|
-
* @returns {Object}
|
|
176
|
-
*/
|
|
177
|
-
describeEventBus(params = {}) {
|
|
178
|
-
const { Name = 'default' } = params;
|
|
179
|
-
const bus = this.buses.get(Name);
|
|
180
|
-
if (!bus) throw this._error('ResourceNotFoundException', `Event bus ${Name} not found`);
|
|
181
|
-
return bus;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ==================== Rules ====================
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* PutRule
|
|
188
|
-
* @param {Object} params
|
|
189
|
-
* @returns {Object}
|
|
190
|
-
*/
|
|
191
|
-
async putRule(params) {
|
|
192
|
-
const {
|
|
193
|
-
Name, EventBusName = 'default', EventPattern, ScheduleExpression,
|
|
194
|
-
State = 'ENABLED', Description, RoleArn, Tags
|
|
195
|
-
} = params;
|
|
196
|
-
|
|
197
|
-
if (!Name) throw this._error('ValidationException', 'Name is required');
|
|
198
|
-
if (!EventPattern && !ScheduleExpression) {
|
|
199
|
-
throw this._error('ValidationException', 'Either EventPattern or ScheduleExpression is required');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const bus = this.buses.get(EventBusName);
|
|
203
|
-
if (!bus) throw this._error('ResourceNotFoundException', `Event bus ${EventBusName} not found`);
|
|
204
|
-
|
|
205
|
-
// Validate EventPattern
|
|
206
|
-
let parsedPattern = null;
|
|
207
|
-
if (EventPattern) {
|
|
208
|
-
try {
|
|
209
|
-
parsedPattern = JSON.parse(EventPattern);
|
|
210
|
-
} catch {
|
|
211
|
-
throw this._error('InvalidEventPatternException', 'Event pattern is not valid JSON');
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
216
|
-
|
|
217
|
-
const rule = {
|
|
218
|
-
Name,
|
|
219
|
-
Arn: ruleArn,
|
|
220
|
-
EventBusName,
|
|
221
|
-
EventPattern: EventPattern || null,
|
|
222
|
-
ParsedPattern: parsedPattern,
|
|
223
|
-
ScheduleExpression: ScheduleExpression || null,
|
|
224
|
-
State,
|
|
225
|
-
Description: Description || '',
|
|
226
|
-
RoleArn: RoleArn || null,
|
|
227
|
-
Tags: Tags || [],
|
|
228
|
-
CreatedAt: new Date().toISOString()
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
this.rules.set(ruleArn, rule);
|
|
232
|
-
await this._save();
|
|
233
|
-
|
|
234
|
-
this.logger.info('EventBridge', `Put rule: ${Name} on bus ${EventBusName}`);
|
|
235
|
-
return { RuleArn: ruleArn };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* DeleteRule
|
|
240
|
-
* @param {Object} params
|
|
241
|
-
*/
|
|
242
|
-
async deleteRule(params) {
|
|
243
|
-
const { Name, EventBusName = 'default', Force } = params;
|
|
244
|
-
|
|
245
|
-
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
246
|
-
|
|
247
|
-
if (!this.rules.has(ruleArn)) {
|
|
248
|
-
throw this._error('ResourceNotFoundException', `Rule ${Name} not found`);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Check for targets unless Force
|
|
252
|
-
const ruleTargets = this.targets.get(ruleArn) || [];
|
|
253
|
-
if (ruleTargets.length > 0 && !Force) {
|
|
254
|
-
throw this._error('ValidationException', 'Rule has targets. Use Force=true to delete anyway');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
this.rules.delete(ruleArn);
|
|
258
|
-
this.targets.delete(ruleArn);
|
|
259
|
-
await this._save();
|
|
260
|
-
|
|
261
|
-
this.logger.info('EventBridge', `Deleted rule: ${Name}`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* ListRules
|
|
266
|
-
* @param {Object} params
|
|
267
|
-
* @returns {Object}
|
|
268
|
-
*/
|
|
269
|
-
listRules(params = {}) {
|
|
270
|
-
const { EventBusName = 'default', NamePrefix, Limit = 100 } = params;
|
|
271
|
-
let rules = Array.from(this.rules.values())
|
|
272
|
-
.filter(r => r.EventBusName === EventBusName);
|
|
273
|
-
|
|
274
|
-
if (NamePrefix) {
|
|
275
|
-
rules = rules.filter(r => r.Name.startsWith(NamePrefix));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
Rules: rules.slice(0, Limit).map(r => ({
|
|
280
|
-
Name: r.Name,
|
|
281
|
-
Arn: r.Arn,
|
|
282
|
-
EventBusName: r.EventBusName,
|
|
283
|
-
EventPattern: r.EventPattern,
|
|
284
|
-
ScheduleExpression: r.ScheduleExpression,
|
|
285
|
-
State: r.State,
|
|
286
|
-
Description: r.Description
|
|
287
|
-
}))
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* DescribeRule
|
|
293
|
-
* @param {Object} params
|
|
294
|
-
* @returns {Object}
|
|
295
|
-
*/
|
|
296
|
-
describeRule(params) {
|
|
297
|
-
const { Name, EventBusName = 'default' } = params;
|
|
298
|
-
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
299
|
-
const rule = this.rules.get(ruleArn);
|
|
300
|
-
if (!rule) throw this._error('ResourceNotFoundException', `Rule ${Name} not found`);
|
|
301
|
-
return rule;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* EnableRule
|
|
306
|
-
* @param {Object} params
|
|
307
|
-
*/
|
|
308
|
-
async enableRule(params) {
|
|
309
|
-
const { Name, EventBusName = 'default' } = params;
|
|
310
|
-
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
311
|
-
const rule = this.rules.get(ruleArn);
|
|
312
|
-
if (!rule) throw this._error('ResourceNotFoundException', `Rule ${Name} not found`);
|
|
313
|
-
rule.State = 'ENABLED';
|
|
314
|
-
await this._save();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* DisableRule
|
|
319
|
-
* @param {Object} params
|
|
320
|
-
*/
|
|
321
|
-
async disableRule(params) {
|
|
322
|
-
const { Name, EventBusName = 'default' } = params;
|
|
323
|
-
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
324
|
-
const rule = this.rules.get(ruleArn);
|
|
325
|
-
if (!rule) throw this._error('ResourceNotFoundException', `Rule ${Name} not found`);
|
|
326
|
-
rule.State = 'DISABLED';
|
|
327
|
-
await this._save();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ==================== Targets ====================
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* PutTargets
|
|
334
|
-
* @param {Object} params
|
|
335
|
-
* @returns {Object}
|
|
336
|
-
*/
|
|
337
|
-
async putTargets(params) {
|
|
338
|
-
const { Rule, EventBusName = 'default', Targets } = params;
|
|
339
|
-
|
|
340
|
-
if (!Rule) throw this._error('ValidationException', 'Rule is required');
|
|
341
|
-
if (!Targets || !Targets.length) throw this._error('ValidationException', 'Targets are required');
|
|
342
|
-
|
|
343
|
-
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Rule}`;
|
|
344
|
-
if (!this.rules.has(ruleArn)) throw this._error('ResourceNotFoundException', `Rule ${Rule} not found`);
|
|
345
|
-
|
|
346
|
-
const existing = this.targets.get(ruleArn) || [];
|
|
347
|
-
const failedEntries = [];
|
|
348
|
-
|
|
349
|
-
for (const target of Targets) {
|
|
350
|
-
if (!target.Id || !target.Arn) {
|
|
351
|
-
failedEntries.push({ TargetId: target.Id, ErrorCode: 'ValidationException', ErrorMessage: 'Id and Arn are required' });
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Remove existing target with same Id
|
|
356
|
-
const idx = existing.findIndex(t => t.Id === target.Id);
|
|
357
|
-
if (idx >= 0) existing.splice(idx, 1);
|
|
358
|
-
|
|
359
|
-
existing.push({
|
|
360
|
-
Id: target.Id,
|
|
361
|
-
Arn: target.Arn,
|
|
362
|
-
Input: target.Input || null,
|
|
363
|
-
InputPath: target.InputPath || null,
|
|
364
|
-
InputTransformer: target.InputTransformer || null,
|
|
365
|
-
RoleArn: target.RoleArn || null,
|
|
366
|
-
RetryPolicy: target.RetryPolicy || { MaximumRetryAttempts: 185, MaximumEventAgeInSeconds: 86400 },
|
|
367
|
-
DeadLetterConfig: target.DeadLetterConfig || null
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
this.targets.set(ruleArn, existing);
|
|
372
|
-
await this._save();
|
|
373
|
-
|
|
374
|
-
this.logger.info('EventBridge', `Put ${Targets.length} targets for rule ${Rule}`);
|
|
375
|
-
return {
|
|
376
|
-
FailedEntryCount: failedEntries.length,
|
|
377
|
-
FailedEntries: failedEntries
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* RemoveTargets
|
|
383
|
-
* @param {Object} params
|
|
384
|
-
* @returns {Object}
|
|
385
|
-
*/
|
|
386
|
-
async removeTargets(params) {
|
|
387
|
-
const { Rule, EventBusName = 'default', Ids } = params;
|
|
388
|
-
|
|
389
|
-
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Rule}`;
|
|
390
|
-
const existing = this.targets.get(ruleArn) || [];
|
|
391
|
-
|
|
392
|
-
const remaining = existing.filter(t => !Ids.includes(t.Id));
|
|
393
|
-
this.targets.set(ruleArn, remaining);
|
|
394
|
-
await this._save();
|
|
395
|
-
|
|
396
|
-
return { FailedEntryCount: 0, FailedEntries: [] };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* ListTargetsByRule
|
|
401
|
-
* @param {Object} params
|
|
402
|
-
* @returns {Object}
|
|
403
|
-
*/
|
|
404
|
-
listTargetsByRule(params) {
|
|
405
|
-
const { Rule, EventBusName = 'default' } = params;
|
|
406
|
-
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Rule}`;
|
|
407
|
-
|
|
408
|
-
if (!this.rules.has(ruleArn)) throw this._error('ResourceNotFoundException', `Rule ${Rule} not found`);
|
|
409
|
-
|
|
410
|
-
const targets = this.targets.get(ruleArn) || [];
|
|
411
|
-
return { Targets: targets };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// ==================== PutEvents ====================
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* PutEvents
|
|
418
|
-
* @param {Object} params
|
|
419
|
-
* @returns {Object}
|
|
420
|
-
*/
|
|
421
|
-
async putEvents(params) {
|
|
422
|
-
const { Entries } = params;
|
|
423
|
-
|
|
424
|
-
if (!Entries || !Entries.length) {
|
|
425
|
-
throw this._error('ValidationException', 'Entries are required');
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const results = [];
|
|
429
|
-
const failedEntries = [];
|
|
430
|
-
|
|
431
|
-
for (const entry of Entries) {
|
|
432
|
-
const eventId = crypto.randomUUID();
|
|
433
|
-
|
|
434
|
-
// Validate entry
|
|
435
|
-
if (!entry.Source) {
|
|
436
|
-
failedEntries.push({ ErrorCode: 'ValidationException', ErrorMessage: 'Source is required' });
|
|
437
|
-
results.push({ EventId: null, ErrorCode: 'ValidationException' });
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const busName = entry.EventBusName || 'default';
|
|
442
|
-
if (!this.buses.has(busName)) {
|
|
443
|
-
failedEntries.push({ ErrorCode: 'ResourceNotFoundException', ErrorMessage: `Bus ${busName} not found` });
|
|
444
|
-
results.push({ EventId: null, ErrorCode: 'ResourceNotFoundException' });
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Build event
|
|
449
|
-
let detail = entry.Detail;
|
|
450
|
-
if (typeof detail === 'string') {
|
|
451
|
-
try { detail = JSON.parse(detail); } catch { detail = {}; }
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const event = {
|
|
455
|
-
id: eventId,
|
|
456
|
-
version: '0',
|
|
457
|
-
account: this.accountId,
|
|
458
|
-
time: entry.Time || new Date().toISOString(),
|
|
459
|
-
region: this.region,
|
|
460
|
-
source: entry.Source,
|
|
461
|
-
'detail-type': entry.DetailType || '',
|
|
462
|
-
resources: entry.Resources || [],
|
|
463
|
-
detail: detail || {}
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
// Archive event
|
|
467
|
-
this.eventArchive.push(event);
|
|
468
|
-
if (this.eventArchive.length > 1000) {
|
|
469
|
-
this.eventArchive.shift();
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Match and deliver to rules
|
|
473
|
-
await this._matchAndDeliver(busName, event);
|
|
474
|
-
|
|
475
|
-
results.push({ EventId: eventId });
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
this.logger.debug('EventBridge', `PutEvents: ${Entries.length} events, ${failedEntries.length} failed`);
|
|
479
|
-
|
|
480
|
-
return {
|
|
481
|
-
FailedEntryCount: failedEntries.length,
|
|
482
|
-
Entries: results
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Match event to rules and deliver to targets
|
|
488
|
-
* @param {string} busName
|
|
489
|
-
* @param {Object} event
|
|
490
|
-
*/
|
|
491
|
-
async _matchAndDeliver(busName, event) {
|
|
492
|
-
const busRules = Array.from(this.rules.values())
|
|
493
|
-
.filter(r => r.EventBusName === busName && r.State === 'ENABLED');
|
|
494
|
-
|
|
495
|
-
for (const rule of busRules) {
|
|
496
|
-
if (rule.ParsedPattern && this._matchesPattern(event, rule.ParsedPattern)) {
|
|
497
|
-
const targets = this.targets.get(rule.Arn) || [];
|
|
498
|
-
for (const target of targets) {
|
|
499
|
-
try {
|
|
500
|
-
await this._deliverToTarget(target, event);
|
|
501
|
-
} catch (err) {
|
|
502
|
-
this.logger.warn('EventBridge', `Target delivery failed: ${err.message}`);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Check if event matches pattern
|
|
511
|
-
* @param {Object} event
|
|
512
|
-
* @param {Object} pattern
|
|
513
|
-
* @returns {boolean}
|
|
514
|
-
*/
|
|
515
|
-
_matchesPattern(event, pattern) {
|
|
516
|
-
for (const [key, matchers] of Object.entries(pattern)) {
|
|
517
|
-
const eventValue = key === 'detail' ? event.detail : event[key];
|
|
518
|
-
|
|
519
|
-
if (key === 'detail' && typeof matchers === 'object' && !Array.isArray(matchers)) {
|
|
520
|
-
// Recursive detail matching
|
|
521
|
-
if (!this._matchesPattern(event.detail || {}, matchers)) return false;
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (!Array.isArray(matchers)) continue;
|
|
526
|
-
|
|
527
|
-
const matches = matchers.some(matcher => {
|
|
528
|
-
if (matcher === null) return eventValue === null;
|
|
529
|
-
if (typeof matcher === 'string') return eventValue === matcher;
|
|
530
|
-
if (typeof matcher === 'object') {
|
|
531
|
-
if (matcher.prefix) return String(eventValue).startsWith(matcher.prefix);
|
|
532
|
-
if (matcher['anything-but']) {
|
|
533
|
-
return !matcher['anything-but'].includes(eventValue);
|
|
534
|
-
}
|
|
535
|
-
if (matcher.exists !== undefined) {
|
|
536
|
-
return matcher.exists ? eventValue !== undefined : eventValue === undefined;
|
|
537
|
-
}
|
|
538
|
-
if (matcher.numeric) return this._checkNumeric(parseFloat(eventValue), matcher.numeric);
|
|
539
|
-
}
|
|
540
|
-
return false;
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
if (!matches) return false;
|
|
544
|
-
}
|
|
545
|
-
return true;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* Check numeric conditions
|
|
550
|
-
* @param {number} value
|
|
551
|
-
* @param {Array} conditions
|
|
552
|
-
* @returns {boolean}
|
|
553
|
-
*/
|
|
554
|
-
_checkNumeric(value, conditions) {
|
|
555
|
-
for (let i = 0; i < conditions.length; i += 2) {
|
|
556
|
-
const op = conditions[i];
|
|
557
|
-
const threshold = conditions[i + 1];
|
|
558
|
-
if (op === '=' && value !== threshold) return false;
|
|
559
|
-
if (op === '>' && value <= threshold) return false;
|
|
560
|
-
if (op === '>=' && value < threshold) return false;
|
|
561
|
-
if (op === '<' && value >= threshold) return false;
|
|
562
|
-
if (op === '<=' && value > threshold) return false;
|
|
563
|
-
}
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Deliver event to target
|
|
569
|
-
* @param {Object} target
|
|
570
|
-
* @param {Object} event
|
|
571
|
-
*/
|
|
572
|
-
async _deliverToTarget(target, event) {
|
|
573
|
-
// Transform input
|
|
574
|
-
let inputEvent = event;
|
|
575
|
-
if (target.Input) {
|
|
576
|
-
try { inputEvent = JSON.parse(target.Input); } catch { inputEvent = target.Input; }
|
|
577
|
-
} else if (target.InputPath) {
|
|
578
|
-
inputEvent = this._extractPath(event, target.InputPath);
|
|
579
|
-
} else if (target.InputTransformer) {
|
|
580
|
-
inputEvent = this._transformInput(event, target.InputTransformer);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const arn = target.Arn;
|
|
584
|
-
|
|
585
|
-
// Lambda target
|
|
586
|
-
if (arn.includes(':lambda:') || arn.includes('function:')) {
|
|
587
|
-
if (!this.lambdaService) return;
|
|
588
|
-
const match = arn.match(/function:([^:]+)/);
|
|
589
|
-
if (!match) return;
|
|
590
|
-
await this.lambdaService.simulator.invokeFunction(match[1], inputEvent);
|
|
591
|
-
this.logger.debug('EventBridge', `Delivered to Lambda: ${match[1]}`);
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// SQS target
|
|
596
|
-
if (arn.includes(':sqs:') || arn.includes(':queue:')) {
|
|
597
|
-
if (!this.sqsService) return;
|
|
598
|
-
await this.sqsService.simulator.sendMessage({
|
|
599
|
-
QueueUrl: arn,
|
|
600
|
-
MessageBody: JSON.stringify(inputEvent)
|
|
601
|
-
});
|
|
602
|
-
this.logger.debug('EventBridge', `Delivered to SQS: ${arn}`);
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// SNS target
|
|
607
|
-
if (arn.includes(':sns:')) {
|
|
608
|
-
if (!this.snsService) return;
|
|
609
|
-
await this.snsService.simulator.publish({
|
|
610
|
-
TopicArn: arn,
|
|
611
|
-
Message: JSON.stringify(inputEvent)
|
|
612
|
-
});
|
|
613
|
-
this.logger.debug('EventBridge', `Delivered to SNS: ${arn}`);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
this.logger.warn('EventBridge', `Unsupported target ARN: ${arn}`);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Extract value at JSONPath
|
|
622
|
-
* @param {Object} obj
|
|
623
|
-
* @param {string} path
|
|
624
|
-
* @returns {*}
|
|
625
|
-
*/
|
|
626
|
-
_extractPath(obj, path) {
|
|
627
|
-
if (path === '$') return obj;
|
|
628
|
-
const parts = path.replace(/^\$\./, '').split('.');
|
|
629
|
-
let current = obj;
|
|
630
|
-
for (const part of parts) {
|
|
631
|
-
if (current === null || current === undefined) return null;
|
|
632
|
-
current = current[part];
|
|
633
|
-
}
|
|
634
|
-
return current;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Transform input using InputTransformer
|
|
639
|
-
* @param {Object} event
|
|
640
|
-
* @param {Object} transformer
|
|
641
|
-
* @returns {*}
|
|
642
|
-
*/
|
|
643
|
-
_transformInput(event, transformer) {
|
|
644
|
-
const { InputPathsMap, InputTemplate } = transformer;
|
|
645
|
-
let result = InputTemplate;
|
|
646
|
-
|
|
647
|
-
if (InputPathsMap && InputTemplate) {
|
|
648
|
-
for (const [key, path] of Object.entries(InputPathsMap)) {
|
|
649
|
-
const value = this._extractPath(event, path);
|
|
650
|
-
result = result.replace(new RegExp(`<${key}>`, 'g'), JSON.stringify(value));
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
try { return JSON.parse(result); } catch { return result; }
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Reset all data
|
|
659
|
-
*/
|
|
660
|
-
async reset() {
|
|
661
|
-
this.buses.clear();
|
|
662
|
-
this.rules.clear();
|
|
663
|
-
this.targets.clear();
|
|
664
|
-
this.eventArchive = [];
|
|
665
|
-
this._createDefaultBus();
|
|
666
|
-
await this._save();
|
|
667
|
-
this.logger.info('EventBridge', 'Data reset');
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Create AWS-formatted error
|
|
672
|
-
* @param {string} code
|
|
673
|
-
* @param {string} message
|
|
674
|
-
* @returns {Error}
|
|
675
|
-
*/
|
|
676
|
-
_error(code, message) {
|
|
677
|
-
const err = new Error(message);
|
|
678
|
-
err.code = code;
|
|
679
|
-
err.__type = code;
|
|
680
|
-
return err;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
module.exports = { EventBridgeSimulator };
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview EventBridge Simulator
|
|
3
|
+
* Simula o Amazon EventBridge com event buses, rules e targets
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* EventBridge Simulator
|
|
12
|
+
*/
|
|
13
|
+
class EventBridgeSimulator {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} config - Service configuration
|
|
16
|
+
* @param {Object} store - LocalStore instance
|
|
17
|
+
* @param {Object} logger - Logger instance
|
|
18
|
+
*/
|
|
19
|
+
constructor(config, store, logger) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.store = store;
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
|
|
24
|
+
/** @type {Map<string, Object>} Event buses */
|
|
25
|
+
this.buses = new Map();
|
|
26
|
+
/** @type {Map<string, Object>} Rules by ruleArn */
|
|
27
|
+
this.rules = new Map();
|
|
28
|
+
/** @type {Map<string, Object[]>} Targets by ruleArn */
|
|
29
|
+
this.targets = new Map();
|
|
30
|
+
/** @type {Array} Event archive (recent events) */
|
|
31
|
+
this.eventArchive = [];
|
|
32
|
+
|
|
33
|
+
this.region = 'us-east-1';
|
|
34
|
+
this.accountId = '123456789012';
|
|
35
|
+
|
|
36
|
+
// Services for target delivery
|
|
37
|
+
this.lambdaService = null;
|
|
38
|
+
this.sqsService = null;
|
|
39
|
+
this.snsService = null;
|
|
40
|
+
|
|
41
|
+
// Create default event bus
|
|
42
|
+
this._createDefaultBus();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create the default event bus
|
|
47
|
+
*/
|
|
48
|
+
_createDefaultBus() {
|
|
49
|
+
const defaultBus = {
|
|
50
|
+
Name: 'default',
|
|
51
|
+
Arn: `arn:aws:events:${this.region}:${this.accountId}:event-bus/default`,
|
|
52
|
+
State: 'ACTIVE',
|
|
53
|
+
CreationTime: new Date().toISOString()
|
|
54
|
+
};
|
|
55
|
+
this.buses.set('default', defaultBus);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {Object} s */ setLambdaService(s) { this.lambdaService = s; }
|
|
59
|
+
/** @param {Object} s */ setSqsService(s) { this.sqsService = s; }
|
|
60
|
+
/** @param {Object} s */ setSnsService(s) { this.snsService = s; }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load persisted data
|
|
64
|
+
*/
|
|
65
|
+
async load() {
|
|
66
|
+
try {
|
|
67
|
+
const buses = await this.store.read('eventbridge/buses');
|
|
68
|
+
if (Array.isArray(buses)) {
|
|
69
|
+
buses.forEach(b => this.buses.set(b.Name, b));
|
|
70
|
+
}
|
|
71
|
+
const rules = await this.store.read('eventbridge/rules');
|
|
72
|
+
if (Array.isArray(rules)) {
|
|
73
|
+
rules.forEach(r => this.rules.set(r.Arn, r));
|
|
74
|
+
}
|
|
75
|
+
const targets = await this.store.read('eventbridge/targets');
|
|
76
|
+
if (Array.isArray(targets)) {
|
|
77
|
+
targets.forEach(({ ruleArn, list }) => this.targets.set(ruleArn, list));
|
|
78
|
+
}
|
|
79
|
+
this.logger.debug('EventBridge', `Loaded ${this.buses.size} buses, ${this.rules.size} rules`);
|
|
80
|
+
} catch {
|
|
81
|
+
this.logger.debug('EventBridge', 'No persisted data, starting fresh');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Save data
|
|
87
|
+
*/
|
|
88
|
+
async _save() {
|
|
89
|
+
await this.store.write('eventbridge/buses', null, Array.from(this.buses.values()));
|
|
90
|
+
await this.store.write('eventbridge/rules', null, Array.from(this.rules.values()));
|
|
91
|
+
const targetsList = Array.from(this.targets.entries())
|
|
92
|
+
.map(([ruleArn, list]) => ({ ruleArn, list }));
|
|
93
|
+
await this.store.write('eventbridge/targets', null, targetsList);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ==================== Event Bus Operations ====================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* CreateEventBus
|
|
100
|
+
* @param {Object} params
|
|
101
|
+
* @returns {Object}
|
|
102
|
+
*/
|
|
103
|
+
async createEventBus(params) {
|
|
104
|
+
const { Name, EventSourceName, Tags } = params;
|
|
105
|
+
|
|
106
|
+
if (!Name) throw this._error('ValidationException', 'Name is required');
|
|
107
|
+
if (Name === 'default') throw this._error('ResourceAlreadyExistsException', 'Default event bus already exists');
|
|
108
|
+
if (this.buses.has(Name)) throw this._error('ResourceAlreadyExistsException', `Event bus ${Name} already exists`);
|
|
109
|
+
|
|
110
|
+
const bus = {
|
|
111
|
+
Name,
|
|
112
|
+
Arn: `arn:aws:events:${this.region}:${this.accountId}:event-bus/${Name}`,
|
|
113
|
+
State: 'ACTIVE',
|
|
114
|
+
EventSourceName: EventSourceName || null,
|
|
115
|
+
Tags: Tags || [],
|
|
116
|
+
CreationTime: new Date().toISOString()
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.buses.set(Name, bus);
|
|
120
|
+
await this._save();
|
|
121
|
+
|
|
122
|
+
this.logger.info('EventBridge', `Created event bus: ${Name}`);
|
|
123
|
+
return { EventBusArn: bus.Arn };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* DeleteEventBus
|
|
128
|
+
* @param {Object} params
|
|
129
|
+
*/
|
|
130
|
+
async deleteEventBus(params) {
|
|
131
|
+
const { Name } = params;
|
|
132
|
+
|
|
133
|
+
if (Name === 'default') throw this._error('ValidationException', 'Cannot delete default event bus');
|
|
134
|
+
if (!this.buses.has(Name)) throw this._error('ResourceNotFoundException', `Event bus ${Name} not found`);
|
|
135
|
+
|
|
136
|
+
// Delete all rules for this bus
|
|
137
|
+
for (const [arn, rule] of this.rules.entries()) {
|
|
138
|
+
if (rule.EventBusName === Name) {
|
|
139
|
+
this.rules.delete(arn);
|
|
140
|
+
this.targets.delete(arn);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.buses.delete(Name);
|
|
145
|
+
await this._save();
|
|
146
|
+
|
|
147
|
+
this.logger.info('EventBridge', `Deleted event bus: ${Name}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* ListEventBuses
|
|
152
|
+
* @param {Object} params
|
|
153
|
+
* @returns {Object}
|
|
154
|
+
*/
|
|
155
|
+
listEventBuses(params = {}) {
|
|
156
|
+
const { Limit = 100, NamePrefix, NextToken } = params;
|
|
157
|
+
let buses = Array.from(this.buses.values());
|
|
158
|
+
|
|
159
|
+
if (NamePrefix) {
|
|
160
|
+
buses = buses.filter(b => b.Name.startsWith(NamePrefix));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
EventBuses: buses.slice(0, Limit).map(b => ({
|
|
165
|
+
Name: b.Name,
|
|
166
|
+
Arn: b.Arn,
|
|
167
|
+
State: b.State
|
|
168
|
+
}))
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* DescribeEventBus
|
|
174
|
+
* @param {Object} params
|
|
175
|
+
* @returns {Object}
|
|
176
|
+
*/
|
|
177
|
+
describeEventBus(params = {}) {
|
|
178
|
+
const { Name = 'default' } = params;
|
|
179
|
+
const bus = this.buses.get(Name);
|
|
180
|
+
if (!bus) throw this._error('ResourceNotFoundException', `Event bus ${Name} not found`);
|
|
181
|
+
return bus;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ==================== Rules ====================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* PutRule
|
|
188
|
+
* @param {Object} params
|
|
189
|
+
* @returns {Object}
|
|
190
|
+
*/
|
|
191
|
+
async putRule(params) {
|
|
192
|
+
const {
|
|
193
|
+
Name, EventBusName = 'default', EventPattern, ScheduleExpression,
|
|
194
|
+
State = 'ENABLED', Description, RoleArn, Tags
|
|
195
|
+
} = params;
|
|
196
|
+
|
|
197
|
+
if (!Name) throw this._error('ValidationException', 'Name is required');
|
|
198
|
+
if (!EventPattern && !ScheduleExpression) {
|
|
199
|
+
throw this._error('ValidationException', 'Either EventPattern or ScheduleExpression is required');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const bus = this.buses.get(EventBusName);
|
|
203
|
+
if (!bus) throw this._error('ResourceNotFoundException', `Event bus ${EventBusName} not found`);
|
|
204
|
+
|
|
205
|
+
// Validate EventPattern
|
|
206
|
+
let parsedPattern = null;
|
|
207
|
+
if (EventPattern) {
|
|
208
|
+
try {
|
|
209
|
+
parsedPattern = JSON.parse(EventPattern);
|
|
210
|
+
} catch {
|
|
211
|
+
throw this._error('InvalidEventPatternException', 'Event pattern is not valid JSON');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
216
|
+
|
|
217
|
+
const rule = {
|
|
218
|
+
Name,
|
|
219
|
+
Arn: ruleArn,
|
|
220
|
+
EventBusName,
|
|
221
|
+
EventPattern: EventPattern || null,
|
|
222
|
+
ParsedPattern: parsedPattern,
|
|
223
|
+
ScheduleExpression: ScheduleExpression || null,
|
|
224
|
+
State,
|
|
225
|
+
Description: Description || '',
|
|
226
|
+
RoleArn: RoleArn || null,
|
|
227
|
+
Tags: Tags || [],
|
|
228
|
+
CreatedAt: new Date().toISOString()
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
this.rules.set(ruleArn, rule);
|
|
232
|
+
await this._save();
|
|
233
|
+
|
|
234
|
+
this.logger.info('EventBridge', `Put rule: ${Name} on bus ${EventBusName}`);
|
|
235
|
+
return { RuleArn: ruleArn };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* DeleteRule
|
|
240
|
+
* @param {Object} params
|
|
241
|
+
*/
|
|
242
|
+
async deleteRule(params) {
|
|
243
|
+
const { Name, EventBusName = 'default', Force } = params;
|
|
244
|
+
|
|
245
|
+
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
246
|
+
|
|
247
|
+
if (!this.rules.has(ruleArn)) {
|
|
248
|
+
throw this._error('ResourceNotFoundException', `Rule ${Name} not found`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check for targets unless Force
|
|
252
|
+
const ruleTargets = this.targets.get(ruleArn) || [];
|
|
253
|
+
if (ruleTargets.length > 0 && !Force) {
|
|
254
|
+
throw this._error('ValidationException', 'Rule has targets. Use Force=true to delete anyway');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.rules.delete(ruleArn);
|
|
258
|
+
this.targets.delete(ruleArn);
|
|
259
|
+
await this._save();
|
|
260
|
+
|
|
261
|
+
this.logger.info('EventBridge', `Deleted rule: ${Name}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* ListRules
|
|
266
|
+
* @param {Object} params
|
|
267
|
+
* @returns {Object}
|
|
268
|
+
*/
|
|
269
|
+
listRules(params = {}) {
|
|
270
|
+
const { EventBusName = 'default', NamePrefix, Limit = 100 } = params;
|
|
271
|
+
let rules = Array.from(this.rules.values())
|
|
272
|
+
.filter(r => r.EventBusName === EventBusName);
|
|
273
|
+
|
|
274
|
+
if (NamePrefix) {
|
|
275
|
+
rules = rules.filter(r => r.Name.startsWith(NamePrefix));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
Rules: rules.slice(0, Limit).map(r => ({
|
|
280
|
+
Name: r.Name,
|
|
281
|
+
Arn: r.Arn,
|
|
282
|
+
EventBusName: r.EventBusName,
|
|
283
|
+
EventPattern: r.EventPattern,
|
|
284
|
+
ScheduleExpression: r.ScheduleExpression,
|
|
285
|
+
State: r.State,
|
|
286
|
+
Description: r.Description
|
|
287
|
+
}))
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* DescribeRule
|
|
293
|
+
* @param {Object} params
|
|
294
|
+
* @returns {Object}
|
|
295
|
+
*/
|
|
296
|
+
describeRule(params) {
|
|
297
|
+
const { Name, EventBusName = 'default' } = params;
|
|
298
|
+
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
299
|
+
const rule = this.rules.get(ruleArn);
|
|
300
|
+
if (!rule) throw this._error('ResourceNotFoundException', `Rule ${Name} not found`);
|
|
301
|
+
return rule;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* EnableRule
|
|
306
|
+
* @param {Object} params
|
|
307
|
+
*/
|
|
308
|
+
async enableRule(params) {
|
|
309
|
+
const { Name, EventBusName = 'default' } = params;
|
|
310
|
+
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
311
|
+
const rule = this.rules.get(ruleArn);
|
|
312
|
+
if (!rule) throw this._error('ResourceNotFoundException', `Rule ${Name} not found`);
|
|
313
|
+
rule.State = 'ENABLED';
|
|
314
|
+
await this._save();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* DisableRule
|
|
319
|
+
* @param {Object} params
|
|
320
|
+
*/
|
|
321
|
+
async disableRule(params) {
|
|
322
|
+
const { Name, EventBusName = 'default' } = params;
|
|
323
|
+
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Name}`;
|
|
324
|
+
const rule = this.rules.get(ruleArn);
|
|
325
|
+
if (!rule) throw this._error('ResourceNotFoundException', `Rule ${Name} not found`);
|
|
326
|
+
rule.State = 'DISABLED';
|
|
327
|
+
await this._save();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ==================== Targets ====================
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* PutTargets
|
|
334
|
+
* @param {Object} params
|
|
335
|
+
* @returns {Object}
|
|
336
|
+
*/
|
|
337
|
+
async putTargets(params) {
|
|
338
|
+
const { Rule, EventBusName = 'default', Targets } = params;
|
|
339
|
+
|
|
340
|
+
if (!Rule) throw this._error('ValidationException', 'Rule is required');
|
|
341
|
+
if (!Targets || !Targets.length) throw this._error('ValidationException', 'Targets are required');
|
|
342
|
+
|
|
343
|
+
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Rule}`;
|
|
344
|
+
if (!this.rules.has(ruleArn)) throw this._error('ResourceNotFoundException', `Rule ${Rule} not found`);
|
|
345
|
+
|
|
346
|
+
const existing = this.targets.get(ruleArn) || [];
|
|
347
|
+
const failedEntries = [];
|
|
348
|
+
|
|
349
|
+
for (const target of Targets) {
|
|
350
|
+
if (!target.Id || !target.Arn) {
|
|
351
|
+
failedEntries.push({ TargetId: target.Id, ErrorCode: 'ValidationException', ErrorMessage: 'Id and Arn are required' });
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Remove existing target with same Id
|
|
356
|
+
const idx = existing.findIndex(t => t.Id === target.Id);
|
|
357
|
+
if (idx >= 0) existing.splice(idx, 1);
|
|
358
|
+
|
|
359
|
+
existing.push({
|
|
360
|
+
Id: target.Id,
|
|
361
|
+
Arn: target.Arn,
|
|
362
|
+
Input: target.Input || null,
|
|
363
|
+
InputPath: target.InputPath || null,
|
|
364
|
+
InputTransformer: target.InputTransformer || null,
|
|
365
|
+
RoleArn: target.RoleArn || null,
|
|
366
|
+
RetryPolicy: target.RetryPolicy || { MaximumRetryAttempts: 185, MaximumEventAgeInSeconds: 86400 },
|
|
367
|
+
DeadLetterConfig: target.DeadLetterConfig || null
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.targets.set(ruleArn, existing);
|
|
372
|
+
await this._save();
|
|
373
|
+
|
|
374
|
+
this.logger.info('EventBridge', `Put ${Targets.length} targets for rule ${Rule}`);
|
|
375
|
+
return {
|
|
376
|
+
FailedEntryCount: failedEntries.length,
|
|
377
|
+
FailedEntries: failedEntries
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* RemoveTargets
|
|
383
|
+
* @param {Object} params
|
|
384
|
+
* @returns {Object}
|
|
385
|
+
*/
|
|
386
|
+
async removeTargets(params) {
|
|
387
|
+
const { Rule, EventBusName = 'default', Ids } = params;
|
|
388
|
+
|
|
389
|
+
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Rule}`;
|
|
390
|
+
const existing = this.targets.get(ruleArn) || [];
|
|
391
|
+
|
|
392
|
+
const remaining = existing.filter(t => !Ids.includes(t.Id));
|
|
393
|
+
this.targets.set(ruleArn, remaining);
|
|
394
|
+
await this._save();
|
|
395
|
+
|
|
396
|
+
return { FailedEntryCount: 0, FailedEntries: [] };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* ListTargetsByRule
|
|
401
|
+
* @param {Object} params
|
|
402
|
+
* @returns {Object}
|
|
403
|
+
*/
|
|
404
|
+
listTargetsByRule(params) {
|
|
405
|
+
const { Rule, EventBusName = 'default' } = params;
|
|
406
|
+
const ruleArn = `arn:aws:events:${this.region}:${this.accountId}:rule/${EventBusName}/${Rule}`;
|
|
407
|
+
|
|
408
|
+
if (!this.rules.has(ruleArn)) throw this._error('ResourceNotFoundException', `Rule ${Rule} not found`);
|
|
409
|
+
|
|
410
|
+
const targets = this.targets.get(ruleArn) || [];
|
|
411
|
+
return { Targets: targets };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ==================== PutEvents ====================
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* PutEvents
|
|
418
|
+
* @param {Object} params
|
|
419
|
+
* @returns {Object}
|
|
420
|
+
*/
|
|
421
|
+
async putEvents(params) {
|
|
422
|
+
const { Entries } = params;
|
|
423
|
+
|
|
424
|
+
if (!Entries || !Entries.length) {
|
|
425
|
+
throw this._error('ValidationException', 'Entries are required');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const results = [];
|
|
429
|
+
const failedEntries = [];
|
|
430
|
+
|
|
431
|
+
for (const entry of Entries) {
|
|
432
|
+
const eventId = crypto.randomUUID();
|
|
433
|
+
|
|
434
|
+
// Validate entry
|
|
435
|
+
if (!entry.Source) {
|
|
436
|
+
failedEntries.push({ ErrorCode: 'ValidationException', ErrorMessage: 'Source is required' });
|
|
437
|
+
results.push({ EventId: null, ErrorCode: 'ValidationException' });
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const busName = entry.EventBusName || 'default';
|
|
442
|
+
if (!this.buses.has(busName)) {
|
|
443
|
+
failedEntries.push({ ErrorCode: 'ResourceNotFoundException', ErrorMessage: `Bus ${busName} not found` });
|
|
444
|
+
results.push({ EventId: null, ErrorCode: 'ResourceNotFoundException' });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Build event
|
|
449
|
+
let detail = entry.Detail;
|
|
450
|
+
if (typeof detail === 'string') {
|
|
451
|
+
try { detail = JSON.parse(detail); } catch { detail = {}; }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const event = {
|
|
455
|
+
id: eventId,
|
|
456
|
+
version: '0',
|
|
457
|
+
account: this.accountId,
|
|
458
|
+
time: entry.Time || new Date().toISOString(),
|
|
459
|
+
region: this.region,
|
|
460
|
+
source: entry.Source,
|
|
461
|
+
'detail-type': entry.DetailType || '',
|
|
462
|
+
resources: entry.Resources || [],
|
|
463
|
+
detail: detail || {}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// Archive event
|
|
467
|
+
this.eventArchive.push(event);
|
|
468
|
+
if (this.eventArchive.length > 1000) {
|
|
469
|
+
this.eventArchive.shift();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Match and deliver to rules
|
|
473
|
+
await this._matchAndDeliver(busName, event);
|
|
474
|
+
|
|
475
|
+
results.push({ EventId: eventId });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.logger.debug('EventBridge', `PutEvents: ${Entries.length} events, ${failedEntries.length} failed`);
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
FailedEntryCount: failedEntries.length,
|
|
482
|
+
Entries: results
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Match event to rules and deliver to targets
|
|
488
|
+
* @param {string} busName
|
|
489
|
+
* @param {Object} event
|
|
490
|
+
*/
|
|
491
|
+
async _matchAndDeliver(busName, event) {
|
|
492
|
+
const busRules = Array.from(this.rules.values())
|
|
493
|
+
.filter(r => r.EventBusName === busName && r.State === 'ENABLED');
|
|
494
|
+
|
|
495
|
+
for (const rule of busRules) {
|
|
496
|
+
if (rule.ParsedPattern && this._matchesPattern(event, rule.ParsedPattern)) {
|
|
497
|
+
const targets = this.targets.get(rule.Arn) || [];
|
|
498
|
+
for (const target of targets) {
|
|
499
|
+
try {
|
|
500
|
+
await this._deliverToTarget(target, event);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
this.logger.warn('EventBridge', `Target delivery failed: ${err.message}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Check if event matches pattern
|
|
511
|
+
* @param {Object} event
|
|
512
|
+
* @param {Object} pattern
|
|
513
|
+
* @returns {boolean}
|
|
514
|
+
*/
|
|
515
|
+
_matchesPattern(event, pattern) {
|
|
516
|
+
for (const [key, matchers] of Object.entries(pattern)) {
|
|
517
|
+
const eventValue = key === 'detail' ? event.detail : event[key];
|
|
518
|
+
|
|
519
|
+
if (key === 'detail' && typeof matchers === 'object' && !Array.isArray(matchers)) {
|
|
520
|
+
// Recursive detail matching
|
|
521
|
+
if (!this._matchesPattern(event.detail || {}, matchers)) return false;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!Array.isArray(matchers)) continue;
|
|
526
|
+
|
|
527
|
+
const matches = matchers.some(matcher => {
|
|
528
|
+
if (matcher === null) return eventValue === null;
|
|
529
|
+
if (typeof matcher === 'string') return eventValue === matcher;
|
|
530
|
+
if (typeof matcher === 'object') {
|
|
531
|
+
if (matcher.prefix) return String(eventValue).startsWith(matcher.prefix);
|
|
532
|
+
if (matcher['anything-but']) {
|
|
533
|
+
return !matcher['anything-but'].includes(eventValue);
|
|
534
|
+
}
|
|
535
|
+
if (matcher.exists !== undefined) {
|
|
536
|
+
return matcher.exists ? eventValue !== undefined : eventValue === undefined;
|
|
537
|
+
}
|
|
538
|
+
if (matcher.numeric) return this._checkNumeric(parseFloat(eventValue), matcher.numeric);
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
if (!matches) return false;
|
|
544
|
+
}
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Check numeric conditions
|
|
550
|
+
* @param {number} value
|
|
551
|
+
* @param {Array} conditions
|
|
552
|
+
* @returns {boolean}
|
|
553
|
+
*/
|
|
554
|
+
_checkNumeric(value, conditions) {
|
|
555
|
+
for (let i = 0; i < conditions.length; i += 2) {
|
|
556
|
+
const op = conditions[i];
|
|
557
|
+
const threshold = conditions[i + 1];
|
|
558
|
+
if (op === '=' && value !== threshold) return false;
|
|
559
|
+
if (op === '>' && value <= threshold) return false;
|
|
560
|
+
if (op === '>=' && value < threshold) return false;
|
|
561
|
+
if (op === '<' && value >= threshold) return false;
|
|
562
|
+
if (op === '<=' && value > threshold) return false;
|
|
563
|
+
}
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Deliver event to target
|
|
569
|
+
* @param {Object} target
|
|
570
|
+
* @param {Object} event
|
|
571
|
+
*/
|
|
572
|
+
async _deliverToTarget(target, event) {
|
|
573
|
+
// Transform input
|
|
574
|
+
let inputEvent = event;
|
|
575
|
+
if (target.Input) {
|
|
576
|
+
try { inputEvent = JSON.parse(target.Input); } catch { inputEvent = target.Input; }
|
|
577
|
+
} else if (target.InputPath) {
|
|
578
|
+
inputEvent = this._extractPath(event, target.InputPath);
|
|
579
|
+
} else if (target.InputTransformer) {
|
|
580
|
+
inputEvent = this._transformInput(event, target.InputTransformer);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const arn = target.Arn;
|
|
584
|
+
|
|
585
|
+
// Lambda target
|
|
586
|
+
if (arn.includes(':lambda:') || arn.includes('function:')) {
|
|
587
|
+
if (!this.lambdaService) return;
|
|
588
|
+
const match = arn.match(/function:([^:]+)/);
|
|
589
|
+
if (!match) return;
|
|
590
|
+
await this.lambdaService.simulator.invokeFunction(match[1], inputEvent);
|
|
591
|
+
this.logger.debug('EventBridge', `Delivered to Lambda: ${match[1]}`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// SQS target
|
|
596
|
+
if (arn.includes(':sqs:') || arn.includes(':queue:')) {
|
|
597
|
+
if (!this.sqsService) return;
|
|
598
|
+
await this.sqsService.simulator.sendMessage({
|
|
599
|
+
QueueUrl: arn,
|
|
600
|
+
MessageBody: JSON.stringify(inputEvent)
|
|
601
|
+
});
|
|
602
|
+
this.logger.debug('EventBridge', `Delivered to SQS: ${arn}`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// SNS target
|
|
607
|
+
if (arn.includes(':sns:')) {
|
|
608
|
+
if (!this.snsService) return;
|
|
609
|
+
await this.snsService.simulator.publish({
|
|
610
|
+
TopicArn: arn,
|
|
611
|
+
Message: JSON.stringify(inputEvent)
|
|
612
|
+
});
|
|
613
|
+
this.logger.debug('EventBridge', `Delivered to SNS: ${arn}`);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
this.logger.warn('EventBridge', `Unsupported target ARN: ${arn}`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Extract value at JSONPath
|
|
622
|
+
* @param {Object} obj
|
|
623
|
+
* @param {string} path
|
|
624
|
+
* @returns {*}
|
|
625
|
+
*/
|
|
626
|
+
_extractPath(obj, path) {
|
|
627
|
+
if (path === '$') return obj;
|
|
628
|
+
const parts = path.replace(/^\$\./, '').split('.');
|
|
629
|
+
let current = obj;
|
|
630
|
+
for (const part of parts) {
|
|
631
|
+
if (current === null || current === undefined) return null;
|
|
632
|
+
current = current[part];
|
|
633
|
+
}
|
|
634
|
+
return current;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Transform input using InputTransformer
|
|
639
|
+
* @param {Object} event
|
|
640
|
+
* @param {Object} transformer
|
|
641
|
+
* @returns {*}
|
|
642
|
+
*/
|
|
643
|
+
_transformInput(event, transformer) {
|
|
644
|
+
const { InputPathsMap, InputTemplate } = transformer;
|
|
645
|
+
let result = InputTemplate;
|
|
646
|
+
|
|
647
|
+
if (InputPathsMap && InputTemplate) {
|
|
648
|
+
for (const [key, path] of Object.entries(InputPathsMap)) {
|
|
649
|
+
const value = this._extractPath(event, path);
|
|
650
|
+
result = result.replace(new RegExp(`<${key}>`, 'g'), JSON.stringify(value));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
try { return JSON.parse(result); } catch { return result; }
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Reset all data
|
|
659
|
+
*/
|
|
660
|
+
async reset() {
|
|
661
|
+
this.buses.clear();
|
|
662
|
+
this.rules.clear();
|
|
663
|
+
this.targets.clear();
|
|
664
|
+
this.eventArchive = [];
|
|
665
|
+
this._createDefaultBus();
|
|
666
|
+
await this._save();
|
|
667
|
+
this.logger.info('EventBridge', 'Data reset');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Create AWS-formatted error
|
|
672
|
+
* @param {string} code
|
|
673
|
+
* @param {string} message
|
|
674
|
+
* @returns {Error}
|
|
675
|
+
*/
|
|
676
|
+
_error(code, message) {
|
|
677
|
+
const err = new Error(message);
|
|
678
|
+
err.code = code;
|
|
679
|
+
err.__type = code;
|
|
680
|
+
return err;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
module.exports = { EventBridgeSimulator };
|