@nestia/core 14.0.1 → 14.0.3
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/lib/adaptors/McpAdaptor.d.ts +24 -3
- package/lib/adaptors/McpAdaptor.js +72 -34
- package/lib/adaptors/McpAdaptor.js.map +1 -1
- package/lib/adaptors/WebSocketAdaptor.d.ts +37 -0
- package/lib/adaptors/WebSocketAdaptor.js +60 -16
- package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
- package/lib/adaptors/internal/external_context.js +18 -5
- package/lib/adaptors/internal/external_context.js.map +1 -1
- package/lib/decorators/DynamicModule.d.ts +13 -1
- 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 +37 -14
- package/lib/decorators/EncryptedBody.js.map +1 -1
- package/lib/decorators/EncryptedController.d.ts +6 -1
- package/lib/decorators/EncryptedController.js +6 -1
- package/lib/decorators/EncryptedController.js.map +1 -1
- package/lib/decorators/EncryptedModule.d.ts +18 -3
- package/lib/decorators/EncryptedModule.js +18 -3
- package/lib/decorators/EncryptedModule.js.map +1 -1
- package/lib/decorators/EncryptedRoute.d.ts +20 -11
- package/lib/decorators/EncryptedRoute.js +20 -11
- 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 +9 -2
- package/lib/decorators/TypedBody.js +19 -9
- package/lib/decorators/TypedBody.js.map +1 -1
- package/lib/decorators/TypedException.d.ts +10 -0
- package/lib/decorators/TypedFormData.d.ts +59 -1
- package/lib/decorators/TypedFormData.js +5 -0
- 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 +9 -0
- package/lib/decorators/TypedParam.js +9 -0
- package/lib/decorators/TypedParam.js.map +1 -1
- package/lib/decorators/TypedQuery.d.ts +16 -0
- package/lib/decorators/TypedQuery.js +16 -0
- 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/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_path_and_querify.js +3 -7
- 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/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/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 +120 -0
- 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 +40 -0
- package/lib/utils/ExceptionManager.js +34 -0
- package/lib/utils/ExceptionManager.js.map +1 -1
- package/lib/utils/SourceFinder.d.ts +22 -0
- package/lib/utils/SourceFinder.js +34 -7
- package/lib/utils/SourceFinder.js.map +1 -1
- package/lib/utils/VersioningStrategy.d.ts +37 -2
- package/lib/utils/VersioningStrategy.js +30 -9
- 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 +19 -1
- 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 +37 -3
- package/native/transform/core_transform.go +105 -98
- package/native/transform/core_websocket.go +21 -5
- package/native/transform/node_transform.go +11 -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 +21 -0
- package/package.json +9 -7
- package/src/adaptors/McpAdaptor.ts +86 -26
- package/src/adaptors/WebSocketAdaptor.ts +70 -15
- package/src/adaptors/internal/external_context.ts +43 -6
- package/src/decorators/DynamicModule.ts +13 -1
- package/src/decorators/EncryptedBody.ts +37 -13
- package/src/decorators/EncryptedController.ts +6 -1
- package/src/decorators/EncryptedModule.ts +18 -3
- package/src/decorators/EncryptedRoute.ts +21 -12
- 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 +17 -8
- package/src/decorators/TypedException.ts +10 -0
- package/src/decorators/TypedFormData.ts +60 -2
- package/src/decorators/TypedHeaders.ts +5 -0
- package/src/decorators/TypedParam.ts +9 -0
- package/src/decorators/TypedQuery.ts +17 -1
- package/src/decorators/TypedRoute.ts +26 -11
- package/src/decorators/WebSocketRoute.ts +18 -1
- package/src/decorators/doNotThrowTransformError.ts +14 -0
- 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_path_and_querify.ts +3 -9
- package/src/decorators/internal/get_path_and_stringify.ts +3 -9
- 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/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 +120 -0
- 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 +40 -0
- package/src/utils/SourceFinder.ts +38 -7
- package/src/utils/VersioningStrategy.ts +47 -14
|
@@ -11,6 +11,7 @@ import { InstanceWrapper } from "@nestjs/core/injector/instance-wrapper";
|
|
|
11
11
|
import { Module } from "@nestjs/core/injector/module";
|
|
12
12
|
|
|
13
13
|
import { IMcpRouteReflect } from "../decorators/internal/IMcpRouteReflect";
|
|
14
|
+
import { resolve_request_body } from "../decorators/internal/validate_request_body";
|
|
14
15
|
import {
|
|
15
16
|
create_external_context_creator,
|
|
16
17
|
get_request_context_id,
|
|
@@ -47,14 +48,12 @@ import {
|
|
|
47
48
|
*
|
|
48
49
|
* @author wildduck - https://github.com/wildduck2
|
|
49
50
|
* @example
|
|
50
|
-
* ```typescript
|
|
51
51
|
* import core from "@nestia/core";
|
|
52
52
|
* import { NestFactory } from "@nestjs/core";
|
|
53
53
|
*
|
|
54
54
|
* const app = await NestFactory.create(AppModule);
|
|
55
55
|
* await core.McpAdaptor.upgrade(app, { path: "/mcp" });
|
|
56
56
|
* await app.listen(3000);
|
|
57
|
-
* ```;
|
|
58
57
|
*/
|
|
59
58
|
export class McpAdaptor {
|
|
60
59
|
/**
|
|
@@ -71,6 +70,11 @@ export class McpAdaptor {
|
|
|
71
70
|
*
|
|
72
71
|
* @param app Running Nest application instance.
|
|
73
72
|
* @param options Transport and identity overrides.
|
|
73
|
+
* @evidence contracts/common.md#principled-implementation Tools are collected once from the prototype chain of every controller instance by the `nestia/McpRoute` metadata, with the first definition of a property name winning and accessors skipped without execution, duplicates by tool name refused before serving, and each call runs through NestJS's `ExternalContextCreator` so guards, interceptors, pipes, and exception filters apply as for an HTTP route; every request gets a fresh MCP server and Streamable HTTP transport, which stateless mode requires, and both are closed in a `finally`.
|
|
74
|
+
* @evidence contracts/common.md#clear-and-simple-design One entry point does discovery and registration; the per-tool call (`createHandler`), the duplicate check, the SDK loader, and the per-request response facade are separate module-private functions.
|
|
75
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Reading the module graph goes through the internal `app.container`, because `INestApplication` exposes no public accessor for it, `create_external_context_creator` constructs an owned subclass through the Nest constructor and overrides module selection on that type, because the default looks for provider classes while controllers belong to the controller collection, and `get_request_context_id` registers the request provider on the request object as NestJS's router does for an HTTP route, and a per-request response facade forwards native state, events and method receivers without replacing raw methods. After a tool or its exception filter has sent headers, only the facade suppresses subsequent transport writes and preserves callbacks and chaining. Native transport writes do not themselves activate takeover. The remaining foreign-internal reads are the application container and Nest request context registration; the MCP SDK uses its public Node response injection boundary.
|
|
76
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the stateless mode, what is discovered, and that the SDK is an optional dependency; the options documentation states the path and server information.
|
|
77
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation MCP tool discovery and HTTP transport callbacks carry protocol values; optional SDK imports use Node module resolution and no native file identity or child process is defined here.
|
|
74
78
|
*/
|
|
75
79
|
public static async upgrade(
|
|
76
80
|
app: INestApplication,
|
|
@@ -101,7 +105,8 @@ export class McpAdaptor {
|
|
|
101
105
|
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
102
106
|
if (key === "constructor" || visited.has(key)) continue;
|
|
103
107
|
visited.add(key);
|
|
104
|
-
|
|
108
|
+
// Route discovery inspects data descriptors without invoking accessors.
|
|
109
|
+
const method = Object.getOwnPropertyDescriptor(proto, key)?.value;
|
|
105
110
|
if (typeof method !== "function") continue;
|
|
106
111
|
|
|
107
112
|
const meta: IMcpRouteReflect | undefined = Reflect.getMetadata(
|
|
@@ -154,6 +159,7 @@ export class McpAdaptor {
|
|
|
154
159
|
capabilities: { tools: {} },
|
|
155
160
|
});
|
|
156
161
|
const server = mcp.server;
|
|
162
|
+
const relay = transportResponse(res.raw ?? res);
|
|
157
163
|
|
|
158
164
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
159
165
|
tools: tools.map((t) => ({
|
|
@@ -181,7 +187,7 @@ export class McpAdaptor {
|
|
|
181
187
|
response: res,
|
|
182
188
|
args,
|
|
183
189
|
});
|
|
184
|
-
if (
|
|
190
|
+
if (relay.takeOver()) return { content: [] };
|
|
185
191
|
if (result instanceof Error) throw result;
|
|
186
192
|
if (result === undefined) return { content: [] };
|
|
187
193
|
return {
|
|
@@ -194,7 +200,7 @@ export class McpAdaptor {
|
|
|
194
200
|
],
|
|
195
201
|
};
|
|
196
202
|
} catch (e) {
|
|
197
|
-
if (
|
|
203
|
+
if (relay.takeOver()) return { content: [] };
|
|
198
204
|
if (INVALID_ARGUMENTS.has(e as object)) {
|
|
199
205
|
const body = (e as BadRequestException).getResponse() as any;
|
|
200
206
|
throw new McpError(ErrorCode.InvalidParams, (e as Error).message, {
|
|
@@ -224,7 +230,7 @@ export class McpAdaptor {
|
|
|
224
230
|
});
|
|
225
231
|
try {
|
|
226
232
|
await mcp.connect(transport);
|
|
227
|
-
await transport.handleRequest(req.raw ?? req,
|
|
233
|
+
await transport.handleRequest(req.raw ?? req, relay.response, req.body);
|
|
228
234
|
} finally {
|
|
229
235
|
await transport.close().catch(() => {});
|
|
230
236
|
await mcp.close().catch(() => {});
|
|
@@ -242,7 +248,9 @@ export class McpAdaptor {
|
|
|
242
248
|
* The arguments reach the method through the pipes as its body would, validated
|
|
243
249
|
* by typia at that stage: after the guards, as `@TypedBody()` is. An invalid
|
|
244
250
|
* argument is thrown as the validator's `BadRequestException`, which an
|
|
245
|
-
* exception filter may map; unmapped, it becomes JSON-RPC `-32602`.
|
|
251
|
+
* exception filter may map; unmapped, it becomes JSON-RPC `-32602`. Successful
|
|
252
|
+
* resolver data becomes the argument before pipes execute. Legacy error-only
|
|
253
|
+
* metadata callbacks retain raw input on null success.
|
|
246
254
|
*
|
|
247
255
|
* A controller that is request-scoped, itself or through an enhancer or a
|
|
248
256
|
* dependency, is built per request with its enhancers, as NestJS builds it for
|
|
@@ -275,10 +283,11 @@ const createHandler = (props: {
|
|
|
275
283
|
{
|
|
276
284
|
// [request, response, next] as an HTTP route has, then the arguments
|
|
277
285
|
exchangeKeyForValue: (_type, _data, [, , , args]) => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
286
|
+
if (!validate) return args;
|
|
287
|
+
const result = resolve_request_body(validate, args);
|
|
288
|
+
if (result.success) return result.data;
|
|
289
|
+
INVALID_ARGUMENTS.add(result.error);
|
|
290
|
+
throw result.error;
|
|
282
291
|
},
|
|
283
292
|
},
|
|
284
293
|
contextId,
|
|
@@ -307,22 +316,55 @@ const createHandler = (props: {
|
|
|
307
316
|
};
|
|
308
317
|
|
|
309
318
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
319
|
+
* The transport's response view. Native state, events and method receivers stay
|
|
320
|
+
* on the real response; only this view's writes stop after a tool's exception
|
|
321
|
+
* filter has already answered. Normal transport writes never activate
|
|
322
|
+
* takeover.
|
|
313
323
|
*/
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
const transportResponse = (
|
|
325
|
+
raw: any,
|
|
326
|
+
): { response: any; takeOver: () => boolean } => {
|
|
327
|
+
let takenOver = false;
|
|
328
|
+
const respond = (key: string, args: unknown[]): unknown => {
|
|
329
|
+
if (takenOver) {
|
|
330
|
+
const callback = args.find((arg) => typeof arg === "function");
|
|
331
|
+
if ((key === "write" || key === "end") && typeof callback === "function")
|
|
332
|
+
queueMicrotask(() => callback());
|
|
333
|
+
return key === "write"
|
|
334
|
+
? true
|
|
335
|
+
: key === "flushHeaders"
|
|
336
|
+
? undefined
|
|
337
|
+
: response;
|
|
338
|
+
}
|
|
339
|
+
const result = Reflect.apply(raw[key], raw, args);
|
|
340
|
+
return result === raw ? response : result;
|
|
341
|
+
};
|
|
342
|
+
const writes = new Map<PropertyKey, (...args: unknown[]) => unknown>(
|
|
343
|
+
["writeHead", "flushHeaders", "write", "end"].map((key) => [
|
|
344
|
+
key,
|
|
345
|
+
(...args: unknown[]) => respond(key, args),
|
|
346
|
+
]),
|
|
347
|
+
);
|
|
348
|
+
const response: any = new Proxy(raw, {
|
|
349
|
+
get: (target, key) => {
|
|
350
|
+
const write = writes.get(key);
|
|
351
|
+
if (write !== undefined) return write;
|
|
352
|
+
const value = Reflect.get(target, key, target);
|
|
353
|
+
if (typeof value !== "function") return value;
|
|
354
|
+
return (...args: unknown[]) => {
|
|
355
|
+
const result = Reflect.apply(value, target, args);
|
|
356
|
+
return result === target ? response : result;
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
set: (target, key, value) => Reflect.set(target, key, value, target),
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
response,
|
|
363
|
+
takeOver: () => {
|
|
364
|
+
takenOver ||= raw.headersSent === true;
|
|
365
|
+
return takenOver;
|
|
366
|
+
},
|
|
324
367
|
};
|
|
325
|
-
return true;
|
|
326
368
|
};
|
|
327
369
|
|
|
328
370
|
const PARAMS_METADATA = "nestia/McpRoute/ExternalParameters";
|
|
@@ -374,8 +416,26 @@ const loadMcpSdk = async () => {
|
|
|
374
416
|
}
|
|
375
417
|
};
|
|
376
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Types of {@link McpAdaptor}: the options of `upgrade` and the record of a
|
|
421
|
+
* registered tool.
|
|
422
|
+
*
|
|
423
|
+
* @evidence contracts/common.md#principled-implementation The class exposes one static entry point, so the adapter has no instance state: the tool list is closed over by the registered route handler.
|
|
424
|
+
* @evidence contracts/common.md#clear-and-simple-design One static method and two nested types.
|
|
425
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It has no fixture or tool name, and the MCP SDK is loaded lazily so an application that never enables MCP does not need it.
|
|
426
|
+
* @evidence contracts/common.md#meaningful-documentation The class comment explains what an MCP route is and how to enable it.
|
|
427
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation MCP tool discovery and HTTP transport callbacks carry protocol values; optional SDK imports use Node module resolution and no native file identity or child process is defined here.
|
|
428
|
+
*/
|
|
377
429
|
export namespace McpAdaptor {
|
|
378
|
-
/**
|
|
430
|
+
/**
|
|
431
|
+
* Configuration options for {@link McpAdaptor.upgrade}.
|
|
432
|
+
*
|
|
433
|
+
* @evidence contracts/common.md#principled-implementation The two options are the route path, with `/mcp` as the default, and the server information reported to clients, with a default name and version.
|
|
434
|
+
* @evidence contracts/common.md#clear-and-simple-design A flat record of two optional values.
|
|
435
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type; the defaults are stated in the code that reads them.
|
|
436
|
+
* @evidence contracts/common.md#meaningful-documentation Each option documents its meaning.
|
|
437
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation MCP tool discovery and HTTP transport callbacks carry protocol values; optional SDK imports use Node module resolution and no native file identity or child process is defined here.
|
|
438
|
+
*/
|
|
379
439
|
export interface IOptions {
|
|
380
440
|
/**
|
|
381
441
|
* HTTP path where the MCP endpoint will be mounted.
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
SCOPE_OPTIONS_METADATA,
|
|
12
12
|
VERSION_METADATA,
|
|
13
13
|
} from "@nestjs/common/constants";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
VERSION_NEUTRAL,
|
|
16
|
+
VersionValue,
|
|
17
|
+
} from "@nestjs/common/interfaces/index.js";
|
|
15
18
|
import { NestContainer } from "@nestjs/core";
|
|
16
19
|
import { InstanceWrapper } from "@nestjs/core/injector/instance-wrapper";
|
|
17
20
|
import { Module } from "@nestjs/core/injector/module";
|
|
@@ -21,20 +24,59 @@ import path from "path";
|
|
|
21
24
|
import { Duplex } from "stream";
|
|
22
25
|
import { WebSocketAcceptor } from "tgrid";
|
|
23
26
|
import typia from "typia";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
24
28
|
import WebSocket from "ws";
|
|
25
29
|
|
|
26
30
|
import { IWebSocketRouteReflect } from "../decorators/internal/IWebSocketRouteReflect";
|
|
31
|
+
import { resolve_request_body } from "../decorators/internal/validate_request_body";
|
|
27
32
|
import { ArrayUtil } from "../utils/ArrayUtil";
|
|
28
33
|
import { VersioningStrategy } from "../utils/VersioningStrategy";
|
|
29
34
|
import { RoutePathMatcher } from "./internal/RoutePathMatcher";
|
|
30
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Serves the `@WebSocketRoute()` methods of a NestJS application over
|
|
38
|
+
* WebSocket.
|
|
39
|
+
*
|
|
40
|
+
* Call {@link upgrade} after the application has been created. It finds the
|
|
41
|
+
* routes, checks them, and takes over the HTTP server's `upgrade` events. A
|
|
42
|
+
* handshake whose path matches no route is rejected with close code 1002.
|
|
43
|
+
*
|
|
44
|
+
* @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.
|
|
45
|
+
* @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.
|
|
46
|
+
* @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.
|
|
47
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states when to call it and what happens to an unmatched handshake.
|
|
48
|
+
* @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.
|
|
49
|
+
*/
|
|
31
50
|
export class WebSocketAdaptor {
|
|
51
|
+
/**
|
|
52
|
+
* Creates the adapter for the application: it visits every controller,
|
|
53
|
+
* validates the WebSocket routes, and starts listening for upgrade events.
|
|
54
|
+
*
|
|
55
|
+
* All route errors of the application are collected and thrown together, with
|
|
56
|
+
* the controller, method, source location, and reasons of each.
|
|
57
|
+
*
|
|
58
|
+
* @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.
|
|
59
|
+
* @evidence contracts/common.md#clear-and-simple-design One asynchronous factory that awaits the visitor and hands the operator list to the private constructor.
|
|
60
|
+
* @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.
|
|
61
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the collected error report and the effect on the server.
|
|
62
|
+
* @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.
|
|
63
|
+
*/
|
|
32
64
|
public static async upgrade(
|
|
33
65
|
app: INestApplication,
|
|
34
66
|
): Promise<WebSocketAdaptor> {
|
|
35
67
|
return new this(app, await visitApplication(app));
|
|
36
68
|
}
|
|
37
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Stops the adapter: removes its listeners from the HTTP server and closes
|
|
72
|
+
* the WebSocket server.
|
|
73
|
+
*
|
|
74
|
+
* @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.
|
|
75
|
+
* @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.
|
|
76
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It releases exactly what the adapter registered.
|
|
77
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states what is released.
|
|
78
|
+
* @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.
|
|
79
|
+
*/
|
|
38
80
|
public readonly close = async (): Promise<void> =>
|
|
39
81
|
new Promise((resolve) => {
|
|
40
82
|
this.http.off("close", this.close);
|
|
@@ -207,8 +249,7 @@ const visitController = async (props: {
|
|
|
207
249
|
modulePrefix: props.modulePrefix,
|
|
208
250
|
};
|
|
209
251
|
for (const mk of getOwnPropertyNames(controller.prototype).filter(
|
|
210
|
-
(key) =>
|
|
211
|
-
key !== "constructor" && typeof controller.prototype[key] === "function",
|
|
252
|
+
(key) => key !== "constructor",
|
|
212
253
|
)) {
|
|
213
254
|
const errorMessages: string[] = [];
|
|
214
255
|
visitMethod({
|
|
@@ -229,7 +270,9 @@ const visitController = async (props: {
|
|
|
229
270
|
...file,
|
|
230
271
|
source: path.relative(
|
|
231
272
|
process.cwd(),
|
|
232
|
-
file.source.
|
|
273
|
+
file.source.startsWith("file:")
|
|
274
|
+
? fileURLToPath(file.source)
|
|
275
|
+
: file.source,
|
|
233
276
|
),
|
|
234
277
|
});
|
|
235
278
|
}
|
|
@@ -342,9 +385,12 @@ const visitMethod = (props: {
|
|
|
342
385
|
else if (p.category === "driver")
|
|
343
386
|
args.push(input.acceptor.getDriver());
|
|
344
387
|
else if (p.category === "header") {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
388
|
+
const result = resolve_request_body(
|
|
389
|
+
p.validate,
|
|
390
|
+
input.acceptor.header,
|
|
391
|
+
);
|
|
392
|
+
if (!result.success) throw result.error;
|
|
393
|
+
args.push(result.data);
|
|
348
394
|
} else if (p.category === "param")
|
|
349
395
|
args.push(p.assert(input.params[p.field]!));
|
|
350
396
|
else if (p.category === "query") {
|
|
@@ -475,14 +521,23 @@ const MAX_CLOSE_REASON_BYTES: number = 123;
|
|
|
475
521
|
|
|
476
522
|
const wrapPaths = (value: string[]) => (value.length === 0 ? [""] : value);
|
|
477
523
|
const getOwnPropertyNames = (prototype: any): string[] => {
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
524
|
+
const visited: Set<string> = new Set();
|
|
525
|
+
const result: string[] = [];
|
|
526
|
+
for (
|
|
527
|
+
let current = prototype;
|
|
528
|
+
current !== null;
|
|
529
|
+
current = Object.getPrototypeOf(current)
|
|
530
|
+
)
|
|
531
|
+
for (const key of Object.getOwnPropertyNames(current)) {
|
|
532
|
+
if (visited.has(key)) continue;
|
|
533
|
+
visited.add(key);
|
|
534
|
+
if (
|
|
535
|
+
typeof Object.getOwnPropertyDescriptor(current, key)?.value ===
|
|
536
|
+
"function"
|
|
537
|
+
)
|
|
538
|
+
result.push(key);
|
|
539
|
+
}
|
|
540
|
+
return result;
|
|
486
541
|
};
|
|
487
542
|
|
|
488
543
|
interface Entry<T> {
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { ContextId, ContextIdFactory, NestContainer } from "@nestjs/core";
|
|
2
|
+
import { ExternalExceptionFilterContext } from "@nestjs/core/exceptions/external-exception-filter-context";
|
|
3
|
+
import {
|
|
4
|
+
GuardsConsumer,
|
|
5
|
+
GuardsContextCreator,
|
|
6
|
+
} from "@nestjs/core/guards/index.js";
|
|
2
7
|
import { ExternalContextCreator } from "@nestjs/core/helpers/external-context-creator";
|
|
8
|
+
import {
|
|
9
|
+
InterceptorsConsumer,
|
|
10
|
+
InterceptorsContextCreator,
|
|
11
|
+
} from "@nestjs/core/interceptors/index.js";
|
|
12
|
+
import {
|
|
13
|
+
PipesConsumer,
|
|
14
|
+
PipesContextCreator,
|
|
15
|
+
} from "@nestjs/core/pipes/index.js";
|
|
3
16
|
import { REQUEST_CONTEXT_ID } from "@nestjs/core/router/request/request-constants";
|
|
4
17
|
|
|
5
18
|
/**
|
|
@@ -15,12 +28,36 @@ import { REQUEST_CONTEXT_ID } from "@nestjs/core/router/request/request-constant
|
|
|
15
28
|
export const create_external_context_creator = (
|
|
16
29
|
container: NestContainer,
|
|
17
30
|
moduleKey: string,
|
|
18
|
-
): ExternalContextCreator =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
): ExternalContextCreator => new ControllerContextCreator(container, moduleKey);
|
|
32
|
+
|
|
33
|
+
class ControllerContextCreator extends ExternalContextCreator {
|
|
34
|
+
public constructor(
|
|
35
|
+
private readonly owner: NestContainer,
|
|
36
|
+
private readonly moduleKey: string,
|
|
37
|
+
) {
|
|
38
|
+
super(
|
|
39
|
+
new GuardsContextCreator(owner, owner.applicationConfig),
|
|
40
|
+
new GuardsConsumer(),
|
|
41
|
+
new InterceptorsContextCreator(owner, owner.applicationConfig),
|
|
42
|
+
new InterceptorsConsumer(),
|
|
43
|
+
owner.getModules(),
|
|
44
|
+
new PipesContextCreator(owner, owner.applicationConfig),
|
|
45
|
+
new PipesConsumer(),
|
|
46
|
+
new ExternalExceptionFilterContext(owner, owner.applicationConfig),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public override getContextModuleKey(): string {
|
|
51
|
+
return this.moduleKey;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public override registerRequestProvider<T = any>(
|
|
55
|
+
request: T,
|
|
56
|
+
contextId: ContextId,
|
|
57
|
+
): void {
|
|
58
|
+
this.owner.registerRequestProvider(request, contextId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
24
61
|
|
|
25
62
|
/**
|
|
26
63
|
* The context a request-scoped provider is built in for this request, the one
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Module } from "@nestjs/common";
|
|
2
|
-
import { ModuleMetadata } from "@nestjs/common/interfaces";
|
|
2
|
+
import { ModuleMetadata } from "@nestjs/common/interfaces/index.js";
|
|
3
3
|
|
|
4
4
|
import { Creator } from "../typings/Creator";
|
|
5
5
|
import { load_controllers } from "./internal/load_controller";
|
|
@@ -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[] },
|
|
@@ -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>,
|
|
@@ -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
|
+
]);
|
|
@@ -7,7 +7,7 @@ import { ENCRYPTION_CONTROLLER_METADATA_KEY } from "./internal/EncryptedConstant
|
|
|
7
7
|
* Encrypted controller.
|
|
8
8
|
*
|
|
9
9
|
* `EncryptedController` is an extension of the {@link nest.Controller} class
|
|
10
|
-
* decorator function who configures encryption password of the AES-128/256
|
|
10
|
+
* decorator function who configures encryption password of the AES-128/192/256
|
|
11
11
|
* algorithm. The encryption algorithm and password would be used by
|
|
12
12
|
* {@link EncryptedRoute} and {@link EncryptedBody} to encrypt the request and
|
|
13
13
|
* response body of the HTTP protocol.
|
|
@@ -28,6 +28,11 @@ import { ENCRYPTION_CONTROLLER_METADATA_KEY } from "./internal/EncryptedConstant
|
|
|
28
28
|
* @param path Path of the HTTP request
|
|
29
29
|
* @param password Encryption password or its getter function
|
|
30
30
|
* @returns Class decorator
|
|
31
|
+
* @evidence contracts/common.md#principled-implementation The decorator stores the password as metadata on the class and then applies Nest's `Controller(path)`, so the encrypted decorators can find the password by reading the class.
|
|
32
|
+
* @evidence contracts/common.md#clear-and-simple-design One decorator that composes a metadata write with the standard decorator.
|
|
33
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It uses Nest's public `Controller` decorator and the reflect metadata API.
|
|
34
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the password forms and the encrypted decorators it enables.
|
|
35
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation Controller paths are HTTP routing metadata and passwords are in-memory values; neither is a native filesystem path.
|
|
31
36
|
*/
|
|
32
37
|
export function EncryptedController(
|
|
33
38
|
path: string,
|
|
@@ -9,9 +9,9 @@ import { load_controllers } from "./internal/load_controller";
|
|
|
9
9
|
* Encrypted module.
|
|
10
10
|
*
|
|
11
11
|
* `EncryptedModule` is an extension of the {@link Module} class decorator
|
|
12
|
-
* function who configures encryption password of the AES-128/256 algorithm.
|
|
13
|
-
* encryption algorithm and password would be used by {@link EncryptedRoute}
|
|
14
|
-
* {@link EncryptedBody} to encrypt the request and response bod of the HTTP
|
|
12
|
+
* function who configures encryption password of the AES-128/192/256 algorithm.
|
|
13
|
+
* The encryption algorithm and password would be used by {@link EncryptedRoute}
|
|
14
|
+
* and {@link EncryptedBody} to encrypt the request and response bod of the HTTP
|
|
15
15
|
* protocol.
|
|
16
16
|
*
|
|
17
17
|
* By using this `EncryptedModule` decorator function, all of the
|
|
@@ -38,6 +38,11 @@ import { load_controllers } from "./internal/load_controller";
|
|
|
38
38
|
* @param metadata Module configuration metadata
|
|
39
39
|
* @param password Encryption password or its getter function
|
|
40
40
|
* @returns Class decorator
|
|
41
|
+
* @evidence contracts/common.md#principled-implementation A visited set breaks import cycles, the controllers of each visited module get the password as metadata, asynchronous modules are awaited through the same promise Nest awaits, and a forward reference is resolved once its class exists.
|
|
42
|
+
* @evidence contracts/common.md#clear-and-simple-design One decorator whose traversal is a private function; the dynamic-module helper is in the merged namespace.
|
|
43
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The traversal uses the public metadata keys of Nest modules; no module name is special-cased.
|
|
44
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states which module shapes are followed.
|
|
45
|
+
* @evidence contracts/portability.md#os-neutral-implementation The decorator traverses Nest module metadata in memory; its dynamic helper delegates native paths and file-URL imports to load_controllers instead of composing platform-specific import strings.
|
|
41
46
|
*/
|
|
42
47
|
export function EncryptedModule(
|
|
43
48
|
metadata: Parameters<typeof Module>[0],
|
|
@@ -49,6 +54,16 @@ export function EncryptedModule(
|
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Helpers of {@link EncryptedModule}, namely `dynamic`, which builds an
|
|
59
|
+
* encrypted module from controller paths.
|
|
60
|
+
*
|
|
61
|
+
* @evidence contracts/common.md#principled-implementation `dynamic` loads the controllers with the shared loader, wraps an object password into a closure, and returns a class decorated by `EncryptedModule`.
|
|
62
|
+
* @evidence contracts/common.md#clear-and-simple-design One function that reuses the loader and the decorator.
|
|
63
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Paths and password are caller input.
|
|
64
|
+
* @evidence contracts/common.md#meaningful-documentation The comment documents the path forms and the password argument.
|
|
65
|
+
* @evidence contracts/portability.md#os-neutral-implementation The decorator traverses Nest module metadata in memory; its dynamic helper delegates native paths and file-URL imports to load_controllers instead of composing platform-specific import strings.
|
|
66
|
+
*/
|
|
52
67
|
export namespace EncryptedModule {
|
|
53
68
|
/**
|
|
54
69
|
* Dynamic encrypted module.
|