barehttp 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +185 -28
  2. package/lib/context/execution.d.ts +7 -0
  3. package/lib/context/execution.js +14 -0
  4. package/lib/context/index.d.ts +10 -0
  5. package/lib/context/index.js +46 -0
  6. package/lib/env.d.ts +5 -0
  7. package/lib/env.js +5 -0
  8. package/lib/index.d.ts +5 -0
  9. package/lib/index.js +3 -0
  10. package/lib/logger/index.d.ts +16 -0
  11. package/lib/logger/index.js +26 -0
  12. package/lib/logger/serializers.d.ts +28 -0
  13. package/lib/logger/serializers.js +78 -0
  14. package/lib/middlewares/cookies/cookie-manager.d.ts +25 -0
  15. package/lib/middlewares/cookies/cookie-manager.js +68 -0
  16. package/lib/middlewares/cookies/signer.d.ts +8 -0
  17. package/lib/middlewares/cookies/signer.js +25 -0
  18. package/lib/middlewares/cors/cors.d.ts +38 -0
  19. package/lib/middlewares/cors/cors.js +164 -0
  20. package/lib/request.d.ts +84 -0
  21. package/lib/request.js +260 -0
  22. package/lib/schemas/custom-schema.d.ts +32 -0
  23. package/lib/schemas/custom-schema.js +62 -0
  24. package/lib/schemas/dirty-tsm.d.ts +1 -0
  25. package/lib/schemas/dirty-tsm.js +199 -0
  26. package/lib/schemas/generator.d.ts +7 -0
  27. package/lib/schemas/generator.js +179 -0
  28. package/lib/schemas/helpers.d.ts +27 -0
  29. package/lib/schemas/helpers.js +40 -0
  30. package/lib/schemas/json-schema.d.ts +2 -0
  31. package/lib/schemas/json-schema.js +48 -0
  32. package/lib/schemas/openami-schema.d.ts +2 -0
  33. package/lib/schemas/openami-schema.js +59 -0
  34. package/lib/schemas/project.d.ts +1 -0
  35. package/lib/schemas/project.js +1 -0
  36. package/lib/server.d.ts +154 -0
  37. package/lib/server.js +396 -0
  38. package/lib/utils/content-type.d.ts +54 -0
  39. package/lib/utils/content-type.js +54 -0
  40. package/lib/utils/http-methods.d.ts +11 -0
  41. package/lib/utils/http-methods.js +9 -0
  42. package/lib/utils/index.d.ts +4 -0
  43. package/lib/utils/index.js +4 -0
  44. package/lib/utils/safe-json.d.ts +2 -0
  45. package/lib/utils/safe-json.js +18 -0
  46. package/lib/utils/status-codes.d.ts +339 -0
  47. package/lib/utils/status-codes.js +339 -0
  48. package/lib/utils/status-phrases.d.ts +338 -0
  49. package/lib/utils/status-phrases.js +339 -0
  50. package/lib/websocket.d.ts +36 -0
  51. package/lib/websocket.js +176 -0
  52. package/package.json +65 -33
  53. package/.eslintrc.js +0 -47
  54. package/.github/workflows/release.yml +0 -27
  55. package/.jest-setup.js +0 -1
  56. package/jest.config.js +0 -8
  57. package/prettier.config.js +0 -6
  58. package/src/context/context.test.ts +0 -30
  59. package/src/context/execution.ts +0 -17
  60. package/src/context/index.ts +0 -61
  61. package/src/env.ts +0 -5
  62. package/src/examples/bare-http.ts +0 -36
  63. package/src/examples/express.ts +0 -11
  64. package/src/examples/fastify.ts +0 -18
  65. package/src/index.ts +0 -4
  66. package/src/logger/index.ts +0 -67
  67. package/src/logger/serializers.test.ts +0 -186
  68. package/src/logger/serializers.ts +0 -109
  69. package/src/middlewares/cookies/cookie-manager.ts +0 -86
  70. package/src/middlewares/cookies/signer.ts +0 -30
  71. package/src/report.ts +0 -25
  72. package/src/request.test.ts +0 -143
  73. package/src/request.ts +0 -277
  74. package/src/server.integration.test.ts +0 -296
  75. package/src/server.middlewares.test.ts +0 -93
  76. package/src/server.routes.test.ts +0 -71
  77. package/src/server.ts +0 -450
  78. package/src/utils/content-type.ts +0 -59
  79. package/src/utils/index.ts +0 -2
  80. package/src/utils/safe-json.ts +0 -17
  81. package/src/utils/status-codes.ts +0 -339
  82. package/src/utils/status-phrases.ts +0 -339
  83. package/tsconfig.json +0 -24
package/src/server.ts DELETED
@@ -1,450 +0,0 @@
1
- import Router from 'find-my-way';
2
-
3
- import { BareRequest, CacheOpts } from './request';
4
- import { logMe } from './logger';
5
- import { context, enableContext, newContext } from './context';
6
- import { generateReport } from './report';
7
- import { CookieManagerOptions } from './middlewares/cookies/cookie-manager';
8
- import { StatusCodes } from './utils';
9
-
10
- import dns from 'dns';
11
- import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
12
- import { Writable } from 'stream';
13
-
14
- type Middleware = (flow: BareRequest) => Promise<void> | void;
15
- type Handler = (flow: BareRequest) => any;
16
-
17
- type RouteOpts<C> = {
18
- disableCache?: C extends true ? C : undefined;
19
- cache?: C extends true ? undefined : CacheOpts;
20
- /**
21
- * Request timeout handler in `ms`
22
- */
23
- timeout?: number;
24
- };
25
- interface HandlerExposed {
26
- <R extends `/${string}`, C>(setUp: {
27
- route: R;
28
- options?: RouteOpts<C>;
29
- handler: Handler;
30
- }): BareServer<any>;
31
- }
32
-
33
- type ErrorHandler = (
34
- err: any,
35
- flow: BareRequest,
36
- status?: typeof StatusCodes[keyof typeof StatusCodes],
37
- ) => void;
38
-
39
- type BareOptions<A extends `${number}.${number}.${number}.${number}`> = {
40
- middlewares?: Array<Middleware>;
41
- serverPort?: number;
42
- /**
43
- * Address to bind the web server to
44
- * Default '0.0.0.0'
45
- */
46
- serverAddress?: A | 'localhost';
47
- /**
48
- * Enable request context storage
49
- * Default `false`
50
- */
51
- context?: boolean;
52
- /**
53
- * Enable request/response predefined logging
54
- * Default `false`
55
- */
56
- logging?: boolean;
57
- errorHandlerMiddleware?: ErrorHandler;
58
- /**
59
- * Request time format in `seconds` or `milliseconds`
60
- * Default 's' - seconds
61
- */
62
- requestTimeFormat?: 's' | 'ms';
63
- /**
64
- * Control over cookies.
65
- * This will enable automatic cookies decoding
66
- */
67
- cookies?: boolean;
68
- cookiesOptions?: CookieManagerOptions;
69
- /**
70
- * Log the resolved reverse DNS first hop for remote ip of the client (first proxy)
71
- */
72
- reverseDns?: boolean;
73
- /**
74
- * Exposes a report with the routes usage.
75
- * Default `false`
76
- */
77
- statisticReport?: boolean;
78
- };
79
-
80
- type Methods = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
81
- const HttpMethods = {
82
- get: 'GET',
83
- post: 'POST',
84
- put: 'PUT',
85
- delete: 'DELETE',
86
- patch: 'PATCH',
87
- options: 'OPTIONS',
88
- head: 'HEAD',
89
- } as const;
90
-
91
- export type RouteReport = { hits: number; success: number; fails: number };
92
-
93
- export class BareServer<A extends `${number}.${number}.${number}.${number}`> {
94
- server: Server;
95
- #middlewares: Array<Middleware> = [];
96
- #routes: Map<string, RouteReport> = new Map();
97
- #router = Router({ ignoreTrailingSlash: true });
98
- #flows: Map<string, BareRequest> = new Map();
99
- #errorHandler: ErrorHandler;
100
-
101
- #runMiddlewaresSequence: (flow: BareRequest) => void = (_) => _;
102
-
103
- constructor(private bareOptions: BareOptions<A> = {}) {
104
- // init
105
- this.server = createServer(this.#listener.bind(this));
106
- this.attachGracefulHandlers();
107
-
108
- // context setting
109
- if (bareOptions.context) enableContext();
110
-
111
- // middlewares settings
112
- this.#errorHandler = bareOptions?.errorHandlerMiddleware || this.basicErrorHandler;
113
- this.#middlewares.push(...(bareOptions?.middlewares || []));
114
- if (bareOptions.statisticReport) this.registerReport();
115
-
116
- return this;
117
- }
118
-
119
- #listener = (request: IncomingMessage, response: ServerResponse) => {
120
- const { requestTimeFormat, logging } = this.bareOptions;
121
-
122
- const flow = new BareRequest(request, response, logging);
123
-
124
- // init and attach request uuid to the context
125
- if (this.bareOptions.context) {
126
- newContext('request');
127
- context.current?.store.set('id', flow.uuid);
128
- }
129
-
130
- if (requestTimeFormat) flow['setTimeFormat'](requestTimeFormat);
131
-
132
- // listener to remove already finished flow from the memory storage
133
- request.on('close', () => this.#flows.delete(flow.uuid));
134
-
135
- // attach a flow to the flow memory storage
136
- this.#flows.set(flow.uuid, flow);
137
- this.applyMiddlewares(flow.uuid).catch((e) => this.#errorHandler(e, flow, 400));
138
- };
139
-
140
- /**
141
- * This function generates previously defined middlewares for the sequential execution
142
- */
143
- #writeMiddlewares = () => {
144
- const lines: string[] = [];
145
- let order = 0;
146
- const maxOrder = this.#middlewares.length;
147
-
148
- const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
149
-
150
- if (maxOrder > 0) {
151
- while (order <= maxOrder - 1) {
152
- lines.push(`if (flow.sent) return;`);
153
- lines.push(`await this.resolveMiddleware(${order}, flow);`);
154
- order++;
155
- }
156
- }
157
-
158
- const text = lines.join('\n');
159
-
160
- this.#runMiddlewaresSequence = new AsyncFunction('flow', text);
161
- };
162
-
163
- private async applyMiddlewares(flowId: string) {
164
- const flow = this.#flows.get(flowId);
165
- if (!flow) {
166
- throw new Error(`No flow been found for id ${flowId}, theres a sync mistake in the server.`); // should NEVER happen
167
- }
168
-
169
- // invoke body stream consumption
170
- await flow['readBody']();
171
-
172
- // attach cookies middleware
173
- if (this.bareOptions.cookies) {
174
- flow['attachCookieManager'](this.bareOptions.cookiesOptions);
175
- flow['populateCookies']();
176
- }
177
-
178
- // to test in cloud provider
179
- // this should resolve the name of the first hop from the dns chain
180
- if (this.bareOptions.reverseDns) {
181
- const remoteClient = await dns.promises.reverse(flow.remoteIp!);
182
- flow['setRemoteClient'](remoteClient[0]);
183
- }
184
-
185
- if (this.#middlewares.length) await this.#runMiddlewaresSequence(flow);
186
-
187
- // now route the request if middlewares did not send the response back
188
- if (!flow.sent) {
189
- this.#router.lookup(flow._originalRequest, flow._originalResponse);
190
- }
191
- }
192
-
193
- /**
194
- * This handler is used in async generated middlewares runtime function
195
- */
196
- private async resolveMiddleware(order: number, flow: BareRequest) {
197
- try {
198
- const response = this.#middlewares[order](flow);
199
- if (response instanceof Promise) await response;
200
- } catch (e) {
201
- this.#errorHandler(e, flow);
202
- }
203
- }
204
-
205
- private setRoute(method: Methods, route: string, handler: Handler, opts?: RouteOpts<any>) {
206
- const encode = this.encodeRoute(method, route);
207
- this.#routes.set(encode, { hits: 0, fails: 0, success: 0 });
208
-
209
- this.#router.on(method, route, (req, _, routeParams) => {
210
- this.#routes.get(encode)!.hits++;
211
- this.handleRoute(req, checkParams(routeParams), handler, encode, opts);
212
- });
213
- }
214
-
215
- private registerReport() {
216
- this.setRoute('GET', '/_report', (flow) => {
217
- flow.setHeader('content-type', 'text/html');
218
- flow.send(generateReport(this.#routes));
219
- });
220
- }
221
-
222
- private handleRoute(
223
- req: IncomingMessage,
224
- routeParams: { [k: string]: string | undefined },
225
- handle: Handler,
226
- encodedRoute: string,
227
- opts?: RouteOpts<any>,
228
- ) {
229
- const flow = this.#flows.get((req as any).id)!;
230
-
231
- // apply possible route options
232
- if (opts?.disableCache) flow.disableCache();
233
- if (opts?.cache) flow.setCache(opts.cache);
234
- if (opts?.timeout) flow['attachTimeout'](opts.timeout);
235
-
236
- // populate with route params
237
- if (routeParams) flow['setParams'](routeParams);
238
-
239
- // attach a general statistic reports counter
240
- if (this.bareOptions.statisticReport) {
241
- flow._originalRequest.on('close', () => {
242
- if (flow.statusToSend < 300 && flow.statusToSend >= 200) {
243
- this.#routes.get(encodedRoute)!.success++;
244
- } else {
245
- this.#routes.get(encodedRoute)!.fails++;
246
- }
247
- });
248
- }
249
-
250
- try {
251
- const routeReturn = handle.bind(undefined)(flow);
252
- if (routeReturn instanceof Promise) {
253
- routeReturn
254
- .catch((e) => this.#errorHandler(e, flow))
255
- .then((result) => this.soundRouteReturn(result, flow));
256
- } else {
257
- this.soundRouteReturn(routeReturn, flow);
258
- }
259
- } catch (e) {
260
- this.#errorHandler(e, flow);
261
- }
262
- }
263
-
264
- private soundRouteReturn(response: any, flow: BareRequest) {
265
- if (flow.sent) return;
266
- if (!response) flow.send();
267
-
268
- switch (response.constructor) {
269
- case Uint8Array:
270
- case Uint16Array:
271
- case Uint32Array:
272
- case Buffer:
273
- case String:
274
- flow.send(response);
275
- break;
276
- case Boolean:
277
- case Number:
278
- flow.send('' + response);
279
- case Writable:
280
- flow.stream(response);
281
- break;
282
- case Object:
283
- flow.json(response);
284
- break;
285
- default:
286
- logMe.warn('Unknown type to send');
287
- }
288
- }
289
-
290
- private encodeRoute(method: string, route: string) {
291
- return `${method} ${route}`;
292
- }
293
-
294
- private basicErrorHandler(
295
- e: any,
296
- flow: BareRequest,
297
- status?: typeof StatusCodes[keyof typeof StatusCodes],
298
- ) {
299
- flow.status(status ?? 500);
300
- flow.json({ ...e, message: e.message, stack: e.stack });
301
- }
302
-
303
- private attachGracefulHandlers() {
304
- const graceful = async (code = 0) => {
305
- await this.stop();
306
- process.exit(code);
307
- };
308
-
309
- // Stop graceful
310
- process.on('uncaughtException', (err) => {
311
- console.error(err);
312
- graceful(1);
313
- });
314
-
315
- process.on('unhandledRejection', (err) => {
316
- console.error(err);
317
- });
318
-
319
- process.on('SIGTERM', graceful);
320
- process.on('SIGINT', graceful);
321
- }
322
-
323
- // ========= PUBLIC APIS ==========
324
-
325
- start(cb?: (address: string) => void) {
326
- this.#writeMiddlewares();
327
-
328
- const port = this.bareOptions?.serverPort || process.env.PORT || 3000;
329
- const address = this.bareOptions?.serverAddress || '0.0.0.0';
330
-
331
- // https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
332
- return new Promise<void>((res) =>
333
- this.server.listen(+port, address, undefined, () => {
334
- cb ? cb(`http://0.0.0.0:${port}`) : void 0;
335
- res();
336
- }),
337
- );
338
- }
339
-
340
- stop(cb?: (e?: Error) => void) {
341
- for (const flow of this.#flows.values()) {
342
- if (!flow.sent) {
343
- flow.status(500);
344
- flow.send('Server terminated');
345
- }
346
- }
347
- return new Promise<void>((res, rej) => {
348
- this.server?.close((e) => {
349
- if (e) {
350
- rej(e);
351
- cb?.(e);
352
- } else {
353
- cb?.();
354
- res();
355
- }
356
- });
357
- });
358
- }
359
-
360
- use(middleware: Middleware) {
361
- this.#middlewares.push(middleware);
362
- return this;
363
- }
364
-
365
- getMiddlewares(): Middleware[] {
366
- return this.#middlewares;
367
- }
368
-
369
- setCustomErrorHandler(eh: ErrorHandler) {
370
- this.#errorHandler = eh;
371
- }
372
-
373
- getRoutes() {
374
- return [...this.#routes.keys()];
375
- }
376
-
377
- get route() {
378
- // eslint-disable-next-line @typescript-eslint/no-this-alias
379
- const self = this;
380
- return new Proxy(
381
- {},
382
- {
383
- get(_, key) {
384
- if (typeof key === 'symbol') return self;
385
-
386
- if (Object.keys(HttpMethods).includes(key as string)) {
387
- return function (routeSetUp: any) {
388
- checkRouteSetUp(routeSetUp, key);
389
- self.setRoute(
390
- HttpMethods[key],
391
- routeSetUp.route,
392
- routeSetUp.handler,
393
- routeSetUp.options,
394
- );
395
- return self;
396
- };
397
- }
398
-
399
- return self;
400
- },
401
- },
402
- ) as Readonly<
403
- {
404
- [K in keyof typeof HttpMethods]: HandlerExposed;
405
- }
406
- >;
407
- }
408
- }
409
-
410
- function checkRouteSetUp(routeSetUp: { [setting: string]: any }, key: string) {
411
- if (typeof routeSetUp.route !== 'string') {
412
- throw new TypeError(`A route path for the method ${key} is not a a string`);
413
- } else if (routeSetUp.route[0] !== '/') {
414
- throw new SyntaxError(
415
- `A route path should start with '/' for route ${routeSetUp.route} for method ${key}`,
416
- );
417
- } else if (routeSetUp.route[1] === '/') {
418
- throw new SyntaxError(
419
- `Declared route ${routeSetUp.route} for method ${key} is not correct, review the syntax`,
420
- );
421
- } else if (typeof routeSetUp.handler !== 'function') {
422
- throw new TypeError(
423
- `Handler for the route ${routeSetUp.route} for method ${key} is not a function`,
424
- );
425
- } else if (
426
- routeSetUp.options?.timeout &&
427
- typeof routeSetUp.options.timeout !== 'number' &&
428
- !Number.isFinite(routeSetUp.options.timeout)
429
- ) {
430
- throw new TypeError(
431
- `Only numeric values are valid per-route timeout, submitted ${routeSetUp.options.timeout}`,
432
- );
433
- }
434
- }
435
-
436
- function checkParams(params: { [param: string]: string | undefined }) {
437
- if (!params || Object.keys(params).length === 0) return params;
438
- for (const [param, value] of Object.entries(params)) {
439
- if (value === undefined) continue;
440
-
441
- if (/(\.\/)(\.\.)(\\.)/.test(decodeURI(value))) {
442
- logMe.warn(
443
- `Param ${param} value ${value} was redacted because contained dangerous characters`,
444
- );
445
- param[param] = 'REDACTED';
446
- }
447
- }
448
-
449
- return params;
450
- }
@@ -1,59 +0,0 @@
1
- export const ContentType = {
2
- 'application/EDI-X12': 'application/EDI-X12',
3
- 'application/EDIFACT': 'application/EDIFACT',
4
- 'application/javascript': 'application/javascript',
5
- 'application/octet-stream': 'application/octet-stream',
6
- 'application/ogg': 'application/ogg',
7
- 'application/pdf': 'application/pdf',
8
- 'application/xhtml+xml': 'application/xhtml+xml',
9
- 'application/x-shockwave-flash': 'application/x-shockwave-flash',
10
- 'application/json': 'application/json',
11
- 'application/ld+json': 'application/ld+json',
12
- 'application/xml': 'application/xml',
13
- 'application/zip': 'application/zip',
14
- 'application/x-www-form-urlencoded': 'application/x-www-form-urlencoded',
15
- 'audio/mpeg': 'audio/mpeg',
16
- 'audio/x-ms-wma': 'audio/x-ms-wma',
17
- 'audio/vnd.rn-realaudio': 'audio/vnd.rn-realaudio',
18
- 'audio/x-wav': 'audio/x-wav',
19
- 'image/gif': 'image/gif',
20
- 'image/jpeg': 'image/jpeg',
21
- 'image/png': 'image/png',
22
- 'image/tiff': 'image/tiff',
23
- 'image/vnd.microsoft.icon': 'image/vnd.microsoft.icon',
24
- 'image/x-icon': 'image/x-icon',
25
- 'image/vnd.djvu': 'image/vnd.djvu',
26
- 'image/svg+xml': 'image/svg+xml',
27
- 'multipart/mixed': 'multipart/mixed',
28
- 'multipart/alternative': 'multipart/alternative',
29
- 'multipart/related': 'multipart/related', // (using by MHTML (HTML mail).)
30
- 'multipart/form-data': 'multipart/form-data',
31
- 'text/css': 'text/css',
32
- 'text/csv': 'text/csv',
33
- 'text/html': 'text/html',
34
- 'text/plain': 'text/plain',
35
- 'text/xml': 'text/xml',
36
- 'video/mpeg': 'video/mpeg',
37
- 'video/mp4': 'video/mp4',
38
- 'video/quicktime': 'video/quicktime',
39
- 'video/x-ms-wmv': 'video/x-ms-wmv',
40
- 'video/x-msvideo': 'video/x-msvideo',
41
- 'video/x-flv': 'video/x-flv',
42
- 'video/webm': 'video/webm',
43
- 'application/vnd.oasis.opendocument.text': 'application/vnd.oasis.opendocument.text',
44
- 'application/vnd.oasis.opendocument.spreadsheet':
45
- 'application/vnd.oasis.opendocument.spreadsheet',
46
- 'application/vnd.oasis.opendocument.presentation':
47
- 'application/vnd.oasis.opendocument.presentation',
48
- 'application/vnd.oasis.opendocument.graphics': 'application/vnd.oasis.opendocument.graphics',
49
- 'application/vnd.ms-excel': 'application/vnd.ms-excel',
50
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
51
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
52
- 'application/vnd.ms-powerpoint': 'application/vnd.ms-powerpoint',
53
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
54
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
55
- 'application/msword': 'application/msword',
56
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
57
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
58
- 'application/vnd.mozilla.xul+xml': 'application/vnd.mozilla.xul+xml',
59
- } as const;
@@ -1,2 +0,0 @@
1
- export { StatusPhrases } from './status-phrases';
2
- export { StatusCodes } from './status-codes';
@@ -1,17 +0,0 @@
1
- export const JSONStringify = (data: any) => {
2
- try {
3
- return JSON.stringify(data);
4
- } catch (e) {
5
- console.log('Error stringifying, data not serializable', e);
6
- return null;
7
- }
8
- };
9
-
10
- export const JSONParse = (data: any) => {
11
- try {
12
- return JSON.parse(data);
13
- } catch (e) {
14
- console.log('Error parsing, data not deserializable', e);
15
- return e;
16
- }
17
- };