@silkweave/nestjs 1.12.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/README.md +108 -163
- package/build/index.d.mts +250 -267
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +736 -511
- package/build/index.mjs.map +1 -1
- package/package.json +22 -16
- package/.turbo/turbo-build.log +0 -16
- package/.turbo/turbo-check.log +0 -3
- package/.turbo/turbo-lint.log +0 -2
- package/.turbo/turbo-prepack.log +0 -18
- package/eslint.config.mjs +0 -93
- package/src/adapter/mcp.ts +0 -102
- package/src/adapter/rest.ts +0 -173
- package/src/adapter/trpc.ts +0 -58
- package/src/adapter/typegen.ts +0 -55
- package/src/decorator/action.ts +0 -38
- package/src/decorator/actions.ts +0 -25
- package/src/index.ts +0 -14
- package/src/lib/discovery.ts +0 -131
- package/src/lib/executionContext.ts +0 -76
- package/src/lib/filter.ts +0 -26
- package/src/lib/guards.ts +0 -70
- package/src/lib/metadata.ts +0 -58
- package/src/lib/openapi.ts +0 -116
- package/src/lib/silkweave.module.ts +0 -75
- package/src/lib/swagger.ts +0 -74
- package/src/lib/types.ts +0 -58
- package/tsconfig.json +0 -25
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silkweave/nestjs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Silkweave NestJS Adapter",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,29 +8,39 @@
|
|
|
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
|
-
"@nestjs/swagger": "^7.0.0 || ^8.0.0 || ^11.0.0"
|
|
26
|
+
"@nestjs/swagger": "^7.0.0 || ^8.0.0 || ^11.0.0",
|
|
27
|
+
"class-validator": "^0.14.0 || ^0.15.0"
|
|
16
28
|
},
|
|
17
29
|
"peerDependenciesMeta": {
|
|
18
30
|
"@nestjs/swagger": {
|
|
19
31
|
"optional": true
|
|
32
|
+
},
|
|
33
|
+
"class-validator": {
|
|
34
|
+
"optional": true
|
|
20
35
|
}
|
|
21
36
|
},
|
|
22
37
|
"dependencies": {
|
|
23
|
-
"@trpc/server": "^11.7.1",
|
|
24
|
-
"change-case": "^5.4.4",
|
|
25
38
|
"cors": "^2.8.6",
|
|
26
39
|
"express": "^5.2.1",
|
|
27
40
|
"zod": "^3.25.0",
|
|
28
|
-
"@silkweave/
|
|
29
|
-
"@silkweave/
|
|
30
|
-
"@silkweave/mcp": "
|
|
31
|
-
"@silkweave/trpc": "1.12.0",
|
|
32
|
-
"@silkweave/typegen": "1.12.0",
|
|
33
|
-
"@silkweave/logger": "1.12.0"
|
|
41
|
+
"@silkweave/core": "2.2.0",
|
|
42
|
+
"@silkweave/auth": "2.2.0",
|
|
43
|
+
"@silkweave/mcp": "2.2.0"
|
|
34
44
|
},
|
|
35
45
|
"devDependencies": {
|
|
36
46
|
"@eslint/js": "^10.0.1",
|
|
@@ -57,9 +67,5 @@
|
|
|
57
67
|
"typecheck": "tsc --noEmit",
|
|
58
68
|
"lint": "eslint",
|
|
59
69
|
"check": "pnpm lint && pnpm typecheck"
|
|
60
|
-
}
|
|
61
|
-
"exports": {
|
|
62
|
-
".": "./build/index.mjs"
|
|
63
|
-
},
|
|
64
|
-
"types": "./build/index.d.ts"
|
|
70
|
+
}
|
|
65
71
|
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
[2m$ tsdown[22m
|
|
2
|
-
[34mℹ[39m [34mtsdown v0.21.10[39m powered by [38;2;255;126;23mrolldown v1.0.0-rc.17[39m
|
|
3
|
-
[34mℹ[39m config file: [4m/Users/atomic/projects/silkweave/silkweave/packages/nestjs/tsdown.config.ts[24m
|
|
4
|
-
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
5
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
6
|
-
[34mℹ[39m Build start
|
|
7
|
-
[34mℹ[39m Hint: consider adding [34mdeps.onlyBundle[39m option to avoid unintended bundling of dependencies, or set [34mdeps.onlyBundle: false[39m to disable this hint.
|
|
8
|
-
See more at [4mhttps://tsdown.dev/options/dependencies#deps-onlybundle[24m
|
|
9
|
-
Detected dependencies in bundle:
|
|
10
|
-
- [34mrxjs[39m
|
|
11
|
-
[34mℹ[39m [2mbuild/[22m[1mindex.mjs[22m [2m339.14 kB[22m [2m│ gzip: 49.99 kB[22m
|
|
12
|
-
[34mℹ[39m [2mbuild/[22mindex.mjs.map [2m579.22 kB[22m [2m│ gzip: 88.28 kB[22m
|
|
13
|
-
[34mℹ[39m [2mbuild/[22mindex.d.mts.map [2m 3.06 kB[22m [2m│ gzip: 1.10 kB[22m
|
|
14
|
-
[34mℹ[39m [2mbuild/[22m[32m[1mindex.d.mts[22m[39m [2m 17.08 kB[22m [2m│ gzip: 6.02 kB[22m
|
|
15
|
-
[34mℹ[39m 4 files, total: 938.50 kB
|
|
16
|
-
[32m✔[39m Build complete in [32m1034ms[39m
|
package/.turbo/turbo-check.log
DELETED
package/.turbo/turbo-lint.log
DELETED
package/.turbo/turbo-prepack.log
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
[2m$ pnpm clean && pnpm build[22m
|
|
2
|
-
[2m$ rimraf build[22m
|
|
3
|
-
[2m$ tsdown[22m
|
|
4
|
-
[34mℹ[39m [34mtsdown v0.21.10[39m powered by [38;2;255;126;23mrolldown v1.0.0-rc.17[39m
|
|
5
|
-
[34mℹ[39m config file: [4m/Users/atomic/projects/silkweave/silkweave/packages/nestjs/tsdown.config.ts[24m
|
|
6
|
-
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
7
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
8
|
-
[34mℹ[39m Build start
|
|
9
|
-
[34mℹ[39m Hint: consider adding [34mdeps.onlyBundle[39m option to avoid unintended bundling of dependencies, or set [34mdeps.onlyBundle: false[39m to disable this hint.
|
|
10
|
-
See more at [4mhttps://tsdown.dev/options/dependencies#deps-onlybundle[24m
|
|
11
|
-
Detected dependencies in bundle:
|
|
12
|
-
- [34mrxjs[39m
|
|
13
|
-
[34mℹ[39m [2mbuild/[22m[1mindex.mjs[22m [2m344.18 kB[22m [2m│ gzip: 51.78 kB[22m
|
|
14
|
-
[34mℹ[39m [2mbuild/[22mindex.mjs.map [2m589.05 kB[22m [2m│ gzip: 91.22 kB[22m
|
|
15
|
-
[34mℹ[39m [2mbuild/[22mindex.d.mts.map [2m 3.45 kB[22m [2m│ gzip: 1.22 kB[22m
|
|
16
|
-
[34mℹ[39m [2mbuild/[22m[32m[1mindex.d.mts[22m[39m [2m 20.10 kB[22m [2m│ gzip: 6.89 kB[22m
|
|
17
|
-
[34mℹ[39m 4 files, total: 956.79 kB
|
|
18
|
-
[32m✔[39m Build complete in [32m822ms[39m
|
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
|
-
})
|
package/src/adapter/mcp.ts
DELETED
|
@@ -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
|
-
}
|
package/src/adapter/rest.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
-
import { HttpException } from '@nestjs/common'
|
|
3
|
-
import { AuthConfig, AuthInfo, validateToken } from '@silkweave/auth'
|
|
4
|
-
import { type Action, actionMethod, methodHasBody, resolveActionInput, type SilkweaveContext, SilkweaveError, validateActionRouting } from '@silkweave/core'
|
|
5
|
-
import { buildLogLevels, type Logger, type LogLevel } from '@silkweave/logger'
|
|
6
|
-
import type { IncomingMessage, ServerResponse } from 'http'
|
|
7
|
-
import { z } from 'zod/v4'
|
|
8
|
-
import type { NestAdapterRegisterContext, NestSilkweaveAdapter } from '../lib/types.js'
|
|
9
|
-
|
|
10
|
-
const CONSOLE_LEVEL_MAP: Record<LogLevel, 'log' | 'info' | 'warn' | 'error'> = {
|
|
11
|
-
emergency: 'error',
|
|
12
|
-
alert: 'error',
|
|
13
|
-
critical: 'error',
|
|
14
|
-
error: 'error',
|
|
15
|
-
warning: 'warn',
|
|
16
|
-
notice: 'info',
|
|
17
|
-
info: 'info',
|
|
18
|
-
debug: 'log'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface RestAdapterOptions {
|
|
22
|
-
/** URL prefix joined to each action's path. e.g. `'/api'` → `POST /api/users/list`. Default: `'/api'`. */
|
|
23
|
-
basePath?: string
|
|
24
|
-
/** Optional bearer-token auth applied to every REST action. */
|
|
25
|
-
auth?: AuthConfig
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface RequestLike {
|
|
29
|
-
headers: Record<string, string | string[] | undefined>
|
|
30
|
-
body?: unknown
|
|
31
|
-
query?: Record<string, unknown>
|
|
32
|
-
params?: Record<string, string | undefined>
|
|
33
|
-
url?: string
|
|
34
|
-
method?: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function createRestLogger(): Logger {
|
|
38
|
-
return {
|
|
39
|
-
...buildLogLevels((level, data) => { console[CONSOLE_LEVEL_MAP[level]](data) }),
|
|
40
|
-
progress: () => { /* progress notifications not supported on REST */ }
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function actionNameToPath(name: string): string {
|
|
45
|
-
return `/${name.replace(/\./g, '/')}`
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
const chunks: Buffer[] = []
|
|
51
|
-
req.on('data', (c: Buffer) => chunks.push(c))
|
|
52
|
-
req.on('end', () => {
|
|
53
|
-
if (chunks.length === 0) { return resolve({}) }
|
|
54
|
-
try {
|
|
55
|
-
resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')))
|
|
56
|
-
} catch (err) {
|
|
57
|
-
reject(err instanceof Error ? err : new Error('Unable to parse JSON body'))
|
|
58
|
-
}
|
|
59
|
-
})
|
|
60
|
-
req.on('error', reject)
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function parseQuery(url: string | undefined): Record<string, string> {
|
|
65
|
-
if (!url) { return {} }
|
|
66
|
-
const qIndex = url.indexOf('?')
|
|
67
|
-
if (qIndex === -1) { return {} }
|
|
68
|
-
const params: Record<string, string> = {}
|
|
69
|
-
const search = new URLSearchParams(url.slice(qIndex + 1))
|
|
70
|
-
for (const [k, v] of search.entries()) { params[k] = v }
|
|
71
|
-
return params
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function sendJson(res: ServerResponse, body: unknown, status: number): void {
|
|
75
|
-
if (res.headersSent) { return }
|
|
76
|
-
res.statusCode = status
|
|
77
|
-
res.setHeader('Content-Type', 'application/json')
|
|
78
|
-
res.end(JSON.stringify(body))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function handleRestError(err: unknown, res: ServerResponse): void {
|
|
82
|
-
if (err instanceof z.ZodError) {
|
|
83
|
-
sendJson(res, { error: 'validation_error', issues: err.issues }, 400)
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
if (err instanceof SilkweaveError) {
|
|
87
|
-
sendJson(res, { error: err.code, message: err.message }, err.statusCode)
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
if (err instanceof HttpException) {
|
|
91
|
-
const body = err.getResponse()
|
|
92
|
-
const payload = typeof body === 'string' ? { error: err.name, message: body } : body
|
|
93
|
-
sendJson(res, payload, err.getStatus())
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
console.error(err)
|
|
97
|
-
sendJson(res, { error: 'internal', message: 'Internal error' }, 500)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function createActionHandler(
|
|
101
|
-
action: Action,
|
|
102
|
-
context: SilkweaveContext,
|
|
103
|
-
auth: AuthConfig | undefined,
|
|
104
|
-
logger: Logger
|
|
105
|
-
): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
|
|
106
|
-
return async (req, res) => {
|
|
107
|
-
try {
|
|
108
|
-
const reqLike = req as unknown as RequestLike
|
|
109
|
-
let authInfo: AuthInfo | undefined
|
|
110
|
-
if (auth) {
|
|
111
|
-
const authHeader = typeof reqLike.headers.authorization === 'string' ? reqLike.headers.authorization : undefined
|
|
112
|
-
const result = await validateToken(authHeader, auth, context.fork({ request: req }))
|
|
113
|
-
if (result.error) {
|
|
114
|
-
for (const [k, v] of Object.entries(result.error.headers)) { res.setHeader(k, v) }
|
|
115
|
-
sendJson(res, result.error.body, result.error.statusCode)
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
authInfo = result.auth
|
|
119
|
-
}
|
|
120
|
-
const query = reqLike.query ?? parseQuery(req.url)
|
|
121
|
-
const body = methodHasBody(actionMethod(action))
|
|
122
|
-
? (reqLike.body ?? await readJsonBody(req))
|
|
123
|
-
: undefined
|
|
124
|
-
const raw = resolveActionInput(action, { params: reqLike.params, query, body })
|
|
125
|
-
const input = action.input.parse(raw) as object
|
|
126
|
-
const result = await action.run(input, context.fork({
|
|
127
|
-
logger,
|
|
128
|
-
request: req,
|
|
129
|
-
response: res,
|
|
130
|
-
...(authInfo ? { auth: authInfo } : {})
|
|
131
|
-
}))
|
|
132
|
-
sendJson(res, result, 200)
|
|
133
|
-
} catch (err) {
|
|
134
|
-
handleRestError(err, res)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* REST adapter for `@silkweave/nestjs`. Registers each discovered `@Action`
|
|
141
|
-
* as an individual route on Nest's HTTP adapter:
|
|
142
|
-
*
|
|
143
|
-
* - HTTP verb comes from `method`, else `GET` for `kind: 'query'`, else `POST`.
|
|
144
|
-
* - Path comes from `path` (joined to `basePath`, may contain `:param`
|
|
145
|
-
* placeholders), else `${basePath}/${actionName-with-slashes}`.
|
|
146
|
-
* - Input is merged from the request body, the `queryParams` query-string
|
|
147
|
-
* fields, and any `:param` path placeholders (see `resolveActionInput`).
|
|
148
|
-
*
|
|
149
|
-
* Routes show up in Nest's `RoutesResolver` logger. They are registered
|
|
150
|
-
* directly on the HTTP adapter (not as Nest controllers), so `@nestjs/swagger`'s
|
|
151
|
-
* controller scanner does not see them - use `addSilkweaveActions()` to add them
|
|
152
|
-
* to the OpenAPI document. Works on `@nestjs/platform-express` out of the box.
|
|
153
|
-
* For `@nestjs/platform-fastify`, register `@fastify/express` first.
|
|
154
|
-
*/
|
|
155
|
-
export function rest(options: RestAdapterOptions = {}): NestSilkweaveAdapter {
|
|
156
|
-
const basePath = (options.basePath ?? '/api').replace(/\/$/, '')
|
|
157
|
-
return {
|
|
158
|
-
name: 'rest',
|
|
159
|
-
basePath,
|
|
160
|
-
register({ httpAdapter, baseContext, actions }: NestAdapterRegisterContext): void {
|
|
161
|
-
const logger = createRestLogger()
|
|
162
|
-
for (const action of actions) {
|
|
163
|
-
validateActionRouting(action)
|
|
164
|
-
const path = action.path
|
|
165
|
-
? `${basePath}/${action.path.replace(/^\//, '')}`
|
|
166
|
-
: `${basePath}${actionNameToPath(action.name)}`
|
|
167
|
-
const method = actionMethod(action).toLowerCase()
|
|
168
|
-
const handler = createActionHandler(action, baseContext, options.auth, logger)
|
|
169
|
-
; (httpAdapter as unknown as Record<string, (path: string, h: typeof handler) => unknown>)[method](path, handler)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
package/src/adapter/trpc.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { AuthConfig } from '@silkweave/auth'
|
|
2
|
-
import { buildRouter, createActionLogger, resolveAuth, type TrpcHandlerContext } from '@silkweave/trpc'
|
|
3
|
-
import { createExpressMiddleware } from '@trpc/server/adapters/express'
|
|
4
|
-
import type { IncomingMessage, ServerResponse } from 'http'
|
|
5
|
-
import type { NestAdapterRegisterContext, NestSilkweaveAdapter } from '../lib/types.js'
|
|
6
|
-
|
|
7
|
-
export interface TrpcAdapterOptions {
|
|
8
|
-
/** URL prefix at which the tRPC handler is mounted. Default `'/trpc'`. */
|
|
9
|
-
basePath?: string
|
|
10
|
-
/** Optional bearer-token auth applied to every tRPC procedure. */
|
|
11
|
-
auth?: AuthConfig
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* tRPC adapter for `@silkweave/nestjs`. Builds a tRPC router from discovered
|
|
16
|
-
* `@Action` methods and mounts the resulting express middleware at
|
|
17
|
-
* `basePath` on Nest's HTTP adapter.
|
|
18
|
-
*
|
|
19
|
-
* Action names with dots (e.g. `users.list` from `@Actions('users')`) collapse
|
|
20
|
-
* to camelCase procedure keys (`usersList`).
|
|
21
|
-
*
|
|
22
|
-
* Works on `@nestjs/platform-express`. On `@nestjs/platform-fastify`, register
|
|
23
|
-
* `@fastify/express` first so Nest can serve Express-style middleware.
|
|
24
|
-
*/
|
|
25
|
-
export function trpc(options: TrpcAdapterOptions = {}): NestSilkweaveAdapter {
|
|
26
|
-
return {
|
|
27
|
-
name: 'trpc',
|
|
28
|
-
register({ httpAdapter, baseContext, actions }: NestAdapterRegisterContext): void {
|
|
29
|
-
const basePath = (options.basePath ?? '/trpc').replace(/\/$/, '') || '/'
|
|
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
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
silkweaveContext: baseContext.fork({
|
|
45
|
-
logger,
|
|
46
|
-
request: opts.req,
|
|
47
|
-
response: opts.res,
|
|
48
|
-
...(resolved.authInfo ? { auth: resolved.authInfo } : {})
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const middleware = createExpressMiddleware({ router, createContext })
|
|
53
|
-
;(httpAdapter as unknown as { use: (path: string, h: unknown) => unknown }).use(basePath, middleware)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export { type InferTrpcRouter } from '@silkweave/trpc'
|
package/src/adapter/typegen.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
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/action.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { SetMetadata } from '@nestjs/common'
|
|
2
|
-
import { ACTION_METADATA, type ActionMetadata } from '../lib/metadata.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Method decorator that registers a Silkweave action.
|
|
6
|
-
*
|
|
7
|
-
* The decorated method becomes an Action and is exposed via every adapter
|
|
8
|
-
* configured on `SilkweaveModule` (REST/tRPC/MCP), unless `transports` is
|
|
9
|
-
* provided to restrict it.
|
|
10
|
-
*
|
|
11
|
-
* The method receives `(input, context)` where `input` is the parsed Zod input
|
|
12
|
-
* and `context` is the `SilkweaveContext` (with `logger`, `request`, optional
|
|
13
|
-
* `auth`). The class instance is a normal Nest provider, so other services can
|
|
14
|
-
* be injected via the constructor.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* @Injectable()
|
|
19
|
-
* @Actions('users')
|
|
20
|
-
* export class UserActions {
|
|
21
|
-
* constructor(private db: DbService) {}
|
|
22
|
-
*
|
|
23
|
-
* @Action({
|
|
24
|
-
* description: 'List users',
|
|
25
|
-
* input: z.object({ limit: z.number().optional() }),
|
|
26
|
-
* kind: 'query'
|
|
27
|
-
* })
|
|
28
|
-
* list(input: { limit?: number }, ctx: SilkweaveContext) {
|
|
29
|
-
* return this.db.listUsers(input.limit)
|
|
30
|
-
* }
|
|
31
|
-
* }
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export function Action<I extends object = object, O extends object = object, C = unknown>(
|
|
35
|
-
options: ActionMetadata<I, O, C>
|
|
36
|
-
): MethodDecorator {
|
|
37
|
-
return SetMetadata(ACTION_METADATA, options)
|
|
38
|
-
}
|
package/src/decorator/actions.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { SetMetadata } from '@nestjs/common'
|
|
2
|
-
import { ACTIONS_METADATA, type ActionsClassMetadata, type Transport } from '../lib/metadata.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Class decorator that groups a provider's `@Action` methods under a common
|
|
6
|
-
* prefix. The prefix is joined to each method's action name with a dot
|
|
7
|
-
* (e.g. `@Actions('users')` + method `list` → action name `users.list`).
|
|
8
|
-
*
|
|
9
|
-
* The class itself remains a normal Nest provider - add `@Injectable()`
|
|
10
|
-
* separately so it can be resolved by the DI container.
|
|
11
|
-
*
|
|
12
|
-
* Accepts either a prefix string (shorthand) or a full options object:
|
|
13
|
-
* ```ts
|
|
14
|
-
* @Actions('users')
|
|
15
|
-
* @Actions({ prefix: 'users', transports: ['rest', 'trpc'] })
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
export function Actions(prefixOrOptions: string | ActionsClassMetadata = {}): ClassDecorator {
|
|
19
|
-
const options: ActionsClassMetadata = typeof prefixOrOptions === 'string'
|
|
20
|
-
? { prefix: prefixOrOptions }
|
|
21
|
-
: prefixOrOptions
|
|
22
|
-
return SetMetadata(ACTIONS_METADATA, options)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type { ActionsClassMetadata, Transport }
|