@silkweave/nestjs 2.0.0 → 2.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silkweave/nestjs",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Silkweave NestJS Adapter",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,12 +8,23 @@
8
8
  "directory": "packages/nestjs"
9
9
  },
10
10
  "type": "module",
11
- "main": "build/index.mjs",
11
+ "main": "./build/index.mjs",
12
+ "types": "./build/index.d.mts",
13
+ "files": [
14
+ "build"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "@silkweave/source": "./src/index.ts",
19
+ "types": "./build/index.d.mts",
20
+ "default": "./build/index.mjs"
21
+ }
22
+ },
12
23
  "peerDependencies": {
13
24
  "@nestjs/common": "^10.0.0 || ^11.0.0",
14
25
  "@nestjs/core": "^10.0.0 || ^11.0.0",
15
26
  "@nestjs/swagger": "^7.0.0 || ^8.0.0 || ^11.0.0",
16
- "class-validator": "^0.14.0"
27
+ "class-validator": "^0.14.0 || ^0.15.0"
17
28
  },
18
29
  "peerDependenciesMeta": {
19
30
  "@nestjs/swagger": {
@@ -27,9 +38,9 @@
27
38
  "cors": "^2.8.6",
28
39
  "express": "^5.2.1",
29
40
  "zod": "^3.25.0",
30
- "@silkweave/auth": "2.0.0",
31
- "@silkweave/core": "2.0.0",
32
- "@silkweave/mcp": "2.0.0"
41
+ "@silkweave/core": "2.2.0",
42
+ "@silkweave/auth": "2.2.0",
43
+ "@silkweave/mcp": "2.2.0"
33
44
  },
34
45
  "devDependencies": {
35
46
  "@eslint/js": "^10.0.1",
@@ -56,9 +67,5 @@
56
67
  "typecheck": "tsc --noEmit",
57
68
  "lint": "eslint",
58
69
  "check": "pnpm lint && pnpm typecheck"
59
- },
60
- "exports": {
61
- ".": "./build/index.mjs"
62
- },
63
- "types": "./build/index.d.ts"
70
+ }
64
71
  }
@@ -1,16 +0,0 @@
1
- $ tsdown
2
- ℹ tsdown v0.21.10 powered by rolldown v1.0.0-rc.17
3
- ℹ config file: /Users/atomic/projects/silkweave/silkweave/packages/nestjs/tsdown.config.ts
4
- ℹ entry: src/index.ts
5
- ℹ tsconfig: tsconfig.json
6
- ℹ Build start
7
- ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
8
- See more at https://tsdown.dev/options/dependencies#deps-onlybundle
9
- Detected dependencies in bundle:
10
- - rxjs
11
- ℹ build/index.mjs 339.14 kB │ gzip: 49.99 kB
12
- ℹ build/index.mjs.map 579.22 kB │ gzip: 88.28 kB
13
- ℹ build/index.d.mts.map  3.06 kB │ gzip: 1.10 kB
14
- ℹ build/index.d.mts  17.08 kB │ gzip: 6.02 kB
15
- ℹ 4 files, total: 938.50 kB
16
- ✔ Build complete in 1034ms
@@ -1,3 +0,0 @@
1
- $ pnpm lint && pnpm typecheck
2
- $ eslint
3
- $ tsc --noEmit
@@ -1,2 +0,0 @@
1
-
2
- $ eslint
@@ -1,18 +0,0 @@
1
- $ pnpm clean && pnpm build
2
- $ rimraf build
3
- $ tsdown
4
- ℹ tsdown v0.21.10 powered by rolldown v1.0.0-rc.17
5
- ℹ config file: /Users/atomic/projects/silkweave/silkweave/packages/nestjs/tsdown.config.ts
6
- ℹ entry: src/index.ts
7
- ℹ tsconfig: tsconfig.json
8
- ℹ Build start
9
- ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
10
- See more at https://tsdown.dev/options/dependencies#deps-onlybundle
11
- Detected dependencies in bundle:
12
- - rxjs
13
- ℹ build/index.mjs 344.18 kB │ gzip: 51.78 kB
14
- ℹ build/index.mjs.map 589.05 kB │ gzip: 91.22 kB
15
- ℹ build/index.d.mts.map  3.45 kB │ gzip: 1.22 kB
16
- ℹ build/index.d.mts  20.10 kB │ gzip: 6.89 kB
17
- ℹ 4 files, total: 956.79 kB
18
- ✔ Build complete in 822ms
package/eslint.config.mjs DELETED
@@ -1,93 +0,0 @@
1
- import eslint from '@eslint/js'
2
- import stylistic from '@stylistic/eslint-plugin'
3
- import { defineConfig } from 'eslint/config'
4
- import tseslint from 'typescript-eslint'
5
-
6
- export default defineConfig(eslint.configs.recommended, tseslint.configs.recommendedTypeChecked, tseslint.configs.stylisticTypeChecked, {
7
- languageOptions: {
8
- parserOptions: {
9
- project: 'tsconfig.json',
10
- tsconfigRootDir: import.meta.dirname
11
- }
12
- },
13
- plugins: { '@stylistic': stylistic },
14
- rules: {
15
- '@stylistic/space-in-parens': ['error'],
16
- '@stylistic/comma-spacing': ['error'],
17
- '@stylistic/no-multi-spaces': ['error'],
18
- '@stylistic/no-trailing-spaces': ['error'],
19
- '@stylistic/no-whitespace-before-property': ['error'],
20
- '@stylistic/array-bracket-newline': ['error', 'consistent'],
21
- '@stylistic/array-bracket-spacing': ['error'],
22
- '@stylistic/arrow-spacing': ['error'],
23
- '@stylistic/arrow-parens': ['error', 'always'],
24
- '@stylistic/block-spacing': ['error', 'always'],
25
- '@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
26
- '@stylistic/comma-dangle': ['error', 'never'],
27
- '@stylistic/key-spacing': ['error'],
28
- '@stylistic/keyword-spacing': ['error'],
29
- '@stylistic/member-delimiter-style': ['error', { 'multiline': { 'delimiter': 'none' } }],
30
- '@stylistic/no-extra-semi': ['error'],
31
- '@stylistic/indent': ['error', 2],
32
- '@stylistic/no-multiple-empty-lines': ['error', { 'max': 1, 'maxEOF': 0, 'maxBOF': 0 }],
33
- '@stylistic/object-curly-spacing': ['error', 'always'],
34
- '@stylistic/quotes': ['error', 'single'],
35
- '@stylistic/semi': ['error', 'never'],
36
- '@stylistic/space-before-blocks': ['error', 'always'],
37
- '@stylistic/space-before-function-paren': ['error', { 'anonymous': 'always', 'named': 'never', 'asyncArrow': 'always' }],
38
- '@typescript-eslint/adjacent-overload-signatures': 'error',
39
- '@typescript-eslint/array-type': 'off',
40
- '@typescript-eslint/await-thenable': 'error',
41
- '@typescript-eslint/ban-types': 'off',
42
- '@typescript-eslint/consistent-type-assertions': 'off',
43
- '@typescript-eslint/explicit-function-return-type': 'off',
44
- '@typescript-eslint/explicit-member-accessibility': 'off',
45
- '@typescript-eslint/no-angle-bracket-type-assertion': 'off',
46
- '@typescript-eslint/no-empty-function': 'off',
47
- '@typescript-eslint/no-empty-interface': 'off',
48
- '@typescript-eslint/no-extra-non-null-assertion': 'error',
49
- '@typescript-eslint/no-floating-promises': 'off',
50
- '@typescript-eslint/no-misused-new': 'error',
51
- '@typescript-eslint/no-misused-promises': 'off',
52
- '@typescript-eslint/no-namespace': 'off',
53
- '@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
54
- '@typescript-eslint/no-non-null-assertion': 'off',
55
- '@typescript-eslint/no-object-literal-type-assertion': 'off',
56
- '@typescript-eslint/no-parameter-properties': 'off',
57
- '@typescript-eslint/no-shadow': 'error',
58
- '@typescript-eslint/no-triple-slash-reference': 'off',
59
- '@typescript-eslint/no-unused-vars': ['error', {
60
- 'args': 'all',
61
- 'argsIgnorePattern': '^_',
62
- 'caughtErrors': 'all',
63
- 'caughtErrorsIgnorePattern': '^_',
64
- 'destructuredArrayIgnorePattern': '^_',
65
- 'varsIgnorePattern': '^_',
66
- 'ignoreRestSiblings': true
67
- }],
68
- '@typescript-eslint/no-use-before-define': 'off',
69
- '@typescript-eslint/no-var-requires': 'off',
70
- '@typescript-eslint/prefer-for-of': 'error',
71
- '@typescript-eslint/prefer-interface': 'off',
72
- '@typescript-eslint/prefer-nullish-coalescing': 'off',
73
- '@typescript-eslint/prefer-optional-chain': 'error',
74
- '@typescript-eslint/return-await': 'error',
75
- '@typescript-eslint/unified-signatures': 'error',
76
- '@typescript-eslint/no-unsafe-return': 'off',
77
- '@typescript-eslint/no-redundant-type-constituents': 'off',
78
- '@typescript-eslint/no-unsafe-member-access': 'off',
79
- '@typescript-eslint/no-unsafe-call': 'off',
80
- '@typescript-eslint/no-unsafe-argument': 'off',
81
- '@typescript-eslint/dot-notation': 'off',
82
- '@typescript-eslint/prefer-regexp-exec': 'off',
83
- '@typescript-eslint/require-await': 'off',
84
- '@typescript-eslint/only-throw-error': 'error',
85
- '@typescript-eslint/no-base-to-string': 'off',
86
- '@typescript-eslint/restrict-template-expressions': 'off'
87
- }
88
- }, {
89
- files: ['eslint.config.mjs'],
90
- extends: [tseslint.configs.disableTypeChecked]
91
- }, {
92
- ignores: ['build', 'tsdown.config.ts', 'scratch']
93
- })
@@ -1,102 +0,0 @@
1
- import { AuthConfig } from '@silkweave/auth'
2
- import {
3
- authMiddleware,
4
- mcpCors,
5
- mcpTransport,
6
- oauthRoutes,
7
- protectedResourceMetadata,
8
- sideloadResource
9
- } from '@silkweave/mcp'
10
- import { type CorsOptions } from 'cors'
11
- import express, { type RequestHandler } from 'express'
12
- import type { NestAdapterRegisterContext, NestSilkweaveAdapter } from '../lib/types.js'
13
-
14
- export interface McpAdapterOptions {
15
- /** URL prefix the MCP namespace lives under - the transport itself is at this exact path. Default `'/mcp'`. */
16
- basePath?: string
17
- /** Optional bearer-token / OAuth 2.1 config. */
18
- auth?: AuthConfig
19
- /** CORS configuration. `false` to disable, `true`/omitted for permissive defaults, or a `CorsOptions` object. */
20
- cors?: CorsOptions | boolean
21
- /** Mount the sideload resource route at `${basePath}/resource/:id`. Default `true`. */
22
- sideloadResources?: boolean
23
- /** Directory the sideload route reads from. Default `'resources'`. */
24
- resourceDir?: string
25
- }
26
-
27
- function compose(...handlers: RequestHandler[]): RequestHandler {
28
- return (req, res, next) => {
29
- let i = 0
30
- const dispatch: () => void = () => {
31
- const h = handlers[i++]
32
- if (!h) { return next() }
33
- h(req, res, (err) => err ? next(err) : dispatch())
34
- }
35
- dispatch()
36
- }
37
- }
38
-
39
- /**
40
- * MCP adapter for `@silkweave/nestjs`. Registers the MCP Streamable HTTP
41
- * transport, sideload, well-known and OAuth routes individually on Nest's
42
- * HTTP adapter at the configured `basePath` (default `/mcp`):
43
- *
44
- * - `POST/GET/DELETE ${basePath}` - Streamable HTTP transport
45
- * - `GET ${basePath}/resource/:id` - sideload (`sideloadResources` opt-out)
46
- * - `GET ${basePath}/.well-known/oauth-protected-resource` - RFC 9728 metadata (when `auth.resourceUrl`/`auth.authorizationServers` set)
47
- * - `GET ${basePath}/authorize`, `POST ${basePath}/token`, `POST ${basePath}/register`, `GET ${basePath}${callbackPath}` (when `auth.provider` set)
48
- *
49
- * Each route is a real Nest-level route - they show up in
50
- * `RoutesResolver`'s log and there is no sub-app or middleware-slot
51
- * indirection.
52
- */
53
- export function mcp(options: McpAdapterOptions = {}): NestSilkweaveAdapter {
54
- return {
55
- name: 'mcp',
56
- register({ httpAdapter, silkweaveOptions, baseContext, actions }: NestAdapterRegisterContext): void {
57
- const basePath = (options.basePath ?? '/mcp').replace(/\/$/, '')
58
- if (!basePath) { throw new Error('@silkweave/nestjs mcp(): basePath cannot be empty or "/" - pick a path like "/mcp".') }
59
-
60
- const adapter = httpAdapter as unknown as {
61
- get: (path: string, ...h: RequestHandler[]) => unknown
62
- post: (path: string, ...h: RequestHandler[]) => unknown
63
- delete: (path: string, ...h: RequestHandler[]) => unknown
64
- }
65
-
66
- const corsHandler = mcpCors(options.cors ?? true)
67
- const auth = options.auth
68
- const guard = auth ? authMiddleware(auth, baseContext) : null
69
- const prefix = (...handlers: (RequestHandler | null)[]): RequestHandler[] =>
70
- handlers.filter((h): h is RequestHandler => Boolean(h))
71
-
72
- // Public auth-discovery / OAuth routes - registered first so they're
73
- // never inadvertently guarded by the auth middleware.
74
- if (auth?.authorizationServers?.length && auth.resourceUrl) {
75
- adapter.get(
76
- `${basePath}/.well-known/oauth-protected-resource`,
77
- ...prefix(corsHandler, protectedResourceMetadata(auth))
78
- )
79
- }
80
- if (auth?.provider) {
81
- const oauth = oauthRoutes(auth)
82
- adapter.get(`${basePath}/.well-known/oauth-authorization-server`, ...prefix(corsHandler, oauth.wellKnownAuthServer))
83
- adapter.get(`${basePath}/authorize`, ...prefix(corsHandler, oauth.authorize))
84
- adapter.get(`${basePath}${oauth.callbackPath}`, ...prefix(corsHandler, oauth.callback))
85
- adapter.post(`${basePath}/token`, ...prefix(corsHandler, ...oauth.token))
86
- adapter.post(`${basePath}/register`, ...prefix(corsHandler, ...oauth.register))
87
- }
88
-
89
- // Protected routes - wrapped with auth middleware (when configured).
90
- const protect = guard ? (h: RequestHandler) => compose(guard, h) : (h: RequestHandler) => h
91
-
92
- if (options.sideloadResources !== false) {
93
- adapter.get(`${basePath}/resource/:id`, ...prefix(corsHandler, protect(sideloadResource({ resourceDir: options.resourceDir }))))
94
- }
95
-
96
- const transport = mcpTransport(silkweaveOptions, baseContext, actions)
97
- adapter.post(basePath, ...prefix(corsHandler, express.json(), protect(transport.post)))
98
- adapter.get(basePath, ...prefix(corsHandler, protect(transport.stream)))
99
- adapter.delete(basePath, ...prefix(corsHandler, protect(transport.stream)))
100
- }
101
- }
102
- }
@@ -1,39 +0,0 @@
1
- import { SetMetadata } from '@nestjs/common'
2
- import { MCP_METADATA, type McpMetadata } from '../lib/metadata.js'
3
-
4
- /**
5
- * Method decorator that exposes an existing NestJS controller route as an MCP
6
- * tool. It is **additive** - the route keeps serving HTTP exactly as before;
7
- * `@Mcp()` just opts the method into MCP discovery.
8
- *
9
- * The tool's name, description, and input schema are reflected from the
10
- * method's own metadata:
11
- * - **fields** from the parameter decorators (`@Param`/`@Query`/`@Body`) - a
12
- * `@Param('id')` becomes an `id` field; a whole-DTO `@Body() dto: CreateDto`
13
- * is flattened to its properties,
14
- * - **types/constraints/descriptions** from `@nestjs/swagger`
15
- * (`@ApiParam`/`@ApiQuery`/`@ApiProperty`/`@ApiOperation`) and, when present,
16
- * `class-validator` decorators on the DTOs,
17
- * - optionally refined by an OpenAPI document passed to `SilkweaveModule`.
18
- *
19
- * On a tool call the input is split back into the method's positional arguments
20
- * and the method is invoked directly (with `@UseGuards` guards applied first).
21
- *
22
- * @example
23
- * ```ts
24
- * @Controller('sessions/:sessionId/channels')
25
- * export class ChannelsController {
26
- * @Get(':channelId')
27
- * @ApiOperation({ summary: 'Get a specific channel by ID' })
28
- * @ApiParam({ name: 'sessionId', description: 'Session ID' })
29
- * @ApiParam({ name: 'channelId', description: 'Channel ID' })
30
- * @Mcp()
31
- * findOne(@Param('sessionId') sessionId: string, @Param('channelId') channelId: string) {
32
- * return this.service.get(sessionId, channelId)
33
- * }
34
- * }
35
- * ```
36
- */
37
- export function Mcp(options: McpMetadata = {}): MethodDecorator {
38
- return SetMetadata(MCP_METADATA, options)
39
- }
package/src/index.ts DELETED
@@ -1,10 +0,0 @@
1
- export * from './adapter/mcp.js'
2
- export * from './decorator/mcp.js'
3
- export * from './lib/controllerDiscovery.js'
4
- export * from './lib/guards.js'
5
- export * from './lib/metadata.js'
6
- export * from './lib/rebind.js'
7
- export * from './lib/silkweave.module.js'
8
- export * from './lib/types.js'
9
- export * from './lib/reflect/openapi.js'
10
- export * from './lib/reflect/schema.js'
@@ -1,201 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
- import { Injectable, type Type } from '@nestjs/common'
3
- import { DiscoveryService, MetadataScanner, ModuleRef, Reflector } from '@nestjs/core'
4
- import { createAction, type Action, type SilkweaveContext } from '@silkweave/core'
5
- import { z } from 'zod/v4'
6
- import { collectGuards, runGuards } from './guards.js'
7
- import { MCP_METADATA, type McpMetadata } from './metadata.js'
8
- import { invokeRebound, specialBinding, type Binding } from './rebind.js'
9
- import { buildOpenApiLookup, openApiFields, type OpenApiDocument, type OpenApiLookup } from './reflect/openapi.js'
10
- import { PARAMTYPE, type ParamSlot, readParamSlots } from './reflect/params.js'
11
- import { reflectRoute } from './reflect/route.js'
12
- import { type FieldDesc, fieldToZod, mergeField, reflectDtoFields } from './reflect/schema.js'
13
- import { reflectOperation } from './reflect/swagger.js'
14
-
15
- interface DiscoveredMcp {
16
- instance: object
17
- classRef: Type<unknown>
18
- method: (...args: unknown[]) => unknown
19
- methodName: string
20
- meta: McpMetadata
21
- }
22
-
23
- interface BuiltInput {
24
- shape: Record<string, z.ZodType>
25
- bindings: Binding[]
26
- }
27
-
28
- @Injectable()
29
- export class ControllerDiscovery {
30
- constructor(
31
- private readonly discovery: DiscoveryService,
32
- private readonly scanner: MetadataScanner,
33
- private readonly reflector: Reflector,
34
- private readonly moduleRef: ModuleRef
35
- ) { }
36
-
37
- /**
38
- * Walk every Nest provider/controller, find methods annotated with `@Mcp`,
39
- * and build a core `Action` per method whose input schema is reflected from
40
- * the route + parameter decorators (+ optional OpenAPI document) and whose
41
- * `run` re-binds the validated input back into the method's positional
42
- * arguments (with `@UseGuards` guards applied first).
43
- */
44
- discover(openapi?: OpenApiDocument): Action[] {
45
- const lookup = openapi ? buildOpenApiLookup(openapi) : undefined
46
- const discovered: DiscoveredMcp[] = []
47
- for (const wrapper of this.discovery.getProviders().concat(this.discovery.getControllers())) {
48
- const { instance } = wrapper
49
- if (!instance || typeof instance !== 'object') { continue }
50
- const proto = Object.getPrototypeOf(instance) as object | null
51
- if (!proto) { continue }
52
- const classRef = instance.constructor as Type<unknown>
53
- for (const methodName of this.scanner.getAllMethodNames(proto)) {
54
- const method = (proto as Record<string, unknown>)[methodName] as ((...args: unknown[]) => unknown) | undefined
55
- if (typeof method !== 'function') { continue }
56
- const meta = this.reflector.get<McpMetadata>(MCP_METADATA, method)
57
- if (!meta) { continue }
58
- discovered.push({ instance, classRef, method, methodName, meta })
59
- }
60
- }
61
- return discovered.map((d) => this.toAction(d, lookup))
62
- }
63
-
64
- private toAction(d: DiscoveredMcp, lookup: OpenApiLookup | undefined): Action {
65
- const proto = Object.getPrototypeOf(d.instance) as object
66
- const route = reflectRoute(d.classRef, d.method)
67
- const slots = readParamSlots(d.classRef, d.methodName, proto)
68
- const operation = reflectOperation(d.method)
69
- const docFields = lookup ? openApiFields(lookup, route.method, route.openapiPath) : {}
70
-
71
- const { shape, bindings } = this.buildInput(d, route.pathParams, slots, operation.params, docFields)
72
-
73
- const base = d.classRef.name.replace(/Controller$/, '')
74
- const name = d.meta.name ?? `${base}.${d.methodName}`
75
- const description = d.meta.description ?? operation.description ?? `${d.methodName} (${route.method} /${route.path})`
76
- const applyParamPipes = d.meta.pipes !== 'skip'
77
-
78
- const guards = collectGuards(this.reflector, d.classRef, d.method)
79
- const { moduleRef, reflector } = this
80
- const classRef = d.classRef
81
- const method = d.method
82
- const instance = d.instance
83
-
84
- const applyGuards = async (context: SilkweaveContext): Promise<void> => {
85
- if (guards.length === 0) { return }
86
- const request = context.getOptional<unknown>('request')
87
- const response = context.getOptional<unknown>('response') ?? null
88
- const hasRequest = request != null
89
- const guardRequest = hasRequest ? request : { headers: {}, params: {}, query: {} }
90
- await runGuards(guards, moduleRef, reflector, classRef, method, guardRequest, response, hasRequest ? 'http' : 'rpc')
91
- }
92
-
93
- return createAction({
94
- name,
95
- description,
96
- input: z.object(shape),
97
- // Only the MCP adapter exposes `@Mcp` methods; a future `@Trpc` decorator
98
- // would tag its own actions for the tRPC adapter.
99
- isEnabled: (ctx) => ctx.getOptional<string>('adapter') === 'mcp',
100
- run: async (input: object, context: SilkweaveContext): Promise<object> => {
101
- await applyGuards(context)
102
- const request = context.getOptional<{ headers?: Record<string, unknown> }>('request')
103
- const result = await invokeRebound(method, instance, input as Record<string, unknown>, bindings, request, applyParamPipes)
104
- return (result ?? {})
105
- }
106
- }) as Action
107
- }
108
-
109
- /** Build the merged Zod input shape and the per-argument re-bind plan. */
110
- private buildInput(
111
- d: DiscoveredMcp,
112
- pathParams: string[],
113
- slots: ReturnType<typeof readParamSlots>,
114
- operationParams: Record<string, FieldDesc>,
115
- docFields: Record<string, FieldDesc>
116
- ): BuiltInput {
117
- const proto = Object.getPrototypeOf(d.instance) as object
118
- const designTypes = (Reflect.getMetadata('design:paramtypes', proto, d.methodName) as unknown[] | undefined) ?? []
119
- const fields: Record<string, FieldDesc> = {}
120
- const maxIndex = slots.reduce((m, s) => Math.max(m, s.index), -1)
121
- const bindings: Binding[] = Array.from({ length: maxIndex + 1 }, () => ({ kind: 'missing' as const }))
122
-
123
- const addField = (name: string, desc: FieldDesc): void => {
124
- fields[name] = name in fields ? mergeField(fields[name], desc) : desc
125
- }
126
-
127
- for (const slot of slots) {
128
- const { binding, fields: contributed } = contributeSlot(slot, pathParams, designTypes)
129
- bindings[slot.index] = binding
130
- for (const [name, desc] of Object.entries(contributed)) { addField(name, desc) }
131
- }
132
-
133
- // Layer operation-level (`@ApiParam`/`@ApiQuery`) then OpenAPI-document
134
- // metadata over the structural fields (later sources win per field).
135
- for (const [name, desc] of Object.entries(operationParams)) {
136
- if (name in fields) { fields[name] = mergeField(fields[name], desc) }
137
- }
138
- for (const [name, desc] of Object.entries(docFields)) {
139
- if (name in fields) { fields[name] = mergeField(fields[name], desc) }
140
- }
141
-
142
- const shape: Record<string, z.ZodType> = {}
143
- for (const [name, desc] of Object.entries(fields)) { shape[name] = fieldToZod(desc) }
144
- // `@Mcp({ input })` raw-shape override wins per field.
145
- Object.assign(shape, d.meta.input ?? {})
146
-
147
- return { shape, bindings }
148
- }
149
- }
150
-
151
- function designTypeAt(designTypes: unknown[], index: number): FieldDesc {
152
- const ctor = designTypes[index]
153
- if (ctor === String) { return { type: 'string' } }
154
- if (ctor === Number) { return { type: 'number' } }
155
- if (ctor === Boolean) { return { type: 'boolean' } }
156
- return {}
157
- }
158
-
159
- interface SlotContribution {
160
- binding: Binding
161
- /** Input fields this slot contributes, keyed by field name. */
162
- fields: Record<string, FieldDesc>
163
- }
164
-
165
- /** A `@Param('id')` scalar or a bare `@Param()` covering all path params. */
166
- function paramContribution(slot: ParamSlot, pathParams: string[]): SlotContribution {
167
- if (slot.data) {
168
- return {
169
- binding: { kind: 'value', field: slot.data, source: 'path', metatype: slot.designType, pipes: slot.pipes },
170
- fields: { [slot.data]: { type: 'string', required: true } }
171
- }
172
- }
173
- const fields: Record<string, FieldDesc> = {}
174
- for (const p of pathParams) { fields[p] = { type: 'string', required: true } }
175
- return { binding: { kind: 'params', fields: pathParams }, fields }
176
- }
177
-
178
- /** A `@Query('x')`/`@Body('x')` scalar or a whole-DTO `@Query()`/`@Body()`. */
179
- function bodyOrQueryContribution(slot: ParamSlot, source: 'query' | 'body', requiredScalar: boolean, designTypes: unknown[]): SlotContribution {
180
- if (slot.data) {
181
- return {
182
- binding: { kind: 'value', field: slot.data, source, metatype: slot.designType, pipes: slot.pipes },
183
- fields: { [slot.data]: mergeField(designTypeAt(designTypes, slot.index), { required: requiredScalar }) }
184
- }
185
- }
186
- const dtoFields = reflectDtoFields(slot.designType)
187
- return {
188
- binding: { kind: 'object', source, fields: Object.keys(dtoFields), metatype: slot.designType, pipes: slot.pipes },
189
- fields: dtoFields
190
- }
191
- }
192
-
193
- /** Map one parameter slot to its input-field contribution and re-bind instruction. */
194
- function contributeSlot(slot: ParamSlot, pathParams: string[], designTypes: unknown[]): SlotContribution {
195
- switch (slot.paramtype) {
196
- case PARAMTYPE.PARAM: return paramContribution(slot, pathParams)
197
- case PARAMTYPE.QUERY: return bodyOrQueryContribution(slot, 'query', false, designTypes)
198
- case PARAMTYPE.BODY: return bodyOrQueryContribution(slot, 'body', true, designTypes)
199
- default: return { binding: specialBinding(slot.paramtype, slot.data) ?? { kind: 'missing' }, fields: {} }
200
- }
201
- }
@@ -1,76 +0,0 @@
1
- import type { ArgumentsHost, ContextType, ExecutionContext, Type } from '@nestjs/common'
2
-
3
- /** Subset of `HttpArgumentsHost` we need - re-declared inline to avoid deep `@nestjs/common/interfaces` imports. */
4
- interface HttpHost {
5
- getRequest<T = unknown>(): T
6
- getResponse<T = unknown>(): T
7
- getNext<T = unknown>(): T
8
- }
9
-
10
- interface RpcHost {
11
- getData<T = unknown>(): T
12
- getContext<T = unknown>(): T
13
- }
14
-
15
- interface WsHost {
16
- getClient<T = unknown>(): T
17
- getData<T = unknown>(): T
18
- getPattern(): string
19
- }
20
-
21
- /**
22
- * Minimal `ExecutionContext` impl Nest guards can consume. We only need
23
- * `switchToHttp()` to work for our HTTP-backed transports; the RPC/WS shims are
24
- * stubbed so guards that introspect type can still call them without crashing.
25
- */
26
- export class SilkweaveExecutionContext implements ExecutionContext, ArgumentsHost {
27
- constructor(
28
- private readonly args: unknown[],
29
- private readonly classRef: Type<unknown>,
30
- private readonly handler: (...handlerArgs: unknown[]) => unknown,
31
- private readonly contextType = 'http'
32
- ) { }
33
-
34
- getType<T extends string = ContextType>(): T {
35
- return this.contextType as T
36
- }
37
-
38
- getClass<T = unknown>(): Type<T> {
39
- return this.classRef as Type<T>
40
- }
41
-
42
- getHandler(): (...handlerArgs: unknown[]) => unknown {
43
- return this.handler
44
- }
45
-
46
- getArgs<T extends unknown[] = unknown[]>(): T {
47
- return this.args as T
48
- }
49
-
50
- getArgByIndex<T = unknown>(index: number): T {
51
- return this.args[index] as T
52
- }
53
-
54
- switchToHttp(): HttpHost {
55
- return {
56
- getRequest: <T = unknown>(): T => this.args[0] as T,
57
- getResponse: <T = unknown>(): T => this.args[1] as T,
58
- getNext: <T = unknown>(): T => this.args[2] as T
59
- }
60
- }
61
-
62
- switchToRpc(): RpcHost {
63
- return {
64
- getData: <T = unknown>(): T => this.args[0] as T,
65
- getContext: <T = unknown>(): T => this.args[1] as T
66
- }
67
- }
68
-
69
- switchToWs(): WsHost {
70
- return {
71
- getClient: <T = unknown>(): T => this.args[0] as T,
72
- getData: <T = unknown>(): T => this.args[1] as T,
73
- getPattern: (): string => ''
74
- }
75
- }
76
- }