@zero-server/sdk 0.9.0 → 0.9.2
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 +21 -21
- package/README.md +460 -437
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +460 -460
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +136 -136
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +254 -254
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
package/lib/app.js
CHANGED
|
@@ -1,1172 +1,1172 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module app
|
|
3
|
-
* @description HTTP application with middleware pipeline,
|
|
4
|
-
* method-based routing, HTTP/2, HTTPS, and HTTP/1.1 support,
|
|
5
|
-
* built-in WebSocket upgrade handling, gRPC service hosting,
|
|
6
|
-
* trust proxy resolution, and route introspection.
|
|
7
|
-
* Created via `createApp()` in the public API.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* const { createApp } = require('@zero-server/sdk');
|
|
11
|
-
* const app = createApp();
|
|
12
|
-
*
|
|
13
|
-
* app.use(logger());
|
|
14
|
-
* app.get('/hello', (req, res) => res.json({ hello: 'world' }));
|
|
15
|
-
* app.listen(3000);
|
|
16
|
-
*
|
|
17
|
-
* @example | HTTP/2 with TLS
|
|
18
|
-
* const fs = require('fs');
|
|
19
|
-
* app.listen(443, {
|
|
20
|
-
* http2: true,
|
|
21
|
-
* key: fs.readFileSync('key.pem'),
|
|
22
|
-
* cert: fs.readFileSync('cert.pem'),
|
|
23
|
-
* });
|
|
24
|
-
*
|
|
25
|
-
* @example | HTTP/2 Cleartext
|
|
26
|
-
* app.listen(3000, { http2: true });
|
|
27
|
-
*/
|
|
28
|
-
const http = require('http');
|
|
29
|
-
const https = require('https');
|
|
30
|
-
const http2 = require('http2');
|
|
31
|
-
const Router = require('./router');
|
|
32
|
-
const { Request, Response } = require('./http');
|
|
33
|
-
const { handleUpgrade } = require('./ws');
|
|
34
|
-
const { GrpcServiceRegistry } = require('./grpc/server');
|
|
35
|
-
const { LifecycleManager, LIFECYCLE_STATE } = require('./lifecycle');
|
|
36
|
-
const { healthCheck, createHealthHandlers, memoryCheck, eventLoopCheck, diskSpaceCheck } = require('./observe/health');
|
|
37
|
-
const { MetricsRegistry, createDefaultMetrics, metricsMiddleware, metricsEndpoint } = require('./observe/metrics');
|
|
38
|
-
const { Tracer, tracingMiddleware, instrumentFetch } = require('./observe/tracing');
|
|
39
|
-
const log = require('./debug')('zero:app');
|
|
40
|
-
|
|
41
|
-
class App
|
|
42
|
-
{
|
|
43
|
-
/**
|
|
44
|
-
* Create a new App instance.
|
|
45
|
-
* Initialises an empty middleware stack, a `Router`, and binds
|
|
46
|
-
* `this.handler` for direct use with `http.createServer()`.
|
|
47
|
-
*
|
|
48
|
-
* @constructor
|
|
49
|
-
*/
|
|
50
|
-
constructor()
|
|
51
|
-
{
|
|
52
|
-
/** @type {Router} */
|
|
53
|
-
this.router = new Router();
|
|
54
|
-
/** @type {Function[]} */
|
|
55
|
-
this.middlewares = [];
|
|
56
|
-
/** @type {Function|null} */
|
|
57
|
-
this._errorHandler = null;
|
|
58
|
-
/** @type {Map<string, { handler: Function, opts: object }>} WebSocket upgrade handlers keyed by path */
|
|
59
|
-
this._wsHandlers = new Map();
|
|
60
|
-
/** @type {GrpcServiceRegistry|null} gRPC service registry (lazily created). */
|
|
61
|
-
this._grpcRegistry = null;
|
|
62
|
-
/** @type {import('./grpc/health').HealthService|null} gRPC health service. */
|
|
63
|
-
this._healthService = null;
|
|
64
|
-
/** @type {import('./grpc/reflection').ReflectionService|null} gRPC reflection service. */
|
|
65
|
-
this._reflectionService = null;
|
|
66
|
-
/** @type {import('http').Server|import('https').Server|null} */
|
|
67
|
-
this._server = null;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Application-level settings store.
|
|
71
|
-
* @type {Object<string, *>}
|
|
72
|
-
* @private
|
|
73
|
-
*/
|
|
74
|
-
this._settings = {};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Application-level locals — persistent across the app lifecycle.
|
|
78
|
-
* Merged into every `req.locals` and `res.locals` on every request.
|
|
79
|
-
* @type {Object<string, *>}
|
|
80
|
-
*/
|
|
81
|
-
this.locals = {};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Parameter pre-processing handlers.
|
|
85
|
-
* @type {Object<string, Function[]>}
|
|
86
|
-
* @private
|
|
87
|
-
*/
|
|
88
|
-
this._paramHandlers = {};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Lifecycle manager for graceful shutdown and connection tracking.
|
|
92
|
-
* @type {LifecycleManager}
|
|
93
|
-
* @private
|
|
94
|
-
*/
|
|
95
|
-
this._lifecycle = new LifecycleManager(this);
|
|
96
|
-
|
|
97
|
-
// Bind for use as `http.createServer(app.handler)`
|
|
98
|
-
this.handler = (req, res) => this.handle(req, res);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// -- Settings ------------------------------------
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Set an application setting, or retrieve one when called with a single argument.
|
|
105
|
-
*
|
|
106
|
-
* When called with two arguments, sets the value and returns `this` for chaining.
|
|
107
|
-
* When called with one argument, returns the stored value.
|
|
108
|
-
*
|
|
109
|
-
* Common settings: `'trust proxy'`, `'env'`, `'json spaces'`, `'etag'`,
|
|
110
|
-
* `'view engine'`, `'views'`, `'case sensitive routing'`.
|
|
111
|
-
*
|
|
112
|
-
* @param {string} key - Setting name.
|
|
113
|
-
* @param {*} [val] - Setting value.
|
|
114
|
-
* @returns {*|App} The stored value (getter) or `this` (setter).
|
|
115
|
-
*
|
|
116
|
-
* @example
|
|
117
|
-
* app.set('trust proxy', true);
|
|
118
|
-
* app.set('json spaces', 2);
|
|
119
|
-
* app.set('env'); // => undefined (or previously set value)
|
|
120
|
-
*/
|
|
121
|
-
set(key, val)
|
|
122
|
-
{
|
|
123
|
-
if (arguments.length === 1) return this._settings[key];
|
|
124
|
-
this._settings[key] = val;
|
|
125
|
-
return this;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Set a boolean setting to `true`.
|
|
130
|
-
*
|
|
131
|
-
* @param {string} key - Setting name.
|
|
132
|
-
* @returns {App} `this` for chaining.
|
|
133
|
-
*
|
|
134
|
-
* @example
|
|
135
|
-
* app.enable('trust proxy');
|
|
136
|
-
*/
|
|
137
|
-
enable(key) { this._settings[key] = true; return this; }
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Set a boolean setting to `false`.
|
|
141
|
-
*
|
|
142
|
-
* @param {string} key - Setting name.
|
|
143
|
-
* @returns {App} `this` for chaining.
|
|
144
|
-
*
|
|
145
|
-
* @example
|
|
146
|
-
* app.disable('etag');
|
|
147
|
-
*/
|
|
148
|
-
disable(key) { this._settings[key] = false; return this; }
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Check if a setting is truthy.
|
|
152
|
-
*
|
|
153
|
-
* @param {string} key - Setting name.
|
|
154
|
-
* @returns {boolean} `true` if the setting is truthy.
|
|
155
|
-
*/
|
|
156
|
-
enabled(key) { return !!this._settings[key]; }
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Check if a setting is falsy.
|
|
160
|
-
*
|
|
161
|
-
* @param {string} key - Setting name.
|
|
162
|
-
* @returns {boolean} `true` if the setting is falsy.
|
|
163
|
-
*/
|
|
164
|
-
disabled(key) { return !this._settings[key]; }
|
|
165
|
-
|
|
166
|
-
// -- Middleware -------------------------------------
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Register middleware or mount a sub-router.
|
|
170
|
-
* - `use(fn)` — global middleware applied to every request.
|
|
171
|
-
* - `use('/prefix', fn)` — path-scoped middleware (strips the prefix
|
|
172
|
-
* before calling `fn` so downstream sees relative paths).
|
|
173
|
-
* - `use('/prefix', router)` — mount a Router sub-app at the given prefix.
|
|
174
|
-
*
|
|
175
|
-
* @param {string|Function} pathOrFn - A path prefix string, or middleware function.
|
|
176
|
-
* @param {Function|Router} [fn] - Middleware function or Router when first arg is a path.
|
|
177
|
-
*/
|
|
178
|
-
use(pathOrFn, fn)
|
|
179
|
-
{
|
|
180
|
-
if (typeof pathOrFn === 'function')
|
|
181
|
-
{
|
|
182
|
-
this.middlewares.push(pathOrFn);
|
|
183
|
-
}
|
|
184
|
-
else if (typeof pathOrFn === 'string' && fn instanceof Router)
|
|
185
|
-
{
|
|
186
|
-
// Mount a sub-router
|
|
187
|
-
this.router.use(pathOrFn, fn);
|
|
188
|
-
}
|
|
189
|
-
else if (typeof pathOrFn === 'string' && typeof fn === 'function')
|
|
190
|
-
{
|
|
191
|
-
const prefix = pathOrFn.endsWith('/') ? pathOrFn.slice(0, -1) : pathOrFn;
|
|
192
|
-
this.middlewares.push((req, res, next) =>
|
|
193
|
-
{
|
|
194
|
-
const urlPath = req.url.split('?')[0];
|
|
195
|
-
if (urlPath === prefix || urlPath.startsWith(prefix + '/'))
|
|
196
|
-
{
|
|
197
|
-
// strip prefix from url so downstream sees relative paths
|
|
198
|
-
const origUrl = req.url;
|
|
199
|
-
req.url = req.url.slice(prefix.length) || '/';
|
|
200
|
-
fn(req, res, () => { req.url = origUrl; next(); });
|
|
201
|
-
}
|
|
202
|
-
else
|
|
203
|
-
{
|
|
204
|
-
next();
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Register a global error handler.
|
|
212
|
-
* The handler receives `(err, req, res, next)` and is invoked whenever
|
|
213
|
-
* a middleware or route handler throws or passes an error to `next(err)`.
|
|
214
|
-
*
|
|
215
|
-
* @param {Function} fn - Error-handling function `(err, req, res, next) => void`.
|
|
216
|
-
*/
|
|
217
|
-
onError(fn)
|
|
218
|
-
{
|
|
219
|
-
this._errorHandler = fn;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Register a parameter pre-processing handler.
|
|
224
|
-
* Runs before route handlers for any route containing a `:name` parameter.
|
|
225
|
-
*
|
|
226
|
-
* @param {string} name - Parameter name.
|
|
227
|
-
* @param {Function} fn - `(req, res, next, value) => void`.
|
|
228
|
-
* @returns {App} `this` for chaining.
|
|
229
|
-
*
|
|
230
|
-
* @example
|
|
231
|
-
* app.param('userId', async (req, res, next, id) => {
|
|
232
|
-
* req.locals.user = await db.users.findById(id);
|
|
233
|
-
* if (!req.locals.user) return res.status(404).json({ error: 'User not found' });
|
|
234
|
-
* next();
|
|
235
|
-
* });
|
|
236
|
-
*/
|
|
237
|
-
param(name, fn)
|
|
238
|
-
{
|
|
239
|
-
if (!this._paramHandlers[name]) this._paramHandlers[name] = [];
|
|
240
|
-
this._paramHandlers[name].push(fn);
|
|
241
|
-
this.router._paramHandlers = this._paramHandlers;
|
|
242
|
-
return this;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// -- Request Handling ------------------------------
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Core request handler. Wraps the raw Node `req`/`res` in
|
|
249
|
-
* `Request`/`Response` wrappers, runs the middleware
|
|
250
|
-
* pipeline, then falls through to the router.
|
|
251
|
-
*
|
|
252
|
-
* @param {import('http').IncomingMessage} req - Raw Node request.
|
|
253
|
-
* @param {import('http').ServerResponse} res - Raw Node response.
|
|
254
|
-
*/
|
|
255
|
-
handle(req, res)
|
|
256
|
-
{
|
|
257
|
-
// Skip gRPC requests — already handled via server.on('stream')
|
|
258
|
-
if (this._grpcRegistry && req.headers['content-type']?.startsWith('application/grpc'))
|
|
259
|
-
return;
|
|
260
|
-
|
|
261
|
-
// Reject new requests during shutdown drain
|
|
262
|
-
if (this._lifecycle.isDraining)
|
|
263
|
-
{
|
|
264
|
-
// HTTP/2 doesn't use Connection header
|
|
265
|
-
const headers = {
|
|
266
|
-
'Content-Type': 'application/json',
|
|
267
|
-
'Retry-After': '5',
|
|
268
|
-
};
|
|
269
|
-
if (req.httpVersionMajor < 2) headers['Connection'] = 'close';
|
|
270
|
-
res.writeHead(503, headers);
|
|
271
|
-
res.end(JSON.stringify({ error: 'Service Unavailable', message: 'Server is shutting down' }));
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Track active request for graceful shutdown
|
|
276
|
-
this._lifecycle.trackRequest(res);
|
|
277
|
-
|
|
278
|
-
const request = new Request(req);
|
|
279
|
-
const response = new Response(res);
|
|
280
|
-
|
|
281
|
-
// Inject app reference into request and response
|
|
282
|
-
request.app = this;
|
|
283
|
-
response.app = this;
|
|
284
|
-
response._req = request;
|
|
285
|
-
request._res = response;
|
|
286
|
-
|
|
287
|
-
// Preserve original URL before any middleware rewrites
|
|
288
|
-
request.originalUrl = request.url;
|
|
289
|
-
|
|
290
|
-
// Merge app.locals into request/response locals via prototype chain (avoids copy per request)
|
|
291
|
-
request.locals = Object.create(this.locals);
|
|
292
|
-
response.locals = Object.create(this.locals);
|
|
293
|
-
|
|
294
|
-
let idx = 0;
|
|
295
|
-
const run = (err) =>
|
|
296
|
-
{
|
|
297
|
-
if (err)
|
|
298
|
-
{
|
|
299
|
-
log.error('middleware error: %s', err.message || err);
|
|
300
|
-
if (this._errorHandler) return this._errorHandler(err, request, response, run);
|
|
301
|
-
response.status(500).json({ error: err.message || 'Internal Server Error' });
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (idx < this.middlewares.length)
|
|
305
|
-
{
|
|
306
|
-
const mw = this.middlewares[idx++];
|
|
307
|
-
try
|
|
308
|
-
{
|
|
309
|
-
const result = mw(request, response, run);
|
|
310
|
-
// Handle promise-returning middleware
|
|
311
|
-
if (result && typeof result.catch === 'function')
|
|
312
|
-
{
|
|
313
|
-
result.catch(run);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
catch (e)
|
|
317
|
-
{
|
|
318
|
-
run(e);
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
this.router.handle(request, response);
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
run();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// -- Server Lifecycle ------------------------------
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Start listening for HTTP, HTTPS, or HTTP/2 connections.
|
|
332
|
-
*
|
|
333
|
-
* @param {number} [port=3000] - Port number to bind.
|
|
334
|
-
* @param {object|Function} [opts] - Server options or callback.
|
|
335
|
-
* @param {boolean} [opts.http2] - Create an HTTP/2 server.
|
|
336
|
-
* @param {Buffer|string} [opts.key] - Private key for TLS (HTTPS or HTTP/2 with TLS).
|
|
337
|
-
* @param {Buffer|string} [opts.cert] - Certificate for TLS.
|
|
338
|
-
* @param {Buffer|string} [opts.pfx] - PFX/PKCS12 bundle (alternative to key+cert).
|
|
339
|
-
* @param {Buffer|string} [opts.ca] - CA certificate(s) for client verification.
|
|
340
|
-
* @param {boolean} [opts.allowHTTP1=true] - Allow HTTP/1.1 fallback on HTTP/2 secure servers (ALPN).
|
|
341
|
-
* @param {object} [opts.settings] - HTTP/2 settings (SETTINGS frame values).
|
|
342
|
-
* @param {Function} [cb] - Callback invoked once the server is listening.
|
|
343
|
-
* @returns {import('http').Server|import('https').Server|import('http2').Http2SecureServer|import('http2').Http2Server}
|
|
344
|
-
*
|
|
345
|
-
* @example | Plain HTTP
|
|
346
|
-
* app.listen(3000, () => console.log('HTTP on 3000'));
|
|
347
|
-
*
|
|
348
|
-
* @example | HTTPS
|
|
349
|
-
* app.listen(443, { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') });
|
|
350
|
-
*
|
|
351
|
-
* @example | HTTP/2 with TLS and ALPN
|
|
352
|
-
* app.listen(443, { http2: true, key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') });
|
|
353
|
-
*
|
|
354
|
-
* @example | HTTP/2 Cleartext
|
|
355
|
-
* app.listen(3000, { http2: true });
|
|
356
|
-
*/
|
|
357
|
-
listen(port = 3000, opts, cb)
|
|
358
|
-
{
|
|
359
|
-
// Normalise arguments — allow `listen(port, cb)` without opts
|
|
360
|
-
if (typeof opts === 'function') { cb = opts; opts = undefined; }
|
|
361
|
-
|
|
362
|
-
const isH2 = opts && opts.http2;
|
|
363
|
-
const hasTLS = opts && (opts.key || opts.pfx || opts.cert);
|
|
364
|
-
let server;
|
|
365
|
-
|
|
366
|
-
if (isH2 && hasTLS)
|
|
367
|
-
{
|
|
368
|
-
// HTTP/2 over TLS with ALPN negotiation (h2 + HTTP/1.1 fallback)
|
|
369
|
-
const h2Opts = {
|
|
370
|
-
...opts,
|
|
371
|
-
allowHTTP1: opts.allowHTTP1 !== false, // default true for graceful fallback
|
|
372
|
-
};
|
|
373
|
-
delete h2Opts.http2;
|
|
374
|
-
server = http2.createSecureServer(h2Opts, this.handler);
|
|
375
|
-
log.info('starting HTTP/2 (TLS) server on port %d', port);
|
|
376
|
-
}
|
|
377
|
-
else if (isH2)
|
|
378
|
-
{
|
|
379
|
-
// HTTP/2 cleartext (h2c) — no TLS, for internal services
|
|
380
|
-
const h2Opts = opts ? { ...opts } : {};
|
|
381
|
-
delete h2Opts.http2;
|
|
382
|
-
server = http2.createServer(h2Opts, this.handler);
|
|
383
|
-
log.info('starting HTTP/2 (h2c) server on port %d', port);
|
|
384
|
-
}
|
|
385
|
-
else if (hasTLS)
|
|
386
|
-
{
|
|
387
|
-
server = https.createServer(opts, this.handler);
|
|
388
|
-
log.info('starting HTTPS server on port %d', port);
|
|
389
|
-
}
|
|
390
|
-
else
|
|
391
|
-
{
|
|
392
|
-
server = http.createServer(this.handler);
|
|
393
|
-
log.info('starting HTTP server on port %d', port);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
this._server = server;
|
|
397
|
-
|
|
398
|
-
// Intercept HTTP/2 streams for gRPC before the normal request handler
|
|
399
|
-
if (isH2 && this._grpcRegistry)
|
|
400
|
-
{
|
|
401
|
-
server.on('stream', (stream, headers) =>
|
|
402
|
-
{
|
|
403
|
-
if (this._grpcRegistry.handleStream(stream, headers))
|
|
404
|
-
return; // handled as gRPC
|
|
405
|
-
// Otherwise fall through — the compat handler fires the normal request event
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Always attach WebSocket upgrade handling so ws() works
|
|
410
|
-
// regardless of registration order (before or after listen).
|
|
411
|
-
server.on('upgrade', (req, socket, head) =>
|
|
412
|
-
{
|
|
413
|
-
if (this._wsHandlers.size > 0)
|
|
414
|
-
handleUpgrade(req, socket, head, this._wsHandlers);
|
|
415
|
-
else
|
|
416
|
-
socket.destroy();
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
// Install graceful shutdown signal handlers
|
|
420
|
-
this._lifecycle.installSignalHandlers();
|
|
421
|
-
|
|
422
|
-
return server.listen(port, cb);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Gracefully close the server, stopping new connections.
|
|
427
|
-
*
|
|
428
|
-
* @param {Function} [cb] - Callback invoked once the server has closed.
|
|
429
|
-
*/
|
|
430
|
-
close(cb)
|
|
431
|
-
{
|
|
432
|
-
if (this._server) this._server.close(cb);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Perform a full graceful shutdown.
|
|
437
|
-
* Stops accepting new connections, drains in-flight requests, closes
|
|
438
|
-
* WebSocket, SSE, and gRPC connections, and shuts down registered databases.
|
|
439
|
-
*
|
|
440
|
-
* @param {object} [opts] - Shutdown options.
|
|
441
|
-
* @param {number} [opts.timeout] - Maximum ms to wait for in-flight requests (default 30000).
|
|
442
|
-
* @returns {Promise<void>} Resolves when shutdown is complete.
|
|
443
|
-
*
|
|
444
|
-
* @example
|
|
445
|
-
* await app.shutdown();
|
|
446
|
-
* // or with custom timeout
|
|
447
|
-
* await app.shutdown({ timeout: 5000 });
|
|
448
|
-
*/
|
|
449
|
-
shutdown(opts)
|
|
450
|
-
{
|
|
451
|
-
if (!this._shutdownPromise)
|
|
452
|
-
{
|
|
453
|
-
this._shutdownPromise = this._lifecycle.shutdown(opts);
|
|
454
|
-
}
|
|
455
|
-
return this._shutdownPromise;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// -- Lifecycle Events ------------------------------
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Register a lifecycle event listener.
|
|
462
|
-
*
|
|
463
|
-
* Supported events:
|
|
464
|
-
* - `'beforeShutdown'` — fires before shutdown begins (flush caches, finish writes)
|
|
465
|
-
* - `'shutdown'` — fires after shutdown is complete
|
|
466
|
-
*
|
|
467
|
-
* @param {'beforeShutdown'|'shutdown'} event - Lifecycle event name.
|
|
468
|
-
* @param {Function} fn - Async or sync callback.
|
|
469
|
-
* @returns {App} `this` for chaining.
|
|
470
|
-
*
|
|
471
|
-
* @example
|
|
472
|
-
* app.on('beforeShutdown', async () => {
|
|
473
|
-
* await saveMetrics();
|
|
474
|
-
* });
|
|
475
|
-
*
|
|
476
|
-
* app.on('shutdown', () => {
|
|
477
|
-
* console.log('goodbye');
|
|
478
|
-
* });
|
|
479
|
-
*/
|
|
480
|
-
on(event, fn)
|
|
481
|
-
{
|
|
482
|
-
this._lifecycle.on(event, fn);
|
|
483
|
-
return this;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Remove a lifecycle event listener.
|
|
488
|
-
*
|
|
489
|
-
* @param {'beforeShutdown'|'shutdown'} event - Event name.
|
|
490
|
-
* @param {Function} fn - Callback to remove.
|
|
491
|
-
* @returns {App} `this` for chaining.
|
|
492
|
-
*/
|
|
493
|
-
off(event, fn)
|
|
494
|
-
{
|
|
495
|
-
this._lifecycle.off(event, fn);
|
|
496
|
-
return this;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// -- Lifecycle Resource Registration ---------------
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Register a WebSocket pool for graceful shutdown. All connections
|
|
503
|
-
* in the pool are closed with code `1001` when the server shuts down.
|
|
504
|
-
*
|
|
505
|
-
* @param {import('./ws/room')} pool - WebSocket pool instance.
|
|
506
|
-
* @returns {App} `this` for chaining.
|
|
507
|
-
*
|
|
508
|
-
* @example
|
|
509
|
-
* const pool = new WebSocketPool();
|
|
510
|
-
* app.registerPool(pool);
|
|
511
|
-
*/
|
|
512
|
-
registerPool(pool)
|
|
513
|
-
{
|
|
514
|
-
this._lifecycle.registerPool(pool);
|
|
515
|
-
return this;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Unregister a WebSocket pool from lifecycle management.
|
|
520
|
-
*
|
|
521
|
-
* @param {import('./ws/room')} pool - WebSocket pool instance.
|
|
522
|
-
* @returns {App} `this` for chaining.
|
|
523
|
-
*/
|
|
524
|
-
unregisterPool(pool)
|
|
525
|
-
{
|
|
526
|
-
this._lifecycle.unregisterPool(pool);
|
|
527
|
-
return this;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Track an SSE stream for graceful shutdown. The stream
|
|
532
|
-
* is automatically untracked when it closes.
|
|
533
|
-
*
|
|
534
|
-
* @param {import('./sse/stream')} stream - SSE stream instance.
|
|
535
|
-
* @returns {App} `this` for chaining.
|
|
536
|
-
*/
|
|
537
|
-
trackSSE(stream)
|
|
538
|
-
{
|
|
539
|
-
this._lifecycle.trackSSE(stream);
|
|
540
|
-
return this;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Register an ORM Database instance for graceful shutdown.
|
|
545
|
-
* The database connection is closed during shutdown.
|
|
546
|
-
*
|
|
547
|
-
* @param {import('./orm')} db - Database instance.
|
|
548
|
-
* @returns {App} `this` for chaining.
|
|
549
|
-
*
|
|
550
|
-
* @example
|
|
551
|
-
* const db = new Database({ adapter: 'memory' });
|
|
552
|
-
* app.registerDatabase(db);
|
|
553
|
-
*/
|
|
554
|
-
registerDatabase(db)
|
|
555
|
-
{
|
|
556
|
-
this._lifecycle.registerDatabase(db);
|
|
557
|
-
return this;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Unregister an ORM Database instance from lifecycle management.
|
|
562
|
-
*
|
|
563
|
-
* @param {import('./orm')} db - Database instance.
|
|
564
|
-
* @returns {App} `this` for chaining.
|
|
565
|
-
*/
|
|
566
|
-
unregisterDatabase(db)
|
|
567
|
-
{
|
|
568
|
-
this._lifecycle.unregisterDatabase(db);
|
|
569
|
-
return this;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Configure the shutdown timeout—the maximum time (ms) to wait for
|
|
574
|
-
* in-flight requests to finish before forcefully terminating them.
|
|
575
|
-
*
|
|
576
|
-
* @param {number} ms - Timeout in milliseconds.
|
|
577
|
-
* @returns {App} `this` for chaining.
|
|
578
|
-
*
|
|
579
|
-
* @example
|
|
580
|
-
* app.shutdownTimeout(10000); // 10s
|
|
581
|
-
*/
|
|
582
|
-
shutdownTimeout(ms)
|
|
583
|
-
{
|
|
584
|
-
this._lifecycle._shutdownTimeout = ms;
|
|
585
|
-
return this;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Current lifecycle state.
|
|
590
|
-
*
|
|
591
|
-
* @type {'running'|'draining'|'closed'}
|
|
592
|
-
*/
|
|
593
|
-
get lifecycleState()
|
|
594
|
-
{
|
|
595
|
-
return this._lifecycle.state;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// -- Observability ---------------------------------
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Register a liveness health check endpoint.
|
|
602
|
-
* Returns `200` when healthy, `503` during shutdown.
|
|
603
|
-
*
|
|
604
|
-
* @param {string} [path='/healthz'] - Endpoint path.
|
|
605
|
-
* @param {Object<string, Function>} [checks] - Named health check functions.
|
|
606
|
-
* @returns {App} `this` for chaining.
|
|
607
|
-
*
|
|
608
|
-
* @example
|
|
609
|
-
* app.health(); // GET /healthz
|
|
610
|
-
* app.health('/alive'); // GET /alive
|
|
611
|
-
* app.health('/healthz', { memory: memoryCheck() });
|
|
612
|
-
*/
|
|
613
|
-
health(path, checks)
|
|
614
|
-
{
|
|
615
|
-
if (typeof path === 'object') { checks = path; path = '/healthz'; }
|
|
616
|
-
if (!path) path = '/healthz';
|
|
617
|
-
this.get(path, healthCheck({ checks: checks || {} }));
|
|
618
|
-
return this;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Register a readiness health check endpoint.
|
|
623
|
-
* Returns `200` when all checks pass, `503` otherwise.
|
|
624
|
-
*
|
|
625
|
-
* @param {string} [path='/readyz'] - Endpoint path.
|
|
626
|
-
* @param {Object<string, Function>} [checks] - Named readiness check functions.
|
|
627
|
-
* @returns {App} `this` for chaining.
|
|
628
|
-
*
|
|
629
|
-
* @example
|
|
630
|
-
* app.ready('/readyz', {
|
|
631
|
-
* database: () => db.ping(),
|
|
632
|
-
* cache: () => ({ healthy: redis.isConnected }),
|
|
633
|
-
* });
|
|
634
|
-
*/
|
|
635
|
-
ready(path, checks)
|
|
636
|
-
{
|
|
637
|
-
if (typeof path === 'object') { checks = path; path = '/readyz'; }
|
|
638
|
-
if (!path) path = '/readyz';
|
|
639
|
-
this.get(path, healthCheck({ checks: checks || {} }));
|
|
640
|
-
return this;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Register a custom health check.
|
|
645
|
-
*
|
|
646
|
-
* @param {string} name - Check name.
|
|
647
|
-
* @param {Function} fn - Check function `() => { healthy, details }`.
|
|
648
|
-
* @returns {App} `this` for chaining.
|
|
649
|
-
*
|
|
650
|
-
* @example
|
|
651
|
-
* app.addHealthCheck('redis', async () => {
|
|
652
|
-
* await redis.ping();
|
|
653
|
-
* return { healthy: true };
|
|
654
|
-
* });
|
|
655
|
-
*/
|
|
656
|
-
addHealthCheck(name, fn)
|
|
657
|
-
{
|
|
658
|
-
if (!this._healthChecks) this._healthChecks = {};
|
|
659
|
-
this._healthChecks[name] = fn;
|
|
660
|
-
return this;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Get the application metrics registry. Lazily created on first access.
|
|
665
|
-
* Returns a `MetricsRegistry` instance for registering custom metrics.
|
|
666
|
-
*
|
|
667
|
-
* @returns {import('./observe/metrics').MetricsRegistry} The metrics registry.
|
|
668
|
-
*
|
|
669
|
-
* @example
|
|
670
|
-
* const counter = app.metrics().counter({
|
|
671
|
-
* name: 'custom_events_total',
|
|
672
|
-
* help: 'Custom events',
|
|
673
|
-
* });
|
|
674
|
-
* counter.inc();
|
|
675
|
-
*/
|
|
676
|
-
metrics()
|
|
677
|
-
{
|
|
678
|
-
if (!this._metricsRegistry)
|
|
679
|
-
{
|
|
680
|
-
this._metricsRegistry = new MetricsRegistry();
|
|
681
|
-
}
|
|
682
|
-
return this._metricsRegistry;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Mount a Prometheus metrics endpoint.
|
|
687
|
-
* Pair with `metricsMiddleware()` to auto-instrument all HTTP traffic.
|
|
688
|
-
*
|
|
689
|
-
* @param {string} [path='/metrics'] - Endpoint path.
|
|
690
|
-
* @param {object} [opts] - Options.
|
|
691
|
-
* @param {import('./observe/metrics').MetricsRegistry} [opts.registry] - Registry. Uses `app.metrics()` if not provided.
|
|
692
|
-
* @returns {App} `this` for chaining.
|
|
693
|
-
*
|
|
694
|
-
* @example | Full Observability Setup
|
|
695
|
-
* app.use(metricsMiddleware({ registry: app.metrics() }));
|
|
696
|
-
* app.metricsEndpoint(); // GET /metrics
|
|
697
|
-
* app.health(); // GET /healthz
|
|
698
|
-
* app.ready(); // GET /readyz
|
|
699
|
-
*
|
|
700
|
-
* @example yaml | prometheus.yml
|
|
701
|
-
* scrape_configs:
|
|
702
|
-
* - job_name: 'my-app'
|
|
703
|
-
* scrape_interval: 5s
|
|
704
|
-
* static_configs:
|
|
705
|
-
* - targets: ['localhost:3000']
|
|
706
|
-
*
|
|
707
|
-
* @example shell | Start Prometheus
|
|
708
|
-
* $ docker run -d -p 9090:9090 -v ./prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus
|
|
709
|
-
*
|
|
710
|
-
* @example | Custom Path
|
|
711
|
-
*/
|
|
712
|
-
metricsEndpoint(path, opts = {})
|
|
713
|
-
{
|
|
714
|
-
if (typeof path === 'object') { opts = path; path = '/metrics'; }
|
|
715
|
-
if (!path) path = '/metrics';
|
|
716
|
-
const registry = opts.registry || this.metrics();
|
|
717
|
-
this.get(path, metricsEndpoint(registry));
|
|
718
|
-
return this;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// -- WebSocket Support -----------------------------
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Register a WebSocket upgrade handler for a path.
|
|
725
|
-
*
|
|
726
|
-
* The handler receives `(ws, req)` where `ws` is a `WebSocketConnection`
|
|
727
|
-
* instance with methods like `send()`, `sendJSON()`, `on()`, and `close()`.
|
|
728
|
-
*
|
|
729
|
-
* @param {string} path - URL path to listen for upgrade requests.
|
|
730
|
-
* @param {object|Function} [opts] - Options object, or the handler function directly.
|
|
731
|
-
* @param {number} [opts.maxPayload=1048576] - Maximum incoming frame size in bytes (default 1 MB).
|
|
732
|
-
* @param {number} [opts.pingInterval=30000] - Auto-ping interval in ms. Set `0` to disable.
|
|
733
|
-
* @param {Function} [opts.verifyClient] - `(req) => boolean` — return false to reject the upgrade.
|
|
734
|
-
* @param {Function} handler - `(ws, req) => void`.
|
|
735
|
-
*
|
|
736
|
-
* @example | Simple
|
|
737
|
-
* app.ws('/chat', (ws, req) => {
|
|
738
|
-
* ws.on('message', data => ws.send('echo: ' + data));
|
|
739
|
-
* });
|
|
740
|
-
*
|
|
741
|
-
* @example | With Options
|
|
742
|
-
* app.ws('/feed', { maxPayload: 64 * 1024, pingInterval: 15000 }, (ws, req) => {
|
|
743
|
-
* console.log('client', ws.id, 'from', ws.ip);
|
|
744
|
-
* ws.sendJSON({ hello: 'world' });
|
|
745
|
-
* });
|
|
746
|
-
*/
|
|
747
|
-
ws(path, opts, handler)
|
|
748
|
-
{
|
|
749
|
-
// Normalise arguments: ws(path, handler) or ws(path, opts, handler)
|
|
750
|
-
if (typeof opts === 'function') { handler = opts; opts = {}; }
|
|
751
|
-
if (!opts) opts = {};
|
|
752
|
-
|
|
753
|
-
this._wsHandlers.set(path, { handler, opts });
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// -- Route Introspection ---------------------------
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
* Return a flat list of all registered routes across the router tree,
|
|
760
|
-
* including mounted sub-routers. Useful for debugging, auto-generated
|
|
761
|
-
* docs, or CLI tooling.
|
|
762
|
-
*
|
|
763
|
-
* @returns {{ method: string, path: string }[]} Flat array of route entries including WebSocket handlers.
|
|
764
|
-
*
|
|
765
|
-
* @example
|
|
766
|
-
* app.routes().forEach(r => console.log(r.method, r.path));
|
|
767
|
-
* // GET /users
|
|
768
|
-
* // POST /users
|
|
769
|
-
* // GET /api/v1/items/:id
|
|
770
|
-
*/
|
|
771
|
-
routes()
|
|
772
|
-
{
|
|
773
|
-
const list = this.router.inspect();
|
|
774
|
-
|
|
775
|
-
/* Include WebSocket upgrade handlers */
|
|
776
|
-
for (const [wsPath, { opts }] of this._wsHandlers)
|
|
777
|
-
{
|
|
778
|
-
const entry = { method: 'WS', path: wsPath };
|
|
779
|
-
if (opts && opts.maxPayload !== undefined) entry.maxPayload = opts.maxPayload;
|
|
780
|
-
if (opts && opts.pingInterval !== undefined) entry.pingInterval = opts.pingInterval;
|
|
781
|
-
list.push(entry);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
/* Include gRPC routes */
|
|
785
|
-
if (this._grpcRegistry)
|
|
786
|
-
list.push(...this._grpcRegistry.routes());
|
|
787
|
-
|
|
788
|
-
return list;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// -- Route Registration ----------------------------
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Extract an options object from the head of the handlers array when
|
|
795
|
-
* the first argument is a plain object (not a function).
|
|
796
|
-
* @private
|
|
797
|
-
*/
|
|
798
|
-
_extractOpts(fns)
|
|
799
|
-
{
|
|
800
|
-
let opts = {};
|
|
801
|
-
if (fns.length > 0 && typeof fns[0] === 'object' && typeof fns[0] !== 'function')
|
|
802
|
-
{
|
|
803
|
-
opts = fns.shift();
|
|
804
|
-
}
|
|
805
|
-
return opts;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Register one or more handler functions for a specific HTTP method and path.
|
|
810
|
-
*
|
|
811
|
-
* @param {string} method - HTTP method (GET, POST, etc.) or 'ALL'.
|
|
812
|
-
* @param {string} path - Route pattern (e.g. '/users/:id').
|
|
813
|
-
* @param {...Function|object} fns - Optional options object `{ secure }` followed by handler functions.
|
|
814
|
-
*/
|
|
815
|
-
route(method, path, ...fns) { const o = this._extractOpts(fns); this.router.add(method, path, fns, o); }
|
|
816
|
-
|
|
817
|
-
/**
|
|
818
|
-
* @see App#route — shortcut for GET requests.
|
|
819
|
-
* @param {string} path - Route pattern.
|
|
820
|
-
* @param {...Function} fns - Handler functions.
|
|
821
|
-
* @returns {App} `this` for chaining.
|
|
822
|
-
*/
|
|
823
|
-
get(path, ...fns) { if (arguments.length === 1 && typeof path === 'string' && fns.length === 0) return this.set(path); this.route('GET', path, ...fns); return this; }
|
|
824
|
-
/**
|
|
825
|
-
* @see App#route — shortcut for POST requests.
|
|
826
|
-
* @param {string} path - Route pattern.
|
|
827
|
-
* @param {...Function} fns - Handler functions.
|
|
828
|
-
* @returns {App} `this` for chaining.
|
|
829
|
-
*/
|
|
830
|
-
post(path, ...fns) { this.route('POST', path, ...fns); return this; }
|
|
831
|
-
/**
|
|
832
|
-
* @see App#route — shortcut for PUT requests.
|
|
833
|
-
* @param {string} path - Route pattern.
|
|
834
|
-
* @param {...Function} fns - Handler functions.
|
|
835
|
-
* @returns {App} `this` for chaining.
|
|
836
|
-
*/
|
|
837
|
-
put(path, ...fns) { this.route('PUT', path, ...fns); return this; }
|
|
838
|
-
/**
|
|
839
|
-
* @see App#route — shortcut for DELETE requests.
|
|
840
|
-
* @param {string} path - Route pattern.
|
|
841
|
-
* @param {...Function} fns - Handler functions.
|
|
842
|
-
* @returns {App} `this` for chaining.
|
|
843
|
-
*/
|
|
844
|
-
delete(path, ...fns) { this.route('DELETE', path, ...fns); return this; }
|
|
845
|
-
/**
|
|
846
|
-
* @see App#route — shortcut for PATCH requests.
|
|
847
|
-
* @param {string} path - Route pattern.
|
|
848
|
-
* @param {...Function} fns - Handler functions.
|
|
849
|
-
* @returns {App} `this` for chaining.
|
|
850
|
-
*/
|
|
851
|
-
patch(path, ...fns) { this.route('PATCH', path, ...fns); return this; }
|
|
852
|
-
/**
|
|
853
|
-
* @see App#route — shortcut for OPTIONS requests.
|
|
854
|
-
* @param {string} path - Route pattern.
|
|
855
|
-
* @param {...Function} fns - Handler functions.
|
|
856
|
-
* @returns {App} `this` for chaining.
|
|
857
|
-
*/
|
|
858
|
-
options(path, ...fns) { this.route('OPTIONS', path, ...fns); return this; }
|
|
859
|
-
/**
|
|
860
|
-
* @see App#route — shortcut for HEAD requests.
|
|
861
|
-
* @param {string} path - Route pattern.
|
|
862
|
-
* @param {...Function} fns - Handler functions.
|
|
863
|
-
* @returns {App} `this` for chaining.
|
|
864
|
-
*/
|
|
865
|
-
head(path, ...fns) { this.route('HEAD', path, ...fns); return this; }
|
|
866
|
-
/**
|
|
867
|
-
* @see App#route — matches every HTTP method.
|
|
868
|
-
* @param {string} path - Route pattern.
|
|
869
|
-
* @param {...Function} fns - Handler functions.
|
|
870
|
-
* @returns {App} `this` for chaining.
|
|
871
|
-
*/
|
|
872
|
-
all(path, ...fns) { this.route('ALL', path, ...fns); return this; }
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* Chainable route builder — register multiple methods on the same path.
|
|
876
|
-
*
|
|
877
|
-
* @param {string} path - Route pattern.
|
|
878
|
-
* @returns {object} Chain object with HTTP verb methods.
|
|
879
|
-
*
|
|
880
|
-
* @example
|
|
881
|
-
* app.chain('/users')
|
|
882
|
-
* .get((req, res) => res.json(users))
|
|
883
|
-
* .post((req, res) => res.json({ created: true }));
|
|
884
|
-
*/
|
|
885
|
-
chain(path) { return this.router.route(path); }
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* Define a route group with shared middleware prefix.
|
|
889
|
-
* All routes registered inside the callback share the given path prefix
|
|
890
|
-
* and middleware stack.
|
|
891
|
-
*
|
|
892
|
-
* @param {string} prefix - URL prefix for the group.
|
|
893
|
-
* @param {...Function} middleware - Shared middleware, last argument is the callback.
|
|
894
|
-
* @returns {App} `this` for chaining.
|
|
895
|
-
*
|
|
896
|
-
* @example
|
|
897
|
-
* app.group('/api/v1', authMiddleware, (router) => {
|
|
898
|
-
* router.get('/users', listUsers);
|
|
899
|
-
* router.post('/users', createUser);
|
|
900
|
-
* router.get('/users/:id', getUser);
|
|
901
|
-
* });
|
|
902
|
-
*/
|
|
903
|
-
group(prefix, ...args)
|
|
904
|
-
{
|
|
905
|
-
const cb = args.pop();
|
|
906
|
-
const middlewareStack = args;
|
|
907
|
-
const router = new Router();
|
|
908
|
-
cb(router);
|
|
909
|
-
if (middlewareStack.length > 0)
|
|
910
|
-
{
|
|
911
|
-
this.middlewares.push((req, res, next) =>
|
|
912
|
-
{
|
|
913
|
-
const urlPath = req.url.split('?')[0];
|
|
914
|
-
const cleanPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
915
|
-
if (urlPath === cleanPrefix || urlPath.startsWith(cleanPrefix + '/'))
|
|
916
|
-
{
|
|
917
|
-
let i = 0;
|
|
918
|
-
const runMw = () =>
|
|
919
|
-
{
|
|
920
|
-
if (i < middlewareStack.length)
|
|
921
|
-
{
|
|
922
|
-
const mw = middlewareStack[i++];
|
|
923
|
-
try
|
|
924
|
-
{
|
|
925
|
-
const result = mw(req, res, runMw);
|
|
926
|
-
if (result && typeof result.catch === 'function') result.catch(next);
|
|
927
|
-
}
|
|
928
|
-
catch (e) { next(e); }
|
|
929
|
-
}
|
|
930
|
-
else { next(); }
|
|
931
|
-
};
|
|
932
|
-
runMw();
|
|
933
|
-
}
|
|
934
|
-
else { next(); }
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
this.router.use(prefix, router);
|
|
938
|
-
return this;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// -- gRPC Support ----------------------------------
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* Register a gRPC service handler.
|
|
945
|
-
*
|
|
946
|
-
* Accepts a parsed proto schema, a service name, and a map of method handlers.
|
|
947
|
-
* When the server is HTTP/2, incoming `application/grpc` requests are routed
|
|
948
|
-
* to the matching service method automatically.
|
|
949
|
-
*
|
|
950
|
-
* @param {object} schema - Parsed proto schema from `parseProto()` or `parseProtoFile()`.
|
|
951
|
-
* @param {string} serviceName - Service name as defined in the proto file.
|
|
952
|
-
* @param {Object<string, Function>} handlers - Map of method names → handler functions.
|
|
953
|
-
* @param {object} [opts] - Service options.
|
|
954
|
-
* @param {Function[]} [opts.interceptors] - Per-service interceptors `(call, next) => void`.
|
|
955
|
-
* @param {number} [opts.maxMessageSize] - Max incoming message size in bytes.
|
|
956
|
-
* @param {boolean} [opts.compress=false] - Whether to compress outgoing messages.
|
|
957
|
-
* @returns {App} `this` for chaining.
|
|
958
|
-
*
|
|
959
|
-
* @example | Unary RPC
|
|
960
|
-
* const schema = parseProto(fs.readFileSync('hello.proto', 'utf8'));
|
|
961
|
-
* app.grpc(schema, 'Greeter', {
|
|
962
|
-
* SayHello(call) { return { message: 'Hello ' + call.request.name }; },
|
|
963
|
-
* });
|
|
964
|
-
* app.listen(50051, { http2: true });
|
|
965
|
-
*
|
|
966
|
-
* @example | Server streaming with interceptors
|
|
967
|
-
* app.grpc(schema, 'DataService', {
|
|
968
|
-
* StreamData(call) {
|
|
969
|
-
* for (let i = 0; i < 100; i++) call.write({ seq: i });
|
|
970
|
-
* call.end();
|
|
971
|
-
* },
|
|
972
|
-
* }, { interceptors: [authInterceptor] });
|
|
973
|
-
*/
|
|
974
|
-
grpc(schema, serviceName, handlers, opts)
|
|
975
|
-
{
|
|
976
|
-
if (!this._grpcRegistry)
|
|
977
|
-
{
|
|
978
|
-
this._grpcRegistry = new GrpcServiceRegistry();
|
|
979
|
-
this._lifecycle.registerGrpc(this._grpcRegistry);
|
|
980
|
-
}
|
|
981
|
-
this._grpcRegistry.addService(schema, serviceName, handlers, opts);
|
|
982
|
-
return this;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
/**
|
|
986
|
-
* Add a global gRPC interceptor that runs before every gRPC call.
|
|
987
|
-
*
|
|
988
|
-
* @param {Function} fn - Interceptor `(call, next) => void`.
|
|
989
|
-
* @returns {App} `this` for chaining.
|
|
990
|
-
*
|
|
991
|
-
* @example
|
|
992
|
-
* app.grpcInterceptor(async (call, next) => {
|
|
993
|
-
* console.log('gRPC call:', call.method.name);
|
|
994
|
-
* await next();
|
|
995
|
-
* });
|
|
996
|
-
*/
|
|
997
|
-
grpcInterceptor(fn)
|
|
998
|
-
{
|
|
999
|
-
if (!this._grpcRegistry)
|
|
1000
|
-
{
|
|
1001
|
-
this._grpcRegistry = new GrpcServiceRegistry();
|
|
1002
|
-
this._lifecycle.registerGrpc(this._grpcRegistry);
|
|
1003
|
-
}
|
|
1004
|
-
this._grpcRegistry.addInterceptor(fn);
|
|
1005
|
-
return this;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* Enable the gRPC Health Checking Protocol (`grpc.health.v1.Health`).
|
|
1010
|
-
* Registers the `Check` (unary) and `Watch` (server-stream) RPCs
|
|
1011
|
-
* required by Kubernetes, Envoy, and standard gRPC health probes.
|
|
1012
|
-
*
|
|
1013
|
-
* Call `app.setServiceStatus(name, status)` to update individual services.
|
|
1014
|
-
* Overall server health defaults to `SERVING`.
|
|
1015
|
-
*
|
|
1016
|
-
* @returns {App} `this` for chaining.
|
|
1017
|
-
*
|
|
1018
|
-
* @example
|
|
1019
|
-
* app.grpcHealth();
|
|
1020
|
-
* app.listen(50051, { http2: true });
|
|
1021
|
-
*/
|
|
1022
|
-
grpcHealth()
|
|
1023
|
-
{
|
|
1024
|
-
if (this._healthService) return this;
|
|
1025
|
-
|
|
1026
|
-
const { HealthService } = require('./grpc/health');
|
|
1027
|
-
this._healthService = new HealthService();
|
|
1028
|
-
|
|
1029
|
-
const schema = this._healthService.getSchema();
|
|
1030
|
-
const handlers = this._healthService.getHandlers();
|
|
1031
|
-
this.grpc(schema, 'Health', handlers);
|
|
1032
|
-
|
|
1033
|
-
// Set all services to NOT_SERVING during graceful shutdown
|
|
1034
|
-
this._lifecycle.on('beforeShutdown', () =>
|
|
1035
|
-
{
|
|
1036
|
-
this._healthService.setAllNotServing();
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
log.info('gRPC health check service enabled');
|
|
1040
|
-
return this;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
* Set the health status of a gRPC service.
|
|
1045
|
-
* Requires `grpcHealth()` to have been called first.
|
|
1046
|
-
*
|
|
1047
|
-
* @param {string} serviceName - Service name (empty string for overall server health).
|
|
1048
|
-
* @param {string|number} status - `'SERVING'`, `'NOT_SERVING'`, `'UNKNOWN'`, or numeric value.
|
|
1049
|
-
* @returns {App} `this` for chaining.
|
|
1050
|
-
*
|
|
1051
|
-
* @example
|
|
1052
|
-
* app.setServiceStatus('myapp.UserService', 'SERVING');
|
|
1053
|
-
* app.setServiceStatus('myapp.OrderService', 'NOT_SERVING');
|
|
1054
|
-
*/
|
|
1055
|
-
setServiceStatus(serviceName, status)
|
|
1056
|
-
{
|
|
1057
|
-
if (!this._healthService)
|
|
1058
|
-
throw new Error('Call app.grpcHealth() before setServiceStatus()');
|
|
1059
|
-
this._healthService.setStatus(serviceName, status);
|
|
1060
|
-
return this;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
/**
|
|
1064
|
-
* Enable gRPC Server Reflection (`grpc.reflection.v1.ServerReflection`).
|
|
1065
|
-
* Allows `grpcurl`, `grpcui`, Postman, and other tools to introspect services.
|
|
1066
|
-
* Disabled in production by default unless `opts.production` is `true`.
|
|
1067
|
-
*
|
|
1068
|
-
* Must be called AFTER all `app.grpc()` registrations so that all schemas
|
|
1069
|
-
* are available for reflection.
|
|
1070
|
-
*
|
|
1071
|
-
* @param {object} [opts] - Options.
|
|
1072
|
-
* @param {boolean} [opts.production=false] - Enable in production environments.
|
|
1073
|
-
* @returns {App} `this` for chaining.
|
|
1074
|
-
*
|
|
1075
|
-
* @example
|
|
1076
|
-
* app.grpc(schema, 'Greeter', handlers);
|
|
1077
|
-
* app.grpcReflection();
|
|
1078
|
-
*/
|
|
1079
|
-
grpcReflection(opts = {})
|
|
1080
|
-
{
|
|
1081
|
-
if (this._reflectionService) return this;
|
|
1082
|
-
|
|
1083
|
-
if (!opts.production && process.env.NODE_ENV === 'production')
|
|
1084
|
-
{
|
|
1085
|
-
log.warn('gRPC reflection disabled in production (set { production: true } to override)');
|
|
1086
|
-
return this;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const { ReflectionService } = require('./grpc/reflection');
|
|
1090
|
-
this._reflectionService = new ReflectionService(opts);
|
|
1091
|
-
|
|
1092
|
-
// Register all already-registered schemas
|
|
1093
|
-
if (this._grpcRegistry)
|
|
1094
|
-
{
|
|
1095
|
-
for (const [path, route] of this._grpcRegistry._routes)
|
|
1096
|
-
{
|
|
1097
|
-
if (route.schema && !route.schema._reflectionRegistered)
|
|
1098
|
-
{
|
|
1099
|
-
this._reflectionService.addSchema(route.schema);
|
|
1100
|
-
route.schema._reflectionRegistered = true;
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
const schema = this._reflectionService.getSchema();
|
|
1106
|
-
const handlers = this._reflectionService.getHandlers();
|
|
1107
|
-
this.grpc(schema, 'ServerReflection', handlers);
|
|
1108
|
-
|
|
1109
|
-
log.info('gRPC server reflection enabled');
|
|
1110
|
-
return this;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
// -- Authentication & Sessions ---------------------
|
|
1114
|
-
|
|
1115
|
-
/**
|
|
1116
|
-
* Mount JWT authentication middleware.
|
|
1117
|
-
* Shorthand for `app.use(jwt(opts))`.
|
|
1118
|
-
*
|
|
1119
|
-
* @param {object} opts - JWT options (see `jwt()` for full documentation).
|
|
1120
|
-
* @returns {App} `this` for chaining.
|
|
1121
|
-
*
|
|
1122
|
-
* @example
|
|
1123
|
-
* app.jwt({ secret: process.env.JWT_SECRET });
|
|
1124
|
-
* app.jwt({ jwksUri: 'https://auth.example.com/.well-known/jwks.json' });
|
|
1125
|
-
*/
|
|
1126
|
-
jwtAuth(opts)
|
|
1127
|
-
{
|
|
1128
|
-
const { jwt } = require('./auth/jwt');
|
|
1129
|
-
this.use(jwt(opts));
|
|
1130
|
-
return this;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
/**
|
|
1134
|
-
* Mount session middleware.
|
|
1135
|
-
* Shorthand for `app.use(session(opts))`.
|
|
1136
|
-
*
|
|
1137
|
-
* @param {object} opts - Session options (see `session()` for full documentation).
|
|
1138
|
-
* @returns {App} `this` for chaining.
|
|
1139
|
-
*
|
|
1140
|
-
* @example
|
|
1141
|
-
* app.sessions({ secret: process.env.SESSION_SECRET });
|
|
1142
|
-
*/
|
|
1143
|
-
sessions(opts)
|
|
1144
|
-
{
|
|
1145
|
-
const { session } = require('./auth/session');
|
|
1146
|
-
this.use(session(opts));
|
|
1147
|
-
return this;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
/**
|
|
1151
|
-
* Create an OAuth2 client bound to this app.
|
|
1152
|
-
* Returns the client — does NOT mount any middleware automatically.
|
|
1153
|
-
*
|
|
1154
|
-
* @param {object} opts - OAuth options (see `oauth()` for full documentation).
|
|
1155
|
-
* @returns {{ authorize: Function, callback: Function, refresh: Function, userInfo: Function }}
|
|
1156
|
-
*
|
|
1157
|
-
* @example
|
|
1158
|
-
* const github = app.oauth({
|
|
1159
|
-
* provider: 'github',
|
|
1160
|
-
* clientId: process.env.GITHUB_CLIENT_ID,
|
|
1161
|
-
* clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
1162
|
-
* callbackUrl: '/auth/github/callback',
|
|
1163
|
-
* });
|
|
1164
|
-
*/
|
|
1165
|
-
oauth(opts)
|
|
1166
|
-
{
|
|
1167
|
-
const { oauth } = require('./auth/oauth');
|
|
1168
|
-
return oauth(opts);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
module.exports = App;
|
|
1
|
+
/**
|
|
2
|
+
* @module app
|
|
3
|
+
* @description HTTP application with middleware pipeline,
|
|
4
|
+
* method-based routing, HTTP/2, HTTPS, and HTTP/1.1 support,
|
|
5
|
+
* built-in WebSocket upgrade handling, gRPC service hosting,
|
|
6
|
+
* trust proxy resolution, and route introspection.
|
|
7
|
+
* Created via `createApp()` in the public API.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const { createApp } = require('@zero-server/sdk');
|
|
11
|
+
* const app = createApp();
|
|
12
|
+
*
|
|
13
|
+
* app.use(logger());
|
|
14
|
+
* app.get('/hello', (req, res) => res.json({ hello: 'world' }));
|
|
15
|
+
* app.listen(3000);
|
|
16
|
+
*
|
|
17
|
+
* @example | HTTP/2 with TLS
|
|
18
|
+
* const fs = require('fs');
|
|
19
|
+
* app.listen(443, {
|
|
20
|
+
* http2: true,
|
|
21
|
+
* key: fs.readFileSync('key.pem'),
|
|
22
|
+
* cert: fs.readFileSync('cert.pem'),
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* @example | HTTP/2 Cleartext
|
|
26
|
+
* app.listen(3000, { http2: true });
|
|
27
|
+
*/
|
|
28
|
+
const http = require('http');
|
|
29
|
+
const https = require('https');
|
|
30
|
+
const http2 = require('http2');
|
|
31
|
+
const Router = require('./router');
|
|
32
|
+
const { Request, Response } = require('./http');
|
|
33
|
+
const { handleUpgrade } = require('./ws');
|
|
34
|
+
const { GrpcServiceRegistry } = require('./grpc/server');
|
|
35
|
+
const { LifecycleManager, LIFECYCLE_STATE } = require('./lifecycle');
|
|
36
|
+
const { healthCheck, createHealthHandlers, memoryCheck, eventLoopCheck, diskSpaceCheck } = require('./observe/health');
|
|
37
|
+
const { MetricsRegistry, createDefaultMetrics, metricsMiddleware, metricsEndpoint } = require('./observe/metrics');
|
|
38
|
+
const { Tracer, tracingMiddleware, instrumentFetch } = require('./observe/tracing');
|
|
39
|
+
const log = require('./debug')('zero:app');
|
|
40
|
+
|
|
41
|
+
class App
|
|
42
|
+
{
|
|
43
|
+
/**
|
|
44
|
+
* Create a new App instance.
|
|
45
|
+
* Initialises an empty middleware stack, a `Router`, and binds
|
|
46
|
+
* `this.handler` for direct use with `http.createServer()`.
|
|
47
|
+
*
|
|
48
|
+
* @constructor
|
|
49
|
+
*/
|
|
50
|
+
constructor()
|
|
51
|
+
{
|
|
52
|
+
/** @type {Router} */
|
|
53
|
+
this.router = new Router();
|
|
54
|
+
/** @type {Function[]} */
|
|
55
|
+
this.middlewares = [];
|
|
56
|
+
/** @type {Function|null} */
|
|
57
|
+
this._errorHandler = null;
|
|
58
|
+
/** @type {Map<string, { handler: Function, opts: object }>} WebSocket upgrade handlers keyed by path */
|
|
59
|
+
this._wsHandlers = new Map();
|
|
60
|
+
/** @type {GrpcServiceRegistry|null} gRPC service registry (lazily created). */
|
|
61
|
+
this._grpcRegistry = null;
|
|
62
|
+
/** @type {import('./grpc/health').HealthService|null} gRPC health service. */
|
|
63
|
+
this._healthService = null;
|
|
64
|
+
/** @type {import('./grpc/reflection').ReflectionService|null} gRPC reflection service. */
|
|
65
|
+
this._reflectionService = null;
|
|
66
|
+
/** @type {import('http').Server|import('https').Server|null} */
|
|
67
|
+
this._server = null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Application-level settings store.
|
|
71
|
+
* @type {Object<string, *>}
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
this._settings = {};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Application-level locals — persistent across the app lifecycle.
|
|
78
|
+
* Merged into every `req.locals` and `res.locals` on every request.
|
|
79
|
+
* @type {Object<string, *>}
|
|
80
|
+
*/
|
|
81
|
+
this.locals = {};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parameter pre-processing handlers.
|
|
85
|
+
* @type {Object<string, Function[]>}
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
this._paramHandlers = {};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Lifecycle manager for graceful shutdown and connection tracking.
|
|
92
|
+
* @type {LifecycleManager}
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
this._lifecycle = new LifecycleManager(this);
|
|
96
|
+
|
|
97
|
+
// Bind for use as `http.createServer(app.handler)`
|
|
98
|
+
this.handler = (req, res) => this.handle(req, res);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// -- Settings ------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Set an application setting, or retrieve one when called with a single argument.
|
|
105
|
+
*
|
|
106
|
+
* When called with two arguments, sets the value and returns `this` for chaining.
|
|
107
|
+
* When called with one argument, returns the stored value.
|
|
108
|
+
*
|
|
109
|
+
* Common settings: `'trust proxy'`, `'env'`, `'json spaces'`, `'etag'`,
|
|
110
|
+
* `'view engine'`, `'views'`, `'case sensitive routing'`.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} key - Setting name.
|
|
113
|
+
* @param {*} [val] - Setting value.
|
|
114
|
+
* @returns {*|App} The stored value (getter) or `this` (setter).
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* app.set('trust proxy', true);
|
|
118
|
+
* app.set('json spaces', 2);
|
|
119
|
+
* app.set('env'); // => undefined (or previously set value)
|
|
120
|
+
*/
|
|
121
|
+
set(key, val)
|
|
122
|
+
{
|
|
123
|
+
if (arguments.length === 1) return this._settings[key];
|
|
124
|
+
this._settings[key] = val;
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Set a boolean setting to `true`.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} key - Setting name.
|
|
132
|
+
* @returns {App} `this` for chaining.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* app.enable('trust proxy');
|
|
136
|
+
*/
|
|
137
|
+
enable(key) { this._settings[key] = true; return this; }
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set a boolean setting to `false`.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} key - Setting name.
|
|
143
|
+
* @returns {App} `this` for chaining.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* app.disable('etag');
|
|
147
|
+
*/
|
|
148
|
+
disable(key) { this._settings[key] = false; return this; }
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if a setting is truthy.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} key - Setting name.
|
|
154
|
+
* @returns {boolean} `true` if the setting is truthy.
|
|
155
|
+
*/
|
|
156
|
+
enabled(key) { return !!this._settings[key]; }
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if a setting is falsy.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} key - Setting name.
|
|
162
|
+
* @returns {boolean} `true` if the setting is falsy.
|
|
163
|
+
*/
|
|
164
|
+
disabled(key) { return !this._settings[key]; }
|
|
165
|
+
|
|
166
|
+
// -- Middleware -------------------------------------
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Register middleware or mount a sub-router.
|
|
170
|
+
* - `use(fn)` — global middleware applied to every request.
|
|
171
|
+
* - `use('/prefix', fn)` — path-scoped middleware (strips the prefix
|
|
172
|
+
* before calling `fn` so downstream sees relative paths).
|
|
173
|
+
* - `use('/prefix', router)` — mount a Router sub-app at the given prefix.
|
|
174
|
+
*
|
|
175
|
+
* @param {string|Function} pathOrFn - A path prefix string, or middleware function.
|
|
176
|
+
* @param {Function|Router} [fn] - Middleware function or Router when first arg is a path.
|
|
177
|
+
*/
|
|
178
|
+
use(pathOrFn, fn)
|
|
179
|
+
{
|
|
180
|
+
if (typeof pathOrFn === 'function')
|
|
181
|
+
{
|
|
182
|
+
this.middlewares.push(pathOrFn);
|
|
183
|
+
}
|
|
184
|
+
else if (typeof pathOrFn === 'string' && fn instanceof Router)
|
|
185
|
+
{
|
|
186
|
+
// Mount a sub-router
|
|
187
|
+
this.router.use(pathOrFn, fn);
|
|
188
|
+
}
|
|
189
|
+
else if (typeof pathOrFn === 'string' && typeof fn === 'function')
|
|
190
|
+
{
|
|
191
|
+
const prefix = pathOrFn.endsWith('/') ? pathOrFn.slice(0, -1) : pathOrFn;
|
|
192
|
+
this.middlewares.push((req, res, next) =>
|
|
193
|
+
{
|
|
194
|
+
const urlPath = req.url.split('?')[0];
|
|
195
|
+
if (urlPath === prefix || urlPath.startsWith(prefix + '/'))
|
|
196
|
+
{
|
|
197
|
+
// strip prefix from url so downstream sees relative paths
|
|
198
|
+
const origUrl = req.url;
|
|
199
|
+
req.url = req.url.slice(prefix.length) || '/';
|
|
200
|
+
fn(req, res, () => { req.url = origUrl; next(); });
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
{
|
|
204
|
+
next();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Register a global error handler.
|
|
212
|
+
* The handler receives `(err, req, res, next)` and is invoked whenever
|
|
213
|
+
* a middleware or route handler throws or passes an error to `next(err)`.
|
|
214
|
+
*
|
|
215
|
+
* @param {Function} fn - Error-handling function `(err, req, res, next) => void`.
|
|
216
|
+
*/
|
|
217
|
+
onError(fn)
|
|
218
|
+
{
|
|
219
|
+
this._errorHandler = fn;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Register a parameter pre-processing handler.
|
|
224
|
+
* Runs before route handlers for any route containing a `:name` parameter.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} name - Parameter name.
|
|
227
|
+
* @param {Function} fn - `(req, res, next, value) => void`.
|
|
228
|
+
* @returns {App} `this` for chaining.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* app.param('userId', async (req, res, next, id) => {
|
|
232
|
+
* req.locals.user = await db.users.findById(id);
|
|
233
|
+
* if (!req.locals.user) return res.status(404).json({ error: 'User not found' });
|
|
234
|
+
* next();
|
|
235
|
+
* });
|
|
236
|
+
*/
|
|
237
|
+
param(name, fn)
|
|
238
|
+
{
|
|
239
|
+
if (!this._paramHandlers[name]) this._paramHandlers[name] = [];
|
|
240
|
+
this._paramHandlers[name].push(fn);
|
|
241
|
+
this.router._paramHandlers = this._paramHandlers;
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// -- Request Handling ------------------------------
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Core request handler. Wraps the raw Node `req`/`res` in
|
|
249
|
+
* `Request`/`Response` wrappers, runs the middleware
|
|
250
|
+
* pipeline, then falls through to the router.
|
|
251
|
+
*
|
|
252
|
+
* @param {import('http').IncomingMessage} req - Raw Node request.
|
|
253
|
+
* @param {import('http').ServerResponse} res - Raw Node response.
|
|
254
|
+
*/
|
|
255
|
+
handle(req, res)
|
|
256
|
+
{
|
|
257
|
+
// Skip gRPC requests — already handled via server.on('stream')
|
|
258
|
+
if (this._grpcRegistry && req.headers['content-type']?.startsWith('application/grpc'))
|
|
259
|
+
return;
|
|
260
|
+
|
|
261
|
+
// Reject new requests during shutdown drain
|
|
262
|
+
if (this._lifecycle.isDraining)
|
|
263
|
+
{
|
|
264
|
+
// HTTP/2 doesn't use Connection header
|
|
265
|
+
const headers = {
|
|
266
|
+
'Content-Type': 'application/json',
|
|
267
|
+
'Retry-After': '5',
|
|
268
|
+
};
|
|
269
|
+
if (req.httpVersionMajor < 2) headers['Connection'] = 'close';
|
|
270
|
+
res.writeHead(503, headers);
|
|
271
|
+
res.end(JSON.stringify({ error: 'Service Unavailable', message: 'Server is shutting down' }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Track active request for graceful shutdown
|
|
276
|
+
this._lifecycle.trackRequest(res);
|
|
277
|
+
|
|
278
|
+
const request = new Request(req);
|
|
279
|
+
const response = new Response(res);
|
|
280
|
+
|
|
281
|
+
// Inject app reference into request and response
|
|
282
|
+
request.app = this;
|
|
283
|
+
response.app = this;
|
|
284
|
+
response._req = request;
|
|
285
|
+
request._res = response;
|
|
286
|
+
|
|
287
|
+
// Preserve original URL before any middleware rewrites
|
|
288
|
+
request.originalUrl = request.url;
|
|
289
|
+
|
|
290
|
+
// Merge app.locals into request/response locals via prototype chain (avoids copy per request)
|
|
291
|
+
request.locals = Object.create(this.locals);
|
|
292
|
+
response.locals = Object.create(this.locals);
|
|
293
|
+
|
|
294
|
+
let idx = 0;
|
|
295
|
+
const run = (err) =>
|
|
296
|
+
{
|
|
297
|
+
if (err)
|
|
298
|
+
{
|
|
299
|
+
log.error('middleware error: %s', err.message || err);
|
|
300
|
+
if (this._errorHandler) return this._errorHandler(err, request, response, run);
|
|
301
|
+
response.status(500).json({ error: err.message || 'Internal Server Error' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (idx < this.middlewares.length)
|
|
305
|
+
{
|
|
306
|
+
const mw = this.middlewares[idx++];
|
|
307
|
+
try
|
|
308
|
+
{
|
|
309
|
+
const result = mw(request, response, run);
|
|
310
|
+
// Handle promise-returning middleware
|
|
311
|
+
if (result && typeof result.catch === 'function')
|
|
312
|
+
{
|
|
313
|
+
result.catch(run);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (e)
|
|
317
|
+
{
|
|
318
|
+
run(e);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.router.handle(request, response);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
run();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// -- Server Lifecycle ------------------------------
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Start listening for HTTP, HTTPS, or HTTP/2 connections.
|
|
332
|
+
*
|
|
333
|
+
* @param {number} [port=3000] - Port number to bind.
|
|
334
|
+
* @param {object|Function} [opts] - Server options or callback.
|
|
335
|
+
* @param {boolean} [opts.http2] - Create an HTTP/2 server.
|
|
336
|
+
* @param {Buffer|string} [opts.key] - Private key for TLS (HTTPS or HTTP/2 with TLS).
|
|
337
|
+
* @param {Buffer|string} [opts.cert] - Certificate for TLS.
|
|
338
|
+
* @param {Buffer|string} [opts.pfx] - PFX/PKCS12 bundle (alternative to key+cert).
|
|
339
|
+
* @param {Buffer|string} [opts.ca] - CA certificate(s) for client verification.
|
|
340
|
+
* @param {boolean} [opts.allowHTTP1=true] - Allow HTTP/1.1 fallback on HTTP/2 secure servers (ALPN).
|
|
341
|
+
* @param {object} [opts.settings] - HTTP/2 settings (SETTINGS frame values).
|
|
342
|
+
* @param {Function} [cb] - Callback invoked once the server is listening.
|
|
343
|
+
* @returns {import('http').Server|import('https').Server|import('http2').Http2SecureServer|import('http2').Http2Server}
|
|
344
|
+
*
|
|
345
|
+
* @example | Plain HTTP
|
|
346
|
+
* app.listen(3000, () => console.log('HTTP on 3000'));
|
|
347
|
+
*
|
|
348
|
+
* @example | HTTPS
|
|
349
|
+
* app.listen(443, { key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') });
|
|
350
|
+
*
|
|
351
|
+
* @example | HTTP/2 with TLS and ALPN
|
|
352
|
+
* app.listen(443, { http2: true, key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') });
|
|
353
|
+
*
|
|
354
|
+
* @example | HTTP/2 Cleartext
|
|
355
|
+
* app.listen(3000, { http2: true });
|
|
356
|
+
*/
|
|
357
|
+
listen(port = 3000, opts, cb)
|
|
358
|
+
{
|
|
359
|
+
// Normalise arguments — allow `listen(port, cb)` without opts
|
|
360
|
+
if (typeof opts === 'function') { cb = opts; opts = undefined; }
|
|
361
|
+
|
|
362
|
+
const isH2 = opts && opts.http2;
|
|
363
|
+
const hasTLS = opts && (opts.key || opts.pfx || opts.cert);
|
|
364
|
+
let server;
|
|
365
|
+
|
|
366
|
+
if (isH2 && hasTLS)
|
|
367
|
+
{
|
|
368
|
+
// HTTP/2 over TLS with ALPN negotiation (h2 + HTTP/1.1 fallback)
|
|
369
|
+
const h2Opts = {
|
|
370
|
+
...opts,
|
|
371
|
+
allowHTTP1: opts.allowHTTP1 !== false, // default true for graceful fallback
|
|
372
|
+
};
|
|
373
|
+
delete h2Opts.http2;
|
|
374
|
+
server = http2.createSecureServer(h2Opts, this.handler);
|
|
375
|
+
log.info('starting HTTP/2 (TLS) server on port %d', port);
|
|
376
|
+
}
|
|
377
|
+
else if (isH2)
|
|
378
|
+
{
|
|
379
|
+
// HTTP/2 cleartext (h2c) — no TLS, for internal services
|
|
380
|
+
const h2Opts = opts ? { ...opts } : {};
|
|
381
|
+
delete h2Opts.http2;
|
|
382
|
+
server = http2.createServer(h2Opts, this.handler);
|
|
383
|
+
log.info('starting HTTP/2 (h2c) server on port %d', port);
|
|
384
|
+
}
|
|
385
|
+
else if (hasTLS)
|
|
386
|
+
{
|
|
387
|
+
server = https.createServer(opts, this.handler);
|
|
388
|
+
log.info('starting HTTPS server on port %d', port);
|
|
389
|
+
}
|
|
390
|
+
else
|
|
391
|
+
{
|
|
392
|
+
server = http.createServer(this.handler);
|
|
393
|
+
log.info('starting HTTP server on port %d', port);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this._server = server;
|
|
397
|
+
|
|
398
|
+
// Intercept HTTP/2 streams for gRPC before the normal request handler
|
|
399
|
+
if (isH2 && this._grpcRegistry)
|
|
400
|
+
{
|
|
401
|
+
server.on('stream', (stream, headers) =>
|
|
402
|
+
{
|
|
403
|
+
if (this._grpcRegistry.handleStream(stream, headers))
|
|
404
|
+
return; // handled as gRPC
|
|
405
|
+
// Otherwise fall through — the compat handler fires the normal request event
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Always attach WebSocket upgrade handling so ws() works
|
|
410
|
+
// regardless of registration order (before or after listen).
|
|
411
|
+
server.on('upgrade', (req, socket, head) =>
|
|
412
|
+
{
|
|
413
|
+
if (this._wsHandlers.size > 0)
|
|
414
|
+
handleUpgrade(req, socket, head, this._wsHandlers);
|
|
415
|
+
else
|
|
416
|
+
socket.destroy();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Install graceful shutdown signal handlers
|
|
420
|
+
this._lifecycle.installSignalHandlers();
|
|
421
|
+
|
|
422
|
+
return server.listen(port, cb);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Gracefully close the server, stopping new connections.
|
|
427
|
+
*
|
|
428
|
+
* @param {Function} [cb] - Callback invoked once the server has closed.
|
|
429
|
+
*/
|
|
430
|
+
close(cb)
|
|
431
|
+
{
|
|
432
|
+
if (this._server) this._server.close(cb);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Perform a full graceful shutdown.
|
|
437
|
+
* Stops accepting new connections, drains in-flight requests, closes
|
|
438
|
+
* WebSocket, SSE, and gRPC connections, and shuts down registered databases.
|
|
439
|
+
*
|
|
440
|
+
* @param {object} [opts] - Shutdown options.
|
|
441
|
+
* @param {number} [opts.timeout] - Maximum ms to wait for in-flight requests (default 30000).
|
|
442
|
+
* @returns {Promise<void>} Resolves when shutdown is complete.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* await app.shutdown();
|
|
446
|
+
* // or with custom timeout
|
|
447
|
+
* await app.shutdown({ timeout: 5000 });
|
|
448
|
+
*/
|
|
449
|
+
shutdown(opts)
|
|
450
|
+
{
|
|
451
|
+
if (!this._shutdownPromise)
|
|
452
|
+
{
|
|
453
|
+
this._shutdownPromise = this._lifecycle.shutdown(opts);
|
|
454
|
+
}
|
|
455
|
+
return this._shutdownPromise;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// -- Lifecycle Events ------------------------------
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Register a lifecycle event listener.
|
|
462
|
+
*
|
|
463
|
+
* Supported events:
|
|
464
|
+
* - `'beforeShutdown'` — fires before shutdown begins (flush caches, finish writes)
|
|
465
|
+
* - `'shutdown'` — fires after shutdown is complete
|
|
466
|
+
*
|
|
467
|
+
* @param {'beforeShutdown'|'shutdown'} event - Lifecycle event name.
|
|
468
|
+
* @param {Function} fn - Async or sync callback.
|
|
469
|
+
* @returns {App} `this` for chaining.
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* app.on('beforeShutdown', async () => {
|
|
473
|
+
* await saveMetrics();
|
|
474
|
+
* });
|
|
475
|
+
*
|
|
476
|
+
* app.on('shutdown', () => {
|
|
477
|
+
* console.log('goodbye');
|
|
478
|
+
* });
|
|
479
|
+
*/
|
|
480
|
+
on(event, fn)
|
|
481
|
+
{
|
|
482
|
+
this._lifecycle.on(event, fn);
|
|
483
|
+
return this;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Remove a lifecycle event listener.
|
|
488
|
+
*
|
|
489
|
+
* @param {'beforeShutdown'|'shutdown'} event - Event name.
|
|
490
|
+
* @param {Function} fn - Callback to remove.
|
|
491
|
+
* @returns {App} `this` for chaining.
|
|
492
|
+
*/
|
|
493
|
+
off(event, fn)
|
|
494
|
+
{
|
|
495
|
+
this._lifecycle.off(event, fn);
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// -- Lifecycle Resource Registration ---------------
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Register a WebSocket pool for graceful shutdown. All connections
|
|
503
|
+
* in the pool are closed with code `1001` when the server shuts down.
|
|
504
|
+
*
|
|
505
|
+
* @param {import('./ws/room')} pool - WebSocket pool instance.
|
|
506
|
+
* @returns {App} `this` for chaining.
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* const pool = new WebSocketPool();
|
|
510
|
+
* app.registerPool(pool);
|
|
511
|
+
*/
|
|
512
|
+
registerPool(pool)
|
|
513
|
+
{
|
|
514
|
+
this._lifecycle.registerPool(pool);
|
|
515
|
+
return this;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Unregister a WebSocket pool from lifecycle management.
|
|
520
|
+
*
|
|
521
|
+
* @param {import('./ws/room')} pool - WebSocket pool instance.
|
|
522
|
+
* @returns {App} `this` for chaining.
|
|
523
|
+
*/
|
|
524
|
+
unregisterPool(pool)
|
|
525
|
+
{
|
|
526
|
+
this._lifecycle.unregisterPool(pool);
|
|
527
|
+
return this;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Track an SSE stream for graceful shutdown. The stream
|
|
532
|
+
* is automatically untracked when it closes.
|
|
533
|
+
*
|
|
534
|
+
* @param {import('./sse/stream')} stream - SSE stream instance.
|
|
535
|
+
* @returns {App} `this` for chaining.
|
|
536
|
+
*/
|
|
537
|
+
trackSSE(stream)
|
|
538
|
+
{
|
|
539
|
+
this._lifecycle.trackSSE(stream);
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Register an ORM Database instance for graceful shutdown.
|
|
545
|
+
* The database connection is closed during shutdown.
|
|
546
|
+
*
|
|
547
|
+
* @param {import('./orm')} db - Database instance.
|
|
548
|
+
* @returns {App} `this` for chaining.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* const db = new Database({ adapter: 'memory' });
|
|
552
|
+
* app.registerDatabase(db);
|
|
553
|
+
*/
|
|
554
|
+
registerDatabase(db)
|
|
555
|
+
{
|
|
556
|
+
this._lifecycle.registerDatabase(db);
|
|
557
|
+
return this;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Unregister an ORM Database instance from lifecycle management.
|
|
562
|
+
*
|
|
563
|
+
* @param {import('./orm')} db - Database instance.
|
|
564
|
+
* @returns {App} `this` for chaining.
|
|
565
|
+
*/
|
|
566
|
+
unregisterDatabase(db)
|
|
567
|
+
{
|
|
568
|
+
this._lifecycle.unregisterDatabase(db);
|
|
569
|
+
return this;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Configure the shutdown timeout—the maximum time (ms) to wait for
|
|
574
|
+
* in-flight requests to finish before forcefully terminating them.
|
|
575
|
+
*
|
|
576
|
+
* @param {number} ms - Timeout in milliseconds.
|
|
577
|
+
* @returns {App} `this` for chaining.
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* app.shutdownTimeout(10000); // 10s
|
|
581
|
+
*/
|
|
582
|
+
shutdownTimeout(ms)
|
|
583
|
+
{
|
|
584
|
+
this._lifecycle._shutdownTimeout = ms;
|
|
585
|
+
return this;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Current lifecycle state.
|
|
590
|
+
*
|
|
591
|
+
* @type {'running'|'draining'|'closed'}
|
|
592
|
+
*/
|
|
593
|
+
get lifecycleState()
|
|
594
|
+
{
|
|
595
|
+
return this._lifecycle.state;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// -- Observability ---------------------------------
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Register a liveness health check endpoint.
|
|
602
|
+
* Returns `200` when healthy, `503` during shutdown.
|
|
603
|
+
*
|
|
604
|
+
* @param {string} [path='/healthz'] - Endpoint path.
|
|
605
|
+
* @param {Object<string, Function>} [checks] - Named health check functions.
|
|
606
|
+
* @returns {App} `this` for chaining.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* app.health(); // GET /healthz
|
|
610
|
+
* app.health('/alive'); // GET /alive
|
|
611
|
+
* app.health('/healthz', { memory: memoryCheck() });
|
|
612
|
+
*/
|
|
613
|
+
health(path, checks)
|
|
614
|
+
{
|
|
615
|
+
if (typeof path === 'object') { checks = path; path = '/healthz'; }
|
|
616
|
+
if (!path) path = '/healthz';
|
|
617
|
+
this.get(path, healthCheck({ checks: checks || {} }));
|
|
618
|
+
return this;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Register a readiness health check endpoint.
|
|
623
|
+
* Returns `200` when all checks pass, `503` otherwise.
|
|
624
|
+
*
|
|
625
|
+
* @param {string} [path='/readyz'] - Endpoint path.
|
|
626
|
+
* @param {Object<string, Function>} [checks] - Named readiness check functions.
|
|
627
|
+
* @returns {App} `this` for chaining.
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* app.ready('/readyz', {
|
|
631
|
+
* database: () => db.ping(),
|
|
632
|
+
* cache: () => ({ healthy: redis.isConnected }),
|
|
633
|
+
* });
|
|
634
|
+
*/
|
|
635
|
+
ready(path, checks)
|
|
636
|
+
{
|
|
637
|
+
if (typeof path === 'object') { checks = path; path = '/readyz'; }
|
|
638
|
+
if (!path) path = '/readyz';
|
|
639
|
+
this.get(path, healthCheck({ checks: checks || {} }));
|
|
640
|
+
return this;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Register a custom health check.
|
|
645
|
+
*
|
|
646
|
+
* @param {string} name - Check name.
|
|
647
|
+
* @param {Function} fn - Check function `() => { healthy, details }`.
|
|
648
|
+
* @returns {App} `this` for chaining.
|
|
649
|
+
*
|
|
650
|
+
* @example
|
|
651
|
+
* app.addHealthCheck('redis', async () => {
|
|
652
|
+
* await redis.ping();
|
|
653
|
+
* return { healthy: true };
|
|
654
|
+
* });
|
|
655
|
+
*/
|
|
656
|
+
addHealthCheck(name, fn)
|
|
657
|
+
{
|
|
658
|
+
if (!this._healthChecks) this._healthChecks = {};
|
|
659
|
+
this._healthChecks[name] = fn;
|
|
660
|
+
return this;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Get the application metrics registry. Lazily created on first access.
|
|
665
|
+
* Returns a `MetricsRegistry` instance for registering custom metrics.
|
|
666
|
+
*
|
|
667
|
+
* @returns {import('./observe/metrics').MetricsRegistry} The metrics registry.
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* const counter = app.metrics().counter({
|
|
671
|
+
* name: 'custom_events_total',
|
|
672
|
+
* help: 'Custom events',
|
|
673
|
+
* });
|
|
674
|
+
* counter.inc();
|
|
675
|
+
*/
|
|
676
|
+
metrics()
|
|
677
|
+
{
|
|
678
|
+
if (!this._metricsRegistry)
|
|
679
|
+
{
|
|
680
|
+
this._metricsRegistry = new MetricsRegistry();
|
|
681
|
+
}
|
|
682
|
+
return this._metricsRegistry;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Mount a Prometheus metrics endpoint.
|
|
687
|
+
* Pair with `metricsMiddleware()` to auto-instrument all HTTP traffic.
|
|
688
|
+
*
|
|
689
|
+
* @param {string} [path='/metrics'] - Endpoint path.
|
|
690
|
+
* @param {object} [opts] - Options.
|
|
691
|
+
* @param {import('./observe/metrics').MetricsRegistry} [opts.registry] - Registry. Uses `app.metrics()` if not provided.
|
|
692
|
+
* @returns {App} `this` for chaining.
|
|
693
|
+
*
|
|
694
|
+
* @example | Full Observability Setup
|
|
695
|
+
* app.use(metricsMiddleware({ registry: app.metrics() }));
|
|
696
|
+
* app.metricsEndpoint(); // GET /metrics
|
|
697
|
+
* app.health(); // GET /healthz
|
|
698
|
+
* app.ready(); // GET /readyz
|
|
699
|
+
*
|
|
700
|
+
* @example yaml | prometheus.yml
|
|
701
|
+
* scrape_configs:
|
|
702
|
+
* - job_name: 'my-app'
|
|
703
|
+
* scrape_interval: 5s
|
|
704
|
+
* static_configs:
|
|
705
|
+
* - targets: ['localhost:3000']
|
|
706
|
+
*
|
|
707
|
+
* @example shell | Start Prometheus
|
|
708
|
+
* $ docker run -d -p 9090:9090 -v ./prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus
|
|
709
|
+
*
|
|
710
|
+
* @example | Custom Path
|
|
711
|
+
*/
|
|
712
|
+
metricsEndpoint(path, opts = {})
|
|
713
|
+
{
|
|
714
|
+
if (typeof path === 'object') { opts = path; path = '/metrics'; }
|
|
715
|
+
if (!path) path = '/metrics';
|
|
716
|
+
const registry = opts.registry || this.metrics();
|
|
717
|
+
this.get(path, metricsEndpoint(registry));
|
|
718
|
+
return this;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// -- WebSocket Support -----------------------------
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Register a WebSocket upgrade handler for a path.
|
|
725
|
+
*
|
|
726
|
+
* The handler receives `(ws, req)` where `ws` is a `WebSocketConnection`
|
|
727
|
+
* instance with methods like `send()`, `sendJSON()`, `on()`, and `close()`.
|
|
728
|
+
*
|
|
729
|
+
* @param {string} path - URL path to listen for upgrade requests.
|
|
730
|
+
* @param {object|Function} [opts] - Options object, or the handler function directly.
|
|
731
|
+
* @param {number} [opts.maxPayload=1048576] - Maximum incoming frame size in bytes (default 1 MB).
|
|
732
|
+
* @param {number} [opts.pingInterval=30000] - Auto-ping interval in ms. Set `0` to disable.
|
|
733
|
+
* @param {Function} [opts.verifyClient] - `(req) => boolean` — return false to reject the upgrade.
|
|
734
|
+
* @param {Function} handler - `(ws, req) => void`.
|
|
735
|
+
*
|
|
736
|
+
* @example | Simple
|
|
737
|
+
* app.ws('/chat', (ws, req) => {
|
|
738
|
+
* ws.on('message', data => ws.send('echo: ' + data));
|
|
739
|
+
* });
|
|
740
|
+
*
|
|
741
|
+
* @example | With Options
|
|
742
|
+
* app.ws('/feed', { maxPayload: 64 * 1024, pingInterval: 15000 }, (ws, req) => {
|
|
743
|
+
* console.log('client', ws.id, 'from', ws.ip);
|
|
744
|
+
* ws.sendJSON({ hello: 'world' });
|
|
745
|
+
* });
|
|
746
|
+
*/
|
|
747
|
+
ws(path, opts, handler)
|
|
748
|
+
{
|
|
749
|
+
// Normalise arguments: ws(path, handler) or ws(path, opts, handler)
|
|
750
|
+
if (typeof opts === 'function') { handler = opts; opts = {}; }
|
|
751
|
+
if (!opts) opts = {};
|
|
752
|
+
|
|
753
|
+
this._wsHandlers.set(path, { handler, opts });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// -- Route Introspection ---------------------------
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Return a flat list of all registered routes across the router tree,
|
|
760
|
+
* including mounted sub-routers. Useful for debugging, auto-generated
|
|
761
|
+
* docs, or CLI tooling.
|
|
762
|
+
*
|
|
763
|
+
* @returns {{ method: string, path: string }[]} Flat array of route entries including WebSocket handlers.
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* app.routes().forEach(r => console.log(r.method, r.path));
|
|
767
|
+
* // GET /users
|
|
768
|
+
* // POST /users
|
|
769
|
+
* // GET /api/v1/items/:id
|
|
770
|
+
*/
|
|
771
|
+
routes()
|
|
772
|
+
{
|
|
773
|
+
const list = this.router.inspect();
|
|
774
|
+
|
|
775
|
+
/* Include WebSocket upgrade handlers */
|
|
776
|
+
for (const [wsPath, { opts }] of this._wsHandlers)
|
|
777
|
+
{
|
|
778
|
+
const entry = { method: 'WS', path: wsPath };
|
|
779
|
+
if (opts && opts.maxPayload !== undefined) entry.maxPayload = opts.maxPayload;
|
|
780
|
+
if (opts && opts.pingInterval !== undefined) entry.pingInterval = opts.pingInterval;
|
|
781
|
+
list.push(entry);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/* Include gRPC routes */
|
|
785
|
+
if (this._grpcRegistry)
|
|
786
|
+
list.push(...this._grpcRegistry.routes());
|
|
787
|
+
|
|
788
|
+
return list;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// -- Route Registration ----------------------------
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Extract an options object from the head of the handlers array when
|
|
795
|
+
* the first argument is a plain object (not a function).
|
|
796
|
+
* @private
|
|
797
|
+
*/
|
|
798
|
+
_extractOpts(fns)
|
|
799
|
+
{
|
|
800
|
+
let opts = {};
|
|
801
|
+
if (fns.length > 0 && typeof fns[0] === 'object' && typeof fns[0] !== 'function')
|
|
802
|
+
{
|
|
803
|
+
opts = fns.shift();
|
|
804
|
+
}
|
|
805
|
+
return opts;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Register one or more handler functions for a specific HTTP method and path.
|
|
810
|
+
*
|
|
811
|
+
* @param {string} method - HTTP method (GET, POST, etc.) or 'ALL'.
|
|
812
|
+
* @param {string} path - Route pattern (e.g. '/users/:id').
|
|
813
|
+
* @param {...Function|object} fns - Optional options object `{ secure }` followed by handler functions.
|
|
814
|
+
*/
|
|
815
|
+
route(method, path, ...fns) { const o = this._extractOpts(fns); this.router.add(method, path, fns, o); }
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* @see App#route — shortcut for GET requests.
|
|
819
|
+
* @param {string} path - Route pattern.
|
|
820
|
+
* @param {...Function} fns - Handler functions.
|
|
821
|
+
* @returns {App} `this` for chaining.
|
|
822
|
+
*/
|
|
823
|
+
get(path, ...fns) { if (arguments.length === 1 && typeof path === 'string' && fns.length === 0) return this.set(path); this.route('GET', path, ...fns); return this; }
|
|
824
|
+
/**
|
|
825
|
+
* @see App#route — shortcut for POST requests.
|
|
826
|
+
* @param {string} path - Route pattern.
|
|
827
|
+
* @param {...Function} fns - Handler functions.
|
|
828
|
+
* @returns {App} `this` for chaining.
|
|
829
|
+
*/
|
|
830
|
+
post(path, ...fns) { this.route('POST', path, ...fns); return this; }
|
|
831
|
+
/**
|
|
832
|
+
* @see App#route — shortcut for PUT requests.
|
|
833
|
+
* @param {string} path - Route pattern.
|
|
834
|
+
* @param {...Function} fns - Handler functions.
|
|
835
|
+
* @returns {App} `this` for chaining.
|
|
836
|
+
*/
|
|
837
|
+
put(path, ...fns) { this.route('PUT', path, ...fns); return this; }
|
|
838
|
+
/**
|
|
839
|
+
* @see App#route — shortcut for DELETE requests.
|
|
840
|
+
* @param {string} path - Route pattern.
|
|
841
|
+
* @param {...Function} fns - Handler functions.
|
|
842
|
+
* @returns {App} `this` for chaining.
|
|
843
|
+
*/
|
|
844
|
+
delete(path, ...fns) { this.route('DELETE', path, ...fns); return this; }
|
|
845
|
+
/**
|
|
846
|
+
* @see App#route — shortcut for PATCH requests.
|
|
847
|
+
* @param {string} path - Route pattern.
|
|
848
|
+
* @param {...Function} fns - Handler functions.
|
|
849
|
+
* @returns {App} `this` for chaining.
|
|
850
|
+
*/
|
|
851
|
+
patch(path, ...fns) { this.route('PATCH', path, ...fns); return this; }
|
|
852
|
+
/**
|
|
853
|
+
* @see App#route — shortcut for OPTIONS requests.
|
|
854
|
+
* @param {string} path - Route pattern.
|
|
855
|
+
* @param {...Function} fns - Handler functions.
|
|
856
|
+
* @returns {App} `this` for chaining.
|
|
857
|
+
*/
|
|
858
|
+
options(path, ...fns) { this.route('OPTIONS', path, ...fns); return this; }
|
|
859
|
+
/**
|
|
860
|
+
* @see App#route — shortcut for HEAD requests.
|
|
861
|
+
* @param {string} path - Route pattern.
|
|
862
|
+
* @param {...Function} fns - Handler functions.
|
|
863
|
+
* @returns {App} `this` for chaining.
|
|
864
|
+
*/
|
|
865
|
+
head(path, ...fns) { this.route('HEAD', path, ...fns); return this; }
|
|
866
|
+
/**
|
|
867
|
+
* @see App#route — matches every HTTP method.
|
|
868
|
+
* @param {string} path - Route pattern.
|
|
869
|
+
* @param {...Function} fns - Handler functions.
|
|
870
|
+
* @returns {App} `this` for chaining.
|
|
871
|
+
*/
|
|
872
|
+
all(path, ...fns) { this.route('ALL', path, ...fns); return this; }
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Chainable route builder — register multiple methods on the same path.
|
|
876
|
+
*
|
|
877
|
+
* @param {string} path - Route pattern.
|
|
878
|
+
* @returns {object} Chain object with HTTP verb methods.
|
|
879
|
+
*
|
|
880
|
+
* @example
|
|
881
|
+
* app.chain('/users')
|
|
882
|
+
* .get((req, res) => res.json(users))
|
|
883
|
+
* .post((req, res) => res.json({ created: true }));
|
|
884
|
+
*/
|
|
885
|
+
chain(path) { return this.router.route(path); }
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Define a route group with shared middleware prefix.
|
|
889
|
+
* All routes registered inside the callback share the given path prefix
|
|
890
|
+
* and middleware stack.
|
|
891
|
+
*
|
|
892
|
+
* @param {string} prefix - URL prefix for the group.
|
|
893
|
+
* @param {...Function} middleware - Shared middleware, last argument is the callback.
|
|
894
|
+
* @returns {App} `this` for chaining.
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* app.group('/api/v1', authMiddleware, (router) => {
|
|
898
|
+
* router.get('/users', listUsers);
|
|
899
|
+
* router.post('/users', createUser);
|
|
900
|
+
* router.get('/users/:id', getUser);
|
|
901
|
+
* });
|
|
902
|
+
*/
|
|
903
|
+
group(prefix, ...args)
|
|
904
|
+
{
|
|
905
|
+
const cb = args.pop();
|
|
906
|
+
const middlewareStack = args;
|
|
907
|
+
const router = new Router();
|
|
908
|
+
cb(router);
|
|
909
|
+
if (middlewareStack.length > 0)
|
|
910
|
+
{
|
|
911
|
+
this.middlewares.push((req, res, next) =>
|
|
912
|
+
{
|
|
913
|
+
const urlPath = req.url.split('?')[0];
|
|
914
|
+
const cleanPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
915
|
+
if (urlPath === cleanPrefix || urlPath.startsWith(cleanPrefix + '/'))
|
|
916
|
+
{
|
|
917
|
+
let i = 0;
|
|
918
|
+
const runMw = () =>
|
|
919
|
+
{
|
|
920
|
+
if (i < middlewareStack.length)
|
|
921
|
+
{
|
|
922
|
+
const mw = middlewareStack[i++];
|
|
923
|
+
try
|
|
924
|
+
{
|
|
925
|
+
const result = mw(req, res, runMw);
|
|
926
|
+
if (result && typeof result.catch === 'function') result.catch(next);
|
|
927
|
+
}
|
|
928
|
+
catch (e) { next(e); }
|
|
929
|
+
}
|
|
930
|
+
else { next(); }
|
|
931
|
+
};
|
|
932
|
+
runMw();
|
|
933
|
+
}
|
|
934
|
+
else { next(); }
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
this.router.use(prefix, router);
|
|
938
|
+
return this;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// -- gRPC Support ----------------------------------
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Register a gRPC service handler.
|
|
945
|
+
*
|
|
946
|
+
* Accepts a parsed proto schema, a service name, and a map of method handlers.
|
|
947
|
+
* When the server is HTTP/2, incoming `application/grpc` requests are routed
|
|
948
|
+
* to the matching service method automatically.
|
|
949
|
+
*
|
|
950
|
+
* @param {object} schema - Parsed proto schema from `parseProto()` or `parseProtoFile()`.
|
|
951
|
+
* @param {string} serviceName - Service name as defined in the proto file.
|
|
952
|
+
* @param {Object<string, Function>} handlers - Map of method names → handler functions.
|
|
953
|
+
* @param {object} [opts] - Service options.
|
|
954
|
+
* @param {Function[]} [opts.interceptors] - Per-service interceptors `(call, next) => void`.
|
|
955
|
+
* @param {number} [opts.maxMessageSize] - Max incoming message size in bytes.
|
|
956
|
+
* @param {boolean} [opts.compress=false] - Whether to compress outgoing messages.
|
|
957
|
+
* @returns {App} `this` for chaining.
|
|
958
|
+
*
|
|
959
|
+
* @example | Unary RPC
|
|
960
|
+
* const schema = parseProto(fs.readFileSync('hello.proto', 'utf8'));
|
|
961
|
+
* app.grpc(schema, 'Greeter', {
|
|
962
|
+
* SayHello(call) { return { message: 'Hello ' + call.request.name }; },
|
|
963
|
+
* });
|
|
964
|
+
* app.listen(50051, { http2: true });
|
|
965
|
+
*
|
|
966
|
+
* @example | Server streaming with interceptors
|
|
967
|
+
* app.grpc(schema, 'DataService', {
|
|
968
|
+
* StreamData(call) {
|
|
969
|
+
* for (let i = 0; i < 100; i++) call.write({ seq: i });
|
|
970
|
+
* call.end();
|
|
971
|
+
* },
|
|
972
|
+
* }, { interceptors: [authInterceptor] });
|
|
973
|
+
*/
|
|
974
|
+
grpc(schema, serviceName, handlers, opts)
|
|
975
|
+
{
|
|
976
|
+
if (!this._grpcRegistry)
|
|
977
|
+
{
|
|
978
|
+
this._grpcRegistry = new GrpcServiceRegistry();
|
|
979
|
+
this._lifecycle.registerGrpc(this._grpcRegistry);
|
|
980
|
+
}
|
|
981
|
+
this._grpcRegistry.addService(schema, serviceName, handlers, opts);
|
|
982
|
+
return this;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Add a global gRPC interceptor that runs before every gRPC call.
|
|
987
|
+
*
|
|
988
|
+
* @param {Function} fn - Interceptor `(call, next) => void`.
|
|
989
|
+
* @returns {App} `this` for chaining.
|
|
990
|
+
*
|
|
991
|
+
* @example
|
|
992
|
+
* app.grpcInterceptor(async (call, next) => {
|
|
993
|
+
* console.log('gRPC call:', call.method.name);
|
|
994
|
+
* await next();
|
|
995
|
+
* });
|
|
996
|
+
*/
|
|
997
|
+
grpcInterceptor(fn)
|
|
998
|
+
{
|
|
999
|
+
if (!this._grpcRegistry)
|
|
1000
|
+
{
|
|
1001
|
+
this._grpcRegistry = new GrpcServiceRegistry();
|
|
1002
|
+
this._lifecycle.registerGrpc(this._grpcRegistry);
|
|
1003
|
+
}
|
|
1004
|
+
this._grpcRegistry.addInterceptor(fn);
|
|
1005
|
+
return this;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Enable the gRPC Health Checking Protocol (`grpc.health.v1.Health`).
|
|
1010
|
+
* Registers the `Check` (unary) and `Watch` (server-stream) RPCs
|
|
1011
|
+
* required by Kubernetes, Envoy, and standard gRPC health probes.
|
|
1012
|
+
*
|
|
1013
|
+
* Call `app.setServiceStatus(name, status)` to update individual services.
|
|
1014
|
+
* Overall server health defaults to `SERVING`.
|
|
1015
|
+
*
|
|
1016
|
+
* @returns {App} `this` for chaining.
|
|
1017
|
+
*
|
|
1018
|
+
* @example
|
|
1019
|
+
* app.grpcHealth();
|
|
1020
|
+
* app.listen(50051, { http2: true });
|
|
1021
|
+
*/
|
|
1022
|
+
grpcHealth()
|
|
1023
|
+
{
|
|
1024
|
+
if (this._healthService) return this;
|
|
1025
|
+
|
|
1026
|
+
const { HealthService } = require('./grpc/health');
|
|
1027
|
+
this._healthService = new HealthService();
|
|
1028
|
+
|
|
1029
|
+
const schema = this._healthService.getSchema();
|
|
1030
|
+
const handlers = this._healthService.getHandlers();
|
|
1031
|
+
this.grpc(schema, 'Health', handlers);
|
|
1032
|
+
|
|
1033
|
+
// Set all services to NOT_SERVING during graceful shutdown
|
|
1034
|
+
this._lifecycle.on('beforeShutdown', () =>
|
|
1035
|
+
{
|
|
1036
|
+
this._healthService.setAllNotServing();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
log.info('gRPC health check service enabled');
|
|
1040
|
+
return this;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Set the health status of a gRPC service.
|
|
1045
|
+
* Requires `grpcHealth()` to have been called first.
|
|
1046
|
+
*
|
|
1047
|
+
* @param {string} serviceName - Service name (empty string for overall server health).
|
|
1048
|
+
* @param {string|number} status - `'SERVING'`, `'NOT_SERVING'`, `'UNKNOWN'`, or numeric value.
|
|
1049
|
+
* @returns {App} `this` for chaining.
|
|
1050
|
+
*
|
|
1051
|
+
* @example
|
|
1052
|
+
* app.setServiceStatus('myapp.UserService', 'SERVING');
|
|
1053
|
+
* app.setServiceStatus('myapp.OrderService', 'NOT_SERVING');
|
|
1054
|
+
*/
|
|
1055
|
+
setServiceStatus(serviceName, status)
|
|
1056
|
+
{
|
|
1057
|
+
if (!this._healthService)
|
|
1058
|
+
throw new Error('Call app.grpcHealth() before setServiceStatus()');
|
|
1059
|
+
this._healthService.setStatus(serviceName, status);
|
|
1060
|
+
return this;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Enable gRPC Server Reflection (`grpc.reflection.v1.ServerReflection`).
|
|
1065
|
+
* Allows `grpcurl`, `grpcui`, Postman, and other tools to introspect services.
|
|
1066
|
+
* Disabled in production by default unless `opts.production` is `true`.
|
|
1067
|
+
*
|
|
1068
|
+
* Must be called AFTER all `app.grpc()` registrations so that all schemas
|
|
1069
|
+
* are available for reflection.
|
|
1070
|
+
*
|
|
1071
|
+
* @param {object} [opts] - Options.
|
|
1072
|
+
* @param {boolean} [opts.production=false] - Enable in production environments.
|
|
1073
|
+
* @returns {App} `this` for chaining.
|
|
1074
|
+
*
|
|
1075
|
+
* @example
|
|
1076
|
+
* app.grpc(schema, 'Greeter', handlers);
|
|
1077
|
+
* app.grpcReflection();
|
|
1078
|
+
*/
|
|
1079
|
+
grpcReflection(opts = {})
|
|
1080
|
+
{
|
|
1081
|
+
if (this._reflectionService) return this;
|
|
1082
|
+
|
|
1083
|
+
if (!opts.production && process.env.NODE_ENV === 'production')
|
|
1084
|
+
{
|
|
1085
|
+
log.warn('gRPC reflection disabled in production (set { production: true } to override)');
|
|
1086
|
+
return this;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const { ReflectionService } = require('./grpc/reflection');
|
|
1090
|
+
this._reflectionService = new ReflectionService(opts);
|
|
1091
|
+
|
|
1092
|
+
// Register all already-registered schemas
|
|
1093
|
+
if (this._grpcRegistry)
|
|
1094
|
+
{
|
|
1095
|
+
for (const [path, route] of this._grpcRegistry._routes)
|
|
1096
|
+
{
|
|
1097
|
+
if (route.schema && !route.schema._reflectionRegistered)
|
|
1098
|
+
{
|
|
1099
|
+
this._reflectionService.addSchema(route.schema);
|
|
1100
|
+
route.schema._reflectionRegistered = true;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const schema = this._reflectionService.getSchema();
|
|
1106
|
+
const handlers = this._reflectionService.getHandlers();
|
|
1107
|
+
this.grpc(schema, 'ServerReflection', handlers);
|
|
1108
|
+
|
|
1109
|
+
log.info('gRPC server reflection enabled');
|
|
1110
|
+
return this;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// -- Authentication & Sessions ---------------------
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Mount JWT authentication middleware.
|
|
1117
|
+
* Shorthand for `app.use(jwt(opts))`.
|
|
1118
|
+
*
|
|
1119
|
+
* @param {object} opts - JWT options (see `jwt()` for full documentation).
|
|
1120
|
+
* @returns {App} `this` for chaining.
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* app.jwt({ secret: process.env.JWT_SECRET });
|
|
1124
|
+
* app.jwt({ jwksUri: 'https://auth.example.com/.well-known/jwks.json' });
|
|
1125
|
+
*/
|
|
1126
|
+
jwtAuth(opts)
|
|
1127
|
+
{
|
|
1128
|
+
const { jwt } = require('./auth/jwt');
|
|
1129
|
+
this.use(jwt(opts));
|
|
1130
|
+
return this;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Mount session middleware.
|
|
1135
|
+
* Shorthand for `app.use(session(opts))`.
|
|
1136
|
+
*
|
|
1137
|
+
* @param {object} opts - Session options (see `session()` for full documentation).
|
|
1138
|
+
* @returns {App} `this` for chaining.
|
|
1139
|
+
*
|
|
1140
|
+
* @example
|
|
1141
|
+
* app.sessions({ secret: process.env.SESSION_SECRET });
|
|
1142
|
+
*/
|
|
1143
|
+
sessions(opts)
|
|
1144
|
+
{
|
|
1145
|
+
const { session } = require('./auth/session');
|
|
1146
|
+
this.use(session(opts));
|
|
1147
|
+
return this;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Create an OAuth2 client bound to this app.
|
|
1152
|
+
* Returns the client — does NOT mount any middleware automatically.
|
|
1153
|
+
*
|
|
1154
|
+
* @param {object} opts - OAuth options (see `oauth()` for full documentation).
|
|
1155
|
+
* @returns {{ authorize: Function, callback: Function, refresh: Function, userInfo: Function }}
|
|
1156
|
+
*
|
|
1157
|
+
* @example
|
|
1158
|
+
* const github = app.oauth({
|
|
1159
|
+
* provider: 'github',
|
|
1160
|
+
* clientId: process.env.GITHUB_CLIENT_ID,
|
|
1161
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
1162
|
+
* callbackUrl: '/auth/github/callback',
|
|
1163
|
+
* });
|
|
1164
|
+
*/
|
|
1165
|
+
oauth(opts)
|
|
1166
|
+
{
|
|
1167
|
+
const { oauth } = require('./auth/oauth');
|
|
1168
|
+
return oauth(opts);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
module.exports = App;
|