@geekmidas/cli 1.10.15 → 1.10.17

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 (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{bundler-BWsVDer6.mjs → bundler-B4AackW5.mjs} +2 -2
  3. package/dist/{bundler-BWsVDer6.mjs.map → bundler-B4AackW5.mjs.map} +1 -1
  4. package/dist/{bundler-Drh5KoN5.cjs → bundler-BhhfkI9T.cjs} +2 -2
  5. package/dist/{bundler-Drh5KoN5.cjs.map → bundler-BhhfkI9T.cjs.map} +1 -1
  6. package/dist/config.d.cts +2 -2
  7. package/dist/config.d.mts +2 -2
  8. package/dist/{fullstack-secrets-D9rjTNyx.cjs → fullstack-secrets-DOHBU4Rp.cjs} +110 -4
  9. package/dist/fullstack-secrets-DOHBU4Rp.cjs.map +1 -0
  10. package/dist/{fullstack-secrets-BIFFv4UZ.mjs → fullstack-secrets-x2Kffx7-.mjs} +99 -5
  11. package/dist/fullstack-secrets-x2Kffx7-.mjs.map +1 -0
  12. package/dist/{index-UCsZ_Vkw.d.cts → index-BkibYzso.d.cts} +15 -4
  13. package/dist/index-BkibYzso.d.cts.map +1 -0
  14. package/dist/{index-gXAGDSGu.d.mts → index-CY-ieuRp.d.mts} +15 -4
  15. package/dist/index-CY-ieuRp.d.mts.map +1 -0
  16. package/dist/index.cjs +332 -62
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +332 -62
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/openapi-BYxAWwok.cjs.map +1 -1
  21. package/dist/openapi-DenF-okj.mjs.map +1 -1
  22. package/dist/openapi.d.cts +1 -1
  23. package/dist/openapi.d.mts +1 -1
  24. package/dist/{reconcile-DxTEausy.mjs → reconcile-BLh6rswz.mjs} +2 -2
  25. package/dist/{reconcile-DxTEausy.mjs.map → reconcile-BLh6rswz.mjs.map} +1 -1
  26. package/dist/{reconcile-LaaJkFlO.cjs → reconcile-Ch7sIcf8.cjs} +2 -2
  27. package/dist/{reconcile-LaaJkFlO.cjs.map → reconcile-Ch7sIcf8.cjs.map} +1 -1
  28. package/dist/{storage-Bu44pwPJ.cjs → storage-B1wvztiJ.cjs} +11 -1
  29. package/dist/{storage-clMAp4sc.mjs.map → storage-B1wvztiJ.cjs.map} +1 -1
  30. package/dist/{storage-CauTheT9.mjs → storage-Cs4WBsc4.mjs} +1 -1
  31. package/dist/{storage-DpqzcjQ5.cjs → storage-DOEtT2Hr.cjs} +1 -1
  32. package/dist/{storage-clMAp4sc.mjs → storage-dbb9RyBl.mjs} +11 -1
  33. package/dist/{storage-Bu44pwPJ.cjs.map → storage-dbb9RyBl.mjs.map} +1 -1
  34. package/dist/{sync-BkalF65h.mjs → sync-COnAugP-.mjs} +1 -1
  35. package/dist/sync-D1Pa30oV.cjs +4 -0
  36. package/dist/{sync-BeiI5rFC.cjs → sync-DGXXSk2v.cjs} +2 -2
  37. package/dist/{sync-BeiI5rFC.cjs.map → sync-DGXXSk2v.cjs.map} +1 -1
  38. package/dist/{sync-CWJ6tL0s.mjs → sync-D_NowTkZ.mjs} +2 -2
  39. package/dist/{sync-CWJ6tL0s.mjs.map → sync-D_NowTkZ.mjs.map} +1 -1
  40. package/dist/{types-DiV9Mbvc.d.mts → types-DdHfUbxk.d.cts} +13 -3
  41. package/dist/types-DdHfUbxk.d.cts.map +1 -0
  42. package/dist/{types-JvWj5Ckc.d.cts → types-OszPdw9m.d.mts} +13 -3
  43. package/dist/types-OszPdw9m.d.mts.map +1 -0
  44. package/dist/workspace/index.d.cts +2 -2
  45. package/dist/workspace/index.d.mts +2 -2
  46. package/dist/workspace-4SP3Gx4Y.cjs.map +1 -1
  47. package/dist/workspace-D4z4A4cq.mjs.map +1 -1
  48. package/package.json +4 -4
  49. package/src/dev/__tests__/entry.spec.ts +3 -5
  50. package/src/dev/__tests__/index.spec.ts +73 -5
  51. package/src/dev/index.ts +33 -25
  52. package/src/docker/compose.ts +130 -2
  53. package/src/init/__tests__/generators.spec.ts +84 -0
  54. package/src/init/generators/docker.ts +128 -16
  55. package/src/init/index.ts +26 -1
  56. package/src/init/templates/index.ts +28 -0
  57. package/src/init/versions.ts +1 -1
  58. package/src/secrets/__tests__/generator.spec.ts +183 -0
  59. package/src/secrets/generator.ts +116 -4
  60. package/src/secrets/storage.ts +12 -0
  61. package/src/secrets/types.ts +11 -1
  62. package/src/setup/__tests__/reconcile-secrets.spec.ts +86 -0
  63. package/src/setup/index.ts +64 -1
  64. package/src/test/__tests__/index.spec.ts +1 -4
  65. package/src/types.ts +13 -1
  66. package/src/workspace/types.ts +13 -2
  67. package/dist/fullstack-secrets-BIFFv4UZ.mjs.map +0 -1
  68. package/dist/fullstack-secrets-D9rjTNyx.cjs.map +0 -1
  69. package/dist/index-UCsZ_Vkw.d.cts.map +0 -1
  70. package/dist/index-gXAGDSGu.d.mts.map +0 -1
  71. package/dist/sync-Bp8xRcuQ.cjs +0 -4
  72. package/dist/types-DiV9Mbvc.d.mts.map +0 -1
  73. package/dist/types-JvWj5Ckc.d.cts.map +0 -1
@@ -1,3 +1,4 @@
1
+ import type { EventsBackend } from '../../types.js';
1
2
  import type {
2
3
  GeneratedFile,
3
4
  TemplateConfig,
@@ -64,13 +65,13 @@ export function generateDockerFiles(
64
65
  if (isFullstack && dbApps?.length) {
65
66
  files.push({
66
67
  path: 'docker/postgres/init.sh',
67
- content: generatePostgresInitScript(dbApps),
68
+ content: generatePostgresInitScript(dbApps, options.services?.events),
68
69
  });
69
70
 
70
71
  // Generate .env file for docker-compose (contains db passwords)
71
72
  files.push({
72
73
  path: 'docker/.env',
73
- content: generateDockerEnv(dbApps),
74
+ content: generateDockerEnv(dbApps, options.services?.events),
74
75
  });
75
76
  }
76
77
  }
@@ -183,6 +184,53 @@ export function generateDockerFiles(
183
184
  volumes.push(' minio_data:');
184
185
  }
185
186
 
187
+ // LocalStack for SNS events
188
+ if (options.services?.events === 'sns') {
189
+ services.push(` localstack:
190
+ image: localstack/localstack:latest
191
+ container_name: ${options.name}-localstack
192
+ restart: unless-stopped
193
+ environment:
194
+ SERVICES: sns,sqs
195
+ AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
196
+ AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
197
+ AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
198
+ ports:
199
+ - '\${LOCALSTACK_PORT:-4566}:4566'
200
+ volumes:
201
+ - localstack_data:/var/lib/localstack
202
+ healthcheck:
203
+ test: ['CMD', 'curl', '-f', 'http://localhost:4566/_localstack/health']
204
+ interval: 10s
205
+ timeout: 5s
206
+ retries: 5`);
207
+ volumes.push(' localstack_data:');
208
+ }
209
+
210
+ // RabbitMQ for rabbitmq events (when not already added by worker template)
211
+ if (options.services?.events === 'rabbitmq' && !hasWorker) {
212
+ services.push(` rabbitmq:
213
+ image: rabbitmq:3-management-alpine
214
+ container_name: ${options.name}-rabbitmq
215
+ restart: unless-stopped
216
+ ports:
217
+ - '\${RABBITMQ_HOST_PORT:-5672}:5672'
218
+ - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
219
+ environment:
220
+ RABBITMQ_DEFAULT_USER: guest
221
+ RABBITMQ_DEFAULT_PASS: guest
222
+ volumes:
223
+ - rabbitmq_data:/var/lib/rabbitmq
224
+ healthcheck:
225
+ test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
226
+ interval: 10s
227
+ timeout: 5s
228
+ retries: 5`);
229
+ if (!volumes.includes(' rabbitmq_data:')) {
230
+ volumes.push(' rabbitmq_data:');
231
+ }
232
+ }
233
+
186
234
  // Build docker-compose.yml
187
235
  let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
188
236
  # Running "docker compose up" directly will not inject secrets or resolve ports.
@@ -209,12 +257,20 @@ ${volumes.join('\n')}
209
257
  /**
210
258
  * Generate .env file for docker-compose with database passwords
211
259
  */
212
- function generateDockerEnv(apps: DatabaseAppConfig[]): string {
260
+ function generateDockerEnv(
261
+ apps: DatabaseAppConfig[],
262
+ eventsBackend?: EventsBackend,
263
+ ): string {
213
264
  const envVars = apps.map((app) => {
214
265
  const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
215
266
  return `${envVar}=${app.password}`;
216
267
  });
217
268
 
269
+ // Add pgboss password if events backend is pgboss
270
+ if (eventsBackend === 'pgboss') {
271
+ envVars.push(`PGBOSS_DB_PASSWORD=pgboss-dev-password`);
272
+ }
273
+
218
274
  return `# Auto-generated docker environment file
219
275
  # Contains database passwords for docker-compose postgres init
220
276
  # This file is gitignored - do not commit to version control
@@ -223,12 +279,16 @@ ${envVars.join('\n')}
223
279
  }
224
280
 
225
281
  /**
226
- * Generate PostgreSQL init shell script that creates per-app users with separate schemas
227
- * Uses environment variables for passwords (more secure than hardcoded values)
282
+ * Generate PostgreSQL init shell script that creates per-app users with separate schemas.
283
+ * Uses idempotent DO blocks so the script can be re-run safely.
228
284
  * - api user: uses public schema
229
285
  * - auth user: uses auth schema with search_path=auth
286
+ * - pgboss user: uses pgboss schema (when events === 'pgboss')
230
287
  */
231
- function generatePostgresInitScript(apps: DatabaseAppConfig[]): string {
288
+ function generatePostgresInitScript(
289
+ apps: DatabaseAppConfig[],
290
+ eventsBackend?: EventsBackend,
291
+ ): string {
232
292
  const userCreations = apps.map((app) => {
233
293
  const userName = app.name.replace(/-/g, '_');
234
294
  const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
@@ -236,25 +296,39 @@ function generatePostgresInitScript(apps: DatabaseAppConfig[]): string {
236
296
  const schemaName = isApi ? 'public' : userName;
237
297
 
238
298
  if (isApi) {
239
- // API user uses public schema
240
299
  return `
241
- # Create ${app.name} user (uses public schema)
300
+ # Create ${app.name} user (uses public schema) - idempotent
242
301
  echo "Creating user ${userName}..."
243
302
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
244
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
303
+ DO \\$\\$
304
+ BEGIN
305
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
306
+ CREATE USER ${userName} WITH PASSWORD '$${envVar}';
307
+ ELSE
308
+ ALTER USER ${userName} WITH PASSWORD '$${envVar}';
309
+ END IF;
310
+ END
311
+ \\$\\$;
245
312
  GRANT ALL ON SCHEMA public TO ${userName};
246
313
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${userName};
247
314
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${userName};
248
315
  EOSQL
249
316
  `;
250
317
  }
251
- // Other users get their own schema with search_path
252
318
  return `
253
- # Create ${app.name} user with dedicated schema
319
+ # Create ${app.name} user with dedicated schema - idempotent
254
320
  echo "Creating user ${userName} with schema ${schemaName}..."
255
321
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
256
- CREATE USER ${userName} WITH PASSWORD '$${envVar}';
257
- CREATE SCHEMA ${schemaName} AUTHORIZATION ${userName};
322
+ DO \\$\\$
323
+ BEGIN
324
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
325
+ CREATE USER ${userName} WITH PASSWORD '$${envVar}';
326
+ ELSE
327
+ ALTER USER ${userName} WITH PASSWORD '$${envVar}';
328
+ END IF;
329
+ END
330
+ \\$\\$;
331
+ CREATE SCHEMA IF NOT EXISTS ${schemaName} AUTHORIZATION ${userName};
258
332
  ALTER USER ${userName} SET search_path TO ${schemaName};
259
333
  GRANT USAGE ON SCHEMA ${schemaName} TO ${userName};
260
334
  GRANT ALL ON ALL TABLES IN SCHEMA ${schemaName} TO ${userName};
@@ -265,14 +339,52 @@ EOSQL
265
339
  `;
266
340
  });
267
341
 
342
+ // Add pgboss user and schema if events backend is pgboss
343
+ let pgbossBlock = '';
344
+ if (eventsBackend === 'pgboss') {
345
+ pgbossBlock = `
346
+ # Create pgboss user with dedicated schema - idempotent
347
+ echo "Creating pgboss user and schema..."
348
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
349
+ DO \\$\\$
350
+ BEGIN
351
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgboss') THEN
352
+ CREATE USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
353
+ ELSE
354
+ ALTER USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
355
+ END IF;
356
+ END
357
+ \\$\\$;
358
+ CREATE SCHEMA IF NOT EXISTS pgboss AUTHORIZATION pgboss;
359
+ ALTER USER pgboss SET search_path TO pgboss;
360
+ GRANT USAGE ON SCHEMA pgboss TO pgboss;
361
+ GRANT ALL ON ALL TABLES IN SCHEMA pgboss TO pgboss;
362
+ GRANT ALL ON ALL SEQUENCES IN SCHEMA pgboss TO pgboss;
363
+ ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON TABLES TO pgboss;
364
+ ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON SEQUENCES TO pgboss;
365
+ EOSQL
366
+ `;
367
+ }
368
+
369
+ // Add extensions
370
+ const extensions = `
371
+ # Create extensions
372
+ echo "Creating extensions..."
373
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
374
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
375
+ CREATE EXTENSION IF NOT EXISTS citext;
376
+ EOSQL
377
+ `;
378
+
268
379
  return `#!/bin/bash
269
380
  set -e
270
381
 
271
- # Auto-generated PostgreSQL init script
382
+ # Auto-generated PostgreSQL init script (idempotent - safe to re-run)
272
383
  # Creates per-app users with separate schemas in a single database
273
384
  # - api: uses public schema
274
- # - auth: uses auth schema (search_path=auth)
275
- ${userCreations.join('\n')}
385
+ # - auth: uses auth schema (search_path=auth)${eventsBackend === 'pgboss' ? '\n# - pgboss: uses pgboss schema for event processing' : ''}
386
+ ${extensions}
387
+ ${userCreations.join('\n')}${pgbossBlock}
276
388
  echo "Database initialization complete!"
277
389
  `;
278
390
  }
package/src/init/index.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  generateDbPassword,
10
10
  generateDbUrl,
11
11
  } from '../setup/fullstack-secrets.js';
12
- import type { ComposeServiceName } from '../types.js';
12
+ import type { ComposeServiceName, EventsBackend } from '../types.js';
13
13
  import { generateAuthAppFiles } from './generators/auth.js';
14
14
  import { generateConfigFiles } from './generators/config.js';
15
15
  import {
@@ -27,6 +27,7 @@ import { generateWebAppFiles } from './generators/web.js';
27
27
  import {
28
28
  type DeployTarget,
29
29
  deployTargetChoices,
30
+ eventsBackendChoices,
30
31
  getTemplate,
31
32
  isFullstackTemplate,
32
33
  loggerTypeChoices,
@@ -110,6 +111,13 @@ export async function initCommand(
110
111
  choices: servicesChoices.map((c) => ({ ...c, selected: true })),
111
112
  hint: '- Space to select. Return to submit',
112
113
  },
114
+ {
115
+ type: options.yes ? null : 'select',
116
+ name: 'eventsBackend',
117
+ message: 'Event backend:',
118
+ choices: eventsBackendChoices,
119
+ initial: 0,
120
+ },
113
121
  {
114
122
  type: options.yes ? null : 'select',
115
123
  name: 'packageManager',
@@ -183,13 +191,27 @@ export async function initCommand(
183
191
  const servicesArray: string[] = options.yes
184
192
  ? ['db', 'cache', 'mail', 'storage']
185
193
  : answers.services || [];
194
+
195
+ // Determine events backend (default to pgboss for fullstack with --yes)
196
+ const eventsBackend: EventsBackend | undefined = options.yes
197
+ ? isFullstack
198
+ ? 'pgboss'
199
+ : undefined
200
+ : answers.eventsBackend;
201
+
186
202
  const services: ServicesSelection = {
187
203
  db: servicesArray.includes('db'),
188
204
  cache: servicesArray.includes('cache'),
189
205
  mail: servicesArray.includes('mail'),
190
206
  storage: servicesArray.includes('storage'),
207
+ events: eventsBackend,
191
208
  };
192
209
 
210
+ // pgboss requires postgres
211
+ if (services.events === 'pgboss') {
212
+ services.db = true;
213
+ }
214
+
193
215
  const pkgManager: PackageManager = options.pm
194
216
  ? options.pm
195
217
  : options.yes
@@ -333,9 +355,12 @@ export async function initCommand(
333
355
  if (services.cache) secretServices.push('redis');
334
356
  if (services.storage) secretServices.push('minio');
335
357
  if (services.mail) secretServices.push('mailpit');
358
+ if (services.events === 'sns') secretServices.push('localstack');
359
+ if (services.events === 'rabbitmq') secretServices.push('rabbitmq');
336
360
 
337
361
  const devSecrets = createStageSecrets('development', secretServices, {
338
362
  projectName: name,
363
+ eventsBackend: services.events,
339
364
  });
340
365
 
341
366
  // Add common custom secrets
@@ -39,6 +39,7 @@ export interface ServicesSelection {
39
39
  cache: boolean;
40
40
  mail: boolean;
41
41
  storage: boolean;
42
+ events?: import('../../types.js').EventsBackend;
42
43
  }
43
44
 
44
45
  /**
@@ -255,6 +256,33 @@ export const servicesChoices = [
255
256
  },
256
257
  ];
257
258
 
259
+ /**
260
+ * Event backend choices for prompts
261
+ */
262
+ export const eventsBackendChoices = [
263
+ {
264
+ title: 'pg-boss',
265
+ value: 'pgboss' as const,
266
+ description:
267
+ 'PostgreSQL-based job queue (reuses postgres, no extra container)',
268
+ },
269
+ {
270
+ title: 'SNS/SQS',
271
+ value: 'sns' as const,
272
+ description: 'AWS SNS+SQS via LocalStack for local dev',
273
+ },
274
+ {
275
+ title: 'RabbitMQ',
276
+ value: 'rabbitmq' as const,
277
+ description: 'AMQP message broker',
278
+ },
279
+ {
280
+ title: 'None',
281
+ value: undefined,
282
+ description: 'Skip event backend',
283
+ },
284
+ ];
285
+
258
286
  /**
259
287
  * Get a template by name
260
288
  */
@@ -35,7 +35,7 @@ export const GEEKMIDAS_VERSIONS = {
35
35
  '@geekmidas/constructs': '~3.0.2',
36
36
  '@geekmidas/db': '~1.0.0',
37
37
  '@geekmidas/emailkit': '~1.0.0',
38
- '@geekmidas/envkit': '~1.0.3',
38
+ '@geekmidas/envkit': '~1.0.4',
39
39
  '@geekmidas/errors': '~1.0.0',
40
40
  '@geekmidas/events': '~1.1.0',
41
41
  '@geekmidas/logger': '~1.0.0',
@@ -2,7 +2,11 @@ import { describe, expect, it } from 'vitest';
2
2
  import {
3
3
  createStageSecrets,
4
4
  generateConnectionUrls,
5
+ generateEventConnectionStrings,
6
+ generateLocalStackAccessKeyId,
7
+ generateLocalStackCredentials,
5
8
  generateMinioEndpoint,
9
+ generatePgBossUrl,
6
10
  generatePostgresUrl,
7
11
  generateRabbitmqUrl,
8
12
  generateRedisUrl,
@@ -385,3 +389,182 @@ describe('rotateServicePassword', () => {
385
389
  expect(rotated.services.rabbitmq).toEqual(original.services.rabbitmq);
386
390
  });
387
391
  });
392
+
393
+ describe('generateLocalStackAccessKeyId', () => {
394
+ it('should start with LSIA prefix', () => {
395
+ const keyId = generateLocalStackAccessKeyId();
396
+ expect(keyId).toMatch(/^LSIA/);
397
+ });
398
+
399
+ it('should be at least 20 characters', () => {
400
+ const keyId = generateLocalStackAccessKeyId();
401
+ expect(keyId.length).toBeGreaterThanOrEqual(20);
402
+ });
403
+
404
+ it('should generate unique keys', () => {
405
+ const key1 = generateLocalStackAccessKeyId();
406
+ const key2 = generateLocalStackAccessKeyId();
407
+ expect(key1).not.toBe(key2);
408
+ });
409
+ });
410
+
411
+ describe('generatePgBossUrl', () => {
412
+ it('should generate pgboss connection URL', () => {
413
+ const creds: ServiceCredentials = {
414
+ host: 'localhost',
415
+ port: 5432,
416
+ username: 'pgboss',
417
+ password: 'secret',
418
+ database: 'myapp_dev',
419
+ };
420
+
421
+ const url = generatePgBossUrl(creds);
422
+ expect(url).toBe(
423
+ 'pgboss://pgboss:secret@localhost:5432/myapp_dev?schema=pgboss',
424
+ );
425
+ });
426
+
427
+ it('should encode special characters in password', () => {
428
+ const creds: ServiceCredentials = {
429
+ host: 'localhost',
430
+ port: 5432,
431
+ username: 'pgboss',
432
+ password: 'p@ss/word',
433
+ database: 'app',
434
+ };
435
+
436
+ const url = generatePgBossUrl(creds);
437
+ expect(url).toContain('p%40ss%2Fword');
438
+ });
439
+ });
440
+
441
+ describe('generateLocalStackCredentials', () => {
442
+ it('should generate credentials with LSIA-prefixed access key', () => {
443
+ const creds = generateLocalStackCredentials();
444
+ expect(creds.accessKeyId).toMatch(/^LSIA/);
445
+ expect(creds.host).toBe('localhost');
446
+ expect(creds.port).toBe(4566);
447
+ expect(creds.region).toBe('us-east-1');
448
+ expect(creds.password).toHaveLength(32);
449
+ });
450
+ });
451
+
452
+ describe('generateEventConnectionStrings', () => {
453
+ it('should generate pgboss connection strings', () => {
454
+ const services: StageSecrets['services'] = {
455
+ pgboss: {
456
+ host: 'localhost',
457
+ port: 5432,
458
+ username: 'pgboss',
459
+ password: 'secret',
460
+ database: 'myapp_dev',
461
+ },
462
+ };
463
+
464
+ const result = generateEventConnectionStrings('pgboss', services);
465
+ expect(result.publisher).toContain('pgboss://');
466
+ expect(result.subscriber).toContain('pgboss://');
467
+ expect(result.publisher).toBe(result.subscriber);
468
+ });
469
+
470
+ it('should generate sns/sqs connection strings', () => {
471
+ const services: StageSecrets['services'] = {
472
+ localstack: {
473
+ host: 'localhost',
474
+ port: 4566,
475
+ username: 'localstack',
476
+ password: 'secret',
477
+ accessKeyId: 'LSIAtest1234567890xx',
478
+ region: 'us-east-1',
479
+ },
480
+ };
481
+
482
+ const result = generateEventConnectionStrings('sns', services);
483
+ expect(result.publisher).toContain('sns://');
484
+ expect(result.subscriber).toContain('sqs://');
485
+ expect(result.publisher).toContain('LSIAtest1234567890xx');
486
+ });
487
+
488
+ it('should generate rabbitmq connection strings', () => {
489
+ const services: StageSecrets['services'] = {
490
+ rabbitmq: {
491
+ host: 'localhost',
492
+ port: 5672,
493
+ username: 'app',
494
+ password: 'secret',
495
+ vhost: '/',
496
+ },
497
+ };
498
+
499
+ const result = generateEventConnectionStrings('rabbitmq', services);
500
+ expect(result.publisher).toContain('amqp://');
501
+ expect(result.subscriber).toContain('amqp://');
502
+ expect(result.publisher).toBe(result.subscriber);
503
+ });
504
+
505
+ it('should throw if pgboss credentials missing', () => {
506
+ expect(() => generateEventConnectionStrings('pgboss', {})).toThrow(
507
+ 'pgboss credentials required',
508
+ );
509
+ });
510
+
511
+ it('should throw if localstack credentials missing', () => {
512
+ expect(() => generateEventConnectionStrings('sns', {})).toThrow(
513
+ 'localstack credentials required',
514
+ );
515
+ });
516
+ });
517
+
518
+ describe('createStageSecrets with events', () => {
519
+ it('should create pgboss credentials when eventsBackend is pgboss', () => {
520
+ const secrets = createStageSecrets('development', ['postgres'], {
521
+ eventsBackend: 'pgboss',
522
+ });
523
+
524
+ expect(secrets.eventsBackend).toBe('pgboss');
525
+ expect(secrets.services.pgboss).toBeDefined();
526
+ expect(secrets.services.pgboss!.username).toBe('pgboss');
527
+ expect(secrets.services.pgboss!.host).toBe(secrets.services.postgres!.host);
528
+ expect(secrets.services.pgboss!.database).toBe(
529
+ secrets.services.postgres!.database,
530
+ );
531
+ expect(secrets.urls.EVENT_PUBLISHER_CONNECTION_STRING).toContain(
532
+ 'pgboss://',
533
+ );
534
+ expect(secrets.urls.EVENT_SUBSCRIBER_CONNECTION_STRING).toContain(
535
+ 'pgboss://',
536
+ );
537
+ });
538
+
539
+ it('should create localstack credentials when eventsBackend is sns', () => {
540
+ const secrets = createStageSecrets('development', [], {
541
+ eventsBackend: 'sns',
542
+ });
543
+
544
+ expect(secrets.eventsBackend).toBe('sns');
545
+ expect(secrets.services.localstack).toBeDefined();
546
+ expect(secrets.services.localstack!.accessKeyId).toMatch(/^LSIA/);
547
+ expect(secrets.urls.EVENT_PUBLISHER_CONNECTION_STRING).toContain('sns://');
548
+ expect(secrets.urls.EVENT_SUBSCRIBER_CONNECTION_STRING).toContain('sqs://');
549
+ });
550
+
551
+ it('should use rabbitmq credentials when eventsBackend is rabbitmq', () => {
552
+ const secrets = createStageSecrets('development', ['rabbitmq'], {
553
+ eventsBackend: 'rabbitmq',
554
+ });
555
+
556
+ expect(secrets.eventsBackend).toBe('rabbitmq');
557
+ expect(secrets.urls.EVENT_PUBLISHER_CONNECTION_STRING).toContain('amqp://');
558
+ expect(secrets.urls.EVENT_SUBSCRIBER_CONNECTION_STRING).toContain(
559
+ 'amqp://',
560
+ );
561
+ });
562
+
563
+ it('should not create event URLs without eventsBackend', () => {
564
+ const secrets = createStageSecrets('development', ['postgres']);
565
+
566
+ expect(secrets.eventsBackend).toBeUndefined();
567
+ expect(secrets.urls.EVENT_PUBLISHER_CONNECTION_STRING).toBeUndefined();
568
+ expect(secrets.urls.EVENT_SUBSCRIBER_CONNECTION_STRING).toBeUndefined();
569
+ });
570
+ });