@thi.ng/server 0.1.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.
@@ -0,0 +1,25 @@
1
+ import type { Interceptor } from "../api";
2
+ export interface CacheControlOpts {
3
+ maxAge: number;
4
+ sMaxAge: number;
5
+ noCache: boolean;
6
+ noStore: boolean;
7
+ noTransform: boolean;
8
+ mustRevalidate: boolean;
9
+ proxyRevalidate: boolean;
10
+ mustUnderstand: boolean;
11
+ private: boolean;
12
+ public: boolean;
13
+ immutable: boolean;
14
+ staleWhileRevalidate: boolean;
15
+ staleIfError: boolean;
16
+ }
17
+ /**
18
+ * @remarks
19
+ * Reference:
20
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
21
+ *
22
+ * @param opts
23
+ */
24
+ export declare const cacheControl: (opts?: Partial<CacheControlOpts>) => Interceptor;
25
+ //# sourceMappingURL=cache-control.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { kebab } from "@thi.ng/strings";
2
+ const cacheControl = (opts = {}) => {
3
+ const acc = [];
4
+ for (let [k, v] of Object.entries(opts)) {
5
+ switch (k) {
6
+ case "maxAge":
7
+ acc.push("max-age=" + v);
8
+ break;
9
+ case "sMaxAge":
10
+ acc.push("s-maxage=" + v);
11
+ break;
12
+ default:
13
+ acc.push(kebab(k));
14
+ }
15
+ }
16
+ const value = acc.join(", ");
17
+ return {
18
+ pre: (ctx) => (value && ctx.res.setHeader("cache-control", value), true)
19
+ };
20
+ };
21
+ export {
22
+ cacheControl
23
+ };
@@ -0,0 +1,8 @@
1
+ import type { Interceptor } from "../api.js";
2
+ /**
3
+ * Pre-interceptor to inject given headers into the response.
4
+ *
5
+ * @param headers
6
+ */
7
+ export declare const injectHeaders: (headers: Record<string, string | string[]>) => Interceptor;
8
+ //# sourceMappingURL=inject-headers.d.ts.map
@@ -0,0 +1,11 @@
1
+ const injectHeaders = (headers) => ({
2
+ pre: (ctx) => {
3
+ for (let header of Object.entries(headers)) {
4
+ ctx.res.appendHeader(...header);
5
+ }
6
+ return true;
7
+ }
8
+ });
9
+ export {
10
+ injectHeaders
11
+ };
@@ -0,0 +1,19 @@
1
+ import { LogLevel, type LogLevelName } from "@thi.ng/logger";
2
+ import type { Interceptor } from "../api.js";
3
+ /**
4
+ * Pre-interceptor to log request details (route, headers, query string args)
5
+ * using the server's {@link ServerOpts.logger}. The `level` arg can be used to
6
+ * customize which log level to use.
7
+ *
8
+ * @param level
9
+ */
10
+ export declare const logRequest: (level?: LogLevel | LogLevelName) => Interceptor;
11
+ /**
12
+ * Pre-interceptor to log response details (status, route, headers) using the
13
+ * server's {@link ServerOpts.logger}. The `level` arg can be used to customize
14
+ * which log level to use.
15
+ *
16
+ * @param level
17
+ */
18
+ export declare const logResponse: (level?: LogLevel | LogLevelName) => Interceptor;
19
+ //# sourceMappingURL=logging.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { isString } from "@thi.ng/checks";
2
+ import { LogLevel } from "@thi.ng/logger";
3
+ const __method = (level) => (isString(level) ? level : LogLevel[level]).toLowerCase();
4
+ const logRequest = (level = "INFO") => {
5
+ const method = __method(level);
6
+ return {
7
+ pre: ({ logger, req, match, query }) => {
8
+ logger[method]("request route", req.method, match);
9
+ logger[method]("request headers", req.headers);
10
+ if (Object.keys(query).length) {
11
+ logger[method]("request query", query);
12
+ }
13
+ return true;
14
+ }
15
+ };
16
+ };
17
+ const logResponse = (level = "INFO") => {
18
+ const method = __method(level);
19
+ return {
20
+ post: ({ logger, match, res }) => {
21
+ logger[method]("response status", res.statusCode, match);
22
+ logger[method]("response headers", res.getHeaders());
23
+ return true;
24
+ }
25
+ };
26
+ };
27
+ export {
28
+ logRequest,
29
+ logResponse
30
+ };
@@ -0,0 +1,11 @@
1
+ import type { Interceptor } from "../api.js";
2
+ export type ReferrerPolicy = "no-referrer" | "no-referrer-when-downgrade" | "origin" | "origin-when-cross-origin" | "same-origin" | "strict-origin" | "strict-origin-when-cross-origin" | "unsafe-url";
3
+ /**
4
+ * @remarks
5
+ * Reference:
6
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
7
+ *
8
+ * @param policy
9
+ */
10
+ export declare const referrerPolicy: (policy?: ReferrerPolicy) => Interceptor;
11
+ //# sourceMappingURL=referrer-policy.d.ts.map
@@ -0,0 +1,6 @@
1
+ const referrerPolicy = (policy = "no-referrer") => ({
2
+ pre: (ctx) => (ctx.res.setHeader("referrer-policy", policy), true)
3
+ });
4
+ export {
5
+ referrerPolicy
6
+ };
@@ -0,0 +1,51 @@
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
@@ -0,0 +1,38 @@
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
+ };
@@ -0,0 +1,11 @@
1
+ import type { Interceptor } from "../api.js";
2
+ /**
3
+ * @remarks
4
+ * Reference:
5
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
6
+ *
7
+ * @param maxAge
8
+ * @param includeSubDomains
9
+ */
10
+ export declare const strictTransportSecurity: (maxAge?: number, includeSubDomains?: boolean) => Interceptor;
11
+ //# sourceMappingURL=strict-transport.d.ts.map
@@ -0,0 +1,9 @@
1
+ const strictTransportSecurity = (maxAge = 63072e3, includeSubDomains = true) => {
2
+ const value = `max-age=${maxAge}${includeSubDomains ? "; includeSubDomains" : ""}`;
3
+ return {
4
+ pre: (ctx) => (ctx.res.setHeader("strict-transport-security", value), true)
5
+ };
6
+ };
7
+ export {
8
+ strictTransportSecurity
9
+ };
@@ -0,0 +1,11 @@
1
+ import type { Interceptor } from "../api.js";
2
+ export type CrossOriginOpenerPolicy = "unsafe-none" | "same-origin-allow-popups" | "same-origin" | "noopener-allow-popups";
3
+ /**
4
+ * @remarks
5
+ * Reference:
6
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy
7
+ *
8
+ * @param policy
9
+ */
10
+ export declare const crossOriginOpenerPolicy: (policy?: CrossOriginOpenerPolicy) => Interceptor;
11
+ //# sourceMappingURL=x-origin-opener.d.ts.map
@@ -0,0 +1,6 @@
1
+ const crossOriginOpenerPolicy = (policy = "same-origin") => ({
2
+ pre: (ctx) => (ctx.res.setHeader("cross-origin-opener-policy", policy), true)
3
+ });
4
+ export {
5
+ crossOriginOpenerPolicy
6
+ };
@@ -0,0 +1,11 @@
1
+ import type { Interceptor } from "../api.js";
2
+ export type CrossOriginResourcePolicy = "same-origin" | "same-site" | "cross-origin";
3
+ /**
4
+ * @remarks
5
+ * Reference:
6
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy
7
+ *
8
+ * @param policy
9
+ */
10
+ export declare const crossOriginResourcePolicy: (policy?: CrossOriginResourcePolicy) => Interceptor;
11
+ //# sourceMappingURL=x-origin-resource.d.ts.map
@@ -0,0 +1,6 @@
1
+ const crossOriginResourcePolicy = (policy = "same-origin") => ({
2
+ pre: (ctx) => (ctx.res.setHeader("cross-origin-resource-policy", policy), true)
3
+ });
4
+ export {
5
+ crossOriginResourcePolicy
6
+ };
package/package.json ADDED
@@ -0,0 +1,153 @@
1
+ {
2
+ "name": "@thi.ng/server",
3
+ "version": "0.1.0",
4
+ "description": "Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors",
5
+ "type": "module",
6
+ "module": "./index.js",
7
+ "typings": "./index.d.ts",
8
+ "sideEffects": false,
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/thi-ng/umbrella.git"
12
+ },
13
+ "homepage": "https://thi.ng/server",
14
+ "funding": [
15
+ {
16
+ "type": "github",
17
+ "url": "https://github.com/sponsors/postspectacular"
18
+ },
19
+ {
20
+ "type": "patreon",
21
+ "url": "https://patreon.com/thing_umbrella"
22
+ },
23
+ {
24
+ "type": "liberapay",
25
+ "url": "https://liberapay.com/thi.ng"
26
+ }
27
+ ],
28
+ "author": "Karsten Schmidt (https://thi.ng)",
29
+ "license": "Apache-2.0",
30
+ "scripts": {
31
+ "build": "yarn build:esbuild && yarn build:decl",
32
+ "build:decl": "tsc --declaration --emitDeclarationOnly",
33
+ "build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts",
34
+ "clean": "bun ../../tools/src/clean-package.ts",
35
+ "doc": "typedoc --options ../../typedoc.json --out doc src/index.ts",
36
+ "doc:readme": "bun ../../tools/src/module-stats.ts && bun ../../tools/src/readme.ts",
37
+ "pub": "yarn npm publish --access public",
38
+ "test": "bun test",
39
+ "tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts"
40
+ },
41
+ "dependencies": {
42
+ "@thi.ng/api": "^8.11.19",
43
+ "@thi.ng/arrays": "^2.10.14",
44
+ "@thi.ng/cache": "^2.3.22",
45
+ "@thi.ng/checks": "^3.6.22",
46
+ "@thi.ng/errors": "^2.5.25",
47
+ "@thi.ng/file-io": "^2.1.25",
48
+ "@thi.ng/logger": "^3.0.30",
49
+ "@thi.ng/mime": "^2.7.0",
50
+ "@thi.ng/paths": "^5.2.0",
51
+ "@thi.ng/router": "^4.1.17",
52
+ "@thi.ng/strings": "^3.9.4",
53
+ "@thi.ng/uuid": "^1.1.16"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^22.10.10",
57
+ "esbuild": "^0.24.2",
58
+ "typedoc": "^0.27.6",
59
+ "typescript": "^5.7.3"
60
+ },
61
+ "keywords": [
62
+ "cookie",
63
+ "compression",
64
+ "file",
65
+ "formdata",
66
+ "headers",
67
+ "http",
68
+ "https",
69
+ "interceptors",
70
+ "logger",
71
+ "multipart",
72
+ "nodejs",
73
+ "querystring",
74
+ "security",
75
+ "server",
76
+ "session",
77
+ "typescript"
78
+ ],
79
+ "publishConfig": {
80
+ "access": "public"
81
+ },
82
+ "browser": {
83
+ "process": false,
84
+ "setTimeout": false
85
+ },
86
+ "engines": {
87
+ "node": ">=18"
88
+ },
89
+ "files": [
90
+ "./*.js",
91
+ "./*.d.ts",
92
+ "interceptors",
93
+ "utils"
94
+ ],
95
+ "exports": {
96
+ ".": {
97
+ "default": "./index.js"
98
+ },
99
+ "./api": {
100
+ "default": "./api.js"
101
+ },
102
+ "./interceptors/auth-route": {
103
+ "default": "./interceptors/auth-route.js"
104
+ },
105
+ "./interceptors/cache-control": {
106
+ "default": "./interceptors/cache-control.js"
107
+ },
108
+ "./interceptors/inject-headers": {
109
+ "default": "./interceptors/inject-headers.js"
110
+ },
111
+ "./interceptors/logging": {
112
+ "default": "./interceptors/logging.js"
113
+ },
114
+ "./interceptors/referrer-policy": {
115
+ "default": "./interceptors/referrer-policy.js"
116
+ },
117
+ "./interceptors/session": {
118
+ "default": "./interceptors/session.js"
119
+ },
120
+ "./interceptors/strict-transport": {
121
+ "default": "./interceptors/strict-transport.js"
122
+ },
123
+ "./interceptors/x-origin-opener": {
124
+ "default": "./interceptors/x-origin-opener.js"
125
+ },
126
+ "./interceptors/x-origin-resource": {
127
+ "default": "./interceptors/x-origin-resource.js"
128
+ },
129
+ "./server": {
130
+ "default": "./server.js"
131
+ },
132
+ "./static": {
133
+ "default": "./static.js"
134
+ },
135
+ "./utils/cache": {
136
+ "default": "./utils/cache.js"
137
+ },
138
+ "./utils/cookies": {
139
+ "default": "./utils/cookies.js"
140
+ },
141
+ "./utils/formdata": {
142
+ "default": "./utils/formdata.js"
143
+ },
144
+ "./utils/multipart": {
145
+ "default": "./utils/multipart.js"
146
+ }
147
+ },
148
+ "thi.ng": {
149
+ "status": "alpha",
150
+ "year": 2024
151
+ },
152
+ "gitHead": "fc1d498e8d4b690db873c30cc594352a804e7a65\n"
153
+ }
package/server.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { type ILogger } from "@thi.ng/logger";
2
+ import { Router, type RouteMatch } from "@thi.ng/router";
3
+ import * as http from "node:http";
4
+ import type { CompiledHandler, CompiledServerRoute, RequestCtx, ServerOpts, ServerRoute } from "./api.js";
5
+ export declare class Server {
6
+ opts: Partial<ServerOpts>;
7
+ logger: ILogger;
8
+ router: Router;
9
+ server: http.Server;
10
+ constructor(opts?: Partial<ServerOpts>);
11
+ start(): Promise<boolean>;
12
+ 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;
17
+ 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;
23
+ }
24
+ export declare const server: (opts?: Partial<ServerOpts>) => Server;
25
+ //# sourceMappingURL=server.d.ts.map