@thi.ng/server 0.1.0 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2025-01-29T16:25:48Z
3
+ - **Last updated**: 2025-02-02T22:46:17Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -11,6 +11,29 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
11
11
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
12
12
  and/or version bumps of transitive dependencies.
13
13
 
14
+ ## [0.3.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.3.0) (2025-02-02)
15
+
16
+ #### 🚀 Features
17
+
18
+ - add more HTTP error response methods ([5731ff3](https://github.com/thi-ng/umbrella/commit/5731ff3))
19
+ - add ServerResponse, IPv6 support ([22f64c5](https://github.com/thi-ng/umbrella/commit/22f64c5))
20
+
21
+ ## [0.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.2.0) (2025-01-30)
22
+
23
+ #### 🚀 Features
24
+
25
+ - add generics, various other updates ([a340f65](https://github.com/thi-ng/umbrella/commit/a340f65))
26
+ - add generics to most main types/interfaces
27
+ - refactor `SessionInterceptor` as class w/ pluggable storage
28
+ - add `ISessionStore` and `InMemorySessionStore` impl
29
+ - update ServerOpts to allow augmenting request context object
30
+ - add default HTTP OPTIONS handler
31
+ - update Server cookie parsing
32
+ - add StaticOpts.auth flag
33
+ - update logRequest() interceptor
34
+ - update pkg exports
35
+ - update tests
36
+
14
37
  ## [0.1.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.1.0) (2025-01-29)
15
38
 
16
39
  #### 🚀 Features
package/README.md CHANGED
@@ -39,38 +39,47 @@ implementations.
39
39
  ### Main features
40
40
 
41
41
  - Declarative & parametric routing (incl. validation and coercion of route
42
- params)
42
+ params)
43
+ - Uses [@thi.ng/router](https://github.com/thi-ng/umbrella/tree/develop/packages/router) as implementation
43
44
  - Multiple HTTP methods per route
44
- - Composable & customizable interceptors (global for all routes and/or for
45
- indidual routes & HTTP methods)
45
+ - Built-in HTTP OPTIONS handler for listing available route methods
46
+ - Fallback HTTP HEAD to GET method (if available)
46
47
  - Asynchronous route handler processing
48
+ - Composable & customizable interceptor chains
49
+ - Global interceptors for all routes and/or local for individual routes & HTTP methods
47
50
  - Automatic parsing of cookies and URL query strings (incl. nested params)
51
+ - In-memory session storage & route interceptor
48
52
  - Configurable file serving (`ReadableStream`-based) with automatic MIME-type
49
- detection and support for Etags, as well as Brotli, Gzip and Deflate compression
53
+ detection and support for Etags, as well as Brotli, Gzip and Deflate
54
+ compression
50
55
  - Utilities for parsing form-encoded multipart request bodies
51
56
 
52
57
  ### Interceptors
53
58
 
54
- Interceptors are additionally injected handlers (aka middleware) which are
59
+ Interceptors are additionally injected route handlers (aka middleware) which are
55
60
  pre/post-processed before/after a route's main handler and can be used for
56
61
  validation, cancellation or other side effects. Each single interceptor can have
57
- a `pre` and/or `post` phase function. Post-phase interceptors are processed in
58
- reverse order. See
62
+ a `pre` and/or `post` phase function. Each route handler can define its own
63
+ interceptor chains, which will be appended to the globally defined interceptors
64
+ (applied to all routes). Post-phase interceptors are processed in reverse order.
65
+ See
59
66
  [`Interceptor`](https://docs.thi.ng/umbrella/server/interfaces/Interceptor.html)
60
67
  for more details.
61
68
 
69
+ ![Diagram illustrating interceptor processing order](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/server/server-interceptors.png)
70
+
62
71
  #### Available interceptors
63
72
 
64
- - [`authenticateWith()`](https://docs.thi.ng/umbrella/server/functions/authenticateWith.html)
65
- - [`cacheControl()`](https://docs.thi.ng/umbrella/server/functions/cacheControl.html)
66
- - [`crossOriginOpenerPolicy()`](https://docs.thi.ng/umbrella/server/functions/crossOriginOpenerPolicy.html)
67
- - [`crossOriginResourcePolicy()`](https://docs.thi.ng/umbrella/server/functions/crossOriginResourcePolicy.html)
68
- - [`injectHeaders()`](https://docs.thi.ng/umbrella/server/functions/injectHeaders.html)
69
- - [`logRequest()`](https://docs.thi.ng/umbrella/server/functions/logRequest.html)
70
- - [`logResponse()`](https://docs.thi.ng/umbrella/server/functions/logResponse.html)
71
- - [`referrerPolicy()`](https://docs.thi.ng/umbrella/server/functions/referrerPolicy.html)
72
- - [`serverSession()`](https://docs.thi.ng/umbrella/server/functions/serverSession.html)
73
- - [`strictTransportSecurity()`](https://docs.thi.ng/umbrella/server/functions/strictTransportSecurity.html)
73
+ - [`authenticateWith()`](https://docs.thi.ng/umbrella/server/functions/authenticateWith.html): Predicate function based authentication
74
+ - [`cacheControl()`](https://docs.thi.ng/umbrella/server/functions/cacheControl.html): Cache control header injection
75
+ - [`crossOriginOpenerPolicy()`](https://docs.thi.ng/umbrella/server/functions/crossOriginOpenerPolicy-1.html): Policy header injection
76
+ - [`crossOriginResourcePolicy()`](https://docs.thi.ng/umbrella/server/functions/crossOriginResourcePolicy-1.html): Policy header injection
77
+ - [`injectHeaders()`](https://docs.thi.ng/umbrella/server/functions/injectHeaders.html): Arbitrary header injection
78
+ - [`logRequest()`](https://docs.thi.ng/umbrella/server/functions/logRequest.html): Request detail logging
79
+ - [`logResponse()`](https://docs.thi.ng/umbrella/server/functions/logResponse.html): Response logging
80
+ - [`referrerPolicy()`](https://docs.thi.ng/umbrella/server/functions/referrerPolicy-1.html): Policy header injection
81
+ - [`serverSession()`](https://docs.thi.ng/umbrella/server/functions/serverSession-1.html): User defined in-memory sessions with TTL
82
+ - [`strictTransportSecurity()`](https://docs.thi.ng/umbrella/server/functions/strictTransportSecurity.html): Policy header injection
74
83
 
75
84
  #### Custom interceptors
76
85
 
@@ -138,7 +147,7 @@ For Node.js REPL:
138
147
  const ser = await import("@thi.ng/server");
139
148
  ```
140
149
 
141
- Package sizes (brotli'd, pre-treeshake): ESM: 3.75 KB
150
+ Package sizes (brotli'd, pre-treeshake): ESM: 4.02 KB
142
151
 
143
152
  ## Dependencies
144
153
 
@@ -164,34 +173,96 @@ Note: @thi.ng/api is in _most_ cases a type-only import (not used at runtime)
164
173
  ### Usage example
165
174
 
166
175
  ```ts tangle:export/readme-hello.ts
167
- import {
168
- server, staticFiles, cacheControl, etagFileHash, logRequest
169
- } from "@thi.ng/server";
176
+ import * as srv from "@thi.ng/server";
177
+
178
+ // all route handlers & interceptors receive a request context object
179
+ // here we define an extended/customized version
180
+ interface AppCtx extends srv.RequestCtx {
181
+ session?: AppSession;
182
+ }
183
+
184
+ // customized version of the default server session type
185
+ interface AppSession extends srv.ServerSession {
186
+ user?: string;
187
+ locale?: string;
188
+ }
189
+
190
+ // interceptor for injecting/managing sessions
191
+ // by default uses in-memory storage/cache
192
+ const session = srv.serverSession<AppCtx, AppSession>();
170
193
 
171
- const app = server({
194
+ // create server with given config
195
+ const app = srv.server<AppCtx>({
196
+ // global interceptors (used for all routes)
197
+ intercept: [
198
+ // log all requests (using server's configured logger)
199
+ srv.logRequest(),
200
+ // lookup/create sessions (using above interceptor)
201
+ session,
202
+ // ensure routes with `auth` flag have a logged-in user
203
+ srv.authenticateWith<AppCtx>((ctx) => !!ctx.session?.user),
204
+ ],
205
+ // route definitions (more can be added dynamically later)
172
206
  routes: [
173
- // define a route for static files
174
- staticFiles({
207
+ // define a route for serving static assets
208
+ srv.staticFiles({
209
+ // ensure only logged-in users can access
210
+ auth: true,
211
+ // use compression (if client supports it)
175
212
  compress: true,
176
- etag: etagFileHash(),
213
+ // route prefix
214
+ prefix: "assets",
215
+ // map to current CWD
216
+ rootDir: ".",
217
+ // strategy for computing etags (optional)
218
+ etag: srv.etagFileHash(),
177
219
  // route specific interceptors
178
- intercept: [
179
- cacheControl({ maxAge: 3600 })
180
- ]
220
+ intercept: [srv.cacheControl({ maxAge: 3600 })],
181
221
  }),
182
- // parametric route
222
+ // define a dummy login route
223
+ {
224
+ id: "login",
225
+ match: "/login",
226
+ handlers: {
227
+ // each route can specify handlers for various HTTP methods
228
+ post: async (ctx) => {
229
+ const { user, pass } = await srv.parseRequestFormData(ctx.req);
230
+ ctx.logger.info("login details", user, pass);
231
+ if (user === "thi.ng" && pass === "1234") {
232
+ ctx.session!.user = user;
233
+ ctx.res.writeHead(200).end("logged in as " + user);
234
+ } else {
235
+ ctx.res.writeHead(403).end("login failed");
236
+ }
237
+ },
238
+ },
239
+ },
240
+ // dummy logout route
241
+ {
242
+ id: "logout",
243
+ match: "/logout",
244
+ // use auth flag here to ensure route is only accessible if valid session
245
+ auth: true,
246
+ handlers: {
247
+ get: async (ctx) => {
248
+ // remove session & force expire session cookie
249
+ await session.delete(ctx, ctx.session!.id);
250
+ ctx.res.writeHead(200).end("logged out");
251
+ },
252
+ },
253
+ },
254
+ // parametric route (w/ optional validator)
183
255
  {
184
256
  id: "hello",
185
257
  match: "/hello/?name",
186
- // optional validator(s)
187
258
  validate: {
188
259
  name: { check: (x) => /^[a-z]+$/i.test(x) },
189
260
  },
190
- // each route can specify handlers for various HTTP methods
191
261
  handlers: {
192
- get: async ({ match, res }) =>
193
- res.writeHead(200, { "content-type": "text/plain"})
194
- .end(`hello, ${match.params!.name.toLowerCase()}!`)
262
+ get: async ({ match, res }) => {
263
+ res.writeHead(200, { "content-type": "text/plain" })
264
+ .end(`hello, ${match.params!.name}!`);
265
+ },
195
266
  },
196
267
  },
197
268
  // another route to demonstrate role/usage of route IDs
@@ -201,14 +272,13 @@ const app = server({
201
272
  match: "/alias/?name",
202
273
  handlers: {
203
274
  get: ({ server, match, res }) =>
204
- server.redirectToRoute(res, { id: "hello", params: match.params })
205
- }
206
- }
275
+ server.redirectToRoute(res, {
276
+ id: "hello",
277
+ params: match.params,
278
+ }),
279
+ },
280
+ },
207
281
  ],
208
- // global interceptors (used for all routes)
209
- intercept: [
210
- logRequest(),
211
- ]
212
282
  });
213
283
 
214
284
  await app.start();
package/api.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { Fn, Maybe, MaybePromise } from "@thi.ng/api";
2
2
  import type { ILogger } from "@thi.ng/logger";
3
3
  import type { Route, RouteMatch } from "@thi.ng/router";
4
- import type { IncomingMessage, ServerResponse } from "node:http";
5
- import type { Server } from "./server.js";
6
- import type { ServerSession } from "./interceptors/session.js";
7
- export interface ServerOpts {
4
+ import type { IncomingMessage } from "node:http";
5
+ import type { ServerResponse, Server } from "./server.js";
6
+ export type Method = "get" | "put" | "post" | "delete" | "head" | "options" | "patch";
7
+ export interface ServerOpts<CTX extends RequestCtx = RequestCtx> {
8
8
  logger: ILogger;
9
9
  /**
10
10
  * SSL configuration
@@ -29,7 +29,7 @@ export interface ServerOpts {
29
29
  * Initial list of routes (more can be added dynamically via
30
30
  * {@link Server.addRoutes}).
31
31
  */
32
- routes: ServerRoute[];
32
+ routes: ServerRoute<CTX>[];
33
33
  /**
34
34
  * Route prefix. Default: `/`. All routes are assumed to have this prefix
35
35
  * prepended. If given, the prefix MUST end with `/`.
@@ -44,13 +44,22 @@ export interface ServerOpts {
44
44
  * Interceptors (aka pre/post middlewares) which are to be applied to all
45
45
  * route handlers (in the given order).
46
46
  */
47
- intercept: Interceptor[];
47
+ intercept: Interceptor<CTX>[];
48
+ /**
49
+ * User defined function to augment the {@link RequestCtx} object for each
50
+ * request before processing its handler & interceptors.
51
+ */
52
+ context: Fn<RequestCtx, CTX>;
48
53
  }
49
- export interface ServerRoute extends Route {
50
- handlers: Partial<Record<Method, RequestHandler>>;
54
+ export interface ServerRoute<CTX extends RequestCtx = RequestCtx> extends Route {
55
+ handlers: Partial<Record<Method, RequestHandler<CTX>>>;
51
56
  }
52
- export interface CompiledServerRoute extends Route {
53
- handlers: Partial<Record<Method, CompiledHandler>>;
57
+ /**
58
+ * Version of {@link ServerRoute} whose handlers/interceptors already have been
59
+ * pre-processed.
60
+ */
61
+ export interface CompiledServerRoute<CTX extends RequestCtx = RequestCtx> extends Route {
62
+ handlers: Partial<Record<Method, CompiledHandler<CTX>>>;
54
63
  }
55
64
  export interface RequestCtx {
56
65
  server: Server;
@@ -63,12 +72,12 @@ export interface RequestCtx {
63
72
  query: Record<string, any>;
64
73
  cookies?: Record<string, string>;
65
74
  session?: ServerSession;
66
- [id: string]: any;
67
75
  }
68
- export type HandlerResult = MaybePromise<Maybe<RequestCtx> | void>;
69
- export type RequestHandler = Fn<RequestCtx, HandlerResult> | InterceptedRequestHandler;
70
- export interface InterceptedRequestHandler {
71
- fn: Fn<RequestCtx, HandlerResult>;
76
+ export type HandlerResult = MaybePromise<void>;
77
+ export type InterceptorResult = MaybePromise<boolean>;
78
+ export type RequestHandler<CTX extends RequestCtx = RequestCtx> = Fn<CTX, HandlerResult> | InterceptedRequestHandler<CTX>;
79
+ export interface InterceptedRequestHandler<CTX extends RequestCtx = RequestCtx> {
80
+ fn: Fn<CTX, HandlerResult>;
72
81
  /**
73
82
  * List of interceptors which will be executed when processing the main
74
83
  * handler {@link InterceptedRequestHandler.fn}.
@@ -85,14 +94,14 @@ export interface InterceptedRequestHandler {
85
94
  * If an interceptor function returns false, further processing stops and
86
95
  * response will be closed.
87
96
  */
88
- intercept: Interceptor[];
97
+ intercept: Interceptor<CTX>[];
89
98
  }
90
- export interface CompiledHandler {
91
- fn: Fn<RequestCtx, HandlerResult>;
92
- pre?: Fn<RequestCtx, InterceptorResult>[];
93
- post?: Fn<RequestCtx, InterceptorResult>[];
99
+ export interface CompiledHandler<CTX extends RequestCtx = RequestCtx> {
100
+ fn: Fn<CTX, HandlerResult>;
101
+ pre?: Fn<CTX, InterceptorResult>[];
102
+ post?: Fn<CTX, InterceptorResult>[];
94
103
  }
95
- export interface Interceptor {
104
+ export interface Interceptor<CTX extends RequestCtx = RequestCtx> {
96
105
  /**
97
106
  * Interceptor function which will be run BEFORE the main route handler (aka
98
107
  * {@link InterceptedRequestHandler.fn}). If an interceptor needs to cancel
@@ -100,15 +109,47 @@ export interface Interceptor {
100
109
  * pre-interceptors, the main handler and all post-interceptors will be
101
110
  * skipped.
102
111
  */
103
- pre?: Fn<RequestCtx, InterceptorResult>;
112
+ pre?: Fn<CTX, InterceptorResult>;
104
113
  /**
105
114
  * Interceptor function which will be run AFTER the main route handler (aka
106
115
  * {@link InterceptedRequestHandler.fn}). If an interceptor needs to cancel
107
116
  * the request processing it must return `false`. In this case any further
108
117
  * post-interceptors will be skipped.
109
118
  */
110
- post?: Fn<RequestCtx, InterceptorResult>;
119
+ post?: Fn<CTX, InterceptorResult>;
120
+ }
121
+ export interface ServerSession {
122
+ id: string;
123
+ flash?: FlashMsg;
124
+ }
125
+ export interface FlashMsg {
126
+ type: "success" | "info" | "warn" | "error";
127
+ body: any;
128
+ }
129
+ export interface ISessionStore<T extends ServerSession = ServerSession> {
130
+ /**
131
+ * Attempts to retrieve the session for given `id`.
132
+ *
133
+ * @param id
134
+ */
135
+ get(id: string): MaybePromise<Maybe<T>>;
136
+ /**
137
+ * Adds given `session` to underlying storage.
138
+ *
139
+ * @param session
140
+ */
141
+ set(session: T): MaybePromise<boolean>;
142
+ /**
143
+ * Attempts to delete the session for given `id` from storage. Returns true
144
+ * if successful.
145
+ *
146
+ * @param id
147
+ */
148
+ delete(id: string): MaybePromise<boolean>;
149
+ /**
150
+ * Configured Time-To-Live for stored sessions. Will also be used to
151
+ * configure the `max-age` attribute of the session ID cookie.
152
+ */
153
+ readonly ttl: number;
111
154
  }
112
- export type InterceptorResult = MaybePromise<boolean>;
113
- export type Method = "get" | "put" | "post" | "delete" | "head" | "options" | "patch";
114
155
  //# sourceMappingURL=api.d.ts.map
package/index.d.ts CHANGED
@@ -6,12 +6,14 @@ export * from "./interceptors/cache-control.js";
6
6
  export * from "./interceptors/inject-headers.js";
7
7
  export * from "./interceptors/logging.js";
8
8
  export * from "./interceptors/referrer-policy.js";
9
- export * from "./interceptors/session.js";
10
9
  export * from "./interceptors/strict-transport.js";
11
10
  export * from "./interceptors/x-origin-opener.js";
12
11
  export * from "./interceptors/x-origin-resource.js";
12
+ export * from "./session/session.js";
13
+ export * from "./session/memory.js";
13
14
  export * from "./utils/cookies.js";
14
15
  export * from "./utils/cache.js";
16
+ export * from "./utils/host.js";
15
17
  export * from "./utils/formdata.js";
16
18
  export * from "./utils/multipart.js";
17
19
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -6,11 +6,13 @@ export * from "./interceptors/cache-control.js";
6
6
  export * from "./interceptors/inject-headers.js";
7
7
  export * from "./interceptors/logging.js";
8
8
  export * from "./interceptors/referrer-policy.js";
9
- export * from "./interceptors/session.js";
10
9
  export * from "./interceptors/strict-transport.js";
11
10
  export * from "./interceptors/x-origin-opener.js";
12
11
  export * from "./interceptors/x-origin-resource.js";
12
+ export * from "./session/session.js";
13
+ export * from "./session/memory.js";
13
14
  export * from "./utils/cookies.js";
14
15
  export * from "./utils/cache.js";
16
+ export * from "./utils/host.js";
15
17
  export * from "./utils/formdata.js";
16
18
  export * from "./utils/multipart.js";
@@ -14,5 +14,5 @@ import type { Interceptor, RequestCtx } from "../api.js";
14
14
  *
15
15
  * @param pred
16
16
  */
17
- export declare const authenticateWith: (pred: Predicate<RequestCtx>) => Interceptor;
17
+ export declare const authenticateWith: <CTX extends RequestCtx>(pred: Predicate<CTX>) => Interceptor<CTX>;
18
18
  //# sourceMappingURL=auth-route.d.ts.map
@@ -1,7 +1,7 @@
1
1
  const authenticateWith = (pred) => ({
2
2
  pre: (ctx) => {
3
3
  if (ctx.route.auth && !pred(ctx)) {
4
- ctx.server.unauthorized(ctx.res);
4
+ ctx.res.unauthorized();
5
5
  return false;
6
6
  }
7
7
  return true;
@@ -4,12 +4,11 @@ const __method = (level) => (isString(level) ? level : LogLevel[level]).toLowerC
4
4
  const logRequest = (level = "INFO") => {
5
5
  const method = __method(level);
6
6
  return {
7
- pre: ({ logger, req, match, query }) => {
7
+ pre: ({ logger, req, match, query, cookies }) => {
8
8
  logger[method]("request route", req.method, match);
9
9
  logger[method]("request headers", req.headers);
10
- if (Object.keys(query).length) {
11
- logger[method]("request query", query);
12
- }
10
+ logger[method]("request cookies", cookies);
11
+ logger[method]("request query", query);
13
12
  return true;
14
13
  }
15
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/server",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -90,6 +90,7 @@
90
90
  "./*.js",
91
91
  "./*.d.ts",
92
92
  "interceptors",
93
+ "session",
93
94
  "utils"
94
95
  ],
95
96
  "exports": {
@@ -114,9 +115,6 @@
114
115
  "./interceptors/referrer-policy": {
115
116
  "default": "./interceptors/referrer-policy.js"
116
117
  },
117
- "./interceptors/session": {
118
- "default": "./interceptors/session.js"
119
- },
120
118
  "./interceptors/strict-transport": {
121
119
  "default": "./interceptors/strict-transport.js"
122
120
  },
@@ -129,6 +127,12 @@
129
127
  "./server": {
130
128
  "default": "./server.js"
131
129
  },
130
+ "./session/memory": {
131
+ "default": "./session/memory.js"
132
+ },
133
+ "./session/session": {
134
+ "default": "./session/session.js"
135
+ },
132
136
  "./static": {
133
137
  "default": "./static.js"
134
138
  },
@@ -141,6 +145,9 @@
141
145
  "./utils/formdata": {
142
146
  "default": "./utils/formdata.js"
143
147
  },
148
+ "./utils/host": {
149
+ "default": "./utils/host.js"
150
+ },
144
151
  "./utils/multipart": {
145
152
  "default": "./utils/multipart.js"
146
153
  }
@@ -149,5 +156,5 @@
149
156
  "status": "alpha",
150
157
  "year": 2024
151
158
  },
152
- "gitHead": "fc1d498e8d4b690db873c30cc594352a804e7a65\n"
159
+ "gitHead": "fa1407b41ef907a5523d30bcb28691a5aed6e85c\n"
153
160
  }
package/server.d.ts CHANGED
@@ -1,25 +1,39 @@
1
+ import { type Fn } from "@thi.ng/api";
1
2
  import { type ILogger } from "@thi.ng/logger";
2
3
  import { Router, type RouteMatch } from "@thi.ng/router";
3
4
  import * as http from "node:http";
4
5
  import type { CompiledHandler, CompiledServerRoute, RequestCtx, ServerOpts, ServerRoute } from "./api.js";
5
- export declare class Server {
6
- opts: Partial<ServerOpts>;
6
+ export declare class Server<CTX extends RequestCtx = RequestCtx> {
7
+ opts: Partial<ServerOpts<CTX>>;
7
8
  logger: ILogger;
8
- router: Router;
9
- server: http.Server;
10
- constructor(opts?: Partial<ServerOpts>);
9
+ router: Router<CompiledServerRoute<CTX>>;
10
+ server: http.Server<typeof http.IncomingMessage, typeof ServerResponse>;
11
+ host: string;
12
+ protected augmentCtx: Fn<RequestCtx, CTX>;
13
+ constructor(opts?: Partial<ServerOpts<CTX>>);
11
14
  start(): Promise<boolean>;
12
15
  stop(): Promise<boolean>;
13
- listener(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
14
- runHandler({ fn, pre, post }: CompiledHandler, ctx: RequestCtx): Promise<void>;
15
- protected compileRoute(route: ServerRoute): CompiledServerRoute;
16
- addRoutes(routes: ServerRoute[]): void;
16
+ protected listener(req: http.IncomingMessage, res: ServerResponse): Promise<void>;
17
+ protected runHandler({ fn, pre, post }: CompiledHandler, ctx: CTX): Promise<void>;
18
+ protected compileRoute(route: ServerRoute<CTX>): CompiledServerRoute<CTX>;
19
+ addRoutes(routes: ServerRoute<CTX>[]): void;
17
20
  sendFile({ req, res }: RequestCtx, path: string, headers?: http.OutgoingHttpHeaders, compress?: boolean): Promise<void>;
18
- unauthorized(res: http.ServerResponse): void;
19
- unmodified(res: http.ServerResponse): void;
20
- missing(res: http.ServerResponse): void;
21
- redirectTo(res: http.ServerResponse, location: string): void;
22
- redirectToRoute(res: http.ServerResponse, route: RouteMatch): void;
21
+ redirectToRoute(res: ServerResponse, route: RouteMatch): void;
22
+ }
23
+ export declare const server: <CTX extends RequestCtx>(opts?: Partial<ServerOpts<CTX>>) => Server<CTX>;
24
+ /**
25
+ * Extended version of the default NodeJS ServerResponse with additional methods
26
+ * for various commonly used HTTP statuses/errors.
27
+ */
28
+ export declare class ServerResponse extends http.ServerResponse<http.IncomingMessage> {
29
+ noContent(headers?: http.OutgoingHttpHeaders): void;
30
+ redirectTo(location: string, headers?: http.OutgoingHttpHeaders): void;
31
+ seeOther(location: string, headers?: http.OutgoingHttpHeaders): void;
32
+ unmodified(headers?: http.OutgoingHttpHeaders): void;
33
+ unauthorized(headers?: http.OutgoingHttpHeaders, body?: any): void;
34
+ forbidden(headers?: http.OutgoingHttpHeaders, body?: any): void;
35
+ missing(headers?: http.OutgoingHttpHeaders, body?: any): void;
36
+ notAllowed(headers?: http.OutgoingHttpHeaders, body?: any): void;
37
+ notAcceptable(headers?: http.OutgoingHttpHeaders, body?: any): void;
23
38
  }
24
- export declare const server: (opts?: Partial<ServerOpts>) => Server;
25
39
  //# sourceMappingURL=server.d.ts.map
package/server.js CHANGED
@@ -1,26 +1,33 @@
1
+ import { identity } from "@thi.ng/api";
1
2
  import { isFunction } from "@thi.ng/checks";
2
3
  import { readText } from "@thi.ng/file-io";
3
4
  import { ConsoleLogger } from "@thi.ng/logger";
4
5
  import { preferredTypeForPath } from "@thi.ng/mime";
5
6
  import { Router } from "@thi.ng/router";
7
+ import { upper } from "@thi.ng/strings";
6
8
  import { createReadStream } from "node:fs";
7
9
  import * as http from "node:http";
8
10
  import * as https from "node:https";
11
+ import { isIPv6 } from "node:net";
9
12
  import { pipeline, Transform } from "node:stream";
10
13
  import { createBrotliCompress, createDeflate, createGzip } from "node:zlib";
11
14
  import { parseCoookies } from "./utils/cookies.js";
12
15
  import { parseSearchParams } from "./utils/formdata.js";
16
+ import { isMatchingHost, normalizeIPv6Address } from "./utils/host.js";
13
17
  const MISSING = "__missing";
14
18
  class Server {
15
19
  constructor(opts = {}) {
16
20
  this.opts = opts;
17
21
  this.logger = opts.logger ?? new ConsoleLogger("server");
22
+ this.host = opts.host ?? "localhost";
23
+ if (isIPv6(this.host)) this.host = normalizeIPv6Address(this.host);
24
+ this.augmentCtx = opts.context ?? identity;
18
25
  const routes = [
19
26
  {
20
27
  id: MISSING,
21
28
  match: ["__404__"],
22
29
  handlers: {
23
- get: async ({ res }) => this.missing(res)
30
+ get: async ({ res }) => res.missing()
24
31
  }
25
32
  },
26
33
  ...this.opts.routes ?? []
@@ -35,18 +42,22 @@ class Server {
35
42
  logger;
36
43
  router;
37
44
  server;
45
+ host;
46
+ augmentCtx;
38
47
  async start() {
39
- const ssl = this.opts.ssl;
40
- const port = this.opts.port ?? (ssl ? 443 : 8080);
41
- const host = this.opts.host ?? "localhost";
48
+ const { ssl, host = "localhost", port = ssl ? 443 : 8080 } = this.opts;
42
49
  try {
43
50
  this.server = ssl ? https.createServer(
44
51
  {
45
52
  key: readText(ssl.key, this.logger),
46
- cert: readText(ssl.cert, this.logger)
53
+ cert: readText(ssl.cert, this.logger),
54
+ ServerResponse
47
55
  },
48
56
  this.listener.bind(this)
49
- ) : http.createServer({}, this.listener.bind(this));
57
+ ) : http.createServer(
58
+ { ServerResponse },
59
+ this.listener.bind(this)
60
+ );
50
61
  this.server.listen(port, host, void 0, () => {
51
62
  this.logger.info(
52
63
  `starting server: http${ssl ? "s" : ""}://${host}:${port}`
@@ -66,19 +77,20 @@ class Server {
66
77
  return true;
67
78
  }
68
79
  async listener(req, res) {
69
- const url = new URL(req.url, `http://${req.headers.host}`);
70
- if (this.opts.host && this.opts.host !== url.host) {
71
- res.writeHead(503).end();
72
- return;
73
- }
74
- const path = decodeURIComponent(url.pathname);
75
80
  try {
81
+ const url = new URL(req.url, `http://${req.headers.host}`);
82
+ if (this.opts.host && !isMatchingHost(url.hostname, this.opts.host)) {
83
+ res.writeHead(503).end();
84
+ return;
85
+ }
86
+ const path = decodeURIComponent(url.pathname);
76
87
  const query = parseSearchParams(url.searchParams);
77
88
  const match = this.router.route(path);
78
89
  const route = this.router.routeForID(match.id).spec;
79
- const rawCookies = req.headers["set-cookie"]?.join(";");
90
+ const rawCookies = req.headers["cookie"] || req.headers["set-cookie"]?.join(";");
80
91
  const cookies = rawCookies ? parseCoookies(rawCookies) : {};
81
- const ctx = {
92
+ const ctx = this.augmentCtx({
93
+ // @ts-ignore
82
94
  server: this,
83
95
  logger: this.logger,
84
96
  req,
@@ -88,12 +100,18 @@ class Server {
88
100
  cookies,
89
101
  route,
90
102
  match
91
- };
103
+ });
92
104
  if (match.id === MISSING) {
93
105
  this.runHandler(route.handlers.get, ctx);
94
106
  return;
95
107
  }
96
108
  let method = ctx.query?.__method || req.method.toLowerCase();
109
+ if (method === "options" && !route.handlers.options) {
110
+ res.writeHead(204, {
111
+ allow: Object.keys(route.handlers).map(upper).join(", ")
112
+ }).end();
113
+ return;
114
+ }
97
115
  if (method === "head" && !route.handlers.head && route.handlers.get) {
98
116
  method = "get";
99
117
  }
@@ -101,7 +119,7 @@ class Server {
101
119
  if (handler) {
102
120
  this.runHandler(handler, ctx);
103
121
  } else {
104
- res.writeHead(405).end();
122
+ res.notAllowed();
105
123
  }
106
124
  } catch (e) {
107
125
  this.logger.warn("error:", e.message);
@@ -185,29 +203,47 @@ class Server {
185
203
  encoding ? pipeline(src, encoding.tx(), res, finalize) : pipeline(src, res, finalize);
186
204
  } catch (e) {
187
205
  this.logger.warn(e.message);
188
- this.missing(res);
206
+ res.missing();
189
207
  resolve();
190
208
  }
191
209
  });
192
210
  }
193
- unauthorized(res) {
194
- res.writeHead(403, "Forbidden").end();
211
+ redirectToRoute(res, route) {
212
+ res.redirectTo(this.router.format(route));
213
+ }
214
+ }
215
+ const server = (opts) => new Server(opts);
216
+ class ServerResponse extends http.ServerResponse {
217
+ noContent(headers) {
218
+ this.writeHead(204, headers).end();
195
219
  }
196
- unmodified(res) {
197
- res.writeHead(304, "Not modified").end();
220
+ redirectTo(location, headers) {
221
+ this.writeHead(302, { ...headers, location }).end();
198
222
  }
199
- missing(res) {
200
- res.writeHead(404, "Not found").end();
223
+ seeOther(location, headers) {
224
+ this.writeHead(303, { ...headers, location }).end();
201
225
  }
202
- redirectTo(res, location) {
203
- res.writeHead(302, { location }).end();
226
+ unmodified(headers) {
227
+ this.writeHead(304, headers).end();
204
228
  }
205
- redirectToRoute(res, route) {
206
- this.redirectTo(res, this.router.format(route));
229
+ unauthorized(headers, body) {
230
+ this.writeHead(401, headers).end(body);
231
+ }
232
+ forbidden(headers, body) {
233
+ this.writeHead(403, headers).end(body);
234
+ }
235
+ missing(headers, body) {
236
+ this.writeHead(404, headers).end(body);
237
+ }
238
+ notAllowed(headers, body) {
239
+ this.writeHead(405, headers).end(body);
240
+ }
241
+ notAcceptable(headers, body) {
242
+ this.writeHead(406, headers).end(body);
207
243
  }
208
244
  }
209
- const server = (opts) => new Server(opts);
210
245
  export {
211
246
  Server,
247
+ ServerResponse,
212
248
  server
213
249
  };
@@ -0,0 +1,41 @@
1
+ import { TLRUCache } from "@thi.ng/cache";
2
+ import type { ISessionStore, ServerSession } from "../api.js";
3
+ export interface InMemorySessionOpts<T extends ServerSession = ServerSession> {
4
+ /**
5
+ * Session timeout in seconds.
6
+ *
7
+ * @defaultValue 3600
8
+ */
9
+ ttl: number;
10
+ /**
11
+ * If true (default), a session's cache span automatically extends by
12
+ * {@link InMemorySessionOpts.ttl} with each request. If false, the session
13
+ * auto-expires after TTL since session creation.
14
+ *
15
+ * @defaultValue true
16
+ */
17
+ autoExtend: boolean;
18
+ /**
19
+ * Initial record of active sessions (none by default).
20
+ */
21
+ initial: Record<string, T>;
22
+ }
23
+ /**
24
+ * Session storage implementation for use with {@link serverSession}, using an
25
+ * in-memory TLRU Cache with configurable TTL.
26
+ */
27
+ export declare class InMemorySessionStore<T extends ServerSession = ServerSession> implements ISessionStore<T> {
28
+ readonly ttl: number;
29
+ protected sessions: TLRUCache<string, T>;
30
+ constructor({ ttl, autoExtend, initial, }?: Partial<InMemorySessionOpts<T>>);
31
+ get(id: string): T | undefined;
32
+ set(session: T): boolean;
33
+ delete(id: string): boolean;
34
+ }
35
+ /**
36
+ * Factory function for creating a new {@link InMemorySessionStore}.
37
+ *
38
+ * @param opts
39
+ */
40
+ export declare const inMemorySessionStore: <T extends ServerSession = ServerSession>(opts?: Partial<InMemorySessionOpts<T>>) => InMemorySessionStore<T>;
41
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { TLRUCache } from "@thi.ng/cache";
2
+ class InMemorySessionStore {
3
+ ttl;
4
+ sessions;
5
+ constructor({
6
+ ttl = 3600,
7
+ autoExtend = true,
8
+ initial
9
+ } = {}) {
10
+ this.ttl = ttl;
11
+ this.sessions = new TLRUCache(
12
+ initial ? Object.entries(initial) : null,
13
+ {
14
+ ttl: ttl * 1e3,
15
+ autoExtend
16
+ }
17
+ );
18
+ }
19
+ get(id) {
20
+ return this.sessions.get(id);
21
+ }
22
+ set(session) {
23
+ this.sessions.set(session.id, session);
24
+ return true;
25
+ }
26
+ delete(id) {
27
+ return this.sessions.delete(id);
28
+ }
29
+ }
30
+ const inMemorySessionStore = (opts) => new InMemorySessionStore(opts);
31
+ export {
32
+ InMemorySessionStore,
33
+ inMemorySessionStore
34
+ };
@@ -0,0 +1,44 @@
1
+ import type { Fn } from "@thi.ng/api";
2
+ import { ServerResponse } from "node:http";
3
+ import type { Interceptor, ISessionStore, RequestCtx, ServerSession } from "../api.js";
4
+ export interface SessionOpts<CTX extends RequestCtx = RequestCtx, SESSION extends ServerSession = ServerSession> {
5
+ /**
6
+ * Session storage implementation. Default: {@link InMemorySessionStore}.
7
+ */
8
+ store: ISessionStore<SESSION>;
9
+ /**
10
+ * Factory function to create a new session object. By default the object
11
+ * only contains a {@link ServerSession.id} (UUID v4).
12
+ */
13
+ factory: Fn<CTX, SESSION>;
14
+ /**
15
+ * Session cookie name
16
+ *
17
+ * @defaultValue "__sid"
18
+ */
19
+ cookieName?: string;
20
+ /**
21
+ * Additional session cookie config options.
22
+ *
23
+ * @defaultValue "Secure;HttpOnly;SameSite=Strict;Path=/"
24
+ */
25
+ cookieOpts?: string;
26
+ }
27
+ export declare class SessionInterceptor<CTX extends RequestCtx = RequestCtx, SESSION extends ServerSession = ServerSession> implements Interceptor<CTX> {
28
+ store: ISessionStore<SESSION>;
29
+ factory: Fn<CTX, SESSION>;
30
+ cookieName: string;
31
+ cookieOpts: string;
32
+ constructor({ store, factory, cookieName, cookieOpts, }?: Partial<SessionOpts<CTX, SESSION>>);
33
+ pre(ctx: CTX): Promise<boolean>;
34
+ delete(ctx: CTX, sessionID: string): Promise<void>;
35
+ newSession(ctx: CTX): Promise<SESSION | undefined>;
36
+ withSession(res: ServerResponse, sessionID: string): ServerResponse<import("http").IncomingMessage>;
37
+ }
38
+ /**
39
+ * Factory function to create a new {@link SessionInterceptor} instance.
40
+ *
41
+ * @param opts
42
+ */
43
+ export declare const serverSession: <CTX extends RequestCtx = RequestCtx, SESSION extends ServerSession = ServerSession>(opts?: Partial<SessionOpts<CTX, SESSION>>) => SessionInterceptor<CTX, SESSION>;
44
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { uuid } from "@thi.ng/uuid";
2
+ import { ServerResponse } from "node:http";
3
+ import { inMemorySessionStore } from "./memory.js";
4
+ class SessionInterceptor {
5
+ store;
6
+ factory;
7
+ cookieName;
8
+ cookieOpts;
9
+ constructor({
10
+ store = inMemorySessionStore(),
11
+ factory = () => ({ id: uuid() }),
12
+ cookieName = "__sid",
13
+ cookieOpts = "Secure;HttpOnly;SameSite=Strict;Path=/"
14
+ } = {}) {
15
+ this.store = store;
16
+ this.factory = factory;
17
+ this.cookieName = cookieName;
18
+ this.cookieOpts = cookieOpts;
19
+ }
20
+ async pre(ctx) {
21
+ const id = ctx.cookies?.[this.cookieName];
22
+ let session = id ? await this.store.get(id) : void 0;
23
+ if (!session) {
24
+ session = await this.newSession(ctx);
25
+ if (!session) return false;
26
+ }
27
+ ctx.session = session;
28
+ this.withSession(ctx.res, session.id);
29
+ return true;
30
+ }
31
+ async delete(ctx, sessionID) {
32
+ if (await this.store.delete(sessionID)) {
33
+ ctx.logger.info("delete session:", sessionID);
34
+ ctx.session = void 0;
35
+ ctx.res.appendHeader(
36
+ "set-cookie",
37
+ `${this.cookieName}=;Expires=Thu, 01 Jan 1970 00:00:00 GMT;${this.cookieOpts}`
38
+ );
39
+ }
40
+ }
41
+ async newSession(ctx) {
42
+ const session = this.factory(ctx);
43
+ ctx.logger.info("new session:", session.id);
44
+ if (!await this.store.set(session)) {
45
+ ctx.logger.warn("could not store session...");
46
+ return;
47
+ }
48
+ return session;
49
+ }
50
+ withSession(res, sessionID) {
51
+ return res.appendHeader(
52
+ "set-cookie",
53
+ `${this.cookieName}=${sessionID};Max-Age=${this.store.ttl};${this.cookieOpts}`
54
+ );
55
+ }
56
+ }
57
+ const serverSession = (opts) => new SessionInterceptor(opts);
58
+ export {
59
+ SessionInterceptor,
60
+ serverSession
61
+ };
package/static.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import type { Fn, MaybePromise, Predicate } from "@thi.ng/api";
2
2
  import { type HashAlgo } from "@thi.ng/file-io";
3
3
  import type { OutgoingHttpHeaders } from "node:http";
4
- import type { Interceptor, ServerRoute } from "./api.js";
4
+ import type { Interceptor, RequestCtx, ServerRoute } from "./api.js";
5
5
  /**
6
6
  * Static file configuration options.
7
7
  */
8
- export interface StaticOpts {
8
+ export interface StaticOpts<CTX extends RequestCtx = RequestCtx> {
9
9
  /**
10
10
  * Path to local root directory for static assets. Also see
11
11
  * {@link StaticOpts.prefix}
@@ -29,7 +29,7 @@ export interface StaticOpts {
29
29
  /**
30
30
  * Additional route specific interceptors.
31
31
  */
32
- intercept: Interceptor[];
32
+ intercept: Interceptor<CTX>[];
33
33
  /**
34
34
  * Additional common headers (e.g. cache control) for all static files
35
35
  */
@@ -46,6 +46,13 @@ export interface StaticOpts {
46
46
  * file is guaranteed to exist when this function is called.
47
47
  */
48
48
  etag: Fn<string, MaybePromise<string>>;
49
+ /**
50
+ * If true, the route will have its `auth` flag enabled, e.g. for use with
51
+ * the {@link authenticateWith} interceptor.
52
+ *
53
+ * @defaultValue false
54
+ */
55
+ auth: boolean;
49
56
  }
50
57
  /**
51
58
  * Defines a configurable {@link ServerRoute} and handler for serving static
@@ -54,7 +61,7 @@ export interface StaticOpts {
54
61
  *
55
62
  * @param opts
56
63
  */
57
- export declare const staticFiles: ({ prefix, rootDir, intercept, filter, compress, etag, headers, }?: Partial<StaticOpts>) => ServerRoute;
64
+ export declare const staticFiles: <CTX extends RequestCtx = RequestCtx>({ prefix, rootDir, intercept, filter, compress, auth, etag, headers, }?: Partial<StaticOpts<CTX>>) => ServerRoute<CTX>;
58
65
  /**
59
66
  * Etag header value function for {@link StaticOpts.etag}. Computes Etag based
60
67
  * on file modified date.
package/static.js CHANGED
@@ -9,11 +9,13 @@ const staticFiles = ({
9
9
  intercept = [],
10
10
  filter = () => true,
11
11
  compress = false,
12
+ auth = false,
12
13
  etag,
13
14
  headers
14
15
  } = {}) => ({
15
16
  id: "__static",
16
17
  match: [prefix, "+"],
18
+ auth,
17
19
  handlers: {
18
20
  head: {
19
21
  fn: async (ctx) => {
@@ -52,11 +54,11 @@ const staticFiles = ({
52
54
  });
53
55
  const __fileHeaders = async (path, ctx, filter, etag, headers) => {
54
56
  if (!(existsSync(path) && filter(path))) {
55
- return ctx.server.missing(ctx.res);
57
+ return ctx.res.missing();
56
58
  }
57
59
  if (etag) {
58
60
  const etagValue = await etag(path);
59
- return isUnmodified(etagValue, ctx.req.headers["if-none-match"]) ? ctx.server.unmodified(ctx.res) : { ...headers, etag: etagValue };
61
+ return isUnmodified(etagValue, ctx.req.headers["if-none-match"]) ? ctx.res.unmodified() : { ...headers, etag: etagValue };
60
62
  }
61
63
  return { ...headers };
62
64
  };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Takes a hostname or IPv6 address `test` and compares it against `expected`,
3
+ * returns true if matching.
4
+ *
5
+ * @remarks
6
+ * If `test` is an IPv6 address in the format `[...]`, it will be compared
7
+ * without surrounding square brackets. If `expected` is an IPv6 address it MUST
8
+ * have been pre-normalized using {@link normalizeIPv6Address}. For performance
9
+ * reasons only `test` will be automatically normalized, i.e. `::1` will be
10
+ * normalized as `0:0:0:0:0:0:0:1`.
11
+ *
12
+ * @param test
13
+ * @param expected
14
+ *
15
+ * @internal
16
+ */
17
+ export declare const isMatchingHost: (test: string, expected: string) => boolean;
18
+ /**
19
+ * Parses given IPv6 address into an 8-tuple of 16bit uints. Throws error if
20
+ * address is invalid.
21
+ *
22
+ * @remarks
23
+ * https://en.wikipedia.org/wiki/IPv6_address
24
+ *
25
+ * @param addr
26
+ */
27
+ export declare const parseIPv6Address: (addr: string) => number[];
28
+ /**
29
+ * Returns normalized version of given IPv6 address, i.e. expanding `::`
30
+ * sections, removing leading zeroes and performing other syntactic validations.
31
+ * The returned address always has 8 parts. Throws error if address is invalid.
32
+ *
33
+ * @remarks
34
+ * Internally uses {@link parseIPv6Address}.
35
+ *
36
+ * Reference:
37
+ * https://en.wikipedia.org/wiki/IPv6_address
38
+ *
39
+ * @param addr
40
+ */
41
+ export declare const normalizeIPv6Address: (addr: string) => string;
42
+ //# sourceMappingURL=host.d.ts.map
package/utils/host.js ADDED
@@ -0,0 +1,40 @@
1
+ import { illegalArgs } from "@thi.ng/errors";
2
+ import { HEX } from "@thi.ng/strings";
3
+ const isMatchingHost = (test, expected) => /^\[[0-9a-f:]+\]$/.test(test) ? normalizeIPv6Address(test.substring(1, test.length - 1)) === expected : test === expected;
4
+ const parseIPv6Address = (addr) => {
5
+ if (addr == "::") return [0, 0, 0, 0, 0, 0, 0, 0];
6
+ if (addr == "::1") return [0, 0, 0, 0, 0, 0, 0, 1];
7
+ const n = addr.length - 1;
8
+ if (n > 38) invalidIPv6(addr);
9
+ const parts = [];
10
+ let curr = 0;
11
+ let zeroes = -1;
12
+ for (let i = 0; i <= n; i++) {
13
+ const ch = addr[i];
14
+ if (i === n && ch == ":") illegalArgs(addr);
15
+ if (i === n || ch === ":") {
16
+ if (parts.length >= (zeroes >= 0 ? 6 : 8)) invalidIPv6(addr);
17
+ const end = i === n ? n + 1 : i > curr ? i : invalidIPv6(addr);
18
+ if (end - curr > 4) invalidIPv6(addr);
19
+ parts.push(parseInt(addr.substring(curr, end), 16));
20
+ if (addr[i + 1] === ":") {
21
+ if (zeroes >= 0) invalidIPv6(addr);
22
+ zeroes = parts.length;
23
+ i++;
24
+ }
25
+ curr = i + 1;
26
+ } else if (!HEX[ch]) invalidIPv6(addr);
27
+ }
28
+ if (zeroes >= 0) {
29
+ parts.splice(zeroes, 0, ...new Array(8 - parts.length).fill(0));
30
+ }
31
+ if (parts.length !== 8) invalidIPv6(addr);
32
+ return parts;
33
+ };
34
+ const normalizeIPv6Address = (addr) => parseIPv6Address(addr).map((x) => x.toString(16)).join(":");
35
+ const invalidIPv6 = (addr) => illegalArgs("invalid IPv6 address: " + addr);
36
+ export {
37
+ isMatchingHost,
38
+ normalizeIPv6Address,
39
+ parseIPv6Address
40
+ };
@@ -1,51 +0,0 @@
1
- import type { Fn } from "@thi.ng/api";
2
- import * as http from "node:http";
3
- import type { Interceptor, RequestCtx } from "../api.js";
4
- export interface SessionOpts<T extends ServerSession> {
5
- /**
6
- * Factory function to create a new session object. By default the object
7
- * only contains a {@link ServerSession.id} (UUID v4).
8
- */
9
- factory: Fn<RequestCtx, T>;
10
- /**
11
- * Initial record of active sessions (none by default).
12
- */
13
- initial: Record<string, T>;
14
- /**
15
- * Session cookie name
16
- *
17
- * @defaultValue "__sid"
18
- */
19
- cookieName?: string;
20
- /**
21
- * Additional session cookie config options.
22
- *
23
- * @defaultValue "Secure;HttpOnly;SameSite=Strict;Path=/"
24
- */
25
- cookieOpts?: string;
26
- /**
27
- * Session timeout in seconds.
28
- *
29
- * @defaultValue 3600
30
- */
31
- ttl?: number;
32
- }
33
- export interface ServerSession {
34
- id: string;
35
- flash?: FlashMsg;
36
- }
37
- export interface FlashMsg {
38
- type: "success" | "info" | "warn" | "error";
39
- body: any;
40
- }
41
- export interface SessionInterceptor extends Interceptor {
42
- /**
43
- * Adds configured session cookie to response.
44
- *
45
- * @param res
46
- * @param sessionID
47
- */
48
- withSession(res: http.ServerResponse, sessionID: string): http.ServerResponse;
49
- }
50
- export declare const serverSession: <T extends ServerSession>(opts?: Partial<SessionOpts<T>>) => SessionInterceptor;
51
- //# sourceMappingURL=session.d.ts.map
@@ -1,38 +0,0 @@
1
- import { TLRUCache } from "@thi.ng/cache";
2
- import { uuid } from "@thi.ng/uuid";
3
- import * as http from "node:http";
4
- const serverSession = (opts = {}) => {
5
- const factory = opts.factory ?? (() => ({ id: uuid() }));
6
- const ttl = opts.ttl ?? 3600;
7
- const cookieName = opts.cookieName ?? "__sid";
8
- const cookieOpts = `Max-Age=${ttl};` + (opts.cookieOpts ?? "Secure;HttpOnly;SameSite=Strict;Path=/");
9
- const sessions = new TLRUCache(
10
- opts.initial ? Object.entries(opts.initial) : null,
11
- {
12
- ttl: ttl * 1e3,
13
- autoExtend: true
14
- }
15
- );
16
- return {
17
- pre(ctx) {
18
- const { res, logger, cookies } = ctx;
19
- let id = cookies?.[cookieName];
20
- let session = id ? sessions.get(id) : void 0;
21
- if (!session) {
22
- session = factory(ctx);
23
- logger.info("new session", session);
24
- sessions.set(session.id, session);
25
- }
26
- ctx.session = session;
27
- this.withSession(res, session.id);
28
- return true;
29
- },
30
- withSession: (res, sessionID) => res.appendHeader(
31
- "set-cookie",
32
- `${cookieName}=${sessionID};${cookieOpts}`
33
- )
34
- };
35
- };
36
- export {
37
- serverSession
38
- };