@gugananuvem/aws-local-simulator 1.0.25 → 1.0.26

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.
@@ -65,44 +65,56 @@ class KMSSimulator {
65
65
  }
66
66
 
67
67
  async createKey(params) {
68
- const { Description, KeyUsage = 'ENCRYPT_DECRYPT', KeySpec = 'SYMMETRIC_DEFAULT', Tags = [], MultiRegion = false } = params || {};
68
+ const Description = params.Description || params.description || '';
69
+ const KeyUsage = params.KeyUsage || params.keyUsage || 'ENCRYPT_DECRYPT';
70
+ const KeySpec = params.KeySpec || params.keySpec || 'SYMMETRIC_DEFAULT';
71
+ const Tags = params.Tags || params.tags || [];
72
+ const MultiRegion = params.MultiRegion || params.multiRegion || false;
73
+
69
74
  const keyId = uuidv4();
70
75
  const keyArn = `arn:aws:kms:local:000000000000:key/${keyId}`;
71
76
  let keyMaterial;
72
77
  let publicKey = null;
73
78
  let privateKey = null;
74
79
 
75
- if (KeySpec === 'SYMMETRIC_DEFAULT') {
76
- keyMaterial = crypto.randomBytes(32);
77
- } else if (KeySpec.startsWith('RSA_')) {
78
- const bits = KeySpec === 'RSA_2048' ? 2048 : KeySpec === 'RSA_3072' ? 3072 : 4096;
79
- const pair = crypto.generateKeyPairSync('rsa', {
80
- modulusLength: bits,
81
- publicKeyEncoding: { type: 'spki', format: 'pem' },
82
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
83
- });
84
- publicKey = pair.publicKey;
85
- privateKey = pair.privateKey;
86
- keyMaterial = Buffer.from(privateKey);
87
- } else if (KeySpec.startsWith('ECC_')) {
88
- const curve = KeySpec.includes('P256') ? 'prime256v1' : KeySpec.includes('P384') ? 'secp384r1' : 'secp521r1';
89
- const pair = crypto.generateKeyPairSync('ec', {
90
- namedCurve: curve,
91
- publicKeyEncoding: { type: 'spki', format: 'pem' },
92
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
93
- });
94
- publicKey = pair.publicKey;
95
- privateKey = pair.privateKey;
96
- keyMaterial = Buffer.from(privateKey);
97
- } else {
98
- keyMaterial = crypto.randomBytes(32);
80
+ this.logger.debug(`KMS: Criando chave ${keyId} (Spec: ${KeySpec})`, 'kms');
81
+
82
+ try {
83
+ if (KeySpec === 'SYMMETRIC_DEFAULT') {
84
+ keyMaterial = crypto.randomBytes(32);
85
+ } else if (KeySpec.startsWith('RSA_')) {
86
+ const bits = KeySpec === 'RSA_2048' ? 2048 : KeySpec === 'RSA_3072' ? 3072 : 4096;
87
+ const pair = crypto.generateKeyPairSync('rsa', {
88
+ modulusLength: bits,
89
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
90
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
91
+ });
92
+ publicKey = pair.publicKey;
93
+ privateKey = pair.privateKey;
94
+ keyMaterial = Buffer.from(privateKey);
95
+ } else if (KeySpec.startsWith('ECC_')) {
96
+ const curve = KeySpec.includes('P256') ? 'prime256v1' : KeySpec.includes('P384') ? 'secp384r1' : 'secp521r1';
97
+ const pair = crypto.generateKeyPairSync('ec', {
98
+ namedCurve: curve,
99
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
100
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
101
+ });
102
+ publicKey = pair.publicKey;
103
+ privateKey = pair.privateKey;
104
+ keyMaterial = Buffer.from(privateKey);
105
+ } else {
106
+ keyMaterial = crypto.randomBytes(32);
107
+ }
108
+ } catch (err) {
109
+ this.logger.error(`KMS: Erro ao gerar material da chave: ${err.message}`, 'kms');
110
+ throw err;
99
111
  }
100
112
 
101
113
  this.keyMaterial.set(keyId, keyMaterial);
102
114
  const key = {
103
115
  KeyId: keyId,
104
116
  KeyArn: keyArn,
105
- Description: Description || '',
117
+ Description,
106
118
  KeyUsage,
107
119
  KeySpec,
108
120
  KeyState: 'Enabled',
@@ -115,22 +127,30 @@ class KMSSimulator {
115
127
  };
116
128
  this.keys.set(keyId, key);
117
129
  await this._persistKeys();
118
- this.logger.info(`KMS: chave criada: ${keyId}`, 'kms');
130
+ this.logger.info(`KMS: chave criada com sucesso: ${keyId}`, 'kms');
119
131
  this.audit.record({ eventName: 'CreateKey', readOnly: false, resources: [{ ARN: keyArn, type: 'AWS::KMS::Key' }], requestParameters: { description: Description, keyUsage: KeyUsage, keySpec: KeySpec } });
120
132
  return { KeyMetadata: this._sanitizeKey(key) };
121
133
  }
122
134
 
123
135
  async describeKey(params) {
124
- const key = this._requireKey(params.KeyId);
136
+ const keyId = params.KeyId || params.keyId;
137
+ const key = this._requireKey(keyId);
125
138
  return { KeyMetadata: this._sanitizeKey(key) };
126
139
  }
127
140
 
128
141
  async listKeys(params) {
129
142
  const { Limit = 100 } = params || {};
130
143
  const keys = Array.from(this.keys.values()).slice(0, Limit);
144
+ // Para o Dashboard, retornamos mais detalhes se for solicitado via admin
145
+ // Mas para o SDK padrão, retornamos apenas o que o SDK espera
131
146
  return { Keys: keys.map(k => ({ KeyId: k.KeyId, KeyArn: k.KeyArn })) };
132
147
  }
133
148
 
149
+ listKeysFull() {
150
+ return Array.from(this.keys.values()).map(k => this._sanitizeKey(k));
151
+ }
152
+
153
+
134
154
  async enableKey(params) {
135
155
  const key = this._requireKey(params.KeyId);
136
156
  key.KeyState = 'Enabled'; key.Enabled = true;
@@ -80,6 +80,30 @@ class LambdaServer {
80
80
  this.app.get('/__admin/functions', (req, res) => {
81
81
  res.json(this.simulator.listLambdas());
82
82
  });
83
+
84
+ this.app.post('/__admin/functions', async (req, res) => {
85
+ try {
86
+ const lambda = await this.simulator.createFunction(req.body);
87
+ res.status(201).json(lambda);
88
+ } catch (err) {
89
+ res.status(400).json({ error: err.message });
90
+ }
91
+ });
92
+
93
+ this.app.put('/__admin/functions/:name', async (req, res) => {
94
+ try {
95
+ const lambda = await this.simulator.updateFunction(req.params.name, req.body);
96
+ res.json(lambda);
97
+ } catch (err) {
98
+ res.status(400).json({ error: err.message });
99
+ }
100
+ });
101
+
102
+ this.app.delete('/__admin/functions/:name', async (req, res) => {
103
+ const deleted = await this.simulator.deleteFunction(req.params.name);
104
+ if (deleted) res.json({ message: 'Lambda deleted' });
105
+ else res.status(404).json({ error: 'Lambda not found' });
106
+ });
83
107
 
84
108
  this.app.post('/__admin/reload', async (req, res) => {
85
109
  await this.simulator.reloadLambdas();
@@ -1,7 +1,6 @@
1
- /**
2
- * Lambda Simulator - Simula execução de funções Lambda
3
- */
4
-
1
+ const LocalStore = require("../../utils/local-store");
2
+ const path = require("path");
3
+ const fs = require("fs");
5
4
  const HandlerLoader = require("./handler-loader");
6
5
  const logger = require("../../utils/logger");
7
6
  const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
@@ -9,6 +8,8 @@ const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
9
8
  class LambdaSimulator {
10
9
  constructor(config) {
11
10
  this.config = config;
11
+ this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, "lambda");
12
+ this.store = new LocalStore(this.dataDir);
12
13
  this.lambdas = new Map(); // functionName -> { handler, env, config }
13
14
  this.environment = { ...process.env };
14
15
  this.audit = new CloudTrailAudit("lambda.amazonaws.com");
@@ -23,15 +24,27 @@ class LambdaSimulator {
23
24
  this.globalTimeout = globalDefaults.timeout || null;
24
25
  this.globalMemorySize = globalDefaults.memorySize || null;
25
26
 
27
+ // Carrega do config.lambdas (fixo)
26
28
  if (this.config.lambdas && this.config.lambdas.length > 0) {
27
29
  for (const lambdaConfig of this.config.lambdas) {
28
30
  await this.registerLambda(lambdaConfig);
29
31
  }
30
32
  }
31
33
 
34
+ // Carrega lambdas dinâmicas do disco
35
+ const savedLambdas = this.store.read("__functions__");
36
+ if (savedLambdas && Array.isArray(savedLambdas)) {
37
+ for (const lambdaConfig of savedLambdas) {
38
+ if (!this.lambdas.has(lambdaConfig.name)) {
39
+ await this.registerLambda(lambdaConfig);
40
+ }
41
+ }
42
+ }
43
+
32
44
  logger.debug(`✅ ${this.lambdas.size} Lambdas registradas`);
33
45
  }
34
46
 
47
+
35
48
  async registerLambda(lambdaConfig) {
36
49
  try {
37
50
  const { name, handler: handlerPath, type = "auto" } = lambdaConfig;
@@ -51,6 +64,14 @@ class LambdaSimulator {
51
64
  const memorySize = lambdaConfig.memorySize ?? this.globalMemorySize ?? 128;
52
65
 
53
66
  const handler = await HandlerLoader.load(handlerPath, type);
67
+ let codeSize = 0;
68
+ try {
69
+ const info = await HandlerLoader.getInfo(handlerPath);
70
+ codeSize = info.size;
71
+ } catch (err) {
72
+ // Ignore size errors
73
+ }
74
+
54
75
  if (handler != undefined) {
55
76
  this.lambdas.set(name, {
56
77
  name,
@@ -60,6 +81,7 @@ class LambdaSimulator {
60
81
  env,
61
82
  timeout,
62
83
  memorySize,
84
+ codeSize,
63
85
  type,
64
86
  registeredAt: new Date().toISOString(),
65
87
  });
@@ -147,16 +169,32 @@ class LambdaSimulator {
147
169
  }
148
170
 
149
171
  listLambdas() {
150
- return Array.from(this.lambdas.values()).map((l) => ({
151
- name: l.name,
152
- handlerName: l.handlerName,
153
- handlerPath: l.handlerPath,
154
- type: l.type,
155
- env: l.env,
156
- registeredAt: l.registeredAt,
157
- }));
172
+ return Array.from(this.lambdas.values()).map((l) => {
173
+ let code = "";
174
+ try {
175
+ if (fs.existsSync(l.handlerPath)) {
176
+ code = fs.readFileSync(l.handlerPath, "utf8");
177
+ }
178
+ } catch (err) {
179
+ logger.error(`Erro ao ler código da lambda ${l.name}:`, err);
180
+ }
181
+
182
+ return {
183
+ name: l.name,
184
+ handlerName: l.handlerName,
185
+ handlerPath: l.handlerPath,
186
+ type: l.type,
187
+ env: l.env,
188
+ timeout: l.timeout,
189
+ memorySize: l.memorySize,
190
+ codeSize: l.codeSize,
191
+ registeredAt: l.registeredAt,
192
+ code: code
193
+ };
194
+ });
158
195
  }
159
196
 
197
+
160
198
  getLambda(name) {
161
199
  return this.lambdas.get(name);
162
200
  }
@@ -182,6 +220,91 @@ class LambdaSimulator {
182
220
  logger.info(`✅ ${this.lambdas.size} Lambdas recarregadas`);
183
221
  }
184
222
 
223
+ async createFunction(lambdaConfig) {
224
+ const { name, code, runtime, handler, timeout, memorySize, environment } = lambdaConfig;
225
+
226
+ if (!name) throw new Error("Function name is required");
227
+
228
+ // Define o caminho do arquivo (dentro do dataDir/functions)
229
+ const functionsDir = path.join(this.dataDir, "functions");
230
+ if (!fs.existsSync(functionsDir)) fs.mkdirSync(functionsDir, { recursive: true });
231
+
232
+ const fileName = `${name}.js`;
233
+ const filePath = path.join(functionsDir, fileName);
234
+
235
+ // Salva o código no disco
236
+ fs.writeFileSync(filePath, code || "// Hello Lambda");
237
+
238
+ // Registra a lambda
239
+ const config = {
240
+ name,
241
+ handler: filePath,
242
+ runtime: runtime || "nodejs18.x",
243
+ timeout: timeout || 30,
244
+ memorySize: memorySize || 128,
245
+ env: environment || {},
246
+ type: "commonjs"
247
+ };
248
+
249
+ await this.registerLambda(config);
250
+ this.persistLambdas();
251
+
252
+ return this.lambdas.get(name);
253
+ }
254
+
255
+ async updateFunction(name, lambdaConfig) {
256
+ const lambda = this.lambdas.get(name);
257
+ if (!lambda) throw new Error(`Function not found: ${name}`);
258
+
259
+ const { code, runtime, handler, timeout, memorySize, environment } = lambdaConfig;
260
+
261
+ // Se houver código novo, sobrescreve o arquivo
262
+ if (code !== undefined) {
263
+ fs.writeFileSync(lambda.handlerPath, code);
264
+ }
265
+
266
+ // Atualiza a configuração
267
+ const updatedConfig = {
268
+ name,
269
+ handler: lambda.handlerPath,
270
+ runtime: runtime || lambda.runtime,
271
+ timeout: timeout || lambda.timeout,
272
+ memorySize: memorySize || lambda.memorySize,
273
+ env: environment || lambda.env,
274
+ type: lambda.type
275
+ };
276
+
277
+ await this.registerLambda(updatedConfig);
278
+ this.persistLambdas();
279
+
280
+ return this.lambdas.get(name);
281
+ }
282
+
283
+ async deleteFunction(name) {
284
+
285
+ if (this.lambdas.has(name)) {
286
+ this.lambdas.delete(name);
287
+ this.persistLambdas();
288
+ return true;
289
+ }
290
+ return false;
291
+ }
292
+
293
+ persistLambdas() {
294
+ const functionsToSave = Array.from(this.lambdas.values())
295
+ .filter(l => l.handlerPath.includes(this.dataDir)) // Apenas as dinâmicas
296
+ .map(l => ({
297
+ name: l.name,
298
+ handler: l.handlerPath,
299
+ type: l.type,
300
+ env: l.env,
301
+ timeout: l.timeout,
302
+ memorySize: l.memorySize,
303
+ }));
304
+
305
+ this.store.write("__functions__", functionsToSave);
306
+ }
307
+
185
308
  getStats() {
186
309
  return {
187
310
  totalLambdas: this.lambdas.size,
@@ -189,6 +312,7 @@ class LambdaSimulator {
189
312
  };
190
313
  }
191
314
 
315
+
192
316
  async reset() {
193
317
  await this.reloadLambdas();
194
318
  this.environment = { ...process.env };
@@ -50,7 +50,7 @@ class ParameterStoreSimulator {
50
50
  const storedValue = Type === 'SecureString' ? this._encrypt(Value) : Value;
51
51
  const param = {
52
52
  Name, Value: storedValue, Type, Description: Description || '', Version: version,
53
- LastModifiedDate: new Date().toISOString(),
53
+ LastModifiedDate: Math.floor(Date.now() / 1000),
54
54
  LastModifiedUser: 'local',
55
55
  ARN: `arn:aws:ssm:local:000000000000:parameter${Name}`,
56
56
  DataType, Tags: Tags || [], KeyId: Type === 'SecureString' ? (KeyId || 'aws/ssm') : undefined
@@ -23,6 +23,26 @@ class S3Server {
23
23
  this.app.use(express.raw({ type: () => true, limit: '100mb' }));
24
24
  this.app.use(express.text({ limit: '100mb' }));
25
25
 
26
+ // Suporte a Virtual Host Style addressing (bucket.localhost:4566)
27
+ this.app.use((req, res, next) => {
28
+ const host = req.headers.host || '';
29
+ // Se o host contém múltiplos pontos e não é apenas um IP ou localhost puro
30
+ if (host.includes('.') && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
31
+ const parts = host.split('.');
32
+ // O primeiro fragmento é o nome do bucket se houver mais de 2 partes (bucket.localhost:port)
33
+ // ou se a segunda parte for localhost
34
+ if (parts.length >= 2) {
35
+ const bucket = parts[0];
36
+ // Evita processar se for admin ou se já estiver no path style (heurística simples)
37
+ if (bucket !== 'localhost' && !req.path.startsWith('/__admin/')) {
38
+ req.url = `/${bucket}${req.url}`;
39
+ logger.verboso(`S3: Virtual Host Style detected. Rewriting to ${req.url}`);
40
+ }
41
+ }
42
+ }
43
+ next();
44
+ });
45
+
26
46
  // Logging de requisições
27
47
  if (logger.currentLogLevel === 'verboso') {
28
48
  this.app.use((req, res, next) => {
@@ -36,6 +56,7 @@ class S3Server {
36
56
  }
37
57
  }
38
58
 
59
+
39
60
  async initialize() {
40
61
  if (!this.simulator) {
41
62
  this.simulator = new S3Simulator(this.config);
@@ -489,10 +489,13 @@ class S3Simulator {
489
489
  // ─── Utilitários ──────────────────────────────────────────────────────────
490
490
 
491
491
  isValidBucketName(bucketName) {
492
- const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
492
+ // S3 oficial é rigoroso, mas para simulador local vamos ser mais tolerantes
493
+ // permitindo underscores e letras maiúsculas que são comuns em testes locais
494
+ const regex = /^[a-zA-Z0-9][a-zA-Z0-9._-]{1,61}[a-zA-Z0-9]$/;
493
495
  return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
494
496
  }
495
497
 
498
+
496
499
  extractMetadata(headers) {
497
500
  const metadata = {};
498
501
  for (const [key, value] of Object.entries(headers)) {
@@ -37,7 +37,8 @@ class SecretManagerServer {
37
37
  if (!operation) return res.status(400).json({ __type: 'UnknownOperationException', message: `Unknown: ${target}` });
38
38
  try {
39
39
  const result = await this.simulator[operation](req.body || {});
40
- res.json(result || {});
40
+ res.setHeader('Content-Type', 'application/x-amz-json-1.1');
41
+ res.send(JSON.stringify(result || {}));
41
42
  } catch (err) {
42
43
  this.logger.error(`SecretsManager ${target}: ${err.message}`, 'secret-manager');
43
44
  res.status(err.code === 'ResourceNotFoundException' ? 404 : 400).json({ __type: err.code || 'InternalServiceError', Message: err.message });
@@ -17,7 +17,18 @@ class SecretManagerSimulator {
17
17
  async initialize() {
18
18
  try {
19
19
  const secrets = await this.store.read('secret-manager/secrets');
20
- if (Array.isArray(secrets)) for (const s of secrets) this.secrets.set(s.Name, s);
20
+ if (Array.isArray(secrets)) {
21
+ for (const s of secrets) {
22
+ if (typeof s.CreatedDate === 'string') s.CreatedDate = Math.floor(new Date(s.CreatedDate).getTime() / 1000);
23
+ if (typeof s.LastChangedDate === 'string') s.LastChangedDate = Math.floor(new Date(s.LastChangedDate).getTime() / 1000);
24
+ if (s._versions) {
25
+ for (const v of Object.values(s._versions)) {
26
+ if (typeof v.CreatedDate === 'string') v.CreatedDate = Math.floor(new Date(v.CreatedDate).getTime() / 1000);
27
+ }
28
+ }
29
+ this.secrets.set(s.Name, s);
30
+ }
31
+ }
21
32
  this.logger.info('SecretsManager: dados carregados', 'secret-manager');
22
33
  } catch { this.logger.debug('SecretsManager: sem dados anteriores', 'secret-manager'); }
23
34
  }
@@ -38,12 +49,12 @@ class SecretManagerSimulator {
38
49
  ARN: `arn:aws:secretsmanager:local:000000000000:secret:${Name}-${secretId.slice(0, 6)}`,
39
50
  Name, Description: Description || '', Tags,
40
51
  KmsKeyId: KmsKeyId || 'aws/secretsmanager',
41
- CreatedDate: new Date().toISOString(),
42
- LastChangedDate: new Date().toISOString(),
52
+ CreatedDate: Math.floor(Date.now() / 1000),
53
+ LastChangedDate: Math.floor(Date.now() / 1000),
43
54
  LastAccessedDate: null,
44
55
  RotationEnabled: false,
45
56
  VersionsToStages: { [secretId]: ['AWSCURRENT'] },
46
- _versions: { [secretId]: { SecretString, SecretBinary, CreatedDate: new Date().toISOString() } }
57
+ _versions: { [secretId]: { SecretString, SecretBinary, CreatedDate: Math.floor(Date.now() / 1000) } }
47
58
  };
48
59
  this.secrets.set(Name, secret);
49
60
  await this._persist();
@@ -55,7 +66,7 @@ class SecretManagerSimulator {
55
66
  async getSecretValue(params) {
56
67
  const { SecretId, VersionId, VersionStage = 'AWSCURRENT' } = params;
57
68
  const secret = this._requireSecret(SecretId);
58
- secret.LastAccessedDate = new Date().toISOString();
69
+ secret.LastAccessedDate = Math.floor(Date.now() / 1000);
59
70
  let versionId = VersionId;
60
71
  if (!versionId) {
61
72
  versionId = Object.entries(secret.VersionsToStages).find(([, stages]) => stages.includes(VersionStage))?.[0];
@@ -81,9 +92,9 @@ class SecretManagerSimulator {
81
92
  secret.VersionsToStages[vid] = stages.filter(s => s !== 'AWSCURRENT').concat(['AWSPREVIOUS']);
82
93
  }
83
94
  }
84
- secret._versions[versionId] = { SecretString, SecretBinary, CreatedDate: new Date().toISOString() };
95
+ secret._versions[versionId] = { SecretString, SecretBinary, CreatedDate: Math.floor(Date.now() / 1000) };
85
96
  secret.VersionsToStages[versionId] = VersionStages;
86
- secret.LastChangedDate = new Date().toISOString();
97
+ secret.LastChangedDate = Math.floor(Date.now() / 1000);
87
98
  await this._persist();
88
99
  return { ARN: secret.ARN, Name: secret.Name, VersionId: versionId, VersionStages };
89
100
  }
@@ -103,8 +114,8 @@ class SecretManagerSimulator {
103
114
  async deleteSecret(params) {
104
115
  const { SecretId, RecoveryWindowInDays = 30, ForceDeleteWithoutRecovery } = params;
105
116
  const secret = this._requireSecret(SecretId);
106
- const deletionDate = ForceDeleteWithoutRecovery ? new Date().toISOString() : new Date(Date.now() + RecoveryWindowInDays * 86400000).toISOString();
107
- secret.DeletedDate = new Date().toISOString();
117
+ const deletionDate = ForceDeleteWithoutRecovery ? Math.floor(Date.now() / 1000) : Math.floor((Date.now() + RecoveryWindowInDays * 86400000) / 1000);
118
+ secret.DeletedDate = Math.floor(Date.now() / 1000);
108
119
  secret.DeletionDate = deletionDate;
109
120
  if (ForceDeleteWithoutRecovery) this.secrets.delete(secret.Name);
110
121
  await this._persist();
@@ -146,7 +157,7 @@ class SecretManagerSimulator {
146
157
  secret.RotationEnabled = true;
147
158
  secret.RotationLambdaARN = RotationLambdaARN;
148
159
  secret.RotationRules = RotationRules;
149
- secret.LastRotatedDate = new Date().toISOString();
160
+ secret.LastRotatedDate = Math.floor(Date.now() / 1000);
150
161
  await this._persist();
151
162
  return { ARN: secret.ARN, Name: secret.Name };
152
163
  }
@@ -81,27 +81,39 @@ function createSNSServer(simulator, config, logger) {
81
81
  });
82
82
 
83
83
  app.get('/__admin/topics', (_req, res) => {
84
- res.json({ topics: Array.from(simulator.topics.values()) });
84
+ res.json(Array.from(simulator.topics.values()));
85
85
  });
86
86
 
87
+ app.post('/__admin/topics', async (req, res) => {
88
+ try {
89
+ const topic = await simulator.createTopic({ Name: req.body.name });
90
+ res.status(201).json(topic);
91
+ } catch (err) {
92
+ res.status(400).json({ error: err.message });
93
+ }
94
+ });
95
+
96
+
87
97
  app.get('/__admin/subscriptions', (_req, res) => {
88
- res.json({ subscriptions: Array.from(simulator.subscriptions.values()) });
98
+ res.json(Array.from(simulator.subscriptions.values()));
89
99
  });
90
100
 
91
101
  app.get('/__admin/subscriptions/:topicName', (req, res) => {
92
102
  const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
93
103
  const subs = Array.from(simulator.subscriptions.values()).filter(s => s.TopicArn === arn);
94
- res.json({ subscriptions: subs });
104
+ res.json(subs);
95
105
  });
96
106
 
107
+
97
108
  app.get('/__admin/publish-log', (_req, res) => {
98
- res.json({ messages: simulator.publishLog });
109
+ res.json(simulator.publishLog);
99
110
  });
100
111
 
101
112
  app.get('/__admin/platform-apps', (_req, res) => {
102
- res.json({ platformApplications: Array.from(simulator.platformApps.values()) });
113
+ res.json(Array.from(simulator.platformApps.values()));
103
114
  });
104
115
 
116
+
105
117
  app.delete('/__admin/topics/:topicName', async (req, res) => {
106
118
  try {
107
119
  const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
@@ -112,6 +124,21 @@ function createSNSServer(simulator, config, logger) {
112
124
  }
113
125
  });
114
126
 
127
+ app.post('/__admin/topics/:topicName/publish', async (req, res) => {
128
+ try {
129
+ const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
130
+ const result = await simulator.publish({
131
+ TopicArn: arn,
132
+ Subject: req.body.subject,
133
+ Message: req.body.message
134
+ });
135
+ res.json(result);
136
+ } catch (err) {
137
+ res.status(400).json({ error: err.message });
138
+ }
139
+ });
140
+
141
+
115
142
  app.post('/__admin/reset', async (_req, res) => {
116
143
  await simulator.reset();
117
144
  res.json({ message: 'SNS data reset complete' });
@@ -187,7 +187,10 @@ class SQSServer {
187
187
  return this.generateReceiveMessageResponse(result.messages);
188
188
  case 'DeleteMessage':
189
189
  return this.generateDeleteMessageResponse();
190
+ case 'SetQueueAttributes':
191
+ return this.generateSetQueueAttributesResponse();
190
192
  case 'GetQueueUrl':
193
+
191
194
  return this.generateGetQueueUrlResponse(result.queueUrl);
192
195
  case 'ListQueues':
193
196
  return this.generateListQueuesResponse(result.queues);
@@ -196,6 +199,14 @@ class SQSServer {
196
199
  }
197
200
  }
198
201
 
202
+ generateSetQueueAttributesResponse() {
203
+ return `<?xml version="1.0" encoding="UTF-8"?>
204
+ <SetQueueAttributesResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
205
+ <ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
206
+ </SetQueueAttributesResponse>`;
207
+ }
208
+
209
+
199
210
  generateCreateQueueResponse(queueUrl) {
200
211
  return `<?xml version="1.0" encoding="UTF-8"?>
201
212
  <CreateQueueResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">