@silkweave/nestjs 1.9.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.
@@ -0,0 +1,76 @@
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: string = '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
+ }
@@ -0,0 +1,26 @@
1
+ import type { SilkweaveContext } from '@silkweave/core'
2
+ import type { Transport } from './metadata.js'
3
+
4
+ /**
5
+ * Compile a `transports` allowlist + optional user `isEnabled` into a single
6
+ * `(ctx) => boolean` callback compatible with `Action.isEnabled`.
7
+ *
8
+ * - If `transports` is omitted, the action runs on every adapter.
9
+ * - If `transports` is set, the action is gated on `ctx.get<string>('adapter')`
10
+ * matching one of the listed transports. Each adapter in `@silkweave/nestjs`
11
+ * forks its context with `{ adapter: 'rest' | 'trpc' | 'mcp' }`.
12
+ * - If both `transports` and `userIsEnabled` are set, they are AND-combined.
13
+ */
14
+ export function buildIsEnabled(
15
+ transports: Transport[] | undefined,
16
+ userIsEnabled: ((ctx: SilkweaveContext) => boolean) | undefined
17
+ ): ((ctx: SilkweaveContext) => boolean) | undefined {
18
+ if (!transports && !userIsEnabled) { return undefined }
19
+ return (ctx) => {
20
+ if (transports) {
21
+ const adapter = ctx.getOptional<string>('adapter')
22
+ if (adapter && !transports.includes(adapter as Transport)) { return false }
23
+ }
24
+ return userIsEnabled ? userIsEnabled(ctx) : true
25
+ }
26
+ }
@@ -0,0 +1,64 @@
1
+ import { ForbiddenException, type CanActivate, type Type } from '@nestjs/common'
2
+ import { GUARDS_METADATA } from '@nestjs/common/constants.js'
3
+ import { ModuleRef, Reflector } from '@nestjs/core'
4
+ import { isObservable, lastValueFrom } from 'rxjs'
5
+ import { SilkweaveExecutionContext } from './executionContext.js'
6
+
7
+ type GuardRef = Type<CanActivate> | CanActivate
8
+
9
+ /**
10
+ * Read `@UseGuards(...)` metadata for both the method and its class and merge
11
+ * the two lists. Method-level guards run AFTER class-level guards (matching
12
+ * Nest's own behavior).
13
+ */
14
+ export function collectGuards(
15
+ reflector: Reflector,
16
+ classRef: Type<unknown>,
17
+ handler: (...args: unknown[]) => unknown
18
+ ): GuardRef[] {
19
+ const classGuards = reflector.get<GuardRef[]>(GUARDS_METADATA, classRef) ?? []
20
+ const methodGuards = reflector.get<GuardRef[]>(GUARDS_METADATA, handler) ?? []
21
+ return [...classGuards, ...methodGuards]
22
+ }
23
+
24
+ async function resolveGuard(ref: GuardRef, moduleRef: ModuleRef): Promise<CanActivate> {
25
+ if (typeof ref === 'function') {
26
+ try {
27
+ return moduleRef.get(ref, { strict: false })
28
+ } catch {
29
+ return moduleRef.create(ref)
30
+ }
31
+ }
32
+ return ref
33
+ }
34
+
35
+ /**
36
+ * Run the configured guards against an HTTP request. Throws `ForbiddenException`
37
+ * if any guard rejects, mirroring Nest's HTTP request-pipeline behavior.
38
+ *
39
+ * Pass `null` for `response` when running on top of a tRPC or MCP request that
40
+ * doesn't surface a raw response object; guards that introspect the response
41
+ * will receive `null`.
42
+ */
43
+ export async function runGuards(
44
+ guards: GuardRef[],
45
+ moduleRef: ModuleRef,
46
+ reflector: Reflector,
47
+ classRef: Type<unknown>,
48
+ handler: (...args: unknown[]) => unknown,
49
+ request: unknown,
50
+ response: unknown
51
+ ): Promise<void> {
52
+ if (guards.length === 0) { return }
53
+ const context = new SilkweaveExecutionContext([request, response], classRef, handler, 'http')
54
+ for (const ref of guards) {
55
+ const guard = await resolveGuard(ref, moduleRef)
56
+ const result = guard.canActivate(context)
57
+ const allowed = isObservable(result) ? await lastValueFrom(result) : await Promise.resolve(result)
58
+ if (!allowed) {
59
+ throw new ForbiddenException('Forbidden resource')
60
+ }
61
+ }
62
+ // Reflector kept in the signature for future use (e.g., per-guard metadata) — unused here.
63
+ void reflector
64
+ }
@@ -0,0 +1,45 @@
1
+ import { type Action, type ActionKind, type SilkweaveContext } from '@silkweave/core'
2
+ import type z from 'zod/v4'
3
+
4
+ export const ACTION_METADATA = '__silkweave_action__'
5
+ export const ACTIONS_METADATA = '__silkweave_actions__'
6
+
7
+ export type Transport = 'rest' | 'trpc' | 'mcp'
8
+
9
+ export interface ActionMetadata<I extends object = object, O extends object = object> {
10
+ /** Action name. Defaults to the kebab-cased method name. */
11
+ name?: string
12
+ /** Human-readable description. Becomes the MCP tool description and REST OpenAPI summary. */
13
+ description: string
14
+ /** Zod object schema for the action's input. */
15
+ input: z.ZodType<I> & { shape: Record<string, z.ZodTypeAny> }
16
+ /** Optional Zod object schema for the action's output (used by tRPC type inference). */
17
+ output?: z.ZodType<O> & { shape: Record<string, z.ZodTypeAny> }
18
+ /** `'query'` (GET in REST, `.query()` in tRPC) or `'mutation'` (POST in REST, `.mutation()` in tRPC). Default: `'mutation'`. */
19
+ kind?: ActionKind
20
+ /** Allowlist of transports that should expose this action. Default: all registered transports. */
21
+ transports?: Transport[]
22
+ /** Dynamic enable check (in addition to `transports`). AND-combined with the transports filter. */
23
+ isEnabled?: (context: SilkweaveContext) => boolean
24
+ /** Custom MCP `CallToolResult` formatter. See `@silkweave/mcp`'s `smartToolResult`. */
25
+ toolResult?: Action<I, O>['toolResult']
26
+ /** CLI positional-argument keys (unused for HTTP-only adapters, kept for parity with `createAction`). */
27
+ args?: (keyof I)[]
28
+ /** Custom MCP tool name override. If unset, derived from action name as PascalCase. */
29
+ mcpToolName?: string
30
+ }
31
+
32
+ export interface ActionsClassMetadata {
33
+ /** Prefix joined to the method-level action name with a dot. e.g. `Actions('users')` + method `list` → `users.list`. */
34
+ prefix?: string
35
+ /** Class-level transports allowlist. Method-level `transports` overrides; otherwise inherits this. */
36
+ transports?: Transport[]
37
+ }
38
+
39
+ export interface ResultToolResult<O extends object = object> {
40
+ toolResult?: Action<object, O>['toolResult']
41
+ }
42
+
43
+ export type AnyActionMetadata = ActionMetadata<object, object> & ResultToolResult<object>
44
+
45
+ export const ACTION_RESPONSE_KEY = '__silkweave_response__'
@@ -0,0 +1,52 @@
1
+ import { Module, type DynamicModule } from '@nestjs/common'
2
+ import { DiscoveryModule } from '@nestjs/core'
3
+ import { ActionDiscovery } from './discovery.js'
4
+ import { SilkweaveService } from './silkweave.service.js'
5
+ import { SILKWEAVE_MODULE_OPTIONS, type SilkweaveModuleOptions } from './types.js'
6
+
7
+ /**
8
+ * Root module for `@silkweave/nestjs`.
9
+ *
10
+ * Discovers every `@Action`-decorated method across the Nest app via
11
+ * `DiscoveryService`, builds core `Action` objects from them, and mounts the
12
+ * configured adapters (`rest()`, `trpc()`, `mcp()`) onto Nest's running HTTP
13
+ * server during `OnApplicationBootstrap`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { Module } from '@nestjs/common'
18
+ * import { SilkweaveModule, rest, trpc, mcp } from '@silkweave/nestjs'
19
+ * import { UsersModule } from './users.module.js'
20
+ *
21
+ * @Module({
22
+ * imports: [
23
+ * SilkweaveModule.forRoot({
24
+ * silkweave: { name: 'app', description: 'My App', version: '1.0.0' },
25
+ * adapters: [
26
+ * rest({ basePath: '/api' }),
27
+ * trpc({ basePath: '/trpc' }),
28
+ * mcp({ basePath: '/mcp' })
29
+ * ]
30
+ * }),
31
+ * UsersModule
32
+ * ]
33
+ * })
34
+ * export class AppModule {}
35
+ * ```
36
+ */
37
+ @Module({})
38
+ export class SilkweaveModule {
39
+ static forRoot(options: SilkweaveModuleOptions): DynamicModule {
40
+ return {
41
+ module: SilkweaveModule,
42
+ global: true,
43
+ imports: [DiscoveryModule],
44
+ providers: [
45
+ { provide: SILKWEAVE_MODULE_OPTIONS, useValue: options },
46
+ ActionDiscovery,
47
+ SilkweaveService
48
+ ],
49
+ exports: [SilkweaveService]
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,58 @@
1
+ import { Inject, Injectable, type OnApplicationBootstrap, type OnApplicationShutdown, type OnModuleInit } from '@nestjs/common'
2
+ import { HttpAdapterHost } from '@nestjs/core'
3
+ import { type AdapterGenerator, silkweave as createSilkweave, type Silkweave } from '@silkweave/core'
4
+ import { ActionDiscovery } from './discovery.js'
5
+ import { SILKWEAVE_MODULE_OPTIONS, type SilkweaveModuleOptions } from './types.js'
6
+
7
+ /**
8
+ * Coordinates the Nest lifecycle for `@silkweave/nestjs`:
9
+ *
10
+ * - `onModuleInit`: each configured adapter calls `install(host)` to reserve a
11
+ * placeholder middleware slot at its base path. This must happen here, not
12
+ * in `onApplicationBootstrap`, because Nest installs its 404 catch-all later
13
+ * during `init()` — routes registered after the catch-all are unreachable.
14
+ *
15
+ * - `onApplicationBootstrap`: discovers every `@Action` method, then drives the
16
+ * standard `silkweave().adapter(...).actions(...).start()` flow, which calls
17
+ * each adapter's `start(actions)` to populate the slot reserved earlier.
18
+ */
19
+ @Injectable()
20
+ export class SilkweaveService implements OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown {
21
+ private builder?: Silkweave
22
+ private readonly generators: AdapterGenerator[] = []
23
+
24
+ constructor(
25
+ @Inject(SILKWEAVE_MODULE_OPTIONS) private readonly options: SilkweaveModuleOptions,
26
+ private readonly discovery: ActionDiscovery,
27
+ private readonly httpAdapterHost: HttpAdapterHost
28
+ ) {}
29
+
30
+ onModuleInit(): void {
31
+ for (const adapter of this.options.adapters) {
32
+ this.generators.push(adapter.install(this.httpAdapterHost))
33
+ }
34
+ }
35
+
36
+ async onApplicationBootstrap(): Promise<void> {
37
+ const actions = this.discovery.discover()
38
+ const builder = createSilkweave(this.options.silkweave)
39
+ for (const [key, value] of Object.entries(this.options.context ?? {})) {
40
+ builder.set(key, value)
41
+ }
42
+ for (const gen of this.generators) {
43
+ builder.adapter(gen)
44
+ }
45
+ builder.actions(actions)
46
+ await builder.start()
47
+ this.builder = builder
48
+ }
49
+
50
+ async onApplicationShutdown(): Promise<void> {
51
+ this.builder = undefined
52
+ }
53
+
54
+ /** Access the underlying silkweave builder once bootstrap has completed. */
55
+ getBuilder(): Silkweave | undefined {
56
+ return this.builder
57
+ }
58
+ }
@@ -0,0 +1,51 @@
1
+ import type { IncomingMessage, ServerResponse } from 'http'
2
+
3
+ export type NodeMiddleware = (
4
+ req: IncomingMessage,
5
+ res: ServerResponse,
6
+ next?: (err?: unknown) => void
7
+ ) => unknown
8
+
9
+ /**
10
+ * A mountable middleware slot that delegates to a settable handler. Used to
11
+ * reserve a path on Nest's HTTP server *before* its 404 catch-all is
12
+ * installed, then populate the real handler later in `OnApplicationBootstrap`.
13
+ *
14
+ * Before `set()` is called, the slot responds with HTTP 503.
15
+ */
16
+ export interface MiddlewareSlot {
17
+ middleware: NodeMiddleware
18
+ set(handler: NodeMiddleware): void
19
+ }
20
+
21
+ export function createMiddlewareSlot(label: string): MiddlewareSlot {
22
+ let handler: NodeMiddleware = (_req, res) => {
23
+ res.statusCode = 503
24
+ res.setHeader('Content-Type', 'application/json')
25
+ res.end(JSON.stringify({ error: 'not_ready', message: `${label} adapter has not started yet` }))
26
+ }
27
+ return {
28
+ middleware: (req, res, next) => handler(req, res, next),
29
+ set: (h) => { handler = h }
30
+ }
31
+ }
32
+
33
+ interface HttpAdapterUseLike {
34
+ use(path: string, handler: NodeMiddleware): unknown
35
+ }
36
+
37
+ /**
38
+ * Mount a middleware slot at the given base path on Nest's HTTP adapter and
39
+ * return the slot's `set()` callback. The slot middleware is installed
40
+ * synchronously so it sits in the Express stack before Nest's later-installed
41
+ * 404 catch-all.
42
+ */
43
+ export function reserveSlot(
44
+ httpAdapter: HttpAdapterUseLike,
45
+ basePath: string,
46
+ label: string
47
+ ): (handler: NodeMiddleware) => void {
48
+ const slot = createMiddlewareSlot(label)
49
+ httpAdapter.use(basePath, slot.middleware)
50
+ return slot.set
51
+ }
@@ -0,0 +1,37 @@
1
+ import type { HttpAdapterHost } from '@nestjs/core'
2
+ import type { AdapterGenerator, SilkweaveOptions } from '@silkweave/core'
3
+
4
+ /**
5
+ * A Silkweave NestJS adapter. Each transport (REST, tRPC, MCP) implements this
6
+ * shape: given a Nest `HttpAdapterHost`, it (a) immediately mounts a
7
+ * placeholder middleware slot on Nest's running HTTP server during
8
+ * `OnModuleInit` — which is critical because Nest installs its 404 catch-all
9
+ * later in `init()`, before `OnApplicationBootstrap` fires; routes registered
10
+ * after the catch-all are unreachable — and (b) returns a core
11
+ * `AdapterGenerator` whose `start(actions)` populates that slot with the real
12
+ * handler.
13
+ *
14
+ * Adapters mount onto Nest's HTTP server instead of owning their own, so Nest
15
+ * middleware, lifecycle hooks, and request scoping remain coherent.
16
+ */
17
+ export interface NestSilkweaveAdapter {
18
+ /** Adapter discriminator — set on the silkweave context as `ctx.get('adapter')`. */
19
+ readonly name: 'rest' | 'trpc' | 'mcp'
20
+ /**
21
+ * Reserve the adapter's route prefix on the Nest HTTP server *now* (before
22
+ * Nest's 404 catch-all is installed) and return the core `AdapterGenerator`
23
+ * that will populate the slot during `silkweave().start()`.
24
+ */
25
+ install(host: HttpAdapterHost): AdapterGenerator
26
+ }
27
+
28
+ export interface SilkweaveModuleOptions {
29
+ /** Identity for the silkweave instance — surfaced to MCP clients, OpenAPI, etc. */
30
+ silkweave: SilkweaveOptions
31
+ /** Adapters to mount. Examples: `rest()`, `trpc()`, `mcp()`. */
32
+ adapters: NestSilkweaveAdapter[]
33
+ /** Initial context keys (equivalent to chaining `.set(key, value)` on the builder). */
34
+ context?: Record<string, unknown>
35
+ }
36
+
37
+ export const SILKWEAVE_MODULE_OPTIONS = '__silkweave_module_options__'
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "build",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "allowSyntheticDefaultImports": true,
14
+ "resolveJsonModule": true,
15
+ "composite": true,
16
+ "declaration": true,
17
+ "declarationMap": true,
18
+ "sourceMap": true,
19
+ "experimentalDecorators": true,
20
+ "emitDecoratorMetadata": true,
21
+ "useDefineForClassFields": false
22
+ },
23
+ "include": ["src"],
24
+ "exclude": ["node_modules", "build"]
25
+ }