@nestia/core 14.0.0 → 14.0.2
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/MIGRATION.md +20 -0
- package/README.md +1 -1
- package/lib/adaptors/McpAdaptor.d.ts +29 -3
- package/lib/adaptors/McpAdaptor.js +150 -24
- package/lib/adaptors/McpAdaptor.js.map +1 -1
- package/lib/adaptors/WebSocketAdaptor.d.ts +37 -0
- package/lib/adaptors/WebSocketAdaptor.js +184 -48
- package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
- package/lib/adaptors/internal/RoutePathMatcher.d.ts +1 -0
- package/lib/adaptors/internal/RoutePathMatcher.js +88 -0
- package/lib/adaptors/internal/RoutePathMatcher.js.map +1 -0
- package/lib/adaptors/internal/external_context.d.ts +1 -0
- package/lib/adaptors/internal/external_context.js +56 -0
- package/lib/adaptors/internal/external_context.js.map +1 -0
- package/lib/decorators/DynamicModule.d.ts +12 -0
- package/lib/decorators/DynamicModule.js +12 -0
- package/lib/decorators/DynamicModule.js.map +1 -1
- package/lib/decorators/EncryptedBody.d.ts +14 -6
- package/lib/decorators/EncryptedBody.js +39 -16
- package/lib/decorators/EncryptedBody.js.map +1 -1
- package/lib/decorators/EncryptedController.d.ts +6 -1
- package/lib/decorators/EncryptedController.js +7 -2
- package/lib/decorators/EncryptedController.js.map +1 -1
- package/lib/decorators/EncryptedModule.d.ts +25 -6
- package/lib/decorators/EncryptedModule.js +87 -18
- package/lib/decorators/EncryptedModule.js.map +1 -1
- package/lib/decorators/EncryptedRoute.d.ts +20 -11
- package/lib/decorators/EncryptedRoute.js +27 -13
- package/lib/decorators/EncryptedRoute.js.map +1 -1
- package/lib/decorators/HumanRoute.d.ts +5 -0
- package/lib/decorators/HumanRoute.js +5 -0
- package/lib/decorators/HumanRoute.js.map +1 -1
- package/lib/decorators/McpRoute.d.ts +31 -19
- package/lib/decorators/McpRoute.js +12 -1
- package/lib/decorators/McpRoute.js.map +1 -1
- package/lib/decorators/PlainBody.d.ts +7 -1
- package/lib/decorators/PlainBody.js +16 -8
- package/lib/decorators/PlainBody.js.map +1 -1
- package/lib/decorators/SwaggerCustomizer.d.ts +15 -0
- package/lib/decorators/SwaggerCustomizer.js +5 -0
- package/lib/decorators/SwaggerCustomizer.js.map +1 -1
- package/lib/decorators/SwaggerExample.d.ts +51 -22
- package/lib/decorators/SwaggerExample.js +35 -22
- package/lib/decorators/SwaggerExample.js.map +1 -1
- package/lib/decorators/TypedBody.d.ts +10 -3
- package/lib/decorators/TypedBody.js +20 -10
- package/lib/decorators/TypedBody.js.map +1 -1
- package/lib/decorators/TypedException.d.ts +10 -0
- package/lib/decorators/TypedFormData.d.ts +81 -9
- package/lib/decorators/TypedFormData.js +121 -31
- package/lib/decorators/TypedFormData.js.map +1 -1
- package/lib/decorators/TypedHeaders.d.ts +5 -0
- package/lib/decorators/TypedHeaders.js +5 -0
- package/lib/decorators/TypedHeaders.js.map +1 -1
- package/lib/decorators/TypedParam.d.ts +12 -3
- package/lib/decorators/TypedParam.js +12 -3
- package/lib/decorators/TypedParam.js.map +1 -1
- package/lib/decorators/TypedQuery.d.ts +17 -1
- package/lib/decorators/TypedQuery.js +25 -4
- package/lib/decorators/TypedQuery.js.map +1 -1
- package/lib/decorators/TypedRoute.d.ts +25 -10
- package/lib/decorators/TypedRoute.js +19 -10
- package/lib/decorators/TypedRoute.js.map +1 -1
- package/lib/decorators/WebSocketRoute.d.ts +18 -1
- package/lib/decorators/WebSocketRoute.js +18 -1
- package/lib/decorators/WebSocketRoute.js.map +1 -1
- package/lib/decorators/doNotThrowTransformError.d.ts +14 -0
- package/lib/decorators/doNotThrowTransformError.js +14 -0
- package/lib/decorators/doNotThrowTransformError.js.map +1 -1
- package/lib/decorators/internal/EncryptedConstant.js +13 -2
- package/lib/decorators/internal/EncryptedConstant.js.map +1 -1
- package/lib/decorators/internal/IRequestBodyValidation.d.ts +66 -0
- package/lib/decorators/internal/IRequestBodyValidation.js +3 -0
- package/lib/decorators/internal/IRequestBodyValidation.js.map +1 -0
- package/lib/decorators/internal/IWebSocketRouteReflect.d.ts +93 -1
- package/lib/decorators/internal/get_encryption_password.d.ts +1 -0
- package/lib/decorators/internal/get_encryption_password.js +14 -0
- package/lib/decorators/internal/get_encryption_password.js.map +1 -0
- package/lib/decorators/internal/get_path_and_querify.js +23 -9
- package/lib/decorators/internal/get_path_and_querify.js.map +1 -1
- package/lib/decorators/internal/get_path_and_stringify.js +3 -7
- package/lib/decorators/internal/get_path_and_stringify.js.map +1 -1
- package/lib/decorators/internal/get_text_body.js +9 -3
- package/lib/decorators/internal/get_text_body.js.map +1 -1
- package/lib/decorators/internal/group_entries.d.ts +1 -0
- package/lib/decorators/internal/group_entries.js +19 -0
- package/lib/decorators/internal/group_entries.js.map +1 -0
- package/lib/decorators/internal/load_controller.d.ts +14 -0
- package/lib/decorators/internal/load_controller.js +14 -0
- package/lib/decorators/internal/load_controller.js.map +1 -1
- package/lib/decorators/internal/split_route_arguments.d.ts +1 -0
- package/lib/decorators/internal/split_route_arguments.js +22 -0
- package/lib/decorators/internal/split_route_arguments.js.map +1 -0
- package/lib/decorators/internal/validate_request_body.js +68 -25
- package/lib/decorators/internal/validate_request_body.js.map +1 -1
- package/lib/decorators/internal/validate_request_form_data.js +3 -1
- package/lib/decorators/internal/validate_request_form_data.js.map +1 -1
- package/lib/decorators/internal/validate_request_headers.js +2 -1
- package/lib/decorators/internal/validate_request_headers.js.map +1 -1
- package/lib/decorators/internal/validate_request_query.js +3 -1
- package/lib/decorators/internal/validate_request_query.js.map +1 -1
- package/lib/options/IRequestBodyValidator.d.ts +71 -0
- package/lib/options/IRequestFormDataProps.d.ts +80 -0
- package/lib/options/IRequestHeadersValidator.d.ts +67 -0
- package/lib/options/IRequestQueryValidator.d.ts +68 -0
- package/lib/options/IResponseBodyQuerifier.d.ts +125 -1
- package/lib/options/IResponseBodyStringifier.d.ts +107 -0
- package/lib/typings/Creator.d.ts +10 -0
- package/lib/utils/ArrayUtil.d.ts +21 -0
- package/lib/utils/ArrayUtil.js +22 -1
- package/lib/utils/ArrayUtil.js.map +1 -1
- package/lib/utils/ExceptionManager.d.ts +43 -4
- package/lib/utils/ExceptionManager.js +44 -6
- package/lib/utils/ExceptionManager.js.map +1 -1
- package/lib/utils/SourceFinder.d.ts +22 -0
- package/lib/utils/SourceFinder.js +60 -10
- package/lib/utils/SourceFinder.js.map +1 -1
- package/lib/utils/VersioningStrategy.d.ts +45 -2
- package/lib/utils/VersioningStrategy.js +40 -7
- package/lib/utils/VersioningStrategy.js.map +1 -1
- package/native/go.mod +1 -1
- package/native/go.sum +2 -2
- package/native/plugin/plan.go +68 -2
- package/native/transform/ast.go +23 -3
- package/native/transform/build.go +51 -57
- package/native/transform/buildoutput/output.go +147 -0
- package/native/transform/contributor.go +9 -0
- package/native/transform/core_querify.go +63 -9
- package/native/transform/core_transform.go +180 -98
- package/native/transform/core_websocket.go +111 -14
- package/native/transform/node_transform.go +17 -8
- package/native/transform/package.go +55 -0
- package/native/transform/printer.go +1 -3
- package/native/transform/run.go +18 -0
- package/native/transform/schemaprojection/project.go +213 -0
- package/native/transform/transform.go +29 -18
- package/native/transform/typia_fast.go +6 -0
- package/native/transform.cjs +49 -0
- package/package.json +13 -15
- package/src/adaptors/McpAdaptor.ts +215 -33
- package/src/adaptors/WebSocketAdaptor.ts +202 -52
- package/src/adaptors/internal/RoutePathMatcher.ts +91 -0
- package/src/adaptors/internal/external_context.ts +81 -0
- package/src/decorators/DynamicModule.ts +12 -0
- package/src/decorators/EncryptedBody.ts +39 -15
- package/src/decorators/EncryptedController.ts +12 -3
- package/src/decorators/EncryptedModule.ts +89 -18
- package/src/decorators/EncryptedRoute.ts +27 -16
- package/src/decorators/HumanRoute.ts +5 -0
- package/src/decorators/McpRoute.ts +31 -19
- package/src/decorators/PlainBody.ts +20 -10
- package/src/decorators/SwaggerCustomizer.ts +15 -0
- package/src/decorators/SwaggerExample.ts +51 -22
- package/src/decorators/TypedBody.ts +18 -9
- package/src/decorators/TypedException.ts +10 -0
- package/src/decorators/TypedFormData.ts +207 -45
- package/src/decorators/TypedHeaders.ts +5 -0
- package/src/decorators/TypedParam.ts +12 -3
- package/src/decorators/TypedQuery.ts +35 -7
- package/src/decorators/TypedRoute.ts +25 -10
- package/src/decorators/WebSocketRoute.ts +18 -1
- package/src/decorators/doNotThrowTransformError.ts +14 -0
- package/src/decorators/internal/EncryptedConstant.ts +14 -1
- package/src/decorators/internal/IMcpRouteReflect.ts +7 -1
- package/src/decorators/internal/IRequestBodyValidation.ts +65 -0
- package/src/decorators/internal/IWebSocketRouteReflect.ts +95 -1
- package/src/decorators/internal/get_encryption_password.ts +19 -0
- package/src/decorators/internal/get_path_and_querify.ts +31 -10
- package/src/decorators/internal/get_path_and_stringify.ts +3 -9
- package/src/decorators/internal/get_text_body.ts +8 -4
- package/src/decorators/internal/group_entries.ts +17 -0
- package/src/decorators/internal/load_controller.ts +14 -0
- package/src/decorators/internal/split_route_arguments.ts +20 -0
- package/src/decorators/internal/validate_request_body.ts +87 -34
- package/src/decorators/internal/validate_request_form_data.ts +3 -1
- package/src/decorators/internal/validate_request_headers.ts +2 -2
- package/src/decorators/internal/validate_request_query.ts +3 -2
- package/src/options/IRequestBodyValidator.ts +71 -0
- package/src/options/IRequestFormDataProps.ts +80 -0
- package/src/options/IRequestHeadersValidator.ts +67 -0
- package/src/options/IRequestQueryValidator.ts +68 -0
- package/src/options/IResponseBodyQuerifier.ts +126 -1
- package/src/options/IResponseBodyStringifier.ts +107 -0
- package/src/typings/Creator.ts +10 -0
- package/src/utils/ArrayUtil.ts +22 -3
- package/src/utils/ExceptionManager.ts +50 -6
- package/src/utils/SourceFinder.ts +62 -10
- package/src/utils/VersioningStrategy.ts +55 -11
- package/lib/transform.d.ts +0 -10
- package/lib/transform.js +0 -25
- package/lib/transform.js.map +0 -1
- package/src/transform.ts +0 -26
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/// <reference path="../typings/get-function-location.d.ts" />
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
HttpException,
|
|
4
|
+
INestApplication,
|
|
5
|
+
VersioningType,
|
|
6
|
+
} from "@nestjs/common";
|
|
3
7
|
import {
|
|
4
8
|
HOST_METADATA,
|
|
5
9
|
MODULE_PATH,
|
|
@@ -14,23 +18,62 @@ import { Module } from "@nestjs/core/injector/module";
|
|
|
14
18
|
import getFunctionLocation from "get-function-location";
|
|
15
19
|
import { IncomingMessage, Server } from "http";
|
|
16
20
|
import path from "path";
|
|
17
|
-
import { Path } from "path-parser";
|
|
18
21
|
import { Duplex } from "stream";
|
|
19
22
|
import { WebSocketAcceptor } from "tgrid";
|
|
20
23
|
import typia from "typia";
|
|
24
|
+
import { fileURLToPath } from "url";
|
|
21
25
|
import WebSocket from "ws";
|
|
22
26
|
|
|
23
27
|
import { IWebSocketRouteReflect } from "../decorators/internal/IWebSocketRouteReflect";
|
|
28
|
+
import { resolve_request_body } from "../decorators/internal/validate_request_body";
|
|
24
29
|
import { ArrayUtil } from "../utils/ArrayUtil";
|
|
25
30
|
import { VersioningStrategy } from "../utils/VersioningStrategy";
|
|
31
|
+
import { RoutePathMatcher } from "./internal/RoutePathMatcher";
|
|
26
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Serves the `@WebSocketRoute()` methods of a NestJS application over
|
|
35
|
+
* WebSocket.
|
|
36
|
+
*
|
|
37
|
+
* Call {@link upgrade} after the application has been created. It finds the
|
|
38
|
+
* routes, checks them, and takes over the HTTP server's `upgrade` events. A
|
|
39
|
+
* handshake whose path matches no route is rejected with close code 1002.
|
|
40
|
+
*
|
|
41
|
+
* @evidence contracts/common.md#principled-implementation Routes are matched by paths built from the global prefix, the version, the module prefix, the controller path, and the method path, joined and normalized the same way for every combination; the adapter answers upgrade events itself through a `ws` server in no-server mode and removes its listeners when the HTTP server closes.
|
|
42
|
+
* @evidence contracts/common.md#clear-and-simple-design A class with one static factory, one `close` property, and the private handler; discovery, method checks, termination, and close reasons are module-private functions.
|
|
43
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The global prefix, versioning options, and module graph are read from `app.config` and `app.container`, which NestJS keeps internal because `INestApplication` has no public accessor for them; this is the limitation of the adapter, not a fixture-specific branch.
|
|
44
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states when to call it and what happens to an unmatched handshake.
|
|
45
|
+
* @evidence contracts/portability.md#os-neutral-implementation Diagnostic source URLs are converted by fileURLToPath before path.relative uses native paths; route matching uses HTTP path segments separately from filesystem paths. No filesystem case policy is inferred from the OS.
|
|
46
|
+
*/
|
|
27
47
|
export class WebSocketAdaptor {
|
|
48
|
+
/**
|
|
49
|
+
* Creates the adapter for the application: it visits every controller,
|
|
50
|
+
* validates the WebSocket routes, and starts listening for upgrade events.
|
|
51
|
+
*
|
|
52
|
+
* All route errors of the application are collected and thrown together, with
|
|
53
|
+
* the controller, method, source location, and reasons of each.
|
|
54
|
+
*
|
|
55
|
+
* @evidence contracts/common.md#principled-implementation Controller data-method descriptors are checked before any handshake is served, with child properties shadowing inherited methods and accessors never executed, and all defects are reported in one error, so a misconfigured route fails at start and not at the first client; source locations come from `get-function-location`, converted with `fileURLToPath` so a `file:` URL becomes the path on every platform.
|
|
56
|
+
* @evidence contracts/common.md#clear-and-simple-design One asynchronous factory that awaits the visitor and hands the operator list to the private constructor.
|
|
57
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The visitor reports through the same error path for every controller; no controller or route name is special-cased.
|
|
58
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the collected error report and the effect on the server.
|
|
59
|
+
* @evidence contracts/portability.md#os-neutral-implementation Diagnostic source URLs are converted by fileURLToPath before path.relative uses native paths; route matching uses HTTP path segments separately from filesystem paths. No filesystem case policy is inferred from the OS.
|
|
60
|
+
*/
|
|
28
61
|
public static async upgrade(
|
|
29
62
|
app: INestApplication,
|
|
30
63
|
): Promise<WebSocketAdaptor> {
|
|
31
64
|
return new this(app, await visitApplication(app));
|
|
32
65
|
}
|
|
33
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Stops the adapter: removes its listeners from the HTTP server and closes
|
|
69
|
+
* the WebSocket server.
|
|
70
|
+
*
|
|
71
|
+
* @evidence contracts/common.md#principled-implementation The listeners are removed before the `ws` server closes, so no upgrade event reaches a closing server, and the returned promise resolves when the server has closed.
|
|
72
|
+
* @evidence contracts/common.md#clear-and-simple-design An arrow property so the same function value can be registered and unregistered as the HTTP server's close listener.
|
|
73
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It releases exactly what the adapter registered.
|
|
74
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states what is released.
|
|
75
|
+
* @evidence contracts/portability.md#os-neutral-implementation Diagnostic source URLs are converted by fileURLToPath before path.relative uses native paths; route matching uses HTTP path segments separately from filesystem paths. No filesystem case policy is inferred from the OS.
|
|
76
|
+
*/
|
|
34
77
|
public readonly close = async (): Promise<void> =>
|
|
35
78
|
new Promise((resolve) => {
|
|
36
79
|
this.http.off("close", this.close);
|
|
@@ -62,25 +105,19 @@ export class WebSocketAdaptor {
|
|
|
62
105
|
})();
|
|
63
106
|
for (const op of this.operators) {
|
|
64
107
|
const params: Record<string, string> | null = op.parser.test(path);
|
|
65
|
-
if (params
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
await acceptor.reject(
|
|
74
|
-
1008,
|
|
75
|
-
error instanceof Error
|
|
76
|
-
? JSON.stringify({ ...error })
|
|
77
|
-
: "unknown error",
|
|
78
|
-
);
|
|
79
|
-
} finally {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
108
|
+
if (params === null) continue;
|
|
109
|
+
try {
|
|
110
|
+
await op.handler({ params, acceptor });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
await terminate({ acceptor, socket: client, error });
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
82
115
|
}
|
|
83
|
-
await
|
|
116
|
+
await terminate({
|
|
117
|
+
acceptor,
|
|
118
|
+
socket: client,
|
|
119
|
+
error: new WebSocketRejection(1002, "WebSocket API not found"),
|
|
120
|
+
});
|
|
84
121
|
},
|
|
85
122
|
),
|
|
86
123
|
);
|
|
@@ -107,10 +144,10 @@ const visitApplication = async (
|
|
|
107
144
|
return versioning === undefined || versioning.type !== VersioningType.URI
|
|
108
145
|
? undefined
|
|
109
146
|
: {
|
|
147
|
+
// as NestJS's RoutePathFactory.getVersionPrefix(): `false` is no
|
|
148
|
+
// prefix at all, and only an absent one is the default "v"
|
|
110
149
|
prefix:
|
|
111
|
-
versioning.prefix ===
|
|
112
|
-
? "v"
|
|
113
|
-
: versioning.prefix,
|
|
150
|
+
versioning.prefix === false ? "" : (versioning.prefix ?? "v"),
|
|
114
151
|
defaultVersion: versioning.defaultVersion,
|
|
115
152
|
};
|
|
116
153
|
})(),
|
|
@@ -209,8 +246,7 @@ const visitController = async (props: {
|
|
|
209
246
|
modulePrefix: props.modulePrefix,
|
|
210
247
|
};
|
|
211
248
|
for (const mk of getOwnPropertyNames(controller.prototype).filter(
|
|
212
|
-
(key) =>
|
|
213
|
-
key !== "constructor" && typeof controller.prototype[key] === "function",
|
|
249
|
+
(key) => key !== "constructor",
|
|
214
250
|
)) {
|
|
215
251
|
const errorMessages: string[] = [];
|
|
216
252
|
visitMethod({
|
|
@@ -231,7 +267,9 @@ const visitController = async (props: {
|
|
|
231
267
|
...file,
|
|
232
268
|
source: path.relative(
|
|
233
269
|
process.cwd(),
|
|
234
|
-
file.source.
|
|
270
|
+
file.source.startsWith("file:")
|
|
271
|
+
? fileURLToPath(file.source)
|
|
272
|
+
: file.source,
|
|
235
273
|
),
|
|
236
274
|
});
|
|
237
275
|
}
|
|
@@ -282,16 +320,16 @@ const visitMethod = (props: {
|
|
|
282
320
|
].join("\n"),
|
|
283
321
|
);
|
|
284
322
|
|
|
285
|
-
const versions: string[] = VersioningStrategy.merge(props.config.versioning)(
|
|
286
|
-
|
|
287
|
-
|
|
323
|
+
const versions: string[] = VersioningStrategy.merge(props.config.versioning)({
|
|
324
|
+
controller: props.controller.versions,
|
|
325
|
+
method: VersioningStrategy.cast(
|
|
288
326
|
Reflect.getMetadata(VERSION_METADATA, props.method.value),
|
|
289
327
|
),
|
|
290
|
-
|
|
328
|
+
});
|
|
291
329
|
for (const v of versions)
|
|
292
330
|
for (const cp of wrapPaths(props.controller.prefixes))
|
|
293
331
|
for (const mp of wrapPaths(route.paths)) {
|
|
294
|
-
const parser:
|
|
332
|
+
const parser: RoutePathMatcher = new RoutePathMatcher(
|
|
295
333
|
"/" +
|
|
296
334
|
[
|
|
297
335
|
props.config.globalPrefix ?? "",
|
|
@@ -344,16 +382,21 @@ const visitMethod = (props: {
|
|
|
344
382
|
else if (p.category === "driver")
|
|
345
383
|
args.push(input.acceptor.getDriver());
|
|
346
384
|
else if (p.category === "header") {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
385
|
+
const result = resolve_request_body(
|
|
386
|
+
p.validate,
|
|
387
|
+
input.acceptor.header,
|
|
388
|
+
);
|
|
389
|
+
if (!result.success) throw result.error;
|
|
390
|
+
args.push(result.data);
|
|
350
391
|
} else if (p.category === "param")
|
|
351
392
|
args.push(p.assert(input.params[p.field]!));
|
|
352
393
|
else if (p.category === "query") {
|
|
394
|
+
// the query is all after the first "?", which it may hold too
|
|
395
|
+
const index: number = input.acceptor.path.indexOf("?");
|
|
353
396
|
const query: any | Error = p.validate(
|
|
354
397
|
new URLSearchParams(
|
|
355
|
-
|
|
356
|
-
? input.acceptor.path.
|
|
398
|
+
index !== -1
|
|
399
|
+
? input.acceptor.path.substring(index + 1)
|
|
357
400
|
: "",
|
|
358
401
|
),
|
|
359
402
|
);
|
|
@@ -361,13 +404,8 @@ const visitMethod = (props: {
|
|
|
361
404
|
args.push(query);
|
|
362
405
|
}
|
|
363
406
|
} catch (exp) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
exp instanceof Error
|
|
367
|
-
? JSON.stringify({ ...exp })
|
|
368
|
-
: "unknown error",
|
|
369
|
-
);
|
|
370
|
-
return;
|
|
407
|
+
// an invalid handshake: rejected before the handler runs
|
|
408
|
+
throw new WebSocketRejection(1003, exp);
|
|
371
409
|
}
|
|
372
410
|
await props.method.value.call(props.controller.instance, ...args);
|
|
373
411
|
},
|
|
@@ -375,16 +413,128 @@ const visitMethod = (props: {
|
|
|
375
413
|
}
|
|
376
414
|
};
|
|
377
415
|
|
|
416
|
+
/**
|
|
417
|
+
* A handshake rejected on purpose, with its close code: 1002 for no route, 1003
|
|
418
|
+
* for a header, param, or query failing its type.
|
|
419
|
+
*/
|
|
420
|
+
class WebSocketRejection {
|
|
421
|
+
public constructor(
|
|
422
|
+
public readonly code: number,
|
|
423
|
+
public readonly cause: unknown,
|
|
424
|
+
) {}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Ends a WebSocket request that failed, so the client always learns it: a
|
|
429
|
+
* handshake not accepted yet is rejected, 1008 for an error of the route
|
|
430
|
+
* itself, and an accepted connection is closed with 1011. When tgrid refuses
|
|
431
|
+
* either, as it does while `accept()` is still running, the socket is closed
|
|
432
|
+
* directly. Nothing escapes, as the upgrade callback has no one to catch it.
|
|
433
|
+
*/
|
|
434
|
+
const terminate = async (props: {
|
|
435
|
+
acceptor: WebSocketAcceptor<any, any, any>;
|
|
436
|
+
socket: WebSocket;
|
|
437
|
+
error: unknown;
|
|
438
|
+
}): Promise<void> => {
|
|
439
|
+
const reason: string = closeReason(
|
|
440
|
+
props.error instanceof WebSocketRejection ? props.error.cause : props.error,
|
|
441
|
+
);
|
|
442
|
+
const state: WebSocketAcceptor.State = props.acceptor.state;
|
|
443
|
+
if (
|
|
444
|
+
state === WebSocketAcceptor.State.REJECTING ||
|
|
445
|
+
state === WebSocketAcceptor.State.CLOSING ||
|
|
446
|
+
state === WebSocketAcceptor.State.CLOSED
|
|
447
|
+
)
|
|
448
|
+
return; // the route already ends the connection itself
|
|
449
|
+
const code: number =
|
|
450
|
+
state !== WebSocketAcceptor.State.NONE
|
|
451
|
+
? 1011
|
|
452
|
+
: props.error instanceof WebSocketRejection
|
|
453
|
+
? props.error.code
|
|
454
|
+
: 1008;
|
|
455
|
+
try {
|
|
456
|
+
if (state === WebSocketAcceptor.State.NONE)
|
|
457
|
+
return await props.acceptor.reject(code, reason);
|
|
458
|
+
else if (state === WebSocketAcceptor.State.OPEN)
|
|
459
|
+
return await props.acceptor.close(code, reason);
|
|
460
|
+
} catch {}
|
|
461
|
+
try {
|
|
462
|
+
props.socket.close(code, reason);
|
|
463
|
+
} catch {
|
|
464
|
+
props.socket.terminate();
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The reason a close frame carries: the error's message, cut to the 123 bytes
|
|
470
|
+
* of UTF-8 a close reason may hold, never inside a character. A longer reason
|
|
471
|
+
* makes `ws` throw instead of closing, and the client waits forever.
|
|
472
|
+
*
|
|
473
|
+
* A validator's `BadRequestException` carries a generic message, so the
|
|
474
|
+
* property it names is preferred: typia's `reason` from an assertion, or the
|
|
475
|
+
* first of the `errors` from a validation.
|
|
476
|
+
*/
|
|
477
|
+
const closeReason = (error: unknown): string => {
|
|
478
|
+
const message: string =
|
|
479
|
+
invalidProperty(error) ??
|
|
480
|
+
(error instanceof Error
|
|
481
|
+
? error.message
|
|
482
|
+
: typeof error === "string"
|
|
483
|
+
? error
|
|
484
|
+
: "unknown error");
|
|
485
|
+
const encoded: Buffer = Buffer.from(message, "utf8");
|
|
486
|
+
if (encoded.length <= MAX_CLOSE_REASON_BYTES) return message;
|
|
487
|
+
let end: number = MAX_CLOSE_REASON_BYTES;
|
|
488
|
+
// back off continuation bytes (10xxxxxx) to a character boundary
|
|
489
|
+
while (end > 0 && (encoded[end]! & 0xc0) === 0x80) --end;
|
|
490
|
+
return encoded.subarray(0, end).toString("utf8");
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const invalidProperty = (error: unknown): string | null => {
|
|
494
|
+
if (!(error instanceof HttpException)) return null;
|
|
495
|
+
const response: unknown = error.getResponse();
|
|
496
|
+
if (typeof response !== "object" || response === null) return null;
|
|
497
|
+
const { reason, errors } = response as {
|
|
498
|
+
reason?: unknown;
|
|
499
|
+
errors?: unknown;
|
|
500
|
+
};
|
|
501
|
+
if (typeof reason === "string") return reason;
|
|
502
|
+
if (Array.isArray(errors) && errors.length !== 0) {
|
|
503
|
+
const first: { path?: unknown; expected?: unknown } = errors[0];
|
|
504
|
+
if (typeof first?.path === "string" && typeof first.expected === "string")
|
|
505
|
+
return [
|
|
506
|
+
`invalid type on ${first.path}, expect to be ${first.expected}`,
|
|
507
|
+
errors.length > 1 ? ` (and ${errors.length - 1} more)` : "",
|
|
508
|
+
].join("");
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* RFC 6455 §5.5: a control frame's payload is at most 125 bytes, 2 of them the
|
|
515
|
+
* code.
|
|
516
|
+
*/
|
|
517
|
+
const MAX_CLOSE_REASON_BYTES: number = 123;
|
|
518
|
+
|
|
378
519
|
const wrapPaths = (value: string[]) => (value.length === 0 ? [""] : value);
|
|
379
520
|
const getOwnPropertyNames = (prototype: any): string[] => {
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
521
|
+
const visited: Set<string> = new Set();
|
|
522
|
+
const result: string[] = [];
|
|
523
|
+
for (
|
|
524
|
+
let current = prototype;
|
|
525
|
+
current !== null;
|
|
526
|
+
current = Object.getPrototypeOf(current)
|
|
527
|
+
)
|
|
528
|
+
for (const key of Object.getOwnPropertyNames(current)) {
|
|
529
|
+
if (visited.has(key)) continue;
|
|
530
|
+
visited.add(key);
|
|
531
|
+
if (
|
|
532
|
+
typeof Object.getOwnPropertyDescriptor(current, key)?.value ===
|
|
533
|
+
"function"
|
|
534
|
+
)
|
|
535
|
+
result.push(key);
|
|
536
|
+
}
|
|
537
|
+
return result;
|
|
388
538
|
};
|
|
389
539
|
|
|
390
540
|
interface Entry<T> {
|
|
@@ -402,7 +552,7 @@ interface IController {
|
|
|
402
552
|
modulePrefix: string;
|
|
403
553
|
}
|
|
404
554
|
interface IOperator {
|
|
405
|
-
parser:
|
|
555
|
+
parser: RoutePathMatcher;
|
|
406
556
|
handler: (props: {
|
|
407
557
|
params: Record<string, string>;
|
|
408
558
|
acceptor: WebSocketAcceptor<any, any, any>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Token, match, parse } from "path-to-regexp";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A WebSocket route's path, matched as NestJS 11's Express router matches an
|
|
5
|
+
* HTTP route: path-to-regexp 8's syntax, where `\:` is a literal colon, `*name`
|
|
6
|
+
* a wildcard, and `{...}` an optional group; find-my-way's (Fastify) literal
|
|
7
|
+
* colon `::` is read too, as `@nestia/sdk` reads it when it builds the SDK's
|
|
8
|
+
* URL.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export class RoutePathMatcher {
|
|
13
|
+
/** The names of the path's parameters and wildcards, in order. */
|
|
14
|
+
public readonly params: string[];
|
|
15
|
+
|
|
16
|
+
private readonly match_: (
|
|
17
|
+
path: string,
|
|
18
|
+
) => false | { params: Partial<Record<string, string | string[]>> };
|
|
19
|
+
|
|
20
|
+
public constructor(private readonly path: string) {
|
|
21
|
+
const route: string = fromFastifyColons(path);
|
|
22
|
+
this.params = names(parse(route).tokens);
|
|
23
|
+
this.match_ = match(route, {
|
|
24
|
+
decode: (value: string) => {
|
|
25
|
+
try {
|
|
26
|
+
return decodeURIComponent(value);
|
|
27
|
+
} catch {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The parameters of a matching request path, or `null`. A wildcard's segments
|
|
36
|
+
* are joined by `/`, and an optional parameter absent is left out.
|
|
37
|
+
*/
|
|
38
|
+
public test(path: string): Record<string, string> | null {
|
|
39
|
+
const matched = this.match_(path);
|
|
40
|
+
if (matched === false) return null;
|
|
41
|
+
const output: Record<string, string> = {};
|
|
42
|
+
for (const [key, value] of Object.entries(matched.params))
|
|
43
|
+
if (value !== undefined)
|
|
44
|
+
output[key] = Array.isArray(value) ? value.join("/") : value;
|
|
45
|
+
return output;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public toString(): string {
|
|
49
|
+
return this.path;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const names = (tokens: Token[]): string[] =>
|
|
54
|
+
tokens.flatMap((token) =>
|
|
55
|
+
token.type === "param" || token.type === "wildcard"
|
|
56
|
+
? [token.name]
|
|
57
|
+
: token.type === "group"
|
|
58
|
+
? names(token.tokens)
|
|
59
|
+
: [],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A route with find-my-way's (Fastify) literal colon `::` spelled as
|
|
64
|
+
* path-to-regexp's `\:`. find-my-way reads `::` as a colon in static text
|
|
65
|
+
* alone: after a parameter it is part of the parameter's name, so it is left as
|
|
66
|
+
* is there, and path-to-regexp then rejects the route, as no parameter of that
|
|
67
|
+
* name is what the handler reads.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
const fromFastifyColons = (route: string): string => {
|
|
72
|
+
let output: string = "";
|
|
73
|
+
for (let i: number = 0; i < route.length; ) {
|
|
74
|
+
if (route[i] === "\\") {
|
|
75
|
+
output += route.slice(i, i + 2);
|
|
76
|
+
i += 2;
|
|
77
|
+
} else if (route.startsWith("::", i)) {
|
|
78
|
+
output += "\\:";
|
|
79
|
+
i += 2;
|
|
80
|
+
} else if (route[i] === ":") {
|
|
81
|
+
const name: string = /^:[A-Za-z0-9_$]*/.exec(route.slice(i))![0];
|
|
82
|
+
output += name;
|
|
83
|
+
i += name.length;
|
|
84
|
+
if (route.startsWith("::", i)) {
|
|
85
|
+
output += "::";
|
|
86
|
+
i += 2;
|
|
87
|
+
}
|
|
88
|
+
} else output += route[i++];
|
|
89
|
+
}
|
|
90
|
+
return output;
|
|
91
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ContextId, ContextIdFactory, NestContainer } from "@nestjs/core";
|
|
2
|
+
import { ExternalExceptionFilterContext } from "@nestjs/core/exceptions/external-exception-filter-context";
|
|
3
|
+
import { GuardsConsumer, GuardsContextCreator } from "@nestjs/core/guards";
|
|
4
|
+
import { ExternalContextCreator } from "@nestjs/core/helpers/external-context-creator";
|
|
5
|
+
import {
|
|
6
|
+
InterceptorsConsumer,
|
|
7
|
+
InterceptorsContextCreator,
|
|
8
|
+
} from "@nestjs/core/interceptors";
|
|
9
|
+
import { PipesConsumer, PipesContextCreator } from "@nestjs/core/pipes";
|
|
10
|
+
import { REQUEST_CONTEXT_ID } from "@nestjs/core/router/request/request-constants";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The creator of NestJS's enhancer pipeline (guards, interceptors, pipes, and
|
|
14
|
+
* exception filters) for the controllers of one module.
|
|
15
|
+
*
|
|
16
|
+
* `ExternalContextCreator` looks the module up among those providing the class,
|
|
17
|
+
* and a controller is provided by none, so its enhancers would not resolve: the
|
|
18
|
+
* module is named here instead.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export const create_external_context_creator = (
|
|
23
|
+
container: NestContainer,
|
|
24
|
+
moduleKey: string,
|
|
25
|
+
): ExternalContextCreator => new ControllerContextCreator(container, moduleKey);
|
|
26
|
+
|
|
27
|
+
class ControllerContextCreator extends ExternalContextCreator {
|
|
28
|
+
public constructor(
|
|
29
|
+
private readonly owner: NestContainer,
|
|
30
|
+
private readonly moduleKey: string,
|
|
31
|
+
) {
|
|
32
|
+
super(
|
|
33
|
+
new GuardsContextCreator(owner, owner.applicationConfig),
|
|
34
|
+
new GuardsConsumer(),
|
|
35
|
+
new InterceptorsContextCreator(owner, owner.applicationConfig),
|
|
36
|
+
new InterceptorsConsumer(),
|
|
37
|
+
owner.getModules(),
|
|
38
|
+
new PipesContextCreator(owner, owner.applicationConfig),
|
|
39
|
+
new PipesConsumer(),
|
|
40
|
+
new ExternalExceptionFilterContext(owner, owner.applicationConfig),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public override getContextModuleKey(): string {
|
|
45
|
+
return this.moduleKey;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public override registerRequestProvider<T = any>(
|
|
49
|
+
request: T,
|
|
50
|
+
contextId: ContextId,
|
|
51
|
+
): void {
|
|
52
|
+
this.owner.registerRequestProvider(request, contextId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The context a request-scoped provider is built in for this request, the one
|
|
58
|
+
* NestJS's router attaches to it, registering the request as `REQUEST`.
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export const get_request_context_id = (
|
|
63
|
+
container: NestContainer,
|
|
64
|
+
request: any,
|
|
65
|
+
durable: boolean,
|
|
66
|
+
): ContextId => {
|
|
67
|
+
const contextId: ContextId = ContextIdFactory.getByRequest(request);
|
|
68
|
+
if (!request[REQUEST_CONTEXT_ID]) {
|
|
69
|
+
Object.defineProperty(request, REQUEST_CONTEXT_ID, {
|
|
70
|
+
value: contextId,
|
|
71
|
+
enumerable: false,
|
|
72
|
+
writable: false,
|
|
73
|
+
configurable: false,
|
|
74
|
+
});
|
|
75
|
+
container.registerRequestProvider(
|
|
76
|
+
durable ? contextId.payload : Object.assign(request, contextId.payload),
|
|
77
|
+
contextId,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return contextId;
|
|
81
|
+
};
|
|
@@ -11,6 +11,11 @@ import { load_controllers } from "./internal/load_controller";
|
|
|
11
11
|
* controller classes dynamically just by specifying their directory path.
|
|
12
12
|
*
|
|
13
13
|
* @author Jeongho Nam - https://github.com/samchon
|
|
14
|
+
* @evidence contracts/common.md#principled-implementation The namespace builds a Nest module whose controllers are found on disk at startup instead of listed in code, using the shared loader.
|
|
15
|
+
* @evidence contracts/common.md#clear-and-simple-design One function that delegates discovery to `load_controllers` and decorates a class with the result.
|
|
16
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Paths are caller input, and no controller name is assumed.
|
|
17
|
+
* @evidence contracts/common.md#meaningful-documentation The comment describes the purpose and the path forms.
|
|
18
|
+
* @evidence contracts/portability.md#os-neutral-implementation Controller discovery delegates native path handling to SourceFinder and conversion of import paths to file URLs to load_controllers; module metadata contains controller constructors rather than filesystem identities.
|
|
14
19
|
*/
|
|
15
20
|
export namespace DynamicModule {
|
|
16
21
|
/**
|
|
@@ -23,7 +28,14 @@ export namespace DynamicModule {
|
|
|
23
28
|
*
|
|
24
29
|
* @param path Path of controllers
|
|
25
30
|
* @param metadata Additional metadata except controllers
|
|
31
|
+
* @param isTsNode Select TypeScript source files explicitly; otherwise
|
|
32
|
+
* compiled JavaScript is tried first, with a source fallback under `ttsx`.
|
|
26
33
|
* @returns Module instance
|
|
34
|
+
* @evidence contracts/common.md#principled-implementation Controllers are loaded from the paths, given as a path, a list, or include and exclude lists, and then a class decorated with `@Module` and those controllers is returned, so Nest treats the result as an ordinary module.
|
|
35
|
+
* @evidence contracts/common.md#clear-and-simple-design A short function whose discovery details live in `load_controllers`.
|
|
36
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It calls Nest's public `Module` decorator with the metadata; nothing is patched.
|
|
37
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents the path forms and the `isTsNode` option.
|
|
38
|
+
* @evidence contracts/portability.md#os-neutral-implementation Controller discovery delegates native path handling to SourceFinder and conversion of import paths to file URLs to load_controllers; module metadata contains controller constructors rather than filesystem identities.
|
|
27
39
|
*/
|
|
28
40
|
export async function mount(
|
|
29
41
|
path: string | string[] | { include: string[]; exclude?: string[] },
|
|
@@ -10,7 +10,7 @@ import type { FastifyRequest } from "fastify";
|
|
|
10
10
|
|
|
11
11
|
import { IRequestBodyValidator } from "../options/IRequestBodyValidator";
|
|
12
12
|
import { Singleton } from "../utils/Singleton";
|
|
13
|
-
import {
|
|
13
|
+
import { get_encryption_password } from "./internal/get_encryption_password";
|
|
14
14
|
import { get_text_body } from "./internal/get_text_body";
|
|
15
15
|
import { headers_to_object } from "./internal/headers_to_object";
|
|
16
16
|
import { is_media_type } from "./internal/is_media_type";
|
|
@@ -20,22 +20,30 @@ import { validate_request_body } from "./internal/validate_request_body";
|
|
|
20
20
|
* Encrypted body decorator.
|
|
21
21
|
*
|
|
22
22
|
* `EncryptedBody` is a decorator function getting `application/json` typed data
|
|
23
|
-
* from request body which has been encrypted by AES-128/256 algorithm.
|
|
24
|
-
* `EncryptedBody` validates the request body data type through
|
|
25
|
-
* [typia](https://github.com/samchon/typia)
|
|
26
|
-
*
|
|
23
|
+
* from request body which has been encrypted by AES-128/192/256 algorithm.
|
|
24
|
+
* Also, `EncryptedBody` validates the request body data type through
|
|
25
|
+
* [typia](https://github.com/samchon/typia) using the declared TypeScript
|
|
26
|
+
* type.
|
|
27
27
|
*
|
|
28
28
|
* For reference, when the request body data is not following the promised type
|
|
29
29
|
* `T`, `BadRequestException` error (status code: 400) would be thrown. Also,
|
|
30
|
-
*
|
|
30
|
+
* This decorator decrypts the request body using these options.
|
|
31
31
|
*
|
|
32
|
-
* - AES-128/256
|
|
32
|
+
* - AES-128/192/256
|
|
33
33
|
* - CBC mode
|
|
34
34
|
* - PKCS #5 Padding
|
|
35
35
|
* - Base64 Encoding
|
|
36
36
|
*
|
|
37
|
+
* Successful validator data becomes the decorated argument, including copies
|
|
38
|
+
* returned by clone validators after decryption and JSON parsing.
|
|
39
|
+
*
|
|
37
40
|
* @author Jeongho Nam - https://github.com/samchon
|
|
38
41
|
* @returns Parameter decorator
|
|
42
|
+
* @evidence contracts/common.md#principled-implementation The body must be `text/plain` ciphertext; it is decrypted with the password of the controller or module, the plain text is parsed as JSON and validated by the transformed validator, and a decryption or parse failure becomes a 400 with one fixed message that does not echo the ciphertext and does not tell the two failures apart; a key or initialization vector the cipher itself refuses is the server's configuration, independent of the request, and is thrown as it is.
|
|
43
|
+
* @evidence contracts/common.md#clear-and-simple-design One parameter decorator composes shared text reading, password lookup and validator resolution; the successfully resolved data becomes the argument without repeating validation or changing decryption error handling.
|
|
44
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The algorithm comes from `AesPkcs5` of the fetcher, the same one the client uses, and no key is embedded; a missing password is an error rather than a plain pass-through.
|
|
45
|
+
* @evidence contracts/common.md#meaningful-documentation The comment describes the protocol and the requirement of a password on the controller or module, and the private decode helper documents why every decryption failure collapses into one response.
|
|
46
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation Ciphertext parsing, AES decryption and request validation operate on HTTP values, with no native filesystem or subprocess boundary.
|
|
39
47
|
*/
|
|
40
48
|
export function EncryptedBody<T>(
|
|
41
49
|
validator?: IRequestBodyValidator<T>,
|
|
@@ -52,7 +60,7 @@ export function EncryptedBody<T>(
|
|
|
52
60
|
throw new BadRequestException(`Request body type is not "text/plain".`);
|
|
53
61
|
|
|
54
62
|
const param: IEncryptionPassword | IEncryptionPassword.Closure | undefined =
|
|
55
|
-
|
|
63
|
+
get_encryption_password(context.getClass());
|
|
56
64
|
if (!param)
|
|
57
65
|
throw new Error(
|
|
58
66
|
"Error on nestia.core.EncryptedBody(): no encryption password is given.",
|
|
@@ -70,9 +78,9 @@ export function EncryptedBody<T>(
|
|
|
70
78
|
|
|
71
79
|
// PARSE AND VALIDATE DATA
|
|
72
80
|
const data: any = decode(body, password.key, password.iv);
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
return data;
|
|
81
|
+
const result = checker.resolve(data);
|
|
82
|
+
if (!result.success) throw result.error;
|
|
83
|
+
return result.data;
|
|
76
84
|
})();
|
|
77
85
|
}
|
|
78
86
|
|
|
@@ -89,9 +97,13 @@ export function EncryptedBody<T>(
|
|
|
89
97
|
*
|
|
90
98
|
* Both failure modes must therefore collapse into one indistinguishable
|
|
91
99
|
* `BadRequestException` (identical status, message, and body). Only the
|
|
92
|
-
* downstream type validation of an already-parsed body
|
|
93
|
-
*
|
|
94
|
-
*
|
|
100
|
+
* downstream type validation of an already-parsed body reports its own distinct
|
|
101
|
+
* error. AES-CBC provides no authentication; collapsing parse and padding
|
|
102
|
+
* errors does not establish ciphertext authenticity.
|
|
103
|
+
*
|
|
104
|
+
* A key or initialization vector the cipher refuses is thrown as it is. It says
|
|
105
|
+
* nothing about the body, so it opens no oracle, and it is a defect of the
|
|
106
|
+
* server's configuration that a 400 would blame on the client.
|
|
95
107
|
*
|
|
96
108
|
* @internal
|
|
97
109
|
*/
|
|
@@ -99,10 +111,22 @@ const decode = (body: string, key: string, iv: string): unknown => {
|
|
|
99
111
|
try {
|
|
100
112
|
return JSON.parse(AesPkcs5.decrypt(body, key, iv));
|
|
101
113
|
} catch (exp) {
|
|
102
|
-
|
|
114
|
+
// A password the cipher itself refuses is the server's configuration, which
|
|
115
|
+
// no request body can influence, so it is no oracle and no client's fault.
|
|
116
|
+
if (exp instanceof Error && PASSWORD_ERRORS.has((exp as any).code))
|
|
117
|
+
throw exp;
|
|
118
|
+
else if (exp instanceof Error)
|
|
103
119
|
throw new BadRequestException(
|
|
104
120
|
"Failed to decrypt the request body. Check your body content or encryption password.",
|
|
105
121
|
);
|
|
106
122
|
else throw exp;
|
|
107
123
|
}
|
|
108
124
|
};
|
|
125
|
+
|
|
126
|
+
/** The error codes of a key or an initialization vector the cipher refuses. */
|
|
127
|
+
const PASSWORD_ERRORS: Set<unknown> = new Set([
|
|
128
|
+
"ERR_INVALID_ARG_TYPE",
|
|
129
|
+
"ERR_CRYPTO_INVALID_IV",
|
|
130
|
+
"ERR_CRYPTO_INVALID_KEYLEN",
|
|
131
|
+
"ERR_CRYPTO_UNKNOWN_CIPHER",
|
|
132
|
+
]);
|