@sourceregistry/node-webserver 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,21 +1,37 @@
1
+ <div align="center">
2
+
1
3
  # @sourceregistry/node-webserver
2
4
 
3
- [![npm version](https://img.shields.io/npm/v/%40sourceregistry%2Fnode-webserver?logo=npm)](https://www.npmjs.com/package/@sourceregistry/node-webserver)
4
- [![JSR](https://jsr.io/badges/@sourceregistry/node-webserver)](https://jsr.io/@sourceregistry/node-webserver)
5
- [![License](https://img.shields.io/npm/l/%40sourceregistry%2Fnode-webserver)](./LICENSE)
6
- [![CI](https://github.com/SourceRegistry/node-webserver/actions/workflows/ci.yml/badge.svg)](https://github.com/SourceRegistry/node-webserver/actions/workflows/ci.yml)
5
+ **TypeScript web server for Node.js built on the web-standard `Request` and `Response` APIs**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@sourceregistry/node-webserver?style=flat-square&color=f96743)](https://www.npmjs.com/package/@sourceregistry/node-webserver)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@sourceregistry/node-webserver?style=flat-square)](https://www.npmjs.com/package/@sourceregistry/node-webserver)
9
+ [![JSR](https://jsr.io/badges/@sourceregistry/node-webserver?style=flat-square)](https://jsr.io/@sourceregistry/node-webserver)
10
+ [![license](https://img.shields.io/npm/l/@sourceregistry/node-webserver?style=flat-square)](./LICENSE)
11
+ [![node](https://img.shields.io/node/v/@sourceregistry/node-webserver?style=flat-square&color=339933&logo=node.js&logoColor=white)](https://nodejs.org)
12
+ [![CI](https://img.shields.io/github/actions/workflow/status/SourceRegistry/node-webserver/ci.yml?style=flat-square&label=CI)](https://github.com/SourceRegistry/node-webserver/actions/workflows/ci.yml)
13
+ [![issues](https://img.shields.io/github/issues/SourceRegistry/node-webserver?style=flat-square)](https://github.com/SourceRegistry/node-webserver/issues)
14
+
15
+ Typed router · Middleware · Route enhancers · WebSockets · SSE · Static files · CORS · Rate limiting · Security headers
16
+
17
+ [Docs](https://sourceregistry.github.io/node-webserver/) · [npm](https://www.npmjs.com/package/@sourceregistry/node-webserver) · [JSR](https://jsr.io/@sourceregistry/node-webserver) · [Issues](https://github.com/SourceRegistry/node-webserver/issues)
7
18
 
8
- TypeScript web server for Node.js built around the web platform `Request` and `Response` APIs.
19
+ </div>
9
20
 
10
- It provides:
21
+ ---
11
22
 
12
- - A typed router with path params
13
- - Middleware support
23
+ ## Features
24
+
25
+ - Typed router with path params and nested routers
26
+ - Middleware with short-circuit support
14
27
  - Route enhancers for typed request-scoped context
15
- - Router lifecycle hooks with `pre()` and `post()`
16
- - WebSocket routing
28
+ - Router lifecycle hooks `pre()` and `post()`
29
+ - WebSocket routing with enhancer support
30
+ - Server-Sent Events via `sse()`
17
31
  - Cookie helpers
18
- - Built-in middleware for CORS, rate limiting, security headers, request IDs, and timeouts
32
+ - Static file serving with path traversal protection
33
+ - Built-in middleware: CORS, rate limiting, security headers, request IDs, timeouts
34
+ - HTTPS support
19
35
  - Safer defaults for host handling and WebSocket upgrade validation
20
36
 
21
37
  ## Installation
@@ -34,16 +50,54 @@ import { WebServer, json, text } from "@sourceregistry/node-webserver";
34
50
  const app = new WebServer();
35
51
 
36
52
  app.GET("/", () => text("hello world"));
53
+ app.GET("/health", () => json({ ok: true }));
37
54
 
38
- app.GET("/health", () => json({
39
- ok: true
40
- }));
55
+ app.listen(3000, () => console.log("listening on http://127.0.0.1:3000"));
56
+ ```
57
+
58
+ ## Overview
41
59
 
42
- app.listen(3000, () => {
43
- console.log("listening on http://127.0.0.1:3000");
60
+ ```ts
61
+ import {
62
+ WebServer, Router, enhance, json, text, html, sse,
63
+ CORS, RateLimiter, RequestId, Security, Timeout,
64
+ error, redirect
65
+ } from "@sourceregistry/node-webserver";
66
+
67
+ const app = new WebServer({
68
+ locals: (event) => ({ requestId: crypto.randomUUID() }),
69
+ security: { trustedProxies: ["127.0.0.1"], maxRequestBodySize: 1024 * 1024 }
44
70
  });
71
+
72
+ // Built-in middleware
73
+ app.useMiddleware(RequestId.assign());
74
+ app.useMiddleware(Security.headers());
75
+ app.useMiddleware(CORS.policy({ origin: ["https://app.example.com"], credentials: true }));
76
+ app.useMiddleware(RateLimiter.slidingWindowLimit({ windowMs: 60_000, max: 100 }));
77
+ app.useMiddleware(Timeout.deadline({ ms: 5000 }));
78
+
79
+ // Typed route enhancers
80
+ const withAuth = async (event) => {
81
+ const token = event.request.headers.get("authorization");
82
+ if (!token) error(401, { message: "Unauthorized" });
83
+ return { user: await verifyToken(token) };
84
+ };
85
+
86
+ app.GET("/me", enhance(
87
+ async (event) => json(event.context.user),
88
+ withAuth,
89
+ ));
90
+
91
+ // Nested routers
92
+ const api = new Router();
93
+ api.GET("/status", () => json({ ok: true }));
94
+ app.use("/api", api);
95
+
96
+ app.listen(3000);
45
97
  ```
46
98
 
99
+ ---
100
+
47
101
  ## Core Concepts
48
102
 
49
103
  ### Create a server
@@ -413,6 +467,45 @@ app.GET("/admin", enhance(
413
467
  ));
414
468
  ```
415
469
 
470
+ ### Typed enhancers
471
+
472
+ You can define reusable enhancers with the `EventEnhancer` type. The fifth type parameter (`TExtra`) controls whether the enhancer receives extra event properties like `websocket`.
473
+
474
+ Without WebSocket — works in any HTTP route:
475
+
476
+ ```ts
477
+ import type { EventEnhancer } from "@sourceregistry/node-webserver";
478
+
479
+ const withAuth: EventEnhancer<any, any, App.Locals, { user: { id: string; role: string } }> = async (event) => {
480
+ const token = event.request.headers.get("authorization");
481
+ if (!token) error(401, { message: "Unauthorized" });
482
+ return { user: await verifyToken(token) };
483
+ };
484
+ ```
485
+
486
+ With WebSocket — only usable in `router.WS()` routes:
487
+
488
+ ```ts
489
+ import type { EventEnhancer } from "@sourceregistry/node-webserver";
490
+ import type { WebSocket } from "ws";
491
+
492
+ const withWsAuth: EventEnhancer<any, any, App.Locals, { user: { id: string } }, { websocket: WebSocket }> = async (event) => {
493
+ // event.websocket is available here
494
+ const token = event.request.headers.get("authorization");
495
+ if (!token) error(401, { message: "Unauthorized" });
496
+ return { user: await verifyToken(token) };
497
+ };
498
+
499
+ router.WS("/ws/chat", enhance(
500
+ async ({ context, websocket }) => {
501
+ websocket.send(`hello ${context.user.id}`);
502
+ },
503
+ withWsAuth
504
+ ));
505
+ ```
506
+
507
+ Plain HTTP enhancers (`TExtra = {}`) can still be passed to a WS `enhance()` call — they just won't have access to `websocket`.
508
+
416
509
  ## Router Lifecycle Hooks
417
510
 
418
511
  Use `pre()` for logic that should run before route resolution, and `post()` for logic that should run after a response has been produced.
@@ -486,6 +579,32 @@ app.WS("/ws/chat/[room]", async (event) => {
486
579
  });
487
580
  ```
488
581
 
582
+ `enhance()` works with WebSocket handlers too. When your handler includes `websocket` in the event type, the returned function requires it automatically:
583
+
584
+ ```ts
585
+ import { enhance, error } from "@sourceregistry/node-webserver";
586
+
587
+ app.WS("/ws/chat/[room]", enhance(
588
+ async ({ context, params, websocket }) => {
589
+ websocket.send(`joined:${params.room} as ${context.user.id}`);
590
+
591
+ websocket.on("message", (message) => {
592
+ websocket.send(`echo:${message.toString()}`);
593
+ });
594
+ },
595
+ async (event) => {
596
+ const token = event.request.headers.get("authorization");
597
+ if (!token) {
598
+ error(401, { message: "Unauthorized" });
599
+ }
600
+
601
+ return { user: { id: "u_1", role: "member" } };
602
+ }
603
+ ));
604
+ ```
605
+
606
+ If an enhancer returns a `Response` (or throws via `error()`), the WebSocket handler is skipped and the connection closes.
607
+
489
608
  ## Static Files
490
609
 
491
610
  Use `dir()` to expose a directory through a route, or `serveStatic()` directly if you want manual control.
@@ -657,13 +776,20 @@ For a production-oriented baseline with:
657
776
 
658
777
  see [examples/public-baseline.ts](./examples/public-baseline.ts).
659
778
 
779
+ ---
780
+
660
781
  ## Development
661
782
 
662
783
  ```bash
663
- npm test
784
+ npm test # run tests
785
+ npm run test:ui # vitest UI
786
+ npm run test:coverage
664
787
  npm run build
788
+ npm run docs:build # generate TypeDoc
665
789
  ```
666
790
 
791
+ ---
792
+
667
793
  ## License
668
794
 
669
- Apache-2.0. See [LICENSE](./LICENSE).
795
+ [Apache-2.0](./LICENSE) © [Alexander Slaa](https://github.com/SourceRegistry)
package/dist/enhance.d.ts CHANGED
@@ -3,7 +3,7 @@ import { MaybePromise } from './types/MaybePromise';
3
3
  import { WebSocket } from 'ws';
4
4
  type AnyFn = (...args: any[]) => any;
5
5
  type ConcatReturnTypes<T extends AnyFn[]> = T extends [] ? {} : T extends [infer First, ...infer Rest] ? First extends AnyFn ? Awaited<ReturnType<First>> & ConcatReturnTypes<Rest extends AnyFn[] ? Rest : []> : {} : {};
6
- export type EventEnhancer<Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, RouteId extends string | null = string | null, Locals extends App.Locals = App.Locals, Context extends Record<string, any> = Record<string, any>> = (event: RequestEvent<Params, RouteId, Locals>) => MaybePromise<Context | void | undefined | Response>;
6
+ export type EventEnhancer<Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, RouteId extends string | null = string | null, Locals extends App.Locals = App.Locals, Context extends Record<string, any> = Record<string, any>, TExtra extends object = {}> = (event: RequestEvent<Params, RouteId, Locals> & TExtra) => MaybePromise<Context | void | undefined | Response>;
7
7
  export type EnhancedRequestEvent<Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, RouteId extends string | null = string | null, Locals extends App.Locals = App.Locals, Context extends Record<string, any> = Record<string, any>> = RequestEvent<Params, RouteId, Locals> & {
8
8
  context: Context;
9
9
  };
@@ -11,5 +11,5 @@ export type EnhancedRouteHandler<Params extends Partial<Record<string, string>>
11
11
  export type EnhancedWebSocketHandler<Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, RouteId extends string | null = string | null, Locals extends App.Locals = App.Locals, Context extends Record<string, any> = Record<string, any>> = (event: EnhancedRequestEvent<Params, RouteId, Locals, Context> & {
12
12
  websocket: WebSocket;
13
13
  }) => MaybePromise<any>;
14
- export declare const enhance: <Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, RouteId extends string | null = string | null, Locals extends App.Locals = App.Locals, Enhancers extends EventEnhancer<Params, RouteId, Locals, any>[] = EventEnhancer<Params, RouteId, Locals, any>[], Context extends Awaited<ConcatReturnTypes<Enhancers>> = Awaited<ConcatReturnTypes<Enhancers>>, TExtra extends object = {}, TReturn = unknown>(handler: (event: EnhancedRequestEvent<Params, RouteId, Locals, Context> & TExtra) => MaybePromise<TReturn>, ...enhancers: Enhancers) => (event: RequestEvent<Params, RouteId, Locals> & TExtra) => Promise<TReturn | Response>;
14
+ export declare const enhance: <Params extends Partial<Record<string, string>> = Partial<Record<string, string>>, RouteId extends string | null = string | null, Locals extends App.Locals = App.Locals, TExtra extends object = {}, TReturn = unknown, Enhancers extends EventEnhancer<Params, RouteId, Locals, any, TExtra>[] = EventEnhancer<Params, RouteId, Locals, any, TExtra>[], Context extends Awaited<ConcatReturnTypes<Enhancers>> = Awaited<ConcatReturnTypes<Enhancers>>>(handler: (event: EnhancedRequestEvent<Params, RouteId, Locals, Context> & TExtra) => MaybePromise<TReturn>, ...enhancers: Enhancers) => (event: RequestEvent<Params, RouteId, Locals> & TExtra) => Promise<TReturn | Response>;
15
15
  export {};