@soapjs/cli 1.0.0 → 1.0.1

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 (54) 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 +5 -6
  9. package/build/commands/add/resource-plan.js +16 -17
  10. package/build/commands/add/route-plan.js +7 -7
  11. package/build/commands/add/socket-plan.js +1 -2
  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 +256 -216
  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 +5 -8
  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/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,7 +405,7 @@ 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
411
  ? "import { SoapMongo } from '@soapjs/soap-node-mongo';\nimport { createMongoClient } from '../common/data/mongo/mongo.client';\n"
@@ -357,7 +419,7 @@ ${sqlDatabases.map((database) => `import { create${(0, naming_1.createNameVarian
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 [];
@@ -563,14 +624,6 @@ function createCqrsFiles(plan) {
563
624
  type: "config",
564
625
  content: `// Generated CQRS handler imports. Updated by soap add command/query.
565
626
  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
627
  `,
575
628
  },
576
629
  ];
@@ -763,17 +816,9 @@ ${names}
763
816
  ];
764
817
  `;
765
818
  }
766
- exports.createControllersTs = createControllersTs;
767
819
  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
- ];
820
+ void plan;
821
+ return [];
777
822
  }
778
823
  function createEnvExample(plan) {
779
824
  const lines = ["NODE_ENV=development", "PORT=3000", "LOG_LEVEL=debug"];
@@ -793,7 +838,7 @@ function createEnvExample(plan) {
793
838
  lines.push("REDIS_URL=redis://localhost:6379");
794
839
  }
795
840
  if (plan.capabilities.auth.includes("jwt") || plan.capabilities.auth.includes("local")) {
796
- lines.push("JWT_SECRET=change-me");
841
+ lines.push("JWT_ACCESS_SECRET=change-me-access", "JWT_REFRESH_SECRET=change-me-refresh");
797
842
  }
798
843
  if (plan.capabilities.auth.includes("api-key")) {
799
844
  lines.push("API_KEY_HEADER=x-api-key", "DEV_API_KEY=dev-api-key");
@@ -847,6 +892,7 @@ curl http://localhost:3000/openapi.json
847
892
  `
848
893
  : "";
849
894
  const authSection = createReadmeAuthSection(plan);
895
+ const monitoringSection = createReadmeMonitoringSection(plan);
850
896
  const addCommands = `## Add More Code
851
897
 
852
898
  \`\`\`bash
@@ -860,6 +906,8 @@ soap update config --add-api-client bruno
860
906
 
861
907
  Generated SoapJS service.
862
908
 
909
+ 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.
910
+
863
911
  ## Capabilities
864
912
 
865
913
  - Framework: ${plan.framework}
@@ -892,7 +940,7 @@ Build:
892
940
  ${buildCommand}
893
941
  \`\`\`
894
942
 
895
- ${dockerSection}${brunoSection}${openApiSection}${authSection}## Folder Structure
943
+ ${dockerSection}${brunoSection}${openApiSection}${authSection}${monitoringSection}## Folder Structure
896
944
 
897
945
  \`\`\`txt
898
946
  src/
@@ -915,6 +963,25 @@ src/
915
963
  Generated metadata is stored in \`.soap/\`.
916
964
 
917
965
  ${addCommands}
966
+ `;
967
+ }
968
+ function createReadmeMonitoringSection(plan) {
969
+ const endpoints = [];
970
+ if (plan.capabilities.telemetry.includes("metrics")) {
971
+ endpoints.push("- Metrics: http://localhost:3000/metrics");
972
+ }
973
+ if (plan.capabilities.telemetry.includes("memory")) {
974
+ endpoints.push("- Memory monitoring: http://localhost:3000/memory");
975
+ }
976
+ if (endpoints.length === 0) {
977
+ return "";
978
+ }
979
+ return `## Monitoring
980
+
981
+ Monitoring endpoints are opt-in and were generated because telemetry includes \`${plan.capabilities.telemetry.filter((item) => item === "metrics" || item === "memory").join(", ")}\`.
982
+
983
+ ${endpoints.join("\n")}
984
+
918
985
  `;
919
986
  }
920
987
  function createReadmeAuthSection(plan) {
@@ -927,7 +994,7 @@ Default Bruno login variables:
927
994
  - email: \`admin@example.com\`
928
995
  - password: \`admin123\`
929
996
 
930
- Set \`JWT_SECRET\` in \`.env.example\` or your local \`.env\`.
997
+ Auth is registered through \`SoapAuth.create(...)\` and \`createAuthRouter(...)\`. Set \`JWT_ACCESS_SECRET\` and \`JWT_REFRESH_SECRET\` in \`.env.example\` or your local \`.env\`.
931
998
 
932
999
  `);
933
1000
  }
@@ -939,12 +1006,17 @@ Default header and key:
939
1006
  - \`API_KEY_HEADER=x-api-key\`
940
1007
  - \`DEV_API_KEY=dev-api-key\`
941
1008
 
1009
+ API key auth uses \`createApiKeyAuthConfig(...)\` with a development \`retrieveUserByApiKey\` implementation.
1010
+
942
1011
  `);
943
1012
  }
944
- return sections.length > 0 ? `## Auth\n\n${sections.join("\n")}` : "";
1013
+ const security = sections.length > 0
1014
+ ? `Route-specific throttling is enabled for \`POST /auth/login\`, \`POST /auth/refresh\`, and OAuth callbacks through soap-express security config.\n\n`
1015
+ : "";
1016
+ return sections.length > 0 ? `## Auth\n\n${security}${sections.join("\n")}` : "";
945
1017
  }
946
1018
  function createDockerfile() {
947
- return `FROM node:20-alpine
1019
+ return `FROM node:24-alpine
948
1020
  WORKDIR /app
949
1021
  COPY package*.json ./
950
1022
  RUN npm install
@@ -1039,16 +1111,24 @@ function createAuthFiles(plan) {
1039
1111
  path: "src/features/auth/domain/auth-user.ts",
1040
1112
  type: "config",
1041
1113
  owner: "auth",
1042
- content: `export interface AuthUser {
1043
- id: string;
1044
- email: string;
1045
- name: string;
1046
- roles: string[];
1114
+ content: `import { AuthUser as SoapAuthUser } from '@soapjs/soap/http';
1115
+
1116
+ export interface AuthUser extends SoapAuthUser {
1117
+ name?: string;
1118
+ }
1119
+ `,
1120
+ },
1121
+ {
1122
+ path: "src/types/soap-express-auth.d.ts",
1123
+ type: "config",
1124
+ owner: "auth",
1125
+ content: `declare module '@soapjs/soap-express/auth' {
1126
+ export * from '@soapjs/soap-express/build/auth';
1047
1127
  }
1048
1128
  `,
1049
1129
  },
1050
1130
  ];
1051
- if (usesJwtAuth(plan)) {
1131
+ if (usesJwtAuth(plan) || plan.capabilities.auth.includes("api-key")) {
1052
1132
  files.push({
1053
1133
  path: "src/features/auth/data/dev-users.ts",
1054
1134
  type: "config",
@@ -1072,151 +1152,6 @@ export const devUsers: DevUser[] = [
1072
1152
  export function findDevUser(email: string): DevUser | undefined {
1073
1153
  return devUsers.find((user) => user.email === email);
1074
1154
  }
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
1155
  `,
1221
1156
  });
1222
1157
  }
@@ -1238,34 +1173,139 @@ function usesJwtAuth(plan) {
1238
1173
  }
1239
1174
  function createAuthSetupTs(plan) {
1240
1175
  const imports = [
1241
- "import { AuthStrategy } from '@soapjs/soap/http';",
1176
+ "import { SoapAuth } from '@soapjs/soap-auth';",
1177
+ "import { createApiKeyAuthConfig, createJwtAuthConfig, createLocalAuthConfig } from '@soapjs/soap-auth/recipes';",
1178
+ "import { Logger } from '@soapjs/soap/common';",
1242
1179
  "import { AppConfig } from '../../config/config';",
1180
+ "import { AuthUser } from './domain/auth-user';",
1181
+ "import { devUsers, findDevUser } from './data/dev-users';",
1243
1182
  ];
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));");
1183
+ const httpConfigs = [];
1184
+ if (plan.capabilities.auth.includes("jwt")) {
1185
+ httpConfigs.push(` jwt: createJwtConfig(config),`);
1186
+ }
1187
+ if (plan.capabilities.auth.includes("local") || plan.capabilities.auth.includes("jwt")) {
1188
+ httpConfigs.push(` local: createLocalConfig(config),`);
1248
1189
  }
1249
1190
  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));");
1191
+ httpConfigs.push(` apiKey: createApiKeyConfig(config),`);
1252
1192
  }
1253
- body.push("", " return strategies;");
1254
1193
  return `${imports.join("\n")}
1255
1194
 
1256
- export function createAuthStrategies(config: AppConfig): AuthStrategy[] {
1257
- ${body.join("\n")}
1195
+ interface DevLoginPayload {
1196
+ identifier?: string;
1197
+ email?: string;
1198
+ password?: string;
1199
+ }
1200
+
1201
+ function safeUser(user: AuthUser & { password?: string }): AuthUser {
1202
+ return {
1203
+ id: user.id,
1204
+ email: user.email,
1205
+ name: user.name,
1206
+ roles: user.roles,
1207
+ };
1208
+ }
1209
+
1210
+ async function fetchUser(payload: unknown): Promise<AuthUser | null> {
1211
+ const id = typeof payload === 'object' && payload !== null && 'id' in payload ? String((payload as { id: unknown }).id) : undefined;
1212
+ const email = typeof payload === 'string'
1213
+ ? payload
1214
+ : typeof payload === 'object' && payload !== null && 'email' in payload
1215
+ ? String((payload as { email: unknown }).email)
1216
+ : undefined;
1217
+ const user = id
1218
+ ? devUsers.find((candidate) => candidate.id === id)
1219
+ : email
1220
+ ? findDevUser(email)
1221
+ : undefined;
1222
+
1223
+ return user ? safeUser(user) : null;
1224
+ }
1225
+
1226
+ function createJwtConfig(config: AppConfig) {
1227
+ return createJwtAuthConfig({
1228
+ accessSecret: config.jwtAccessSecret,
1229
+ refreshSecret: config.jwtRefreshSecret,
1230
+ user: { fetchUser },
1231
+ accessToken: {
1232
+ options: { expiresIn: '15m' },
1233
+ buildPayload: (user: AuthUser) => ({ id: user.id, email: user.email, roles: user.roles }),
1234
+ },
1235
+ refreshToken: {
1236
+ options: { expiresIn: '7d' },
1237
+ buildPayload: (user: AuthUser) => ({ id: user.id }),
1238
+ },
1239
+ routes: {
1240
+ login: { path: '/auth/login' },
1241
+ refresh: { path: '/auth/refresh' },
1242
+ logout: { path: '/auth/logout' },
1243
+ },
1244
+ });
1245
+ }
1246
+
1247
+ function createLocalConfig(_config: AppConfig) {
1248
+ return createLocalAuthConfig({
1249
+ basePath: '/auth',
1250
+ credentials: {
1251
+ extractCredentials: <TCredentials>(context: unknown): TCredentials => {
1252
+ const authContext = context as { body?: DevLoginPayload; req?: { body?: DevLoginPayload } };
1253
+ const body = authContext.body ?? authContext.req?.body ?? {};
1254
+ return {
1255
+ identifier: body.identifier ?? body.email ?? '',
1256
+ password: body.password ?? '',
1257
+ } as TCredentials;
1258
+ },
1259
+ verifyCredentials: async (identifier: string, password: string) => {
1260
+ const user = findDevUser(identifier);
1261
+ return user?.password === password;
1262
+ },
1263
+ },
1264
+ user: { fetchUser },
1265
+ routes: {
1266
+ login: { path: '/auth/login' },
1267
+ logout: { path: '/auth/logout' },
1268
+ },
1269
+ });
1270
+ }
1271
+
1272
+ function createApiKeyConfig(config: AppConfig) {
1273
+ return createApiKeyAuthConfig({
1274
+ keyType: 'long-term',
1275
+ extractApiKey: (context: unknown) => {
1276
+ const authContext = context as { req?: { headers?: Record<string, string | string[] | undefined> }; headers?: Record<string, string | string[] | undefined> };
1277
+ const headers = authContext.req?.headers ?? authContext.headers ?? {};
1278
+ const value = headers[config.apiKeyHeader.toLowerCase()];
1279
+ return Array.isArray(value) ? value[0] ?? null : value ?? null;
1280
+ },
1281
+ retrieveUserByApiKey: async (apiKey: string): Promise<AuthUser | null> => {
1282
+ if (apiKey !== config.devApiKey) {
1283
+ return null;
1284
+ }
1285
+
1286
+ return {
1287
+ id: 'api-key-client',
1288
+ email: 'api-key@example.com',
1289
+ name: 'API Key Client',
1290
+ roles: ['admin'],
1291
+ };
1292
+ },
1293
+ trackApiKeyUsage: async (_apiKey: string) => undefined,
1294
+ });
1295
+ }
1296
+
1297
+ export async function createAuthProvider(config: AppConfig, logger?: Logger): Promise<SoapAuth> {
1298
+ return SoapAuth.create({
1299
+ logger,
1300
+ http: {
1301
+ ${httpConfigs.join("\n")}
1302
+ },
1303
+ });
1258
1304
  }
1259
1305
  `;
1260
1306
  }
1261
1307
  function createAuthIndexTs(plan) {
1262
1308
  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
1309
  return `${exports.join("\n")}\n`;
1270
1310
  }
1271
1311
  function createFeaturesIndexTs(plan) {