@rexeus/typeweaver-server 0.10.4 → 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/Middleware.ts +2 -1
- package/dist/lib/NodeAdapter.ts +7 -3
- package/dist/lib/Router.ts +9 -5
- package/dist/lib/TypeweaverApp.ts +13 -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/cors.ts +152 -72
- package/dist/lib/middleware/scoped.ts +13 -3
- package/dist/templates/Router.ejs +2 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -294,10 +294,10 @@ Each middleware is documented in detail — click the links above.
|
|
|
294
294
|
|
|
295
295
|
`TypeweaverApp` accepts an optional options object:
|
|
296
296
|
|
|
297
|
-
| Option | Type | Default | Description
|
|
298
|
-
| ------------- | -------------------------- | ------------------ |
|
|
299
|
-
| `maxBodySize` | `number` | `1_048_576` (1 MB) | Max request body size in bytes. Exceeding returns `413`.
|
|
300
|
-
| `onError` | `(error: unknown) => void` | `console.error` |
|
|
297
|
+
| Option | Type | Default | Description |
|
|
298
|
+
| ------------- | -------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
|
299
|
+
| `maxBodySize` | `number` | `1_048_576` (1 MB) | Max request body size in bytes. Exceeding returns `413`. |
|
|
300
|
+
| `onError` | `(error: unknown) => void` | `console.error` | Reports errors from the default unknown handler and top-level safety net. Falls back to `console.error` if it throws. |
|
|
301
301
|
|
|
302
302
|
```ts
|
|
303
303
|
const app = new TypeweaverApp({
|
|
@@ -325,7 +325,7 @@ errors fall through to the next handler in the chain (except `handleResponseVali
|
|
|
325
325
|
`false` means the invalid response is returned as-is — validation still runs for field stripping,
|
|
326
326
|
but invalid responses pass through unchanged). When set to a function, it receives the error and
|
|
327
327
|
`ServerContext` and must return an `IHttpResponse`. If a custom error handler throws, the framework
|
|
328
|
-
|
|
328
|
+
reports that handler failure through `onError` and falls through gracefully to the next handler.
|
|
329
329
|
|
|
330
330
|
### 🚨 Error Handling
|
|
331
331
|
|
|
@@ -431,6 +431,12 @@ new UserRouter({
|
|
|
431
431
|
});
|
|
432
432
|
```
|
|
433
433
|
|
|
434
|
+
The default unknown-error handler calls `onError` before returning a sanitized 500 response. A
|
|
435
|
+
custom `handleUnknownErrors` function replaces that default reporting strategy; if you need logging
|
|
436
|
+
or metrics with a custom unknown handler, perform that reporting inside the custom handler. If the
|
|
437
|
+
custom unknown handler throws, Typeweaver reports the handler failure through `onError` and then
|
|
438
|
+
falls through to the top-level safety net.
|
|
439
|
+
|
|
434
440
|
### 📋 Error Responses
|
|
435
441
|
|
|
436
442
|
| Status | Code | When |
|
package/dist/index.cjs
CHANGED
|
@@ -61,10 +61,12 @@ function writeRouter(resource, templateFile, context) {
|
|
|
61
61
|
function createOperationData(operation) {
|
|
62
62
|
const operationId = operation.operationId;
|
|
63
63
|
const className = (0, polycase.pascalCase)(operationId);
|
|
64
|
+
const jsDoc = (0, _rexeus_typeweaver_gen.createJSDocComment)(operation.summary, { indentation: " " });
|
|
64
65
|
return {
|
|
65
66
|
operationId,
|
|
66
67
|
className,
|
|
67
68
|
handlerName: `handle${className}Request`,
|
|
69
|
+
...jsDoc ? { jsDoc } : {},
|
|
68
70
|
method: operation.method,
|
|
69
71
|
path: operation.path
|
|
70
72
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { BasePlugin, compareRoutes, relative } from "@rexeus/typeweaver-gen";
|
|
3
|
+
import { BasePlugin, compareRoutes, createJSDocComment, relative } from "@rexeus/typeweaver-gen";
|
|
4
4
|
import { HttpMethod } from "@rexeus/typeweaver-core";
|
|
5
5
|
import { pascalCase } from "polycase";
|
|
6
6
|
//#region src/routerGenerator.ts
|
|
@@ -37,10 +37,12 @@ function writeRouter(resource, templateFile, context) {
|
|
|
37
37
|
function createOperationData(operation) {
|
|
38
38
|
const operationId = operation.operationId;
|
|
39
39
|
const className = pascalCase(operationId);
|
|
40
|
+
const jsDoc = createJSDocComment(operation.summary, { indentation: " " });
|
|
40
41
|
return {
|
|
41
42
|
operationId,
|
|
42
43
|
className,
|
|
43
44
|
handlerName: `handle${className}Request`,
|
|
45
|
+
...jsDoc ? { jsDoc } : {},
|
|
44
46
|
method: operation.method,
|
|
45
47
|
path: operation.path
|
|
46
48
|
};
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/routerGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport {
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/routerGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport {\n compareRoutes,\n createJSDocComment,\n relative,\n} from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n NormalizedOperation,\n NormalizedResource,\n} from \"@rexeus/typeweaver-gen\";\nimport { pascalCase } from \"polycase\";\n\nexport type RouterGenerationContext = Pick<\n GeneratorContext,\n | \"normalizedSpec\"\n | \"outputDir\"\n | \"getResourceOutputDir\"\n | \"renderTemplate\"\n | \"writeFile\"\n>;\n\ntype OperationData = {\n readonly operationId: string;\n readonly className: string;\n readonly handlerName: string;\n readonly jsDoc?: string;\n readonly method: string;\n readonly path: string;\n};\n\n/**\n * Generates TypeweaverRouter subclasses from API definitions.\n *\n * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`\n * file that extends `TypeweaverRouter` and registers all operations as routes.\n */\n\n/**\n * Generates router files for all resources in the given context.\n *\n * @param context - The generator context containing resources, templates, and output configuration\n */\nexport function generate(context: RouterGenerationContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"Router.ejs\");\n\n for (const resource of context.normalizedSpec.resources) {\n writeRouter(resource, templateFile, context);\n }\n}\n\nfunction writeRouter(\n resource: NormalizedResource,\n templateFile: string,\n context: RouterGenerationContext\n): void {\n const pascalCaseEntityName = pascalCase(resource.name);\n const outputDir = context.getResourceOutputDir(resource.name);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);\n\n const operations = resource.operations\n .filter(operation => operation.method !== HttpMethod.HEAD)\n .map(operation => createOperationData(operation))\n .sort((a, b) => compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: relative(outputDir, context.outputDir),\n entityName: resource.name,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n}\n\nfunction createOperationData(operation: NormalizedOperation): OperationData {\n const operationId = operation.operationId;\n const className = pascalCase(operationId);\n const jsDoc = createJSDocComment(operation.summary, { indentation: \" \" });\n\n return {\n operationId,\n className,\n handlerName: `handle${className}Request`,\n ...(jsDoc ? { jsDoc } : {}),\n method: operation.method,\n path: operation.path,\n };\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { BasePlugin } from \"@rexeus/typeweaver-gen\";\nimport type { GeneratorContext } from \"@rexeus/typeweaver-gen\";\nimport { generate as generateRouters } from \"./routerGenerator.js\";\n\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Typeweaver plugin that generates a lightweight, dependency-free server\n * with built-in routing and middleware support.\n *\n * Copies the runtime library files (`TypeweaverApp`, `TypeweaverRouter`, `Router`,\n * `Middleware`, etc.) and generates typed router classes for each resource.\n */\nexport class ServerPlugin extends BasePlugin {\n public name = \"server\";\n public override depends = [\"types\"];\n\n /**\n * Generates the server runtime and typed routers for all resources.\n *\n * @param context - The generator context\n */\n public override generate(context: GeneratorContext): void {\n const libSourceDir = path.join(moduleDir, \"lib\");\n this.copyLibFiles(context, libSourceDir, this.name);\n\n generateRouters(context);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA6CA,SAAgB,SAAS,SAAwC;CAC/D,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CAC9D,MAAM,eAAe,KAAK,KAAK,WAAW,aAAa,aAAa;AAEpE,MAAK,MAAM,YAAY,QAAQ,eAAe,UAC5C,aAAY,UAAU,cAAc,QAAQ;;AAIhD,SAAS,YACP,UACA,cACA,SACM;CACN,MAAM,uBAAuB,WAAW,SAAS,KAAK;CACtD,MAAM,YAAY,QAAQ,qBAAqB,SAAS,KAAK;CAC7D,MAAM,aAAa,KAAK,KAAK,WAAW,GAAG,qBAAqB,WAAW;CAE3E,MAAM,aAAa,SAAS,WACzB,QAAO,cAAa,UAAU,WAAW,WAAW,KAAK,CACzD,KAAI,cAAa,oBAAoB,UAAU,CAAC,CAChD,MAAM,GAAG,MAAM,cAAc,GAAG,EAAE,CAAC;CAEtC,MAAM,UAAU,QAAQ,eAAe,cAAc;EACnD,SAAS,SAAS,WAAW,QAAQ,UAAU;EAC/C,YAAY,SAAS;EACrB;EACA;EACD,CAAC;CAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,WAAW,WAAW;AACjE,SAAQ,UAAU,cAAc,QAAQ;;AAG1C,SAAS,oBAAoB,WAA+C;CAC1E,MAAM,cAAc,UAAU;CAC9B,MAAM,YAAY,WAAW,YAAY;CACzC,MAAM,QAAQ,mBAAmB,UAAU,SAAS,EAAE,aAAa,MAAM,CAAC;AAE1E,QAAO;EACL;EACA;EACA,aAAa,SAAS,UAAU;EAChC,GAAI,QAAQ,EAAE,OAAO,GAAG,EAAE;EAC1B,QAAQ,UAAU;EAClB,MAAM,UAAU;EACjB;;;;ACrFH,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;AAS9D,IAAa,eAAb,cAAkC,WAAW;CAC3C,OAAc;CACd,UAA0B,CAAC,QAAQ;;;;;;CAOnC,SAAyB,SAAiC;EACxD,MAAM,eAAe,KAAK,KAAK,WAAW,MAAM;AAChD,OAAK,aAAa,SAAS,cAAc,KAAK,KAAK;AAEnD,WAAgB,QAAQ"}
|
package/dist/lib/Middleware.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
9
|
+
import { MiddlewareNextAlreadyCalledError } from "./errors/index.js";
|
|
9
10
|
import type { ServerContext } from "./ServerContext.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -52,7 +53,7 @@ export async function executeMiddlewarePipeline(
|
|
|
52
53
|
|
|
53
54
|
return middlewares[currentIndex]!(ctx, async state => {
|
|
54
55
|
if (called) {
|
|
55
|
-
throw new
|
|
56
|
+
throw new MiddlewareNextAlreadyCalledError(currentIndex);
|
|
56
57
|
}
|
|
57
58
|
called = true;
|
|
58
59
|
|
package/dist/lib/NodeAdapter.ts
CHANGED
|
@@ -19,8 +19,10 @@ import {
|
|
|
19
19
|
} from "./BodyLimitPolicy.js";
|
|
20
20
|
import {
|
|
21
21
|
PayloadTooLargeError,
|
|
22
|
+
RequestBodyClosedBeforeEndError,
|
|
22
23
|
RequestBodyDrainTimeoutError,
|
|
23
|
-
|
|
24
|
+
RequestBodyReadAbortedError,
|
|
25
|
+
} from "./errors/index.js";
|
|
24
26
|
import {
|
|
25
27
|
getTypeweaverAppErrorReporter,
|
|
26
28
|
getTypeweaverAppRuntimeContext,
|
|
@@ -648,12 +650,14 @@ function collectBody(
|
|
|
648
650
|
};
|
|
649
651
|
|
|
650
652
|
const handleAborted = (): void => {
|
|
651
|
-
rejectOnce(new
|
|
653
|
+
rejectOnce(new RequestBodyReadAbortedError(totalBytes, maxBodySize));
|
|
652
654
|
};
|
|
653
655
|
|
|
654
656
|
const handleClose = (): void => {
|
|
655
657
|
if (!req.readableEnded) {
|
|
656
|
-
rejectOnce(
|
|
658
|
+
rejectOnce(
|
|
659
|
+
new RequestBodyClosedBeforeEndError(totalBytes, maxBodySize)
|
|
660
|
+
);
|
|
657
661
|
}
|
|
658
662
|
};
|
|
659
663
|
|
package/dist/lib/Router.ts
CHANGED
|
@@ -14,6 +14,10 @@ import type {
|
|
|
14
14
|
RequestValidationError,
|
|
15
15
|
ResponseValidationError,
|
|
16
16
|
} from "@rexeus/typeweaver-core";
|
|
17
|
+
import {
|
|
18
|
+
ConflictingPathParameterNameError,
|
|
19
|
+
DuplicateRouteRegistrationError,
|
|
20
|
+
} from "./errors/index.js";
|
|
17
21
|
import type { RequestHandler } from "./RequestHandler.js";
|
|
18
22
|
import type { ServerContext } from "./ServerContext.js";
|
|
19
23
|
|
|
@@ -148,8 +152,10 @@ export class Router {
|
|
|
148
152
|
if (segment.startsWith(":")) {
|
|
149
153
|
const paramName = segment.slice(1);
|
|
150
154
|
if (current.paramChild && current.paramChild.name !== paramName) {
|
|
151
|
-
throw new
|
|
152
|
-
|
|
155
|
+
throw new ConflictingPathParameterNameError(
|
|
156
|
+
definition.path,
|
|
157
|
+
current.paramChild.name,
|
|
158
|
+
paramName
|
|
153
159
|
);
|
|
154
160
|
}
|
|
155
161
|
if (!current.paramChild) {
|
|
@@ -167,9 +173,7 @@ export class Router {
|
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
if (current.methods.has(method)) {
|
|
170
|
-
throw new
|
|
171
|
-
`Route conflict: ${method} ${definition.path} is already registered`
|
|
172
|
-
);
|
|
176
|
+
throw new DuplicateRouteRegistrationError(method, definition.path);
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
current.methods.set(method, normalizedDefinition);
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
validationDefaultError,
|
|
22
22
|
} from "@rexeus/typeweaver-core";
|
|
23
23
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
BodyParseError,
|
|
26
|
+
MissingRouterForPrefixedMountError,
|
|
27
|
+
PayloadTooLargeError,
|
|
28
|
+
} from "./errors/index.js";
|
|
25
29
|
import { executeMiddlewarePipeline } from "./Middleware.js";
|
|
26
30
|
import { Router } from "./Router.js";
|
|
27
31
|
import { StateMap } from "./StateMap.js";
|
|
@@ -154,7 +158,7 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
154
158
|
): this {
|
|
155
159
|
if (typeof prefixOrRouter === "string") {
|
|
156
160
|
if (!router) {
|
|
157
|
-
throw new
|
|
161
|
+
throw new MissingRouterForPrefixedMountError(prefixOrRouter);
|
|
158
162
|
}
|
|
159
163
|
return this.mountRouter(router, prefixOrRouter);
|
|
160
164
|
}
|
|
@@ -248,16 +252,6 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
248
252
|
routeCtx
|
|
249
253
|
);
|
|
250
254
|
} catch (error) {
|
|
251
|
-
if (
|
|
252
|
-
isTypedHttpResponse(error) &&
|
|
253
|
-
match.route.routerConfig.validateResponses
|
|
254
|
-
) {
|
|
255
|
-
return await this.validateResponse(
|
|
256
|
-
match.route,
|
|
257
|
-
toHttpResponse(error),
|
|
258
|
-
routeCtx
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
255
|
return this.handleError(error, routeCtx, match.route);
|
|
262
256
|
}
|
|
263
257
|
}
|
|
@@ -395,7 +389,13 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
395
389
|
const response = await this.safelyExecuteErrorHandler(() =>
|
|
396
390
|
handler(error, ctx)
|
|
397
391
|
);
|
|
398
|
-
if (response)
|
|
392
|
+
if (response) {
|
|
393
|
+
return await this.validateResponse(
|
|
394
|
+
route,
|
|
395
|
+
normalizeHttpResponse(response),
|
|
396
|
+
ctx
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class ConflictingPathParameterNameError extends Error {
|
|
9
|
+
public override readonly name = "ConflictingPathParameterNameError";
|
|
10
|
+
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly path: string,
|
|
13
|
+
public readonly existingParameterName: string,
|
|
14
|
+
public readonly conflictingParameterName: string
|
|
15
|
+
) {
|
|
16
|
+
super(
|
|
17
|
+
`Conflicting path parameter names in '${path}': existing parameter ':${existingParameterName}' conflicts with ':${conflictingParameterName}' at the same route position.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class DuplicateRouteRegistrationError extends Error {
|
|
9
|
+
public override readonly name = "DuplicateRouteRegistrationError";
|
|
10
|
+
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly method: string,
|
|
13
|
+
public readonly path: string
|
|
14
|
+
) {
|
|
15
|
+
super(
|
|
16
|
+
`Duplicate route registration refused: ${method} ${path} is already registered.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class MiddlewareNextAlreadyCalledError extends Error {
|
|
9
|
+
public override readonly name = "MiddlewareNextAlreadyCalledError";
|
|
10
|
+
|
|
11
|
+
public constructor(public readonly middlewareIndex: number) {
|
|
12
|
+
super(
|
|
13
|
+
`Middleware at index ${middlewareIndex} called next() more than once.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class MissingRouterForPrefixedMountError extends Error {
|
|
9
|
+
public override readonly name = "MissingRouterForPrefixedMountError";
|
|
10
|
+
|
|
11
|
+
public constructor(public readonly prefix: string) {
|
|
12
|
+
super(
|
|
13
|
+
`Router is required when mounting with prefix '${prefix}'. Pass both a prefix and a Typeweaver router instance.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class RequestBodyClosedBeforeEndError extends Error {
|
|
9
|
+
public override readonly name = "RequestBodyClosedBeforeEndError";
|
|
10
|
+
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly bytesRead: number,
|
|
13
|
+
public readonly maxBodySize: number
|
|
14
|
+
) {
|
|
15
|
+
super(
|
|
16
|
+
`Request closed before the body finished reading after ${bytesRead} bytes; maximum allowed body size is ${maxBodySize} bytes.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class RequestBodyReadAbortedError extends Error {
|
|
9
|
+
public override readonly name = "RequestBodyReadAbortedError";
|
|
10
|
+
|
|
11
|
+
public constructor(
|
|
12
|
+
public readonly bytesRead: number,
|
|
13
|
+
public readonly maxBodySize: number
|
|
14
|
+
) {
|
|
15
|
+
super(
|
|
16
|
+
`Request was aborted while reading the body after ${bytesRead} bytes; maximum allowed body size is ${maxBodySize} bytes.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
BodyParseError,
|
|
10
|
+
PayloadTooLargeError,
|
|
11
|
+
RequestBodyDrainTimeoutError,
|
|
12
|
+
ResponseSerializationError,
|
|
13
|
+
} from "../Errors.js";
|
|
14
|
+
export { ConflictingPathParameterNameError } from "./ConflictingPathParameterNameError.js";
|
|
15
|
+
export { DuplicateRouteRegistrationError } from "./DuplicateRouteRegistrationError.js";
|
|
16
|
+
export { MiddlewareNextAlreadyCalledError } from "./MiddlewareNextAlreadyCalledError.js";
|
|
17
|
+
export { MissingRouterForPrefixedMountError } from "./MissingRouterForPrefixedMountError.js";
|
|
18
|
+
export { RequestBodyClosedBeforeEndError } from "./RequestBodyClosedBeforeEndError.js";
|
|
19
|
+
export { RequestBodyReadAbortedError } from "./RequestBodyReadAbortedError.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
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
3
|
import {
|
|
4
4
|
hasHeaderName,
|
|
@@ -36,6 +36,33 @@ const POLICY_CONTROLLED_CORS_HEADERS = new Set([
|
|
|
36
36
|
"access-control-max-age",
|
|
37
37
|
]);
|
|
38
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
|
+
|
|
39
66
|
function resolveOrigin(
|
|
40
67
|
configOrigin: CorsOptions["origin"],
|
|
41
68
|
requestOrigin: string | undefined,
|
|
@@ -65,6 +92,15 @@ function getRequestOrigin(
|
|
|
65
92
|
return readSingletonHeader(header, "origin");
|
|
66
93
|
}
|
|
67
94
|
|
|
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
|
+
}
|
|
103
|
+
|
|
68
104
|
function isOriginDependentWithoutRequestOrigin(
|
|
69
105
|
configOrigin: CorsOptions["origin"],
|
|
70
106
|
credentials: boolean
|
|
@@ -76,6 +112,97 @@ function isOriginDependentWithoutRequestOrigin(
|
|
|
76
112
|
);
|
|
77
113
|
}
|
|
78
114
|
|
|
115
|
+
function resolveRequestOrigin(
|
|
116
|
+
options: NormalizedCorsOptions,
|
|
117
|
+
request: CorsRequest
|
|
118
|
+
): string | undefined {
|
|
119
|
+
if (request.hasOrigin && request.origin === undefined) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const resolvedOrigin = resolveOrigin(
|
|
124
|
+
options.origin,
|
|
125
|
+
request.origin,
|
|
126
|
+
options.credentials
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return options.credentials && resolvedOrigin === "*"
|
|
130
|
+
? undefined
|
|
131
|
+
: resolvedOrigin;
|
|
132
|
+
}
|
|
133
|
+
|
|
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(", ");
|
|
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
|
+
}
|
|
198
|
+
|
|
199
|
+
if (options.maxAge !== undefined) {
|
|
200
|
+
corsHeaders["access-control-max-age"] = options.maxAge;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return corsHeaders;
|
|
204
|
+
}
|
|
205
|
+
|
|
79
206
|
function splitHeaderValues(values: readonly string[]): readonly string[] {
|
|
80
207
|
return values.flatMap(value =>
|
|
81
208
|
value
|
|
@@ -131,97 +258,50 @@ function mergeResponseHeaders(
|
|
|
131
258
|
return { ...result, ...mergedCorsHeaders };
|
|
132
259
|
}
|
|
133
260
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
270
|
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const maxAge = options?.maxAge?.toString();
|
|
271
|
+
export function cors(options?: CorsOptions) {
|
|
272
|
+
const normalizedOptions = normalizeCorsOptions(options);
|
|
140
273
|
|
|
141
274
|
return defineMiddleware(async (ctx, next) => {
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const resolvedOrigin =
|
|
145
|
-
hasOrigin && requestOrigin === undefined
|
|
146
|
-
? undefined
|
|
147
|
-
: resolveOrigin(options?.origin, requestOrigin, credentials);
|
|
148
|
-
const origin =
|
|
149
|
-
credentials && resolvedOrigin === "*" ? undefined : resolvedOrigin;
|
|
275
|
+
const corsRequest = readCorsRequest(ctx.request);
|
|
276
|
+
const origin = resolveRequestOrigin(normalizedOptions, corsRequest);
|
|
150
277
|
|
|
151
278
|
if (origin === undefined) {
|
|
152
279
|
const response = await next();
|
|
153
280
|
|
|
154
|
-
if (
|
|
155
|
-
!hasOrigin &&
|
|
156
|
-
!isOriginDependentWithoutRequestOrigin(options?.origin, credentials)
|
|
157
|
-
) {
|
|
281
|
+
if (!shouldVaryDeniedCorsResponse(normalizedOptions, corsRequest)) {
|
|
158
282
|
return response;
|
|
159
283
|
}
|
|
160
284
|
|
|
161
|
-
return {
|
|
162
|
-
...response,
|
|
163
|
-
header: mergeResponseHeaders(response.header, { vary: "Origin" }),
|
|
164
|
-
} satisfies IHttpResponse;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const corsHeaders: Record<string, string> = {
|
|
168
|
-
"access-control-allow-origin": origin,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
if (credentials) {
|
|
172
|
-
corsHeaders["access-control-allow-credentials"] = "true";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (origin !== "*") {
|
|
176
|
-
corsHeaders["vary"] = "Origin";
|
|
285
|
+
return mergeCorsHeadersIntoResponse(response, { vary: "Origin" });
|
|
177
286
|
}
|
|
178
287
|
|
|
179
|
-
|
|
180
|
-
corsHeaders["access-control-expose-headers"] = exposeHeaders;
|
|
181
|
-
}
|
|
288
|
+
const corsHeaders = buildSimpleCorsHeaders(normalizedOptions, origin);
|
|
182
289
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
) !== undefined;
|
|
190
|
-
|
|
191
|
-
if (isPreflight) {
|
|
192
|
-
corsHeaders["access-control-allow-methods"] = methods;
|
|
193
|
-
|
|
194
|
-
const configuredHeaders = options?.allowHeaders;
|
|
195
|
-
if (configuredHeaders !== undefined) {
|
|
196
|
-
if (configuredHeaders.length > 0) {
|
|
197
|
-
corsHeaders["access-control-allow-headers"] =
|
|
198
|
-
configuredHeaders.join(", ");
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
const requestedHeaders = readSingletonHeader(
|
|
202
|
-
ctx.request.header,
|
|
203
|
-
"access-control-request-headers"
|
|
204
|
-
);
|
|
205
|
-
if (typeof requestedHeaders === "string") {
|
|
206
|
-
corsHeaders["access-control-allow-headers"] = requestedHeaders;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (maxAge) {
|
|
211
|
-
corsHeaders["access-control-max-age"] = maxAge;
|
|
212
|
-
}
|
|
290
|
+
if (isPreflightCorsRequest(corsRequest)) {
|
|
291
|
+
const preflightHeaders = buildPreflightCorsHeaders(
|
|
292
|
+
normalizedOptions,
|
|
293
|
+
corsRequest,
|
|
294
|
+
corsHeaders
|
|
295
|
+
);
|
|
213
296
|
|
|
214
297
|
return {
|
|
215
298
|
statusCode: 204,
|
|
216
|
-
header:
|
|
299
|
+
header: preflightHeaders,
|
|
217
300
|
} satisfies IHttpResponse;
|
|
218
301
|
}
|
|
219
302
|
|
|
220
303
|
const response = await next();
|
|
221
304
|
|
|
222
|
-
return
|
|
223
|
-
...response,
|
|
224
|
-
header: mergeResponseHeaders(response.header, corsHeaders),
|
|
225
|
-
} satisfies IHttpResponse;
|
|
305
|
+
return mergeCorsHeadersIntoResponse(response, corsHeaders);
|
|
226
306
|
});
|
|
227
307
|
}
|
|
@@ -2,12 +2,22 @@ import { pathMatcher } from "../PathMatcher.js";
|
|
|
2
2
|
import { defineMiddleware } from "../TypedMiddleware.js";
|
|
3
3
|
import type { TypedMiddleware } from "../TypedMiddleware.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Rejects middleware that would provide state from a conditional branch. */
|
|
6
|
+
type RejectsProvidedState<TProvides extends Record<string, unknown>> = [
|
|
6
7
|
keyof TProvides,
|
|
7
8
|
] extends [never]
|
|
8
9
|
? unknown
|
|
9
10
|
: never;
|
|
10
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
|
+
|
|
11
21
|
/**
|
|
12
22
|
* Restricts a middleware to only run on paths matching the given patterns.
|
|
13
23
|
*
|
|
@@ -30,7 +40,7 @@ export function scoped<
|
|
|
30
40
|
TRequires extends Record<string, unknown>,
|
|
31
41
|
>(
|
|
32
42
|
paths: readonly string[],
|
|
33
|
-
middleware:
|
|
43
|
+
middleware: StateNeutralMiddleware<TProvides, TRequires>
|
|
34
44
|
): TypedMiddleware<TProvides, TRequires> {
|
|
35
45
|
const matchers = paths.map(pathMatcher);
|
|
36
46
|
|
|
@@ -58,7 +68,7 @@ export function except<
|
|
|
58
68
|
TRequires extends Record<string, unknown>,
|
|
59
69
|
>(
|
|
60
70
|
paths: readonly string[],
|
|
61
|
-
middleware:
|
|
71
|
+
middleware: StateNeutralMiddleware<TProvides, TRequires>
|
|
62
72
|
): TypedMiddleware<TProvides, TRequires> {
|
|
63
73
|
const matchers = paths.map(pathMatcher);
|
|
64
74
|
|
|
@@ -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"
|