@soapjs/cli 1.0.0 → 1.0.2

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 (55) hide show
  1. package/README.md +3 -0
  2. package/build/cli.js +2 -3
  3. package/build/commands/add/add.command.js +1 -2
  4. package/build/commands/add/command-plan.js +7 -8
  5. package/build/commands/add/entity-plan.js +1 -2
  6. package/build/commands/add/event-plan.js +1 -2
  7. package/build/commands/add/query-plan.js +6 -7
  8. package/build/commands/add/repository-plan.js +8 -9
  9. package/build/commands/add/resource-plan.js +20 -21
  10. package/build/commands/add/route-plan.js +7 -7
  11. package/build/commands/add/socket-plan.js +2 -3
  12. package/build/commands/add/use-case-plan.js +2 -3
  13. package/build/commands/check/check.command.js +1 -2
  14. package/build/commands/create/create.command.js +2 -3
  15. package/build/commands/create/project-plan.js +272 -227
  16. package/build/commands/doctor/doctor.command.js +1 -2
  17. package/build/commands/generate/bruno-analysis.js +4 -5
  18. package/build/commands/generate/bruno-plan.js +1 -2
  19. package/build/commands/generate/generate.command.js +1 -2
  20. package/build/commands/info/info.command.js +1 -2
  21. package/build/commands/remove/remove.command.js +1 -2
  22. package/build/commands/shared/common-options.js +3 -4
  23. package/build/commands/update/update.command.js +1 -2
  24. package/build/config/auth-policy.js +3 -4
  25. package/build/config/find-soap-root.js +1 -2
  26. package/build/config/load-soap-config.js +1 -2
  27. package/build/config/schemas/types.d.ts +1 -1
  28. package/build/config/schemas/validation.js +5 -6
  29. package/build/config/write-soap-config.js +1 -2
  30. package/build/core/command-context.js +2 -3
  31. package/build/core/errors.js +2 -2
  32. package/build/core/output.js +1 -2
  33. package/build/core/result.js +2 -3
  34. package/build/dependencies/dependency-resolver.js +12 -17
  35. package/build/dependencies/package-manager.js +3 -4
  36. package/build/io/conflict-policy.js +3 -4
  37. package/build/io/file-writer.js +1 -2
  38. package/build/io/format-file.js +1 -2
  39. package/build/presets/create-presets.js +3 -3
  40. package/build/prompts/add-resource.prompt.js +1 -2
  41. package/build/prompts/add-route.prompt.js +1 -2
  42. package/build/prompts/create-project.prompt.js +3 -2
  43. package/build/prompts/generate-bruno.prompt.js +1 -2
  44. package/build/prompts/inquirer-prompt-adapter.js +14 -5
  45. package/build/registry/registry.service.js +5 -6
  46. package/build/summary/create-summary.js +1 -2
  47. package/build/templates/naming.js +2 -3
  48. package/build/templates/template-engine.js +1 -2
  49. package/build/templates/template-resolver.js +2 -3
  50. package/build/terminal/terminal-capabilities.js +2 -3
  51. package/docs/cli/create.md +4 -1
  52. package/docs/guides/auth.md +9 -1
  53. package/docs/guides/index.md +1 -0
  54. package/docs/plans/KAFKA_1_0_UPDATE.md +30 -0
  55. package/package.json +3 -3
@@ -3,7 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createControllersTs = exports.createResourcesTs = exports.createProjectFiles = exports.targetRoot = exports.zonesOptions = exports.contractsOptions = exports.docsOptions = exports.telemetryOptions = exports.realtimeOptions = exports.messagingOptions = exports.authOptions = exports.databaseOptions = exports.parseCsvOption = exports.createSoapConfigBundle = exports.createDefaultCapabilities = void 0;
6
+ exports.zonesOptions = exports.contractsOptions = exports.docsOptions = exports.telemetryOptions = exports.realtimeOptions = exports.messagingOptions = exports.authOptions = exports.databaseOptions = void 0;
7
+ exports.createDefaultCapabilities = createDefaultCapabilities;
8
+ exports.createSoapConfigBundle = createSoapConfigBundle;
9
+ exports.parseCsvOption = parseCsvOption;
10
+ exports.targetRoot = targetRoot;
11
+ exports.createProjectFiles = createProjectFiles;
12
+ exports.createResourcesTs = createResourcesTs;
13
+ exports.createControllersTs = createControllersTs;
7
14
  const path_1 = __importDefault(require("path"));
8
15
  const naming_1 = require("../../templates/naming");
9
16
  function createDefaultCapabilities() {
@@ -18,7 +25,6 @@ function createDefaultCapabilities() {
18
25
  contracts: [],
19
26
  };
20
27
  }
21
- exports.createDefaultCapabilities = createDefaultCapabilities;
22
28
  function createSoapConfigBundle(plan) {
23
29
  const defaultAuth = plan.capabilities.auth[0] ?? "none";
24
30
  const brunoEnabled = plan.capabilities.apiClient.includes("bruno");
@@ -79,7 +85,6 @@ function createSoapConfigBundle(plan) {
79
85
  },
80
86
  };
81
87
  }
82
- exports.createSoapConfigBundle = createSoapConfigBundle;
83
88
  function parseCsvOption(value, allowed) {
84
89
  if (!value) {
85
90
  return [];
@@ -93,13 +98,12 @@ function parseCsvOption(value, allowed) {
93
98
  }
94
99
  return Array.from(new Set(normalized));
95
100
  }
96
- exports.parseCsvOption = parseCsvOption;
97
101
  exports.databaseOptions = ["mongo", "postgres", "mysql", "sqlite", "redis"];
98
102
  const sqlDatabaseOptions = ["postgres", "mysql", "sqlite"];
99
103
  exports.authOptions = ["jwt", "api-key", "local"];
100
104
  exports.messagingOptions = ["in-memory", "kafka"];
101
105
  exports.realtimeOptions = ["ws"];
102
- exports.telemetryOptions = ["logs", "otel-noop"];
106
+ exports.telemetryOptions = ["logs", "otel-noop", "metrics", "memory"];
103
107
  exports.docsOptions = ["openapi"];
104
108
  exports.contractsOptions = ["zod"];
105
109
  exports.zonesOptions = ["public", "private", "admin"];
@@ -109,7 +113,6 @@ function enabledSqlDatabases(capabilities) {
109
113
  function targetRoot(cwd, name) {
110
114
  return path_1.default.resolve(cwd, name);
111
115
  }
112
- exports.targetRoot = targetRoot;
113
116
  function createProjectFiles(plan) {
114
117
  const packageJson = {
115
118
  name: plan.name,
@@ -243,11 +246,16 @@ function createProjectFiles(plan) {
243
246
  ...createBrunoFiles(plan),
244
247
  ];
245
248
  }
246
- exports.createProjectFiles = createProjectFiles;
247
249
  function createIndexTs(plan) {
248
250
  const docsImports = plan.capabilities.docs.includes("openapi")
249
251
  ? "import { DocumentationPlugin } from '@soapjs/soap-openapi';\n"
250
252
  : "";
253
+ const expressImport = plan.capabilities.telemetry.includes("memory")
254
+ ? "import { createApp, MemoryMonitoringPlugin } from '@soapjs/soap-express';"
255
+ : "import { createApp } from '@soapjs/soap-express';";
256
+ const authImport = plan.capabilities.auth.length > 0
257
+ ? "import { createAuthRouter } from '@soapjs/soap-express/auth';\n"
258
+ : "";
251
259
  const socketImport = plan.capabilities.realtime.includes("ws")
252
260
  ? "import { createSocketRuntime } from './common/sockets/socket.setup';\n"
253
261
  : "";
@@ -256,32 +264,51 @@ function createIndexTs(plan) {
256
264
  ? `\n plugins: [\n {\n plugin: new DocumentationPlugin(),\n options: {\n info: {\n title: '${plan.name} API',\n version: '0.1.0',\n },\n servers: [{ url: \`http://localhost:\${config.port}\`, description: 'Local' }],\n interactivePath: '/docs',\n openApiPath: '/openapi.json',\n },\n },\n ],`
257
265
  : "";
258
266
  const cqrsOption = plan.architecture === "cqrs" ? "\n cqrs: true," : "";
267
+ const authRegistration = plan.capabilities.auth.length > 0
268
+ ? `\n app.registerAuth(auth);\n app.getApp().use(createAuthRouter(auth, {\n basePath: '/auth',\n strategy: '${defaultAuthStrategy(plan)}',\n routes: {\n login: { path: '/auth/login', strategy: '${loginAuthStrategy(plan)}' },\n logout: { path: '/auth/logout', strategy: '${loginAuthStrategy(plan)}' },\n refresh: { path: '/auth/refresh', strategy: 'jwt', enabled: ${usesJwtAuth(plan)} },\n me: { path: '/auth/me', strategy: '${defaultAuthStrategy(plan)}' },\n verify: { path: '/auth/verify', strategy: '${defaultAuthStrategy(plan)}' },\n },\n }));\n`
269
+ : "";
270
+ const metrics = plan.capabilities.telemetry.includes("metrics")
271
+ ? `\n app.useMetrics({\n enabled: true,\n exposeEndpoint: true,\n metricsPath: '/metrics',\n metricsFormat: 'prometheus',\n });\n`
272
+ : "";
273
+ const memory = plan.capabilities.telemetry.includes("memory")
274
+ ? `\n const memoryMonitoringOptions = {\n enabled: true,\n exposeEndpoints: true,\n basePath: '/memory',\n includeInRequest: false,\n };\n let memoryMonitoringPlugin: MemoryMonitoringPlugin | undefined;\n\n try {\n app.useMemoryMonitoring(memoryMonitoringOptions);\n } catch (error) {\n memoryMonitoringPlugin = new MemoryMonitoringPlugin(memoryMonitoringOptions, logger);\n (memoryMonitoringPlugin as { version?: string }).version ??= '0.1.0';\n app.usePlugin(memoryMonitoringPlugin, memoryMonitoringOptions);\n }\n\n const getMemorySummary = () => app.getMemorySummary() ?? memoryMonitoringPlugin?.getMemorySummary();\n app.getApp().get('/memory', (_req, res) => res.json(getMemorySummary()));\n app.getApp().get('/memory/summary', (_req, res) => res.json(getMemorySummary()));\n app.getApp().get('/memory/health', (_req, res) => {\n const summary = getMemorySummary();\n res.status(summary?.status === 'critical' ? 503 : 200).json({ status: summary?.status ?? 'unknown', summary });\n });\n`
275
+ : "";
276
+ const socketRuntime = plan.capabilities.realtime.includes("ws")
277
+ ? `\n const socketRuntime = createSocketRuntime(config, app.getServer());\n app.registerDrainable(socketRuntime);\n`
278
+ : "";
259
279
  return `import 'reflect-metadata';
260
- import { bootstrap } from '@soapjs/soap-express';
261
- ${docsImports}${socketImport}${cqrsImport}import './features';
280
+ ${expressImport}
281
+ ${docsImports}${authImport}${socketImport}${cqrsImport}import './features';
262
282
  import { config } from './config/config';
263
283
  import { controllers } from './config/controllers';
264
284
  import { buildContainer } from './config/dependencies';
265
285
 
266
286
  async function main(): Promise<void> {
267
- const { container, drainables, logger, authStrategies } = await buildContainer(config);
287
+ const { container, drainables, logger, auth } = await buildContainer(config);
268
288
 
269
- const app = await bootstrap({
270
- port: config.port,
289
+ const app = createApp({
271
290
  container,
272
291
  logger,
273
292
  drainables,
274
293
  controllers,
275
294
  middleware: {
276
- cors: true,
277
- helmet: true,
278
295
  logging: true,
279
296
  compression: true,
280
297
  },
281
- auth: authStrategies,
298
+ app: {
299
+ security: ${createSecurityConfig(plan)},
300
+ },
282
301
  healthCheck: true,${cqrsOption}${docsPlugin}
283
302
  });
284
- ${plan.capabilities.realtime.includes("ws") ? `\n const socketRuntime = createSocketRuntime(config, app.getServer());\n app.registerDrainable(socketRuntime);\n` : ""}
303
+ ${authRegistration}${metrics}${memory}
304
+
305
+ const cqrsReady = (app as unknown as { cqrsReady?: Promise<void> }).cqrsReady;
306
+ if (cqrsReady) {
307
+ await cqrsReady;
308
+ }
309
+
310
+ await app.start(config.port);
311
+ ${socketRuntime}
285
312
 
286
313
  logger.info('${plan.name} API ready', { port: config.port });
287
314
  }
@@ -292,6 +319,41 @@ main().catch((error) => {
292
319
  });
293
320
  `;
294
321
  }
322
+ function defaultAuthStrategy(plan) {
323
+ if (plan.capabilities.auth.includes("jwt"))
324
+ return "jwt";
325
+ if (plan.capabilities.auth.includes("local"))
326
+ return "local";
327
+ if (plan.capabilities.auth.includes("api-key"))
328
+ return "api-key";
329
+ return "jwt";
330
+ }
331
+ function loginAuthStrategy(plan) {
332
+ if (plan.capabilities.auth.includes("local") || plan.capabilities.auth.includes("jwt"))
333
+ return "local";
334
+ if (plan.capabilities.auth.includes("api-key"))
335
+ return "api-key";
336
+ return defaultAuthStrategy(plan);
337
+ }
338
+ function createSecurityConfig(plan) {
339
+ const throttle = plan.capabilities.auth.length > 0
340
+ ? `{
341
+ global: { windowMs: 60_000, max: 300 },
342
+ routes: {
343
+ 'POST /auth/login': { windowMs: 60_000, max: 5 },
344
+ 'POST /auth/refresh': { windowMs: 60_000, max: 20, keyBy: 'user' },
345
+ 'GET /auth/oauth/:provider/callback': { windowMs: 60_000, max: 30 },
346
+ },
347
+ }`
348
+ : "{ global: { windowMs: 60_000, max: 300 } }";
349
+ return `{
350
+ disablePoweredBy: true,
351
+ trustProxy: 1,
352
+ helmet: true,
353
+ cors: true,
354
+ throttle: ${throttle},
355
+ }`;
356
+ }
295
357
  function createConfigTs(plan) {
296
358
  const mongo = plan.capabilities.databases.includes("mongo")
297
359
  ? "\n mongoUri: readEnv('MONGO_URI', 'mongodb://localhost:27017/app'),"
@@ -314,10 +376,10 @@ function createConfigTs(plan) {
314
376
  const sockets = plan.capabilities.realtime.includes("ws")
315
377
  ? "\n wsPath: readEnv('WS_PATH', '/ws'),\n wsHeartbeatMs: Number(readEnv('WS_HEARTBEAT_MS', '30000')),"
316
378
  : "";
317
- const jwt = plan.capabilities.auth.includes("jwt") || plan.capabilities.auth.includes("local")
318
- ? "\n jwtSecret: readEnv('JWT_SECRET', 'dev-secret'),"
379
+ const jwt = plan.capabilities.auth.length > 0
380
+ ? "\n jwtAccessSecret: readEnv('JWT_ACCESS_SECRET', 'dev-access-secret'),\n jwtRefreshSecret: readEnv('JWT_REFRESH_SECRET', 'dev-refresh-secret'),"
319
381
  : "";
320
- const apiKey = plan.capabilities.auth.includes("api-key")
382
+ const apiKey = plan.capabilities.auth.length > 0
321
383
  ? "\n apiKeyHeader: readEnv('API_KEY_HEADER', 'x-api-key'),\n devApiKey: readEnv('DEV_API_KEY', 'dev-api-key'),"
322
384
  : "";
323
385
  return `import 'dotenv/config';
@@ -343,21 +405,21 @@ export type AppConfig = typeof config;
343
405
  }
344
406
  function createDependenciesTs(plan) {
345
407
  const authImport = plan.capabilities.auth.length > 0
346
- ? "import { createAuthStrategies } from '../features/auth/auth.setup';\n"
408
+ ? "import { SoapAuth } from '@soapjs/soap-auth';\nimport { createAuthProvider } from '../features/auth/auth.setup';\n"
347
409
  : "";
348
410
  const mongoImport = plan.capabilities.databases.includes("mongo")
349
- ? "import { SoapMongo } from '@soapjs/soap-node-mongo';\nimport { createMongoClient } from '../common/data/mongo/mongo.client';\n"
411
+ ? "import { SoapMongo } from '@soapjs/soap-mongo';\nimport { createMongoClient } from '../common/data/mongo/mongo.client';\n"
350
412
  : "";
351
413
  const sqlDatabases = enabledSqlDatabases(plan.capabilities);
352
414
  const sqlImport = sqlDatabases.length > 0
353
- ? `import { SoapSQL } from '@soapjs/soap-node-sql';
415
+ ? `import { SoapSQL } from '@soapjs/soap-sql';
354
416
  ${sqlDatabases.map((database) => `import { create${(0, naming_1.createNameVariants)(database).pascalName}Client } from '../common/data/${database}/${database}.client';`).join("\n")}
355
417
  `
356
418
  : "";
357
419
  const eventImport = plan.capabilities.messaging.length > 0
358
420
  ? "import { DomainEventBus } from '@soapjs/soap/cqrs';\nimport { createEventBus } from '../common/events/event-bus.setup';\n"
359
421
  : "";
360
- const authStrategies = plan.capabilities.auth.length > 0 ? "createAuthStrategies(_config)" : "[]";
422
+ const authProvider = plan.capabilities.auth.length > 0 ? "await createAuthProvider(_config, logger)" : "undefined";
361
423
  const resourceContextType = [
362
424
  plan.capabilities.databases.includes("mongo") ? " mongo?: SoapMongo;" : undefined,
363
425
  sqlDatabases.length > 0 ? " sql?: Partial<Record<'postgres' | 'mysql' | 'sqlite', SoapSQL>>;" : undefined,
@@ -377,9 +439,9 @@ ${sqlDatabases.map((database) => {
377
439
  const eventSetup = plan.capabilities.messaging.length > 0
378
440
  ? `\n const eventBusRuntime = await createEventBus(_config, logger);\n container.bindValue(DomainEventBus.Token, eventBusRuntime.bus);\n if (eventBusRuntime.drainable) {\n drainables.push(eventBusRuntime.drainable);\n }\n`
379
441
  : "";
442
+ const authReturnType = plan.capabilities.auth.length > 0 ? " auth: SoapAuth;" : " auth?: undefined;";
380
443
  return `import { ConsoleLogger, DIContainer } from '@soapjs/soap-express';
381
444
  import { Drainable } from '@soapjs/soap/events';
382
- import { AuthStrategy } from '@soapjs/soap/http';
383
445
  import { AppConfig } from './config';
384
446
  import { registerResources } from './resources';
385
447
  ${mongoImport}${sqlImport}${eventImport}${authImport}
@@ -392,13 +454,13 @@ export async function buildContainer(_config: AppConfig): Promise<{
392
454
  container: DIContainer;
393
455
  drainables: Drainable[];
394
456
  logger: ConsoleLogger;
395
- authStrategies: AuthStrategy[];
457
+ ${authReturnType}
396
458
  }> {
397
459
  const container = new DIContainer();
398
460
  const logger = new ConsoleLogger();
399
461
  const drainables: Drainable[] = [];
400
462
  const resources: ResourceContext = {};
401
- const authStrategies: AuthStrategy[] = ${authStrategies};
463
+ const auth = ${authProvider};
402
464
  ${mongoSetup}${sqlSetup}${eventSetup}
403
465
 
404
466
  await registerResources(container, resources);
@@ -407,7 +469,7 @@ ${mongoSetup}${sqlSetup}${eventSetup}
407
469
  container,
408
470
  drainables,
409
471
  logger,
410
- authStrategies,
472
+ auth,
411
473
  };
412
474
  }
413
475
  `;
@@ -432,7 +494,6 @@ ${resources.map((resource) => ` await ${resource.functionName}(container, resou
432
494
  }
433
495
  `;
434
496
  }
435
- exports.createResourcesTs = createResourcesTs;
436
497
  function createMongoFiles(plan) {
437
498
  if (!plan.capabilities.databases.includes("mongo")) {
438
499
  return [];
@@ -441,7 +502,7 @@ function createMongoFiles(plan) {
441
502
  {
442
503
  path: "src/config/mongo.config.ts",
443
504
  type: "config",
444
- content: `import { MongoConfig } from '@soapjs/soap-node-mongo';
505
+ content: `import { MongoConfig } from '@soapjs/soap-mongo';
445
506
  import { AppConfig } from './config';
446
507
 
447
508
  export function createMongoConfig(config: Pick<AppConfig, 'mongoUri'>): MongoConfig {
@@ -462,7 +523,7 @@ export function createMongoConfig(config: Pick<AppConfig, 'mongoUri'>): MongoCon
462
523
  {
463
524
  path: "src/common/data/mongo/mongo.client.ts",
464
525
  type: "config",
465
- content: `import { SoapMongo } from '@soapjs/soap-node-mongo';
526
+ content: `import { SoapMongo } from '@soapjs/soap-mongo';
466
527
  import { AppConfig } from '../../../config/config';
467
528
  import { createMongoConfig } from '../../../config/mongo.config';
468
529
 
@@ -474,7 +535,7 @@ export async function createMongoClient(config: AppConfig): Promise<SoapMongo> {
474
535
  {
475
536
  path: "src/common/data/mongo/mongo.source-factory.ts",
476
537
  type: "config",
477
- content: `import { MongoSource, SoapMongo } from '@soapjs/soap-node-mongo';
538
+ content: `import { MongoSource, SoapMongo } from '@soapjs/soap-mongo';
478
539
  import { Document } from 'mongodb';
479
540
 
480
541
  export function createMongoSource<T extends Document = Document>(mongo: SoapMongo, collectionName: string): MongoSource<T> {
@@ -494,7 +555,7 @@ function createSqlFiles(plan) {
494
555
  {
495
556
  path: "src/common/data/sql/sql.source-factory.ts",
496
557
  type: "config",
497
- content: `import { SoapSQL, SqlDataSource } from '@soapjs/soap-node-sql';
558
+ content: `import { SoapSQL, SqlDataSource } from '@soapjs/soap-sql';
498
559
 
499
560
  export function createSqlSource<T = Record<string, unknown>>(sql: SoapSQL, tableName: string): SqlDataSource<T> {
500
561
  return new SqlDataSource<T>(sql, tableName);
@@ -515,7 +576,7 @@ function createSqlDatabaseFiles(database) {
515
576
  {
516
577
  path: `src/config/${database}.config.ts`,
517
578
  type: "config",
518
- content: `import { SqlDatabaseConfig } from '@soapjs/soap-node-sql';
579
+ content: `import { SqlDatabaseConfig } from '@soapjs/soap-sql';
519
580
  import { AppConfig } from './config';
520
581
 
521
582
  export function create${names.pascalName}Config(config: Pick<AppConfig, '${configProperty}'>): SqlDatabaseConfig {
@@ -529,7 +590,7 @@ ${createSqlConfigProperties(database, configProperty)}
529
590
  {
530
591
  path: `src/common/data/${database}/${database}.client.ts`,
531
592
  type: "config",
532
- content: `import { SoapSQL } from '@soapjs/soap-node-sql';
593
+ content: `import { SoapSQL } from '@soapjs/soap-sql';
533
594
  import { AppConfig } from '../../../config/config';
534
595
  import { create${names.pascalName}Config } from '../../../config/${database}.config';
535
596
 
@@ -543,6 +604,11 @@ export async function create${names.pascalName}Client(config: AppConfig): Promis
543
604
  function createSqlConfigProperties(database, configProperty) {
544
605
  if (database === "sqlite") {
545
606
  return ` type: 'sqlite',
607
+ host: 'localhost',
608
+ port: 0,
609
+ database: config.${configProperty}.filename,
610
+ username: '',
611
+ password: '',
546
612
  filename: config.${configProperty}.filename,`;
547
613
  }
548
614
  const sqlType = database === "postgres" ? "postgresql" : "mysql";
@@ -563,14 +629,6 @@ function createCqrsFiles(plan) {
563
629
  type: "config",
564
630
  content: `// Generated CQRS handler imports. Updated by soap add command/query.
565
631
  export {};
566
- `,
567
- },
568
- {
569
- path: "src/types/soap-express-cqrs.d.ts",
570
- type: "config",
571
- content: `declare module '@soapjs/soap-express/cqrs' {
572
- export * from '@soapjs/soap-express/build/cqrs';
573
- }
574
632
  `,
575
633
  },
576
634
  ];
@@ -653,7 +711,7 @@ export function createKafkaConfig(config: Pick<AppConfig, 'kafkaBrokers'>) {
653
711
  }, {
654
712
  path: "src/common/events/kafka/kafka.client.ts",
655
713
  type: "config",
656
- content: `import { KafkaEventBus } from '@soapjs/soap-node-kafka';
714
+ content: `import { KafkaEventBus } from '@soapjs/soap-kafka';
657
715
  import { AppConfig } from '../../../config/config';
658
716
  import { createKafkaConfig } from '../../../config/kafka.config';
659
717
 
@@ -664,7 +722,7 @@ export function createKafkaClient(config: AppConfig): KafkaEventBus<Record<strin
664
722
  }, {
665
723
  path: "src/common/events/kafka/kafka-event-bus.ts",
666
724
  type: "config",
667
- content: `import { KafkaDomainEventBus } from '@soapjs/soap-node-kafka';
725
+ content: `import { KafkaDomainEventBus } from '@soapjs/soap-kafka';
668
726
  import { Logger } from '@soapjs/soap/common';
669
727
  import { AppConfig } from '../../../config/config';
670
728
  import { createKafkaClient } from './kafka.client';
@@ -699,7 +757,7 @@ export const socketHandlers: AppSocketHandler[] = [];
699
757
  type: "config",
700
758
  content: `import { Server } from 'http';
701
759
  import { Drainable } from '@soapjs/soap/events';
702
- import { SocketMessage, SocketServer, WebSocketServerAdapter } from '@soapjs/soap-node-socket';
760
+ import { SocketMessage, SocketServer, WebSocketServerAdapter } from '@soapjs/soap-socket';
703
761
  import { AppConfig } from '../../config/config';
704
762
  import { socketHandlers } from '../../config/sockets';
705
763
 
@@ -763,17 +821,9 @@ ${names}
763
821
  ];
764
822
  `;
765
823
  }
766
- exports.createControllersTs = createControllersTs;
767
824
  function createInitialControllers(plan) {
768
- if (!usesJwtAuth(plan)) {
769
- return [];
770
- }
771
- return [
772
- {
773
- className: "AuthController",
774
- importPath: "../features/auth/api/auth.controller",
775
- },
776
- ];
825
+ void plan;
826
+ return [];
777
827
  }
778
828
  function createEnvExample(plan) {
779
829
  const lines = ["NODE_ENV=development", "PORT=3000", "LOG_LEVEL=debug"];
@@ -793,7 +843,7 @@ function createEnvExample(plan) {
793
843
  lines.push("REDIS_URL=redis://localhost:6379");
794
844
  }
795
845
  if (plan.capabilities.auth.includes("jwt") || plan.capabilities.auth.includes("local")) {
796
- lines.push("JWT_SECRET=change-me");
846
+ lines.push("JWT_ACCESS_SECRET=change-me-access", "JWT_REFRESH_SECRET=change-me-refresh");
797
847
  }
798
848
  if (plan.capabilities.auth.includes("api-key")) {
799
849
  lines.push("API_KEY_HEADER=x-api-key", "DEV_API_KEY=dev-api-key");
@@ -847,6 +897,7 @@ curl http://localhost:3000/openapi.json
847
897
  `
848
898
  : "";
849
899
  const authSection = createReadmeAuthSection(plan);
900
+ const monitoringSection = createReadmeMonitoringSection(plan);
850
901
  const addCommands = `## Add More Code
851
902
 
852
903
  \`\`\`bash
@@ -860,6 +911,8 @@ soap update config --add-api-client bruno
860
911
 
861
912
  Generated SoapJS service.
862
913
 
914
+ Requires Node.js 24.17.0 or newer. The generated runtime uses SoapJS 0.14, soap-auth 1.x, and the soap-express security/auth helpers.
915
+
863
916
  ## Capabilities
864
917
 
865
918
  - Framework: ${plan.framework}
@@ -892,7 +945,7 @@ Build:
892
945
  ${buildCommand}
893
946
  \`\`\`
894
947
 
895
- ${dockerSection}${brunoSection}${openApiSection}${authSection}## Folder Structure
948
+ ${dockerSection}${brunoSection}${openApiSection}${authSection}${monitoringSection}## Folder Structure
896
949
 
897
950
  \`\`\`txt
898
951
  src/
@@ -915,6 +968,25 @@ src/
915
968
  Generated metadata is stored in \`.soap/\`.
916
969
 
917
970
  ${addCommands}
971
+ `;
972
+ }
973
+ function createReadmeMonitoringSection(plan) {
974
+ const endpoints = [];
975
+ if (plan.capabilities.telemetry.includes("metrics")) {
976
+ endpoints.push("- Metrics: http://localhost:3000/metrics");
977
+ }
978
+ if (plan.capabilities.telemetry.includes("memory")) {
979
+ endpoints.push("- Memory monitoring: http://localhost:3000/memory");
980
+ }
981
+ if (endpoints.length === 0) {
982
+ return "";
983
+ }
984
+ return `## Monitoring
985
+
986
+ Monitoring endpoints are opt-in and were generated because telemetry includes \`${plan.capabilities.telemetry.filter((item) => item === "metrics" || item === "memory").join(", ")}\`.
987
+
988
+ ${endpoints.join("\n")}
989
+
918
990
  `;
919
991
  }
920
992
  function createReadmeAuthSection(plan) {
@@ -927,7 +999,7 @@ Default Bruno login variables:
927
999
  - email: \`admin@example.com\`
928
1000
  - password: \`admin123\`
929
1001
 
930
- Set \`JWT_SECRET\` in \`.env.example\` or your local \`.env\`.
1002
+ Auth is registered through \`SoapAuth.create(...)\` and \`createAuthRouter(...)\`. Set \`JWT_ACCESS_SECRET\` and \`JWT_REFRESH_SECRET\` in \`.env.example\` or your local \`.env\`.
931
1003
 
932
1004
  `);
933
1005
  }
@@ -939,12 +1011,17 @@ Default header and key:
939
1011
  - \`API_KEY_HEADER=x-api-key\`
940
1012
  - \`DEV_API_KEY=dev-api-key\`
941
1013
 
1014
+ API key auth uses \`createApiKeyAuthConfig(...)\` with a development \`retrieveUserByApiKey\` implementation.
1015
+
942
1016
  `);
943
1017
  }
944
- return sections.length > 0 ? `## Auth\n\n${sections.join("\n")}` : "";
1018
+ const security = sections.length > 0
1019
+ ? `Route-specific throttling is enabled for \`POST /auth/login\`, \`POST /auth/refresh\`, and OAuth callbacks through soap-express security config.\n\n`
1020
+ : "";
1021
+ return sections.length > 0 ? `## Auth\n\n${security}${sections.join("\n")}` : "";
945
1022
  }
946
1023
  function createDockerfile() {
947
- return `FROM node:20-alpine
1024
+ return `FROM node:24-alpine
948
1025
  WORKDIR /app
949
1026
  COPY package*.json ./
950
1027
  RUN npm install
@@ -1039,16 +1116,24 @@ function createAuthFiles(plan) {
1039
1116
  path: "src/features/auth/domain/auth-user.ts",
1040
1117
  type: "config",
1041
1118
  owner: "auth",
1042
- content: `export interface AuthUser {
1043
- id: string;
1044
- email: string;
1045
- name: string;
1046
- roles: string[];
1119
+ content: `import { AuthUser as SoapAuthUser } from '@soapjs/soap/http';
1120
+
1121
+ export interface AuthUser extends SoapAuthUser {
1122
+ name?: string;
1123
+ }
1124
+ `,
1125
+ },
1126
+ {
1127
+ path: "src/types/soap-express-auth.d.ts",
1128
+ type: "config",
1129
+ owner: "auth",
1130
+ content: `declare module '@soapjs/soap-express/auth' {
1131
+ export * from '@soapjs/soap-express/build/auth';
1047
1132
  }
1048
1133
  `,
1049
1134
  },
1050
1135
  ];
1051
- if (usesJwtAuth(plan)) {
1136
+ if (usesJwtAuth(plan) || plan.capabilities.auth.includes("api-key")) {
1052
1137
  files.push({
1053
1138
  path: "src/features/auth/data/dev-users.ts",
1054
1139
  type: "config",
@@ -1072,151 +1157,6 @@ export const devUsers: DevUser[] = [
1072
1157
  export function findDevUser(email: string): DevUser | undefined {
1073
1158
  return devUsers.find((user) => user.email === email);
1074
1159
  }
1075
- `,
1076
- }, {
1077
- path: "src/features/auth/auth.tokens.ts",
1078
- type: "config",
1079
- owner: "auth",
1080
- content: `import jwt from 'jsonwebtoken';
1081
- import { AuthUser } from './domain/auth-user';
1082
-
1083
- export interface AuthTokenPayload {
1084
- sub: string;
1085
- email: string;
1086
- name: string;
1087
- roles: string[];
1088
- }
1089
-
1090
- export function signAccessToken(user: AuthUser, secret: string): string {
1091
- return jwt.sign(
1092
- {
1093
- sub: user.id,
1094
- email: user.email,
1095
- name: user.name,
1096
- roles: user.roles,
1097
- },
1098
- secret,
1099
- { expiresIn: '1h' }
1100
- );
1101
- }
1102
-
1103
- export function verifyAccessToken(token: string, secret: string): AuthUser {
1104
- const payload = jwt.verify(token, secret) as AuthTokenPayload;
1105
-
1106
- return {
1107
- id: payload.sub,
1108
- email: payload.email,
1109
- name: payload.name,
1110
- roles: payload.roles ?? [],
1111
- };
1112
- }
1113
- `,
1114
- }, {
1115
- path: "src/features/auth/jwt.strategy.ts",
1116
- type: "config",
1117
- owner: "auth",
1118
- content: `import { AuthStrategy, HttpContext } from '@soapjs/soap/http';
1119
- import { verifyAccessToken } from './auth.tokens';
1120
- import { AuthUser } from './domain/auth-user';
1121
-
1122
- export class JwtAuthStrategy implements AuthStrategy<AuthUser> {
1123
- readonly name = 'jwt';
1124
-
1125
- constructor(private readonly secret: string) {}
1126
-
1127
- async authenticate(ctx: HttpContext) {
1128
- const authorization = ctx.req.headers.authorization;
1129
- const header = Array.isArray(authorization) ? authorization[0] : authorization;
1130
-
1131
- if (!header?.startsWith('Bearer ')) {
1132
- return null;
1133
- }
1134
-
1135
- const token = header.slice('Bearer '.length);
1136
- const user = verifyAccessToken(token, this.secret);
1137
-
1138
- return { user };
1139
- }
1140
- }
1141
- `,
1142
- }, {
1143
- path: "src/features/auth/api/auth.controller.ts",
1144
- type: "config",
1145
- owner: "auth",
1146
- content: `import { Request } from 'express';
1147
- import { Auth, Controller, Get, Post } from '@soapjs/soap-express';
1148
- import { config } from '../../../config/config';
1149
- import { signAccessToken } from '../auth.tokens';
1150
- import { findDevUser } from '../data/dev-users';
1151
-
1152
- @Controller('/auth', {
1153
- apiDoc: {
1154
- tags: ['Auth'],
1155
- description: 'Development auth endpoints generated by SoapJS CLI',
1156
- },
1157
- })
1158
- export class AuthController {
1159
- @Post('/login')
1160
- async login(req: Request): Promise<unknown> {
1161
- const { email, password } = req.body ?? {};
1162
- const user = typeof email === 'string' ? findDevUser(email) : undefined;
1163
-
1164
- if (!user || user.password !== password) {
1165
- return { error: 'Invalid credentials' };
1166
- }
1167
-
1168
- const { password: _password, ...safeUser } = user;
1169
- const accessToken = signAccessToken(safeUser, config.jwtSecret);
1170
-
1171
- return {
1172
- accessToken,
1173
- user: safeUser,
1174
- };
1175
- }
1176
-
1177
- @Get('/me')
1178
- @Auth('jwt')
1179
- async me(req: Request): Promise<unknown> {
1180
- return (req as Request & { user?: unknown }).user;
1181
- }
1182
- }
1183
- `,
1184
- });
1185
- }
1186
- if (plan.capabilities.auth.includes("api-key")) {
1187
- files.push({
1188
- path: "src/features/auth/api-key.strategy.ts",
1189
- type: "config",
1190
- owner: "auth",
1191
- content: `import { AuthStrategy, HttpContext } from '@soapjs/soap/http';
1192
- import { AuthUser } from './domain/auth-user';
1193
-
1194
- export class ApiKeyAuthStrategy implements AuthStrategy<AuthUser> {
1195
- readonly name = 'api-key';
1196
-
1197
- constructor(
1198
- private readonly headerName: string,
1199
- private readonly expectedApiKey: string
1200
- ) {}
1201
-
1202
- async authenticate(ctx: HttpContext) {
1203
- const value = ctx.req.headers[this.headerName.toLowerCase()];
1204
- const apiKey = Array.isArray(value) ? value[0] : value;
1205
-
1206
- if (!apiKey || apiKey !== this.expectedApiKey) {
1207
- return null;
1208
- }
1209
-
1210
- return {
1211
- user: {
1212
- id: 'api-key-client',
1213
- email: 'api-key@example.com',
1214
- name: 'API Key Client',
1215
- roles: ['admin'],
1216
- },
1217
- };
1218
- }
1219
- }
1220
1160
  `,
1221
1161
  });
1222
1162
  }
@@ -1238,34 +1178,139 @@ function usesJwtAuth(plan) {
1238
1178
  }
1239
1179
  function createAuthSetupTs(plan) {
1240
1180
  const imports = [
1241
- "import { AuthStrategy } from '@soapjs/soap/http';",
1181
+ "import { SoapAuth } from '@soapjs/soap-auth';",
1182
+ "import { createApiKeyAuthConfig, createJwtAuthConfig, createLocalAuthConfig } from '@soapjs/soap-auth/recipes';",
1183
+ "import { Logger } from '@soapjs/soap/common';",
1242
1184
  "import { AppConfig } from '../../config/config';",
1185
+ "import { AuthUser } from './domain/auth-user';",
1186
+ "import { devUsers, findDevUser } from './data/dev-users';",
1243
1187
  ];
1244
- const body = [" const strategies: AuthStrategy[] = [];"];
1245
- if (usesJwtAuth(plan)) {
1246
- imports.push("import { JwtAuthStrategy } from './jwt.strategy';");
1247
- body.push("", " strategies.push(new JwtAuthStrategy(config.jwtSecret));");
1188
+ const httpConfigs = [];
1189
+ if (plan.capabilities.auth.includes("jwt")) {
1190
+ httpConfigs.push(` jwt: createJwtConfig(config),`);
1191
+ }
1192
+ if (plan.capabilities.auth.includes("local") || plan.capabilities.auth.includes("jwt")) {
1193
+ httpConfigs.push(` local: createLocalConfig(config),`);
1248
1194
  }
1249
1195
  if (plan.capabilities.auth.includes("api-key")) {
1250
- imports.push("import { ApiKeyAuthStrategy } from './api-key.strategy';");
1251
- body.push("", " strategies.push(new ApiKeyAuthStrategy(config.apiKeyHeader, config.devApiKey));");
1196
+ httpConfigs.push(` apiKey: createApiKeyConfig(config),`);
1252
1197
  }
1253
- body.push("", " return strategies;");
1254
1198
  return `${imports.join("\n")}
1255
1199
 
1256
- export function createAuthStrategies(config: AppConfig): AuthStrategy[] {
1257
- ${body.join("\n")}
1200
+ interface DevLoginPayload {
1201
+ identifier?: string;
1202
+ email?: string;
1203
+ password?: string;
1204
+ }
1205
+
1206
+ function safeUser(user: AuthUser & { password?: string }): AuthUser {
1207
+ return {
1208
+ id: user.id,
1209
+ email: user.email,
1210
+ name: user.name,
1211
+ roles: user.roles,
1212
+ };
1213
+ }
1214
+
1215
+ async function fetchUser(payload: unknown): Promise<AuthUser | null> {
1216
+ const id = typeof payload === 'object' && payload !== null && 'id' in payload ? String((payload as { id: unknown }).id) : undefined;
1217
+ const email = typeof payload === 'string'
1218
+ ? payload
1219
+ : typeof payload === 'object' && payload !== null && 'email' in payload
1220
+ ? String((payload as { email: unknown }).email)
1221
+ : undefined;
1222
+ const user = id
1223
+ ? devUsers.find((candidate) => candidate.id === id)
1224
+ : email
1225
+ ? findDevUser(email)
1226
+ : undefined;
1227
+
1228
+ return user ? safeUser(user) : null;
1229
+ }
1230
+
1231
+ function createJwtConfig(config: AppConfig) {
1232
+ return createJwtAuthConfig({
1233
+ accessSecret: config.jwtAccessSecret,
1234
+ refreshSecret: config.jwtRefreshSecret,
1235
+ user: { fetchUser },
1236
+ accessToken: {
1237
+ options: { expiresIn: '15m' },
1238
+ buildPayload: (user: AuthUser) => ({ id: user.id, email: user.email, roles: user.roles }),
1239
+ },
1240
+ refreshToken: {
1241
+ options: { expiresIn: '7d' },
1242
+ buildPayload: (user: AuthUser) => ({ id: user.id }),
1243
+ },
1244
+ routes: {
1245
+ login: { path: '/auth/login' },
1246
+ refresh: { path: '/auth/refresh' },
1247
+ logout: { path: '/auth/logout' },
1248
+ },
1249
+ });
1250
+ }
1251
+
1252
+ function createLocalConfig(_config: AppConfig) {
1253
+ return createLocalAuthConfig({
1254
+ basePath: '/auth',
1255
+ credentials: {
1256
+ extractCredentials: <TCredentials>(context: unknown): TCredentials => {
1257
+ const authContext = context as { body?: DevLoginPayload; req?: { body?: DevLoginPayload } };
1258
+ const body = authContext.body ?? authContext.req?.body ?? {};
1259
+ return {
1260
+ identifier: body.identifier ?? body.email ?? '',
1261
+ password: body.password ?? '',
1262
+ } as TCredentials;
1263
+ },
1264
+ verifyCredentials: async (identifier: string, password: string) => {
1265
+ const user = findDevUser(identifier);
1266
+ return user?.password === password;
1267
+ },
1268
+ },
1269
+ user: { fetchUser },
1270
+ routes: {
1271
+ login: { path: '/auth/login' },
1272
+ logout: { path: '/auth/logout' },
1273
+ },
1274
+ });
1275
+ }
1276
+
1277
+ function createApiKeyConfig(config: AppConfig) {
1278
+ return createApiKeyAuthConfig({
1279
+ keyType: 'long-term',
1280
+ extractApiKey: (context: unknown) => {
1281
+ const authContext = context as { req?: { headers?: Record<string, string | string[] | undefined> }; headers?: Record<string, string | string[] | undefined> };
1282
+ const headers = authContext.req?.headers ?? authContext.headers ?? {};
1283
+ const value = headers[config.apiKeyHeader.toLowerCase()];
1284
+ return Array.isArray(value) ? value[0] ?? null : value ?? null;
1285
+ },
1286
+ retrieveUserByApiKey: async (apiKey: string): Promise<AuthUser | null> => {
1287
+ if (apiKey !== config.devApiKey) {
1288
+ return null;
1289
+ }
1290
+
1291
+ return {
1292
+ id: 'api-key-client',
1293
+ email: 'api-key@example.com',
1294
+ name: 'API Key Client',
1295
+ roles: ['admin'],
1296
+ };
1297
+ },
1298
+ trackApiKeyUsage: async (_apiKey: string) => undefined,
1299
+ });
1300
+ }
1301
+
1302
+ export async function createAuthProvider(config: AppConfig, logger?: Logger): Promise<SoapAuth> {
1303
+ return SoapAuth.create({
1304
+ logger,
1305
+ http: {
1306
+ ${httpConfigs.join("\n")}
1307
+ },
1308
+ });
1258
1309
  }
1259
1310
  `;
1260
1311
  }
1261
1312
  function createAuthIndexTs(plan) {
1262
1313
  const exports = ["export * from './auth.setup';", "export * from './domain/auth-user';"];
1263
- if (usesJwtAuth(plan)) {
1264
- exports.push("export * from './api/auth.controller';", "export * from './auth.tokens';", "export * from './jwt.strategy';");
1265
- }
1266
- if (plan.capabilities.auth.includes("api-key")) {
1267
- exports.push("export * from './api-key.strategy';");
1268
- }
1269
1314
  return `${exports.join("\n")}\n`;
1270
1315
  }
1271
1316
  function createFeaturesIndexTs(plan) {