@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.
- package/README.md +3 -0
- package/build/cli.js +2 -3
- package/build/commands/add/add.command.js +1 -2
- package/build/commands/add/command-plan.js +7 -8
- package/build/commands/add/entity-plan.js +1 -2
- package/build/commands/add/event-plan.js +1 -2
- package/build/commands/add/query-plan.js +6 -7
- package/build/commands/add/repository-plan.js +5 -6
- package/build/commands/add/resource-plan.js +16 -17
- package/build/commands/add/route-plan.js +7 -7
- package/build/commands/add/socket-plan.js +1 -2
- package/build/commands/add/use-case-plan.js +2 -3
- package/build/commands/check/check.command.js +1 -2
- package/build/commands/create/create.command.js +2 -3
- package/build/commands/create/project-plan.js +256 -216
- package/build/commands/doctor/doctor.command.js +1 -2
- package/build/commands/generate/bruno-analysis.js +4 -5
- package/build/commands/generate/bruno-plan.js +1 -2
- package/build/commands/generate/generate.command.js +1 -2
- package/build/commands/info/info.command.js +1 -2
- package/build/commands/remove/remove.command.js +1 -2
- package/build/commands/shared/common-options.js +3 -4
- package/build/commands/update/update.command.js +1 -2
- package/build/config/auth-policy.js +3 -4
- package/build/config/find-soap-root.js +1 -2
- package/build/config/load-soap-config.js +1 -2
- package/build/config/schemas/types.d.ts +1 -1
- package/build/config/schemas/validation.js +5 -6
- package/build/config/write-soap-config.js +1 -2
- package/build/core/command-context.js +2 -3
- package/build/core/errors.js +2 -2
- package/build/core/output.js +1 -2
- package/build/core/result.js +2 -3
- package/build/dependencies/dependency-resolver.js +5 -8
- package/build/dependencies/package-manager.js +3 -4
- package/build/io/conflict-policy.js +3 -4
- package/build/io/file-writer.js +1 -2
- package/build/io/format-file.js +1 -2
- package/build/presets/create-presets.js +3 -3
- package/build/prompts/add-resource.prompt.js +1 -2
- package/build/prompts/add-route.prompt.js +1 -2
- package/build/prompts/create-project.prompt.js +3 -2
- package/build/prompts/generate-bruno.prompt.js +1 -2
- package/build/prompts/inquirer-prompt-adapter.js +14 -5
- package/build/registry/registry.service.js +5 -6
- package/build/summary/create-summary.js +1 -2
- package/build/templates/naming.js +2 -3
- package/build/templates/template-engine.js +1 -2
- package/build/templates/template-resolver.js +2 -3
- package/build/terminal/terminal-capabilities.js +2 -3
- package/docs/cli/create.md +4 -1
- package/docs/guides/auth.md +9 -1
- package/docs/guides/index.md +1 -0
- 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.
|
|
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
|
-
|
|
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,
|
|
287
|
+
const { container, drainables, logger, auth } = await buildContainer(config);
|
|
268
288
|
|
|
269
|
-
const app =
|
|
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
|
-
|
|
298
|
+
app: {
|
|
299
|
+
security: ${createSecurityConfig(plan)},
|
|
300
|
+
},
|
|
282
301
|
healthCheck: true,${cqrsOption}${docsPlugin}
|
|
283
302
|
});
|
|
284
|
-
${
|
|
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.
|
|
318
|
-
? "\n
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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("
|
|
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 \`
|
|
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
|
-
|
|
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:
|
|
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: `
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
name
|
|
1046
|
-
|
|
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 {
|
|
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
|
|
1245
|
-
if (
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1257
|
-
|
|
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) {
|