@thi.ng/server 0.4.1 → 0.6.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 +27 -1
- package/README.md +6 -4
- package/api.d.ts +32 -12
- package/interceptors/auth-route.d.ts +7 -6
- package/interceptors/logging.d.ts +1 -1
- package/interceptors/logging.js +0 -1
- package/interceptors/measure.js +0 -1
- package/package.json +15 -15
- package/server.js +39 -21
- package/session/memory.d.ts +2 -2
- package/session/session.d.ts +47 -7
- package/session/session.js +43 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2025-02-
|
|
3
|
+
- **Last updated**: 2025-02-21T21:54: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,32 @@ 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.6.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.6.0) (2025-02-21)
|
|
15
|
+
|
|
16
|
+
#### 🚀 Features
|
|
17
|
+
|
|
18
|
+
- update `sessionInterceptor()` cookie signing/handling ([bdd4d66](https://github.com/thi-ng/umbrella/commit/bdd4d66))
|
|
19
|
+
- update `SessionInterceptor.newSession()`
|
|
20
|
+
- pre-compute session metadata (HMAC & cookie values), store in WeakMap
|
|
21
|
+
- update `.withSession()`
|
|
22
|
+
- update `.validateSession()` to use cached signature
|
|
23
|
+
- update tests
|
|
24
|
+
- add/update `ServerOpts` ([23b5321](https://github.com/thi-ng/umbrella/commit/23b5321))
|
|
25
|
+
|
|
26
|
+
## [0.5.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.5.0) (2025-02-19)
|
|
27
|
+
|
|
28
|
+
#### 🚀 Features
|
|
29
|
+
|
|
30
|
+
- update interceptor handling ([9cdb8b8](https://github.com/thi-ng/umbrella/commit/9cdb8b8))
|
|
31
|
+
- update post-interceptor execution logic & return values
|
|
32
|
+
- update `Server.runHandler()`, `Server.compileRoute()`
|
|
33
|
+
- update `logResponse()`, `measure()` interceptors
|
|
34
|
+
|
|
35
|
+
#### ♻️ Refactoring
|
|
36
|
+
|
|
37
|
+
- rename `serverSession()` => `sessionInterceptor()` ([2ada168](https://github.com/thi-ng/umbrella/commit/2ada168))
|
|
38
|
+
- add docs
|
|
39
|
+
|
|
14
40
|
## [0.4.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.4.0) (2025-02-10)
|
|
15
41
|
|
|
16
42
|
#### 🚀 Features
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://mastodon.thi.ng/@toxi)
|
|
8
8
|
|
|
9
9
|
> [!NOTE]
|
|
10
|
-
> This is one of
|
|
10
|
+
> This is one of 202 standalone projects, maintained as part
|
|
11
11
|
> of the [@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo
|
|
12
12
|
> and anti-framework.
|
|
13
13
|
>
|
|
@@ -80,7 +80,7 @@ for more details.
|
|
|
80
80
|
- [`logResponse()`](https://docs.thi.ng/umbrella/server/functions/logResponse.html): Response logging
|
|
81
81
|
- [`rateLimiter()`](https://docs.thi.ng/umbrella/server/functions/rateLimiter-1.html): Configurable rate limiting
|
|
82
82
|
- [`referrerPolicy()`](https://docs.thi.ng/umbrella/server/functions/referrerPolicy-1.html): Policy header injection
|
|
83
|
-
- [`
|
|
83
|
+
- [`sessionInterceptor()`](https://docs.thi.ng/umbrella/server/functions/sessionInterceptor-1.html): User defined in-memory sessions with TTL
|
|
84
84
|
- [`strictTransportSecurity()`](https://docs.thi.ng/umbrella/server/functions/strictTransportSecurity.html): Policy header injection
|
|
85
85
|
|
|
86
86
|
#### Custom interceptors
|
|
@@ -149,7 +149,7 @@ For Node.js REPL:
|
|
|
149
149
|
const ser = await import("@thi.ng/server");
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
Package sizes (brotli'd, pre-treeshake): ESM: 5.
|
|
152
|
+
Package sizes (brotli'd, pre-treeshake): ESM: 5.28 KB
|
|
153
153
|
|
|
154
154
|
## Dependencies
|
|
155
155
|
|
|
@@ -192,7 +192,9 @@ interface AppSession extends srv.ServerSession {
|
|
|
192
192
|
|
|
193
193
|
// interceptor for injecting/managing sessions
|
|
194
194
|
// by default uses in-memory storage/cache
|
|
195
|
-
const session = srv.
|
|
195
|
+
const session = srv.sessionInterceptor<AppCtx, AppSession>({
|
|
196
|
+
factory: srv.createSession
|
|
197
|
+
});
|
|
196
198
|
|
|
197
199
|
// create server with given config
|
|
198
200
|
const app = srv.server<AppCtx>({
|
package/api.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ 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
4
|
import type { IncomingMessage } from "node:http";
|
|
5
|
-
import type {
|
|
5
|
+
import type { BlockList } from "node:net";
|
|
6
|
+
import type { Server, ServerResponse } from "./server.js";
|
|
6
7
|
export type Method = "get" | "put" | "post" | "delete" | "head" | "options" | "patch";
|
|
7
8
|
export interface ServerOpts<CTX extends RequestCtx = RequestCtx> {
|
|
8
9
|
logger: ILogger;
|
|
@@ -50,6 +51,22 @@ export interface ServerOpts<CTX extends RequestCtx = RequestCtx> {
|
|
|
50
51
|
* request before processing its handler & interceptors.
|
|
51
52
|
*/
|
|
52
53
|
context: Fn<RequestCtx, CTX>;
|
|
54
|
+
/**
|
|
55
|
+
* Timeout in milliseconds for receiving the entire request from the client.
|
|
56
|
+
*/
|
|
57
|
+
requestTimeout: number;
|
|
58
|
+
/**
|
|
59
|
+
* List of response headers that should be sent only once.
|
|
60
|
+
*/
|
|
61
|
+
uniqueHeaders: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Used for disabling inbound access to specific IP addresses, IP ranges, or
|
|
64
|
+
* IP subnets. This does not work if the server is behind a reverse proxy.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* Reference: https://nodejs.org/api/net.html#class-netblocklist
|
|
68
|
+
*/
|
|
69
|
+
blockList: BlockList;
|
|
53
70
|
}
|
|
54
71
|
export interface ServerRoute<CTX extends RequestCtx = RequestCtx> extends Route {
|
|
55
72
|
handlers: Partial<Record<Method, RequestHandler<CTX>>>;
|
|
@@ -74,7 +91,8 @@ export interface RequestCtx {
|
|
|
74
91
|
session?: ServerSession;
|
|
75
92
|
}
|
|
76
93
|
export type HandlerResult = MaybePromise<void>;
|
|
77
|
-
export type
|
|
94
|
+
export type PreInterceptorResult = MaybePromise<boolean>;
|
|
95
|
+
export type PostInterceptorResult = MaybePromise<void>;
|
|
78
96
|
export type RequestHandler<CTX extends RequestCtx = RequestCtx> = Fn<CTX, HandlerResult> | InterceptedRequestHandler<CTX>;
|
|
79
97
|
export interface InterceptedRequestHandler<CTX extends RequestCtx = RequestCtx> {
|
|
80
98
|
fn: Fn<CTX, HandlerResult>;
|
|
@@ -98,25 +116,27 @@ export interface InterceptedRequestHandler<CTX extends RequestCtx = RequestCtx>
|
|
|
98
116
|
}
|
|
99
117
|
export interface CompiledHandler<CTX extends RequestCtx = RequestCtx> {
|
|
100
118
|
fn: Fn<CTX, HandlerResult>;
|
|
101
|
-
pre?: Fn<CTX,
|
|
102
|
-
post?: Fn<CTX,
|
|
119
|
+
pre?: Maybe<Fn<CTX, PreInterceptorResult>>[];
|
|
120
|
+
post?: Maybe<Fn<CTX, PostInterceptorResult>>[];
|
|
103
121
|
}
|
|
104
122
|
export interface Interceptor<CTX extends RequestCtx = RequestCtx> {
|
|
105
123
|
/**
|
|
106
124
|
* Interceptor function which will be run BEFORE the main route handler (aka
|
|
107
125
|
* {@link InterceptedRequestHandler.fn}). If an interceptor needs to cancel
|
|
108
126
|
* the request processing it must return `false`. In this case any further
|
|
109
|
-
* pre-interceptors
|
|
110
|
-
*
|
|
127
|
+
* pre-interceptors and the main handler will be skipped. In the post-phase,
|
|
128
|
+
* only the interceptors preceding the failed one will be run (though in
|
|
129
|
+
* reverse order). E.g. If the 3rd pre-interceptor failed, only the post
|
|
130
|
+
* phases of the first two will still be run (if available)...
|
|
111
131
|
*/
|
|
112
|
-
pre?: Fn<CTX,
|
|
132
|
+
pre?: Fn<CTX, PreInterceptorResult>;
|
|
113
133
|
/**
|
|
114
134
|
* Interceptor function which will be run AFTER the main route handler (aka
|
|
115
|
-
* {@link InterceptedRequestHandler.fn}).
|
|
116
|
-
*
|
|
117
|
-
*
|
|
135
|
+
* {@link InterceptedRequestHandler.fn}). Post-interceptors cannot cancel
|
|
136
|
+
* request processing and are mainly intended for logging or clean-up
|
|
137
|
+
* purposes. Post interceptors
|
|
118
138
|
*/
|
|
119
|
-
post?: Fn<CTX,
|
|
139
|
+
post?: Fn<CTX, PostInterceptorResult>;
|
|
120
140
|
}
|
|
121
141
|
export interface ServerSession {
|
|
122
142
|
/**
|
|
@@ -140,7 +160,7 @@ export interface ISessionStore<T extends ServerSession = ServerSession> {
|
|
|
140
160
|
*/
|
|
141
161
|
get(id: string): MaybePromise<Maybe<T>>;
|
|
142
162
|
/**
|
|
143
|
-
* Adds given `session`
|
|
163
|
+
* Adds to or updates given `session` in underlying storage.
|
|
144
164
|
*
|
|
145
165
|
* @param session
|
|
146
166
|
*/
|
|
@@ -2,14 +2,15 @@ import type { Predicate } from "@thi.ng/api";
|
|
|
2
2
|
import type { Interceptor, RequestCtx } from "../api.js";
|
|
3
3
|
/**
|
|
4
4
|
* Pre-interceptor. Checks if current route has `auth` flag enabled and if so
|
|
5
|
-
* applies given predicate function to {@link RequestCtx}. If the
|
|
6
|
-
* returns false, the server responds with
|
|
7
|
-
* following pre-interceptors and
|
|
8
|
-
* post-interceptors (if any) will
|
|
5
|
+
* applies given predicate function to current {@link RequestCtx}. If the
|
|
6
|
+
* predicate returns false, the server responds with
|
|
7
|
+
* {@link ServerResponse.unauthorized} and any following pre-interceptors and
|
|
8
|
+
* the main route handler will be skipped. Only post-interceptors (if any) will
|
|
9
|
+
* still be executed.
|
|
9
10
|
*
|
|
10
11
|
* @remarks
|
|
11
|
-
* If this interceptor is used with the {@link
|
|
12
|
-
* MUST come after it, otherwise the session information will not yet be
|
|
12
|
+
* If this interceptor is used with the {@link sessionInterceptor} interceptor,
|
|
13
|
+
* it MUST come after it, otherwise the session information will not yet be
|
|
13
14
|
* available in the context object given to the predicate.
|
|
14
15
|
*
|
|
15
16
|
* @param pred
|
|
@@ -9,7 +9,7 @@ import type { Interceptor } from "../api.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const logRequest: (level?: LogLevel | LogLevelName) => Interceptor;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Post-interceptor to log response details (status, route, headers) using the
|
|
13
13
|
* server's {@link ServerOpts.logger}. The `level` arg can be used to customize
|
|
14
14
|
* which log level to use.
|
|
15
15
|
*
|
package/interceptors/logging.js
CHANGED
package/interceptors/measure.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -39,19 +39,19 @@
|
|
|
39
39
|
"tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@thi.ng/api": "^8.11.
|
|
43
|
-
"@thi.ng/arrays": "^2.10.
|
|
44
|
-
"@thi.ng/cache": "^2.3.
|
|
45
|
-
"@thi.ng/checks": "^3.
|
|
46
|
-
"@thi.ng/errors": "^2.5.
|
|
47
|
-
"@thi.ng/file-io": "^2.1.
|
|
48
|
-
"@thi.ng/logger": "^3.1.
|
|
49
|
-
"@thi.ng/mime": "^2.7.
|
|
50
|
-
"@thi.ng/paths": "^5.2.
|
|
51
|
-
"@thi.ng/router": "^4.1.
|
|
52
|
-
"@thi.ng/strings": "^3.9.
|
|
53
|
-
"@thi.ng/timestamp": "^1.1.
|
|
54
|
-
"@thi.ng/uuid": "^1.1.
|
|
42
|
+
"@thi.ng/api": "^8.11.21",
|
|
43
|
+
"@thi.ng/arrays": "^2.10.17",
|
|
44
|
+
"@thi.ng/cache": "^2.3.25",
|
|
45
|
+
"@thi.ng/checks": "^3.7.0",
|
|
46
|
+
"@thi.ng/errors": "^2.5.27",
|
|
47
|
+
"@thi.ng/file-io": "^2.1.29",
|
|
48
|
+
"@thi.ng/logger": "^3.1.2",
|
|
49
|
+
"@thi.ng/mime": "^2.7.3",
|
|
50
|
+
"@thi.ng/paths": "^5.2.3",
|
|
51
|
+
"@thi.ng/router": "^4.1.20",
|
|
52
|
+
"@thi.ng/strings": "^3.9.6",
|
|
53
|
+
"@thi.ng/timestamp": "^1.1.6",
|
|
54
|
+
"@thi.ng/uuid": "^1.1.18"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^22.13.1",
|
|
@@ -163,5 +163,5 @@
|
|
|
163
163
|
"status": "alpha",
|
|
164
164
|
"year": 2024
|
|
165
165
|
},
|
|
166
|
-
"gitHead": "
|
|
166
|
+
"gitHead": "2958e6a9881bd9e06de587a2141f6d23c64278db\n"
|
|
167
167
|
}
|
package/server.js
CHANGED
|
@@ -45,13 +45,23 @@ class Server {
|
|
|
45
45
|
host;
|
|
46
46
|
augmentCtx;
|
|
47
47
|
async start() {
|
|
48
|
-
const {
|
|
48
|
+
const {
|
|
49
|
+
ssl,
|
|
50
|
+
host = "localhost",
|
|
51
|
+
port = ssl ? 443 : 8080,
|
|
52
|
+
uniqueHeaders,
|
|
53
|
+
blockList,
|
|
54
|
+
requestTimeout
|
|
55
|
+
} = this.opts;
|
|
49
56
|
try {
|
|
50
57
|
this.server = ssl ? https.createServer(
|
|
51
58
|
{
|
|
52
59
|
key: readText(ssl.key, this.logger),
|
|
53
60
|
cert: readText(ssl.cert, this.logger),
|
|
54
|
-
ServerResponse
|
|
61
|
+
ServerResponse,
|
|
62
|
+
uniqueHeaders,
|
|
63
|
+
blockList,
|
|
64
|
+
requestTimeout
|
|
55
65
|
},
|
|
56
66
|
this.listener.bind(this)
|
|
57
67
|
) : http.createServer(
|
|
@@ -127,19 +137,25 @@ class Server {
|
|
|
127
137
|
}
|
|
128
138
|
}
|
|
129
139
|
async runHandler({ fn, pre, post }, ctx) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
try {
|
|
141
|
+
let failed;
|
|
142
|
+
if (pre) {
|
|
143
|
+
for (let i = 0, n = pre.length; i < n; i++) {
|
|
144
|
+
const fn2 = pre[i];
|
|
145
|
+
if (fn2 && !await fn2(ctx)) {
|
|
146
|
+
ctx.res.end();
|
|
147
|
+
failed = i;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (failed === void 0) await fn(ctx);
|
|
153
|
+
if (post) {
|
|
154
|
+
for (let i = failed ?? post.length; --i >= 0; ) {
|
|
155
|
+
const fn2 = post[i];
|
|
156
|
+
if (fn2) await fn2(ctx);
|
|
135
157
|
}
|
|
136
158
|
}
|
|
137
|
-
return true;
|
|
138
|
-
};
|
|
139
|
-
try {
|
|
140
|
-
if (pre && !await runPhase(pre)) return;
|
|
141
|
-
await fn(ctx);
|
|
142
|
-
if (post && !await runPhase(post)) return;
|
|
143
159
|
ctx.res.end();
|
|
144
160
|
} catch (e) {
|
|
145
161
|
this.logger.warn(`handler error:`, e);
|
|
@@ -150,16 +166,18 @@ class Server {
|
|
|
150
166
|
}
|
|
151
167
|
compileRoute(route) {
|
|
152
168
|
const compilePhase = (handler, phase) => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (x[phase])
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
for (let x of handler.intercept ?? []) {
|
|
159
|
-
if (x[phase]) fns.push(x[phase].bind(x));
|
|
169
|
+
let isPhaseUsed = false;
|
|
170
|
+
const $bind = (iceps) => (iceps ?? []).map((x) => {
|
|
171
|
+
if (x[phase]) {
|
|
172
|
+
isPhaseUsed = true;
|
|
173
|
+
return x[phase].bind(x);
|
|
160
174
|
}
|
|
175
|
+
});
|
|
176
|
+
const fns = [...$bind(this.opts.intercept)];
|
|
177
|
+
if (!isFunction(handler)) {
|
|
178
|
+
fns.push(...$bind(handler.intercept));
|
|
161
179
|
}
|
|
162
|
-
return
|
|
180
|
+
return isPhaseUsed ? fns : void 0;
|
|
163
181
|
};
|
|
164
182
|
const result = { ...route, handlers: {} };
|
|
165
183
|
for (let method in route.handlers) {
|
package/session/memory.d.ts
CHANGED
|
@@ -21,8 +21,8 @@ export interface InMemorySessionOpts<T extends ServerSession = ServerSession> {
|
|
|
21
21
|
initial: Record<string, T>;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Session storage implementation for use with {@link
|
|
25
|
-
* in-memory TLRU Cache with configurable TTL.
|
|
24
|
+
* Session storage implementation for use with {@link sessionInterceptor}, using
|
|
25
|
+
* an in-memory TLRU Cache with configurable TTL.
|
|
26
26
|
*/
|
|
27
27
|
export declare class InMemorySessionStore<T extends ServerSession = ServerSession> implements ISessionStore<T> {
|
|
28
28
|
readonly ttl: number;
|
package/session/session.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { Fn } from "@thi.ng/api";
|
|
2
2
|
import { ServerResponse } from "node:http";
|
|
3
3
|
import type { Interceptor, ISessionStore, RequestCtx, ServerSession } from "../api.js";
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for {@link SessionInterceptor}.
|
|
6
|
+
*/
|
|
4
7
|
export interface SessionOpts<CTX extends RequestCtx = RequestCtx, SESSION extends ServerSession = ServerSession> {
|
|
5
8
|
/**
|
|
6
|
-
* Factory function to create a new session object. See
|
|
9
|
+
* Factory function to create a new session object. See
|
|
10
|
+
* {@link createSession} for a base implementation.
|
|
7
11
|
*/
|
|
8
12
|
factory: Fn<CTX, SESSION>;
|
|
9
13
|
/**
|
|
@@ -28,15 +32,50 @@ export interface SessionOpts<CTX extends RequestCtx = RequestCtx, SESSION extend
|
|
|
28
32
|
*/
|
|
29
33
|
secret?: number | string | Buffer;
|
|
30
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Cached session metadata, stored in a WeakMap.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export interface SessionMeta {
|
|
41
|
+
hmac: Buffer;
|
|
42
|
+
cookie: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pre-interceptor which parses & validates session cookie (if available) from
|
|
46
|
+
* current request and injects/updates session cookie in response. Only a signed
|
|
47
|
+
* session ID will be stored in the cookie. Thr actual session data and HMAC is
|
|
48
|
+
* held server side (via configured session storage, see
|
|
49
|
+
* {@link SessionOpts.store}; by default uses {@link InMemorySessionStore}).
|
|
50
|
+
*/
|
|
31
51
|
export declare class SessionInterceptor<CTX extends RequestCtx = RequestCtx, SESSION extends ServerSession = ServerSession> implements Interceptor<CTX> {
|
|
32
|
-
factory:
|
|
52
|
+
factory: SessionOpts<CTX, SESSION>["factory"];
|
|
33
53
|
store: ISessionStore<SESSION>;
|
|
54
|
+
meta: WeakMap<SESSION, SessionMeta>;
|
|
55
|
+
secret: Buffer;
|
|
34
56
|
cookieName: string;
|
|
35
57
|
cookieOpts: string;
|
|
36
|
-
secret: Buffer;
|
|
37
58
|
constructor({ factory, store, cookieName, cookieOpts, secret, }: SessionOpts<CTX, SESSION>);
|
|
38
59
|
pre(ctx: CTX): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Attempts to delete session for given ID and if successful also sets
|
|
62
|
+
* force-expired cookie in response.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* Intended for logout handlers and/or switching sessions when a user has
|
|
66
|
+
* successfully authenticated (to avoid session fixation).
|
|
67
|
+
*
|
|
68
|
+
* @param ctx
|
|
69
|
+
* @param sessionID
|
|
70
|
+
*/
|
|
39
71
|
deleteSession(ctx: CTX, sessionID: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Creates a new session object (via configured {@link SessionOpts.factory}), pre-computes HMAC
|
|
74
|
+
* and submits it to configured {@link SessionOpts.store}. If successful, Returns session
|
|
75
|
+
* , otherwise returns `undefined`.
|
|
76
|
+
*
|
|
77
|
+
* @param ctx
|
|
78
|
+
*/
|
|
40
79
|
newSession(ctx: CTX): Promise<SESSION | undefined>;
|
|
41
80
|
/**
|
|
42
81
|
* Calls {@link SessionInterceptor.newSession} to create a new session and,
|
|
@@ -46,15 +85,16 @@ export declare class SessionInterceptor<CTX extends RequestCtx = RequestCtx, SES
|
|
|
46
85
|
* @param ctx
|
|
47
86
|
*/
|
|
48
87
|
replaceSession(ctx: CTX): Promise<SESSION | undefined>;
|
|
49
|
-
withSession(res: ServerResponse,
|
|
50
|
-
validateSession(cookie: string):
|
|
88
|
+
withSession(res: ServerResponse, session: SESSION): ServerResponse<import("http").IncomingMessage>;
|
|
89
|
+
validateSession(cookie: string): Promise<SESSION | undefined>;
|
|
51
90
|
}
|
|
52
91
|
/**
|
|
53
|
-
* Factory function to create a new {@link SessionInterceptor} instance
|
|
92
|
+
* Factory function to create a new {@link SessionInterceptor} instance
|
|
93
|
+
* configured with given options.
|
|
54
94
|
*
|
|
55
95
|
* @param opts
|
|
56
96
|
*/
|
|
57
|
-
export declare const
|
|
97
|
+
export declare const sessionInterceptor: <CTX extends RequestCtx = RequestCtx, SESSION extends ServerSession = ServerSession>(opts: SessionOpts<CTX, SESSION>) => SessionInterceptor<CTX, SESSION>;
|
|
58
98
|
/**
|
|
59
99
|
* Creates a new basic {@link ServerSession}, using a UUID v4 for
|
|
60
100
|
* {@link ServerSession.id}.
|
package/session/session.js
CHANGED
|
@@ -6,9 +6,10 @@ import { inMemorySessionStore } from "./memory.js";
|
|
|
6
6
|
class SessionInterceptor {
|
|
7
7
|
factory;
|
|
8
8
|
store;
|
|
9
|
+
meta = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
secret;
|
|
9
11
|
cookieName;
|
|
10
12
|
cookieOpts;
|
|
11
|
-
secret;
|
|
12
13
|
constructor({
|
|
13
14
|
factory,
|
|
14
15
|
store = inMemorySessionStore(),
|
|
@@ -24,23 +25,33 @@ class SessionInterceptor {
|
|
|
24
25
|
}
|
|
25
26
|
async pre(ctx) {
|
|
26
27
|
const cookie = ctx.cookies?.[this.cookieName];
|
|
27
|
-
let
|
|
28
|
+
let session;
|
|
28
29
|
if (cookie) {
|
|
29
|
-
|
|
30
|
-
if (!
|
|
30
|
+
session = await this.validateSession(cookie);
|
|
31
|
+
if (!session) {
|
|
31
32
|
ctx.res.forbidden();
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
let session = id ? await this.store.get(id) : void 0;
|
|
36
36
|
if (!session || session.ip !== ctx.req.socket.remoteAddress) {
|
|
37
37
|
session = await this.newSession(ctx);
|
|
38
38
|
if (!session) return false;
|
|
39
39
|
}
|
|
40
40
|
ctx.session = session;
|
|
41
|
-
this.withSession(ctx.res, session
|
|
41
|
+
this.withSession(ctx.res, session);
|
|
42
42
|
return true;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Attempts to delete session for given ID and if successful also sets
|
|
46
|
+
* force-expired cookie in response.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* Intended for logout handlers and/or switching sessions when a user has
|
|
50
|
+
* successfully authenticated (to avoid session fixation).
|
|
51
|
+
*
|
|
52
|
+
* @param ctx
|
|
53
|
+
* @param sessionID
|
|
54
|
+
*/
|
|
44
55
|
async deleteSession(ctx, sessionID) {
|
|
45
56
|
if (await this.store.delete(sessionID)) {
|
|
46
57
|
ctx.logger.info("delete session:", sessionID);
|
|
@@ -51,6 +62,13 @@ class SessionInterceptor {
|
|
|
51
62
|
);
|
|
52
63
|
}
|
|
53
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new session object (via configured {@link SessionOpts.factory}), pre-computes HMAC
|
|
67
|
+
* and submits it to configured {@link SessionOpts.store}. If successful, Returns session
|
|
68
|
+
* , otherwise returns `undefined`.
|
|
69
|
+
*
|
|
70
|
+
* @param ctx
|
|
71
|
+
*/
|
|
54
72
|
async newSession(ctx) {
|
|
55
73
|
const session = this.factory(ctx);
|
|
56
74
|
ctx.logger.info("new session:", session.id);
|
|
@@ -58,6 +76,11 @@ class SessionInterceptor {
|
|
|
58
76
|
ctx.logger.warn("could not store session...");
|
|
59
77
|
return;
|
|
60
78
|
}
|
|
79
|
+
const hmac = createHmac("sha256", this.secret).update(session.id, "ascii").update(randomBytes(8)).digest();
|
|
80
|
+
this.meta.set(session, {
|
|
81
|
+
hmac,
|
|
82
|
+
cookie: session.id + ":" + hmac.toString("base64url")
|
|
83
|
+
});
|
|
61
84
|
return session;
|
|
62
85
|
}
|
|
63
86
|
/**
|
|
@@ -72,31 +95,30 @@ class SessionInterceptor {
|
|
|
72
95
|
if (session) {
|
|
73
96
|
if (ctx.session?.id) this.store.delete(ctx.session.id);
|
|
74
97
|
ctx.session = session;
|
|
75
|
-
this.withSession(ctx.res, session
|
|
98
|
+
this.withSession(ctx.res, session);
|
|
76
99
|
return session;
|
|
77
100
|
}
|
|
78
101
|
}
|
|
79
|
-
withSession(res,
|
|
80
|
-
const cookie =
|
|
81
|
-
const signature = createHmac("sha256", this.secret).update(cookie, "ascii").digest().toString("base64url");
|
|
102
|
+
withSession(res, session) {
|
|
103
|
+
const cookie = this.meta.get(session)?.cookie;
|
|
82
104
|
return res.appendHeader(
|
|
83
105
|
"set-cookie",
|
|
84
|
-
`${this.cookieName}=${cookie}
|
|
106
|
+
`${this.cookieName}=${cookie};Max-Age=${this.store.ttl};${this.cookieOpts}`
|
|
85
107
|
);
|
|
86
108
|
}
|
|
87
|
-
validateSession(cookie) {
|
|
109
|
+
async validateSession(cookie) {
|
|
88
110
|
const parts = cookie.split(":");
|
|
89
|
-
if (parts.length
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
if (parts.length !== 2) return;
|
|
112
|
+
const session = await this.store.get(parts[0]);
|
|
113
|
+
if (!session) return;
|
|
114
|
+
const actual = Buffer.from(parts[1], "base64url");
|
|
115
|
+
const expected = this.meta.get(session)?.hmac;
|
|
116
|
+
if (!expected) return;
|
|
95
117
|
const sameLength = actual.length === expected.length;
|
|
96
|
-
return timingSafeEqual(sameLength ? actual : expected, expected) && sameLength ?
|
|
118
|
+
return timingSafeEqual(sameLength ? actual : expected, expected) && sameLength ? session : void 0;
|
|
97
119
|
}
|
|
98
120
|
}
|
|
99
|
-
const
|
|
121
|
+
const sessionInterceptor = (opts) => new SessionInterceptor(opts);
|
|
100
122
|
const createSession = (ctx) => ({
|
|
101
123
|
id: uuid(),
|
|
102
124
|
ip: ctx.req.socket.remoteAddress
|
|
@@ -104,5 +126,5 @@ const createSession = (ctx) => ({
|
|
|
104
126
|
export {
|
|
105
127
|
SessionInterceptor,
|
|
106
128
|
createSession,
|
|
107
|
-
|
|
129
|
+
sessionInterceptor
|
|
108
130
|
};
|