@rexeus/typeweaver-server 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -261,6 +261,35 @@ type AppState = InferState<typeof app>;
261
261
  Middleware runs for **all** requests, including 404s and 405s, so global concerns like logging and
262
262
  CORS always execute.
263
263
 
264
+ ### 📦 Built-in Middleware
265
+
266
+ Ready-to-use middleware included with the server plugin.
267
+
268
+ | Middleware | Description | State |
269
+ | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | --------------- |
270
+ | [`cors`](https://github.com/rexeus/typeweaver/blob/main/packages/server/docs/middleware/cors.md) | CORS headers & preflight handling | — |
271
+ | [`basicAuth`](https://github.com/rexeus/typeweaver/blob/main/packages/server/docs/middleware/basic-auth.md) | HTTP Basic Authentication | `{ username }` |
272
+ | [`bearerAuth`](https://github.com/rexeus/typeweaver/blob/main/packages/server/docs/middleware/bearer-auth.md) | HTTP Bearer Token Authentication | `{ token }` |
273
+ | [`logger`](https://github.com/rexeus/typeweaver/blob/main/packages/server/docs/middleware/logger.md) | Request/response logging with timing | — |
274
+ | [`secureHeaders`](https://github.com/rexeus/typeweaver/blob/main/packages/server/docs/middleware/secure-headers.md) | OWASP security headers | — |
275
+ | [`requestId`](https://github.com/rexeus/typeweaver/blob/main/packages/server/docs/middleware/request-id.md) | Request ID generation & propagation | `{ requestId }` |
276
+ | [`poweredBy`](https://github.com/rexeus/typeweaver/blob/main/packages/server/docs/middleware/powered-by.md) | `X-Powered-By` header | — |
277
+ | [`scoped` / `except`](https://github.com/rexeus/typeweaver/blob/main/packages/server/docs/middleware/scoped.md) | Path-based middleware filtering | — |
278
+
279
+ ```ts
280
+ import { cors, logger, secureHeaders, bearerAuth, requestId } from "@rexeus/typeweaver-server";
281
+
282
+ const app = new TypeweaverApp()
283
+ .use(cors())
284
+ .use(secureHeaders())
285
+ .use(logger())
286
+ .use(requestId())
287
+ .use(bearerAuth({ verifyToken: verify }))
288
+ .route(new UserRouter({ requestHandlers }));
289
+ ```
290
+
291
+ Each middleware is documented in detail — click the links above.
292
+
264
293
  ### 🛠️ App Options
265
294
 
266
295
  `TypeweaverApp` accepts an optional options object:
@@ -21,7 +21,7 @@ export type RouteDefinition = {
21
21
  readonly method: HttpMethod;
22
22
  readonly path: string;
23
23
  readonly validator: IRequestValidator;
24
- readonly handler: RequestHandler;
24
+ readonly handler: RequestHandler<any, any, any>;
25
25
  /** Reference to the router config for error handling. */
26
26
  readonly routerConfig: RouterErrorConfig;
27
27
  };
@@ -118,7 +118,7 @@ export abstract class TypeweaverRouter<
118
118
  method: HttpMethod,
119
119
  path: string,
120
120
  validator: IRequestValidator,
121
- handler: RequestHandler
121
+ handler: RequestHandler<any, any, any>
122
122
  ): void {
123
123
  this.routes.push({
124
124
  method,
package/dist/lib/index.ts CHANGED
@@ -36,3 +36,22 @@ export {
36
36
  export { FetchApiAdapter } from "./FetchApiAdapter";
37
37
  export { nodeAdapter, type NodeAdapterOptions } from "./NodeAdapter";
38
38
  export { pathMatcher } from "./PathMatcher";
39
+ export {
40
+ basicAuth,
41
+ type BasicAuthOptions,
42
+ bearerAuth,
43
+ type BearerAuthOptions,
44
+ cors,
45
+ type CorsOptions,
46
+ except,
47
+ logger,
48
+ type LogData,
49
+ type LoggerOptions,
50
+ poweredBy,
51
+ type PoweredByOptions,
52
+ requestId,
53
+ type RequestIdOptions,
54
+ scoped,
55
+ secureHeaders,
56
+ type SecureHeadersOptions,
57
+ } from "./middleware/index";
@@ -0,0 +1,64 @@
1
+ import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
+ import { defineMiddleware } from "../TypedMiddleware";
3
+ import type { ServerContext } from "../ServerContext";
4
+
5
+ export type BasicAuthOptions = {
6
+ readonly verifyCredentials: (
7
+ username: string,
8
+ password: string,
9
+ ctx: ServerContext
10
+ ) => boolean | Promise<boolean>;
11
+ readonly realm?: string;
12
+ readonly unauthorizedMessage?: string;
13
+ readonly onUnauthorized?: (ctx: ServerContext) => IHttpResponse;
14
+ };
15
+
16
+ const BASIC_PREFIX = "Basic ";
17
+
18
+ export function basicAuth(options: BasicAuthOptions) {
19
+ const realm = options.realm ?? "Secure Area";
20
+ const message = options.unauthorizedMessage ?? "Unauthorized";
21
+
22
+ const defaultResponse: IHttpResponse = {
23
+ statusCode: 401,
24
+ header: { "www-authenticate": `Basic realm="${realm}"` },
25
+ body: { code: "UNAUTHORIZED", message },
26
+ };
27
+
28
+ const deny = (ctx: ServerContext): IHttpResponse =>
29
+ options.onUnauthorized?.(ctx) ?? defaultResponse;
30
+
31
+ return defineMiddleware<{ username: string }>(async (ctx, next) => {
32
+ const authorization = ctx.request.header?.["authorization"];
33
+ if (typeof authorization !== "string") return deny(ctx);
34
+
35
+ if (!authorization.startsWith(BASIC_PREFIX)) return deny(ctx);
36
+
37
+ const encoded = authorization.slice(BASIC_PREFIX.length);
38
+ if (encoded.length === 0) return deny(ctx);
39
+
40
+ let decoded: string;
41
+ try {
42
+ decoded = atob(encoded);
43
+ } catch {
44
+ return deny(ctx);
45
+ }
46
+
47
+ const colonIndex = decoded.indexOf(":");
48
+ if (colonIndex <= 0) return deny(ctx);
49
+
50
+ const username = decoded.slice(0, colonIndex);
51
+ const password = decoded.slice(colonIndex + 1);
52
+
53
+ let valid: boolean;
54
+ try {
55
+ valid = await options.verifyCredentials(username, password, ctx);
56
+ } catch {
57
+ return deny(ctx);
58
+ }
59
+
60
+ if (!valid) return deny(ctx);
61
+
62
+ return next({ username });
63
+ });
64
+ }
@@ -0,0 +1,50 @@
1
+ import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
+ import { defineMiddleware } from "../TypedMiddleware";
3
+ import type { ServerContext } from "../ServerContext";
4
+
5
+ export type BearerAuthOptions = {
6
+ readonly verifyToken: (
7
+ token: string,
8
+ ctx: ServerContext
9
+ ) => boolean | Promise<boolean>;
10
+ readonly realm?: string;
11
+ readonly unauthorizedMessage?: string;
12
+ readonly onUnauthorized?: (ctx: ServerContext) => IHttpResponse;
13
+ };
14
+
15
+ const BEARER_PREFIX = "Bearer ";
16
+
17
+ export function bearerAuth(options: BearerAuthOptions) {
18
+ const realm = options.realm ?? "Secure Area";
19
+ const message = options.unauthorizedMessage ?? "Unauthorized";
20
+
21
+ const defaultResponse: IHttpResponse = {
22
+ statusCode: 401,
23
+ header: { "www-authenticate": `Bearer realm="${realm}"` },
24
+ body: { code: "UNAUTHORIZED", message },
25
+ };
26
+
27
+ const deny = (ctx: ServerContext): IHttpResponse =>
28
+ options.onUnauthorized?.(ctx) ?? defaultResponse;
29
+
30
+ return defineMiddleware<{ token: string }>(async (ctx, next) => {
31
+ const authorization = ctx.request.header?.["authorization"];
32
+ if (typeof authorization !== "string") return deny(ctx);
33
+
34
+ if (!authorization.startsWith(BEARER_PREFIX)) return deny(ctx);
35
+
36
+ const token = authorization.slice(BEARER_PREFIX.length);
37
+ if (token.length === 0) return deny(ctx);
38
+
39
+ let valid: boolean;
40
+ try {
41
+ valid = await options.verifyToken(token, ctx);
42
+ } catch {
43
+ return deny(ctx);
44
+ }
45
+
46
+ if (!valid) return deny(ctx);
47
+
48
+ return next({ token });
49
+ });
50
+ }
@@ -0,0 +1,119 @@
1
+ import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
+ import { defineMiddleware } from "../TypedMiddleware";
3
+
4
+ export type CorsOptions = {
5
+ readonly origin?:
6
+ | string
7
+ | readonly string[]
8
+ | ((origin: string) => string | undefined);
9
+ readonly allowMethods?: readonly string[];
10
+ readonly allowHeaders?: readonly string[];
11
+ readonly exposeHeaders?: readonly string[];
12
+ readonly maxAge?: number;
13
+ readonly credentials?: boolean;
14
+ };
15
+
16
+ const DEFAULT_METHODS = [
17
+ "GET",
18
+ "HEAD",
19
+ "PUT",
20
+ "POST",
21
+ "PATCH",
22
+ "DELETE",
23
+ ] as const;
24
+
25
+ function resolveOrigin(
26
+ configOrigin: CorsOptions["origin"],
27
+ requestOrigin: string | undefined,
28
+ credentials: boolean
29
+ ): string | undefined {
30
+ if (configOrigin === undefined || configOrigin === "*") {
31
+ if (credentials && requestOrigin) return requestOrigin;
32
+ return "*";
33
+ }
34
+
35
+ if (typeof configOrigin === "function") {
36
+ return requestOrigin ? configOrigin(requestOrigin) : undefined;
37
+ }
38
+
39
+ if (typeof configOrigin === "string") {
40
+ return configOrigin;
41
+ }
42
+
43
+ return requestOrigin && configOrigin.includes(requestOrigin)
44
+ ? requestOrigin
45
+ : undefined;
46
+ }
47
+
48
+ function getRequestOrigin(
49
+ header: Record<string, string | string[]> | undefined
50
+ ): string | undefined {
51
+ const origin = header?.["origin"];
52
+ return typeof origin === "string" ? origin : undefined;
53
+ }
54
+
55
+ export function cors(options?: CorsOptions) {
56
+ const credentials = options?.credentials ?? false;
57
+ const methods = (options?.allowMethods ?? DEFAULT_METHODS).join(", ");
58
+ const exposeHeaders = options?.exposeHeaders?.join(", ");
59
+ const maxAge = options?.maxAge?.toString();
60
+
61
+ return defineMiddleware(async (ctx, next) => {
62
+ const requestOrigin = getRequestOrigin(ctx.request.header);
63
+ const origin = resolveOrigin(options?.origin, requestOrigin, credentials);
64
+
65
+ if (origin === undefined) return next();
66
+
67
+ const corsHeaders: Record<string, string> = {
68
+ "access-control-allow-origin": origin,
69
+ };
70
+
71
+ if (credentials) {
72
+ corsHeaders["access-control-allow-credentials"] = "true";
73
+ }
74
+
75
+ if (origin !== "*") {
76
+ corsHeaders["vary"] = "Origin";
77
+ }
78
+
79
+ if (exposeHeaders) {
80
+ corsHeaders["access-control-expose-headers"] = exposeHeaders;
81
+ }
82
+
83
+ const isPreflight =
84
+ ctx.request.method === "OPTIONS" &&
85
+ ctx.request.header?.["access-control-request-method"] !== undefined;
86
+
87
+ if (isPreflight) {
88
+ corsHeaders["access-control-allow-methods"] = methods;
89
+
90
+ const configuredHeaders = options?.allowHeaders;
91
+ if (configuredHeaders && configuredHeaders.length > 0) {
92
+ corsHeaders["access-control-allow-headers"] =
93
+ configuredHeaders.join(", ");
94
+ } else {
95
+ const requestedHeaders =
96
+ ctx.request.header?.["access-control-request-headers"];
97
+ if (typeof requestedHeaders === "string") {
98
+ corsHeaders["access-control-allow-headers"] = requestedHeaders;
99
+ }
100
+ }
101
+
102
+ if (maxAge) {
103
+ corsHeaders["access-control-max-age"] = maxAge;
104
+ }
105
+
106
+ return {
107
+ statusCode: 204,
108
+ header: corsHeaders,
109
+ } satisfies IHttpResponse;
110
+ }
111
+
112
+ const response = await next();
113
+
114
+ return {
115
+ ...response,
116
+ header: { ...corsHeaders, ...response.header },
117
+ } satisfies IHttpResponse;
118
+ });
119
+ }
@@ -0,0 +1,8 @@
1
+ export { basicAuth, type BasicAuthOptions } from "./basicAuth";
2
+ export { bearerAuth, type BearerAuthOptions } from "./bearerAuth";
3
+ export { cors, type CorsOptions } from "./cors";
4
+ export { logger, type LogData, type LoggerOptions } from "./logger";
5
+ export { poweredBy, type PoweredByOptions } from "./poweredBy";
6
+ export { requestId, type RequestIdOptions } from "./requestId";
7
+ export { scoped, except } from "./scoped";
8
+ export { secureHeaders, type SecureHeadersOptions } from "./secureHeaders";
@@ -0,0 +1,38 @@
1
+ import { defineMiddleware } from "../TypedMiddleware";
2
+
3
+ export type LogData = {
4
+ readonly method: string;
5
+ readonly path: string;
6
+ readonly statusCode: number;
7
+ readonly durationMs: number;
8
+ };
9
+
10
+ export type LoggerOptions = {
11
+ readonly logFn?: (message: string) => void;
12
+ readonly format?: (data: LogData) => string;
13
+ };
14
+
15
+ const defaultFormat = (data: LogData): string =>
16
+ `${data.method} ${data.path} ${data.statusCode} ${data.durationMs}ms`;
17
+
18
+ export function logger(options?: LoggerOptions) {
19
+ const logFn = options?.logFn ?? console.log;
20
+ const format = options?.format ?? defaultFormat;
21
+
22
+ return defineMiddleware(async (ctx, next) => {
23
+ const start = performance.now();
24
+ const response = await next();
25
+ const durationMs = Math.round(performance.now() - start);
26
+
27
+ logFn(
28
+ format({
29
+ method: ctx.request.method,
30
+ path: ctx.request.path,
31
+ statusCode: response.statusCode,
32
+ durationMs,
33
+ })
34
+ );
35
+
36
+ return response;
37
+ });
38
+ }
@@ -0,0 +1,19 @@
1
+ import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
+ import { defineMiddleware } from "../TypedMiddleware";
3
+
4
+ export type PoweredByOptions = {
5
+ readonly name?: string;
6
+ };
7
+
8
+ export function poweredBy(options?: PoweredByOptions) {
9
+ const value = options?.name ?? "TypeWeaver";
10
+
11
+ return defineMiddleware(async (_ctx, next) => {
12
+ const response = await next();
13
+
14
+ return {
15
+ ...response,
16
+ header: { ...response.header, "x-powered-by": value },
17
+ } satisfies IHttpResponse;
18
+ });
19
+ }
@@ -0,0 +1,29 @@
1
+ import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
+ import { defineMiddleware } from "../TypedMiddleware";
3
+
4
+ export type RequestIdOptions = {
5
+ readonly headerName?: string;
6
+ readonly generator?: () => string;
7
+ };
8
+
9
+ export function requestId(options?: RequestIdOptions) {
10
+ const headerName = (options?.headerName ?? "x-request-id").toLowerCase();
11
+ const generator = options?.generator ?? (() => crypto.randomUUID());
12
+
13
+ return defineMiddleware<{ requestId: string }>(async (ctx, next) => {
14
+ const existing = ctx.request.header?.[headerName];
15
+ const id =
16
+ typeof existing === "string"
17
+ ? existing
18
+ : Array.isArray(existing)
19
+ ? (existing[0] ?? generator())
20
+ : generator();
21
+
22
+ const response = await next({ requestId: id });
23
+
24
+ return {
25
+ ...response,
26
+ header: { ...response.header, [headerName]: id },
27
+ } satisfies IHttpResponse;
28
+ });
29
+ }
@@ -0,0 +1,56 @@
1
+ import { pathMatcher } from "../PathMatcher";
2
+ import { defineMiddleware } from "../TypedMiddleware";
3
+ import type { TypedMiddleware } from "../TypedMiddleware";
4
+
5
+ /**
6
+ * Restricts a middleware to only run on paths matching the given patterns.
7
+ *
8
+ * Accepts the same pattern syntax as {@link pathMatcher}: exact (`"/users"`),
9
+ * prefix (`"/api/*"`), and parameterized (`"/users/:id"`).
10
+ *
11
+ * Only accepts non-state middleware (`TypedMiddleware<{}, {}>`) to preserve
12
+ * TypeWeaver's compile-time state guarantees — skipping a state-providing
13
+ * middleware would leave downstream consumers with missing state.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * app.use(scoped(["/api/*"], cors({ origin: "https://app.com" })));
18
+ * ```
19
+ */
20
+ export function scoped(
21
+ paths: readonly string[],
22
+ middleware: TypedMiddleware<{}, {}>
23
+ ): TypedMiddleware<{}, {}> {
24
+ const matchers = paths.map(pathMatcher);
25
+
26
+ return defineMiddleware(async (ctx, next) => {
27
+ if (!matchers.some(match => match(ctx.request.path))) {
28
+ return next();
29
+ }
30
+ return middleware.handler(ctx, next);
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Runs a middleware on all paths *except* those matching the given patterns.
36
+ *
37
+ * The inverse of {@link scoped}. Same pattern syntax and type constraint.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * app.use(except(["/health", "/ready"], logger()));
42
+ * ```
43
+ */
44
+ export function except(
45
+ paths: readonly string[],
46
+ middleware: TypedMiddleware<{}, {}>
47
+ ): TypedMiddleware<{}, {}> {
48
+ const matchers = paths.map(pathMatcher);
49
+
50
+ return defineMiddleware(async (ctx, next) => {
51
+ if (matchers.some(match => match(ctx.request.path))) {
52
+ return next();
53
+ }
54
+ return middleware.handler(ctx, next);
55
+ });
56
+ }
@@ -0,0 +1,57 @@
1
+ import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
+ import { defineMiddleware } from "../TypedMiddleware";
3
+
4
+ export type SecureHeadersOptions = {
5
+ readonly contentTypeOptions?: string | false;
6
+ readonly frameOptions?: string | false;
7
+ readonly strictTransportSecurity?: string | false;
8
+ readonly referrerPolicy?: string | false;
9
+ readonly xssProtection?: string | false;
10
+ readonly downloadOptions?: string | false;
11
+ readonly dnsPrefetchControl?: string | false;
12
+ readonly permittedCrossDomainPolicies?: string | false;
13
+ readonly crossOriginResourcePolicy?: string | false;
14
+ readonly crossOriginOpenerPolicy?: string | false;
15
+ readonly crossOriginEmbedderPolicy?: string | false;
16
+ readonly originAgentCluster?: string | false;
17
+ };
18
+
19
+ const DEFAULTS: ReadonlyArray<
20
+ readonly [keyof SecureHeadersOptions, string, string]
21
+ > = [
22
+ ["contentTypeOptions", "x-content-type-options", "nosniff"],
23
+ ["frameOptions", "x-frame-options", "SAMEORIGIN"],
24
+ [
25
+ "strictTransportSecurity",
26
+ "strict-transport-security",
27
+ "max-age=15552000; includeSubDomains",
28
+ ],
29
+ ["referrerPolicy", "referrer-policy", "no-referrer"],
30
+ ["xssProtection", "x-xss-protection", "0"],
31
+ ["downloadOptions", "x-download-options", "noopen"],
32
+ ["dnsPrefetchControl", "x-dns-prefetch-control", "off"],
33
+ ["permittedCrossDomainPolicies", "x-permitted-cross-domain-policies", "none"],
34
+ ["crossOriginResourcePolicy", "cross-origin-resource-policy", "same-origin"],
35
+ ["crossOriginOpenerPolicy", "cross-origin-opener-policy", "same-origin"],
36
+ ["crossOriginEmbedderPolicy", "cross-origin-embedder-policy", "require-corp"],
37
+ ["originAgentCluster", "origin-agent-cluster", "?1"],
38
+ ];
39
+
40
+ export function secureHeaders(options?: SecureHeadersOptions) {
41
+ const headers: Record<string, string> = {};
42
+
43
+ for (const [optionKey, headerName, defaultValue] of DEFAULTS) {
44
+ const value = options?.[optionKey];
45
+ if (value === false) continue;
46
+ headers[headerName] = value ?? defaultValue;
47
+ }
48
+
49
+ return defineMiddleware(async (_ctx, next) => {
50
+ const response = await next();
51
+
52
+ return {
53
+ ...response,
54
+ header: { ...headers, ...response.header },
55
+ } satisfies IHttpResponse;
56
+ });
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexeus/typeweaver-server",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Generates a lightweight, dependency-free server with built-in routing and middleware from your API definitions. Powered by Typeweaver.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -47,15 +47,15 @@
47
47
  },
48
48
  "homepage": "https://github.com/rexeus/typeweaver#readme",
49
49
  "peerDependencies": {
50
- "@rexeus/typeweaver-core": "^0.6.2",
51
- "@rexeus/typeweaver-gen": "^0.6.2"
50
+ "@rexeus/typeweaver-gen": "^0.6.4",
51
+ "@rexeus/typeweaver-core": "^0.6.4"
52
52
  },
53
53
  "devDependencies": {
54
54
  "get-port": "^7.1.0",
55
55
  "test-utils": "file:../test-utils",
56
56
  "tsx": "^4.21.0",
57
- "@rexeus/typeweaver-core": "^0.6.2",
58
- "@rexeus/typeweaver-gen": "^0.6.2"
57
+ "@rexeus/typeweaver-gen": "^0.6.4",
58
+ "@rexeus/typeweaver-core": "^0.6.4"
59
59
  },
60
60
  "dependencies": {
61
61
  "case": "^1.6.3"