@morojs/moro 1.8.11 → 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.
Files changed (84) hide show
  1. package/README.md +4 -2
  2. package/dist/core/cluster/thread-cluster.d.ts +25 -0
  3. package/dist/core/cluster/thread-cluster.js +95 -3
  4. package/dist/core/cluster/thread-cluster.js.map +1 -1
  5. package/dist/core/config/config-validator.js +15 -0
  6. package/dist/core/config/config-validator.js.map +1 -1
  7. package/dist/core/config/index.js +19 -2
  8. package/dist/core/config/index.js.map +1 -1
  9. package/dist/core/framework.d.ts +26 -0
  10. package/dist/core/framework.js +195 -6
  11. package/dist/core/framework.js.map +1 -1
  12. package/dist/core/http/http-server.d.ts +17 -0
  13. package/dist/core/http/http-server.js +21 -1
  14. package/dist/core/http/http-server.js.map +1 -1
  15. package/dist/core/http/http2-server.d.ts +13 -0
  16. package/dist/core/http/http2-server.js +11 -0
  17. package/dist/core/http/http2-server.js.map +1 -1
  18. package/dist/core/http/moro-engine-server.d.ts +28 -0
  19. package/dist/core/http/moro-engine-server.js +118 -12
  20. package/dist/core/http/moro-engine-server.js.map +1 -1
  21. package/dist/core/http/utils/body-type.d.ts +12 -0
  22. package/dist/core/http/utils/body-type.js +70 -0
  23. package/dist/core/http/utils/body-type.js.map +1 -0
  24. package/dist/core/http/utils/ssl-config.d.ts +11 -0
  25. package/dist/core/http/utils/ssl-config.js +3 -0
  26. package/dist/core/http/utils/ssl-config.js.map +1 -1
  27. package/dist/core/http/utils/uws-worker-clustering.js +5 -4
  28. package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
  29. package/dist/core/http/uws-http-server.d.ts +3 -0
  30. package/dist/core/http/uws-http-server.js +22 -1
  31. package/dist/core/http/uws-http-server.js.map +1 -1
  32. package/dist/core/middleware/built-in/body-parsers/index.d.ts +18 -0
  33. package/dist/core/middleware/built-in/body-parsers/index.js +41 -0
  34. package/dist/core/middleware/built-in/body-parsers/index.js.map +1 -1
  35. package/dist/core/middleware/built-in/cache/adapters/cache/redis.d.ts +32 -8
  36. package/dist/core/middleware/built-in/cache/adapters/cache/redis.js +117 -25
  37. package/dist/core/middleware/built-in/cache/adapters/cache/redis.js.map +1 -1
  38. package/dist/core/middleware/built-in/compression/core.js +5 -0
  39. package/dist/core/middleware/built-in/compression/core.js.map +1 -1
  40. package/dist/core/middleware/built-in/session/core.d.ts +1 -0
  41. package/dist/core/middleware/built-in/session/core.js.map +1 -1
  42. package/dist/core/middleware/built-in/static/core.d.ts +18 -0
  43. package/dist/core/middleware/built-in/static/core.js +128 -7
  44. package/dist/core/middleware/built-in/static/core.js.map +1 -1
  45. package/dist/core/networking/adapters/engine-adapter.d.ts +2 -2
  46. package/dist/core/networking/adapters/engine-adapter.js +40 -1
  47. package/dist/core/networking/adapters/engine-adapter.js.map +1 -1
  48. package/dist/core/networking/adapters/socketio-adapter.d.ts +2 -2
  49. package/dist/core/networking/adapters/socketio-adapter.js +10 -1
  50. package/dist/core/networking/adapters/socketio-adapter.js.map +1 -1
  51. package/dist/core/networking/adapters/uws-adapter.d.ts +2 -2
  52. package/dist/core/networking/adapters/uws-adapter.js +54 -3
  53. package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
  54. package/dist/core/networking/adapters/ws-adapter.d.ts +9 -2
  55. package/dist/core/networking/adapters/ws-adapter.js +97 -12
  56. package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
  57. package/dist/core/networking/websocket-adapter.d.ts +19 -1
  58. package/dist/core/networking/websocket-adapter.js.map +1 -1
  59. package/dist/core/routing/index.d.ts +12 -7
  60. package/dist/core/routing/index.js +7 -2
  61. package/dist/core/routing/index.js.map +1 -1
  62. package/dist/core/routing/unified-router.d.ts +48 -9
  63. package/dist/core/routing/unified-router.js +112 -34
  64. package/dist/core/routing/unified-router.js.map +1 -1
  65. package/dist/core/utilities/package-utils.d.ts +11 -0
  66. package/dist/core/utilities/package-utils.js +3 -0
  67. package/dist/core/utilities/package-utils.js.map +1 -1
  68. package/dist/core/workers/worker-manager.js +2 -2
  69. package/dist/core/workers/worker-manager.js.map +1 -1
  70. package/dist/index.d.ts +4 -3
  71. package/dist/index.js +2 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/moro.d.ts +29 -18
  74. package/dist/moro.js +63 -7
  75. package/dist/moro.js.map +1 -1
  76. package/dist/types/cache.d.ts +2 -0
  77. package/dist/types/cache.js.map +1 -1
  78. package/dist/types/config.d.ts +9 -0
  79. package/dist/types/config.js.map +1 -1
  80. package/dist/types/http.d.ts +6 -0
  81. package/dist/types/http.js.map +1 -1
  82. package/dist/types/session.d.ts +1 -0
  83. package/dist/types/session.js.map +1 -1
  84. package/package.json +1 -1
@@ -20,6 +20,7 @@ import { createFrameworkLogger } from '../logger/index.js';
20
20
  import { loadNativeEngine, getNativeEngineLoadErrors, } from '../utilities/package-utils.js';
21
21
  import { parseMultipart as parseMultipartBuffer, } from './utils/multipart-parser.js';
22
22
  import { resolveCompressionSettings, compressBuffer, isCompressible, negotiateEncoding, } from './utils/compression.js';
23
+ import { bodyIsText } from './utils/body-type.js';
23
24
  import { LazyEventEmitter } from './utils/lazy-event-emitter.js';
24
25
  import { parseRawQueryString as parseUwsQueryString } from './utils/query-parser.js';
25
26
  import { PathMatcher } from '../routing/path-matcher.js';
@@ -148,6 +149,12 @@ export class EngineRequest extends LazyEventEmitter {
148
149
  method;
149
150
  path;
150
151
  body = null;
152
+ /** Undecoded request body bytes (parseBody keeps the same Buffer it parsed) */
153
+ _rawBody = undefined;
154
+ /** The request body exactly as received, before any parsing; null when empty. */
155
+ get rawBody() {
156
+ return this._rawBody ?? null;
157
+ }
151
158
  _server;
152
159
  _reqId;
153
160
  _params = undefined;
@@ -1468,6 +1475,13 @@ export class MoroEngineServer {
1468
1475
  /** @internal */ _setStaticRoute;
1469
1476
  _staticOn = false;
1470
1477
  staticRoutes = [];
1478
+ // Engine-answered parameter routes (engine >= 1.1.9, capabilities.paramRoutes):
1479
+ // one variable path segment between prefix and suffix, echoed as the body,
1480
+ // sent without calling into JS. Same guard and same re-apply story as the
1481
+ // static routes above.
1482
+ /** @internal */ _setParamRoute;
1483
+ _paramOn = false;
1484
+ paramRoutes = [];
1471
1485
  // Batched pipelined dispatch (engine >= 1.1.6, capabilities.batchDispatch):
1472
1486
  // the engine parses complete pipelined requests ahead and delivers them in
1473
1487
  // one onRequestBatch(count) call; the descriptors/control/paths buffers are
@@ -1593,6 +1607,10 @@ export class MoroEngineServer {
1593
1607
  this._setStaticRoute = surface.setStaticRoute;
1594
1608
  this._staticOn = true;
1595
1609
  }
1610
+ if (this._capabilities?.paramRoutes === true && typeof surface.setParamRoute === 'function') {
1611
+ this._setParamRoute = surface.setParamRoute;
1612
+ this._paramOn = true;
1613
+ }
1596
1614
  if (this._capabilities?.batchDispatch === true &&
1597
1615
  typeof surface.getBatchBuffers === 'function' &&
1598
1616
  typeof surface.getPath === 'function') {
@@ -1619,7 +1637,11 @@ export class MoroEngineServer {
1619
1637
  this.handle = Object.defineProperties({
1620
1638
  engine: '@morojs/engine',
1621
1639
  address: () => this.isListening
1622
- ? { address: this.host || '0.0.0.0', family: 'IPv4', port: this.port || 0 }
1640
+ ? {
1641
+ address: this.host || '::',
1642
+ family: (this.host || '::').includes(':') ? 'IPv6' : 'IPv4',
1643
+ port: this.port || 0,
1644
+ }
1623
1645
  : null,
1624
1646
  close: (cb) => {
1625
1647
  this.close(cb);
@@ -1709,13 +1731,58 @@ export class MoroEngineServer {
1709
1731
  }
1710
1732
  }
1711
1733
  // listen() after close() registers a fresh native server; give it every
1712
- // static route the old one had.
1734
+ // static and parameter route the old one had.
1713
1735
  _reapplyStaticRoutes() {
1714
1736
  const fn = this._setStaticRoute;
1715
- if (fn === undefined)
1716
- return;
1717
- for (const r of this.staticRoutes) {
1718
- fn(this.serverId, r.methodIdx, r.path, r.status, r.headersFlat, r.body);
1737
+ if (fn !== undefined) {
1738
+ for (const r of this.staticRoutes) {
1739
+ fn(this.serverId, r.methodIdx, r.path, r.status, r.headersFlat, r.body);
1740
+ }
1741
+ }
1742
+ const pfn = this._setParamRoute;
1743
+ if (pfn !== undefined) {
1744
+ for (const r of this.paramRoutes) {
1745
+ pfn(this.serverId, r.methodIdx, r.prefix, r.suffix, r.status, r.headersFlat);
1746
+ }
1747
+ }
1748
+ }
1749
+ /** Whether this engine build answers parameter routes itself. */
1750
+ get paramRoutesEnabled() {
1751
+ return this._paramOn;
1752
+ }
1753
+ /**
1754
+ * Register a route with one variable path segment, between `prefix` and
1755
+ * `suffix`, that the engine answers itself with that segment as the body:
1756
+ * "/user/" + "" for `/user/:id`. The segment goes out undecoded, as it is
1757
+ * on the wire. Returns false when this engine cannot take it (no
1758
+ * capability, compression on, or an unindexed method), so the caller keeps
1759
+ * its JS handler. Registering the same (method, prefix, suffix) again
1760
+ * replaces the earlier reply.
1761
+ */
1762
+ setParamRoute(method, prefix, suffix, status = 200, headers) {
1763
+ const fn = this._setParamRoute;
1764
+ if (!this._paramOn || fn === undefined)
1765
+ return false;
1766
+ if (this._compression.enabled)
1767
+ return false;
1768
+ const methodIdx = METHODS.indexOf(method.toUpperCase());
1769
+ if (methodIdx < 0 || methodIdx >= STATIC_ROUTE_METHODS)
1770
+ return false;
1771
+ const headersFlat = flattenHeaders(headers);
1772
+ const entry = { methodIdx, prefix, suffix, status, headersFlat };
1773
+ const at = this.paramRoutes.findIndex(r => r.methodIdx === methodIdx && r.prefix === prefix && r.suffix === suffix);
1774
+ if (at === -1)
1775
+ this.paramRoutes.push(entry);
1776
+ else
1777
+ this.paramRoutes[at] = entry;
1778
+ fn(this.serverId, methodIdx, prefix, suffix, status, headersFlat);
1779
+ return true;
1780
+ }
1781
+ /** Drop every engine-answered parameter route; the JS routes are untouched. */
1782
+ clearParamRoutes() {
1783
+ this.paramRoutes.length = 0;
1784
+ if (this._paramOn && typeof this._engine.clearParamRoutes === 'function') {
1785
+ this._engine.clearParamRoutes(this.serverId);
1719
1786
  }
1720
1787
  }
1721
1788
  // Register callbacks + limits with the native engine, returning the new
@@ -2113,7 +2180,9 @@ export class MoroEngineServer {
2113
2180
  // closes, in this loop iteration's check phase. One per burst is enough,
2114
2181
  // so it is armed once and re-armed only after it has fired.
2115
2182
  _armMicrotaskDrain() {
2116
- if (this._drainArmed)
2183
+ // An engine that opens a Node callback scope around every callback
2184
+ // (capabilities.callbackScope) drains the queue itself on return.
2185
+ if (this._drainArmed || this._capabilities?.callbackScope === true)
2117
2186
  return;
2118
2187
  this._drainArmed = true;
2119
2188
  setImmediate(() => {
@@ -2277,6 +2346,9 @@ export class MoroEngineServer {
2277
2346
  throw error;
2278
2347
  }
2279
2348
  const buffer = Buffer.from(raw);
2349
+ // getBody() handed us a stable copy, so the raw bytes cost nothing to keep
2350
+ // (webhook signature checks need them even for JSON bodies)
2351
+ httpReq._rawBody = buffer;
2280
2352
  try {
2281
2353
  if (contentType.includes('application/json')) {
2282
2354
  httpReq.body = JSON.parse(buffer.toString('utf-8'));
@@ -2295,7 +2367,9 @@ export class MoroEngineServer {
2295
2367
  httpReq.body = parseMultipartBuffer(buffer, contentType, this.multipartLimits);
2296
2368
  }
2297
2369
  else {
2298
- httpReq.body = buffer.toString('utf-8');
2370
+ // Text decodes to a string; binary (octet-stream, images, protobuf,
2371
+ // ...) stays a Buffer - decoding it would corrupt it irreversibly.
2372
+ httpReq.body = bodyIsText(contentType) ? buffer.toString('utf-8') : buffer;
2299
2373
  }
2300
2374
  }
2301
2375
  catch (parseError) {
@@ -2445,7 +2519,10 @@ export class MoroEngineServer {
2445
2519
  this.logger.warn('Server is already listening', 'Listen');
2446
2520
  return;
2447
2521
  }
2448
- const host = typeof hostOrCallback === 'string' ? hostOrCallback : '0.0.0.0';
2522
+ // No host = every interface, dual-stack, exactly like Node's
2523
+ // server.listen(port): a client that resolves `localhost` to ::1 first
2524
+ // must not be refused. Hosts without IPv6 fall back to IPv4-any below.
2525
+ const host = typeof hostOrCallback === 'string' ? hostOrCallback : '::';
2449
2526
  const cb = typeof hostOrCallback === 'function' ? hostOrCallback : callback;
2450
2527
  try {
2451
2528
  // listen() after close(): the old native server's handles are torn
@@ -2459,11 +2536,23 @@ export class MoroEngineServer {
2459
2536
  }
2460
2537
  // Synchronous bind; throws on bind errors, returns the actual port
2461
2538
  // (meaningful when port 0 asks for an ephemeral one)
2462
- const actualPort = this._engine.listen(this.serverId, host, port);
2539
+ let boundHost = host;
2540
+ let actualPort;
2541
+ try {
2542
+ actualPort = this._engine.listen(this.serverId, host, port);
2543
+ }
2544
+ catch (error) {
2545
+ const code = error?.code;
2546
+ if (host !== '::' || (code !== 'EAFNOSUPPORT' && code !== 'EADDRNOTAVAIL'))
2547
+ throw error;
2548
+ // IPv6 not configured on this host - IPv4-any is what Node does too
2549
+ boundHost = '0.0.0.0';
2550
+ actualPort = this._engine.listen(this.serverId, boundHost, port);
2551
+ }
2463
2552
  this.port = typeof actualPort === 'number' ? actualPort : port;
2464
- this.host = host;
2553
+ this.host = boundHost;
2465
2554
  this.isListening = true;
2466
- this.logger.info(`Moro engine HTTP server listening on ${host}:${this.port}`, 'Listen');
2555
+ this.logger.info(`Moro engine HTTP server listening on ${boundHost}:${this.port}`, 'Listen');
2467
2556
  if (cb)
2468
2557
  cb();
2469
2558
  }
@@ -2537,6 +2626,23 @@ export class MoroEngineServer {
2537
2626
  callback(error instanceof Error ? error : new Error(String(error)));
2538
2627
  }
2539
2628
  }
2629
+ /**
2630
+ * Replace the TLS certificate/key for every handshake from now on
2631
+ * (engine capabilities.tlsReload). Established connections keep the context
2632
+ * they handshaked with. Validated by the engine before the swap: a bad
2633
+ * config throws and the current certificate keeps serving.
2634
+ */
2635
+ updateSsl(ssl) {
2636
+ if (!this._ssl || !this.isSsl) {
2637
+ throw new Error('updateSsl: this server was not started with TLS (server.ssl)');
2638
+ }
2639
+ if (!this._capabilities?.tlsReload || typeof this._engine.updateSsl !== 'function') {
2640
+ throw new Error('TLS reload needs @morojs/engine with capabilities.tlsReload (engine >= 1.1.8)');
2641
+ }
2642
+ this._engine.updateSsl(this.serverId, ssl);
2643
+ // A later close() -> listen() cycle re-registers with the new material
2644
+ this._ssl = ssl;
2645
+ }
2540
2646
  getServer() {
2541
2647
  // Stable observability handle (listening/address) - there is no app object
2542
2648
  return this.handle;