@silkweave/nestjs 1.11.0 → 2.0.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/build/index.mjs CHANGED
@@ -1,18 +1,12 @@
1
+ import { createRequire } from "node:module";
1
2
  import { authMiddleware, mcpCors, mcpTransport, oauthRoutes, protectedResourceMetadata, sideloadResource } from "@silkweave/mcp";
2
3
  import express from "express";
3
- import { ForbiddenException, HttpException, Inject, Injectable, Module, SetMetadata } from "@nestjs/common";
4
- import { validateToken } from "@silkweave/auth";
5
- import { SilkweaveError, createAction, createContext } from "@silkweave/core";
6
- import { buildLogLevels } from "@silkweave/logger";
7
- import { z } from "zod/v4";
8
- import { buildRouter, createActionLogger, resolveAuth } from "@silkweave/trpc";
9
- import { createExpressMiddleware } from "@trpc/server/adapters/express";
10
- import { renderTypegen } from "@silkweave/typegen";
11
- import { mkdir, writeFile } from "node:fs/promises";
12
- import { dirname, resolve } from "node:path";
4
+ import { ForbiddenException, Inject, Injectable, Module, SetMetadata } from "@nestjs/common";
13
5
  import { DiscoveryModule, DiscoveryService, HttpAdapterHost, MetadataScanner, ModuleRef, Reflector } from "@nestjs/core";
14
- import { kebabCase } from "change-case";
15
- import { GUARDS_METADATA } from "@nestjs/common/constants.js";
6
+ import { createAction, createContext } from "@silkweave/core";
7
+ import { z } from "zod/v4";
8
+ import { GUARDS_METADATA, METHOD_METADATA, PATH_METADATA, ROUTE_ARGS_METADATA } from "@nestjs/common/constants.js";
9
+ import { join } from "node:path";
16
10
  //#region \0rolldown/runtime.js
17
11
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
18
12
  //#endregion
@@ -72,296 +66,46 @@ function mcp(options = {}) {
72
66
  };
73
67
  }
74
68
  //#endregion
75
- //#region src/adapter/rest.ts
76
- const CONSOLE_LEVEL_MAP = {
77
- emergency: "error",
78
- alert: "error",
79
- critical: "error",
80
- error: "error",
81
- warning: "warn",
82
- notice: "info",
83
- info: "info",
84
- debug: "log"
85
- };
86
- function createRestLogger() {
87
- return {
88
- ...buildLogLevels((level, data) => {
89
- console[CONSOLE_LEVEL_MAP[level]](data);
90
- }),
91
- progress: () => {}
92
- };
93
- }
94
- function actionNameToPath(name) {
95
- return `/${name.replace(/\./g, "/")}`;
96
- }
97
- async function readJsonBody(req) {
98
- return new Promise((resolve, reject) => {
99
- const chunks = [];
100
- req.on("data", (c) => chunks.push(c));
101
- req.on("end", () => {
102
- if (chunks.length === 0) return resolve({});
103
- try {
104
- resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
105
- } catch (err) {
106
- reject(err instanceof Error ? err : /* @__PURE__ */ new Error("Unable to parse JSON body"));
107
- }
108
- });
109
- req.on("error", reject);
110
- });
111
- }
112
- function parseQuery(url) {
113
- if (!url) return {};
114
- const qIndex = url.indexOf("?");
115
- if (qIndex === -1) return {};
116
- const params = {};
117
- const search = new URLSearchParams(url.slice(qIndex + 1));
118
- for (const [k, v] of search.entries()) params[k] = v;
119
- return params;
120
- }
121
- function sendJson(res, body, status) {
122
- if (res.headersSent) return;
123
- res.statusCode = status;
124
- res.setHeader("Content-Type", "application/json");
125
- res.end(JSON.stringify(body));
126
- }
127
- function handleRestError(err, res) {
128
- if (err instanceof z.ZodError) {
129
- sendJson(res, {
130
- error: "validation_error",
131
- issues: err.issues
132
- }, 400);
133
- return;
134
- }
135
- if (err instanceof SilkweaveError) {
136
- sendJson(res, {
137
- error: err.code,
138
- message: err.message
139
- }, err.statusCode);
140
- return;
141
- }
142
- if (err instanceof HttpException) {
143
- const body = err.getResponse();
144
- sendJson(res, typeof body === "string" ? {
145
- error: err.name,
146
- message: body
147
- } : body, err.getStatus());
148
- return;
149
- }
150
- console.error(err);
151
- sendJson(res, {
152
- error: "internal",
153
- message: "Internal error"
154
- }, 500);
155
- }
156
- function createActionHandler(action, context, auth, logger) {
157
- return async (req, res) => {
158
- try {
159
- const reqLike = req;
160
- let authInfo;
161
- if (auth) {
162
- const result = await validateToken(typeof reqLike.headers.authorization === "string" ? reqLike.headers.authorization : void 0, auth, context.fork({ request: req }));
163
- if (result.error) {
164
- for (const [k, v] of Object.entries(result.error.headers)) res.setHeader(k, v);
165
- sendJson(res, result.error.body, result.error.statusCode);
166
- return;
167
- }
168
- authInfo = result.auth;
169
- }
170
- const raw = action.kind === "query" ? reqLike.query ?? parseQuery(req.url) : reqLike.body ?? await readJsonBody(req);
171
- const input = action.input.parse(raw);
172
- sendJson(res, await action.run(input, context.fork({
173
- logger,
174
- request: req,
175
- response: res,
176
- ...authInfo ? { auth: authInfo } : {}
177
- })), 200);
178
- } catch (err) {
179
- handleRestError(err, res);
180
- }
181
- };
182
- }
183
- /**
184
- * REST adapter for `@silkweave/nestjs`. Registers each discovered `@Action`
185
- * as an individual route on Nest's HTTP adapter:
186
- *
187
- * - `kind: 'query'` → `GET ${basePath}/${actionName-with-slashes}` (input from query string)
188
- * - `kind: 'mutation'` → `POST ${basePath}/${actionName-with-slashes}` (input from JSON body)
189
- *
190
- * Routes show up in Nest's `RoutesResolver` logger and are eligible for
191
- * `@nestjs/swagger` scanning. Works on `@nestjs/platform-express` out of the
192
- * box. For `@nestjs/platform-fastify`, register `@fastify/express` first.
193
- */
194
- function rest(options = {}) {
195
- return {
196
- name: "rest",
197
- register({ httpAdapter, baseContext, actions }) {
198
- const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
199
- const logger = createRestLogger();
200
- for (const action of actions) {
201
- const path = `${basePath}${actionNameToPath(action.name)}`;
202
- const method = action.kind === "query" ? "get" : "post";
203
- const handler = createActionHandler(action, baseContext, options.auth, logger);
204
- httpAdapter[method](path, handler);
205
- }
206
- }
207
- };
208
- }
209
- //#endregion
210
- //#region src/adapter/trpc.ts
211
- /**
212
- * tRPC adapter for `@silkweave/nestjs`. Builds a tRPC router from discovered
213
- * `@Action` methods and mounts the resulting express middleware at
214
- * `basePath` on Nest's HTTP adapter.
215
- *
216
- * Action names with dots (e.g. `users.list` from `@Actions('users')`) collapse
217
- * to camelCase procedure keys (`usersList`).
218
- *
219
- * Works on `@nestjs/platform-express`. On `@nestjs/platform-fastify`, register
220
- * `@fastify/express` first so Nest can serve Express-style middleware.
221
- */
222
- function trpc(options = {}) {
223
- return {
224
- name: "trpc",
225
- register({ httpAdapter, baseContext, actions }) {
226
- const basePath = (options.basePath ?? "/trpc").replace(/\/$/, "") || "/";
227
- const router = buildRouter(actions);
228
- const logger = createActionLogger();
229
- const createContext = async (opts) => {
230
- const resolved = await resolveAuth(options.auth, opts.req.headers.authorization, baseContext.fork({ request: opts.req }));
231
- if (resolved.kind === "error") {
232
- for (const [key, value] of Object.entries(resolved.error.headers)) opts.res.setHeader(key, value);
233
- opts.res.statusCode = resolved.error.statusCode;
234
- opts.res.setHeader("Content-Type", "application/json");
235
- opts.res.end(JSON.stringify(resolved.error.body));
236
- throw new Error("Unauthorized");
237
- }
238
- return { silkweaveContext: baseContext.fork({
239
- logger,
240
- request: opts.req,
241
- response: opts.res,
242
- ...resolved.authInfo ? { auth: resolved.authInfo } : {}
243
- }) };
244
- };
245
- const middleware = createExpressMiddleware({
246
- router,
247
- createContext
248
- });
249
- httpAdapter.use(basePath, middleware);
250
- }
251
- };
252
- }
253
- //#endregion
254
- //#region src/adapter/typegen.ts
255
- /**
256
- * Typegen adapter for `@silkweave/nestjs`. Discovers every `@Action`-decorated
257
- * method (regardless of `transports` filtering) and writes a single `.d.ts`
258
- * file with REST input/output interfaces and/or a tRPC `AppRouter` type alias.
259
- *
260
- * Designed for the monorepo pattern where the server (this app) is the source
261
- * of truth for types and consumer apps import from `path` - e.g.
262
- * `path: '../app/src/types/silkweave.ts'`.
263
- */
264
- function typegen(options) {
265
- const enabled = options.enabled ?? process.env["NODE_ENV"] !== "production";
266
- return {
267
- name: "typegen",
268
- allActions: true,
269
- register({ actions }) {
270
- if (!enabled) return;
271
- const output = renderTypegen(actions, options.format ?? "all");
272
- const target = resolve(options.path);
273
- (async () => {
274
- try {
275
- await mkdir(dirname(target), { recursive: true });
276
- await writeFile(target, output, "utf-8");
277
- console.info(`@silkweave/nestjs typegen: wrote ${actions.length} action types to ${target}`);
278
- } catch (err) {
279
- console.error("@silkweave/nestjs typegen:", err);
280
- }
281
- })();
282
- }
283
- };
284
- }
285
- //#endregion
286
69
  //#region src/lib/metadata.ts
287
- const ACTION_METADATA = "__silkweave_action__";
288
- const ACTIONS_METADATA = "__silkweave_actions__";
289
- const ACTION_RESPONSE_KEY = "__silkweave_response__";
70
+ /** Reflect-metadata key carrying `@Mcp` options on a controller method. */
71
+ const MCP_METADATA = "__silkweave_mcp__";
290
72
  //#endregion
291
- //#region src/decorator/action.ts
73
+ //#region src/decorator/mcp.ts
292
74
  /**
293
- * Method decorator that registers a Silkweave action.
75
+ * Method decorator that exposes an existing NestJS controller route as an MCP
76
+ * tool. It is **additive** - the route keeps serving HTTP exactly as before;
77
+ * `@Mcp()` just opts the method into MCP discovery.
294
78
  *
295
- * The decorated method becomes an Action and is exposed via every adapter
296
- * configured on `SilkweaveModule` (REST/tRPC/MCP), unless `transports` is
297
- * provided to restrict it.
79
+ * The tool's name, description, and input schema are reflected from the
80
+ * method's own metadata:
81
+ * - **fields** from the parameter decorators (`@Param`/`@Query`/`@Body`) - a
82
+ * `@Param('id')` becomes an `id` field; a whole-DTO `@Body() dto: CreateDto`
83
+ * is flattened to its properties,
84
+ * - **types/constraints/descriptions** from `@nestjs/swagger`
85
+ * (`@ApiParam`/`@ApiQuery`/`@ApiProperty`/`@ApiOperation`) and, when present,
86
+ * `class-validator` decorators on the DTOs,
87
+ * - optionally refined by an OpenAPI document passed to `SilkweaveModule`.
298
88
  *
299
- * The method receives `(input, context)` where `input` is the parsed Zod input
300
- * and `context` is the `SilkweaveContext` (with `logger`, `request`, optional
301
- * `auth`). The class instance is a normal Nest provider, so other services can
302
- * be injected via the constructor.
89
+ * On a tool call the input is split back into the method's positional arguments
90
+ * and the method is invoked directly (with `@UseGuards` guards applied first).
303
91
  *
304
92
  * @example
305
93
  * ```ts
306
- * @Injectable()
307
- * @Actions('users')
308
- * export class UserActions {
309
- * constructor(private db: DbService) {}
310
- *
311
- * @Action({
312
- * description: 'List users',
313
- * input: z.object({ limit: z.number().optional() }),
314
- * kind: 'query'
315
- * })
316
- * list(input: { limit?: number }, ctx: SilkweaveContext) {
317
- * return this.db.listUsers(input.limit)
94
+ * @Controller('sessions/:sessionId/channels')
95
+ * export class ChannelsController {
96
+ * @Get(':channelId')
97
+ * @ApiOperation({ summary: 'Get a specific channel by ID' })
98
+ * @ApiParam({ name: 'sessionId', description: 'Session ID' })
99
+ * @ApiParam({ name: 'channelId', description: 'Channel ID' })
100
+ * @Mcp()
101
+ * findOne(@Param('sessionId') sessionId: string, @Param('channelId') channelId: string) {
102
+ * return this.service.get(sessionId, channelId)
318
103
  * }
319
104
  * }
320
105
  * ```
321
106
  */
322
- function Action(options) {
323
- return SetMetadata(ACTION_METADATA, options);
324
- }
325
- //#endregion
326
- //#region src/decorator/actions.ts
327
- /**
328
- * Class decorator that groups a provider's `@Action` methods under a common
329
- * prefix. The prefix is joined to each method's action name with a dot
330
- * (e.g. `@Actions('users')` + method `list` → action name `users.list`).
331
- *
332
- * The class itself remains a normal Nest provider - add `@Injectable()`
333
- * separately so it can be resolved by the DI container.
334
- *
335
- * Accepts either a prefix string (shorthand) or a full options object:
336
- * ```ts
337
- * @Actions('users')
338
- * @Actions({ prefix: 'users', transports: ['rest', 'trpc'] })
339
- * ```
340
- */
341
- function Actions(prefixOrOptions = {}) {
342
- return SetMetadata(ACTIONS_METADATA, typeof prefixOrOptions === "string" ? { prefix: prefixOrOptions } : prefixOrOptions);
343
- }
344
- //#endregion
345
- //#region src/lib/filter.ts
346
- /**
347
- * Compile a `transports` allowlist + optional user `isEnabled` into a single
348
- * `(ctx) => boolean` callback compatible with `Action.isEnabled`.
349
- *
350
- * - If `transports` is omitted, the action runs on every adapter.
351
- * - If `transports` is set, the action is gated on `ctx.get<string>('adapter')`
352
- * matching one of the listed transports. Each adapter in `@silkweave/nestjs`
353
- * forks its context with `{ adapter: 'rest' | 'trpc' | 'mcp' }`.
354
- * - If both `transports` and `userIsEnabled` are set, they are AND-combined.
355
- */
356
- function buildIsEnabled(transports, userIsEnabled) {
357
- if (!transports && !userIsEnabled) return;
358
- return (ctx) => {
359
- if (transports) {
360
- const adapter = ctx.getOptional("adapter");
361
- if (adapter && !transports.includes(adapter)) return false;
362
- }
363
- return userIsEnabled ? userIsEnabled(ctx) : true;
364
- };
107
+ function Mcp(options = {}) {
108
+ return SetMetadata(MCP_METADATA, options);
365
109
  }
366
110
  //#endregion
367
111
  //#region ../../node_modules/.pnpm/rxjs@7.8.2/node_modules/rxjs/dist/cjs/internal/util/isFunction.js
@@ -9056,6 +8800,532 @@ async function runGuards(guards, moduleRef, reflector, classRef, handler, reques
9056
8800
  }
9057
8801
  }
9058
8802
  //#endregion
8803
+ //#region src/lib/reflect/params.ts
8804
+ /**
8805
+ * `RouteParamtypes` numeric values from `@nestjs/common` (re-declared to avoid a
8806
+ * value import of an internal enum). These are how `@Param`/`@Query`/`@Body`/...
8807
+ * tag each handler argument in `ROUTE_ARGS_METADATA`.
8808
+ */
8809
+ const PARAMTYPE = {
8810
+ REQUEST: 0,
8811
+ RESPONSE: 1,
8812
+ NEXT: 2,
8813
+ BODY: 3,
8814
+ QUERY: 4,
8815
+ PARAM: 5,
8816
+ HEADERS: 6,
8817
+ SESSION: 7,
8818
+ FILE: 8,
8819
+ FILES: 9,
8820
+ HOST: 10,
8821
+ IP: 11,
8822
+ RAW_BODY: 12
8823
+ };
8824
+ /**
8825
+ * Read and normalise the route-argument metadata for a controller method.
8826
+ * Returns one {@link ParamSlot} per decorated handler argument, sorted by
8827
+ * argument index.
8828
+ */
8829
+ function readParamSlots(classRef, methodName, proto) {
8830
+ const raw = Reflect.getMetadata(ROUTE_ARGS_METADATA, classRef, methodName);
8831
+ if (!raw) return [];
8832
+ const designTypes = Reflect.getMetadata("design:paramtypes", proto, methodName) ?? [];
8833
+ const slots = [];
8834
+ for (const key of Object.keys(raw)) {
8835
+ const entry = raw[key];
8836
+ const paramtype = Number(key.split(":")[0]);
8837
+ if (Number.isNaN(paramtype)) continue;
8838
+ slots.push({
8839
+ paramtype,
8840
+ index: entry.index,
8841
+ data: typeof entry.data === "string" ? entry.data : void 0,
8842
+ pipes: entry.pipes ?? [],
8843
+ designType: designTypes[entry.index]
8844
+ });
8845
+ }
8846
+ return slots.sort((a, b) => a.index - b.index);
8847
+ }
8848
+ //#endregion
8849
+ //#region src/lib/rebind.ts
8850
+ function newInstance(P) {
8851
+ try {
8852
+ return new P();
8853
+ } catch {
8854
+ return null;
8855
+ }
8856
+ }
8857
+ async function applyPipes(value, pipes, metatype, type, data) {
8858
+ if (!pipes || pipes.length === 0) return value;
8859
+ let current = value;
8860
+ for (const p of pipes) {
8861
+ const pipe = typeof p === "function" ? newInstance(p) : p;
8862
+ if (pipe && typeof pipe.transform === "function") current = await pipe.transform(current, {
8863
+ type,
8864
+ metatype,
8865
+ data
8866
+ });
8867
+ }
8868
+ return current;
8869
+ }
8870
+ /** Pick a subset of keys (skipping `undefined`) into a fresh object. */
8871
+ function pickFields(input, fields) {
8872
+ const obj = {};
8873
+ for (const f of fields) if (input[f] !== void 0) obj[f] = input[f];
8874
+ return obj;
8875
+ }
8876
+ /** Resolve a single positional argument from one binding. */
8877
+ async function resolveArg(b, input, request, applyParamPipes) {
8878
+ switch (b.kind) {
8879
+ case "value": {
8880
+ const raw = input[b.field];
8881
+ const type = b.source === "path" ? "param" : b.source;
8882
+ return applyParamPipes ? applyPipes(raw, b.pipes, b.metatype, type, b.field) : raw;
8883
+ }
8884
+ case "object": {
8885
+ const obj = pickFields(input, b.fields);
8886
+ return applyParamPipes ? applyPipes(obj, b.pipes, b.metatype, b.source, void 0) : obj;
8887
+ }
8888
+ case "params": return pickFields(input, b.fields);
8889
+ case "headers": return b.data ? request?.headers?.[b.data.toLowerCase()] : request?.headers ?? {};
8890
+ case "request": return request ?? { headers: {} };
8891
+ case "ip": return request?.ip;
8892
+ case "host": return b.data ? request?.hosts?.[b.data] : request?.hosts;
8893
+ default: return;
8894
+ }
8895
+ }
8896
+ /**
8897
+ * Reconstruct the controller method's positional arguments from the validated
8898
+ * tool `input` (and the request stand-in for `@Req`/`@Headers`/...), then call
8899
+ * it. `applyParamPipes` controls whether parameter-bound pipes run.
8900
+ */
8901
+ async function invokeRebound(method, instance, input, bindings, request, applyParamPipes) {
8902
+ const args = [];
8903
+ for (let i = 0; i < bindings.length; i += 1) args[i] = await resolveArg(bindings[i], input, request, applyParamPipes);
8904
+ return await method.apply(instance, args);
8905
+ }
8906
+ /** Map a non-input parameter slot (`@Req`/`@Headers`/`@Ip`/...) to its runtime binding. */
8907
+ function specialBinding(paramtype, data) {
8908
+ switch (paramtype) {
8909
+ case PARAMTYPE.REQUEST: return { kind: "request" };
8910
+ case PARAMTYPE.RESPONSE:
8911
+ case PARAMTYPE.NEXT: return { kind: "response" };
8912
+ case PARAMTYPE.HEADERS: return {
8913
+ kind: "headers",
8914
+ data
8915
+ };
8916
+ case PARAMTYPE.IP: return { kind: "ip" };
8917
+ case PARAMTYPE.HOST: return {
8918
+ kind: "host",
8919
+ data
8920
+ };
8921
+ case PARAMTYPE.SESSION:
8922
+ case PARAMTYPE.FILE:
8923
+ case PARAMTYPE.FILES:
8924
+ case PARAMTYPE.RAW_BODY: return { kind: "missing" };
8925
+ default: return null;
8926
+ }
8927
+ }
8928
+ //#endregion
8929
+ //#region src/lib/reflect/classValidator.ts
8930
+ let cached;
8931
+ /**
8932
+ * Lazily resolve the optional `class-validator` peer; `null` when not installed.
8933
+ * Resolution is attempted both from this package and from the app's working
8934
+ * directory - the latter is what finds it in a pnpm install where the optional
8935
+ * peer lives in the consumer app rather than alongside `@silkweave/nestjs`. A
8936
+ * pnpm-deduped install shares one physical copy, so the metadata singleton the
8937
+ * app's decorators wrote to is the same one we read.
8938
+ */
8939
+ function loadClassValidator() {
8940
+ if (cached !== void 0) return cached;
8941
+ for (const base of [import.meta.url, join(process.cwd(), "noop.js")]) try {
8942
+ cached = createRequire(base)("class-validator");
8943
+ return cached;
8944
+ } catch {}
8945
+ cached = null;
8946
+ return cached;
8947
+ }
8948
+ /**
8949
+ * Read `class-validator` metadata for a DTO class, grouped by property name.
8950
+ * Returns an empty map when `class-validator` is not installed or the class
8951
+ * carries no validation decorators - so callers degrade gracefully to swagger /
8952
+ * `design:type` reflection.
8953
+ */
8954
+ function classValidatorMetas(dtoType) {
8955
+ const cv = loadClassValidator();
8956
+ if (!cv?.getMetadataStorage) return {};
8957
+ let metas;
8958
+ try {
8959
+ metas = cv.getMetadataStorage().getTargetValidationMetadatas(dtoType, null, false, false);
8960
+ } catch {
8961
+ return {};
8962
+ }
8963
+ const out = {};
8964
+ for (const m of metas) {
8965
+ if (!m.propertyName) continue;
8966
+ (out[m.propertyName] ??= []).push({
8967
+ type: m.type,
8968
+ name: m.name,
8969
+ constraints: m.constraints
8970
+ });
8971
+ }
8972
+ return out;
8973
+ }
8974
+ //#endregion
8975
+ //#region src/lib/reflect/schema.ts
8976
+ /** `@nestjs/swagger` reflect-metadata keys. Read directly so swagger stays an optional peer. */
8977
+ const API_MODEL_PROPERTIES = "swagger/apiModelProperties";
8978
+ const API_MODEL_PROPERTIES_ARRAY = "swagger/apiModelPropertiesArray";
8979
+ /** Strip `undefined` values so a later source never clobbers an earlier one with a hole. */
8980
+ function defined(obj) {
8981
+ const out = {};
8982
+ for (const [k, v] of Object.entries(obj)) if (v !== void 0) out[k] = v;
8983
+ return out;
8984
+ }
8985
+ /** Merge `over` onto `base` - defined keys of `over` win. */
8986
+ function mergeField(base, over) {
8987
+ return {
8988
+ ...base,
8989
+ ...defined(over)
8990
+ };
8991
+ }
8992
+ function enumToZod(values) {
8993
+ const strings = values.filter((v) => typeof v === "string");
8994
+ if (strings.length === values.length && strings.length > 0) return z.enum(strings);
8995
+ const literals = values.map((v) => z.literal(v));
8996
+ if (literals.length === 0) return z.unknown();
8997
+ if (literals.length === 1) return literals[0];
8998
+ return z.union(literals);
8999
+ }
9000
+ function baseToZod(d) {
9001
+ switch (d.type) {
9002
+ case "string": {
9003
+ let s = z.string();
9004
+ if (d.minLength != null) s = s.min(d.minLength);
9005
+ if (d.maxLength != null) s = s.max(d.maxLength);
9006
+ return s;
9007
+ }
9008
+ case "integer":
9009
+ case "number": {
9010
+ let n = z.number();
9011
+ if (d.type === "integer") n = n.int();
9012
+ if (d.min != null) n = n.min(d.min);
9013
+ if (d.max != null) n = n.max(d.max);
9014
+ return n;
9015
+ }
9016
+ case "boolean": return z.boolean();
9017
+ case "array": return z.array(d.items ? fieldToZod({
9018
+ ...d.items,
9019
+ required: true
9020
+ }) : z.unknown());
9021
+ case "object": return z.record(z.string(), z.unknown());
9022
+ default: return z.unknown();
9023
+ }
9024
+ }
9025
+ /** Convert a merged {@link FieldDesc} to a Zod schema. */
9026
+ function fieldToZod(d) {
9027
+ let schema = d.enum?.length ? enumToZod(d.enum) : baseToZod(d);
9028
+ if (d.description) schema = schema.describe(d.description);
9029
+ if (d.default !== void 0) schema = schema.default(d.default);
9030
+ else if (d.required === false) schema = schema.optional();
9031
+ return schema;
9032
+ }
9033
+ /** Normalise a TS-enum object or an array literal to a flat value list. */
9034
+ function normalizeEnum(value) {
9035
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string" || typeof v === "number");
9036
+ if (value && typeof value === "object") return Object.values(value).filter((v) => typeof v === "string" || typeof v === "number");
9037
+ }
9038
+ /** Map a swagger/`design:type` type token (constructor, `[Type]`, or string) to a base type. */
9039
+ function typeTokenToBase(type) {
9040
+ if (type == null) return;
9041
+ if (type === String) return "string";
9042
+ if (type === Number) return "number";
9043
+ if (type === Boolean) return "boolean";
9044
+ if (type === Array) return "array";
9045
+ if (type === Date) return "string";
9046
+ if (Array.isArray(type)) return "array";
9047
+ if (typeof type === "string") {
9048
+ const t = type.toLowerCase();
9049
+ if (t === "string" || t === "number" || t === "integer" || t === "boolean" || t === "array" || t === "object") return t;
9050
+ }
9051
+ }
9052
+ /** From the constructor TypeScript emits via `emitDecoratorMetadata`. */
9053
+ function designTypeToField(ctor) {
9054
+ const type = typeTokenToBase(ctor);
9055
+ const f = {};
9056
+ if (type) f.type = type;
9057
+ return f;
9058
+ }
9059
+ /** From an `@ApiParam`/`@ApiQuery` entry stored under `swagger/apiParameters`. */
9060
+ function swaggerParamToField(p) {
9061
+ const f = {};
9062
+ if (p["description"]) f.description = p["description"];
9063
+ if (typeof p["required"] === "boolean") f.required = p["required"];
9064
+ const type = typeTokenToBase(p["type"]);
9065
+ if (type) f.type = type;
9066
+ if (p["isArray"]) f.type = "array";
9067
+ const en = normalizeEnum(p["enum"]);
9068
+ if (en) f.enum = en;
9069
+ if (p["schema"] && typeof p["schema"] === "object") return mergeField(f, openapiSchemaToField(p["schema"]));
9070
+ return f;
9071
+ }
9072
+ /** From an `@ApiProperty` options object stored under `swagger/apiModelProperties`. */
9073
+ function apiPropertyToField(o) {
9074
+ const f = {};
9075
+ if (o["description"]) f.description = o["description"];
9076
+ if (typeof o["required"] === "boolean") f.required = o["required"];
9077
+ const type = typeTokenToBase(o["type"]);
9078
+ if (type) f.type = type;
9079
+ if (o["isArray"]) {
9080
+ f.items = { type: type && type !== "array" ? type : "unknown" };
9081
+ f.type = "array";
9082
+ }
9083
+ const en = normalizeEnum(o["enum"]);
9084
+ if (en) f.enum = en;
9085
+ if (o["minimum"] != null) f.min = o["minimum"];
9086
+ if (o["maximum"] != null) f.max = o["maximum"];
9087
+ if (o["minLength"] != null) f.minLength = o["minLength"];
9088
+ if (o["maxLength"] != null) f.maxLength = o["maxLength"];
9089
+ if (o["format"]) f.format = o["format"];
9090
+ if (o["default"] !== void 0) f.default = o["default"];
9091
+ return f;
9092
+ }
9093
+ /** `class-validator` decorator type → field base type. */
9094
+ const CV_TYPE = {
9095
+ isString: "string",
9096
+ isInt: "integer",
9097
+ isBoolean: "boolean",
9098
+ isArray: "array"
9099
+ };
9100
+ /** `class-validator` decorator type → numeric-constraint field key. */
9101
+ const CV_NUMERIC = {
9102
+ min: "min",
9103
+ max: "max",
9104
+ minLength: "minLength",
9105
+ maxLength: "maxLength"
9106
+ };
9107
+ /**
9108
+ * Fold one `class-validator` metadata entry into the field descriptor. Built-in
9109
+ * validators record their identity in `name` (`isString`, `minLength`, ...) with
9110
+ * `type: 'customValidation'`; `@IsOptional` uses `name: 'isOptional'`. We key off
9111
+ * `name`, falling back to `type`.
9112
+ */
9113
+ function applyValidationMeta(f, meta) {
9114
+ const key = meta.name ?? meta.type;
9115
+ if (!key) return;
9116
+ if (key in CV_TYPE) {
9117
+ f.type = CV_TYPE[key];
9118
+ return;
9119
+ }
9120
+ const c0 = meta.constraints?.[0];
9121
+ if (key in CV_NUMERIC) {
9122
+ if (typeof c0 === "number") f[CV_NUMERIC[key]] = c0;
9123
+ return;
9124
+ }
9125
+ if (key === "isNumber") {
9126
+ if (!f.type) f.type = "number";
9127
+ return;
9128
+ }
9129
+ if (key === "isEmail") {
9130
+ if (!f.type) f.type = "string";
9131
+ f.format = "email";
9132
+ return;
9133
+ }
9134
+ if (key === "isDate" || key === "isDateString") {
9135
+ f.type = "string";
9136
+ f.format = "date-time";
9137
+ return;
9138
+ }
9139
+ if (key === "isEnum") {
9140
+ const e = normalizeEnum(c0);
9141
+ if (e) f.enum = e;
9142
+ return;
9143
+ }
9144
+ if (key === "isOptional") f.required = false;
9145
+ }
9146
+ /** From an array of `class-validator` validation-metadata entries for one property. */
9147
+ function classValidatorToField(metas) {
9148
+ const f = {};
9149
+ for (const meta of metas) applyValidationMeta(f, meta);
9150
+ return f;
9151
+ }
9152
+ /** From an OpenAPI Schema Object (used by both the doc and `@ApiParam({ schema })`). */
9153
+ function openapiSchemaToField(schema) {
9154
+ const f = {};
9155
+ const type = typeof schema["type"] === "string" ? schema["type"].toLowerCase() : void 0;
9156
+ if (type === "string" || type === "number" || type === "integer" || type === "boolean" || type === "array" || type === "object") f.type = type;
9157
+ if (schema["description"]) f.description = schema["description"];
9158
+ const en = normalizeEnum(schema["enum"]);
9159
+ if (en) f.enum = en;
9160
+ if (schema["minimum"] != null) f.min = schema["minimum"];
9161
+ if (schema["maximum"] != null) f.max = schema["maximum"];
9162
+ if (schema["minLength"] != null) f.minLength = schema["minLength"];
9163
+ if (schema["maxLength"] != null) f.maxLength = schema["maxLength"];
9164
+ if (schema["format"]) f.format = schema["format"];
9165
+ if (schema["default"] !== void 0) f.default = schema["default"];
9166
+ if (schema["items"] && typeof schema["items"] === "object") f.items = openapiSchemaToField(schema["items"]);
9167
+ return f;
9168
+ }
9169
+ /**
9170
+ * Reflect a whole-DTO class (`@Body() dto: CreateDto`) into per-property
9171
+ * {@link FieldDesc}s. Property names are the union of `@ApiProperty` and
9172
+ * `class-validator` decorated fields; each field merges `design:type` (base),
9173
+ * then `class-validator` constraints, then `@ApiProperty` (highest). Properties
9174
+ * default to required unless a source marks them optional.
9175
+ */
9176
+ function reflectDtoFields(dtoType) {
9177
+ const proto = dtoType?.prototype;
9178
+ if (!proto) return {};
9179
+ const cvMetas = classValidatorMetas(dtoType);
9180
+ const names = /* @__PURE__ */ new Set();
9181
+ const swaggerArray = Reflect.getMetadata(API_MODEL_PROPERTIES_ARRAY, proto) ?? [];
9182
+ for (const entry of swaggerArray) names.add(entry.replace(/^:/, ""));
9183
+ for (const name of Object.keys(cvMetas)) names.add(name);
9184
+ const out = {};
9185
+ for (const name of names) {
9186
+ let f = designTypeToField(Reflect.getMetadata("design:type", proto, name));
9187
+ if (cvMetas[name]) f = mergeField(f, classValidatorToField(cvMetas[name]));
9188
+ const apiProp = Reflect.getMetadata(API_MODEL_PROPERTIES, proto, name);
9189
+ if (apiProp) {
9190
+ const fromApi = apiPropertyToField(apiProp);
9191
+ if (fromApi.required === void 0 && apiProp["required"] === void 0) fromApi.required = true;
9192
+ f = mergeField(f, fromApi);
9193
+ }
9194
+ if (f.required === void 0) f.required = true;
9195
+ out[name] = f;
9196
+ }
9197
+ return out;
9198
+ }
9199
+ //#endregion
9200
+ //#region src/lib/reflect/openapi.ts
9201
+ /** Index a document's operations by `${METHOD} ${path}` for fast matching. */
9202
+ function buildOpenApiLookup(doc) {
9203
+ const operations = /* @__PURE__ */ new Map();
9204
+ for (const [path, item] of Object.entries(doc.paths ?? {})) for (const [verb, op] of Object.entries(item ?? {})) operations.set(`${verb.toUpperCase()} ${path}`, op);
9205
+ return {
9206
+ doc,
9207
+ operations
9208
+ };
9209
+ }
9210
+ /** Locate the operation for a route, tolerating a global path prefix on the document side. */
9211
+ function findOperation(lookup, method, openapiPath) {
9212
+ const exact = lookup.operations.get(`${method} ${openapiPath}`);
9213
+ if (exact) return exact;
9214
+ for (const [key, op] of lookup.operations) {
9215
+ const [verb, path] = key.split(" ");
9216
+ if (verb === method && (path.endsWith(openapiPath) || openapiPath.endsWith(path))) return op;
9217
+ }
9218
+ }
9219
+ function resolveRef(doc, schema) {
9220
+ let current = schema;
9221
+ let guard = 0;
9222
+ while (current && typeof current === "object" && typeof current["$ref"] === "string" && guard < 10) {
9223
+ const match = /^#\/components\/schemas\/(.+)$/.exec(current["$ref"]);
9224
+ if (!match) break;
9225
+ current = doc.components?.schemas?.[match[1]];
9226
+ guard += 1;
9227
+ }
9228
+ return current;
9229
+ }
9230
+ /**
9231
+ * Resolve the per-field descriptors for a route from an ingested OpenAPI
9232
+ * document. Merges `parameters` (path/query/header) and the JSON request-body
9233
+ * schema's properties into a single field map. Returns `{}` when the operation
9234
+ * isn't found - callers fall back to decorator reflection.
9235
+ */
9236
+ function openApiFields(lookup, method, openapiPath) {
9237
+ const op = findOperation(lookup, method, openapiPath);
9238
+ if (!op) return {};
9239
+ const out = {};
9240
+ for (const param of op["parameters"] ?? []) {
9241
+ const name = typeof param["name"] === "string" ? param["name"] : void 0;
9242
+ if (!name) continue;
9243
+ const schema = param["schema"] ? resolveRef(lookup.doc, param["schema"]) : void 0;
9244
+ let field = schema ? openapiSchemaToField(schema) : {};
9245
+ if (param["description"]) field = mergeField(field, { description: param["description"] });
9246
+ if (typeof param["required"] === "boolean") field = mergeField(field, { required: param["required"] });
9247
+ out[name] = field;
9248
+ }
9249
+ const bodySchema = resolveRef(lookup.doc, op["requestBody"]?.["content"]?.["application/json"]?.["schema"]);
9250
+ if (bodySchema && typeof bodySchema === "object" && bodySchema["properties"]) {
9251
+ const required = new Set(Array.isArray(bodySchema["required"]) ? bodySchema["required"] : []);
9252
+ for (const [name, propSchema] of Object.entries(bodySchema["properties"])) {
9253
+ const field = openapiSchemaToField(resolveRef(lookup.doc, propSchema));
9254
+ field.required = required.has(name);
9255
+ out[name] = field;
9256
+ }
9257
+ }
9258
+ return out;
9259
+ }
9260
+ //#endregion
9261
+ //#region src/lib/reflect/route.ts
9262
+ /**
9263
+ * `RequestMethod` numeric values (from `@nestjs/common`) mapped to verbs. We
9264
+ * map our own table rather than importing the enum to avoid pulling a value
9265
+ * import for a handful of constants.
9266
+ */
9267
+ const REQUEST_METHOD = {
9268
+ 0: "GET",
9269
+ 1: "POST",
9270
+ 2: "PUT",
9271
+ 3: "DELETE",
9272
+ 4: "PATCH",
9273
+ 6: "OPTIONS",
9274
+ 7: "HEAD"
9275
+ };
9276
+ function normalizeSegment(value) {
9277
+ if (typeof value !== "string") return "";
9278
+ return value.replace(/^\/+/, "").replace(/\/+$/, "");
9279
+ }
9280
+ function joinPath(...segments) {
9281
+ return segments.map(normalizeSegment).filter(Boolean).join("/");
9282
+ }
9283
+ /**
9284
+ * Resolve the composed route (controller prefix + method path), HTTP verb, and
9285
+ * path-param names for a decorated controller method, reading Nest's own
9286
+ * `PATH_METADATA`/`METHOD_METADATA` reflection.
9287
+ */
9288
+ function reflectRoute(classRef, method) {
9289
+ const classPath = Reflect.getMetadata(PATH_METADATA, classRef);
9290
+ const methodPath = Reflect.getMetadata(PATH_METADATA, method);
9291
+ const verbCode = Reflect.getMetadata(METHOD_METADATA, method);
9292
+ const path = joinPath(Array.isArray(classPath) ? classPath[0] ?? "" : classPath ?? "", Array.isArray(methodPath) ? methodPath[0] ?? "" : methodPath ?? "");
9293
+ const httpMethod = REQUEST_METHOD[verbCode ?? 0] ?? "GET";
9294
+ const pathParams = [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((m) => m[1]);
9295
+ return {
9296
+ method: httpMethod,
9297
+ path,
9298
+ openapiPath: `/${path.replace(/:([A-Za-z0-9_]+)/g, "{$1}")}`,
9299
+ pathParams
9300
+ };
9301
+ }
9302
+ //#endregion
9303
+ //#region src/lib/reflect/swagger.ts
9304
+ /** `@nestjs/swagger` reflect-metadata keys (read directly - swagger is an optional peer). */
9305
+ const API_OPERATION = "swagger/apiOperation";
9306
+ const API_PARAMETERS = "swagger/apiParameters";
9307
+ /**
9308
+ * Read operation-level `@nestjs/swagger` metadata off a controller method:
9309
+ * `@ApiOperation` (summary/description) and the `@ApiParam`/`@ApiQuery` array
9310
+ * (each describing one path/query field). Returns empty data when swagger
9311
+ * decorators are absent.
9312
+ */
9313
+ function reflectOperation(method) {
9314
+ const operation = Reflect.getMetadata(API_OPERATION, method);
9315
+ const description = operation ? typeof operation["summary"] === "string" && operation["summary"] ? operation["summary"] : typeof operation["description"] === "string" ? operation["description"] : void 0 : void 0;
9316
+ const parameters = Reflect.getMetadata(API_PARAMETERS, method) ?? [];
9317
+ const params = {};
9318
+ for (const p of parameters) {
9319
+ const name = typeof p["name"] === "string" ? p["name"] : void 0;
9320
+ if (!name) continue;
9321
+ params[name] = swaggerParamToField(p);
9322
+ }
9323
+ return {
9324
+ description,
9325
+ params
9326
+ };
9327
+ }
9328
+ //#endregion
9059
9329
  //#region \0@oxc-project+runtime@0.127.0/helpers/decorateMetadata.js
9060
9330
  function __decorateMetadata(k, v) {
9061
9331
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
@@ -9069,9 +9339,9 @@ function __decorate(decorators, target, key, desc) {
9069
9339
  return c > 3 && r && Object.defineProperty(target, key, r), r;
9070
9340
  }
9071
9341
  //#endregion
9072
- //#region src/lib/discovery.ts
9342
+ //#region src/lib/controllerDiscovery.ts
9073
9343
  var _ref$1, _ref2$1, _ref3, _ref4;
9074
- let ActionDiscovery = class ActionDiscovery {
9344
+ let ControllerDiscovery = class ControllerDiscovery {
9075
9345
  constructor(discovery, scanner, reflector, moduleRef) {
9076
9346
  this.discovery = discovery;
9077
9347
  this.scanner = scanner;
@@ -9079,102 +9349,178 @@ let ActionDiscovery = class ActionDiscovery {
9079
9349
  this.moduleRef = moduleRef;
9080
9350
  }
9081
9351
  /**
9082
- * Walk every Nest provider, find methods annotated with `@Action`, and build
9083
- * a list of core `Action` objects ready to feed into `silkweave().actions()`.
9084
- *
9085
- * Action invocation is wrapped to (a) run `@UseGuards` guards declared on the
9086
- * method or its class against the incoming request (read from
9087
- * `ctx.get('request')`, populated by REST/tRPC and by MCP-over-HTTP from the
9088
- * SDK's `extra.requestInfo`) and (b) bind `this` to the resolved Nest provider
9089
- * so DI-injected dependencies remain available.
9352
+ * Walk every Nest provider/controller, find methods annotated with `@Mcp`,
9353
+ * and build a core `Action` per method whose input schema is reflected from
9354
+ * the route + parameter decorators (+ optional OpenAPI document) and whose
9355
+ * `run` re-binds the validated input back into the method's positional
9356
+ * arguments (with `@UseGuards` guards applied first).
9090
9357
  */
9091
- discover() {
9358
+ discover(openapi) {
9359
+ const lookup = openapi ? buildOpenApiLookup(openapi) : void 0;
9092
9360
  const discovered = [];
9093
- const wrappers = this.discovery.getProviders();
9094
- for (const wrapper of wrappers) {
9361
+ for (const wrapper of this.discovery.getProviders().concat(this.discovery.getControllers())) {
9095
9362
  const { instance } = wrapper;
9096
9363
  if (!instance || typeof instance !== "object") continue;
9097
9364
  const proto = Object.getPrototypeOf(instance);
9098
9365
  if (!proto) continue;
9099
9366
  const classRef = instance.constructor;
9100
- const classMeta = this.reflector.get("__silkweave_actions__", classRef) ?? {};
9101
9367
  for (const methodName of this.scanner.getAllMethodNames(proto)) {
9102
9368
  const method = proto[methodName];
9103
9369
  if (typeof method !== "function") continue;
9104
- const meta = this.reflector.get(ACTION_METADATA, method);
9370
+ const meta = this.reflector.get(MCP_METADATA, method);
9105
9371
  if (!meta) continue;
9106
9372
  discovered.push({
9107
9373
  instance,
9108
9374
  classRef,
9109
9375
  method,
9110
9376
  methodName,
9111
- meta,
9112
- classMeta
9377
+ meta
9113
9378
  });
9114
9379
  }
9115
9380
  }
9116
- return discovered.map((d) => this.toAction(d));
9117
- }
9118
- toAction(d) {
9119
- const baseName = d.meta.name ?? kebabCase(d.methodName);
9120
- const name = d.classMeta.prefix ? `${d.classMeta.prefix}.${baseName}` : baseName;
9121
- const isEnabled = buildIsEnabled(d.meta.transports ?? d.classMeta.transports, d.meta.isEnabled);
9381
+ return discovered.map((d) => this.toAction(d, lookup));
9382
+ }
9383
+ toAction(d, lookup) {
9384
+ const proto = Object.getPrototypeOf(d.instance);
9385
+ const route = reflectRoute(d.classRef, d.method);
9386
+ const slots = readParamSlots(d.classRef, d.methodName, proto);
9387
+ const operation = reflectOperation(d.method);
9388
+ const docFields = lookup ? openApiFields(lookup, route.method, route.openapiPath) : {};
9389
+ const { shape, bindings } = this.buildInput(d, route.pathParams, slots, operation.params, docFields);
9390
+ const base = d.classRef.name.replace(/Controller$/, "");
9391
+ const name = d.meta.name ?? `${base}.${d.methodName}`;
9392
+ const description = d.meta.description ?? operation.description ?? `${d.methodName} (${route.method} /${route.path})`;
9393
+ const applyParamPipes = d.meta.pipes !== "skip";
9122
9394
  const guards = collectGuards(this.reflector, d.classRef, d.method);
9123
- const moduleRef = this.moduleRef;
9124
- const reflector = this.reflector;
9395
+ const { moduleRef, reflector } = this;
9396
+ const classRef = d.classRef;
9397
+ const method = d.method;
9398
+ const instance = d.instance;
9125
9399
  const applyGuards = async (context) => {
9126
9400
  if (guards.length === 0) return;
9127
9401
  const request = context.getOptional("request");
9128
9402
  const response = context.getOptional("response") ?? null;
9129
9403
  const hasRequest = request != null;
9130
- const guardRequest = hasRequest ? request : {
9404
+ await runGuards(guards, moduleRef, reflector, classRef, method, hasRequest ? request : {
9131
9405
  headers: {},
9132
9406
  params: {},
9133
9407
  query: {}
9134
- };
9135
- await runGuards(guards, moduleRef, reflector, d.classRef, d.method, guardRequest, response, hasRequest ? "http" : "rpc");
9136
- };
9137
- if (d.method.constructor?.name === "AsyncGeneratorFunction") {
9138
- if (!d.meta.chunk) throw new Error(`@Action "${name}" is an async generator but has no \`chunk\` schema`);
9139
- const method = d.method;
9140
- const instance = d.instance;
9141
- return createAction({
9142
- name,
9143
- description: d.meta.description,
9144
- input: d.meta.input,
9145
- chunk: d.meta.chunk,
9146
- kind: d.meta.kind ?? "mutation",
9147
- args: d.meta.args,
9148
- isEnabled,
9149
- toolResult: d.meta.toolResult,
9150
- run: async function* (input, context) {
9151
- await applyGuards(context);
9152
- yield* method.call(instance, input, context);
9153
- }
9154
- });
9155
- }
9408
+ }, response, hasRequest ? "http" : "rpc");
9409
+ };
9156
9410
  return createAction({
9157
9411
  name,
9158
- description: d.meta.description,
9159
- input: d.meta.input,
9160
- output: d.meta.output,
9161
- kind: d.meta.kind ?? "mutation",
9162
- args: d.meta.args,
9163
- isEnabled,
9164
- toolResult: d.meta.toolResult,
9412
+ description,
9413
+ input: z.object(shape),
9414
+ isEnabled: (ctx) => ctx.getOptional("adapter") === "mcp",
9165
9415
  run: async (input, context) => {
9166
9416
  await applyGuards(context);
9167
- return await d.method.call(d.instance, input, context);
9417
+ return await invokeRebound(method, instance, input, bindings, context.getOptional("request"), applyParamPipes) ?? {};
9168
9418
  }
9169
9419
  });
9170
9420
  }
9421
+ /** Build the merged Zod input shape and the per-argument re-bind plan. */
9422
+ buildInput(d, pathParams, slots, operationParams, docFields) {
9423
+ const proto = Object.getPrototypeOf(d.instance);
9424
+ const designTypes = Reflect.getMetadata("design:paramtypes", proto, d.methodName) ?? [];
9425
+ const fields = {};
9426
+ const maxIndex = slots.reduce((m, s) => Math.max(m, s.index), -1);
9427
+ const bindings = Array.from({ length: maxIndex + 1 }, () => ({ kind: "missing" }));
9428
+ const addField = (name, desc) => {
9429
+ fields[name] = name in fields ? mergeField(fields[name], desc) : desc;
9430
+ };
9431
+ for (const slot of slots) {
9432
+ const { binding, fields: contributed } = contributeSlot(slot, pathParams, designTypes);
9433
+ bindings[slot.index] = binding;
9434
+ for (const [name, desc] of Object.entries(contributed)) addField(name, desc);
9435
+ }
9436
+ for (const [name, desc] of Object.entries(operationParams)) if (name in fields) fields[name] = mergeField(fields[name], desc);
9437
+ for (const [name, desc] of Object.entries(docFields)) if (name in fields) fields[name] = mergeField(fields[name], desc);
9438
+ const shape = {};
9439
+ for (const [name, desc] of Object.entries(fields)) shape[name] = fieldToZod(desc);
9440
+ Object.assign(shape, d.meta.input ?? {});
9441
+ return {
9442
+ shape,
9443
+ bindings
9444
+ };
9445
+ }
9171
9446
  };
9172
- ActionDiscovery = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [
9447
+ ControllerDiscovery = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [
9173
9448
  typeof (_ref$1 = typeof DiscoveryService !== "undefined" && DiscoveryService) === "function" ? _ref$1 : Object,
9174
9449
  typeof (_ref2$1 = typeof MetadataScanner !== "undefined" && MetadataScanner) === "function" ? _ref2$1 : Object,
9175
9450
  typeof (_ref3 = typeof Reflector !== "undefined" && Reflector) === "function" ? _ref3 : Object,
9176
9451
  typeof (_ref4 = typeof ModuleRef !== "undefined" && ModuleRef) === "function" ? _ref4 : Object
9177
- ])], ActionDiscovery);
9452
+ ])], ControllerDiscovery);
9453
+ function designTypeAt(designTypes, index) {
9454
+ const ctor = designTypes[index];
9455
+ if (ctor === String) return { type: "string" };
9456
+ if (ctor === Number) return { type: "number" };
9457
+ if (ctor === Boolean) return { type: "boolean" };
9458
+ return {};
9459
+ }
9460
+ /** A `@Param('id')` scalar or a bare `@Param()` covering all path params. */
9461
+ function paramContribution(slot, pathParams) {
9462
+ if (slot.data) return {
9463
+ binding: {
9464
+ kind: "value",
9465
+ field: slot.data,
9466
+ source: "path",
9467
+ metatype: slot.designType,
9468
+ pipes: slot.pipes
9469
+ },
9470
+ fields: { [slot.data]: {
9471
+ type: "string",
9472
+ required: true
9473
+ } }
9474
+ };
9475
+ const fields = {};
9476
+ for (const p of pathParams) fields[p] = {
9477
+ type: "string",
9478
+ required: true
9479
+ };
9480
+ return {
9481
+ binding: {
9482
+ kind: "params",
9483
+ fields: pathParams
9484
+ },
9485
+ fields
9486
+ };
9487
+ }
9488
+ /** A `@Query('x')`/`@Body('x')` scalar or a whole-DTO `@Query()`/`@Body()`. */
9489
+ function bodyOrQueryContribution(slot, source, requiredScalar, designTypes) {
9490
+ if (slot.data) return {
9491
+ binding: {
9492
+ kind: "value",
9493
+ field: slot.data,
9494
+ source,
9495
+ metatype: slot.designType,
9496
+ pipes: slot.pipes
9497
+ },
9498
+ fields: { [slot.data]: mergeField(designTypeAt(designTypes, slot.index), { required: requiredScalar }) }
9499
+ };
9500
+ const dtoFields = reflectDtoFields(slot.designType);
9501
+ return {
9502
+ binding: {
9503
+ kind: "object",
9504
+ source,
9505
+ fields: Object.keys(dtoFields),
9506
+ metatype: slot.designType,
9507
+ pipes: slot.pipes
9508
+ },
9509
+ fields: dtoFields
9510
+ };
9511
+ }
9512
+ /** Map one parameter slot to its input-field contribution and re-bind instruction. */
9513
+ function contributeSlot(slot, pathParams, designTypes) {
9514
+ switch (slot.paramtype) {
9515
+ case PARAMTYPE.PARAM: return paramContribution(slot, pathParams);
9516
+ case PARAMTYPE.QUERY: return bodyOrQueryContribution(slot, "query", false, designTypes);
9517
+ case PARAMTYPE.BODY: return bodyOrQueryContribution(slot, "body", true, designTypes);
9518
+ default: return {
9519
+ binding: specialBinding(slot.paramtype, slot.data) ?? { kind: "missing" },
9520
+ fields: {}
9521
+ };
9522
+ }
9523
+ }
9178
9524
  //#endregion
9179
9525
  //#region src/lib/types.ts
9180
9526
  const SILKWEAVE_MODULE_OPTIONS = "__silkweave_module_options__";
@@ -9202,14 +9548,14 @@ let SilkweaveModule = _SilkweaveModule = class SilkweaveModule {
9202
9548
  providers: [{
9203
9549
  provide: SILKWEAVE_MODULE_OPTIONS,
9204
9550
  useValue: options
9205
- }, ActionDiscovery],
9551
+ }, ControllerDiscovery],
9206
9552
  exports: []
9207
9553
  };
9208
9554
  }
9209
9555
  configure(_consumer) {
9210
9556
  const httpAdapter = this.httpAdapterHost.httpAdapter;
9211
9557
  if (!httpAdapter) throw new Error("@silkweave/nestjs: HttpAdapterHost.httpAdapter is not available.");
9212
- const allActions = this.discovery.discover();
9558
+ const allActions = this.discovery.discover(this.options.openapi);
9213
9559
  for (const adapter of this.options.adapters) {
9214
9560
  const baseContext = createContext({
9215
9561
  ...this.options.context ?? {},
@@ -9230,11 +9576,11 @@ SilkweaveModule = _SilkweaveModule = __decorate([
9230
9576
  __decorateParam(0, Inject(SILKWEAVE_MODULE_OPTIONS)),
9231
9577
  __decorateMetadata("design:paramtypes", [
9232
9578
  Object,
9233
- typeof (_ref = typeof ActionDiscovery !== "undefined" && ActionDiscovery) === "function" ? _ref : Object,
9579
+ typeof (_ref = typeof ControllerDiscovery !== "undefined" && ControllerDiscovery) === "function" ? _ref : Object,
9234
9580
  typeof (_ref2 = typeof HttpAdapterHost !== "undefined" && HttpAdapterHost) === "function" ? _ref2 : Object
9235
9581
  ])
9236
9582
  ], SilkweaveModule);
9237
9583
  //#endregion
9238
- export { ACTIONS_METADATA, ACTION_METADATA, ACTION_RESPONSE_KEY, Action, ActionDiscovery, Actions, SILKWEAVE_MODULE_OPTIONS, SilkweaveModule, buildIsEnabled, collectGuards, mcp, rest, runGuards, trpc, typegen };
9584
+ export { ControllerDiscovery, MCP_METADATA, Mcp, SILKWEAVE_MODULE_OPTIONS, SilkweaveModule, apiPropertyToField, buildOpenApiLookup, classValidatorToField, collectGuards, designTypeToField, fieldToZod, invokeRebound, mcp, mergeField, normalizeEnum, openApiFields, openapiSchemaToField, reflectDtoFields, runGuards, specialBinding, swaggerParamToField, typeTokenToBase };
9239
9585
 
9240
9586
  //# sourceMappingURL=index.mjs.map