@thi.ng/server 0.1.0 → 0.2.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 +17 -1
- package/README.md +111 -41
- package/api.d.ts +64 -23
- package/index.d.ts +2 -1
- package/index.js +2 -1
- package/interceptors/auth-route.d.ts +1 -1
- package/interceptors/logging.js +3 -4
- package/package.json +9 -5
- package/server.d.ts +10 -8
- package/server.js +14 -3
- package/session/memory.d.ts +41 -0
- package/session/memory.js +34 -0
- package/session/session.d.ts +43 -0
- package/session/session.js +54 -0
- package/static.d.ts +11 -4
- package/static.js +2 -0
- package/interceptors/session.d.ts +0 -51
- package/interceptors/session.js +0 -38
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2025-01-
|
|
3
|
+
- **Last updated**: 2025-01-30T15:45:22Z
|
|
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,22 @@ 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.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.2.0) (2025-01-30)
|
|
15
|
+
|
|
16
|
+
#### 🚀 Features
|
|
17
|
+
|
|
18
|
+
- add generics, various other updates ([a340f65](https://github.com/thi-ng/umbrella/commit/a340f65))
|
|
19
|
+
- add generics to most main types/interfaces
|
|
20
|
+
- refactor `SessionInterceptor` as class w/ pluggable storage
|
|
21
|
+
- add `ISessionStore` and `InMemorySessionStore` impl
|
|
22
|
+
- update ServerOpts to allow augmenting request context object
|
|
23
|
+
- add default HTTP OPTIONS handler
|
|
24
|
+
- update Server cookie parsing
|
|
25
|
+
- add StaticOpts.auth flag
|
|
26
|
+
- update logRequest() interceptor
|
|
27
|
+
- update pkg exports
|
|
28
|
+
- update tests
|
|
29
|
+
|
|
14
30
|
## [0.1.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.1.0) (2025-01-29)
|
|
15
31
|
|
|
16
32
|
#### 🚀 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
|
-
-
|
|
45
|
-
|
|
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
|
|
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.
|
|
58
|
-
|
|
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
|
+

|
|
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:
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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, {
|
|
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
|
@@ -3,8 +3,8 @@ import type { ILogger } from "@thi.ng/logger";
|
|
|
3
3
|
import type { Route, RouteMatch } from "@thi.ng/router";
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
5
|
import type { Server } from "./server.js";
|
|
6
|
-
|
|
7
|
-
export interface ServerOpts {
|
|
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
|
-
|
|
53
|
-
|
|
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<
|
|
69
|
-
export type
|
|
70
|
-
export
|
|
71
|
-
|
|
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<
|
|
92
|
-
pre?: Fn<
|
|
93
|
-
post?: Fn<
|
|
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<
|
|
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<
|
|
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,10 +6,11 @@ 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";
|
|
15
16
|
export * from "./utils/formdata.js";
|
package/index.js
CHANGED
|
@@ -6,10 +6,11 @@ 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";
|
|
15
16
|
export * from "./utils/formdata.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<
|
|
17
|
+
export declare const authenticateWith: <CTX extends RequestCtx>(pred: Predicate<CTX>) => Interceptor<CTX>;
|
|
18
18
|
//# sourceMappingURL=auth-route.d.ts.map
|
package/interceptors/logging.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.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
|
},
|
|
@@ -149,5 +153,5 @@
|
|
|
149
153
|
"status": "alpha",
|
|
150
154
|
"year": 2024
|
|
151
155
|
},
|
|
152
|
-
"gitHead": "
|
|
156
|
+
"gitHead": "078de98f4365f0d472c87198bcd6a112e732d9ef\n"
|
|
153
157
|
}
|
package/server.d.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
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
|
+
router: Router<CompiledServerRoute<CTX>>;
|
|
9
10
|
server: http.Server;
|
|
10
|
-
|
|
11
|
+
protected augmentCtx: Fn<RequestCtx, CTX>;
|
|
12
|
+
constructor(opts?: Partial<ServerOpts<CTX>>);
|
|
11
13
|
start(): Promise<boolean>;
|
|
12
14
|
stop(): Promise<boolean>;
|
|
13
|
-
listener(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
|
|
14
|
-
runHandler({ fn, pre, post }: CompiledHandler, ctx:
|
|
15
|
-
protected compileRoute(route: ServerRoute): CompiledServerRoute
|
|
15
|
+
protected listener(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
|
|
16
|
+
protected runHandler({ fn, pre, post }: CompiledHandler, ctx: CTX): Promise<void>;
|
|
17
|
+
protected compileRoute(route: ServerRoute<CTX>): CompiledServerRoute<CTX>;
|
|
16
18
|
addRoutes(routes: ServerRoute[]): void;
|
|
17
19
|
sendFile({ req, res }: RequestCtx, path: string, headers?: http.OutgoingHttpHeaders, compress?: boolean): Promise<void>;
|
|
18
20
|
unauthorized(res: http.ServerResponse): void;
|
|
@@ -21,5 +23,5 @@ export declare class Server {
|
|
|
21
23
|
redirectTo(res: http.ServerResponse, location: string): void;
|
|
22
24
|
redirectToRoute(res: http.ServerResponse, route: RouteMatch): void;
|
|
23
25
|
}
|
|
24
|
-
export declare const server: (opts?: Partial<ServerOpts
|
|
26
|
+
export declare const server: <CTX extends RequestCtx>(opts?: Partial<ServerOpts<CTX>>) => Server<CTX>;
|
|
25
27
|
//# sourceMappingURL=server.d.ts.map
|
package/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
@@ -10,11 +11,13 @@ import { pipeline, Transform } from "node:stream";
|
|
|
10
11
|
import { createBrotliCompress, createDeflate, createGzip } from "node:zlib";
|
|
11
12
|
import { parseCoookies } from "./utils/cookies.js";
|
|
12
13
|
import { parseSearchParams } from "./utils/formdata.js";
|
|
14
|
+
import { upper } from "@thi.ng/strings";
|
|
13
15
|
const MISSING = "__missing";
|
|
14
16
|
class Server {
|
|
15
17
|
constructor(opts = {}) {
|
|
16
18
|
this.opts = opts;
|
|
17
19
|
this.logger = opts.logger ?? new ConsoleLogger("server");
|
|
20
|
+
this.augmentCtx = opts.context ?? identity;
|
|
18
21
|
const routes = [
|
|
19
22
|
{
|
|
20
23
|
id: MISSING,
|
|
@@ -35,6 +38,7 @@ class Server {
|
|
|
35
38
|
logger;
|
|
36
39
|
router;
|
|
37
40
|
server;
|
|
41
|
+
augmentCtx;
|
|
38
42
|
async start() {
|
|
39
43
|
const ssl = this.opts.ssl;
|
|
40
44
|
const port = this.opts.port ?? (ssl ? 443 : 8080);
|
|
@@ -76,9 +80,10 @@ class Server {
|
|
|
76
80
|
const query = parseSearchParams(url.searchParams);
|
|
77
81
|
const match = this.router.route(path);
|
|
78
82
|
const route = this.router.routeForID(match.id).spec;
|
|
79
|
-
const rawCookies = req.headers["set-cookie"]?.join(";");
|
|
83
|
+
const rawCookies = req.headers["cookie"] || req.headers["set-cookie"]?.join(";");
|
|
80
84
|
const cookies = rawCookies ? parseCoookies(rawCookies) : {};
|
|
81
|
-
const ctx = {
|
|
85
|
+
const ctx = this.augmentCtx({
|
|
86
|
+
// @ts-ignore
|
|
82
87
|
server: this,
|
|
83
88
|
logger: this.logger,
|
|
84
89
|
req,
|
|
@@ -88,12 +93,18 @@ class Server {
|
|
|
88
93
|
cookies,
|
|
89
94
|
route,
|
|
90
95
|
match
|
|
91
|
-
};
|
|
96
|
+
});
|
|
92
97
|
if (match.id === MISSING) {
|
|
93
98
|
this.runHandler(route.handlers.get, ctx);
|
|
94
99
|
return;
|
|
95
100
|
}
|
|
96
101
|
let method = ctx.query?.__method || req.method.toLowerCase();
|
|
102
|
+
if (method === "options" && !route.handlers.options) {
|
|
103
|
+
res.writeHead(204, {
|
|
104
|
+
allow: Object.keys(route.handlers).map(upper).join(", ")
|
|
105
|
+
}).end();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
97
108
|
if (method === "head" && !route.handlers.head && route.handlers.get) {
|
|
98
109
|
method = "get";
|
|
99
110
|
}
|
|
@@ -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,43 @@
|
|
|
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
|
+
withSession(res: ServerResponse, sessionID: string): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Factory function to create a new {@link SessionInterceptor} instance.
|
|
39
|
+
*
|
|
40
|
+
* @param opts
|
|
41
|
+
*/
|
|
42
|
+
export declare const serverSession: <CTX extends RequestCtx = RequestCtx, SESSION extends ServerSession = ServerSession>(opts?: Partial<SessionOpts<CTX, SESSION>>) => SessionInterceptor<CTX, SESSION>;
|
|
43
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
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 { res, logger, cookies } = ctx;
|
|
22
|
+
const id = cookies?.[this.cookieName];
|
|
23
|
+
let session = id ? await this.store.get(id) : void 0;
|
|
24
|
+
if (!session) {
|
|
25
|
+
session = this.factory(ctx);
|
|
26
|
+
logger.info("new session:", session.id);
|
|
27
|
+
this.store.set(session);
|
|
28
|
+
}
|
|
29
|
+
ctx.session = session;
|
|
30
|
+
this.withSession(res, session.id);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
async delete(ctx, sessionID) {
|
|
34
|
+
if (await this.store.delete(sessionID)) {
|
|
35
|
+
ctx.logger.info("delete session:", sessionID);
|
|
36
|
+
ctx.session = void 0;
|
|
37
|
+
ctx.res.appendHeader(
|
|
38
|
+
"set-cookie",
|
|
39
|
+
`${this.cookieName}=;Expires=Thu, 01 Jan 1970 00:00:00 GMT;${this.cookieOpts}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
withSession(res, sessionID) {
|
|
44
|
+
res.appendHeader(
|
|
45
|
+
"set-cookie",
|
|
46
|
+
`${this.cookieName}=${sessionID};Max-Age=${this.store.ttl};${this.cookieOpts}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const serverSession = (opts) => new SessionInterceptor(opts);
|
|
51
|
+
export {
|
|
52
|
+
SessionInterceptor,
|
|
53
|
+
serverSession
|
|
54
|
+
};
|
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
|
|
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
|
@@ -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
|
package/interceptors/session.js
DELETED
|
@@ -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
|
-
};
|