@morojs/moro 1.8.12 → 1.8.13
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 +4 -2
- package/dist/core/cluster/thread-cluster.d.ts +25 -0
- package/dist/core/cluster/thread-cluster.js +95 -3
- package/dist/core/cluster/thread-cluster.js.map +1 -1
- package/dist/core/config/config-validator.js +15 -0
- package/dist/core/config/config-validator.js.map +1 -1
- package/dist/core/config/index.js +19 -2
- package/dist/core/config/index.js.map +1 -1
- package/dist/core/framework.d.ts +26 -0
- package/dist/core/framework.js +195 -6
- package/dist/core/framework.js.map +1 -1
- package/dist/core/http/http-server.d.ts +17 -0
- package/dist/core/http/http-server.js +21 -1
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/http/http2-server.d.ts +13 -0
- package/dist/core/http/http2-server.js +11 -0
- package/dist/core/http/http2-server.js.map +1 -1
- package/dist/core/http/moro-engine-server.d.ts +28 -0
- package/dist/core/http/moro-engine-server.js +115 -11
- package/dist/core/http/moro-engine-server.js.map +1 -1
- package/dist/core/http/utils/body-type.d.ts +12 -0
- package/dist/core/http/utils/body-type.js +70 -0
- package/dist/core/http/utils/body-type.js.map +1 -0
- package/dist/core/http/utils/ssl-config.d.ts +11 -0
- package/dist/core/http/utils/ssl-config.js +3 -0
- package/dist/core/http/utils/ssl-config.js.map +1 -1
- package/dist/core/http/utils/uws-worker-clustering.js +5 -4
- package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
- package/dist/core/http/uws-http-server.d.ts +3 -0
- package/dist/core/http/uws-http-server.js +22 -1
- package/dist/core/http/uws-http-server.js.map +1 -1
- package/dist/core/middleware/built-in/body-parsers/index.d.ts +18 -0
- package/dist/core/middleware/built-in/body-parsers/index.js +41 -0
- package/dist/core/middleware/built-in/body-parsers/index.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/redis.d.ts +32 -8
- package/dist/core/middleware/built-in/cache/adapters/cache/redis.js +117 -25
- package/dist/core/middleware/built-in/cache/adapters/cache/redis.js.map +1 -1
- package/dist/core/middleware/built-in/compression/core.js +5 -0
- package/dist/core/middleware/built-in/compression/core.js.map +1 -1
- package/dist/core/middleware/built-in/session/core.d.ts +1 -0
- package/dist/core/middleware/built-in/session/core.js.map +1 -1
- package/dist/core/middleware/built-in/static/core.d.ts +18 -0
- package/dist/core/middleware/built-in/static/core.js +128 -7
- package/dist/core/middleware/built-in/static/core.js.map +1 -1
- package/dist/core/networking/adapters/engine-adapter.d.ts +2 -2
- package/dist/core/networking/adapters/engine-adapter.js +40 -1
- package/dist/core/networking/adapters/engine-adapter.js.map +1 -1
- package/dist/core/networking/adapters/socketio-adapter.d.ts +2 -2
- package/dist/core/networking/adapters/socketio-adapter.js +10 -1
- package/dist/core/networking/adapters/socketio-adapter.js.map +1 -1
- package/dist/core/networking/adapters/uws-adapter.d.ts +2 -2
- package/dist/core/networking/adapters/uws-adapter.js +54 -3
- package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
- package/dist/core/networking/adapters/ws-adapter.d.ts +9 -2
- package/dist/core/networking/adapters/ws-adapter.js +97 -12
- package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
- package/dist/core/networking/websocket-adapter.d.ts +19 -1
- package/dist/core/networking/websocket-adapter.js.map +1 -1
- package/dist/core/routing/index.d.ts +12 -8
- package/dist/core/routing/index.js +7 -2
- package/dist/core/routing/index.js.map +1 -1
- package/dist/core/routing/unified-router.d.ts +46 -8
- package/dist/core/routing/unified-router.js +108 -32
- package/dist/core/routing/unified-router.js.map +1 -1
- package/dist/core/utilities/package-utils.d.ts +7 -0
- package/dist/core/utilities/package-utils.js +2 -0
- package/dist/core/utilities/package-utils.js.map +1 -1
- package/dist/core/workers/worker-manager.js +2 -2
- package/dist/core/workers/worker-manager.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/moro.d.ts +12 -1
- package/dist/moro.js +63 -7
- package/dist/moro.js.map +1 -1
- package/dist/types/cache.d.ts +2 -0
- package/dist/types/cache.js.map +1 -1
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/http.d.ts +6 -0
- package/dist/types/http.js.map +1 -1
- package/dist/types/session.d.ts +1 -0
- package/dist/types/session.js.map +1 -1
- package/package.json +1 -1
|
@@ -23,6 +23,23 @@ import { loadConfigFromAllSources, loadConfigFromAllSourcesAsync } from './confi
|
|
|
23
23
|
import { initializeAndLockConfig, getGlobalConfig as getConfig, isConfigLocked, resetConfigForTesting, } from './config-manager.js';
|
|
24
24
|
import { createFrameworkLogger } from '../logger/index.js';
|
|
25
25
|
const logger = createFrameworkLogger('ConfigSystem');
|
|
26
|
+
/**
|
|
27
|
+
* Configuration is one-per-process: the first createApp() locks it and every
|
|
28
|
+
* later createApp() in the same process (or worker thread) receives that
|
|
29
|
+
* same config. A second app that passes its OWN options (another port, TLS,
|
|
30
|
+
* ...) would otherwise silently boot with the first app's settings, so name
|
|
31
|
+
* the ignored keys at ERROR rather than a debug line nobody sees.
|
|
32
|
+
*/
|
|
33
|
+
function warnIgnoredOptions(options) {
|
|
34
|
+
const ignored = options ? Object.keys(options).filter(k => k !== 'logger') : [];
|
|
35
|
+
if (ignored.length === 0) {
|
|
36
|
+
logger.debug('Configuration already locked, returning existing config');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
logger.error(`createApp() called again after the configuration was locked; ignoring options [${ignored.join(', ')}]. ` +
|
|
40
|
+
'MoroJS keeps one configuration per process: the first createApp() wins and later apps share it. ' +
|
|
41
|
+
'Run a second differently-configured app in its own process.', 'ConfigLock');
|
|
42
|
+
}
|
|
26
43
|
/**
|
|
27
44
|
* Initialize configuration system with createApp options
|
|
28
45
|
* This is the main entry point called by createApp()
|
|
@@ -32,7 +49,7 @@ const logger = createFrameworkLogger('ConfigSystem');
|
|
|
32
49
|
*/
|
|
33
50
|
export function initializeConfig(options) {
|
|
34
51
|
if (isConfigLocked()) {
|
|
35
|
-
|
|
52
|
+
warnIgnoredOptions(options);
|
|
36
53
|
return getConfig();
|
|
37
54
|
}
|
|
38
55
|
logger.debug('Initializing configuration system');
|
|
@@ -53,7 +70,7 @@ export function initializeConfig(options) {
|
|
|
53
70
|
*/
|
|
54
71
|
export async function initializeConfigAsync(options) {
|
|
55
72
|
if (isConfigLocked()) {
|
|
56
|
-
|
|
73
|
+
warnIgnoredOptions(options);
|
|
57
74
|
return getConfig();
|
|
58
75
|
}
|
|
59
76
|
logger.debug('Initializing configuration system (async)');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/config/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,mCAAmC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AAGjC,mEAAmE;AACnE,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,8CAA8C;AAC9C,cAAc,YAAY,CAAC;AAI3B,OAAO,EAAE,wBAAwB,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EACL,uBAAuB,EACvB,eAAe,IAAI,SAAS,EAC5B,cAAc,EACd,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,GAAG,qBAAqB,CAAC,cAAc,CAAC,CAAC;AAErD;;;;;;GAMG;AACH,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/config/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,mCAAmC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AAGjC,mEAAmE;AACnE,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,8CAA8C;AAC9C,cAAc,YAAY,CAAC;AAI3B,OAAO,EAAE,wBAAwB,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EACL,uBAAuB,EACvB,eAAe,IAAI,SAAS,EAC5B,cAAc,EACd,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,GAAG,qBAAqB,CAAC,cAAc,CAAC,CAAC;AAErD;;;;;;GAMG;AACH,SAAS,kBAAkB,CAAC,OAAqB;IAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAChF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;QACxE,OAAO;IACT,CAAC;IACD,MAAM,CAAC,KAAK,CACV,kFAAkF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;QACvG,kGAAkG;QAClG,6DAA6D,EAC/D,YAAY,CACb,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAqB;IACpD,IAAI,cAAc,EAAE,EAAE,CAAC;QACrB,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC5B,OAAO,SAAS,EAAE,CAAC;IACrB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAElD,6DAA6D;IAC7D,MAAM,MAAM,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;IAEjD,oDAAoD;IACpD,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAEhC,MAAM,CAAC,IAAI,CACT,gDAAgD,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,6CAA6C,CACzJ,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAAqB;IAC/D,IAAI,cAAc,EAAE,EAAE,CAAC;QACrB,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC5B,OAAO,SAAS,EAAE,CAAC;IACrB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAE1D,MAAM,MAAM,GAAG,MAAM,6BAA6B,CAAC,OAAO,CAAC,CAAC;IAE5D,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAEhC,MAAM,CAAC,IAAI,CACT,gDAAgD,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,6CAA6C,CACzJ,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,wBAAwB,EAAE,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAoB;IACxD,OAAO,wBAAwB,CAAC,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,SAAS,EAAE,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,cAAc,EAAE,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,qBAAqB,EAAE,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * Configuration System - Immutable Config with createApp Override Support\n *\n * This is the main entry point for the MoroJS configuration system.\n * It provides a clean, immutable configuration that is locked at createApp() time.\n *\n * Key Features:\n * - Immutable configuration after initialization\n * - Clear precedence: Env Vars > createApp Options > Config File > Defaults\n * - Type-safe validation\n * - Single source of truth\n */\n\n// Export types and core components\nexport * from './schema.js';\nexport * from './config-sources.js';\nexport * from './config-validator.js';\nexport * from './file-loader.js';\nexport type { OriginFunction } from '../../types/config.js';\n\n// Export specific functions from config-manager to avoid conflicts\nexport {\n initializeAndLockConfig,\n isConfigLocked,\n resetConfigForTesting,\n} from './config-manager.js';\n\n// Export utilities for backward compatibility\nexport * from './utils.js';\n\nimport { MoroOptions } from '../../types/core.js';\nimport { AppConfig } from '../../types/config.js';\nimport { loadConfigFromAllSources, loadConfigFromAllSourcesAsync } from './config-sources.js';\nimport {\n initializeAndLockConfig,\n getGlobalConfig as getConfig,\n isConfigLocked,\n resetConfigForTesting,\n} from './config-manager.js';\nimport { createFrameworkLogger } from '../logger/index.js';\n\nconst logger = createFrameworkLogger('ConfigSystem');\n\n/**\n * Configuration is one-per-process: the first createApp() locks it and every\n * later createApp() in the same process (or worker thread) receives that\n * same config. A second app that passes its OWN options (another port, TLS,\n * ...) would otherwise silently boot with the first app's settings, so name\n * the ignored keys at ERROR rather than a debug line nobody sees.\n */\nfunction warnIgnoredOptions(options?: MoroOptions): void {\n const ignored = options ? Object.keys(options).filter(k => k !== 'logger') : [];\n if (ignored.length === 0) {\n logger.debug('Configuration already locked, returning existing config');\n return;\n }\n logger.error(\n `createApp() called again after the configuration was locked; ignoring options [${ignored.join(', ')}]. ` +\n 'MoroJS keeps one configuration per process: the first createApp() wins and later apps share it. ' +\n 'Run a second differently-configured app in its own process.',\n 'ConfigLock'\n );\n}\n\n/**\n * Initialize configuration system with createApp options\n * This is the main entry point called by createApp()\n *\n * @param options - createApp options that can override config file and defaults\n * @returns Immutable, validated configuration object\n */\nexport function initializeConfig(options?: MoroOptions): Readonly<AppConfig> {\n if (isConfigLocked()) {\n warnIgnoredOptions(options);\n return getConfig();\n }\n\n logger.debug('Initializing configuration system');\n\n // Load configuration from all sources with proper precedence\n const config = loadConfigFromAllSources(options);\n\n // Lock the configuration to prevent further changes\n initializeAndLockConfig(config);\n\n logger.info(\n `Configuration system initialized and locked: ${process.env.NODE_ENV || 'development'}:${config.server.port} (sources: env + file + options + defaults)`\n );\n\n return config;\n}\n\n/**\n * Initialize configuration system asynchronously.\n * Uses dynamic import() for the config file — supports ESM config files in\n * projects with \"type\":\"module\". Called internally by the async createApp().\n *\n * @param options - createApp options that can override config file and defaults\n * @returns Immutable, validated configuration object\n */\nexport async function initializeConfigAsync(options?: MoroOptions): Promise<Readonly<AppConfig>> {\n if (isConfigLocked()) {\n warnIgnoredOptions(options);\n return getConfig();\n }\n\n logger.debug('Initializing configuration system (async)');\n\n const config = await loadConfigFromAllSourcesAsync(options);\n\n initializeAndLockConfig(config);\n\n logger.info(\n `Configuration system initialized and locked: ${process.env.NODE_ENV || 'development'}:${config.server.port} (sources: env + file + options + defaults)`\n );\n\n return config;\n}\n\n/**\n * Load configuration without locking (for testing and utilities)\n * This maintains backward compatibility with existing code\n */\nexport function loadConfig(): AppConfig {\n return loadConfigFromAllSources();\n}\n\n/**\n * Load configuration with createApp options (for testing and utilities)\n * This maintains backward compatibility with existing code\n */\nexport function loadConfigWithOptions(options: MoroOptions): AppConfig {\n return loadConfigFromAllSources(options);\n}\n\n/**\n * Get the current global configuration\n * Alias for getGlobalConfig() for backward compatibility\n */\nexport function getGlobalConfig(): Readonly<AppConfig> {\n return getConfig();\n}\n\n/**\n * Check if configuration has been initialized and locked\n * Alias for isConfigLocked() for backward compatibility\n */\nexport function isConfigInitialized(): boolean {\n return isConfigLocked();\n}\n\n/**\n * Reset configuration state (for testing only)\n * @internal\n */\nexport function resetConfig(): void {\n resetConfigForTesting();\n}\n"]}
|
package/dist/core/framework.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { MoroEngineServer } from './http/moro-engine-server.js';
|
|
|
5
5
|
import { MoroHttp2Server, Http2ServerOptions } from './http/http2-server.js';
|
|
6
6
|
import { Container } from './utilities/index.js';
|
|
7
7
|
import { WebSocketManager } from './networking/websocket-manager.js';
|
|
8
|
+
import { type SSLConfigInput } from './http/utils/ssl-config.js';
|
|
8
9
|
import { ModuleConfig } from '../types/module.js';
|
|
9
10
|
import { MoroOptions as CoreMoroOptions } from '../types/core.js';
|
|
10
11
|
import { WebSocketAdapter, WebSocketAdapterOptions } from './networking/websocket-adapter.js';
|
|
@@ -56,6 +57,12 @@ export declare class Moro extends EventEmitter {
|
|
|
56
57
|
private usingEngine;
|
|
57
58
|
private usingHttp2;
|
|
58
59
|
private engineInfo;
|
|
60
|
+
/** The normalized server.ssl in effect (updated by reloadTLS) */
|
|
61
|
+
private sslConfig;
|
|
62
|
+
private tlsWatchers;
|
|
63
|
+
private tlsWatchTimer;
|
|
64
|
+
/** Hash of the last key/cert/CA bytes loaded from disk (skips no-op reloads) */
|
|
65
|
+
private tlsMaterialHash;
|
|
59
66
|
private websocketSetupPromise;
|
|
60
67
|
constructor(options?: MoroOptions);
|
|
61
68
|
use(middleware: any): this;
|
|
@@ -116,6 +123,25 @@ export declare class Moro extends EventEmitter {
|
|
|
116
123
|
setupFinalModuleHandler(): void;
|
|
117
124
|
private setupWebSocketHandlers;
|
|
118
125
|
private checkRateLimit;
|
|
126
|
+
/**
|
|
127
|
+
* Rotate the TLS certificate/key on the running listener. The new material
|
|
128
|
+
* is validated (files readable, PEM parses, key matches cert) BEFORE the
|
|
129
|
+
* listener is touched, so a half-written file or a wrong pair rejects and
|
|
130
|
+
* the current certificate keeps serving; connections that already
|
|
131
|
+
* handshaked are never affected. Policy fields (minVersion, mTLS flags,
|
|
132
|
+
* passphrase) and the CA carry over unless the update sets them.
|
|
133
|
+
*/
|
|
134
|
+
reloadTLS(ssl?: SSLConfigInput): Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* `server.ssl.watch`: reload the certificate when its files change on disk.
|
|
137
|
+
* The DIRECTORIES holding the files are watched (an ACME client or a
|
|
138
|
+
* Kubernetes secret mount replaces files by rename/symlink swap, which a
|
|
139
|
+
* per-file watcher misses), changes are debounced, unchanged bytes are
|
|
140
|
+
* ignored, and a failed reload is logged while the current certificate
|
|
141
|
+
* keeps serving. Watchers are non-persistent and closed by stopTlsWatch().
|
|
142
|
+
*/
|
|
143
|
+
startTlsWatch(): void;
|
|
144
|
+
stopTlsWatch(): void;
|
|
119
145
|
/**
|
|
120
146
|
* Which HTTP server backs this app, which package provides it, and the
|
|
121
147
|
* fallback reason when the native engine was requested but unavailable.
|
package/dist/core/framework.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
2
4
|
import { EventEmitter } from 'events';
|
|
5
|
+
import { requireBuiltin } from './utilities/builtin.js';
|
|
3
6
|
import { MoroHttpServer } from './http/index.js';
|
|
4
7
|
import { UWebSocketsHttpServer } from './http/uws-http-server.js';
|
|
5
8
|
import { MoroEngineServer } from './http/moro-engine-server.js';
|
|
@@ -11,7 +14,7 @@ import { WebSocketManager } from './networking/websocket-manager.js';
|
|
|
11
14
|
import { CircuitBreaker } from './utilities/circuit-breaker.js';
|
|
12
15
|
import { isPackageAvailable, loadNativeEngine, getNativeEngineLoadErrors, } from './utilities/package-utils.js';
|
|
13
16
|
import { parseSizeToBytes } from './http/utils/size.js';
|
|
14
|
-
import { normalizeSSLConfig, sslForNode, sslForUws, sslIsComplete, } from './http/utils/ssl-config.js';
|
|
17
|
+
import { normalizeSSLConfig, sslForEngine, sslForNode, sslForUws, sslIsComplete, } from './http/utils/ssl-config.js';
|
|
15
18
|
import { MoroEventBus } from './events/index.js';
|
|
16
19
|
import { createFrameworkLogger, logger as globalLogger } from './logger/index.js';
|
|
17
20
|
import { mergeWebSocketConfig, } from './networking/websocket-adapter.js';
|
|
@@ -38,6 +41,12 @@ export class Moro extends EventEmitter {
|
|
|
38
41
|
usingHttp2 = false;
|
|
39
42
|
// Which server actually booted (engine = native uWS-style engine)
|
|
40
43
|
engineInfo = { server: 'node' };
|
|
44
|
+
/** The normalized server.ssl in effect (updated by reloadTLS) */
|
|
45
|
+
sslConfig = null;
|
|
46
|
+
tlsWatchers = [];
|
|
47
|
+
tlsWatchTimer;
|
|
48
|
+
/** Hash of the last key/cert/CA bytes loaded from disk (skips no-op reloads) */
|
|
49
|
+
tlsMaterialHash;
|
|
41
50
|
// WebSocket initialization promise to handle async adapter detection
|
|
42
51
|
websocketSetupPromise = null;
|
|
43
52
|
constructor(options = {}) {
|
|
@@ -73,6 +82,7 @@ export class Moro extends EventEmitter {
|
|
|
73
82
|
// projected per-runtime, so a single server.ssl / server.limits flows
|
|
74
83
|
// everywhere regardless of which server boots.
|
|
75
84
|
const ssl = normalizeSSLConfig(this.config.server?.ssl, options.https, this.logger);
|
|
85
|
+
this.sslConfig = ssl;
|
|
76
86
|
const rt = this.buildRuntimeLimits();
|
|
77
87
|
if (engineChoice.kind === 'engine') {
|
|
78
88
|
try {
|
|
@@ -84,7 +94,10 @@ export class Moro extends EventEmitter {
|
|
|
84
94
|
if (typeof engineSurface?.serve === 'function' &&
|
|
85
95
|
typeof engineSurface?.respond === 'function') {
|
|
86
96
|
this.httpServer = new MoroEngineServer({
|
|
87
|
-
|
|
97
|
+
// Project to the engine's own option names (key_file_name / key,
|
|
98
|
+
// ...). The normalized shape's keyFile/certFile mean nothing to
|
|
99
|
+
// serve(), which would throw and silently fall back to Node https.
|
|
100
|
+
...(ssl ? { ssl: sslForEngine(ssl) } : {}),
|
|
88
101
|
limits: rt,
|
|
89
102
|
maxBodySize: rt.maxBodySize,
|
|
90
103
|
maxUploadSize: rt.maxUploadSize,
|
|
@@ -124,7 +137,10 @@ export class Moro extends EventEmitter {
|
|
|
124
137
|
// Construction failed after a successful module load - unexpected, but
|
|
125
138
|
// never boot nothing: fall back to the Node.js http server.
|
|
126
139
|
const reason = error instanceof Error ? error.message : String(error);
|
|
127
|
-
|
|
140
|
+
// With TLS configured this is almost always a certificate/key problem,
|
|
141
|
+
// and a deployment tuned for the engine would otherwise run on a
|
|
142
|
+
// different server behind a warning buried among INFO lines.
|
|
143
|
+
this.logger[ssl ? 'error' : 'warn'](`Native engine (${engineChoice.requested}) failed to initialize, falling back to Node.js http.Server. Error: ${reason}`, 'ServerInit');
|
|
128
144
|
this.usingUWebSockets = false;
|
|
129
145
|
this.usingEngine = false;
|
|
130
146
|
this.engineInfo = { server: 'node', fallbackReason: reason };
|
|
@@ -987,6 +1003,162 @@ export class Moro extends EventEmitter {
|
|
|
987
1003
|
limit.count++;
|
|
988
1004
|
return true;
|
|
989
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Rotate the TLS certificate/key on the running listener. The new material
|
|
1008
|
+
* is validated (files readable, PEM parses, key matches cert) BEFORE the
|
|
1009
|
+
* listener is touched, so a half-written file or a wrong pair rejects and
|
|
1010
|
+
* the current certificate keeps serving; connections that already
|
|
1011
|
+
* handshaked are never affected. Policy fields (minVersion, mTLS flags,
|
|
1012
|
+
* passphrase) and the CA carry over unless the update sets them.
|
|
1013
|
+
*/
|
|
1014
|
+
async reloadTLS(ssl) {
|
|
1015
|
+
const current = this.sslConfig;
|
|
1016
|
+
if (!current) {
|
|
1017
|
+
throw new Error('reloadTLS: this app was not started with TLS (server.ssl)');
|
|
1018
|
+
}
|
|
1019
|
+
if (this.usingUWebSockets) {
|
|
1020
|
+
throw new Error('reloadTLS is not supported on the uWebSockets.js engine');
|
|
1021
|
+
}
|
|
1022
|
+
let next;
|
|
1023
|
+
if (ssl) {
|
|
1024
|
+
const update = normalizeSSLConfig(ssl, undefined, this.logger);
|
|
1025
|
+
if (!update || !sslIsComplete(update)) {
|
|
1026
|
+
throw new Error('reloadTLS: ssl must carry both a key and a certificate');
|
|
1027
|
+
}
|
|
1028
|
+
const hasCa = Boolean(update.ca || update.caFile);
|
|
1029
|
+
next = {
|
|
1030
|
+
// operator policy carries over...
|
|
1031
|
+
passphrase: current.passphrase,
|
|
1032
|
+
minVersion: current.minVersion,
|
|
1033
|
+
requestCert: current.requestCert,
|
|
1034
|
+
rejectUnauthorized: current.rejectUnauthorized,
|
|
1035
|
+
// ...as does the CA when the update names none
|
|
1036
|
+
...(hasCa ? {} : { ca: current.ca, caFile: current.caFile }),
|
|
1037
|
+
// and the update's own fields win (undefined entries dropped so they
|
|
1038
|
+
// do not shadow the carried-over values)
|
|
1039
|
+
...Object.fromEntries(Object.entries(update).filter(([, v]) => v !== undefined)),
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
if (!current.keyFile || !current.certFile) {
|
|
1044
|
+
throw new Error('reloadTLS() with no argument re-reads the server.ssl key/cert files, but this app was configured with inline PEM - pass the new material');
|
|
1045
|
+
}
|
|
1046
|
+
next = { ...current };
|
|
1047
|
+
}
|
|
1048
|
+
// One consistent snapshot of the material, validated on every backend the
|
|
1049
|
+
// same way (node's own parser: bad PEM / key-cert mismatch throw here).
|
|
1050
|
+
const nodeSsl = sslForNode(next);
|
|
1051
|
+
const tls = requireBuiltin('tls');
|
|
1052
|
+
tls.createSecureContext(nodeSsl);
|
|
1053
|
+
if (this.usingEngine) {
|
|
1054
|
+
// Hand the engine the validated bytes rather than the paths, so it
|
|
1055
|
+
// cannot read a different (newer, half-written) file than we checked.
|
|
1056
|
+
const engineSsl = sslForEngine({
|
|
1057
|
+
...next,
|
|
1058
|
+
key: nodeSsl.key,
|
|
1059
|
+
cert: nodeSsl.cert,
|
|
1060
|
+
ca: nodeSsl.ca,
|
|
1061
|
+
keyFile: undefined,
|
|
1062
|
+
certFile: undefined,
|
|
1063
|
+
caFile: undefined,
|
|
1064
|
+
});
|
|
1065
|
+
this.httpServer.updateSsl(engineSsl);
|
|
1066
|
+
}
|
|
1067
|
+
else if (this.usingHttp2) {
|
|
1068
|
+
this.httpServer.updateSsl(nodeSsl);
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
this.httpServer.updateSsl(nodeSsl);
|
|
1072
|
+
}
|
|
1073
|
+
this.sslConfig = next;
|
|
1074
|
+
this.tlsMaterialHash = MoroCoreTls.hashMaterial(nodeSsl);
|
|
1075
|
+
this.logger.info('TLS certificate reloaded', 'TLS');
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* `server.ssl.watch`: reload the certificate when its files change on disk.
|
|
1079
|
+
* The DIRECTORIES holding the files are watched (an ACME client or a
|
|
1080
|
+
* Kubernetes secret mount replaces files by rename/symlink swap, which a
|
|
1081
|
+
* per-file watcher misses), changes are debounced, unchanged bytes are
|
|
1082
|
+
* ignored, and a failed reload is logged while the current certificate
|
|
1083
|
+
* keeps serving. Watchers are non-persistent and closed by stopTlsWatch().
|
|
1084
|
+
*/
|
|
1085
|
+
startTlsWatch() {
|
|
1086
|
+
const ssl = this.sslConfig;
|
|
1087
|
+
const watch = this.config.server?.ssl?.watch;
|
|
1088
|
+
if (!ssl || !watch || this.tlsWatchers.length > 0)
|
|
1089
|
+
return;
|
|
1090
|
+
if (!ssl.keyFile || !ssl.certFile) {
|
|
1091
|
+
this.logger.warn('server.ssl.watch needs keyFile/certFile paths - inline PEM cannot be watched', 'TLS');
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (this.usingUWebSockets) {
|
|
1095
|
+
this.logger.warn('server.ssl.watch is not supported on the uWebSockets.js engine', 'TLS');
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const debounceMs = typeof watch === 'object' && typeof watch.debounceMs === 'number' && watch.debounceMs > 0
|
|
1099
|
+
? watch.debounceMs
|
|
1100
|
+
: 1000;
|
|
1101
|
+
try {
|
|
1102
|
+
this.tlsMaterialHash = MoroCoreTls.hashMaterial(sslForNode(ssl));
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
// unreadable now; the first change event will report properly
|
|
1106
|
+
}
|
|
1107
|
+
const files = [ssl.keyFile, ssl.certFile, ...(ssl.caFile ?? [])].map(f => path.resolve(f));
|
|
1108
|
+
const schedule = () => {
|
|
1109
|
+
if (this.tlsWatchTimer)
|
|
1110
|
+
clearTimeout(this.tlsWatchTimer);
|
|
1111
|
+
this.tlsWatchTimer = setTimeout(() => {
|
|
1112
|
+
this.tlsWatchTimer = undefined;
|
|
1113
|
+
let hash;
|
|
1114
|
+
try {
|
|
1115
|
+
hash = MoroCoreTls.hashMaterial(sslForNode(this.sslConfig ?? ssl));
|
|
1116
|
+
}
|
|
1117
|
+
catch (err) {
|
|
1118
|
+
// Mid-rotation (one file replaced, the other not yet): wait for the
|
|
1119
|
+
// next event rather than logging a scary error per half-write.
|
|
1120
|
+
this.logger.debug(`TLS files not readable yet: ${err instanceof Error ? err.message : String(err)}`, 'TLS');
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (hash === this.tlsMaterialHash)
|
|
1124
|
+
return; // touched, not changed
|
|
1125
|
+
this.reloadTLS().catch(err => {
|
|
1126
|
+
this.logger.error(`TLS reload failed; the current certificate keeps serving: ${err instanceof Error ? err.message : String(err)}`, 'TLS');
|
|
1127
|
+
});
|
|
1128
|
+
}, debounceMs);
|
|
1129
|
+
this.tlsWatchTimer.unref?.();
|
|
1130
|
+
};
|
|
1131
|
+
for (const dir of new Set(files.map(f => path.dirname(f)))) {
|
|
1132
|
+
try {
|
|
1133
|
+
const watcher = fs.watch(dir, { persistent: false }, () => schedule());
|
|
1134
|
+
watcher.on('error', err => {
|
|
1135
|
+
this.logger.warn(`TLS file watcher error on ${dir}: ${err.message}`, 'TLS');
|
|
1136
|
+
});
|
|
1137
|
+
this.tlsWatchers.push(watcher);
|
|
1138
|
+
}
|
|
1139
|
+
catch (err) {
|
|
1140
|
+
this.logger.warn(`Cannot watch ${dir} for certificate changes: ${err instanceof Error ? err.message : String(err)}`, 'TLS');
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (this.tlsWatchers.length > 0) {
|
|
1144
|
+
this.logger.info(`Watching TLS certificate files for changes (${files.length} files)`, 'TLS');
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
stopTlsWatch() {
|
|
1148
|
+
if (this.tlsWatchTimer) {
|
|
1149
|
+
clearTimeout(this.tlsWatchTimer);
|
|
1150
|
+
this.tlsWatchTimer = undefined;
|
|
1151
|
+
}
|
|
1152
|
+
for (const watcher of this.tlsWatchers) {
|
|
1153
|
+
try {
|
|
1154
|
+
watcher.close();
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
// already closed
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
this.tlsWatchers = [];
|
|
1161
|
+
}
|
|
990
1162
|
/**
|
|
991
1163
|
* Which HTTP server backs this app, which package provides it, and the
|
|
992
1164
|
* fallback reason when the native engine was requested but unavailable.
|
|
@@ -1214,14 +1386,20 @@ export class Moro extends EventEmitter {
|
|
|
1214
1386
|
return this.circuitBreakers.get(key);
|
|
1215
1387
|
}
|
|
1216
1388
|
listen(port, host, callback) {
|
|
1389
|
+
// Certificate watching (server.ssl.watch) starts once the listener is up
|
|
1390
|
+
const userCb = typeof host === 'function' ? host : callback;
|
|
1391
|
+
const onListening = () => {
|
|
1392
|
+
this.startTlsWatch();
|
|
1393
|
+
userCb?.();
|
|
1394
|
+
};
|
|
1217
1395
|
if (typeof host === 'function') {
|
|
1218
|
-
this.httpServer.listen(port,
|
|
1396
|
+
this.httpServer.listen(port, onListening);
|
|
1219
1397
|
}
|
|
1220
1398
|
else if (host) {
|
|
1221
|
-
this.httpServer.listen(port, host,
|
|
1399
|
+
this.httpServer.listen(port, host, onListening);
|
|
1222
1400
|
}
|
|
1223
1401
|
else {
|
|
1224
|
-
this.httpServer.listen(port,
|
|
1402
|
+
this.httpServer.listen(port, onListening);
|
|
1225
1403
|
}
|
|
1226
1404
|
}
|
|
1227
1405
|
// Compatibility method for existing controllers
|
|
@@ -1238,4 +1416,15 @@ export class Moro extends EventEmitter {
|
|
|
1238
1416
|
return undefined;
|
|
1239
1417
|
}
|
|
1240
1418
|
}
|
|
1419
|
+
/** TLS helpers kept out of the class body (no per-instance state). */
|
|
1420
|
+
const MoroCoreTls = {
|
|
1421
|
+
hashMaterial(ssl) {
|
|
1422
|
+
const h = crypto.createHash('sha256');
|
|
1423
|
+
h.update(ssl.key);
|
|
1424
|
+
h.update(ssl.cert);
|
|
1425
|
+
for (const ca of ssl.ca ?? [])
|
|
1426
|
+
h.update(ca);
|
|
1427
|
+
return h.digest('hex');
|
|
1428
|
+
},
|
|
1429
|
+
};
|
|
1241
1430
|
//# sourceMappingURL=framework.js.map
|