@gugananuvem/aws-local-simulator 1.0.35 → 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.
Files changed (149) hide show
  1. package/README.md +662 -658
  2. package/package.json +10 -3
  3. package/src/config/default-config.js +71 -0
  4. package/src/config/env-loader.js +4 -2
  5. package/src/middleware/authorization.js +271 -0
  6. package/src/middleware/cloudtrail-logger.js +333 -0
  7. package/src/middleware/error-handler.js +138 -0
  8. package/src/middleware/index.js +79 -0
  9. package/src/middleware/policy-enforcement.js +407 -0
  10. package/src/server.js +74 -4
  11. package/src/services/acm/index.js +66 -0
  12. package/src/services/acm/models/certificate.js +202 -0
  13. package/src/services/acm/server.js +224 -0
  14. package/src/services/acm/simulator.js +293 -0
  15. package/src/services/alb/index.js +104 -0
  16. package/src/services/alb/models/loadBalancer.js +88 -0
  17. package/src/services/alb/models/targetGroup.js +128 -0
  18. package/src/services/alb/server.js +160 -0
  19. package/src/services/alb/simulator.js +262 -0
  20. package/src/services/apigateway/index.js +22 -0
  21. package/src/services/apigateway/server.js +247 -35
  22. package/src/services/apigateway/simulator.js +780 -26
  23. package/src/services/asg/index.js +100 -0
  24. package/src/services/asg/models/autoScalingGroup.js +110 -0
  25. package/src/services/asg/models/launchTemplate.js +41 -0
  26. package/src/services/asg/models/scalingPolicy.js +51 -0
  27. package/src/services/asg/server.js +236 -0
  28. package/src/services/asg/simulator.js +410 -0
  29. package/src/services/aurora/cluster-manager.js +731 -0
  30. package/src/services/aurora/data-api.js +514 -0
  31. package/src/services/aurora/db-engine.js +270 -0
  32. package/src/services/aurora/index.js +159 -0
  33. package/src/services/aurora/rds-api.js +189 -0
  34. package/src/services/aurora/server.js +213 -0
  35. package/src/services/aurora/sql-translator.js +334 -0
  36. package/src/services/cloudformation/index.js +2 -2
  37. package/src/services/cloudformation/server.js +1 -0
  38. package/src/services/cloudfront/cache-engine.js +225 -0
  39. package/src/services/cloudfront/geo-restrictions.js +377 -0
  40. package/src/services/cloudfront/index.js +220 -0
  41. package/src/services/cloudfront/models/behavior.js +158 -0
  42. package/src/services/cloudfront/models/distribution.js +284 -0
  43. package/src/services/cloudfront/models/index.js +99 -0
  44. package/src/services/cloudfront/models/invalidation.js +109 -0
  45. package/src/services/cloudfront/models/origin.js +155 -0
  46. package/src/services/cloudfront/origin-router.js +552 -0
  47. package/src/services/cloudfront/origin-validator.js +398 -0
  48. package/src/services/cloudfront/server.js +512 -0
  49. package/src/services/cloudfront/signed-url-generator.js +377 -0
  50. package/src/services/cloudfront/simulator.js +425 -0
  51. package/src/services/cloudfront/store.js +326 -0
  52. package/src/services/cloudshell/env-builder.js +27 -0
  53. package/src/services/cloudshell/index.js +48 -0
  54. package/src/services/cloudshell/routes.js +60 -0
  55. package/src/services/cloudshell/session-manager.js +226 -0
  56. package/src/services/cloudshell/static/.gitkeep +0 -0
  57. package/src/services/cloudshell/static/index.html +406 -0
  58. package/src/services/cloudshell/websocket-handler.js +107 -0
  59. package/src/services/cloudtrail/index.js +2 -3
  60. package/src/services/cloudtrail/server.js +68 -0
  61. package/src/services/cognito/server.js +4 -1
  62. package/src/services/dynamodb/server.js +4 -1
  63. package/src/services/dynamodb/simulator.js +2 -1
  64. package/src/services/ec2/index.js +101 -0
  65. package/src/services/ec2/instanceServer.js +311 -0
  66. package/src/services/ec2/models/ami.js +39 -0
  67. package/src/services/ec2/models/instance.js +98 -0
  68. package/src/services/ec2/models/networkInterface.js +74 -0
  69. package/src/services/ec2/models/securityGroup.js +71 -0
  70. package/src/services/ec2/server.js +350 -0
  71. package/src/services/ec2/simulator.js +436 -0
  72. package/src/services/eventbridge/server.js +36 -0
  73. package/src/services/eventbridge/simulator.js +230 -45
  74. package/src/services/iam/index.js +65 -0
  75. package/src/services/iam/middleware.js +99 -0
  76. package/src/services/iam/server.js +331 -0
  77. package/src/services/iam/simulator.js +1432 -0
  78. package/src/services/index.js +28 -0
  79. package/src/services/kinesis-analytics/index.js +105 -0
  80. package/src/services/kinesis-analytics/models/application.js +325 -0
  81. package/src/services/kinesis-analytics/models/index.js +12 -0
  82. package/src/services/kinesis-analytics/server.js +250 -0
  83. package/src/services/kinesis-analytics/simulator.js +407 -0
  84. package/src/services/kinesis-analytics/store.js +176 -0
  85. package/src/services/kinesis-data-streams/index.js +106 -0
  86. package/src/services/kinesis-data-streams/models/index.js +12 -0
  87. package/src/services/kinesis-data-streams/models/record.js +70 -0
  88. package/src/services/kinesis-data-streams/models/stream.js +280 -0
  89. package/src/services/kinesis-data-streams/server.js +255 -0
  90. package/src/services/kinesis-data-streams/simulator.js +514 -0
  91. package/src/services/kinesis-data-streams/store.js +163 -0
  92. package/src/services/kinesis-firehose/index.js +106 -0
  93. package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
  94. package/src/services/kinesis-firehose/models/index.js +10 -0
  95. package/src/services/kinesis-firehose/server.js +215 -0
  96. package/src/services/kinesis-firehose/simulator.js +388 -0
  97. package/src/services/kinesis-firehose/store.js +163 -0
  98. package/src/services/kms/server.js +4 -1
  99. package/src/services/lambda/index.js +26 -0
  100. package/src/services/lambda/server.js +12 -1
  101. package/src/services/lambda/simulator.js +4 -0
  102. package/src/services/opensearch/index.js +105 -0
  103. package/src/services/opensearch/models/document.js +115 -0
  104. package/src/services/opensearch/models/index.js +73 -0
  105. package/src/services/opensearch/server.js +310 -0
  106. package/src/services/opensearch/simulator.js +605 -0
  107. package/src/services/opensearch/store.js +162 -0
  108. package/src/services/organizations/control-tower-simulator.js +321 -0
  109. package/src/services/organizations/control-tower-store.js +182 -0
  110. package/src/services/organizations/iam-integration.js +413 -0
  111. package/src/services/organizations/index.js +172 -0
  112. package/src/services/organizations/models/index.js +13 -0
  113. package/src/services/organizations/models/invitation.js +48 -0
  114. package/src/services/organizations/models/organization.js +566 -0
  115. package/src/services/organizations/models/role.js +244 -0
  116. package/src/services/organizations/models/scp.js +131 -0
  117. package/src/services/organizations/server.js +736 -0
  118. package/src/services/organizations/simulator.js +593 -0
  119. package/src/services/organizations/store.js +235 -0
  120. package/src/services/organizations/vpc-integration.js +453 -0
  121. package/src/services/route53/index.js +67 -0
  122. package/src/services/route53/models/healthCheck.js +101 -0
  123. package/src/services/route53/models/hostedZone.js +37 -0
  124. package/src/services/route53/models/resourceRecord.js +106 -0
  125. package/src/services/route53/server.js +841 -0
  126. package/src/services/route53/simulator.js +350 -0
  127. package/src/services/s3/index.js +62 -0
  128. package/src/services/s3/presigned-url-generator.js +337 -0
  129. package/src/services/s3/server.js +231 -0
  130. package/src/services/s3/simulator.js +551 -4
  131. package/src/services/sns/index.js +31 -0
  132. package/src/services/sns/server.js +9 -0
  133. package/src/services/sqs/index.js +38 -0
  134. package/src/services/sqs/server.js +57 -4
  135. package/src/services/sqs/simulator.js +423 -33
  136. package/src/services/sts/simulator.js +209 -17
  137. package/src/services/vpc/README.md +459 -0
  138. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  139. package/src/services/vpc/index.js +90 -0
  140. package/src/services/vpc/models/internetGateway.js +71 -0
  141. package/src/services/vpc/models/routeTable.js +160 -0
  142. package/src/services/vpc/models/subnet.js +48 -0
  143. package/src/services/vpc/models/vpc.js +37 -0
  144. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  145. package/src/services/vpc/models/vpcGateway.js +134 -0
  146. package/src/services/vpc/server.js +836 -0
  147. package/src/services/vpc/simulator.js +613 -0
  148. package/src/utils/policy-cache.js +215 -0
  149. package/aws-config +0 -154
@@ -0,0 +1,337 @@
1
+ /**
2
+ * S3 Presigned URL Generator
3
+ * Gera URLs assinadas para upload, download e operações custom
4
+ * Permite acesso temporário a objetos S3 sem credenciais
5
+ */
6
+
7
+ const crypto = require('crypto');
8
+
9
+ class PresignedUrlGenerator {
10
+ constructor(config = {}) {
11
+ this.signatureVersion = 'AWS4-HMAC-SHA256';
12
+ this.algorithm = 'AWS4-HMAC-SHA256';
13
+ this.service = 's3';
14
+ this.region = config.region || 'us-east-1';
15
+ this.s3Endpoint = config.s3Endpoint || 'http://localhost:4566';
16
+ }
17
+
18
+ /**
19
+ * Gerar presigned URL para GET (download)
20
+ * @param {string} bucket - Nome do bucket
21
+ * @param {string} key - Chave/path do objeto
22
+ * @param {object} options - Opções
23
+ * @param {number} [options.expiresIn] - Tempo de expiração em segundos (default: 3600)
24
+ * @param {object} [options.credentials] - AWS credentials {accessKeyId, secretAccessKey}
25
+ * @returns {string} Presigned URL
26
+ */
27
+ generateGetUrl(bucket, key, options = {}) {
28
+ const expiresIn = options.expiresIn || 3600;
29
+ const credentials = options.credentials || this._getDefaultCredentials();
30
+
31
+ return this._generatePresignedUrl('GET', bucket, key, expiresIn, credentials, {});
32
+ }
33
+
34
+ /**
35
+ * Gerar presigned URL para PUT (upload)
36
+ * @param {string} bucket - Nome do bucket
37
+ * @param {string} key - Chave/path do objeto
38
+ * @param {object} options - Opções
39
+ * @param {number} [options.expiresIn] - Tempo de expiração em segundos (default: 3600)
40
+ * @param {object} [options.credentials] - AWS credentials
41
+ * @param {string} [options.contentType] - Content-Type esperado
42
+ * @param {object} [options.metadata] - Metadados (x-amz-meta-*)
43
+ * @param {string} [options.serverSideEncryption] - Encryption type (AES256, aws:kms)
44
+ * @returns {string} Presigned URL
45
+ */
46
+ generatePutUrl(bucket, key, options = {}) {
47
+ const expiresIn = options.expiresIn || 3600;
48
+ const credentials = options.credentials || this._getDefaultCredentials();
49
+ const conditions = {};
50
+
51
+ // Adicionar condições para validação
52
+ if (options.contentType) {
53
+ conditions['content-type'] = options.contentType;
54
+ }
55
+
56
+ if (options.serverSideEncryption) {
57
+ conditions['x-amz-server-side-encryption'] = options.serverSideEncryption;
58
+ }
59
+
60
+ if (options.metadata) {
61
+ Object.entries(options.metadata).forEach(([key, value]) => {
62
+ conditions[`x-amz-meta-${key}`] = value;
63
+ });
64
+ }
65
+
66
+ return this._generatePresignedUrl('PUT', bucket, key, expiresIn, credentials, conditions);
67
+ }
68
+
69
+ /**
70
+ * Gerar presigned URL para DELETE
71
+ * @param {string} bucket - Nome do bucket
72
+ * @param {string} key - Chave/path do objeto
73
+ * @param {object} options - Opções
74
+ * @param {number} [options.expiresIn] - Tempo de expiração em segundos
75
+ * @param {object} [options.credentials] - AWS credentials
76
+ * @returns {string} Presigned URL
77
+ */
78
+ generateDeleteUrl(bucket, key, options = {}) {
79
+ const expiresIn = options.expiresIn || 3600;
80
+ const credentials = options.credentials || this._getDefaultCredentials();
81
+
82
+ return this._generatePresignedUrl('DELETE', bucket, key, expiresIn, credentials, {});
83
+ }
84
+
85
+ /**
86
+ * Gerar presigned URL para HEAD (verificar objeto)
87
+ * @param {string} bucket - Nome do bucket
88
+ * @param {string} key - Chave/path do objeto
89
+ * @param {object} options - Opções
90
+ * @param {number} [options.expiresIn] - Tempo de expiração em segundos
91
+ * @param {object} [options.credentials] - AWS credentials
92
+ * @returns {string} Presigned URL
93
+ */
94
+ generateHeadUrl(bucket, key, options = {}) {
95
+ const expiresIn = options.expiresIn || 3600;
96
+ const credentials = options.credentials || this._getDefaultCredentials();
97
+
98
+ return this._generatePresignedUrl('HEAD', bucket, key, expiresIn, credentials, {});
99
+ }
100
+
101
+ /**
102
+ * Validar presigned URL
103
+ * @param {string} url - URL para validar
104
+ * @param {string} method - HTTP method esperado
105
+ * @param {object} [credentials] - Credentials para validação
106
+ * @returns {object} Resultado da validação
107
+ */
108
+ validatePresignedUrl(url, method = 'GET', credentials = null) {
109
+ try {
110
+ const urlObj = new URL(url);
111
+
112
+ // Extrair parâmetros de query
113
+ const params = Object.fromEntries(urlObj.searchParams);
114
+
115
+ // Verificar assinatura
116
+ if (!params.X-Amz-Signature) {
117
+ return {
118
+ isValid: false,
119
+ error: 'Signature missing'
120
+ };
121
+ }
122
+
123
+ // Verificar expiração
124
+ if (params['X-Amz-Date']) {
125
+ const requestTime = this._parseAmzDate(params['X-Amz-Date']);
126
+ const expiresIn = parseInt(params['X-Amz-Expires']) || 3600;
127
+ const expireTime = new Date(requestTime.getTime() + expiresIn * 1000);
128
+
129
+ if (new Date() > expireTime) {
130
+ return {
131
+ isValid: false,
132
+ error: 'URL expired'
133
+ };
134
+ }
135
+ }
136
+
137
+ // Verificar método
138
+ if (method && method !== 'GET') {
139
+ // A URL assinada funciona para qualquer método que foi assinado
140
+ // Aqui seria verificado qual método foi assinado
141
+ }
142
+
143
+ return {
144
+ isValid: true,
145
+ method: params['X-Amz-Algorithm'] ? 'SIGNATURE_V4' : 'UNKNOWN',
146
+ expiresAt: this._parseAmzDate(params['X-Amz-Date'])
147
+ };
148
+ } catch (error) {
149
+ return {
150
+ isValid: false,
151
+ error: error.message
152
+ };
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Gerar policy para upload direto (POST policy)
158
+ * Usado por navegadores para upload direto sem backend
159
+ * @param {string} bucket - Nome do bucket
160
+ * @param {string} key - Chave/path do objeto
161
+ * @param {object} options - Opções
162
+ * @returns {object} {policy, signature, credentials}
163
+ */
164
+ generatePostPolicy(bucket, key, options = {}) {
165
+ const expiresIn = options.expiresIn || 3600;
166
+ const credentials = options.credentials || this._getDefaultCredentials();
167
+ const contentLengthRange = options.contentLengthRange || [1, 104857600]; // 100MB default
168
+
169
+ // Data no formato ISO8601
170
+ const amzDate = new Date().toISOString().replace(/[:-]/g, '').split('.')[0] + 'Z';
171
+
172
+ // Policy
173
+ const policy = {
174
+ expiration: new Date(Date.now() + expiresIn * 1000).toISOString(),
175
+ conditions: [
176
+ { bucket: bucket },
177
+ { key: key },
178
+ ['content-length-range', contentLengthRange[0], contentLengthRange[1]],
179
+ { 'x-amz-credential': `${credentials.accessKeyId}/${this._getCredentialScope(amzDate)}` },
180
+ { 'x-amz-algorithm': 'AWS4-HMAC-SHA256' },
181
+ { 'x-amz-date': amzDate }
182
+ ]
183
+ };
184
+
185
+ // Adicionar condições customizadas
186
+ if (options.contentType) {
187
+ policy.conditions.push({ 'content-type': options.contentType });
188
+ }
189
+
190
+ if (options.metadata) {
191
+ Object.entries(options.metadata).forEach(([k, v]) => {
192
+ policy.conditions.push({ [`x-amz-meta-${k}`]: v });
193
+ });
194
+ }
195
+
196
+ // Converter policy para base64
197
+ const policyJson = JSON.stringify(policy);
198
+ const policyB64 = Buffer.from(policyJson).toString('base64');
199
+
200
+ // Gerar assinatura
201
+ const signature = this._signPolicy(policyB64, credentials, amzDate);
202
+
203
+ return {
204
+ policy: policyB64,
205
+ signature: signature,
206
+ credentials: {
207
+ accessKeyId: credentials.accessKeyId,
208
+ algorithm: 'AWS4-HMAC-SHA256',
209
+ date: amzDate,
210
+ credentialScope: this._getCredentialScope(amzDate)
211
+ },
212
+ url: `${this.s3Endpoint}/${bucket}`,
213
+ policyObject: policy
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Private Methods
219
+ */
220
+
221
+ _generatePresignedUrl(method, bucket, key, expiresIn, credentials, conditions = {}) {
222
+ const now = new Date();
223
+ const amzDate = now.toISOString().replace(/[:-]/g, '').split('.')[0] + 'Z';
224
+ const dateStamp = amzDate.substring(0, 8);
225
+
226
+ // Construir canonical request
227
+ const host = `${bucket}.s3.amazonaws.com`;
228
+ const canonicalUri = `/${key}`;
229
+
230
+ const canonicalQuerystring = [
231
+ `X-Amz-Algorithm=AWS4-HMAC-SHA256`,
232
+ `X-Amz-Credential=${encodeURIComponent(credentials.accessKeyId)}/${this._getCredentialScope(amzDate)}`,
233
+ `X-Amz-Date=${amzDate}`,
234
+ `X-Amz-Expires=${expiresIn}`,
235
+ `X-Amz-SignedHeaders=host`
236
+ ].sort().join('&');
237
+
238
+ const canonicalHeaders = `host:${host}\n`;
239
+ const signedHeaders = 'host';
240
+
241
+ const canonicalRequest = [
242
+ method,
243
+ canonicalUri,
244
+ canonicalQuerystring,
245
+ canonicalHeaders,
246
+ '',
247
+ signedHeaders
248
+ ].join('\n');
249
+
250
+ // Criar string para assinar
251
+ const canonicalRequestHash = crypto
252
+ .createHash('sha256')
253
+ .update(canonicalRequest)
254
+ .digest('hex');
255
+
256
+ const stringToSign = [
257
+ 'AWS4-HMAC-SHA256',
258
+ amzDate,
259
+ this._getCredentialScope(amzDate),
260
+ canonicalRequestHash
261
+ ].join('\n');
262
+
263
+ // Gerar assinatura
264
+ const signature = this._calculateSignature(
265
+ stringToSign,
266
+ credentials.secretAccessKey,
267
+ dateStamp
268
+ );
269
+
270
+ // Construir presigned URL
271
+ const presignedUrl = `https://${host}${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signature}`;
272
+
273
+ return presignedUrl;
274
+ }
275
+
276
+ _getCredentialScope(amzDate) {
277
+ const dateStamp = amzDate.substring(0, 8);
278
+ return `${dateStamp}/${this.region}/${this.service}/aws4_request`;
279
+ }
280
+
281
+ _calculateSignature(stringToSign, secretAccessKey, dateStamp) {
282
+ const kDate = crypto
283
+ .createHmac('sha256', `AWS4${secretAccessKey}`)
284
+ .update(dateStamp)
285
+ .digest();
286
+
287
+ const kRegion = crypto
288
+ .createHmac('sha256', kDate)
289
+ .update(this.region)
290
+ .digest();
291
+
292
+ const kService = crypto
293
+ .createHmac('sha256', kRegion)
294
+ .update(this.service)
295
+ .digest();
296
+
297
+ const kSigning = crypto
298
+ .createHmac('sha256', kService)
299
+ .update('aws4_request')
300
+ .digest();
301
+
302
+ const signature = crypto
303
+ .createHmac('sha256', kSigning)
304
+ .update(stringToSign)
305
+ .digest('hex');
306
+
307
+ return signature;
308
+ }
309
+
310
+ _signPolicy(policyB64, credentials, amzDate) {
311
+ const dateStamp = amzDate.substring(0, 8);
312
+ const signature = this._calculateSignature(policyB64, credentials.secretAccessKey, dateStamp);
313
+ return signature;
314
+ }
315
+
316
+ _parseAmzDate(amzDate) {
317
+ // Format: 20240115T103000Z
318
+ const year = parseInt(amzDate.substring(0, 4));
319
+ const month = parseInt(amzDate.substring(4, 6)) - 1;
320
+ const day = parseInt(amzDate.substring(6, 8));
321
+ const hour = parseInt(amzDate.substring(9, 11));
322
+ const minute = parseInt(amzDate.substring(11, 13));
323
+ const second = parseInt(amzDate.substring(13, 15));
324
+
325
+ return new Date(Date.UTC(year, month, day, hour, minute, second));
326
+ }
327
+
328
+ _getDefaultCredentials() {
329
+ // Return default test credentials
330
+ return {
331
+ accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
332
+ secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
333
+ };
334
+ }
335
+ }
336
+
337
+ module.exports = PresignedUrlGenerator;
@@ -1,3 +1,4 @@
1
+ const { setupServiceMiddleware } = require('../../middleware');
1
2
  /**
2
3
  * S3 Server - Servidor HTTP para S3
3
4
  */
@@ -9,6 +10,8 @@ const logger = require('../../utils/logger');
9
10
 
10
11
  class S3Server {
11
12
  constructor(port, config) {
13
+ this.organizationsService = null;
14
+ this.stsService = null;
12
15
  this.port = port;
13
16
  this.config = config;
14
17
  this.app = express();
@@ -66,6 +69,14 @@ class S3Server {
66
69
  }
67
70
 
68
71
  setupRoutes() {
72
+ // IAM enforcement (applied conditionally via injectDependencies)
73
+ this.app.use((req, res, next) => {
74
+ if (this.iamMiddleware && !req.path.startsWith('/__admin')) {
75
+ return this.iamMiddleware(req, res, next);
76
+ }
77
+ next();
78
+ });
79
+
69
80
  // Admin endpoints devem vir antes das rotas S3 para evitar conflito com /:bucket
70
81
  this.setupAdminRoutes();
71
82
 
@@ -91,6 +102,18 @@ class S3Server {
91
102
  }
92
103
  return res.status(200).send();
93
104
  }
105
+ if (req.query.notification !== undefined) {
106
+ let config = {};
107
+ try {
108
+ const bodyStr = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : req.body || '{}';
109
+ config = JSON.parse(bodyStr);
110
+ } catch (_) {}
111
+ const result = this.simulator.putBucketNotificationConfiguration(req.params.bucket, config);
112
+ if (result.error) {
113
+ return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
114
+ }
115
+ return res.status(200).send();
116
+ }
94
117
  const result = this.simulator.createBucket(req.params.bucket);
95
118
  if (result.error) {
96
119
  res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
@@ -208,6 +231,13 @@ class S3Server {
208
231
  }
209
232
  return res.set('Content-Type', 'application/xml').send(this._generateWebsiteConfigXml(result.websiteConfiguration));
210
233
  }
234
+ if (req.query.notification !== undefined) {
235
+ const result = this.simulator.getBucketNotificationConfiguration(req.params.bucket);
236
+ if (result.error) {
237
+ return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
238
+ }
239
+ return res.set('Content-Type', 'application/xml').send(this._generateNotificationConfigXml(result.notificationConfiguration));
240
+ }
211
241
  const bucket = req.params.bucket;
212
242
  const prefix = req.query.prefix || '';
213
243
  const delimiter = req.query.delimiter;
@@ -264,6 +294,179 @@ class S3Server {
264
294
  this.app.get('/__admin/stats', (req, res) => {
265
295
  res.json(this.simulator.getStats());
266
296
  });
297
+
298
+ // ─── Presigned URL Endpoints ──────────────────────────────────────────
299
+
300
+ /**
301
+ * POST /__admin/presigned-urls/get
302
+ * Generate a presigned GET URL for downloading an object
303
+ * Body: { bucket, key, expiresIn?, credentials? }
304
+ */
305
+ this.app.post('/__admin/presigned-urls/get', (req, res) => {
306
+ try {
307
+ const { bucket, key, expiresIn, credentials } = req.body || {};
308
+
309
+ if (!bucket || !key) {
310
+ return res.status(400).json({ error: 'bucket and key are required' });
311
+ }
312
+
313
+ const result = this.simulator.generatePresignedGetUrl(bucket, key, { expiresIn, credentials });
314
+
315
+ if (result.error) {
316
+ return res.status(result.status || 500).json(result.error);
317
+ }
318
+
319
+ res.json({
320
+ presignedUrl: result.presignedUrl,
321
+ expiresIn: expiresIn || 3600,
322
+ expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
323
+ });
324
+ } catch (err) {
325
+ logger.error('Error generating presigned GET URL:', err);
326
+ res.status(500).json({ error: err.message });
327
+ }
328
+ });
329
+
330
+ /**
331
+ * POST /__admin/presigned-urls/put
332
+ * Generate a presigned PUT URL for uploading an object
333
+ * Body: { bucket, key, expiresIn?, credentials?, contentType? }
334
+ */
335
+ this.app.post('/__admin/presigned-urls/put', (req, res) => {
336
+ try {
337
+ const { bucket, key, expiresIn, credentials, contentType } = req.body || {};
338
+
339
+ if (!bucket || !key) {
340
+ return res.status(400).json({ error: 'bucket and key are required' });
341
+ }
342
+
343
+ const result = this.simulator.generatePresignedPutUrl(bucket, key, { expiresIn, credentials, contentType });
344
+
345
+ if (result.error) {
346
+ return res.status(result.status || 500).json(result.error);
347
+ }
348
+
349
+ res.json({
350
+ presignedUrl: result.presignedUrl,
351
+ expiresIn: expiresIn || 3600,
352
+ expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
353
+ });
354
+ } catch (err) {
355
+ logger.error('Error generating presigned PUT URL:', err);
356
+ res.status(500).json({ error: err.message });
357
+ }
358
+ });
359
+
360
+ /**
361
+ * POST /__admin/presigned-urls/delete
362
+ * Generate a presigned DELETE URL for deleting an object
363
+ * Body: { bucket, key, expiresIn?, credentials? }
364
+ */
365
+ this.app.post('/__admin/presigned-urls/delete', (req, res) => {
366
+ try {
367
+ const { bucket, key, expiresIn, credentials } = req.body || {};
368
+
369
+ if (!bucket || !key) {
370
+ return res.status(400).json({ error: 'bucket and key are required' });
371
+ }
372
+
373
+ const result = this.simulator.generatePresignedDeleteUrl(bucket, key, { expiresIn, credentials });
374
+
375
+ if (result.error) {
376
+ return res.status(result.status || 500).json(result.error);
377
+ }
378
+
379
+ res.json({
380
+ presignedUrl: result.presignedUrl,
381
+ expiresIn: expiresIn || 3600,
382
+ expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
383
+ });
384
+ } catch (err) {
385
+ logger.error('Error generating presigned DELETE URL:', err);
386
+ res.status(500).json({ error: err.message });
387
+ }
388
+ });
389
+
390
+ /**
391
+ * POST /__admin/presigned-urls/head
392
+ * Generate a presigned HEAD URL for checking object metadata
393
+ * Body: { bucket, key, expiresIn?, credentials? }
394
+ */
395
+ this.app.post('/__admin/presigned-urls/head', (req, res) => {
396
+ try {
397
+ const { bucket, key, expiresIn, credentials } = req.body || {};
398
+
399
+ if (!bucket || !key) {
400
+ return res.status(400).json({ error: 'bucket and key are required' });
401
+ }
402
+
403
+ const result = this.simulator.generatePresignedHeadUrl(bucket, key, { expiresIn, credentials });
404
+
405
+ if (result.error) {
406
+ return res.status(result.status || 500).json(result.error);
407
+ }
408
+
409
+ res.json({
410
+ presignedUrl: result.presignedUrl,
411
+ expiresIn: expiresIn || 3600,
412
+ expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
413
+ });
414
+ } catch (err) {
415
+ logger.error('Error generating presigned HEAD URL:', err);
416
+ res.status(500).json({ error: err.message });
417
+ }
418
+ });
419
+
420
+ /**
421
+ * POST /__admin/presigned-urls/post-policy
422
+ * Generate a presigned POST policy for form uploads
423
+ * Body: { bucket, key, expiresIn?, credentials?, contentType?, conditions? }
424
+ */
425
+ this.app.post('/__admin/presigned-urls/post-policy', (req, res) => {
426
+ try {
427
+ const { bucket, key, expiresIn, credentials, contentType, conditions } = req.body || {};
428
+
429
+ if (!bucket || !key) {
430
+ return res.status(400).json({ error: 'bucket and key are required' });
431
+ }
432
+
433
+ const result = this.simulator.generatePresignedPostPolicy(bucket, key, { expiresIn, credentials, contentType, conditions });
434
+
435
+ if (result.error) {
436
+ return res.status(result.status || 500).json(result.error);
437
+ }
438
+
439
+ res.json({
440
+ ...result,
441
+ expiresIn: expiresIn || 3600,
442
+ expiresAt: new Date(Date.now() + (expiresIn || 3600) * 1000).toISOString()
443
+ });
444
+ } catch (err) {
445
+ logger.error('Error generating presigned POST policy:', err);
446
+ res.status(500).json({ error: err.message });
447
+ }
448
+ });
449
+
450
+ /**
451
+ * POST /__admin/presigned-urls/validate
452
+ * Validate a presigned URL
453
+ * Body: { url, method?, credentials? }
454
+ */
455
+ this.app.post('/__admin/presigned-urls/validate', (req, res) => {
456
+ try {
457
+ const { url, method, credentials } = req.body || {};
458
+
459
+ if (!url) {
460
+ return res.status(400).json({ error: 'url is required' });
461
+ }
462
+
463
+ const result = this.simulator.validatePresignedUrl(url, method || 'GET', credentials);
464
+ res.json(result);
465
+ } catch (err) {
466
+ logger.error('Error validating presigned URL:', err);
467
+ res.status(500).json({ error: err.message });
468
+ }
469
+ });
267
470
  }
268
471
 
269
472
  _serveWebsite(req, res, keyPath) {
@@ -318,6 +521,34 @@ class S3Server {
318
521
  </WebsiteConfiguration>`;
319
522
  }
320
523
 
524
+ _generateNotificationConfigXml(config) {
525
+ const nc = config || { LambdaFunctionConfigurations: [], QueueConfigurations: [], TopicConfigurations: [] };
526
+ const lambdaXml = (nc.LambdaFunctionConfigurations || []).map(cfg => `
527
+ <CloudFunctionConfiguration>
528
+ <Id>${cfg.Id || ''}</Id>
529
+ ${(cfg.Events || []).map(e => `<Event>${e}</Event>`).join('')}
530
+ <CloudFunction>${cfg.LambdaFunctionArn}</CloudFunction>
531
+ ${cfg.Filter && cfg.Filter.Key ? `<Filter><S3Key>${(cfg.Filter.Key.FilterRules || []).map(r => `<FilterRule><Name>${r.Name}</Name><Value>${r.Value}</Value></FilterRule>`).join('')}</S3Key></Filter>` : ''}
532
+ </CloudFunctionConfiguration>`).join('');
533
+ const queueXml = (nc.QueueConfigurations || []).map(cfg => `
534
+ <QueueConfiguration>
535
+ <Id>${cfg.Id || ''}</Id>
536
+ ${(cfg.Events || []).map(e => `<Event>${e}</Event>`).join('')}
537
+ <Queue>${cfg.QueueArn}</Queue>
538
+ ${cfg.Filter && cfg.Filter.Key ? `<Filter><S3Key>${(cfg.Filter.Key.FilterRules || []).map(r => `<FilterRule><Name>${r.Name}</Name><Value>${r.Value}</Value></FilterRule>`).join('')}</S3Key></Filter>` : ''}
539
+ </QueueConfiguration>`).join('');
540
+ const topicXml = (nc.TopicConfigurations || []).map(cfg => `
541
+ <TopicConfiguration>
542
+ <Id>${cfg.Id || ''}</Id>
543
+ ${(cfg.Events || []).map(e => `<Event>${e}</Event>`).join('')}
544
+ <Topic>${cfg.TopicArn}</Topic>
545
+ ${cfg.Filter && cfg.Filter.Key ? `<Filter><S3Key>${(cfg.Filter.Key.FilterRules || []).map(r => `<FilterRule><Name>${r.Name}</Name><Value>${r.Value}</Value></FilterRule>`).join('')}</S3Key></Filter>` : ''}
546
+ </TopicConfiguration>`).join('');
547
+ return `<?xml version="1.0" encoding="UTF-8"?>
548
+ <NotificationConfiguration>${lambdaXml}${queueXml}${topicXml}
549
+ </NotificationConfiguration>`;
550
+ }
551
+
321
552
  start() {
322
553
  return new Promise((resolve) => {
323
554
  this.server = this.app.listen(this.port, () => {