@gugananuvem/aws-local-simulator 1.0.34 → 1.0.39
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 +662 -658
- package/package.json +11 -4
- package/src/config/default-config.js +71 -0
- package/src/config/env-loader.js +4 -2
- package/src/middleware/authorization.js +271 -0
- package/src/middleware/cloudtrail-logger.js +333 -0
- package/src/middleware/error-handler.js +138 -0
- package/src/middleware/index.js +79 -0
- package/src/middleware/policy-enforcement.js +407 -0
- package/src/server.js +74 -4
- package/src/services/acm/index.js +66 -0
- package/src/services/acm/models/certificate.js +202 -0
- package/src/services/acm/server.js +224 -0
- package/src/services/acm/simulator.js +293 -0
- package/src/services/alb/index.js +104 -0
- package/src/services/alb/models/loadBalancer.js +88 -0
- package/src/services/alb/models/targetGroup.js +128 -0
- package/src/services/alb/server.js +160 -0
- package/src/services/alb/simulator.js +262 -0
- package/src/services/apigateway/index.js +22 -0
- package/src/services/apigateway/server.js +247 -35
- package/src/services/apigateway/simulator.js +780 -26
- package/src/services/asg/index.js +100 -0
- package/src/services/asg/models/autoScalingGroup.js +110 -0
- package/src/services/asg/models/launchTemplate.js +41 -0
- package/src/services/asg/models/scalingPolicy.js +51 -0
- package/src/services/asg/server.js +236 -0
- package/src/services/asg/simulator.js +410 -0
- package/src/services/aurora/cluster-manager.js +731 -0
- package/src/services/aurora/data-api.js +514 -0
- package/src/services/aurora/db-engine.js +270 -0
- package/src/services/aurora/index.js +159 -0
- package/src/services/aurora/rds-api.js +189 -0
- package/src/services/aurora/server.js +213 -0
- package/src/services/aurora/sql-translator.js +334 -0
- package/src/services/cloudformation/index.js +2 -2
- package/src/services/cloudformation/server.js +1 -0
- package/src/services/cloudfront/cache-engine.js +225 -0
- package/src/services/cloudfront/geo-restrictions.js +377 -0
- package/src/services/cloudfront/index.js +220 -0
- package/src/services/cloudfront/models/behavior.js +158 -0
- package/src/services/cloudfront/models/distribution.js +284 -0
- package/src/services/cloudfront/models/index.js +99 -0
- package/src/services/cloudfront/models/invalidation.js +109 -0
- package/src/services/cloudfront/models/origin.js +155 -0
- package/src/services/cloudfront/origin-router.js +552 -0
- package/src/services/cloudfront/origin-validator.js +398 -0
- package/src/services/cloudfront/server.js +512 -0
- package/src/services/cloudfront/signed-url-generator.js +377 -0
- package/src/services/cloudfront/simulator.js +425 -0
- package/src/services/cloudfront/store.js +326 -0
- package/src/services/cloudshell/env-builder.js +27 -0
- package/src/services/cloudshell/index.js +48 -0
- package/src/services/cloudshell/routes.js +60 -0
- package/src/services/cloudshell/session-manager.js +226 -0
- package/src/services/cloudshell/static/.gitkeep +0 -0
- package/src/services/cloudshell/static/index.html +406 -0
- package/src/services/cloudshell/websocket-handler.js +107 -0
- package/src/services/cloudtrail/index.js +2 -3
- package/src/services/cloudtrail/server.js +68 -0
- package/src/services/cognito/server.js +4 -1
- package/src/services/dynamodb/server.js +4 -1
- package/src/services/dynamodb/simulator.js +2 -1
- package/src/services/ec2/index.js +101 -0
- package/src/services/ec2/instanceServer.js +311 -0
- package/src/services/ec2/models/ami.js +39 -0
- package/src/services/ec2/models/instance.js +98 -0
- package/src/services/ec2/models/networkInterface.js +74 -0
- package/src/services/ec2/models/securityGroup.js +71 -0
- package/src/services/ec2/server.js +350 -0
- package/src/services/ec2/simulator.js +436 -0
- package/src/services/eventbridge/server.js +36 -0
- package/src/services/eventbridge/simulator.js +230 -45
- package/src/services/iam/index.js +65 -0
- package/src/services/iam/middleware.js +99 -0
- package/src/services/iam/server.js +331 -0
- package/src/services/iam/simulator.js +1432 -0
- package/src/services/index.js +28 -0
- package/src/services/kinesis-analytics/index.js +105 -0
- package/src/services/kinesis-analytics/models/application.js +325 -0
- package/src/services/kinesis-analytics/models/index.js +12 -0
- package/src/services/kinesis-analytics/server.js +250 -0
- package/src/services/kinesis-analytics/simulator.js +407 -0
- package/src/services/kinesis-analytics/store.js +176 -0
- package/src/services/kinesis-data-streams/index.js +106 -0
- package/src/services/kinesis-data-streams/models/index.js +12 -0
- package/src/services/kinesis-data-streams/models/record.js +70 -0
- package/src/services/kinesis-data-streams/models/stream.js +280 -0
- package/src/services/kinesis-data-streams/server.js +255 -0
- package/src/services/kinesis-data-streams/simulator.js +514 -0
- package/src/services/kinesis-data-streams/store.js +163 -0
- package/src/services/kinesis-firehose/index.js +106 -0
- package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
- package/src/services/kinesis-firehose/models/index.js +10 -0
- package/src/services/kinesis-firehose/server.js +215 -0
- package/src/services/kinesis-firehose/simulator.js +388 -0
- package/src/services/kinesis-firehose/store.js +163 -0
- package/src/services/kms/server.js +4 -1
- package/src/services/lambda/handler-loader.js +50 -5
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +18 -12
- package/src/services/lambda/simulator.js +73 -17
- package/src/services/opensearch/index.js +105 -0
- package/src/services/opensearch/models/document.js +115 -0
- package/src/services/opensearch/models/index.js +73 -0
- package/src/services/opensearch/server.js +310 -0
- package/src/services/opensearch/simulator.js +605 -0
- package/src/services/opensearch/store.js +162 -0
- package/src/services/organizations/control-tower-simulator.js +321 -0
- package/src/services/organizations/control-tower-store.js +182 -0
- package/src/services/organizations/iam-integration.js +413 -0
- package/src/services/organizations/index.js +172 -0
- package/src/services/organizations/models/index.js +13 -0
- package/src/services/organizations/models/invitation.js +48 -0
- package/src/services/organizations/models/organization.js +566 -0
- package/src/services/organizations/models/role.js +244 -0
- package/src/services/organizations/models/scp.js +131 -0
- package/src/services/organizations/server.js +736 -0
- package/src/services/organizations/simulator.js +593 -0
- package/src/services/organizations/store.js +235 -0
- package/src/services/organizations/vpc-integration.js +453 -0
- package/src/services/route53/index.js +67 -0
- package/src/services/route53/models/healthCheck.js +101 -0
- package/src/services/route53/models/hostedZone.js +37 -0
- package/src/services/route53/models/resourceRecord.js +106 -0
- package/src/services/route53/server.js +841 -0
- package/src/services/route53/simulator.js +350 -0
- package/src/services/s3/index.js +62 -0
- package/src/services/s3/presigned-url-generator.js +337 -0
- package/src/services/s3/server.js +231 -0
- package/src/services/s3/simulator.js +551 -4
- package/src/services/sns/index.js +31 -0
- package/src/services/sns/server.js +9 -0
- package/src/services/sqs/index.js +38 -0
- package/src/services/sqs/server.js +57 -4
- package/src/services/sqs/simulator.js +423 -33
- package/src/services/sts/simulator.js +209 -17
- package/src/services/vpc/README.md +459 -0
- package/src/services/vpc/ec2ResponseFormatter.js +431 -0
- package/src/services/vpc/index.js +90 -0
- package/src/services/vpc/models/internetGateway.js +71 -0
- package/src/services/vpc/models/routeTable.js +160 -0
- package/src/services/vpc/models/subnet.js +48 -0
- package/src/services/vpc/models/vpc.js +37 -0
- package/src/services/vpc/models/vpcEndpoint.js +100 -0
- package/src/services/vpc/models/vpcGateway.js +134 -0
- package/src/services/vpc/server.js +836 -0
- package/src/services/vpc/simulator.js +613 -0
- package/src/utils/policy-cache.js +215 -0
- package/aws-config +0 -154
|
@@ -8,6 +8,7 @@ const path = require("path");
|
|
|
8
8
|
const LocalStore = require("../../utils/local-store");
|
|
9
9
|
const logger = require("../../utils/logger");
|
|
10
10
|
const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
11
|
+
const PresignedUrlGenerator = require("./presigned-url-generator");
|
|
11
12
|
|
|
12
13
|
class S3Simulator {
|
|
13
14
|
constructor(config) {
|
|
@@ -16,8 +17,20 @@ class S3Simulator {
|
|
|
16
17
|
this.store = new LocalStore(this.dataDir);
|
|
17
18
|
this.buckets = new Map();
|
|
18
19
|
this.audit = new CloudTrailAudit("s3.amazonaws.com");
|
|
20
|
+
this.lambdaService = null;
|
|
21
|
+
this.snsService = null;
|
|
22
|
+
this.sqsService = null;
|
|
23
|
+
this.eventbridgeService = null;
|
|
24
|
+
this.region = "us-east-1";
|
|
25
|
+
this.accountId = "000000000000";
|
|
26
|
+
this.presignedUrlGenerator = new PresignedUrlGenerator(config);
|
|
19
27
|
}
|
|
20
28
|
|
|
29
|
+
setLambdaService(service) { this.lambdaService = service; }
|
|
30
|
+
setSnsService(service) { this.snsService = service; }
|
|
31
|
+
setSqsService(service) { this.sqsService = service; }
|
|
32
|
+
setEventbridgeService(service) { this.eventbridgeService = service; }
|
|
33
|
+
|
|
21
34
|
async initialize() {
|
|
22
35
|
logger.debug("Inicializando S3 Simulator...");
|
|
23
36
|
this.loadBuckets();
|
|
@@ -49,6 +62,7 @@ class S3Simulator {
|
|
|
49
62
|
objects,
|
|
50
63
|
objectCount: data.objectCount || 0,
|
|
51
64
|
totalSize: data.totalSize || 0,
|
|
65
|
+
notificationConfiguration: data.notificationConfiguration || null,
|
|
52
66
|
});
|
|
53
67
|
}
|
|
54
68
|
}
|
|
@@ -57,13 +71,14 @@ class S3Simulator {
|
|
|
57
71
|
// Cria buckets definidos na config que ainda não existem
|
|
58
72
|
if (this.config.s3?.buckets) {
|
|
59
73
|
for (const bucketDef of this.config.s3.buckets) {
|
|
60
|
-
// suporta string simples ou objeto { name, region, websiteConfiguration }
|
|
74
|
+
// suporta string simples ou objeto { name, region, websiteConfiguration, notificationConfiguration }
|
|
61
75
|
const bucketName = typeof bucketDef === "string" ? bucketDef : bucketDef.name;
|
|
62
76
|
const region = typeof bucketDef === "object" ? (bucketDef.region || "us-east-1") : "us-east-1";
|
|
63
77
|
const websiteConfig = typeof bucketDef === "object" ? (bucketDef.websiteConfiguration || null) : null;
|
|
78
|
+
const notificationConfig = typeof bucketDef === "object" ? (bucketDef.notificationConfiguration || null) : null;
|
|
64
79
|
|
|
65
80
|
if (!this.buckets.has(bucketName)) {
|
|
66
|
-
this.createBucket(bucketName, { region, websiteConfiguration: websiteConfig });
|
|
81
|
+
this.createBucket(bucketName, { region, websiteConfiguration: websiteConfig, notificationConfiguration: notificationConfig });
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
}
|
|
@@ -111,7 +126,7 @@ class S3Simulator {
|
|
|
111
126
|
return { error: { code: "BucketAlreadyExists", message: "Bucket already exists" }, status: 409 };
|
|
112
127
|
}
|
|
113
128
|
|
|
114
|
-
const { region = "us-east-1", websiteConfiguration = null } = options;
|
|
129
|
+
const { region = "us-east-1", websiteConfiguration = null, notificationConfiguration = null } = options;
|
|
115
130
|
|
|
116
131
|
const bucket = {
|
|
117
132
|
name: bucketName,
|
|
@@ -127,11 +142,13 @@ class S3Simulator {
|
|
|
127
142
|
objects: new Map(),
|
|
128
143
|
objectCount: 0,
|
|
129
144
|
totalSize: 0,
|
|
145
|
+
notificationConfiguration: this._normalizeNotificationConfig(notificationConfiguration),
|
|
146
|
+
policy: null, // Bucket policy para controle de acesso
|
|
130
147
|
};
|
|
131
148
|
|
|
132
149
|
this.buckets.set(bucketName, bucket);
|
|
133
150
|
this.persistBuckets();
|
|
134
|
-
logger.debug(`✅ Bucket S3 criado: ${bucketName} (region: ${region}${websiteConfiguration ? ", website: enabled" : ""})`);
|
|
151
|
+
logger.debug(`✅ Bucket S3 criado: ${bucketName} (region: ${region}${websiteConfiguration ? ", website: enabled" : ""}${bucket.notificationConfiguration ? ", notifications: enabled" : ""})`);
|
|
135
152
|
this.audit.record({ eventName: "CreateBucket", readOnly: false, resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }], requestParameters: { bucketName } });
|
|
136
153
|
return { bucket };
|
|
137
154
|
}
|
|
@@ -152,6 +169,82 @@ class S3Simulator {
|
|
|
152
169
|
return { success: true };
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Definir política do bucket (bucket policy)
|
|
174
|
+
* Controla quem pode acessar o bucket e seus objetos
|
|
175
|
+
*/
|
|
176
|
+
putBucketPolicy(bucketName, policy) {
|
|
177
|
+
const bucket = this.buckets.get(bucketName);
|
|
178
|
+
if (!bucket) {
|
|
179
|
+
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Validar que é um JSON válido
|
|
184
|
+
const policyObject = typeof policy === 'string' ? JSON.parse(policy) : policy;
|
|
185
|
+
|
|
186
|
+
if (!policyObject.Statement || !Array.isArray(policyObject.Statement)) {
|
|
187
|
+
return { error: { code: "InvalidPolicyDocument", message: "Invalid bucket policy" }, status: 400 };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
bucket.policy = policyObject;
|
|
191
|
+
this.persistBuckets();
|
|
192
|
+
|
|
193
|
+
logger.debug(`📋 Política de bucket definida: ${bucketName}`);
|
|
194
|
+
this.audit.record({
|
|
195
|
+
eventName: "PutBucketPolicy",
|
|
196
|
+
readOnly: false,
|
|
197
|
+
resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }],
|
|
198
|
+
requestParameters: { bucketName, policy: policyObject }
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return { success: true };
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return { error: { code: "InvalidPolicyDocument", message: error.message }, status: 400 };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Obter política do bucket
|
|
209
|
+
*/
|
|
210
|
+
getBucketPolicy(bucketName) {
|
|
211
|
+
const bucket = this.buckets.get(bucketName);
|
|
212
|
+
if (!bucket) {
|
|
213
|
+
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!bucket.policy) {
|
|
217
|
+
return { error: { code: "NoSuchBucketPolicy", message: "The bucket policy does not exist" }, status: 404 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
policy: bucket.policy,
|
|
222
|
+
status: 200
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Deletar política do bucket
|
|
228
|
+
*/
|
|
229
|
+
deleteBucketPolicy(bucketName) {
|
|
230
|
+
const bucket = this.buckets.get(bucketName);
|
|
231
|
+
if (!bucket) {
|
|
232
|
+
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
bucket.policy = null;
|
|
236
|
+
this.persistBuckets();
|
|
237
|
+
|
|
238
|
+
logger.debug(`🗑️ Política de bucket deletada: ${bucketName}`);
|
|
239
|
+
this.audit.record({
|
|
240
|
+
eventName: "DeleteBucketPolicy",
|
|
241
|
+
readOnly: false,
|
|
242
|
+
resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }]
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return { success: true };
|
|
246
|
+
}
|
|
247
|
+
|
|
155
248
|
listBuckets() {
|
|
156
249
|
return Array.from(this.buckets.values()).map((bucket) => ({
|
|
157
250
|
Name: bucket.name,
|
|
@@ -180,6 +273,7 @@ class S3Simulator {
|
|
|
180
273
|
region: bucket.region,
|
|
181
274
|
website: bucket.website,
|
|
182
275
|
websiteConfiguration: bucket.websiteConfiguration,
|
|
276
|
+
notificationConfiguration: bucket.notificationConfiguration,
|
|
183
277
|
creationDate: bucket.creationDate,
|
|
184
278
|
objectCount: bucket.objectCount,
|
|
185
279
|
totalSize: bucket.totalSize,
|
|
@@ -267,6 +361,20 @@ class S3Simulator {
|
|
|
267
361
|
|
|
268
362
|
logger.verboso(`📤 Upload S3: ${bucketName}/${key} (${body.length} bytes)`);
|
|
269
363
|
this.audit.record({ eventName: "PutObject", readOnly: false, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key, contentType }, responseElements: { etag } });
|
|
364
|
+
|
|
365
|
+
this._triggerS3Event(bucketName, key, {
|
|
366
|
+
eventName: oldObject ? "ObjectCreated:Put" : "ObjectCreated:Put",
|
|
367
|
+
eventType: "ObjectCreated",
|
|
368
|
+
objectInfo: {
|
|
369
|
+
key,
|
|
370
|
+
size: body.length,
|
|
371
|
+
etag,
|
|
372
|
+
contentType,
|
|
373
|
+
metadata,
|
|
374
|
+
sequencer: crypto.randomBytes(8).toString("hex").toUpperCase(),
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
270
378
|
return { etag };
|
|
271
379
|
}
|
|
272
380
|
|
|
@@ -338,6 +446,19 @@ class S3Simulator {
|
|
|
338
446
|
this.persistBucket(bucketName);
|
|
339
447
|
logger.verboso(`🗑️ Delete S3: ${bucketName}/${key}`);
|
|
340
448
|
this.audit.record({ eventName: "DeleteObject", readOnly: false, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key } });
|
|
449
|
+
|
|
450
|
+
this._triggerS3Event(bucketName, key, {
|
|
451
|
+
eventName: "ObjectRemoved:Delete",
|
|
452
|
+
eventType: "ObjectRemoved",
|
|
453
|
+
objectInfo: {
|
|
454
|
+
key,
|
|
455
|
+
size: object.size,
|
|
456
|
+
etag: object.etag,
|
|
457
|
+
contentType: object.contentType,
|
|
458
|
+
metadata: object.metadata,
|
|
459
|
+
sequencer: crypto.randomBytes(8).toString("hex").toUpperCase(),
|
|
460
|
+
},
|
|
461
|
+
});
|
|
341
462
|
}
|
|
342
463
|
return { success: true };
|
|
343
464
|
}
|
|
@@ -397,6 +518,302 @@ class S3Simulator {
|
|
|
397
518
|
}));
|
|
398
519
|
}
|
|
399
520
|
|
|
521
|
+
// ─── Notification Configuration ───────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
_normalizeNotificationConfig(config) {
|
|
524
|
+
if (!config) return null;
|
|
525
|
+
const normalized = {
|
|
526
|
+
LambdaFunctionConfigurations: [],
|
|
527
|
+
QueueConfigurations: [],
|
|
528
|
+
TopicConfigurations: [],
|
|
529
|
+
};
|
|
530
|
+
const validEvents = [
|
|
531
|
+
"s3:ObjectCreated:*",
|
|
532
|
+
"s3:ObjectCreated:Put",
|
|
533
|
+
"s3:ObjectCreated:Post",
|
|
534
|
+
"s3:ObjectCreated:Copy",
|
|
535
|
+
"s3:ObjectCreated:CompleteMultipartUpload",
|
|
536
|
+
"s3:ObjectRemoved:*",
|
|
537
|
+
"s3:ObjectRemoved:Delete",
|
|
538
|
+
"s3:ObjectRemoved:DeleteMarkerCreated",
|
|
539
|
+
"s3:ObjectRestore:*",
|
|
540
|
+
"s3:ObjectRestore:Post",
|
|
541
|
+
"s3:ObjectRestore:Completed",
|
|
542
|
+
"s3:ReducedRedundancyLostObject",
|
|
543
|
+
"s3:Replication:*",
|
|
544
|
+
"s3:Replication:OperationFailedReplication",
|
|
545
|
+
"s3:Replication:OperationNotTracked",
|
|
546
|
+
"s3:Replication:OperationMissedThreshold",
|
|
547
|
+
"s3:Replication:OperationReplicatedAfterThreshold",
|
|
548
|
+
"s3:LifecycleExpiration:*",
|
|
549
|
+
"s3:LifecycleExpiration:Delete",
|
|
550
|
+
"s3:LifecycleExpiration:DeleteMarkerCreated",
|
|
551
|
+
"s3:LifecycleTransition",
|
|
552
|
+
"s3:IntelligentTiering",
|
|
553
|
+
"s3:ObjectTagging:*",
|
|
554
|
+
"s3:ObjectTagging:Put",
|
|
555
|
+
"s3:ObjectTagging:Delete",
|
|
556
|
+
"s3:ObjectAcl:Put",
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
if (Array.isArray(config.LambdaFunctionConfigurations)) {
|
|
560
|
+
for (const cfg of config.LambdaFunctionConfigurations) {
|
|
561
|
+
if (!cfg.LambdaFunctionArn) continue;
|
|
562
|
+
normalized.LambdaFunctionConfigurations.push({
|
|
563
|
+
Id: cfg.Id || crypto.randomUUID(),
|
|
564
|
+
LambdaFunctionArn: cfg.LambdaFunctionArn,
|
|
565
|
+
Events: cfg.Events?.filter((e) => validEvents.includes(e)) || ["s3:ObjectCreated:*"],
|
|
566
|
+
Filter: this._normalizeFilter(cfg.Filter),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (Array.isArray(config.QueueConfigurations)) {
|
|
571
|
+
for (const cfg of config.QueueConfigurations) {
|
|
572
|
+
if (!cfg.QueueArn) continue;
|
|
573
|
+
normalized.QueueConfigurations.push({
|
|
574
|
+
Id: cfg.Id || crypto.randomUUID(),
|
|
575
|
+
QueueArn: cfg.QueueArn,
|
|
576
|
+
Events: cfg.Events?.filter((e) => validEvents.includes(e)) || ["s3:ObjectCreated:*"],
|
|
577
|
+
Filter: this._normalizeFilter(cfg.Filter),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (Array.isArray(config.TopicConfigurations)) {
|
|
582
|
+
for (const cfg of config.TopicConfigurations) {
|
|
583
|
+
if (!cfg.TopicArn) continue;
|
|
584
|
+
normalized.TopicConfigurations.push({
|
|
585
|
+
Id: cfg.Id || crypto.randomUUID(),
|
|
586
|
+
TopicArn: cfg.TopicArn,
|
|
587
|
+
Events: cfg.Events?.filter((e) => validEvents.includes(e)) || ["s3:ObjectCreated:*"],
|
|
588
|
+
Filter: this._normalizeFilter(cfg.Filter),
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const hasAny =
|
|
594
|
+
normalized.LambdaFunctionConfigurations.length > 0 ||
|
|
595
|
+
normalized.QueueConfigurations.length > 0 ||
|
|
596
|
+
normalized.TopicConfigurations.length > 0;
|
|
597
|
+
return hasAny ? normalized : null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
_normalizeFilter(filter) {
|
|
601
|
+
if (!filter || !filter.Key) return null;
|
|
602
|
+
const rules = [];
|
|
603
|
+
if (Array.isArray(filter.Key.FilterRules)) {
|
|
604
|
+
for (const r of filter.Key.FilterRules) {
|
|
605
|
+
if (r.Name === "prefix" || r.Name === "suffix") {
|
|
606
|
+
rules.push({ Name: r.Name, Value: r.Value });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return rules.length > 0 ? { Key: { FilterRules: rules } } : null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
_matchesFilter(filter, key) {
|
|
614
|
+
if (!filter || !filter.Key || !Array.isArray(filter.Key.FilterRules)) return true;
|
|
615
|
+
for (const rule of filter.Key.FilterRules) {
|
|
616
|
+
const val = rule.Value || "";
|
|
617
|
+
if (rule.Name === "prefix" && !key.startsWith(val)) return false;
|
|
618
|
+
if (rule.Name === "suffix" && !key.endsWith(val)) return false;
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
_matchesEvent(configuredEvents, eventName, eventType) {
|
|
624
|
+
if (!Array.isArray(configuredEvents) || configuredEvents.length === 0) return true;
|
|
625
|
+
const s3EventName = `s3:${eventName}`;
|
|
626
|
+
const s3EventType = `s3:${eventType}:*`;
|
|
627
|
+
for (const ev of configuredEvents) {
|
|
628
|
+
if (ev === s3EventName) return true;
|
|
629
|
+
if (ev === s3EventType) return true;
|
|
630
|
+
if (ev === "s3:ObjectCreated:*" && eventType === "ObjectCreated") return true;
|
|
631
|
+
if (ev === "s3:ObjectRemoved:*" && eventType === "ObjectRemoved") return true;
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
_buildS3EventRecord(bucketName, key, ctx) {
|
|
637
|
+
const bucket = this.buckets.get(bucketName);
|
|
638
|
+
const region = bucket?.region || this.region;
|
|
639
|
+
const info = ctx.objectInfo || {};
|
|
640
|
+
return {
|
|
641
|
+
eventVersion: "2.1",
|
|
642
|
+
eventSource: "aws:s3",
|
|
643
|
+
awsRegion: region,
|
|
644
|
+
eventTime: new Date().toISOString(),
|
|
645
|
+
eventName: ctx.eventName,
|
|
646
|
+
userIdentity: { principalId: "AWSLocalSimulator" },
|
|
647
|
+
requestParameters: { sourceIPAddress: "127.0.0.1" },
|
|
648
|
+
responseElements: {
|
|
649
|
+
"x-amz-request-id": crypto.randomUUID(),
|
|
650
|
+
"x-amz-id-2": crypto.randomBytes(24).toString("base64"),
|
|
651
|
+
},
|
|
652
|
+
s3: {
|
|
653
|
+
s3SchemaVersion: "1.0",
|
|
654
|
+
configurationId: ctx.configId || "bucket-notification-config",
|
|
655
|
+
bucket: {
|
|
656
|
+
name: bucketName,
|
|
657
|
+
ownerIdentity: { principalId: "AIDAJDPLRKLG7UEXAMPLE" },
|
|
658
|
+
arn: `arn:aws:s3:::${bucketName}`,
|
|
659
|
+
},
|
|
660
|
+
object: {
|
|
661
|
+
key: encodeURIComponent(key),
|
|
662
|
+
size: info.size || 0,
|
|
663
|
+
eTag: info.etag ? `"${info.etag}"` : undefined,
|
|
664
|
+
versionId: undefined,
|
|
665
|
+
sequencer: info.sequencer || crypto.randomBytes(8).toString("hex").toUpperCase(),
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async _triggerS3Event(bucketName, key, ctx) {
|
|
672
|
+
try {
|
|
673
|
+
const bucket = this.buckets.get(bucketName);
|
|
674
|
+
if (!bucket) return;
|
|
675
|
+
|
|
676
|
+
const record = this._buildS3EventRecord(bucketName, key, ctx);
|
|
677
|
+
const s3EventPayload = { Records: [record] };
|
|
678
|
+
|
|
679
|
+
if (bucket.notificationConfiguration) {
|
|
680
|
+
for (const cfg of bucket.notificationConfiguration.LambdaFunctionConfigurations || []) {
|
|
681
|
+
if (!this._matchesEvent(cfg.Events, ctx.eventName, ctx.eventType)) continue;
|
|
682
|
+
if (!this._matchesFilter(cfg.Filter, key)) continue;
|
|
683
|
+
record.s3.configurationId = cfg.Id;
|
|
684
|
+
this._deliverToLambda(cfg.LambdaFunctionArn, { Records: [this._buildS3EventRecord(bucketName, key, { ...ctx, configId: cfg.Id })] });
|
|
685
|
+
}
|
|
686
|
+
for (const cfg of bucket.notificationConfiguration.QueueConfigurations || []) {
|
|
687
|
+
if (!this._matchesEvent(cfg.Events, ctx.eventName, ctx.eventType)) continue;
|
|
688
|
+
if (!this._matchesFilter(cfg.Filter, key)) continue;
|
|
689
|
+
this._deliverToSQS(cfg.QueueArn, { Records: [this._buildS3EventRecord(bucketName, key, { ...ctx, configId: cfg.Id })] });
|
|
690
|
+
}
|
|
691
|
+
for (const cfg of bucket.notificationConfiguration.TopicConfigurations || []) {
|
|
692
|
+
if (!this._matchesEvent(cfg.Events, ctx.eventName, ctx.eventType)) continue;
|
|
693
|
+
if (!this._matchesFilter(cfg.Filter, key)) continue;
|
|
694
|
+
this._deliverToSNS(cfg.TopicArn, { Records: [this._buildS3EventRecord(bucketName, key, { ...ctx, configId: cfg.Id })] });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
this._deliverToEventBridge(bucketName, ctx, s3EventPayload);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
logger.warn(`S3 Event trigger error (${bucketName}/${key}): ${err.message}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
_deliverToLambda(functionArn, event) {
|
|
705
|
+
if (!this.lambdaService?.simulator) return;
|
|
706
|
+
try {
|
|
707
|
+
const match = functionArn.match(/function:([^:]+)/);
|
|
708
|
+
const functionName = match ? match[1] : functionArn;
|
|
709
|
+
this.lambdaService.simulator.invokeFunction(functionName, event);
|
|
710
|
+
logger.verboso(`📡 S3 → Lambda: ${functionArn}`);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
logger.error(`❌ S3 → Lambda delivery failed: ${err.message}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
_deliverToSQS(queueArn, event) {
|
|
717
|
+
if (!this.sqsService?.simulator) return;
|
|
718
|
+
try {
|
|
719
|
+
const match = queueArn.match(/:([^:]+)$/);
|
|
720
|
+
const queueName = match ? match[1] : queueArn;
|
|
721
|
+
const queue = this.sqsService.simulator.queues?.get(queueName);
|
|
722
|
+
const queueUrl = queue?.url || queueArn;
|
|
723
|
+
this.sqsService.simulator.sendMessage({
|
|
724
|
+
QueueUrl: queueUrl,
|
|
725
|
+
MessageBody: JSON.stringify(event),
|
|
726
|
+
});
|
|
727
|
+
logger.verboso(`📡 S3 → SQS: ${queueArn}`);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
logger.error(`❌ S3 → SQS delivery failed: ${err.message}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
_deliverToSNS(topicArn, event) {
|
|
734
|
+
if (!this.snsService?.simulator) return;
|
|
735
|
+
try {
|
|
736
|
+
this.snsService.simulator.publish({
|
|
737
|
+
TopicArn: topicArn,
|
|
738
|
+
Message: JSON.stringify(event),
|
|
739
|
+
});
|
|
740
|
+
logger.verboso(`📡 S3 → SNS: ${topicArn}`);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
logger.error(`❌ S3 → SNS delivery failed: ${err.message}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
_deliverToEventBridge(bucketName, ctx, s3EventPayload) {
|
|
747
|
+
if (!this.eventbridgeService?.simulator) return;
|
|
748
|
+
try {
|
|
749
|
+
const bucket = this.buckets.get(bucketName);
|
|
750
|
+
const region = bucket?.region || this.region;
|
|
751
|
+
const objectKey = ctx.objectInfo?.key;
|
|
752
|
+
const detailType = ctx.eventType === "ObjectCreated"
|
|
753
|
+
? "Object Created"
|
|
754
|
+
: ctx.eventType === "ObjectRemoved"
|
|
755
|
+
? "Object Deleted"
|
|
756
|
+
: ctx.eventName;
|
|
757
|
+
const resources = [
|
|
758
|
+
`arn:aws:s3:::${bucketName}`,
|
|
759
|
+
];
|
|
760
|
+
if (objectKey) {
|
|
761
|
+
resources.push(`arn:aws:s3:::${bucketName}/${objectKey}`);
|
|
762
|
+
}
|
|
763
|
+
this.eventbridgeService.simulator.putEvents({
|
|
764
|
+
Entries: [{
|
|
765
|
+
Time: new Date().toISOString(),
|
|
766
|
+
Source: "aws.s3",
|
|
767
|
+
Resources: resources,
|
|
768
|
+
DetailType: detailType,
|
|
769
|
+
Detail: JSON.stringify({
|
|
770
|
+
version: "0",
|
|
771
|
+
bucket: { name: bucketName },
|
|
772
|
+
object: {
|
|
773
|
+
key: objectKey,
|
|
774
|
+
size: ctx.objectInfo?.size,
|
|
775
|
+
etag: ctx.objectInfo?.etag,
|
|
776
|
+
"version-id": ctx.objectInfo?.versionId,
|
|
777
|
+
sequencer: ctx.objectInfo?.sequencer,
|
|
778
|
+
},
|
|
779
|
+
"request-id": crypto.randomUUID(),
|
|
780
|
+
requester: this.accountId,
|
|
781
|
+
"source-ip-address": "127.0.0.1",
|
|
782
|
+
reason: ctx.eventName,
|
|
783
|
+
}),
|
|
784
|
+
EventBusName: "default",
|
|
785
|
+
}],
|
|
786
|
+
});
|
|
787
|
+
logger.verboso(`📡 S3 → EventBridge (${detailType})`);
|
|
788
|
+
} catch (err) {
|
|
789
|
+
logger.error(`❌ S3 → EventBridge delivery failed: ${err.message}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
getBucketNotificationConfiguration(bucketName) {
|
|
794
|
+
const bucket = this.buckets.get(bucketName);
|
|
795
|
+
if (!bucket) {
|
|
796
|
+
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
797
|
+
}
|
|
798
|
+
const nc = bucket.notificationConfiguration || {
|
|
799
|
+
LambdaFunctionConfigurations: [],
|
|
800
|
+
QueueConfigurations: [],
|
|
801
|
+
TopicConfigurations: [],
|
|
802
|
+
};
|
|
803
|
+
return { notificationConfiguration: nc };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
putBucketNotificationConfiguration(bucketName, config) {
|
|
807
|
+
const bucket = this.buckets.get(bucketName);
|
|
808
|
+
if (!bucket) {
|
|
809
|
+
return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
|
|
810
|
+
}
|
|
811
|
+
bucket.notificationConfiguration = this._normalizeNotificationConfig(config);
|
|
812
|
+
this.persistBuckets();
|
|
813
|
+
logger.debug(`📢 Notification config atualizada: ${bucketName}`);
|
|
814
|
+
return { success: true };
|
|
815
|
+
}
|
|
816
|
+
|
|
400
817
|
// ─── Website Config ───────────────────────────────────────────────────────
|
|
401
818
|
|
|
402
819
|
getWebsiteConfig(bucketName) {
|
|
@@ -464,6 +881,7 @@ class S3Simulator {
|
|
|
464
881
|
region: bucket.region || "us-east-1",
|
|
465
882
|
website: bucket.website || false,
|
|
466
883
|
websiteConfiguration: bucket.websiteConfiguration || null,
|
|
884
|
+
notificationConfiguration: bucket.notificationConfiguration || null,
|
|
467
885
|
objects: objectsObj,
|
|
468
886
|
objectCount: bucket.objectCount,
|
|
469
887
|
totalSize: bucket.totalSize,
|
|
@@ -563,6 +981,135 @@ class S3Simulator {
|
|
|
563
981
|
<HostId>local-simulator</HostId>
|
|
564
982
|
</Error>`;
|
|
565
983
|
}
|
|
984
|
+
|
|
985
|
+
// ─── Presigned URLs ──────────────────────────────────────────────────────
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Generate a presigned GET URL for downloading an object
|
|
989
|
+
* @param {string} bucketName - Bucket name
|
|
990
|
+
* @param {string} key - Object key
|
|
991
|
+
* @param {object} options - Options { expiresIn, credentials }
|
|
992
|
+
* @returns {object} { url, presignedUrl, expiration }
|
|
993
|
+
*/
|
|
994
|
+
generatePresignedGetUrl(bucketName, key, options = {}) {
|
|
995
|
+
const bucket = this.getBucket(bucketName);
|
|
996
|
+
if (!bucket) {
|
|
997
|
+
return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
try {
|
|
1001
|
+
const result = this.presignedUrlGenerator.generateGetUrl(bucketName, key, options);
|
|
1002
|
+
return { presignedUrl: result };
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
logger.error('S3: Error generating presigned GET URL:', err);
|
|
1005
|
+
return { error: { code: 'InternalError', message: err.message }, status: 500 };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Generate a presigned PUT URL for uploading an object
|
|
1011
|
+
* @param {string} bucketName - Bucket name
|
|
1012
|
+
* @param {string} key - Object key
|
|
1013
|
+
* @param {object} options - Options { expiresIn, credentials, contentType }
|
|
1014
|
+
* @returns {object} { presignedUrl, expiration }
|
|
1015
|
+
*/
|
|
1016
|
+
generatePresignedPutUrl(bucketName, key, options = {}) {
|
|
1017
|
+
const bucket = this.getBucket(bucketName);
|
|
1018
|
+
if (!bucket) {
|
|
1019
|
+
return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
const result = this.presignedUrlGenerator.generatePutUrl(bucketName, key, options);
|
|
1024
|
+
return { presignedUrl: result };
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
logger.error('S3: Error generating presigned PUT URL:', err);
|
|
1027
|
+
return { error: { code: 'InternalError', message: err.message }, status: 500 };
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Generate a presigned DELETE URL for deleting an object
|
|
1033
|
+
* @param {string} bucketName - Bucket name
|
|
1034
|
+
* @param {string} key - Object key
|
|
1035
|
+
* @param {object} options - Options { expiresIn, credentials }
|
|
1036
|
+
* @returns {object} { presignedUrl, expiration }
|
|
1037
|
+
*/
|
|
1038
|
+
generatePresignedDeleteUrl(bucketName, key, options = {}) {
|
|
1039
|
+
const bucket = this.getBucket(bucketName);
|
|
1040
|
+
if (!bucket) {
|
|
1041
|
+
return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
try {
|
|
1045
|
+
const result = this.presignedUrlGenerator.generateDeleteUrl(bucketName, key, options);
|
|
1046
|
+
return { presignedUrl: result };
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
logger.error('S3: Error generating presigned DELETE URL:', err);
|
|
1049
|
+
return { error: { code: 'InternalError', message: err.message }, status: 500 };
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Generate a presigned HEAD URL for checking object metadata
|
|
1055
|
+
* @param {string} bucketName - Bucket name
|
|
1056
|
+
* @param {string} key - Object key
|
|
1057
|
+
* @param {object} options - Options { expiresIn, credentials }
|
|
1058
|
+
* @returns {object} { presignedUrl, expiration }
|
|
1059
|
+
*/
|
|
1060
|
+
generatePresignedHeadUrl(bucketName, key, options = {}) {
|
|
1061
|
+
const bucket = this.getBucket(bucketName);
|
|
1062
|
+
if (!bucket) {
|
|
1063
|
+
return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
try {
|
|
1067
|
+
const result = this.presignedUrlGenerator.generateHeadUrl(bucketName, key, options);
|
|
1068
|
+
return { presignedUrl: result };
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
logger.error('S3: Error generating presigned HEAD URL:', err);
|
|
1071
|
+
return { error: { code: 'InternalError', message: err.message }, status: 500 };
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Generate a presigned POST policy for form uploads
|
|
1077
|
+
* @param {string} bucketName - Bucket name
|
|
1078
|
+
* @param {string} key - Object key
|
|
1079
|
+
* @param {object} options - Options { expiresIn, conditions, contentType }
|
|
1080
|
+
* @returns {object} { policy, signature, amzAlgorithm, amzDate, amzCredential }
|
|
1081
|
+
*/
|
|
1082
|
+
generatePresignedPostPolicy(bucketName, key, options = {}) {
|
|
1083
|
+
const bucket = this.getBucket(bucketName);
|
|
1084
|
+
if (!bucket) {
|
|
1085
|
+
return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
try {
|
|
1089
|
+
const result = this.presignedUrlGenerator.generatePostPolicy(bucketName, key, options);
|
|
1090
|
+
return result;
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
logger.error('S3: Error generating presigned POST policy:', err);
|
|
1093
|
+
return { error: { code: 'InternalError', message: err.message }, status: 500 };
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Validate a presigned URL
|
|
1099
|
+
* @param {string} url - Presigned URL to validate
|
|
1100
|
+
* @param {string} method - HTTP method (GET, PUT, DELETE, HEAD)
|
|
1101
|
+
* @param {object} credentials - Optional credentials for validation
|
|
1102
|
+
* @returns {object} { valid: boolean, message: string, expiresAt: Date }
|
|
1103
|
+
*/
|
|
1104
|
+
validatePresignedUrl(url, method = 'GET', credentials = null) {
|
|
1105
|
+
try {
|
|
1106
|
+
const result = this.presignedUrlGenerator.validatePresignedUrl(url, method, credentials);
|
|
1107
|
+
return result;
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
logger.error('S3: Error validating presigned URL:', err);
|
|
1110
|
+
return { valid: false, message: err.message };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
566
1113
|
}
|
|
567
1114
|
|
|
568
1115
|
module.exports = S3Simulator;
|
|
@@ -41,6 +41,37 @@ class SNSService {
|
|
|
41
41
|
if (sqs) this.simulator.setSqsService(sqs);
|
|
42
42
|
const ct = server.getService('cloudtrail');
|
|
43
43
|
if (ct?.simulator) this.simulator.audit.setTrail(ct.simulator);
|
|
44
|
+
|
|
45
|
+
// IAM enforcement middleware integration
|
|
46
|
+
const iamService = server.getService('iam');
|
|
47
|
+
if (iamService && iamService.getSimulator()) {
|
|
48
|
+
const { createIAMEnforcementMiddleware } = require('../iam/middleware');
|
|
49
|
+
const iamSimulator = iamService.getSimulator();
|
|
50
|
+
|
|
51
|
+
const middleware = createIAMEnforcementMiddleware({
|
|
52
|
+
iamSimulator,
|
|
53
|
+
servicePrefix: 'sns',
|
|
54
|
+
extractAction: (req) => req.body?.Action || req.query?.Action || 'Unknown',
|
|
55
|
+
extractResource: (req) => {
|
|
56
|
+
const topicArn = req.body?.TopicArn || req.query?.TopicArn || '';
|
|
57
|
+
if (topicArn) {
|
|
58
|
+
return topicArn; // Already an ARN
|
|
59
|
+
}
|
|
60
|
+
const topicName = req.body?.Name || req.body?.TopicName || 'unknown';
|
|
61
|
+
return `arn:aws:sns:us-east-1:123456789012:${topicName}`;
|
|
62
|
+
},
|
|
63
|
+
formatError: (res, code, message) => {
|
|
64
|
+
res.status(403).set('Content-Type', 'text/xml').send(
|
|
65
|
+
`<ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/"><Error><Type>Sender</Type><Code>${code}</Code><Message>${message}</Message></Error></ErrorResponse>`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Set middleware on the app so it's applied before routes
|
|
71
|
+
if (this.app) {
|
|
72
|
+
this.app.iamMiddleware = middleware;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
44
75
|
}
|
|
45
76
|
|
|
46
77
|
async start() {
|