@expressots/adapter-express 4.0.0-preview.1 → 4.0.0-preview.3

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 (36) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +61 -61
  3. package/lib/CHANGELOG.md +10 -5
  4. package/lib/README.md +61 -61
  5. package/lib/cjs/adapter-express/application-express.js +401 -45
  6. package/lib/cjs/adapter-express/express-utils/decorators.js +44 -15
  7. package/lib/cjs/adapter-express/express-utils/inversify-express-server.js +20 -4
  8. package/lib/cjs/adapter-express/express-utils/path-pattern-compat.js +129 -0
  9. package/lib/cjs/adapter-express/express-utils/route-constraints.js +12 -3
  10. package/lib/cjs/adapter-express/micro-api/application-express-micro.js +5 -9
  11. package/lib/cjs/adapter-express/micro-api/micro.js +96 -41
  12. package/lib/cjs/adapter-express/studio/index.js +2 -1
  13. package/lib/cjs/adapter-express/studio/studio-integration.js +64 -11
  14. package/lib/cjs/types/adapter-express/application-express.d.ts +51 -9
  15. package/lib/cjs/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
  16. package/lib/cjs/types/adapter-express/express-utils/route-constraints.d.ts +12 -3
  17. package/lib/cjs/types/adapter-express/micro-api/micro.d.ts +19 -2
  18. package/lib/cjs/types/adapter-express/studio/index.d.ts +1 -1
  19. package/lib/cjs/types/adapter-express/studio/studio-integration.d.ts +78 -0
  20. package/lib/esm/adapter-express/application-express.js +402 -46
  21. package/lib/esm/adapter-express/express-utils/decorators.js +44 -15
  22. package/lib/esm/adapter-express/express-utils/inversify-express-server.js +20 -4
  23. package/lib/esm/adapter-express/express-utils/path-pattern-compat.js +125 -0
  24. package/lib/esm/adapter-express/express-utils/route-constraints.js +12 -3
  25. package/lib/esm/adapter-express/micro-api/application-express-micro.js +6 -10
  26. package/lib/esm/adapter-express/micro-api/micro.js +97 -42
  27. package/lib/esm/adapter-express/studio/index.js +1 -1
  28. package/lib/esm/adapter-express/studio/studio-integration.js +63 -11
  29. package/lib/esm/types/adapter-express/application-express.d.ts +51 -9
  30. package/lib/esm/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
  31. package/lib/esm/types/adapter-express/express-utils/route-constraints.d.ts +12 -3
  32. package/lib/esm/types/adapter-express/micro-api/micro.d.ts +19 -2
  33. package/lib/esm/types/adapter-express/studio/index.d.ts +1 -1
  34. package/lib/esm/types/adapter-express/studio/studio-integration.d.ts +78 -0
  35. package/lib/package.json +24 -10
  36. package/package.json +25 -11
@@ -17,7 +17,7 @@ import { HttpStatusCodeMiddleware } from "./express-utils/http-status-middleware
17
17
  import { InversifyExpressServer } from "./express-utils/inversify-express-server.js";
18
18
  import { setEngineEjs, setEngineHandlebars, setEnginePug } from "./render/engine.js";
19
19
  import { getControllersFromMetadata, getControllersFromContainer, getControllerMethodMetadata, getControllerMetadata, } from "./express-utils/utils.js";
20
- import { initializeStudio, stopStudio, isStudioEnabled, reportStudioRuntimeInfo, } from "./studio/index.js";
20
+ import { initializeStudio, stopStudio, isStudioEnabled, reportStudioRuntimeInfo, rescanStudioRoutes, } from "./studio/index.js";
21
21
  /**
22
22
  * The AppExpress class provides methods for configuring and running an Express application.
23
23
  * @class AppExpress
@@ -63,23 +63,26 @@ export class AppExpress {
63
63
  portRetryAttempts = 10;
64
64
  /** Delay between port retry attempts (ms) */
65
65
  portRetryDelay = 500;
66
- // Log buffering for banner-first display
67
- // IMPORTANT: All these properties must be declared BEFORE initBuffering!
66
+ // Log buffering for banner-first display.
67
+ //
68
+ // Buffering is **opt-in** and is activated either by:
69
+ // 1. Constructing an `AppExpress` instance (the constructor calls
70
+ // `startLogBuffering()`), or
71
+ // 2. The framework calling `AppExpress.startLogBuffering()` explicitly
72
+ // from `bootstrap()` so logs emitted during container/module setup
73
+ // are captured before the AppExpress instance even exists.
74
+ //
75
+ // Importing `@expressots/adapter-express` does NOT touch stdio. Test
76
+ // harnesses, type-only consumers, and tooling that imports the module
77
+ // without ever booting an app will see normal `process.stdout` /
78
+ // `console.*` behavior. `micro()` calls `disableBuffering()` on entry
79
+ // because it does not use the banner system.
68
80
  static originalStdoutWrite = null;
69
81
  static originalStderrWrite = null;
70
82
  static logBuffer = [];
71
83
  static isBuffering = false;
72
- static bufferingInitialized = false;
73
84
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
85
  static originalGlobalConsole = null;
75
- // Initialize buffering when AppExpress class is loaded (before any instances are created)
76
- // This ensures ALL logs are buffered from the very beginning for the full template.
77
- // The micro() function explicitly disables this buffering since it doesn't use the banner system.
78
- // This MUST be declared AFTER all the static properties it uses!
79
- static initBuffering = (() => {
80
- AppExpress.startLogBuffering();
81
- return true;
82
- })();
83
86
  /**
84
87
  * Disable log buffering. Called by micro() to restore normal console output
85
88
  * since micro API doesn't use the banner system.
@@ -91,9 +94,15 @@ export class AppExpress {
91
94
  AppExpress.logBuffer = [];
92
95
  }
93
96
  /**
94
- * Start buffering all console output.
95
- * This captures both console.log and direct process.stdout.write calls.
96
- * @private
97
+ * Start buffering all console output for the banner-first display flow.
98
+ * Captures both `console.*` and direct `process.stdout.write` / `process.stderr.write`
99
+ * calls so they can be flushed in the correct order after the banner displays.
100
+ *
101
+ * Idempotent: calling this multiple times is safe.
102
+ *
103
+ * @public API — called by `bootstrap()` so logs emitted during container
104
+ * setup are captured before the `AppExpress` instance exists. Also called
105
+ * automatically inside the constructor as a safety net.
97
106
  */
98
107
  static startLogBuffering() {
99
108
  if (AppExpress.isBuffering)
@@ -199,8 +208,11 @@ export class AppExpress {
199
208
  }
200
209
  }
201
210
  constructor() {
202
- // Buffering is already started via static initialization (initBuffering)
203
- // This ensures ALL logs are captured from the very beginning
211
+ // Activate banner-first log buffering on first AppExpress construction.
212
+ // Idempotent bootstrap() typically called this earlier so logs emitted
213
+ // during container/module setup were already buffered. micro() never
214
+ // reaches this constructor; it explicitly disables buffering itself.
215
+ AppExpress.startLogBuffering();
204
216
  this.globalConfiguration();
205
217
  }
206
218
  /**
@@ -280,24 +292,43 @@ export class AppExpress {
280
292
  * @internal
281
293
  */
282
294
  async handleExit(signal) {
283
- // 1. Stop Studio Agent if running
284
- await stopStudio();
285
- // 2. Execute lifecycle shutdown hooks on all IShutdown providers
295
+ // Helper: race any drain against a hard cap. We never want a
296
+ // misbehaving cleanup hook (OTel exporter, slow DB driver, user
297
+ // shutdown promise that never resolves) to hold the whole exit
298
+ // chain. Each phase gets its own bound — total fits inside the
299
+ // outer `setShutdownTimeout` watchdog.
300
+ const withTimeout = async (p, ms) => {
301
+ const value = Promise.resolve(p).then(() => { });
302
+ const timer = new Promise((resolve) => setTimeout(resolve, ms).unref());
303
+ await Promise.race([value, timer]);
304
+ };
305
+ // 1. Stop Studio Agent if running. We cap this at 1.5s — the agent
306
+ // itself caps its websocket drain at 500ms and OpenTelemetry at
307
+ // 500ms, but auto-instrumentations can leave handles around so
308
+ // the wrapper close still occasionally drags. 1.5s is plenty.
309
+ await withTimeout(stopStudio(), 1500);
310
+ // 2. Execute lifecycle shutdown hooks on all IShutdown providers.
311
+ // Capped at the user-configured shutdown timeout (default 5s).
286
312
  if (this.lifecycleRegistry) {
287
- await this.lifecycleRegistry.executeShutdown(signal);
313
+ await withTimeout(this.lifecycleRegistry.executeShutdown(signal), this.shutdownTimeout);
288
314
  }
289
- // 3. Call user's serverShutdown hook
290
- await this.handleSyncOrAsync(this.serverShutdown(signal));
291
- // 4. Gracefully close the HTTP server with connection force-close
315
+ // 3. Call user's serverShutdown hook (also capped).
316
+ await withTimeout(this.handleSyncOrAsync(this.serverShutdown(signal)), this.shutdownTimeout);
317
+ // 4. Gracefully close the HTTP server with aggressive connection
318
+ // teardown. Order matters: we destroy *all* tracked connections
319
+ // immediately (not just idle ones) before calling `close()`,
320
+ // because otherwise an active keep-alive request would hold the
321
+ // `close` callback open until either its keep-alive timer
322
+ // expires (~5s) or the inner `forceCloseTimeout` fires. Killing
323
+ // connections up-front lets `close` resolve in the next tick.
292
324
  if (this.serverInstance) {
293
325
  await new Promise((resolve) => {
294
- // Set a timeout to force-destroy connections if graceful shutdown takes too long
295
326
  const forceCloseTimeout = setTimeout(() => {
296
327
  console.log(`⚠️ Force-closing ${this.activeConnections.size} active connections after ${this.shutdownTimeout}ms timeout`);
297
328
  this.destroyAllConnections();
298
329
  resolve();
299
330
  }, this.shutdownTimeout);
300
- // Try graceful close first
331
+ forceCloseTimeout.unref();
301
332
  this.serverInstance.close((err) => {
302
333
  clearTimeout(forceCloseTimeout);
303
334
  if (err) {
@@ -306,12 +337,17 @@ export class AppExpress {
306
337
  }
307
338
  resolve();
308
339
  });
309
- // Immediately destroy idle connections (keep-alive connections with no pending requests)
310
- // This speeds up shutdown significantly
311
- this.serverInstance.closeIdleConnections?.();
340
+ // Aggressively kill keep-alive sockets so `close` actually
341
+ // resolves promptly. `closeAllConnections` is Node 18.2+; older
342
+ // versions silently no-op via the optional-call.
343
+ try {
344
+ this.serverInstance.closeAllConnections?.();
345
+ }
346
+ catch {
347
+ // best-effort — destroy our tracked set as a fallback
348
+ }
349
+ this.destroyAllConnections();
312
350
  });
313
- // Clear all remaining connections
314
- this.destroyAllConnections();
315
351
  }
316
352
  }
317
353
  /**
@@ -623,7 +659,16 @@ export class AppExpress {
623
659
  interceptorCount: this.lastApplicationMetrics?.interceptors,
624
660
  middlewareCount: this.lastApplicationMetrics?.middleware,
625
661
  runtimeItems: this.collectStudioRuntimeItems(),
662
+ middlewarePreset: this.collectMiddlewarePresetInfo(),
626
663
  });
664
+ // Re-scan routes now that `InversifyExpressServer.build()` has
665
+ // populated the Express `_router` stack. The agent's first scan
666
+ // happens before controllers are bound (Studio middleware ships
667
+ // ahead of route registration so it can capture every request),
668
+ // so without this rescan newly-added or never-bound controllers
669
+ // never appear in the Studio Routes / Architecture views.
670
+ // Fire-and-forget; the Studio Agent broadcasts the result over WS.
671
+ void rescanStudioRoutes();
627
672
  // Setup signal handlers for graceful shutdown
628
673
  // Supported signals:
629
674
  // - SIGTERM: Standard termination (Kubernetes, Docker, process managers)
@@ -651,17 +696,64 @@ export class AppExpress {
651
696
  return;
652
697
  }
653
698
  this.isShuttingDown = true;
654
- // Use console.log for shutdown messages - synchronous and guaranteed to write before exit
655
- console.log(`\n📡 Signal ${signal} received, initiating graceful shutdown...`);
656
- // Execute shutdown hooks and exit
699
+ // Emit the shutdown notice through the framework Logger so it
700
+ // matches the standard "[ExpressoTS] INFO [context] …" output.
701
+ // The leading newline keeps it off the terminal's "^C" echo line.
702
+ process.stdout.write("\n");
703
+ this.logger.info(`Signal ${signal} received, initiating graceful shutdown...`, "adapter-express");
704
+ // Hard overall cap on the graceful shutdown. `handleExit` chains
705
+ // several `await`s — Studio agent stop, lifecycle shutdown
706
+ // hooks, the user's `serverShutdown`, and finally
707
+ // `serverInstance.close`. If any of those hang (an unresponsive
708
+ // OpenTelemetry exporter, a pending DB transaction, a slow
709
+ // user hook, an HTTP keep-alive socket the OS hasn't reaped
710
+ // yet), the host process otherwise sits in
711
+ // "📡 Signal SIGINT received, initiating graceful shutdown…"
712
+ // for minutes. Capping the whole pipeline keeps the developer
713
+ // ergonomics tight (Ctrl+C is interactive — they want their
714
+ // prompt back now) while still allowing fast hooks to run to
715
+ // completion.
716
+ //
717
+ // We expose the cap so apps with legitimately long drains
718
+ // (e.g. flushing a 50k-message queue) can opt into a longer
719
+ // timeout via `setShutdownTimeout`.
720
+ const overallCap = this.shutdownTimeout + 3000;
721
+ let forced = false;
722
+ const overallTimer = setTimeout(() => {
723
+ forced = true;
724
+ console.warn(`⚠️ Graceful shutdown exceeded ${overallCap}ms; ` +
725
+ `force-exiting. If this happens routinely, raise ` +
726
+ `\`setShutdownTimeout\` or audit your IShutdown hooks.`);
727
+ this.destroyAllConnections();
728
+ process.exit(0);
729
+ }, overallCap);
730
+ // Don't let the watchdog timer keep the event loop alive on
731
+ // its own; if everything else releases the loop it's fine
732
+ // for `process.exit(0)` to fire from `handleExit` cleanly.
733
+ overallTimer.unref();
657
734
  this.handleExit(signal)
658
735
  .then(() => {
659
- console.log("✅ Graceful shutdown completed");
660
- process.exit(0);
736
+ if (forced)
737
+ return;
738
+ clearTimeout(overallTimer);
739
+ this.logger.info("Graceful shutdown completed", "adapter-express");
740
+ // Flush stdout before exiting. Writing an empty chunk with a
741
+ // callback guarantees the log line above is fully drained to
742
+ // the terminal (the callback only fires after prior queued
743
+ // writes complete), so the message can't appear after the
744
+ // shell has already redrawn its prompt.
745
+ process.stdout.write("", () => {
746
+ process.exit(0);
747
+ });
661
748
  })
662
749
  .catch((error) => {
663
- console.error(`❌ Error during shutdown: ${error.message}`);
664
- process.exit(1);
750
+ if (forced)
751
+ return;
752
+ clearTimeout(overallTimer);
753
+ this.logger.error(`Error during shutdown: ${error.message}`, "adapter-express");
754
+ process.stderr.write("", () => {
755
+ process.exit(1);
756
+ });
665
757
  });
666
758
  };
667
759
  // Store handler for later removal and register it
@@ -1161,21 +1253,236 @@ export class AppExpress {
1161
1253
  });
1162
1254
  }
1163
1255
  }
1164
- // Bail out if both lists are empty — nothing useful to forward.
1165
- if (providers.length === 0 && interceptors.length === 0) {
1256
+ const middleware = this.collectMiddlewarePipelineItems();
1257
+ const middlewareBindings = this.collectMiddlewareBindings();
1258
+ if (providers.length === 0 &&
1259
+ interceptors.length === 0 &&
1260
+ !middleware &&
1261
+ !middlewareBindings) {
1262
+ return undefined;
1263
+ }
1264
+ return { providers, interceptors, middleware, middlewareBindings };
1265
+ }
1266
+ catch {
1267
+ return undefined;
1268
+ }
1269
+ }
1270
+ /**
1271
+ * Harvest controller- and route-scoped middleware bindings from
1272
+ * Reflect metadata. Each entry describes a single edge the Studio
1273
+ * architecture map should draw, e.g. "AuthMiddleware → UserController
1274
+ * (route POST /users/:id)".
1275
+ *
1276
+ * The middleware values stored on `ControllerMetadata.middleware` are
1277
+ * a polymorphic union (class, function, registered name, conditional
1278
+ * config, …). We normalise each to a display name; entries we can't
1279
+ * name (anonymous arrow functions, plain object configs without a
1280
+ * `name` field) are omitted. The agent's static scan picks up the
1281
+ * remaining named cases via decorator parsing — between the two
1282
+ * sources Studio sees a complete graph for the common patterns.
1283
+ */
1284
+ collectMiddlewareBindings() {
1285
+ try {
1286
+ const controllers = getControllersFromMetadata();
1287
+ if (!controllers || controllers.length === 0)
1166
1288
  return undefined;
1289
+ const out = [];
1290
+ const nameOf = (value) => {
1291
+ if (value == null)
1292
+ return null;
1293
+ if (typeof value === "string")
1294
+ return value;
1295
+ if (typeof value === "symbol") {
1296
+ const desc = value.description;
1297
+ return desc && desc.length > 0 ? desc : null;
1298
+ }
1299
+ if (typeof value === "function") {
1300
+ const fnName = value.name;
1301
+ return typeof fnName === "string" && fnName.length > 0 ? fnName : null;
1302
+ }
1303
+ if (typeof value === "object") {
1304
+ const candidate = value.name;
1305
+ if (typeof candidate === "string" && candidate.length > 0) {
1306
+ return candidate;
1307
+ }
1308
+ const ctorName = value.constructor?.name;
1309
+ if (typeof ctorName === "string" && ctorName.length > 0 && ctorName !== "Object") {
1310
+ return ctorName;
1311
+ }
1312
+ }
1313
+ return null;
1314
+ };
1315
+ for (const controllerTarget of controllers) {
1316
+ const controllerCtor = controllerTarget;
1317
+ const controllerName = controllerCtor.name;
1318
+ if (typeof controllerName !== "string" || controllerName.length === 0) {
1319
+ continue;
1320
+ }
1321
+ const ctrlMeta = getControllerMetadata(controllerCtor);
1322
+ if (ctrlMeta?.middleware && Array.isArray(ctrlMeta.middleware)) {
1323
+ for (const mw of ctrlMeta.middleware) {
1324
+ const middlewareName = nameOf(mw);
1325
+ if (!middlewareName)
1326
+ continue;
1327
+ out.push({
1328
+ middlewareName,
1329
+ scope: "controller",
1330
+ controllerName,
1331
+ });
1332
+ }
1333
+ }
1334
+ const methodMeta = getControllerMethodMetadata(controllerCtor);
1335
+ if (Array.isArray(methodMeta)) {
1336
+ const basePath = ctrlMeta?.path ?? "";
1337
+ for (const route of methodMeta) {
1338
+ if (!route?.middleware || !Array.isArray(route.middleware))
1339
+ continue;
1340
+ const httpMethod = typeof route.method === "string" ? route.method.toUpperCase() : undefined;
1341
+ const fullPath = this.joinRoutePath(basePath, route.path);
1342
+ for (const mw of route.middleware) {
1343
+ const middlewareName = nameOf(mw);
1344
+ if (!middlewareName)
1345
+ continue;
1346
+ out.push({
1347
+ middlewareName,
1348
+ scope: "route",
1349
+ controllerName,
1350
+ controllerMethod: typeof route.key === "string" ? route.key : undefined,
1351
+ httpMethod,
1352
+ routePath: fullPath,
1353
+ });
1354
+ }
1355
+ }
1356
+ }
1167
1357
  }
1168
- return { providers, interceptors };
1358
+ return out.length > 0 ? out : undefined;
1359
+ }
1360
+ catch {
1361
+ return undefined;
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Combine a controller's base path with a route path, normalising
1366
+ * leading/trailing slashes. Mirrors the simpler logic Studio uses to
1367
+ * build `RouteInfo.path` so the bindings line up with route entries.
1368
+ */
1369
+ joinRoutePath(basePath, routePath) {
1370
+ const base = basePath?.startsWith("/") ? basePath : `/${basePath ?? ""}`;
1371
+ if (!routePath || routePath === "/" || routePath === "")
1372
+ return base || "/";
1373
+ const tail = routePath.startsWith("/") ? routePath : `/${routePath}`;
1374
+ return (base + tail).replace(/\/+/g, "/");
1375
+ }
1376
+ /**
1377
+ * Collect the ordered middleware pipeline from the Middleware service
1378
+ * for forwarding to Studio. Uses feature-detection so older core
1379
+ * versions that lack `getPipelineInfo()` won't break.
1380
+ */
1381
+ collectMiddlewarePipelineItems() {
1382
+ try {
1383
+ const mw = this.Middleware;
1384
+ const getPipelineInfo = mw.getPipelineInfo;
1385
+ if (typeof getPipelineInfo !== "function")
1386
+ return undefined;
1387
+ const info = getPipelineInfo.call(mw);
1388
+ if (!info || !info.entries || info.entries.length === 0)
1389
+ return undefined;
1390
+ return info.entries.map((e) => ({
1391
+ name: e.name,
1392
+ category: e.category,
1393
+ type: e.type,
1394
+ order: e.order,
1395
+ path: e.path !== "Global" ? e.path : undefined,
1396
+ }));
1397
+ }
1398
+ catch {
1399
+ return undefined;
1400
+ }
1401
+ }
1402
+ /**
1403
+ * Build the middleware preset info snapshot for Studio. Reads the
1404
+ * last applied preset from the Middleware service and transforms it
1405
+ * into the shape Studio expects.
1406
+ */
1407
+ collectMiddlewarePresetInfo() {
1408
+ try {
1409
+ const mw = this.Middleware;
1410
+ const getPreset = mw.getLastAppliedPreset;
1411
+ if (typeof getPreset !== "function")
1412
+ return undefined;
1413
+ const preset = getPreset.call(mw);
1414
+ if (!preset)
1415
+ return undefined;
1416
+ const cfg = preset.config;
1417
+ const parse = cfg.parse && typeof cfg.parse === "object"
1418
+ ? {
1419
+ json: cfg.parse.json && typeof cfg.parse.json === "object"
1420
+ ? { limit: cfg.parse.json.limit }
1421
+ : undefined,
1422
+ urlencoded: cfg.parse.urlencoded && typeof cfg.parse.urlencoded === "object"
1423
+ ? {
1424
+ limit: cfg.parse.urlencoded.limit,
1425
+ extended: cfg.parse.urlencoded.extended,
1426
+ }
1427
+ : undefined,
1428
+ cookies: !!cfg.parse.cookies,
1429
+ }
1430
+ : cfg.parse
1431
+ ? { json: { limit: "100kb" }, cookies: false }
1432
+ : undefined;
1433
+ let security;
1434
+ if (typeof cfg.security === "string") {
1435
+ security = resolveSecurityTierForStudio(cfg.security);
1436
+ }
1437
+ else if (cfg.security && typeof cfg.security === "object") {
1438
+ const sec = cfg.security;
1439
+ security = {
1440
+ helmet: sec.headers !== false,
1441
+ cors: sec.cors && typeof sec.cors === "object"
1442
+ ? sec.cors
1443
+ : sec.cors !== false
1444
+ ? { origin: true }
1445
+ : undefined,
1446
+ rateLimit: sec.rateLimit && typeof sec.rateLimit === "object"
1447
+ ? sec.rateLimit
1448
+ : sec.rateLimit
1449
+ ? { windowMs: 60000, max: 100 }
1450
+ : false,
1451
+ };
1452
+ }
1453
+ const compress = cfg.compress
1454
+ ? {
1455
+ enabled: true,
1456
+ level: typeof cfg.compress === "object" ? cfg.compress.level : undefined,
1457
+ }
1458
+ : { enabled: false };
1459
+ const logger = cfg.logger
1460
+ ? {
1461
+ enabled: true,
1462
+ implementation: typeof cfg.logger === "object" ? cfg.logger.implementation : "auto",
1463
+ }
1464
+ : { enabled: false };
1465
+ return {
1466
+ name: preset.name,
1467
+ hasOverrides: preset.hasOverrides,
1468
+ parse,
1469
+ security,
1470
+ compress,
1471
+ logger,
1472
+ };
1169
1473
  }
1170
1474
  catch {
1171
- // Metadata reads should never break boot. Status page just falls
1172
- // back to its static-scan list.
1173
1475
  return undefined;
1174
1476
  }
1175
1477
  }
1176
1478
  /**
1177
1479
  * Display middleware startup logs after the banner.
1178
- * This makes startup logging transparent to the user - no need for manual code in postServerInitialization().
1480
+ *
1481
+ * Warnings (e.g. missing optional packages like `helmet`) are always surfaced
1482
+ * so the developer can act on them. Informational entries (e.g. "Security
1483
+ * configured", "Applied preset: api") are demoted to `debug` since the
1484
+ * dashboard already shows the active middleware count; set `LOG_LEVEL=DEBUG`
1485
+ * to see the full breakdown.
1179
1486
  * @private
1180
1487
  */
1181
1488
  displayMiddlewareStartupLogs() {
@@ -1190,7 +1497,7 @@ export class AppExpress {
1190
1497
  this.logger.warn(log.message, "middleware");
1191
1498
  }
1192
1499
  else {
1193
- this.logger.info(log.message, "middleware");
1500
+ this.logger.withContext("middleware").debug(log.message);
1194
1501
  }
1195
1502
  });
1196
1503
  this.Middleware.clearStartupLogs();
@@ -1267,7 +1574,7 @@ export class AppExpress {
1267
1574
  };
1268
1575
  // Display banner
1269
1576
  this.bannerGenerator.display(this.port, this.environment || "development", finalAppInfo, metrics, features, {
1270
- "Global Prefix": this.globalPrefix || "/",
1577
+ Prefix: this.globalPrefix || "/",
1271
1578
  "Node Version": process.version,
1272
1579
  Platform: process.platform,
1273
1580
  }, bannerData);
@@ -1298,3 +1605,52 @@ export class AppExpress {
1298
1605
  }
1299
1606
  }
1300
1607
  }
1608
+ /**
1609
+ * Resolve a named security tier string into the display-friendly shape
1610
+ * expected by the Studio Middleware card. Mirrors the defaults applied
1611
+ * by `Middleware.getSecurityPreset()` in `@expressots/core`.
1612
+ */
1613
+ function resolveSecurityTierForStudio(tier) {
1614
+ switch (tier) {
1615
+ case "api":
1616
+ return {
1617
+ tier,
1618
+ helmet: true,
1619
+ cors: {
1620
+ origin: true,
1621
+ credentials: true,
1622
+ methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
1623
+ allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
1624
+ },
1625
+ rateLimit: { windowMs: 60000, max: 100 },
1626
+ };
1627
+ case "strict":
1628
+ return {
1629
+ tier,
1630
+ helmet: true,
1631
+ cors: { origin: false },
1632
+ rateLimit: { windowMs: 60000, max: 50 },
1633
+ };
1634
+ case "relaxed":
1635
+ return {
1636
+ tier,
1637
+ helmet: true,
1638
+ cors: { origin: true },
1639
+ rateLimit: false,
1640
+ };
1641
+ case "minimal":
1642
+ return {
1643
+ tier,
1644
+ helmet: false,
1645
+ rateLimit: false,
1646
+ };
1647
+ case "standard":
1648
+ default:
1649
+ return {
1650
+ tier,
1651
+ helmet: true,
1652
+ cors: { origin: true },
1653
+ rateLimit: false,
1654
+ };
1655
+ }
1656
+ }