@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 +144 -18
- package/dist/enhance.d.ts +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# @sourceregistry/node-webserver
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@sourceregistry/node-webserver)
|
|
8
|
+
[](https://www.npmjs.com/package/@sourceregistry/node-webserver)
|
|
9
|
+
[](https://jsr.io/@sourceregistry/node-webserver)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](https://github.com/SourceRegistry/node-webserver/actions/workflows/ci.yml)
|
|
13
|
+
[](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
|
-
|
|
19
|
+
</div>
|
|
9
20
|
|
|
10
|
-
|
|
21
|
+
---
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
-
|
|
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.
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
app.listen(3000, () => console.log("listening on http://127.0.0.1:3000"));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Overview
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {};
|