@silkweave/nestjs 1.10.0 → 1.12.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
@@ -2,7 +2,7 @@ import { authMiddleware, mcpCors, mcpTransport, oauthRoutes, protectedResourceMe
2
2
  import express from "express";
3
3
  import { ForbiddenException, HttpException, Inject, Injectable, Module, SetMetadata } from "@nestjs/common";
4
4
  import { validateToken } from "@silkweave/auth";
5
- import { SilkweaveError, createAction, createContext } from "@silkweave/core";
5
+ import { SilkweaveError, actionMethod, createAction, createContext, methodHasBody, pathParamNames, resolveActionInput, validateActionRouting } from "@silkweave/core";
6
6
  import { buildLogLevels } from "@silkweave/logger";
7
7
  import { z } from "zod/v4";
8
8
  import { buildRouter, createActionLogger, resolveAuth } from "@silkweave/trpc";
@@ -11,7 +11,7 @@ import { renderTypegen } from "@silkweave/typegen";
11
11
  import { mkdir, writeFile } from "node:fs/promises";
12
12
  import { dirname, resolve } from "node:path";
13
13
  import { DiscoveryModule, DiscoveryService, HttpAdapterHost, MetadataScanner, ModuleRef, Reflector } from "@nestjs/core";
14
- import { kebabCase } from "change-case";
14
+ import { camelCase, kebabCase } from "change-case";
15
15
  import { GUARDS_METADATA } from "@nestjs/common/constants.js";
16
16
  //#region \0rolldown/runtime.js
17
17
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
@@ -167,7 +167,13 @@ function createActionHandler(action, context, auth, logger) {
167
167
  }
168
168
  authInfo = result.auth;
169
169
  }
170
- const raw = action.kind === "query" ? reqLike.query ?? parseQuery(req.url) : reqLike.body ?? await readJsonBody(req);
170
+ const query = reqLike.query ?? parseQuery(req.url);
171
+ const body = methodHasBody(actionMethod(action)) ? reqLike.body ?? await readJsonBody(req) : void 0;
172
+ const raw = resolveActionInput(action, {
173
+ params: reqLike.params,
174
+ query,
175
+ body
176
+ });
171
177
  const input = action.input.parse(raw);
172
178
  sendJson(res, await action.run(input, context.fork({
173
179
  logger,
@@ -184,22 +190,29 @@ function createActionHandler(action, context, auth, logger) {
184
190
  * REST adapter for `@silkweave/nestjs`. Registers each discovered `@Action`
185
191
  * as an individual route on Nest's HTTP adapter:
186
192
  *
187
- * - `kind: 'query'` `GET ${basePath}/${actionName-with-slashes}` (input from query string)
188
- * - `kind: 'mutation'` `POST ${basePath}/${actionName-with-slashes}` (input from JSON body)
193
+ * - HTTP verb comes from `method`, else `GET` for `kind: 'query'`, else `POST`.
194
+ * - Path comes from `path` (joined to `basePath`, may contain `:param`
195
+ * placeholders), else `${basePath}/${actionName-with-slashes}`.
196
+ * - Input is merged from the request body, the `queryParams` query-string
197
+ * fields, and any `:param` path placeholders (see `resolveActionInput`).
189
198
  *
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.
199
+ * Routes show up in Nest's `RoutesResolver` logger. They are registered
200
+ * directly on the HTTP adapter (not as Nest controllers), so `@nestjs/swagger`'s
201
+ * controller scanner does not see them - use `addSilkweaveActions()` to add them
202
+ * to the OpenAPI document. Works on `@nestjs/platform-express` out of the box.
203
+ * For `@nestjs/platform-fastify`, register `@fastify/express` first.
193
204
  */
194
205
  function rest(options = {}) {
206
+ const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
195
207
  return {
196
208
  name: "rest",
209
+ basePath,
197
210
  register({ httpAdapter, baseContext, actions }) {
198
- const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
199
211
  const logger = createRestLogger();
200
212
  for (const action of actions) {
201
- const path = `${basePath}${actionNameToPath(action.name)}`;
202
- const method = action.kind === "query" ? "get" : "post";
213
+ validateActionRouting(action);
214
+ const path = action.path ? `${basePath}/${action.path.replace(/^\//, "")}` : `${basePath}${actionNameToPath(action.name)}`;
215
+ const method = actionMethod(action).toLowerCase();
203
216
  const handler = createActionHandler(action, baseContext, options.auth, logger);
204
217
  httpAdapter[method](path, handler);
205
218
  }
@@ -9035,16 +9048,21 @@ async function resolveGuard(ref, moduleRef) {
9035
9048
  return ref;
9036
9049
  }
9037
9050
  /**
9038
- * Run the configured guards against an HTTP request. Throws `ForbiddenException`
9051
+ * Run the configured guards against a request. Throws `ForbiddenException`
9039
9052
  * if any guard rejects, mirroring Nest's HTTP request-pipeline behavior.
9040
9053
  *
9041
9054
  * Pass `null` for `response` when running on top of a tRPC or MCP request that
9042
9055
  * doesn't surface a raw response object; guards that introspect the response
9043
9056
  * will receive `null`.
9057
+ *
9058
+ * `contextType` is reflected through `ExecutionContext.getType()` so guards can
9059
+ * branch on the transport. It is `'http'` for REST/tRPC and for MCP-over-HTTP
9060
+ * (where a header-bearing request stand-in is available); transports without any
9061
+ * HTTP request (e.g. MCP stdio) pass `'rpc'`.
9044
9062
  */
9045
- async function runGuards(guards, moduleRef, reflector, classRef, handler, request, response) {
9063
+ async function runGuards(guards, moduleRef, reflector, classRef, handler, request, response, contextType = "http") {
9046
9064
  if (guards.length === 0) return;
9047
- const context = new SilkweaveExecutionContext([request, response], classRef, handler, "http");
9065
+ const context = new SilkweaveExecutionContext([request, response], classRef, handler, contextType);
9048
9066
  for (const ref of guards) {
9049
9067
  const result = (await resolveGuard(ref, moduleRef)).canActivate(context);
9050
9068
  if (!((0, import_cjs.isObservable)(result) ? await (0, import_cjs.lastValueFrom)(result) : await Promise.resolve(result))) throw new ForbiddenException("Forbidden resource");
@@ -9078,9 +9096,10 @@ let ActionDiscovery = class ActionDiscovery {
9078
9096
  * a list of core `Action` objects ready to feed into `silkweave().actions()`.
9079
9097
  *
9080
9098
  * Action invocation is wrapped to (a) run `@UseGuards` guards declared on the
9081
- * method or its class against the incoming HTTP request (read from
9082
- * `ctx.get('request')`) and (b) bind `this` to the resolved Nest provider so
9083
- * DI-injected dependencies remain available.
9099
+ * method or its class against the incoming request (read from
9100
+ * `ctx.get('request')`, populated by REST/tRPC and by MCP-over-HTTP from the
9101
+ * SDK's `extra.requestInfo`) and (b) bind `this` to the resolved Nest provider
9102
+ * so DI-injected dependencies remain available.
9084
9103
  */
9085
9104
  discover() {
9086
9105
  const discovered = [];
@@ -9119,8 +9138,14 @@ let ActionDiscovery = class ActionDiscovery {
9119
9138
  const applyGuards = async (context) => {
9120
9139
  if (guards.length === 0) return;
9121
9140
  const request = context.getOptional("request");
9122
- const response = context.getOptional("response");
9123
- await runGuards(guards, moduleRef, reflector, d.classRef, d.method, request, response);
9141
+ const response = context.getOptional("response") ?? null;
9142
+ const hasRequest = request != null;
9143
+ const guardRequest = hasRequest ? request : {
9144
+ headers: {},
9145
+ params: {},
9146
+ query: {}
9147
+ };
9148
+ await runGuards(guards, moduleRef, reflector, d.classRef, d.method, guardRequest, response, hasRequest ? "http" : "rpc");
9124
9149
  };
9125
9150
  if (d.method.constructor?.name === "AsyncGeneratorFunction") {
9126
9151
  if (!d.meta.chunk) throw new Error(`@Action "${name}" is an async generator but has no \`chunk\` schema`);
@@ -9132,6 +9157,9 @@ let ActionDiscovery = class ActionDiscovery {
9132
9157
  input: d.meta.input,
9133
9158
  chunk: d.meta.chunk,
9134
9159
  kind: d.meta.kind ?? "mutation",
9160
+ method: d.meta.method,
9161
+ path: d.meta.path,
9162
+ queryParams: d.meta.queryParams,
9135
9163
  args: d.meta.args,
9136
9164
  isEnabled,
9137
9165
  toolResult: d.meta.toolResult,
@@ -9147,6 +9175,9 @@ let ActionDiscovery = class ActionDiscovery {
9147
9175
  input: d.meta.input,
9148
9176
  output: d.meta.output,
9149
9177
  kind: d.meta.kind ?? "mutation",
9178
+ method: d.meta.method,
9179
+ path: d.meta.path,
9180
+ queryParams: d.meta.queryParams,
9150
9181
  args: d.meta.args,
9151
9182
  isEnabled,
9152
9183
  toolResult: d.meta.toolResult,
@@ -9164,6 +9195,103 @@ ActionDiscovery = __decorate([Injectable(), __decorateMetadata("design:paramtype
9164
9195
  typeof (_ref4 = typeof ModuleRef !== "undefined" && ModuleRef) === "function" ? _ref4 : Object
9165
9196
  ])], ActionDiscovery);
9166
9197
  //#endregion
9198
+ //#region src/lib/openapi.ts
9199
+ /** Convert a route template's `:param` placeholders to OpenAPI `{param}` form. */
9200
+ function toOpenApiPath(template) {
9201
+ return template.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
9202
+ }
9203
+ /** The OpenAPI path key for an action - mirrors the `rest()` adapter's routing. */
9204
+ function actionRoute(action, basePath) {
9205
+ return toOpenApiPath(`${basePath}/${action.path ? action.path.replace(/^\//, "") : action.name.replace(/\./g, "/")}`.replace(/\/{2,}/g, "/"));
9206
+ }
9207
+ /** `z.toJSONSchema` tags a top-level `$schema`; drop it for a tidy OpenAPI doc. */
9208
+ function toJsonSchema(schema) {
9209
+ const { $schema: _schema, ...rest } = z.toJSONSchema(schema);
9210
+ return rest;
9211
+ }
9212
+ function responseSchema(action) {
9213
+ if (action.output) return toJsonSchema(action.output);
9214
+ if (action.chunk) return {
9215
+ type: "array",
9216
+ items: toJsonSchema(action.chunk)
9217
+ };
9218
+ }
9219
+ /** Split an action's input fields into OpenAPI path/query parameters and a body, matching the `rest()` adapter. */
9220
+ function splitInputSources(action, hasBody) {
9221
+ const json = z.toJSONSchema(action.input);
9222
+ const required = new Set(json.required ?? []);
9223
+ const pathParams = new Set(pathParamNames(action.path));
9224
+ const queryKeys = new Set((action.queryParams ?? []).map(String));
9225
+ const split = {
9226
+ parameters: [],
9227
+ bodyProps: {},
9228
+ bodyRequired: []
9229
+ };
9230
+ for (const [key, schema] of Object.entries(json.properties ?? {})) if (pathParams.has(key)) split.parameters.push({
9231
+ name: key,
9232
+ in: "path",
9233
+ required: true,
9234
+ schema
9235
+ });
9236
+ else if (!hasBody || queryKeys.has(key)) split.parameters.push({
9237
+ name: key,
9238
+ in: "query",
9239
+ required: required.has(key),
9240
+ schema
9241
+ });
9242
+ else {
9243
+ split.bodyProps[key] = schema;
9244
+ if (required.has(key)) split.bodyRequired.push(key);
9245
+ }
9246
+ return split;
9247
+ }
9248
+ /** Build the OpenAPI operation object for a single action. */
9249
+ function buildOperation(action, tag) {
9250
+ const hasBody = methodHasBody(actionMethod(action));
9251
+ const { parameters, bodyProps, bodyRequired } = splitInputSources(action, hasBody);
9252
+ const response = responseSchema(action);
9253
+ const operation = {
9254
+ tags: [tag],
9255
+ summary: action.description,
9256
+ operationId: camelCase(action.name),
9257
+ responses: { 200: {
9258
+ description: "Successful response",
9259
+ ...response ? { content: { "application/json": { schema: response } } } : {}
9260
+ } }
9261
+ };
9262
+ if (parameters.length) operation.parameters = parameters;
9263
+ if (hasBody && Object.keys(bodyProps).length) operation.requestBody = {
9264
+ required: bodyRequired.length > 0,
9265
+ content: { "application/json": { schema: {
9266
+ type: "object",
9267
+ properties: bodyProps,
9268
+ required: bodyRequired
9269
+ } } }
9270
+ };
9271
+ return operation;
9272
+ }
9273
+ /**
9274
+ * Build an OpenAPI `paths` fragment for a set of Silkweave actions, mirroring
9275
+ * exactly how the `rest()` adapter routes them: the same HTTP verb (`method` ??
9276
+ * `kind`), the same `path`/`name`-derived route, and the same path/query/body
9277
+ * field split (`pathParamNames` + `queryParams`). Schemas are inlined from the
9278
+ * Zod input/output via `z.toJSONSchema`; no shared components are emitted.
9279
+ */
9280
+ function buildActionPaths(actions, options = {}) {
9281
+ const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
9282
+ const tag = options.tag ?? "Actions";
9283
+ const paths = {};
9284
+ for (const action of actions) {
9285
+ const method = actionMethod(action).toLowerCase();
9286
+ const route = actionRoute(action, basePath);
9287
+ paths[route] = {
9288
+ ...paths[route] ?? {},
9289
+ [method]: buildOperation(action, tag)
9290
+ };
9291
+ }
9292
+ return paths;
9293
+ }
9294
+ //#endregion
9167
9295
  //#region src/lib/types.ts
9168
9296
  const SILKWEAVE_MODULE_OPTIONS = "__silkweave_module_options__";
9169
9297
  //#endregion
@@ -9223,6 +9351,55 @@ SilkweaveModule = _SilkweaveModule = __decorate([
9223
9351
  ])
9224
9352
  ], SilkweaveModule);
9225
9353
  //#endregion
9226
- export { ACTIONS_METADATA, ACTION_METADATA, ACTION_RESPONSE_KEY, Action, ActionDiscovery, Actions, SILKWEAVE_MODULE_OPTIONS, SilkweaveModule, buildIsEnabled, collectGuards, mcp, rest, runGuards, trpc, typegen };
9354
+ //#region src/lib/swagger.ts
9355
+ /**
9356
+ * Merge every REST-exposed Silkweave `@Action` into a NestJS Swagger
9357
+ * `OpenAPIObject`.
9358
+ *
9359
+ * `@nestjs/swagger` builds its document by scanning **controllers**, but
9360
+ * Silkweave registers action routes directly on the HTTP adapter (so they sit
9361
+ * ahead of controllers in the request pipeline) - which means the scanner never
9362
+ * sees them. This helper closes that gap: it discovers the actions through the
9363
+ * same `ActionDiscovery` provider the `rest()` adapter uses, builds OpenAPI
9364
+ * paths with the same routing logic (`buildActionPaths`), and merges them into
9365
+ * the document. The result stays in sync with the live routes without any
9366
+ * dynamic controllers.
9367
+ *
9368
+ * Call it between `SwaggerModule.createDocument()` and `SwaggerModule.setup()`:
9369
+ *
9370
+ * @example
9371
+ * ```ts
9372
+ * const document = SwaggerModule.createDocument(app, config)
9373
+ * addSilkweaveActions(app, document)
9374
+ * SwaggerModule.setup('api/docs', app, document)
9375
+ * ```
9376
+ */
9377
+ function addSilkweaveActions(app, document, options = {}) {
9378
+ const discovery = app.get(ActionDiscovery);
9379
+ const moduleOptions = app.get(SILKWEAVE_MODULE_OPTIONS);
9380
+ const restAdapter = moduleOptions.adapters.find((adapter) => adapter.name === "rest");
9381
+ const basePath = options.basePath ?? restAdapter?.basePath ?? "/api";
9382
+ const allActions = discovery.discover();
9383
+ const paths = buildActionPaths(options.includeDisabled ? allActions : allActions.filter((action) => isRestEnabled(action, moduleOptions)), {
9384
+ basePath,
9385
+ tag: options.tag
9386
+ });
9387
+ document.paths ??= {};
9388
+ for (const [route, item] of Object.entries(paths)) document.paths[route] = {
9389
+ ...document.paths[route] ?? {},
9390
+ ...item
9391
+ };
9392
+ return document;
9393
+ }
9394
+ /** Whether an action would be registered by the `rest()` adapter (adapter: 'rest'). */
9395
+ function isRestEnabled(action, moduleOptions) {
9396
+ if (!action.isEnabled) return true;
9397
+ return action.isEnabled(createContext({
9398
+ ...moduleOptions.context ?? {},
9399
+ adapter: "rest"
9400
+ }));
9401
+ }
9402
+ //#endregion
9403
+ export { ACTIONS_METADATA, ACTION_METADATA, ACTION_RESPONSE_KEY, Action, ActionDiscovery, Actions, SILKWEAVE_MODULE_OPTIONS, SilkweaveModule, addSilkweaveActions, buildActionPaths, buildIsEnabled, collectGuards, mcp, rest, runGuards, trpc, typegen };
9227
9404
 
9228
9405
  //# sourceMappingURL=index.mjs.map