@silkweave/nestjs 1.9.0 → 1.9.1
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/.turbo/turbo-build.log +17 -17
- package/.turbo/turbo-check.log +1 -2
- package/.turbo/turbo-lint.log +2 -0
- package/.turbo/turbo-prepack.log +19 -0
- package/README.md +17 -15
- package/build/index.d.mts +110 -87
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +171 -195
- package/build/index.mjs.map +1 -1
- package/package.json +12 -6
- package/src/adapter/mcp.ts +84 -37
- package/src/adapter/rest.ts +39 -57
- package/src/adapter/trpc.ts +27 -51
- package/src/adapter/typegen.ts +55 -0
- package/src/decorator/actions.ts +1 -1
- package/src/index.ts +1 -1
- package/src/lib/discovery.ts +8 -8
- package/src/lib/executionContext.ts +3 -3
- package/src/lib/guards.ts +2 -2
- package/src/lib/silkweave.module.ts +40 -17
- package/src/lib/types.ts +35 -20
- package/tsconfig.tsbuildinfo +1 -1
- package/src/lib/silkweave.service.ts +0 -58
- package/src/lib/slot.ts +0 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silkweave/nestjs",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "Silkweave NestJS Adapter",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,19 +16,25 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@trpc/server": "^11.7.1",
|
|
18
18
|
"change-case": "^5.4.4",
|
|
19
|
+
"cors": "^2.8.6",
|
|
20
|
+
"express": "^5.2.1",
|
|
19
21
|
"zod": "^3.25.0",
|
|
20
|
-
"@silkweave/auth": "1.9.
|
|
21
|
-
"@silkweave/logger": "1.9.
|
|
22
|
-
"@silkweave/
|
|
23
|
-
"@silkweave/
|
|
24
|
-
"@silkweave/
|
|
22
|
+
"@silkweave/auth": "1.9.1",
|
|
23
|
+
"@silkweave/logger": "1.9.1",
|
|
24
|
+
"@silkweave/trpc": "1.9.1",
|
|
25
|
+
"@silkweave/core": "1.9.1",
|
|
26
|
+
"@silkweave/typegen": "1.9.1",
|
|
27
|
+
"@silkweave/mcp": "1.9.1"
|
|
25
28
|
},
|
|
26
29
|
"devDependencies": {
|
|
27
30
|
"@eslint/js": "^10.0.1",
|
|
28
31
|
"@nestjs/common": "^11.0.0",
|
|
29
32
|
"@nestjs/core": "^11.0.0",
|
|
30
33
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
34
|
+
"@types/cors": "^2.8.19",
|
|
35
|
+
"@types/express": "^5.0.6",
|
|
31
36
|
"@types/node": "^22.0.0",
|
|
37
|
+
"eslint": "^10.4.0",
|
|
32
38
|
"reflect-metadata": "^0.2.2",
|
|
33
39
|
"rimraf": "^6.1.3",
|
|
34
40
|
"rxjs": "^7.8.1",
|
package/src/adapter/mcp.ts
CHANGED
|
@@ -1,55 +1,102 @@
|
|
|
1
|
-
import type { HttpAdapterHost } from '@nestjs/core'
|
|
2
1
|
import { AuthConfig } from '@silkweave/auth'
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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'
|
|
7
13
|
|
|
8
|
-
export interface McpAdapterOptions
|
|
9
|
-
/** URL prefix
|
|
14
|
+
export interface McpAdapterOptions {
|
|
15
|
+
/** URL prefix the MCP namespace lives under - the transport itself is at this exact path. Default `'/mcp'`. */
|
|
10
16
|
basePath?: string
|
|
11
|
-
/** Optional bearer-token / OAuth
|
|
17
|
+
/** Optional bearer-token / OAuth 2.1 config. */
|
|
12
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
|
+
}
|
|
13
37
|
}
|
|
14
38
|
|
|
15
39
|
/**
|
|
16
|
-
* MCP
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the configured base path.
|
|
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`):
|
|
20
43
|
*
|
|
21
|
-
*
|
|
22
|
-
* - `
|
|
23
|
-
* - `GET /resource
|
|
24
|
-
* - `GET
|
|
25
|
-
* - `GET /authorize`, `POST /token`, `POST /register`, `GET /auth/callback` (when `auth.provider` set)
|
|
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)
|
|
26
48
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
30
52
|
*/
|
|
31
53
|
export function mcp(options: McpAdapterOptions = {}): NestSilkweaveAdapter {
|
|
32
54
|
return {
|
|
33
55
|
name: 'mcp',
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
37
|
-
|
|
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))
|
|
38
87
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
context,
|
|
46
|
-
start: async (actions) => {
|
|
47
|
-
const app = createMcpExpressHandler(silkweaveOptions, context, actions, { ...handlerOptions, auth })
|
|
48
|
-
setHandler(app as unknown as NodeMiddleware)
|
|
49
|
-
},
|
|
50
|
-
stop: async () => { /* Nest owns the HTTP server */ }
|
|
51
|
-
}
|
|
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 }))))
|
|
52
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)))
|
|
53
100
|
}
|
|
54
101
|
}
|
|
55
102
|
}
|
package/src/adapter/rest.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
1
2
|
import { HttpException } from '@nestjs/common'
|
|
2
|
-
import type { HttpAdapterHost } from '@nestjs/core'
|
|
3
3
|
import { AuthConfig, AuthInfo, validateToken } from '@silkweave/auth'
|
|
4
|
-
import { type Action, type
|
|
4
|
+
import { type Action, type SilkweaveContext, SilkweaveError } from '@silkweave/core'
|
|
5
5
|
import { buildLogLevels, type Logger, type LogLevel } from '@silkweave/logger'
|
|
6
6
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
7
7
|
import { z } from 'zod/v4'
|
|
8
|
-
import {
|
|
9
|
-
import type { NestSilkweaveAdapter } from '../lib/types.js'
|
|
8
|
+
import type { NestAdapterRegisterContext, NestSilkweaveAdapter } from '../lib/types.js'
|
|
10
9
|
|
|
11
10
|
const CONSOLE_LEVEL_MAP: Record<LogLevel, 'log' | 'info' | 'warn' | 'error'> = {
|
|
12
11
|
emergency: 'error',
|
|
@@ -20,21 +19,12 @@ const CONSOLE_LEVEL_MAP: Record<LogLevel, 'log' | 'info' | 'warn' | 'error'> = {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export interface RestAdapterOptions {
|
|
23
|
-
/** URL prefix
|
|
22
|
+
/** URL prefix joined to each action's path. e.g. `'/api'` → `POST /api/users/list`. Default: `'/api'`. */
|
|
24
23
|
basePath?: string
|
|
25
24
|
/** Optional bearer-token auth applied to every REST action. */
|
|
26
25
|
auth?: AuthConfig
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
function createRestLogger(): Logger {
|
|
30
|
-
return {
|
|
31
|
-
...buildLogLevels((level, data) => {
|
|
32
|
-
console[CONSOLE_LEVEL_MAP[level]](data)
|
|
33
|
-
}),
|
|
34
|
-
progress: () => { /* progress notifications not supported on REST */ }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
28
|
interface RequestLike {
|
|
39
29
|
headers: Record<string, string | string[] | undefined>
|
|
40
30
|
body?: unknown
|
|
@@ -43,6 +33,13 @@ interface RequestLike {
|
|
|
43
33
|
method?: string
|
|
44
34
|
}
|
|
45
35
|
|
|
36
|
+
function createRestLogger(): Logger {
|
|
37
|
+
return {
|
|
38
|
+
...buildLogLevels((level, data) => { console[CONSOLE_LEVEL_MAP[level]](data) }),
|
|
39
|
+
progress: () => { /* progress notifications not supported on REST */ }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
46
43
|
function actionNameToPath(name: string): string {
|
|
47
44
|
return `/${name.replace(/\./g, '/')}`
|
|
48
45
|
}
|
|
@@ -53,7 +50,11 @@ async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
|
53
50
|
req.on('data', (c: Buffer) => chunks.push(c))
|
|
54
51
|
req.on('end', () => {
|
|
55
52
|
if (chunks.length === 0) { return resolve({}) }
|
|
56
|
-
try {
|
|
53
|
+
try {
|
|
54
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')))
|
|
55
|
+
} catch (err) {
|
|
56
|
+
reject(err instanceof Error ? err : new Error('Unable to parse JSON body'))
|
|
57
|
+
}
|
|
57
58
|
})
|
|
58
59
|
req.on('error', reject)
|
|
59
60
|
})
|
|
@@ -95,22 +96,13 @@ function handleRestError(err: unknown, res: ServerResponse): void {
|
|
|
95
96
|
sendJson(res, { error: 'internal', message: 'Internal error' }, 500)
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return async (req, res, next) => {
|
|
106
|
-
const pathOnly = (req.url ?? '/').split('?')[0]
|
|
107
|
-
const key = `${req.method ?? ''} ${pathOnly}`
|
|
108
|
-
const action = routes.get(key)
|
|
109
|
-
if (!action) {
|
|
110
|
-
if (next) { return next() }
|
|
111
|
-
sendJson(res, { error: 'not_found', message: `No route for ${req.method} ${pathOnly}` }, 404)
|
|
112
|
-
return
|
|
113
|
-
}
|
|
99
|
+
function createActionHandler(
|
|
100
|
+
action: Action,
|
|
101
|
+
context: SilkweaveContext,
|
|
102
|
+
auth: AuthConfig | undefined,
|
|
103
|
+
logger: Logger
|
|
104
|
+
): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
|
|
105
|
+
return async (req, res) => {
|
|
114
106
|
try {
|
|
115
107
|
const reqLike = req as unknown as RequestLike
|
|
116
108
|
let authInfo: AuthInfo | undefined
|
|
@@ -118,9 +110,7 @@ function buildHandler(actions: Action[], context: SilkweaveContext, auth: AuthCo
|
|
|
118
110
|
const authHeader = typeof reqLike.headers.authorization === 'string' ? reqLike.headers.authorization : undefined
|
|
119
111
|
const result = await validateToken(authHeader, auth, context.fork({ request: req }))
|
|
120
112
|
if (result.error) {
|
|
121
|
-
for (const [k, v] of Object.entries(result.error.headers)) {
|
|
122
|
-
res.setHeader(k, v)
|
|
123
|
-
}
|
|
113
|
+
for (const [k, v] of Object.entries(result.error.headers)) { res.setHeader(k, v) }
|
|
124
114
|
sendJson(res, result.error.body, result.error.statusCode)
|
|
125
115
|
return
|
|
126
116
|
}
|
|
@@ -144,35 +134,27 @@ function buildHandler(actions: Action[], context: SilkweaveContext, auth: AuthCo
|
|
|
144
134
|
}
|
|
145
135
|
|
|
146
136
|
/**
|
|
147
|
-
* REST adapter for `@silkweave/nestjs`.
|
|
148
|
-
* route on Nest's
|
|
137
|
+
* REST adapter for `@silkweave/nestjs`. Registers each discovered `@Action`
|
|
138
|
+
* as an individual route on Nest's HTTP adapter:
|
|
149
139
|
*
|
|
150
|
-
* - `kind: 'query'` → `GET ${basePath}/${actionName-with-slashes}` (input
|
|
151
|
-
* - `kind: 'mutation'` → `POST ${basePath}/${actionName-with-slashes}` (input
|
|
140
|
+
* - `kind: 'query'` → `GET ${basePath}/${actionName-with-slashes}` (input from query string)
|
|
141
|
+
* - `kind: 'mutation'` → `POST ${basePath}/${actionName-with-slashes}` (input from JSON body)
|
|
152
142
|
*
|
|
153
|
-
*
|
|
154
|
-
* `@nestjs/
|
|
155
|
-
*
|
|
143
|
+
* Routes show up in Nest's `RoutesResolver` logger and are eligible for
|
|
144
|
+
* `@nestjs/swagger` scanning. Works on `@nestjs/platform-express` out of the
|
|
145
|
+
* box. For `@nestjs/platform-fastify`, register `@fastify/express` first.
|
|
156
146
|
*/
|
|
157
147
|
export function rest(options: RestAdapterOptions = {}): NestSilkweaveAdapter {
|
|
158
148
|
return {
|
|
159
149
|
name: 'rest',
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const context = baseContext.fork({ adapter: 'rest' })
|
|
169
|
-
return {
|
|
170
|
-
context,
|
|
171
|
-
start: async (actions) => {
|
|
172
|
-
setHandler(buildHandler(actions, context, options.auth))
|
|
173
|
-
},
|
|
174
|
-
stop: async () => { /* Nest owns the HTTP server */ }
|
|
175
|
-
}
|
|
150
|
+
register({ httpAdapter, baseContext, actions }: NestAdapterRegisterContext): void {
|
|
151
|
+
const basePath = (options.basePath ?? '/api').replace(/\/$/, '')
|
|
152
|
+
const logger = createRestLogger()
|
|
153
|
+
for (const action of actions) {
|
|
154
|
+
const path = `${basePath}${actionNameToPath(action.name)}`
|
|
155
|
+
const method = action.kind === 'query' ? 'get' : 'post'
|
|
156
|
+
const handler = createActionHandler(action, baseContext, options.auth, logger)
|
|
157
|
+
; (httpAdapter as unknown as Record<string, (path: string, h: typeof handler) => unknown>)[method](path, handler)
|
|
176
158
|
}
|
|
177
159
|
}
|
|
178
160
|
}
|
package/src/adapter/trpc.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type { HttpAdapterHost } from '@nestjs/core'
|
|
2
1
|
import { AuthConfig } from '@silkweave/auth'
|
|
3
|
-
import type { Adapter, AdapterGenerator } from '@silkweave/core'
|
|
4
2
|
import { buildRouter, createActionLogger, resolveAuth, type TrpcHandlerContext } from '@silkweave/trpc'
|
|
5
3
|
import { createExpressMiddleware } from '@trpc/server/adapters/express'
|
|
6
4
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
7
|
-
import {
|
|
8
|
-
import type { NestSilkweaveAdapter } from '../lib/types.js'
|
|
5
|
+
import type { NestAdapterRegisterContext, NestSilkweaveAdapter } from '../lib/types.js'
|
|
9
6
|
|
|
10
7
|
export interface TrpcAdapterOptions {
|
|
11
8
|
/** URL prefix at which the tRPC handler is mounted. Default `'/trpc'`. */
|
|
@@ -16,65 +13,44 @@ export interface TrpcAdapterOptions {
|
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
15
|
* tRPC adapter for `@silkweave/nestjs`. Builds a tRPC router from discovered
|
|
19
|
-
* `@Action` methods
|
|
20
|
-
*
|
|
21
|
-
* underlying HTTP server.
|
|
16
|
+
* `@Action` methods and mounts the resulting express middleware at
|
|
17
|
+
* `basePath` on Nest's HTTP adapter.
|
|
22
18
|
*
|
|
23
19
|
* Action names with dots (e.g. `users.list` from `@Actions('users')`) collapse
|
|
24
|
-
* to camelCase procedure keys (`usersList`)
|
|
20
|
+
* to camelCase procedure keys (`usersList`).
|
|
25
21
|
*
|
|
26
22
|
* Works on `@nestjs/platform-express`. On `@nestjs/platform-fastify`, register
|
|
27
|
-
* `@fastify/express` first so Nest can
|
|
23
|
+
* `@fastify/express` first so Nest can serve Express-style middleware.
|
|
28
24
|
*/
|
|
29
25
|
export function trpc(options: TrpcAdapterOptions = {}): NestSilkweaveAdapter {
|
|
30
26
|
return {
|
|
31
27
|
name: 'trpc',
|
|
32
|
-
|
|
33
|
-
const httpAdapter = host.httpAdapter
|
|
34
|
-
if (!httpAdapter) {
|
|
35
|
-
throw new Error('@silkweave/nestjs trpc(): HttpAdapterHost.httpAdapter is not available.')
|
|
36
|
-
}
|
|
28
|
+
register({ httpAdapter, baseContext, actions }: NestAdapterRegisterContext): void {
|
|
37
29
|
const basePath = (options.basePath ?? '/trpc').replace(/\/$/, '') || '/'
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
const router = buildRouter(actions)
|
|
31
|
+
const logger = createActionLogger()
|
|
32
|
+
const createContext = async (
|
|
33
|
+
opts: { req: IncomingMessage; res: ServerResponse }
|
|
34
|
+
): Promise<TrpcHandlerContext> => {
|
|
35
|
+
const resolved = await resolveAuth(options.auth, opts.req.headers.authorization, baseContext.fork({ request: opts.req }))
|
|
36
|
+
if (resolved.kind === 'error') {
|
|
37
|
+
for (const [key, value] of Object.entries<string>(resolved.error.headers)) { opts.res.setHeader(key, value) }
|
|
38
|
+
opts.res.statusCode = resolved.error.statusCode
|
|
39
|
+
opts.res.setHeader('Content-Type', 'application/json')
|
|
40
|
+
opts.res.end(JSON.stringify(resolved.error.body))
|
|
41
|
+
throw new Error('Unauthorized')
|
|
42
|
+
}
|
|
41
43
|
return {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
): Promise<TrpcHandlerContext> => {
|
|
49
|
-
const resolved = await resolveAuth(
|
|
50
|
-
options.auth,
|
|
51
|
-
opts.req.headers.authorization,
|
|
52
|
-
context.fork({ request: opts.req })
|
|
53
|
-
)
|
|
54
|
-
if (resolved.kind === 'error') {
|
|
55
|
-
for (const [key, value] of Object.entries<string>(resolved.error.headers)) {
|
|
56
|
-
opts.res.setHeader(key, value)
|
|
57
|
-
}
|
|
58
|
-
opts.res.statusCode = resolved.error.statusCode
|
|
59
|
-
opts.res.setHeader('Content-Type', 'application/json')
|
|
60
|
-
opts.res.end(JSON.stringify(resolved.error.body))
|
|
61
|
-
throw new Error('Unauthorized')
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
silkweaveContext: context.fork({
|
|
65
|
-
logger,
|
|
66
|
-
request: opts.req,
|
|
67
|
-
response: opts.res,
|
|
68
|
-
...(resolved.authInfo ? { auth: resolved.authInfo } : {})
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const middleware = createExpressMiddleware({ router, createContext })
|
|
73
|
-
setHandler(middleware as unknown as NodeMiddleware)
|
|
74
|
-
},
|
|
75
|
-
stop: async () => { /* Nest owns the HTTP server */ }
|
|
44
|
+
silkweaveContext: baseContext.fork({
|
|
45
|
+
logger,
|
|
46
|
+
request: opts.req,
|
|
47
|
+
response: opts.res,
|
|
48
|
+
...(resolved.authInfo ? { auth: resolved.authInfo } : {})
|
|
49
|
+
})
|
|
76
50
|
}
|
|
77
51
|
}
|
|
52
|
+
const middleware = createExpressMiddleware({ router, createContext })
|
|
53
|
+
;(httpAdapter as unknown as { use: (path: string, h: unknown) => unknown }).use(basePath, middleware)
|
|
78
54
|
}
|
|
79
55
|
}
|
|
80
56
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { renderTypegen, type TypegenFormat } from '@silkweave/typegen'
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
4
|
+
import type { NestAdapterRegisterContext, NestSilkweaveAdapter } from '../lib/types.js'
|
|
5
|
+
|
|
6
|
+
export interface TypegenAdapterOptions {
|
|
7
|
+
/** Output file path for the generated `.d.ts` file. Resolved against `process.cwd()`. Parent directories are created automatically. */
|
|
8
|
+
path: string
|
|
9
|
+
/**
|
|
10
|
+
* What to emit (default `'all'`):
|
|
11
|
+
* - `'interfaces'` - `{Name}Input` / `{Name}Output` interfaces per action
|
|
12
|
+
* - `'trpc-router'` - `AppRouter` type alias for `createTRPCClient<AppRouter>()`
|
|
13
|
+
* - `'all'` - both
|
|
14
|
+
*/
|
|
15
|
+
format?: TypegenFormat
|
|
16
|
+
/**
|
|
17
|
+
* Whether to write the file at all. Default: `process.env.NODE_ENV !== 'production'`.
|
|
18
|
+
* Server processes generally shouldn't write to disk in production deploys -
|
|
19
|
+
* leave it on the default unless you know you want otherwise.
|
|
20
|
+
*/
|
|
21
|
+
enabled?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Typegen adapter for `@silkweave/nestjs`. Discovers every `@Action`-decorated
|
|
26
|
+
* method (regardless of `transports` filtering) and writes a single `.d.ts`
|
|
27
|
+
* file with REST input/output interfaces and/or a tRPC `AppRouter` type alias.
|
|
28
|
+
*
|
|
29
|
+
* Designed for the monorepo pattern where the server (this app) is the source
|
|
30
|
+
* of truth for types and consumer apps import from `path` - e.g.
|
|
31
|
+
* `path: '../app/src/types/silkweave.ts'`.
|
|
32
|
+
*/
|
|
33
|
+
export function typegen(options: TypegenAdapterOptions): NestSilkweaveAdapter {
|
|
34
|
+
const enabled = options.enabled ?? (process.env['NODE_ENV'] !== 'production')
|
|
35
|
+
return {
|
|
36
|
+
name: 'typegen',
|
|
37
|
+
allActions: true,
|
|
38
|
+
register({ actions }: NestAdapterRegisterContext): void {
|
|
39
|
+
if (!enabled) { return }
|
|
40
|
+
const output = renderTypegen(actions, options.format ?? 'all')
|
|
41
|
+
const target = resolve(options.path)
|
|
42
|
+
// Fire-and-forget: we don't want to make `configure()` await disk IO and
|
|
43
|
+
// delay app startup. Errors are surfaced via console.
|
|
44
|
+
void (async () => {
|
|
45
|
+
try {
|
|
46
|
+
await mkdir(dirname(target), { recursive: true })
|
|
47
|
+
await writeFile(target, output, 'utf-8')
|
|
48
|
+
console.info(`@silkweave/nestjs typegen: wrote ${actions.length} action types to ${target}`)
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('@silkweave/nestjs typegen:', err)
|
|
51
|
+
}
|
|
52
|
+
})()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/decorator/actions.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ACTIONS_METADATA, type ActionsClassMetadata, type Transport } from '../
|
|
|
6
6
|
* prefix. The prefix is joined to each method's action name with a dot
|
|
7
7
|
* (e.g. `@Actions('users')` + method `list` → action name `users.list`).
|
|
8
8
|
*
|
|
9
|
-
* The class itself remains a normal Nest provider
|
|
9
|
+
* The class itself remains a normal Nest provider - add `@Injectable()`
|
|
10
10
|
* separately so it can be resolved by the DI container.
|
|
11
11
|
*
|
|
12
12
|
* Accepts either a prefix string (shorthand) or a full options object:
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './adapter/mcp.js'
|
|
2
2
|
export * from './adapter/rest.js'
|
|
3
3
|
export * from './adapter/trpc.js'
|
|
4
|
+
export * from './adapter/typegen.js'
|
|
4
5
|
export * from './decorator/action.js'
|
|
5
6
|
export * from './decorator/actions.js'
|
|
6
7
|
export * from './lib/discovery.js'
|
|
@@ -8,5 +9,4 @@ export * from './lib/filter.js'
|
|
|
8
9
|
export * from './lib/guards.js'
|
|
9
10
|
export * from './lib/metadata.js'
|
|
10
11
|
export * from './lib/silkweave.module.js'
|
|
11
|
-
export * from './lib/silkweave.service.js'
|
|
12
12
|
export * from './lib/types.js'
|
package/src/lib/discovery.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
1
2
|
import { Injectable, type Type } from '@nestjs/common'
|
|
2
3
|
import { DiscoveryService, MetadataScanner, ModuleRef, Reflector } from '@nestjs/core'
|
|
3
4
|
import { createAction, type Action, type SilkweaveContext } from '@silkweave/core'
|
|
@@ -22,7 +23,7 @@ export class ActionDiscovery {
|
|
|
22
23
|
private readonly scanner: MetadataScanner,
|
|
23
24
|
private readonly reflector: Reflector,
|
|
24
25
|
private readonly moduleRef: ModuleRef
|
|
25
|
-
) {}
|
|
26
|
+
) { }
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Walk every Nest provider, find methods annotated with `@Action`, and build
|
|
@@ -64,18 +65,17 @@ export class ActionDiscovery {
|
|
|
64
65
|
const reflector = this.reflector
|
|
65
66
|
|
|
66
67
|
// Cast at the createAction boundary to bridge dual-zod-version installs
|
|
67
|
-
// (zod@3.25 + zod@4.x can both be present transitively). Runtime is fine
|
|
68
|
-
// they share the same /v4 surface
|
|
69
|
-
type CreateActionArg = Parameters<typeof createAction>[0]
|
|
68
|
+
// (zod@3.25 + zod@4.x can both be present transitively). Runtime is fine -
|
|
69
|
+
// they share the same /v4 surface - but the structural types are distinct.
|
|
70
70
|
return createAction({
|
|
71
71
|
name,
|
|
72
72
|
description: d.meta.description,
|
|
73
|
-
input: d.meta.input
|
|
74
|
-
output: d.meta.output
|
|
73
|
+
input: d.meta.input,
|
|
74
|
+
output: d.meta.output,
|
|
75
75
|
kind: d.meta.kind ?? 'mutation',
|
|
76
|
-
args: d.meta.args
|
|
76
|
+
args: d.meta.args,
|
|
77
77
|
isEnabled,
|
|
78
|
-
toolResult: d.meta.toolResult
|
|
78
|
+
toolResult: d.meta.toolResult,
|
|
79
79
|
run: async (input: object, context: SilkweaveContext): Promise<object> => {
|
|
80
80
|
if (guards.length > 0) {
|
|
81
81
|
const request = context.getOptional<unknown>('request')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ArgumentsHost, ContextType, ExecutionContext, Type } from '@nestjs/common'
|
|
2
2
|
|
|
3
|
-
/** Subset of `HttpArgumentsHost` we need
|
|
3
|
+
/** Subset of `HttpArgumentsHost` we need - re-declared inline to avoid deep `@nestjs/common/interfaces` imports. */
|
|
4
4
|
interface HttpHost {
|
|
5
5
|
getRequest<T = unknown>(): T
|
|
6
6
|
getResponse<T = unknown>(): T
|
|
@@ -28,8 +28,8 @@ export class SilkweaveExecutionContext implements ExecutionContext, ArgumentsHos
|
|
|
28
28
|
private readonly args: unknown[],
|
|
29
29
|
private readonly classRef: Type<unknown>,
|
|
30
30
|
private readonly handler: (...handlerArgs: unknown[]) => unknown,
|
|
31
|
-
private readonly contextType
|
|
32
|
-
) {}
|
|
31
|
+
private readonly contextType = 'http'
|
|
32
|
+
) { }
|
|
33
33
|
|
|
34
34
|
getType<T extends string = ContextType>(): T {
|
|
35
35
|
return this.contextType as T
|
package/src/lib/guards.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function collectGuards(
|
|
|
24
24
|
async function resolveGuard(ref: GuardRef, moduleRef: ModuleRef): Promise<CanActivate> {
|
|
25
25
|
if (typeof ref === 'function') {
|
|
26
26
|
try {
|
|
27
|
-
return moduleRef.get(ref, { strict: false })
|
|
27
|
+
return await moduleRef.get(ref, { strict: false })
|
|
28
28
|
} catch {
|
|
29
29
|
return moduleRef.create(ref)
|
|
30
30
|
}
|
|
@@ -59,6 +59,6 @@ export async function runGuards(
|
|
|
59
59
|
throw new ForbiddenException('Forbidden resource')
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
// Reflector kept in the signature for future use (e.g., per-guard metadata)
|
|
62
|
+
// Reflector kept in the signature for future use (e.g., per-guard metadata) - unused here.
|
|
63
63
|
void reflector
|
|
64
64
|
}
|