@silkweave/nestjs 1.9.1 → 1.11.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": "1.9.1",
3
+ "version": "1.11.0",
4
4
  "description": "Silkweave NestJS Adapter",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,12 +19,12 @@
19
19
  "cors": "^2.8.6",
20
20
  "express": "^5.2.1",
21
21
  "zod": "^3.25.0",
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"
22
+ "@silkweave/auth": "1.11.0",
23
+ "@silkweave/core": "1.11.0",
24
+ "@silkweave/mcp": "1.11.0",
25
+ "@silkweave/logger": "1.11.0",
26
+ "@silkweave/typegen": "1.11.0",
27
+ "@silkweave/trpc": "1.11.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@eslint/js": "^10.0.1",
@@ -31,8 +31,8 @@ import { ACTION_METADATA, type ActionMetadata } from '../lib/metadata.js'
31
31
  * }
32
32
  * ```
33
33
  */
34
- export function Action<I extends object = object, O extends object = object>(
35
- options: ActionMetadata<I, O>
34
+ export function Action<I extends object = object, O extends object = object, C = unknown>(
35
+ options: ActionMetadata<I, O, C>
36
36
  ): MethodDecorator {
37
37
  return SetMetadata(ACTION_METADATA, options)
38
38
  }
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
2
  import { Injectable, type Type } from '@nestjs/common'
3
3
  import { DiscoveryService, MetadataScanner, ModuleRef, Reflector } from '@nestjs/core'
4
- import { createAction, type Action, type SilkweaveContext } from '@silkweave/core'
4
+ import { createAction, type Action, type ActionKind, type SilkweaveContext, type StreamingActionInput } from '@silkweave/core'
5
5
  import { kebabCase } from 'change-case'
6
6
  import { buildIsEnabled } from './filter.js'
7
7
  import { collectGuards, runGuards } from './guards.js'
@@ -30,9 +30,10 @@ export class ActionDiscovery {
30
30
  * a list of core `Action` objects ready to feed into `silkweave().actions()`.
31
31
  *
32
32
  * Action invocation is wrapped to (a) run `@UseGuards` guards declared on the
33
- * method or its class against the incoming HTTP request (read from
34
- * `ctx.get('request')`) and (b) bind `this` to the resolved Nest provider so
35
- * DI-injected dependencies remain available.
33
+ * method or its class against the incoming request (read from
34
+ * `ctx.get('request')`, populated by REST/tRPC and by MCP-over-HTTP from the
35
+ * SDK's `extra.requestInfo`) and (b) bind `this` to the resolved Nest provider
36
+ * so DI-injected dependencies remain available.
36
37
  */
37
38
  discover(): Action[] {
38
39
  const discovered: DiscoveredAction[] = []
@@ -64,9 +65,47 @@ export class ActionDiscovery {
64
65
  const moduleRef = this.moduleRef
65
66
  const reflector = this.reflector
66
67
 
68
+ const applyGuards = async (context: SilkweaveContext): Promise<void> => {
69
+ if (guards.length === 0) { return }
70
+ // REST and tRPC populate `request`/`response`; MCP-over-HTTP populates
71
+ // `request` (a `{ headers, url, params, query }` stand-in built from the
72
+ // SDK's `extra.requestInfo`). Transports with no HTTP request at all (e.g.
73
+ // MCP stdio) get a header-less stand-in so guards reading `req.headers`
74
+ // degrade gracefully (deny) instead of dereferencing `undefined`.
75
+ const request = context.getOptional<unknown>('request')
76
+ const response = context.getOptional<unknown>('response') ?? null
77
+ const hasRequest = request != null
78
+ const guardRequest = hasRequest ? request : { headers: {}, params: {}, query: {} }
79
+ await runGuards(guards, moduleRef, reflector, d.classRef, d.method, guardRequest, response, hasRequest ? 'http' : 'rpc')
80
+ }
81
+
67
82
  // Cast at the createAction boundary to bridge dual-zod-version installs
68
83
  // (zod@3.25 + zod@4.x can both be present transitively). Runtime is fine -
69
84
  // they share the same /v4 surface - but the structural types are distinct.
85
+ const streaming = d.method.constructor?.name === 'AsyncGeneratorFunction'
86
+
87
+ if (streaming) {
88
+ if (!d.meta.chunk) {
89
+ throw new Error(`@Action "${name}" is an async generator but has no \`chunk\` schema`)
90
+ }
91
+ const method = d.method
92
+ const instance = d.instance
93
+ return createAction({
94
+ name,
95
+ description: d.meta.description,
96
+ input: d.meta.input,
97
+ chunk: d.meta.chunk,
98
+ kind: d.meta.kind ?? 'mutation',
99
+ args: d.meta.args,
100
+ isEnabled,
101
+ toolResult: d.meta.toolResult,
102
+ run: async function* (input: object, context: SilkweaveContext) {
103
+ await applyGuards(context)
104
+ yield* (method.call(instance, input, context) as AsyncGenerator<object, void, void>)
105
+ }
106
+ } as StreamingActionInput<object, unknown, string, ActionKind>) as Action
107
+ }
108
+
70
109
  return createAction({
71
110
  name,
72
111
  description: d.meta.description,
@@ -77,11 +116,7 @@ export class ActionDiscovery {
77
116
  isEnabled,
78
117
  toolResult: d.meta.toolResult,
79
118
  run: async (input: object, context: SilkweaveContext): Promise<object> => {
80
- if (guards.length > 0) {
81
- const request = context.getOptional<unknown>('request')
82
- const response = context.getOptional<unknown>('response')
83
- await runGuards(guards, moduleRef, reflector, d.classRef, d.method, request, response)
84
- }
119
+ await applyGuards(context)
85
120
  const result = await (d.method.call(d.instance, input, context) as Promise<object>)
86
121
  return result
87
122
  }
package/src/lib/guards.ts CHANGED
@@ -33,12 +33,17 @@ async function resolveGuard(ref: GuardRef, moduleRef: ModuleRef): Promise<CanAct
33
33
  }
34
34
 
35
35
  /**
36
- * Run the configured guards against an HTTP request. Throws `ForbiddenException`
36
+ * Run the configured guards against a request. Throws `ForbiddenException`
37
37
  * if any guard rejects, mirroring Nest's HTTP request-pipeline behavior.
38
38
  *
39
39
  * Pass `null` for `response` when running on top of a tRPC or MCP request that
40
40
  * doesn't surface a raw response object; guards that introspect the response
41
41
  * will receive `null`.
42
+ *
43
+ * `contextType` is reflected through `ExecutionContext.getType()` so guards can
44
+ * branch on the transport. It is `'http'` for REST/tRPC and for MCP-over-HTTP
45
+ * (where a header-bearing request stand-in is available); transports without any
46
+ * HTTP request (e.g. MCP stdio) pass `'rpc'`.
42
47
  */
43
48
  export async function runGuards(
44
49
  guards: GuardRef[],
@@ -47,10 +52,11 @@ export async function runGuards(
47
52
  classRef: Type<unknown>,
48
53
  handler: (...args: unknown[]) => unknown,
49
54
  request: unknown,
50
- response: unknown
55
+ response: unknown,
56
+ contextType: 'http' | 'rpc' = 'http'
51
57
  ): Promise<void> {
52
58
  if (guards.length === 0) { return }
53
- const context = new SilkweaveExecutionContext([request, response], classRef, handler, 'http')
59
+ const context = new SilkweaveExecutionContext([request, response], classRef, handler, contextType)
54
60
  for (const ref of guards) {
55
61
  const guard = await resolveGuard(ref, moduleRef)
56
62
  const result = guard.canActivate(context)
@@ -6,7 +6,7 @@ export const ACTIONS_METADATA = '__silkweave_actions__'
6
6
 
7
7
  export type Transport = 'rest' | 'trpc' | 'mcp'
8
8
 
9
- export interface ActionMetadata<I extends object = object, O extends object = object> {
9
+ export interface ActionMetadata<I extends object = object, O extends object = object, C = unknown> {
10
10
  /** Action name. Defaults to the kebab-cased method name. */
11
11
  name?: string
12
12
  /** Human-readable description. Becomes the MCP tool description and REST OpenAPI summary. */
@@ -15,6 +15,13 @@ export interface ActionMetadata<I extends object = object, O extends object = ob
15
15
  input: z.ZodType<I> & { shape: Record<string, z.ZodTypeAny> }
16
16
  /** Optional Zod object schema for the action's output (used by tRPC type inference). */
17
17
  output?: z.ZodType<O> & { shape: Record<string, z.ZodTypeAny> }
18
+ /**
19
+ * Zod schema for individual chunks yielded by a streaming (async-generator)
20
+ * action method. Required when the decorated method is an `async function*`.
21
+ * Mirrors `Action.chunk` in @silkweave/core; typegen/trpc use it to expose
22
+ * the action as a tRPC subscription.
23
+ */
24
+ chunk?: z.ZodType<C>
18
25
  /** `'query'` (GET in REST, `.query()` in tRPC) or `'mutation'` (POST in REST, `.mutation()` in tRPC). Default: `'mutation'`. */
19
26
  kind?: ActionKind
20
27
  /** Allowlist of transports that should expose this action. Default: all registered transports. */