@sezzlee/sdk-nestjs 0.0.0-stage → 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -2
  3. package/dist/cache.d.ts +18 -0
  4. package/dist/cache.js +55 -0
  5. package/dist/catalog.d.ts +100 -0
  6. package/dist/catalog.js +456 -0
  7. package/dist/decorators.d.ts +172 -0
  8. package/dist/decorators.js +79 -0
  9. package/dist/discovery/body-encoding.d.ts +24 -0
  10. package/dist/discovery/body-encoding.js +211 -0
  11. package/dist/discovery/diagnostics.d.ts +4 -0
  12. package/dist/discovery/diagnostics.js +49 -0
  13. package/dist/discovery/endpoint-discovery.d.ts +52 -0
  14. package/dist/discovery/endpoint-discovery.js +722 -0
  15. package/dist/discovery/type-shape.d.ts +21 -0
  16. package/dist/discovery/type-shape.js +297 -0
  17. package/dist/dispatcher.d.ts +29 -0
  18. package/dist/dispatcher.js +182 -0
  19. package/dist/extension-points.d.ts +34 -0
  20. package/dist/extension-points.js +35 -0
  21. package/dist/families.d.ts +77 -0
  22. package/dist/families.js +157 -0
  23. package/dist/files.d.ts +43 -0
  24. package/dist/files.js +13 -0
  25. package/dist/index.d.ts +48 -0
  26. package/dist/index.js +26 -0
  27. package/dist/invoke-result-mapper.d.ts +16 -0
  28. package/dist/invoke-result-mapper.js +44 -0
  29. package/dist/markers.d.ts +5 -0
  30. package/dist/markers.js +27 -0
  31. package/dist/meta-tools.d.ts +18 -0
  32. package/dist/meta-tools.js +291 -0
  33. package/dist/options-validation.d.ts +17 -0
  34. package/dist/options-validation.js +79 -0
  35. package/dist/options.d.ts +201 -0
  36. package/dist/options.js +151 -0
  37. package/dist/outer-connection.d.ts +18 -0
  38. package/dist/outer-connection.js +23 -0
  39. package/dist/sezzlee.module.d.ts +16 -0
  40. package/dist/sezzlee.module.js +180 -0
  41. package/dist/synthetic-context.d.ts +18 -0
  42. package/dist/synthetic-context.js +131 -0
  43. package/dist/transport/audience.d.ts +2 -0
  44. package/dist/transport/audience.js +21 -0
  45. package/dist/transport/protected-resource-metadata.d.ts +6 -0
  46. package/dist/transport/protected-resource-metadata.js +23 -0
  47. package/dist/transport/streamable-http.d.ts +25 -0
  48. package/dist/transport/streamable-http.js +52 -0
  49. package/dist/visibility/evaluator.d.ts +8 -0
  50. package/dist/visibility/evaluator.js +5 -0
  51. package/dist/visibility/probe.d.ts +25 -0
  52. package/dist/visibility/probe.js +104 -0
  53. package/dist/visibility/provider.d.ts +23 -0
  54. package/dist/visibility/provider.js +118 -0
  55. package/package.json +99 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaan Akın
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,361 @@
1
- # Temporary Holding Version
1
+ # @sezzlee/sdk-nestjs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An MCP layer that embeds into your existing NestJS backend. It exposes your endpoints to agents as
4
+ a search-first tool catalog and replays each call through **your own** pipeline, so your guards,
5
+ pipes and interceptors keep running unchanged. Not a gateway, not duplicated business logic.
6
+
7
+ > Status: `0.1.0`, alpha; breaking changes are possible. [samples/demo-api](samples/demo-api) is
8
+ > the reference wiring.
9
+
10
+ ## Requirements
11
+
12
+ - NestJS 10 or later (`@nestjs/common`, `@nestjs/core`), dev-tested against 11.x
13
+ - `@modelcontextprotocol/server`, `@modelcontextprotocol/node` and `@modelcontextprotocol/express`
14
+ 2.0 or later, `rxjs` 7 and `zod` 4
15
+ - `class-validator` and `class-transformer` are optional; when installed, their decorators shape
16
+ the argument schemas
17
+ - Node 22 or later, Express as the HTTP adapter
18
+
19
+ Peers are peers on purpose: sezzlee uses your Nest and your MCP SDK, not its own copies.
20
+
21
+ ## 1. Install
22
+
23
+ ```bash
24
+ pnpm add @sezzlee/sdk-nestjs
25
+ ```
26
+
27
+ ## 2. Wiring — three pieces
28
+
29
+ The .NET SDK is three method calls. This one is three _pieces_, because a Nest controller is your
30
+ code and a module cannot add a route to it.
31
+
32
+ **The module.** `forRoot` takes a callback that **mutates** an options instance — not an options
33
+ literal. Passing `{ visibility: { tier: "probe" } }` configures nothing.
34
+
35
+ ```ts
36
+ @Module({
37
+ imports: [
38
+ SezzleeModule.forRoot((options) => {
39
+ options.visibility.tier = "probe";
40
+ }),
41
+ ],
42
+ controllers: [OrdersController, McpController],
43
+ })
44
+ export class AppModule {}
45
+ ```
46
+
47
+ `forRootAsync({ imports, inject, useFactory })` is the async form. The module is `@Global()`, so
48
+ its providers are available without importing it again.
49
+
50
+ **The MCP controller.** Copy this; the seven injections are not guessable.
51
+
52
+ ```ts
53
+ @Controller()
54
+ export class McpController {
55
+ constructor(
56
+ private readonly streamableHttp: SezzleeStreamableHttp,
57
+ private readonly catalog: SezzleeCatalog,
58
+ private readonly dispatcher: SezzleeDispatcher,
59
+ private readonly visibility: CallerVisibilityProvider,
60
+ @Inject(extensionTokens.invokeResultMapper)
61
+ private readonly mapper: InvokeResultMapper,
62
+ @Inject(extensionTokens.callerScopeResolver)
63
+ private readonly scopes: CallerScopeResolver,
64
+ @Inject(SEZZLEE_OPTIONS) private readonly options: SezzleeOptions,
65
+ ) {}
66
+
67
+ @All("mcp")
68
+ async handle(@Req() req: Request, @Res() res: Response): Promise<void> {
69
+ await this.streamableHttp.handle(req, res, () => {
70
+ const server = new McpServer({ name: "your-api", version: "0.0.0" });
71
+ registerSezzleeTools(server, {
72
+ catalog: this.catalog,
73
+ dispatcher: this.dispatcher,
74
+ mapper: this.mapper,
75
+ visibility: this.visibility,
76
+ scopes: this.scopes,
77
+ options: this.options,
78
+ });
79
+ return server;
80
+ });
81
+ }
82
+ }
83
+ ```
84
+
85
+ `@All` rather than `@Post`: the default session mode is `stateless`, and the transport wants to
86
+ answer `GET`/`DELETE` with `405` itself instead of letting Nest return `404`.
87
+
88
+ **The bootstrap.**
89
+
90
+ ```ts
91
+ import "reflect-metadata"; // must be the first import in the process
92
+
93
+ const app = await NestFactory.create(AppModule);
94
+ app.useGlobalPipes(new ValidationPipe({ transform: true }));
95
+
96
+ await app.init(); // before any app.get(...) / app.use(...)
97
+ await app.listen(3000);
98
+ ```
99
+
100
+ > **Order is critical.** `reflect-metadata` must be imported first, or the decorators in §3 store
101
+ > nothing and your catalog comes up empty. And `await app.init()` must precede `app.use(...)` that
102
+ > resolves a provider — `app.get(...)` throws before the container is initialized.
103
+
104
+ ## 3. Which endpoints become tools
105
+
106
+ The default is **opt-in**: nothing is exposed until you mark it.
107
+
108
+ ```ts
109
+ @Controller()
110
+ @McpTool()
111
+ export class OrdersController {
112
+ @Get("orders/:id")
113
+ @McpTool({ description: "Fetches one order by id." })
114
+ getOrder(@Param("id", ParseIntPipe) id: number): Order { ... }
115
+ }
116
+ ```
117
+
118
+ NestJS has no equivalent of ASP.NET's description metadata, so `@McpTool({ description })` is how
119
+ an agent gets something to search against. Input schemas come from your `class-validator` DTOs and
120
+ your pipes: `ParseIntPipe` yields an integer, `@Min`/`@Max` become `minimum`/`maximum`.
121
+
122
+ A whole-object query binding is expanded into one argument per member, so the agent sees the
123
+ filters:
124
+
125
+ ```ts
126
+ class ListOrdersQuery {
127
+ @IsString() customerId!: string;
128
+ @IsString() @IsOptional() status?: string;
129
+ @IsInt() @Min(1) @IsOptional() page?: number;
130
+ }
131
+
132
+ @Get("orders")
133
+ list(@Query() query: ListOrdersQuery) { ... }
134
+ // arguments: customerId (required), status, page
135
+ ```
136
+
137
+ The expansion needs a readable type. A member that cannot be expressed as a query parameter — a
138
+ nested object, a dictionary — is omitted with an `unbound_query_object` warning. If **no** member
139
+ can be read (`@Query() q: Record<string, unknown>`, or a DTO with no `class-validator` decorators)
140
+ the endpoint is dropped with `unresolved_query_shape` rather than published as a tool that claims
141
+ to take no filters. Decorate the type, declare `options.schema.typeShape`, or add the code to
142
+ `options.diagnostics.downgrade` to publish it filterless anyway.
143
+
144
+ A member whose name is already claimed by a path parameter or a named `@Query('x')`/`@Headers('x')`
145
+ binding is omitted too, with a `query_member_shadowed` warning: the flat argument object has one
146
+ slot per name and the explicit binding owns it. Rename the member, or bind it by name.
147
+
148
+ ### Routes
149
+
150
+ Routes are composed by Nest's own route factory, so the descriptor's route is the path Nest
151
+ actually serves: `setGlobalPrefix` (with its `exclude` list), `RouterModule.register({ path })`,
152
+ URI versioning, controller path and method path. Two consequences worth knowing:
153
+
154
+ - **A global prefix moves the protected-resource metadata.** Nest applies the prefix to middleware
155
+ paths too, so `/.well-known/oauth-protected-resource/mcp` becomes `/api/.well-known/...` and RFC
156
+ 9728 discovery breaks. When `resourceServer` is set, sezzlee raises a fatal `prm_path_prefixed`
157
+ diagnostic naming the fix: `setGlobalPrefix("api", { exclude: ["/.well-known/oauth-protected-resource/mcp"] })`.
158
+ - **Non-URI versioning is not carried into invocation.** With `HEADER`, `MEDIA_TYPE` or `CUSTOM`
159
+ versioning the version never enters the path, and the synthetic request sezzlee replays carries no
160
+ version header, so dispatch lands on the default version. URI versioning has no such gap.
161
+
162
+ One operation bound to several routes — `@Controller(["orders", "purchases"])`, a legacy path kept
163
+ alongside a new one, or one handler under two URI versions — stays **one tool**: the same code at
164
+ two URLs is one operation, and two identical tools only dilute search. The invoked route is chosen
165
+ deterministically (shortest, then ordinal), the folded routes are reported as `route_folded`, and
166
+ they stay searchable so a query naming the compatibility path still finds the tool.
167
+
168
+ At scale, invert it and exclude the exceptions:
169
+
170
+ ```ts
171
+ SezzleeModule.forRoot((options) => {
172
+ options.selection.default = "include";
173
+ });
174
+ ```
175
+
176
+ Then `@McpIgnore()` on the endpoints or controllers you withhold.
177
+
178
+ For a subtree that is categorically off limits — or one you cannot decorate, such as a generated
179
+ or third-party controller — put the decision in configuration instead:
180
+
181
+ ```ts
182
+ SezzleeModule.forRoot((options) => {
183
+ options.selection.default = "include";
184
+ options.selection.rules = [
185
+ { route: "/admin/**", decision: "exclude" },
186
+ { method: "POST", decision: "exclude" },
187
+ ];
188
+ });
189
+ ```
190
+
191
+ `*` stays inside one path segment and `**` crosses them, so `**` is the catch-all and `*` is not.
192
+ Route matching is case-sensitive and a path parameter is matched as the literal `{id}` the template
193
+ carries; `method` ignores case. Rules rank by how many fields they name, never by declaration
194
+ order, and two equally specific rules that disagree fail the catalog with `ambiguous_selection`
195
+ rather than one quietly winning. An attribute always outranks a rule, so a carve-out inside an
196
+ excluded subtree goes on the endpoint.
197
+
198
+ Visibility is **not** a security mechanism. A tool hidden from the catalog still runs only if your
199
+ backend permits it; enforcement is always in your pipeline at invoke time.
200
+
201
+ ## 3b. Curating what the agent sees
202
+
203
+ Your DTO was written for HTTP clients. `arguments` declares a different agent-facing surface over
204
+ the same endpoint, without touching the DTO.
205
+
206
+ ```ts
207
+ @Get("orders")
208
+ @McpTool({
209
+ description: "Search your orders by keyword.",
210
+ arguments: curate<ListOrdersQuery>({
211
+ customerId: hidden.from("tenant"),
212
+ status: { description: "active | closed" },
213
+ page: { as: "page_number" },
214
+ }),
215
+ })
216
+ list(@Query() query: ListOrdersQuery) { ... }
217
+ ```
218
+
219
+ The agent sends `page_number`; the request still goes out as `?page=`. It never sees `customerId`,
220
+ and sending it is an `unknown_argument` error. The value comes from a provider you register once:
221
+
222
+ ```ts
223
+ options.arguments.provide("tenant", (caller) => caller.claim("tid"));
224
+ ```
225
+
226
+ `hidden.value(x)` writes a constant instead, and `hidden.omit()` sends nothing so your backend's own
227
+ default stands. Rules can also live centrally — `options.arguments.curate(target, rules)` for
228
+ controllers you cannot decorate, `seal` for a rule no decorator may override.
229
+
230
+ **Curation is not enforcement.** Filling `customerId` from a token does not isolate tenants; your
231
+ pipeline still decides. It changes what the agent has to think about, not what it may do.
232
+
233
+ `@McpVariant({ name, description, arguments })` produces several tools from one handler. Both fields
234
+ are required, because one description cannot honestly describe two differently curated tools.
235
+
236
+ `@McpToolFamily({ parameter: "methodId", source: "..." })` publishes a handler that dispatches on a
237
+ parameter as one tool per method. `options.families.provide(name, source)` supplies the members —
238
+ key, name, description, body schema — loaded in `onApplicationBootstrap` and on every `reload()`,
239
+ which now returns a promise; the key is written into the route and never reaches the agent. Full
240
+ guide: the docs site's _How to expose a dispatching endpoint as one tool per method_.
241
+
242
+ Full guide: the docs site's _How to curate the arguments an agent sees_.
243
+
244
+ ## 3c. Telling the agent what a tool returns
245
+
246
+ `load_tool` publishes an `outputSchema` alongside `inputSchema`, so an agent can plan a chain of
247
+ calls without making the first one. Nest has to be told what a handler returns: TypeScript erases
248
+ generics, so an `async` handler reports `Promise` at runtime and a collection reports `Array`.
249
+
250
+ ```ts
251
+ @Get("orders/:id")
252
+ @McpTool({
253
+ description: "Fetches one order by id.",
254
+ responses: { 200: OrderResponse, 404: {} },
255
+ })
256
+ getOrder(@Param("id", ParseIntPipe) id: number): OrderResponse { ... }
257
+ ```
258
+
259
+ Per status code: a DTO class, `[OrderResponse]` for a collection of it, `{ schema }` for a shape no
260
+ class can express, or `{}` for a status with no body. A DTO needs class-validator decorators, the
261
+ same as a request DTO.
262
+
263
+ If you already write `@ApiOkResponse({ type: OrderResponse })`, it is read as a fallback, and a sync
264
+ handler's plain class return type is read as a last resort. The declaration wins over both.
265
+
266
+ Only one status becomes the schema — `200`, `201`, `202`, `204` in that order, then the lowest
267
+ remaining `2xx`. A non-object root is wrapped under `result`, because MCP requires an object. Unlike
268
+ the input side, read-only members are kept: they are what a response is made of.
269
+
270
+ Full guide: the docs site's _How to tell the agent what a tool returns_.
271
+
272
+ ## 4. Visibility and guards
273
+
274
+ This is the SDK's defining behaviour, and the thing most likely to surprise you.
275
+
276
+ sezzlee asks every guard on an endpoint — global, controller and method — for a declaration, by
277
+ looking for a `describeVisibility()` method. **If any guard lacks it, the endpoint is marked
278
+ imperative and its visibility is `unknown` forever.**
279
+
280
+ ```ts
281
+ @Injectable()
282
+ export class OrdersReadGuard implements CanActivate {
283
+ canActivate(): boolean {
284
+ /* your real check, unchanged */ return true;
285
+ }
286
+
287
+ describeVisibility(): { anonymous: "no"; policies: string[] } {
288
+ return { anonymous: "no", policies: ["OrdersRead"] };
289
+ }
290
+ }
291
+ ```
292
+
293
+ `unknown` is not a denial: with the default `visibility.onUnknown: "show"` the tool still appears,
294
+ flagged `authUncertain`. When the guards cannot declare themselves, `visibility.tier = "probe"`
295
+ resolves them by really running them — at a cost. The full recipe, including
296
+ `visibility.probeValues`, is in the docs site's
297
+ _How to declare visibility for a NestJS guard_.
298
+
299
+ ## 5. Connecting an MCP client
300
+
301
+ The endpoint speaks Streamable HTTP. `tools/list` returns only three meta-tools:
302
+
303
+ | Tool | Job |
304
+ | -------------- | ------------------------------------------ |
305
+ | `search_tools` | Finds operations by keyword, returns cards |
306
+ | `load_tool` | Returns one operation's full input schema |
307
+ | `invoke_tool` | Calls it |
308
+
309
+ The catalog is not dumped into `tools/list`: on a 700-endpoint backend that drowns the agent's
310
+ context. The agent searches, then loads, then calls.
311
+
312
+ `search_tools` takes `query`, `limit`, `detail` and `tags`. `tags` narrows the answer to operations
313
+ carrying every tag listed, matched whole and insensitive to case and accents; every answer carries
314
+ the vocabulary the caller may see, so the agent reads a tag rather than guessing one. Each operation
315
+ is tagged with its controller name unless you say otherwise:
316
+
317
+ ```ts
318
+ @McpTool({ name: "find_orders", tags: ["billing", "read"] })
319
+ ```
320
+
321
+ A declaration replaces the controller-derived tag rather than adding to it, and `options.tags` sets
322
+ the same thing centrally for controllers you cannot decorate. Tags are search text too, so changing
323
+ them changes ranking — see the how-to on grouping operations.
324
+
325
+ Words an agent may type that your operation's text does not contain go in `searchTerms`
326
+ (`@McpTool({ searchTerms: ["sipariş"] })`, or `options.searchTerms` centrally): indexed like the
327
+ description, never a filter key, never shown to the agent. To rank with your own retriever instead
328
+ of BM25, bind a `ToolRanker` through `forRoot`'s overrides (`{ toolRanker: { useClass: MyRanker } }`);
329
+ `options.search.rankerTimeoutMs` (10000 by default) and `options.search.onRankerFailure`
330
+ (`"fallback"` or `"error"`) govern it. Visibility, the tag filter and the payload budget still apply
331
+ to its answer — see the how-to on plugging in your own search.
332
+
333
+ This repository ships a client:
334
+
335
+ ```bash
336
+ SEZZLEE_BASE_URL=http://127.0.0.1:3000 SEZZLEE_AUTH=token SEZZLEE_USER=alice \
337
+ node sdks/nestjs/samples/agent-client/dist/main.js --scenario smoke --query "create order"
338
+ ```
339
+
340
+ To protect `/mcp`, set `options.resourceServer` and the module installs bearer verification plus
341
+ the RFC 9728 metadata handler for you.
342
+
343
+ ## 6. Troubleshooting
344
+
345
+ | Symptom | Cause | Fix |
346
+ | ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
347
+ | `search_tools` always empty | `selection.default` is `exclude` and nothing is decorated | Add `@McpTool()`, or set `selection.default = "include"` |
348
+ | Everything comes back `authUncertain` | A guard without `describeVisibility()`, or `tier` still declarative | Declare the guards, or set `visibility.tier = "probe"` |
349
+ | Catalog empty, decorators appear to do nothing | `reflect-metadata` not imported first | Make it the first import in `main.ts` |
350
+ | Startup throws resolving a provider | `app.use(...)` before `app.init()` | `await app.init()` first |
351
+ | No PRM document, 401 carries no pointer | `resourceServer` unset | Set `options.resourceServer`; check `/.well-known/oauth-protected-resource/mcp` |
352
+ | `GET /mcp` returns 405 | `transport.sessionMode` is `stateless` (the default) | Expected. Use `POST`, or switch to `stateful` |
353
+ | Host will not boot, fatal diagnostic | `name_collision` / `ambiguous_selection` / `invalid_name` | Read the codes in the log, then `options.diagnostics.downgrade` |
354
+ | A tool is missing and `unresolved_query_shape` was reported | Its `@Query()` type carries no readable members | Decorate the DTO, declare `options.schema.typeShape`, or downgrade the code |
355
+ | PRM 404s under a global prefix | `prm_path_prefixed` — Nest prefixed the `.well-known` path | Add the metadata path to `setGlobalPrefix`'s `exclude` list |
356
+
357
+ ## 7. What's next
358
+
359
+ - The docs site: `pnpm --filter @sezzlee/docs dev` → `http://localhost:5180`
360
+ - [samples/demo-api](samples/demo-api) — the same endpoint matrix as the .NET sample: anonymous,
361
+ identity-only, policy, role, imperative ownership, and a POST with path, query and body
@@ -0,0 +1,18 @@
1
+ import { type CacheTag, type CallerScope, type SezzleeCache } from "@sezzlee/core";
2
+ import { SezzleeOptions, type OuterRequest } from "./options.js";
3
+ export interface CallerScopeResolver {
4
+ resolve(outer: OuterRequest | undefined): CallerScope;
5
+ }
6
+ export declare class CarrierHashCallerScopeResolver implements CallerScopeResolver {
7
+ private readonly options;
8
+ constructor(options: SezzleeOptions);
9
+ resolve(outer: OuterRequest | undefined): CallerScope;
10
+ }
11
+ export declare const SEZZLEE_CACHE_INVALIDATOR: unique symbol;
12
+ export declare class SezzleeCacheInvalidator {
13
+ private readonly cache;
14
+ constructor(cache: SezzleeCache);
15
+ invalidateCaller(scope: CallerScope): Promise<void>;
16
+ invalidateTag(tag: CacheTag): Promise<void>;
17
+ invalidateAll(): Promise<void>;
18
+ }
package/dist/cache.js ADDED
@@ -0,0 +1,55 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable } from "@nestjs/common";
14
+ import { createCallerScope, deriveCallerScopeKey, digestInput, } from "@sezzlee/core";
15
+ import { extensionTokens } from "./extension-points.js";
16
+ import { SEZZLEE_OPTIONS, SezzleeOptions, } from "./options.js";
17
+ let CarrierHashCallerScopeResolver = class CarrierHashCallerScopeResolver {
18
+ options;
19
+ constructor(options) {
20
+ this.options = options;
21
+ }
22
+ resolve(outer) {
23
+ const carriers = [...this.options.identity.carriers];
24
+ const digest = digestInput(carriers, (name) => outer?.headers[name]);
25
+ return createCallerScope(deriveCallerScopeKey(digest));
26
+ }
27
+ };
28
+ CarrierHashCallerScopeResolver = __decorate([
29
+ Injectable(),
30
+ __param(0, Inject(SEZZLEE_OPTIONS)),
31
+ __metadata("design:paramtypes", [SezzleeOptions])
32
+ ], CarrierHashCallerScopeResolver);
33
+ export { CarrierHashCallerScopeResolver };
34
+ export const SEZZLEE_CACHE_INVALIDATOR = Symbol("SEZZLEE_CACHE_INVALIDATOR");
35
+ let SezzleeCacheInvalidator = class SezzleeCacheInvalidator {
36
+ cache;
37
+ constructor(cache) {
38
+ this.cache = cache;
39
+ }
40
+ invalidateCaller(scope) {
41
+ return this.cache.removeScope(scope.key);
42
+ }
43
+ invalidateTag(tag) {
44
+ return this.cache.removeTag(tag);
45
+ }
46
+ invalidateAll() {
47
+ return this.cache.clear();
48
+ }
49
+ };
50
+ SezzleeCacheInvalidator = __decorate([
51
+ Injectable(),
52
+ __param(0, Inject(extensionTokens.cache)),
53
+ __metadata("design:paramtypes", [Object])
54
+ ], SezzleeCacheInvalidator);
55
+ export { SezzleeCacheInvalidator };
@@ -0,0 +1,100 @@
1
+ import { type OnApplicationBootstrap } from "@nestjs/common";
2
+ import { ApplicationConfig, DiscoveryService, HttpAdapterHost, ModulesContainer } from "@nestjs/core";
3
+ import { type ArgumentCuration, type CatalogBuild, type CatalogEntry as CoreCatalogEntry, type EndpointDescriptor, type ToolRanker } from "@sezzlee/core";
4
+ import { type CatalogDiagnostic } from "./discovery/diagnostics.js";
5
+ import { type DiscoveredEndpoint } from "./discovery/endpoint-discovery.js";
6
+ import type { ArgumentRule } from "./decorators.js";
7
+ import { type FamilyResolution } from "./families.js";
8
+ import { type SezzleeOptions } from "./options.js";
9
+ export { cleanTags } from "@sezzlee/core";
10
+ export interface NestSource {
11
+ readonly controller: NewableFunction;
12
+ readonly handlerName: string;
13
+ }
14
+ export type CatalogEntry = CoreCatalogEntry<NestSource>;
15
+ export interface CatalogSnapshot extends CatalogBuild<NestSource> {
16
+ readonly discovered: number;
17
+ }
18
+ type LoweredFamily = Extract<FamilyResolution, {
19
+ kind: "members";
20
+ }>;
21
+ export declare class SezzleeCatalog implements OnApplicationBootstrap {
22
+ private readonly discovery;
23
+ private readonly modules;
24
+ private readonly applicationConfig;
25
+ private readonly options;
26
+ private readonly adapterHost?;
27
+ private readonly toolRanker;
28
+ private snapshot;
29
+ private currentGeneration;
30
+ private readonly listeners;
31
+ private loads;
32
+ private pending;
33
+ constructor(discovery: DiscoveryService, modules: ModulesContainer, applicationConfig: ApplicationConfig, options: SezzleeOptions, adapterHost?: HttpAdapterHost | undefined, toolRanker?: ToolRanker | null);
34
+ get ranker(): ToolRanker | null;
35
+ get generation(): number;
36
+ get current(): CatalogSnapshot;
37
+ onChange(listener: () => void): () => void;
38
+ /**
39
+ * Rebuilds the catalog, loading family members first when any source is registered.
40
+ *
41
+ * Without a source the rebuild happens before this returns, as it always has. With one, reloads
42
+ * run one at a time, and a rebuild that would turn a valid catalog fatal is refused and the
43
+ * current catalog kept ([tool-families.md] §Membership): member data added under a name another
44
+ * tool already has would otherwise take every tool offline.
45
+ *
46
+ * @throws SezzleeCatalogError through the returned promise when the rebuild is refused
47
+ */
48
+ reload(): Promise<void>;
49
+ /**
50
+ * Loads family members once the application's own providers are ready, since a source usually
51
+ * reads from one of them. A catalog built earlier carries `family_not_loaded` and is rebuilt
52
+ * here, which signals `listChanged`.
53
+ */
54
+ onApplicationBootstrap(): Promise<void>;
55
+ private enqueue;
56
+ private commit;
57
+ get diagnostics(): readonly CatalogDiagnostic[];
58
+ find(name: string): CatalogEntry | undefined;
59
+ ensureValid(): void;
60
+ /**
61
+ * Guard: Express 5 defaults `query parser` to `simple`, which does not parse
62
+ * brackets — `?filter[status]=x` then arrives as one literal key and the DTO
63
+ * binds nothing. A tool that composes a filter the backend silently ignores
64
+ * returns an unfiltered result set, which is the failure
65
+ * `unresolved_query_shape` exists to prevent, so this is fatal rather than a
66
+ * warning. Only Express is checked: `getInstance().get` is a route
67
+ * registrar on Fastify, not a settings reader.
68
+ */
69
+ private reportUnparsedBrackets;
70
+ /**
71
+ * Guard: Express parses a body only for the media types a registered parser declares, and an
72
+ * unparsed body reaches the handler as `req.body === undefined` with no error (form-body-probe
73
+ * N6) — a tool the backend answers as if every field were absent. The default parsers are
74
+ * `jsonParser` and `urlencodedParser`, visible by name on the router stack (N7); a text body
75
+ * needs `app.useBodyParser('text')`. Only Express is checked, for `reportUnparsedBrackets`'s
76
+ * reason.
77
+ */
78
+ private lacksBodyParser;
79
+ private build;
80
+ private checkMetadataPath;
81
+ /**
82
+ * Applies the declaration ladder, least specific first, with sealed fields last.
83
+ *
84
+ * Merging is per argument and per field, so a global rule that hides a tenant identifier and a
85
+ * method-level rule that renames a page number both survive. A decorator wins a tie against a
86
+ * central rule of the same specificity: it is nearer the code and that is the reading a
87
+ * maintainer expects.
88
+ */
89
+ private curationFor;
90
+ }
91
+ /**
92
+ * Folds the decorator's declarations into the descriptor.
93
+ *
94
+ * Discovery reports facts; declarations are reduced here, the same way `toolName` and
95
+ * `containerPrefix` are. The curation list arrives resolved because the ladder that produces it
96
+ * needs the host's central rules, which are not a property of the endpoint; a family's members
97
+ * arrive already lowered, because they were loaded from a source rather than declared.
98
+ */
99
+ export declare function declaredDescriptor(endpoint: DiscoveredEndpoint, curation: readonly ArgumentCuration[], tags?: readonly string[], family?: LoweredFamily): EndpointDescriptor;
100
+ export declare function toCuration(rules: Readonly<Record<string, ArgumentRule>>): ArgumentCuration[];