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