@rexeus/typeweaver-server 0.10.3 → 0.10.5
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 +11 -5
- package/dist/index.cjs +2 -0
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/dist/lib/BodyLimitPolicy.ts +8 -1
- package/dist/lib/FetchApiAdapter.ts +5 -1
- package/dist/lib/Middleware.ts +2 -1
- package/dist/lib/NodeAdapter.ts +332 -16
- package/dist/lib/PathMatcher.ts +54 -10
- package/dist/lib/Router.ts +24 -8
- package/dist/lib/TypeweaverApp.ts +30 -13
- package/dist/lib/errors/ConflictingPathParameterNameError.ts +20 -0
- package/dist/lib/errors/DuplicateRouteRegistrationError.ts +19 -0
- package/dist/lib/errors/MiddlewareNextAlreadyCalledError.ts +16 -0
- package/dist/lib/errors/MissingRouterForPrefixedMountError.ts +16 -0
- package/dist/lib/errors/RequestBodyClosedBeforeEndError.ts +19 -0
- package/dist/lib/errors/RequestBodyReadAbortedError.ts +19 -0
- package/dist/lib/errors/index.ts +19 -0
- package/dist/lib/middleware/basicAuth.ts +11 -2
- package/dist/lib/middleware/bearerAuth.ts +11 -2
- package/dist/lib/middleware/cors.ts +236 -48
- package/dist/lib/middleware/header.ts +59 -0
- package/dist/lib/middleware/logger.ts +4 -2
- package/dist/lib/middleware/poweredBy.ts +6 -1
- package/dist/lib/middleware/requestId.ts +8 -8
- package/dist/lib/middleware/scoped.ts +37 -12
- package/dist/lib/middleware/secureHeaders.ts +3 -1
- package/dist/templates/Router.ejs +2 -1
- package/package.json +5 -5
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
1
|
+
import type { IHttpRequest, IHttpResponse } from "@rexeus/typeweaver-core";
|
|
2
2
|
import { defineMiddleware } from "../TypedMiddleware.js";
|
|
3
|
+
import {
|
|
4
|
+
hasHeaderName,
|
|
5
|
+
readHeaderValues,
|
|
6
|
+
readSingletonHeader,
|
|
7
|
+
} from "./header.js";
|
|
3
8
|
|
|
4
9
|
export type CorsOptions = {
|
|
5
10
|
readonly origin?:
|
|
@@ -22,13 +27,49 @@ const DEFAULT_METHODS = [
|
|
|
22
27
|
"DELETE",
|
|
23
28
|
] as const;
|
|
24
29
|
|
|
30
|
+
const POLICY_CONTROLLED_CORS_HEADERS = new Set([
|
|
31
|
+
"access-control-allow-origin",
|
|
32
|
+
"access-control-allow-credentials",
|
|
33
|
+
"access-control-expose-headers",
|
|
34
|
+
"access-control-allow-methods",
|
|
35
|
+
"access-control-allow-headers",
|
|
36
|
+
"access-control-max-age",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
type NormalizedCorsOptions = {
|
|
40
|
+
readonly origin: CorsOptions["origin"];
|
|
41
|
+
readonly allowMethods: string;
|
|
42
|
+
readonly allowHeaders: readonly string[] | undefined;
|
|
43
|
+
readonly exposeHeaders: string | undefined;
|
|
44
|
+
readonly maxAge: string | undefined;
|
|
45
|
+
readonly credentials: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type CorsRequest = {
|
|
49
|
+
readonly header: IHttpRequest["header"];
|
|
50
|
+
readonly method: IHttpRequest["method"];
|
|
51
|
+
readonly hasOrigin: boolean;
|
|
52
|
+
readonly origin: string | undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function normalizeCorsOptions(options?: CorsOptions): NormalizedCorsOptions {
|
|
56
|
+
return {
|
|
57
|
+
origin: options?.origin,
|
|
58
|
+
allowMethods: (options?.allowMethods ?? DEFAULT_METHODS).join(", "),
|
|
59
|
+
allowHeaders: options?.allowHeaders,
|
|
60
|
+
exposeHeaders: options?.exposeHeaders?.join(", "),
|
|
61
|
+
maxAge: options?.maxAge?.toString(),
|
|
62
|
+
credentials: options?.credentials ?? false,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
25
66
|
function resolveOrigin(
|
|
26
67
|
configOrigin: CorsOptions["origin"],
|
|
27
68
|
requestOrigin: string | undefined,
|
|
28
69
|
credentials: boolean
|
|
29
70
|
): string | undefined {
|
|
30
71
|
if (configOrigin === undefined || configOrigin === "*") {
|
|
31
|
-
if (credentials
|
|
72
|
+
if (credentials) return undefined;
|
|
32
73
|
return "*";
|
|
33
74
|
}
|
|
34
75
|
|
|
@@ -48,72 +89,219 @@ function resolveOrigin(
|
|
|
48
89
|
function getRequestOrigin(
|
|
49
90
|
header: Record<string, string | string[]> | undefined
|
|
50
91
|
): string | undefined {
|
|
51
|
-
|
|
52
|
-
return typeof origin === "string" ? origin : undefined;
|
|
92
|
+
return readSingletonHeader(header, "origin");
|
|
53
93
|
}
|
|
54
94
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
95
|
+
function readCorsRequest(request: IHttpRequest): CorsRequest {
|
|
96
|
+
return {
|
|
97
|
+
header: request.header,
|
|
98
|
+
method: request.method,
|
|
99
|
+
hasOrigin: hasHeaderName(request.header, "origin"),
|
|
100
|
+
origin: getRequestOrigin(request.header),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
60
103
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
104
|
+
function isOriginDependentWithoutRequestOrigin(
|
|
105
|
+
configOrigin: CorsOptions["origin"],
|
|
106
|
+
credentials: boolean
|
|
107
|
+
): boolean {
|
|
108
|
+
return (
|
|
109
|
+
typeof configOrigin === "function" ||
|
|
110
|
+
Array.isArray(configOrigin) ||
|
|
111
|
+
((configOrigin === undefined || configOrigin === "*") && credentials)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
64
114
|
|
|
65
|
-
|
|
115
|
+
function resolveRequestOrigin(
|
|
116
|
+
options: NormalizedCorsOptions,
|
|
117
|
+
request: CorsRequest
|
|
118
|
+
): string | undefined {
|
|
119
|
+
if (request.hasOrigin && request.origin === undefined) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
66
122
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
123
|
+
const resolvedOrigin = resolveOrigin(
|
|
124
|
+
options.origin,
|
|
125
|
+
request.origin,
|
|
126
|
+
options.credentials
|
|
127
|
+
);
|
|
70
128
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
129
|
+
return options.credentials && resolvedOrigin === "*"
|
|
130
|
+
? undefined
|
|
131
|
+
: resolvedOrigin;
|
|
132
|
+
}
|
|
74
133
|
|
|
75
|
-
|
|
76
|
-
|
|
134
|
+
function shouldVaryDeniedCorsResponse(
|
|
135
|
+
options: NormalizedCorsOptions,
|
|
136
|
+
request: CorsRequest
|
|
137
|
+
): boolean {
|
|
138
|
+
return (
|
|
139
|
+
request.hasOrigin ||
|
|
140
|
+
isOriginDependentWithoutRequestOrigin(options.origin, options.credentials)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildSimpleCorsHeaders(
|
|
145
|
+
options: NormalizedCorsOptions,
|
|
146
|
+
origin: string
|
|
147
|
+
): Record<string, string> {
|
|
148
|
+
const corsHeaders: Record<string, string> = {
|
|
149
|
+
"access-control-allow-origin": origin,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (options.credentials) {
|
|
153
|
+
corsHeaders["access-control-allow-credentials"] = "true";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (origin !== "*") {
|
|
157
|
+
corsHeaders["vary"] = "Origin";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options.exposeHeaders) {
|
|
161
|
+
corsHeaders["access-control-expose-headers"] = options.exposeHeaders;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return corsHeaders;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isPreflightCorsRequest(request: CorsRequest): boolean {
|
|
168
|
+
return (
|
|
169
|
+
request.method === "OPTIONS" &&
|
|
170
|
+
request.origin !== undefined &&
|
|
171
|
+
readSingletonHeader(request.header, "access-control-request-method") !==
|
|
172
|
+
undefined
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildPreflightCorsHeaders(
|
|
177
|
+
options: NormalizedCorsOptions,
|
|
178
|
+
request: CorsRequest,
|
|
179
|
+
simpleCorsHeaders: Record<string, string>
|
|
180
|
+
): Record<string, string> {
|
|
181
|
+
const corsHeaders = { ...simpleCorsHeaders };
|
|
182
|
+
corsHeaders["access-control-allow-methods"] = options.allowMethods;
|
|
183
|
+
|
|
184
|
+
if (options.allowHeaders !== undefined) {
|
|
185
|
+
if (options.allowHeaders.length > 0) {
|
|
186
|
+
corsHeaders["access-control-allow-headers"] =
|
|
187
|
+
options.allowHeaders.join(", ");
|
|
77
188
|
}
|
|
189
|
+
} else {
|
|
190
|
+
const requestedHeaders = readSingletonHeader(
|
|
191
|
+
request.header,
|
|
192
|
+
"access-control-request-headers"
|
|
193
|
+
);
|
|
194
|
+
if (typeof requestedHeaders === "string") {
|
|
195
|
+
corsHeaders["access-control-allow-headers"] = requestedHeaders;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
78
198
|
|
|
79
|
-
|
|
80
|
-
|
|
199
|
+
if (options.maxAge !== undefined) {
|
|
200
|
+
corsHeaders["access-control-max-age"] = options.maxAge;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return corsHeaders;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function splitHeaderValues(values: readonly string[]): readonly string[] {
|
|
207
|
+
return values.flatMap(value =>
|
|
208
|
+
value
|
|
209
|
+
.split(",")
|
|
210
|
+
.map(item => item.trim())
|
|
211
|
+
.filter(item => item.length > 0)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function mergeVary(existing: readonly string[], value: string): string {
|
|
216
|
+
const values = splitHeaderValues(existing);
|
|
217
|
+
if (values.length === 0) return value;
|
|
218
|
+
|
|
219
|
+
const hasValue = values.some(
|
|
220
|
+
item => item.toLowerCase() === value.toLowerCase()
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return hasValue ? values.join(", ") : [...values, value].join(", ");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function removePolicyControlledCorsHeaders(
|
|
227
|
+
responseHeaders: Record<string, string | string[]> | undefined
|
|
228
|
+
): Record<string, string | string[]> {
|
|
229
|
+
const result: Record<string, string | string[]> = {};
|
|
230
|
+
|
|
231
|
+
for (const [key, value] of Object.entries(responseHeaders ?? {})) {
|
|
232
|
+
if (POLICY_CONTROLLED_CORS_HEADERS.has(key.toLowerCase())) continue;
|
|
233
|
+
|
|
234
|
+
result[key] = value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function mergeResponseHeaders(
|
|
241
|
+
responseHeaders: Record<string, string | string[]> | undefined,
|
|
242
|
+
corsHeaders: Record<string, string>
|
|
243
|
+
): Record<string, string | string[]> {
|
|
244
|
+
const result = removePolicyControlledCorsHeaders(responseHeaders);
|
|
245
|
+
|
|
246
|
+
const mergedCorsHeaders = { ...corsHeaders };
|
|
247
|
+
if (corsHeaders.vary !== undefined) {
|
|
248
|
+
for (const key of Object.keys(result)) {
|
|
249
|
+
if (key.toLowerCase() === "vary") delete result[key];
|
|
81
250
|
}
|
|
82
251
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
252
|
+
mergedCorsHeaders.vary = mergeVary(
|
|
253
|
+
readHeaderValues(responseHeaders, "vary"),
|
|
254
|
+
corsHeaders.vary
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { ...result, ...mergedCorsHeaders };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function mergeCorsHeadersIntoResponse(
|
|
262
|
+
response: IHttpResponse,
|
|
263
|
+
corsHeaders: Record<string, string>
|
|
264
|
+
): IHttpResponse {
|
|
265
|
+
return {
|
|
266
|
+
...response,
|
|
267
|
+
header: mergeResponseHeaders(response.header, corsHeaders),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function cors(options?: CorsOptions) {
|
|
272
|
+
const normalizedOptions = normalizeCorsOptions(options);
|
|
273
|
+
|
|
274
|
+
return defineMiddleware(async (ctx, next) => {
|
|
275
|
+
const corsRequest = readCorsRequest(ctx.request);
|
|
276
|
+
const origin = resolveRequestOrigin(normalizedOptions, corsRequest);
|
|
277
|
+
|
|
278
|
+
if (origin === undefined) {
|
|
279
|
+
const response = await next();
|
|
101
280
|
|
|
102
|
-
if (
|
|
103
|
-
|
|
281
|
+
if (!shouldVaryDeniedCorsResponse(normalizedOptions, corsRequest)) {
|
|
282
|
+
return response;
|
|
104
283
|
}
|
|
105
284
|
|
|
285
|
+
return mergeCorsHeadersIntoResponse(response, { vary: "Origin" });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const corsHeaders = buildSimpleCorsHeaders(normalizedOptions, origin);
|
|
289
|
+
|
|
290
|
+
if (isPreflightCorsRequest(corsRequest)) {
|
|
291
|
+
const preflightHeaders = buildPreflightCorsHeaders(
|
|
292
|
+
normalizedOptions,
|
|
293
|
+
corsRequest,
|
|
294
|
+
corsHeaders
|
|
295
|
+
);
|
|
296
|
+
|
|
106
297
|
return {
|
|
107
298
|
statusCode: 204,
|
|
108
|
-
header:
|
|
299
|
+
header: preflightHeaders,
|
|
109
300
|
} satisfies IHttpResponse;
|
|
110
301
|
}
|
|
111
302
|
|
|
112
303
|
const response = await next();
|
|
113
304
|
|
|
114
|
-
return
|
|
115
|
-
...response,
|
|
116
|
-
header: { ...corsHeaders, ...response.header },
|
|
117
|
-
} satisfies IHttpResponse;
|
|
305
|
+
return mergeCorsHeadersIntoResponse(response, corsHeaders);
|
|
118
306
|
});
|
|
119
307
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type HeaderMap = Record<string, string | string[]> | undefined;
|
|
2
|
+
|
|
3
|
+
export function readSingletonHeader(
|
|
4
|
+
header: HeaderMap,
|
|
5
|
+
name: string
|
|
6
|
+
): string | undefined {
|
|
7
|
+
const normalizedName = name.toLowerCase();
|
|
8
|
+
let foundValue: string | undefined;
|
|
9
|
+
|
|
10
|
+
for (const [key, value] of Object.entries(header ?? {})) {
|
|
11
|
+
if (key.toLowerCase() !== normalizedName) continue;
|
|
12
|
+
if (foundValue !== undefined || typeof value !== "string") {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
foundValue = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return foundValue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasHeaderName(header: HeaderMap, name: string): boolean {
|
|
23
|
+
const normalizedName = name.toLowerCase();
|
|
24
|
+
|
|
25
|
+
return Object.keys(header ?? {}).some(
|
|
26
|
+
key => key.toLowerCase() === normalizedName
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readHeaderValues(
|
|
31
|
+
header: HeaderMap,
|
|
32
|
+
name: string
|
|
33
|
+
): readonly string[] {
|
|
34
|
+
const normalizedName = name.toLowerCase();
|
|
35
|
+
const values: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const [key, value] of Object.entries(header ?? {})) {
|
|
38
|
+
if (key.toLowerCase() !== normalizedName) continue;
|
|
39
|
+
|
|
40
|
+
values.push(...(Array.isArray(value) ? value : [value]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return values;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function omitHeaders(
|
|
47
|
+
header: HeaderMap,
|
|
48
|
+
names: readonly string[]
|
|
49
|
+
): Record<string, string | string[]> {
|
|
50
|
+
const normalizedNames = new Set(names.map(name => name.toLowerCase()));
|
|
51
|
+
const headers: Record<string, string | string[]> = {};
|
|
52
|
+
|
|
53
|
+
for (const [key, value] of Object.entries(header ?? {})) {
|
|
54
|
+
if (normalizedNames.has(key.toLowerCase())) continue;
|
|
55
|
+
headers[key] = value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return headers;
|
|
59
|
+
}
|
|
@@ -10,6 +10,7 @@ export type LogData = {
|
|
|
10
10
|
export type LoggerOptions = {
|
|
11
11
|
readonly logFn?: (message: string) => void;
|
|
12
12
|
readonly format?: (data: LogData) => string;
|
|
13
|
+
readonly nowMs?: () => number;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
const defaultFormat = (data: LogData): string =>
|
|
@@ -18,11 +19,12 @@ const defaultFormat = (data: LogData): string =>
|
|
|
18
19
|
export function logger(options?: LoggerOptions) {
|
|
19
20
|
const logFn = options?.logFn ?? console.log;
|
|
20
21
|
const format = options?.format ?? defaultFormat;
|
|
22
|
+
const nowMs = options?.nowMs ?? (() => performance.now());
|
|
21
23
|
|
|
22
24
|
return defineMiddleware(async (ctx, next) => {
|
|
23
|
-
const start =
|
|
25
|
+
const start = nowMs();
|
|
24
26
|
const response = await next();
|
|
25
|
-
const durationMs = Math.round(
|
|
27
|
+
const durationMs = Math.round(nowMs() - start);
|
|
26
28
|
|
|
27
29
|
logFn(
|
|
28
30
|
format({
|
|
@@ -10,10 +10,15 @@ export function poweredBy(options?: PoweredByOptions) {
|
|
|
10
10
|
|
|
11
11
|
return defineMiddleware(async (_ctx, next) => {
|
|
12
12
|
const response = await next();
|
|
13
|
+
const responseHeaders = Object.fromEntries(
|
|
14
|
+
Object.entries(response.header ?? {}).filter(
|
|
15
|
+
([headerName]) => headerName.toLowerCase() !== "x-powered-by"
|
|
16
|
+
)
|
|
17
|
+
);
|
|
13
18
|
|
|
14
19
|
return {
|
|
15
20
|
...response,
|
|
16
|
-
header: { ...
|
|
21
|
+
header: { ...responseHeaders, "x-powered-by": value },
|
|
17
22
|
} satisfies IHttpResponse;
|
|
18
23
|
});
|
|
19
24
|
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
2
2
|
import { defineMiddleware } from "../TypedMiddleware.js";
|
|
3
|
+
import { omitHeaders, readSingletonHeader } from "./header.js";
|
|
3
4
|
|
|
4
5
|
export type RequestIdOptions = {
|
|
5
6
|
readonly headerName?: string;
|
|
6
7
|
readonly generator?: () => string;
|
|
7
8
|
};
|
|
8
9
|
|
|
10
|
+
const isValidRequestId = (value: string | undefined): value is string =>
|
|
11
|
+
value !== undefined && value.length > 0 && !/[\r\n]/.test(value);
|
|
12
|
+
|
|
9
13
|
export function requestId(options?: RequestIdOptions) {
|
|
10
14
|
const headerName = (options?.headerName ?? "x-request-id").toLowerCase();
|
|
11
15
|
const generator = options?.generator ?? (() => crypto.randomUUID());
|
|
12
16
|
|
|
13
17
|
return defineMiddleware<{ requestId: string }>(async (ctx, next) => {
|
|
14
|
-
const existing = ctx.request.header
|
|
15
|
-
const id =
|
|
16
|
-
typeof existing === "string"
|
|
17
|
-
? existing
|
|
18
|
-
: Array.isArray(existing)
|
|
19
|
-
? (existing[0] ?? generator())
|
|
20
|
-
: generator();
|
|
18
|
+
const existing = readSingletonHeader(ctx.request.header, headerName);
|
|
19
|
+
const id = isValidRequestId(existing) ? existing : generator();
|
|
21
20
|
|
|
22
21
|
const response = await next({ requestId: id });
|
|
22
|
+
const header = omitHeaders(response.header, [headerName]);
|
|
23
23
|
|
|
24
24
|
return {
|
|
25
25
|
...response,
|
|
26
|
-
header: { ...
|
|
26
|
+
header: { ...header, [headerName]: id },
|
|
27
27
|
} satisfies IHttpResponse;
|
|
28
28
|
});
|
|
29
29
|
}
|
|
@@ -2,55 +2,80 @@ import { pathMatcher } from "../PathMatcher.js";
|
|
|
2
2
|
import { defineMiddleware } from "../TypedMiddleware.js";
|
|
3
3
|
import type { TypedMiddleware } from "../TypedMiddleware.js";
|
|
4
4
|
|
|
5
|
+
/** Rejects middleware that would provide state from a conditional branch. */
|
|
6
|
+
type RejectsProvidedState<TProvides extends Record<string, unknown>> = [
|
|
7
|
+
keyof TProvides,
|
|
8
|
+
] extends [never]
|
|
9
|
+
? unknown
|
|
10
|
+
: never;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* scoped/except middleware may be skipped, so it cannot safely provide
|
|
14
|
+
* downstream state; any upstream state requirements remain part of its type.
|
|
15
|
+
*/
|
|
16
|
+
type StateNeutralMiddleware<
|
|
17
|
+
TProvides extends Record<string, unknown>,
|
|
18
|
+
TRequires extends Record<string, unknown>,
|
|
19
|
+
> = TypedMiddleware<TProvides, TRequires> & RejectsProvidedState<TProvides>;
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* Restricts a middleware to only run on paths matching the given patterns.
|
|
7
23
|
*
|
|
8
24
|
* Accepts the same pattern syntax as {@link pathMatcher}: exact (`"/users"`),
|
|
9
25
|
* prefix (`"/api/*"`), and parameterized (`"/users/:id"`).
|
|
10
26
|
*
|
|
11
|
-
* Only accepts non-state middleware
|
|
27
|
+
* Only accepts non-state-providing middleware to preserve
|
|
12
28
|
* TypeWeaver's compile-time state guarantees — skipping a state-providing
|
|
13
29
|
* middleware would leave downstream consumers with missing state.
|
|
30
|
+
* Any upstream state requirements declared by the wrapped middleware are
|
|
31
|
+
* preserved on the returned middleware descriptor.
|
|
14
32
|
*
|
|
15
33
|
* @example
|
|
16
34
|
* ```typescript
|
|
17
35
|
* app.use(scoped(["/api/*"], cors({ origin: "https://app.com" })));
|
|
18
36
|
* ```
|
|
19
37
|
*/
|
|
20
|
-
export function scoped
|
|
38
|
+
export function scoped<
|
|
39
|
+
TProvides extends Record<string, unknown>,
|
|
40
|
+
TRequires extends Record<string, unknown>,
|
|
41
|
+
>(
|
|
21
42
|
paths: readonly string[],
|
|
22
|
-
middleware:
|
|
23
|
-
): TypedMiddleware<
|
|
43
|
+
middleware: StateNeutralMiddleware<TProvides, TRequires>
|
|
44
|
+
): TypedMiddleware<TProvides, TRequires> {
|
|
24
45
|
const matchers = paths.map(pathMatcher);
|
|
25
46
|
|
|
26
|
-
return defineMiddleware(async (ctx, next) => {
|
|
47
|
+
return defineMiddleware<{}, TRequires>(async (ctx, next) => {
|
|
27
48
|
if (!matchers.some(match => match(ctx.request.path))) {
|
|
28
49
|
return next();
|
|
29
50
|
}
|
|
30
51
|
return middleware.handler(ctx, next);
|
|
31
|
-
})
|
|
52
|
+
}) as TypedMiddleware<TProvides, TRequires>;
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
/**
|
|
35
56
|
* Runs a middleware on all paths *except* those matching the given patterns.
|
|
36
57
|
*
|
|
37
|
-
* The inverse of {@link scoped}. Same pattern syntax and type
|
|
58
|
+
* The inverse of {@link scoped}. Same pattern syntax and type constraints,
|
|
59
|
+
* including preservation of wrapped middleware state requirements.
|
|
38
60
|
*
|
|
39
61
|
* @example
|
|
40
62
|
* ```typescript
|
|
41
63
|
* app.use(except(["/health", "/ready"], logger()));
|
|
42
64
|
* ```
|
|
43
65
|
*/
|
|
44
|
-
export function except
|
|
66
|
+
export function except<
|
|
67
|
+
TProvides extends Record<string, unknown>,
|
|
68
|
+
TRequires extends Record<string, unknown>,
|
|
69
|
+
>(
|
|
45
70
|
paths: readonly string[],
|
|
46
|
-
middleware:
|
|
47
|
-
): TypedMiddleware<
|
|
71
|
+
middleware: StateNeutralMiddleware<TProvides, TRequires>
|
|
72
|
+
): TypedMiddleware<TProvides, TRequires> {
|
|
48
73
|
const matchers = paths.map(pathMatcher);
|
|
49
74
|
|
|
50
|
-
return defineMiddleware(async (ctx, next) => {
|
|
75
|
+
return defineMiddleware<{}, TRequires>(async (ctx, next) => {
|
|
51
76
|
if (matchers.some(match => match(ctx.request.path))) {
|
|
52
77
|
return next();
|
|
53
78
|
}
|
|
54
79
|
return middleware.handler(ctx, next);
|
|
55
|
-
})
|
|
80
|
+
}) as TypedMiddleware<TProvides, TRequires>;
|
|
56
81
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
2
2
|
import { defineMiddleware } from "../TypedMiddleware.js";
|
|
3
|
+
import { omitHeaders } from "./header.js";
|
|
3
4
|
|
|
4
5
|
export type SecureHeadersOptions = {
|
|
5
6
|
readonly contentTypeOptions?: string | false;
|
|
@@ -48,10 +49,11 @@ export function secureHeaders(options?: SecureHeadersOptions) {
|
|
|
48
49
|
|
|
49
50
|
return defineMiddleware(async (_ctx, next) => {
|
|
50
51
|
const response = await next();
|
|
52
|
+
const header = omitHeaders(response.header, Object.keys(headers));
|
|
51
53
|
|
|
52
54
|
return {
|
|
53
55
|
...response,
|
|
54
|
-
header: { ...
|
|
56
|
+
header: { ...header, ...headers },
|
|
55
57
|
} satisfies IHttpResponse;
|
|
56
58
|
});
|
|
57
59
|
}
|
|
@@ -18,7 +18,8 @@ export type Server<%- pascalCaseEntityName %>ApiHandler<
|
|
|
18
18
|
TState extends Record<string, unknown> = Record<string, unknown>,
|
|
19
19
|
> = {
|
|
20
20
|
<% for (const operation of operations) { %>
|
|
21
|
-
|
|
21
|
+
<% if (operation.jsDoc) { %><%- operation.jsDoc %>
|
|
22
|
+
<% } %> <%- operation.handlerName %>: RequestHandler<I<%- operation.className %>Request, <%- operation.className %>Response, TState>;
|
|
22
23
|
<% } %>
|
|
23
24
|
};
|
|
24
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexeus/typeweaver-server",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
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.10.
|
|
51
|
-
"@rexeus/typeweaver-gen": "^0.10.
|
|
50
|
+
"@rexeus/typeweaver-core": "^0.10.5",
|
|
51
|
+
"@rexeus/typeweaver-gen": "^0.10.5"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"get-port": "^7.2.0",
|
|
55
55
|
"test-utils": "file:../test-utils",
|
|
56
56
|
"tsx": "^4.21.0",
|
|
57
|
-
"@rexeus/typeweaver-core": "^0.10.
|
|
58
|
-
"@rexeus/typeweaver-gen": "^0.10.
|
|
57
|
+
"@rexeus/typeweaver-core": "^0.10.5",
|
|
58
|
+
"@rexeus/typeweaver-gen": "^0.10.5"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"polycase": "^1.1.0"
|