@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.
- package/LICENSE.md +21 -21
- package/README.md +61 -61
- package/lib/CHANGELOG.md +10 -5
- package/lib/README.md +61 -61
- package/lib/cjs/adapter-express/application-express.js +401 -45
- package/lib/cjs/adapter-express/express-utils/decorators.js +44 -15
- package/lib/cjs/adapter-express/express-utils/inversify-express-server.js +20 -4
- package/lib/cjs/adapter-express/express-utils/path-pattern-compat.js +129 -0
- package/lib/cjs/adapter-express/express-utils/route-constraints.js +12 -3
- package/lib/cjs/adapter-express/micro-api/application-express-micro.js +5 -9
- package/lib/cjs/adapter-express/micro-api/micro.js +96 -41
- package/lib/cjs/adapter-express/studio/index.js +2 -1
- package/lib/cjs/adapter-express/studio/studio-integration.js +64 -11
- package/lib/cjs/types/adapter-express/application-express.d.ts +51 -9
- package/lib/cjs/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
- package/lib/cjs/types/adapter-express/express-utils/route-constraints.d.ts +12 -3
- package/lib/cjs/types/adapter-express/micro-api/micro.d.ts +19 -2
- package/lib/cjs/types/adapter-express/studio/index.d.ts +1 -1
- package/lib/cjs/types/adapter-express/studio/studio-integration.d.ts +78 -0
- package/lib/esm/adapter-express/application-express.js +402 -46
- package/lib/esm/adapter-express/express-utils/decorators.js +44 -15
- package/lib/esm/adapter-express/express-utils/inversify-express-server.js +20 -4
- package/lib/esm/adapter-express/express-utils/path-pattern-compat.js +125 -0
- package/lib/esm/adapter-express/express-utils/route-constraints.js +12 -3
- package/lib/esm/adapter-express/micro-api/application-express-micro.js +6 -10
- package/lib/esm/adapter-express/micro-api/micro.js +97 -42
- package/lib/esm/adapter-express/studio/index.js +1 -1
- package/lib/esm/adapter-express/studio/studio-integration.js +63 -11
- package/lib/esm/types/adapter-express/application-express.d.ts +51 -9
- package/lib/esm/types/adapter-express/express-utils/path-pattern-compat.d.ts +66 -0
- package/lib/esm/types/adapter-express/express-utils/route-constraints.d.ts +12 -3
- package/lib/esm/types/adapter-express/micro-api/micro.d.ts +19 -2
- package/lib/esm/types/adapter-express/studio/index.d.ts +1 -1
- package/lib/esm/types/adapter-express/studio/studio-integration.d.ts +78 -0
- package/lib/package.json +24 -10
- 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
|
-
//
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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
|
-
//
|
|
203
|
-
//
|
|
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
|
-
//
|
|
284
|
-
|
|
285
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
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
|
-
//
|
|
655
|
-
|
|
656
|
-
//
|
|
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
|
-
|
|
660
|
-
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
1165
|
-
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|