@sezzlee/sdk-nestjs 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +360 -2
- package/dist/cache.d.ts +18 -0
- package/dist/cache.js +55 -0
- package/dist/catalog.d.ts +100 -0
- package/dist/catalog.js +456 -0
- package/dist/decorators.d.ts +172 -0
- package/dist/decorators.js +79 -0
- package/dist/discovery/body-encoding.d.ts +24 -0
- package/dist/discovery/body-encoding.js +211 -0
- package/dist/discovery/diagnostics.d.ts +4 -0
- package/dist/discovery/diagnostics.js +49 -0
- package/dist/discovery/endpoint-discovery.d.ts +52 -0
- package/dist/discovery/endpoint-discovery.js +722 -0
- package/dist/discovery/type-shape.d.ts +21 -0
- package/dist/discovery/type-shape.js +297 -0
- package/dist/dispatcher.d.ts +29 -0
- package/dist/dispatcher.js +182 -0
- package/dist/extension-points.d.ts +34 -0
- package/dist/extension-points.js +35 -0
- package/dist/families.d.ts +77 -0
- package/dist/families.js +157 -0
- package/dist/files.d.ts +43 -0
- package/dist/files.js +13 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +26 -0
- package/dist/invoke-result-mapper.d.ts +16 -0
- package/dist/invoke-result-mapper.js +44 -0
- package/dist/markers.d.ts +5 -0
- package/dist/markers.js +27 -0
- package/dist/meta-tools.d.ts +18 -0
- package/dist/meta-tools.js +306 -0
- package/dist/options-validation.d.ts +17 -0
- package/dist/options-validation.js +79 -0
- package/dist/options.d.ts +201 -0
- package/dist/options.js +151 -0
- package/dist/outer-connection.d.ts +18 -0
- package/dist/outer-connection.js +23 -0
- package/dist/sezzlee.module.d.ts +16 -0
- package/dist/sezzlee.module.js +180 -0
- package/dist/synthetic-context.d.ts +18 -0
- package/dist/synthetic-context.js +131 -0
- package/dist/transport/audience.d.ts +2 -0
- package/dist/transport/audience.js +21 -0
- package/dist/transport/protected-resource-metadata.d.ts +6 -0
- package/dist/transport/protected-resource-metadata.js +23 -0
- package/dist/transport/streamable-http.d.ts +25 -0
- package/dist/transport/streamable-http.js +52 -0
- package/dist/visibility/evaluator.d.ts +8 -0
- package/dist/visibility/evaluator.js +5 -0
- package/dist/visibility/probe.d.ts +25 -0
- package/dist/visibility/probe.js +104 -0
- package/dist/visibility/provider.d.ts +23 -0
- package/dist/visibility/provider.js +118 -0
- package/package.json +99 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TypeNode, TypeShape } from "@sezzlee/core";
|
|
2
|
+
export interface TypeShapeDiagnostic {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TypeShapeBinderOptions {
|
|
7
|
+
readonly typeShape?: (target: NewableFunction) => TypeShape | TypeNode | undefined;
|
|
8
|
+
readonly report?: (diagnostic: TypeShapeDiagnostic) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare class NestTypeShapeBinder {
|
|
11
|
+
private readonly options;
|
|
12
|
+
private readonly types;
|
|
13
|
+
private readonly keys;
|
|
14
|
+
constructor(options?: TypeShapeBinderOptions);
|
|
15
|
+
bind(target: unknown): TypeShape;
|
|
16
|
+
private node;
|
|
17
|
+
private declare;
|
|
18
|
+
private members;
|
|
19
|
+
private member;
|
|
20
|
+
private fromValidators;
|
|
21
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const load = createRequire(import.meta.url);
|
|
3
|
+
function scalar(kind, format) {
|
|
4
|
+
return format === undefined
|
|
5
|
+
? { kind: "scalar", scalar: kind }
|
|
6
|
+
: { kind: "scalar", scalar: kind, format };
|
|
7
|
+
}
|
|
8
|
+
function unreadable(reason) {
|
|
9
|
+
return { kind: "unknown", reason };
|
|
10
|
+
}
|
|
11
|
+
const byConstructor = new Map([
|
|
12
|
+
[String, () => scalar("string")],
|
|
13
|
+
[Number, () => scalar("number")],
|
|
14
|
+
[Boolean, () => scalar("boolean")],
|
|
15
|
+
[Date, () => scalar("string", "date-time")],
|
|
16
|
+
]);
|
|
17
|
+
export class NestTypeShapeBinder {
|
|
18
|
+
options;
|
|
19
|
+
types = {};
|
|
20
|
+
keys = new Map();
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
bind(target) {
|
|
25
|
+
return { root: this.node(target, undefined), types: this.types };
|
|
26
|
+
}
|
|
27
|
+
node(target, owner) {
|
|
28
|
+
if (typeof target !== "function") {
|
|
29
|
+
return unreadable(owner === undefined
|
|
30
|
+
? "No runtime type is available for this body."
|
|
31
|
+
: `No runtime type is available for member '${owner}'.`);
|
|
32
|
+
}
|
|
33
|
+
const declared = this.options.typeShape?.(target);
|
|
34
|
+
if (declared !== undefined) {
|
|
35
|
+
return "root" in declared ? declared.root : declared;
|
|
36
|
+
}
|
|
37
|
+
const mapped = byConstructor.get(target);
|
|
38
|
+
if (mapped !== undefined) {
|
|
39
|
+
return mapped();
|
|
40
|
+
}
|
|
41
|
+
if (target === Array) {
|
|
42
|
+
return {
|
|
43
|
+
kind: "array",
|
|
44
|
+
items: unreadable("Array element type is erased at runtime; declare @Type(() => X)."),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (target === Object) {
|
|
48
|
+
return unreadable("'Object' carries no readable shape; any JSON value is accepted.");
|
|
49
|
+
}
|
|
50
|
+
return { kind: "ref", ref: this.declare(target) };
|
|
51
|
+
}
|
|
52
|
+
declare(target) {
|
|
53
|
+
const existing = this.keys.get(target);
|
|
54
|
+
if (existing !== undefined) {
|
|
55
|
+
return existing;
|
|
56
|
+
}
|
|
57
|
+
const key = target.name;
|
|
58
|
+
this.keys.set(target, key);
|
|
59
|
+
this.types[key] = { name: target.name, members: [] };
|
|
60
|
+
this.types[key] = { name: target.name, members: this.members(target) };
|
|
61
|
+
return key;
|
|
62
|
+
}
|
|
63
|
+
members(target) {
|
|
64
|
+
const validation = groupValidators(target);
|
|
65
|
+
if (validation.size === 0) {
|
|
66
|
+
this.options.report?.({
|
|
67
|
+
code: "unreadable_shape",
|
|
68
|
+
message: `'${target.name}' carries no class-validator metadata; its shape cannot be read. Decorate its properties, or declare options.schema.typeShape.`,
|
|
69
|
+
});
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return [...validation].map(([property, entries]) => this.member(target, property, entries));
|
|
73
|
+
}
|
|
74
|
+
member(target, property, entries) {
|
|
75
|
+
const nested = nestedType(target, property);
|
|
76
|
+
const reflected = Reflect.getMetadata("design:type", target.prototype, property);
|
|
77
|
+
const inner = nested !== undefined
|
|
78
|
+
? this.node(nested, property)
|
|
79
|
+
: this.fromValidators(entries, reflected, property);
|
|
80
|
+
const each = entries.some((entry) => entry.each === true);
|
|
81
|
+
const type = each ? { kind: "array", items: inner } : inner;
|
|
82
|
+
const constraints = constraintsOf(entries);
|
|
83
|
+
return {
|
|
84
|
+
name: property,
|
|
85
|
+
type,
|
|
86
|
+
required: !entries.some((entry) => entry.type === "conditionalValidation"),
|
|
87
|
+
readOnly: false,
|
|
88
|
+
constructorBound: false,
|
|
89
|
+
...(constraints === undefined ? {} : { constraints }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
fromValidators(entries, reflected, property) {
|
|
93
|
+
const enumFacts = enumFactsOf(entries);
|
|
94
|
+
if (enumFacts !== undefined) {
|
|
95
|
+
return { kind: "enum", enumFacts };
|
|
96
|
+
}
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const mapped = validatorScalar(entry.name);
|
|
99
|
+
if (mapped !== undefined) {
|
|
100
|
+
return mapped;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (reflected !== undefined && reflected !== Array) {
|
|
104
|
+
return this.node(reflected, property);
|
|
105
|
+
}
|
|
106
|
+
return unreadable(`Member '${property}' has no readable type; add a class-validator type decorator or @Type(() => X).`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Reads the members a `@IsEnum` or `@IsIn` decorator allows.
|
|
111
|
+
*
|
|
112
|
+
* @returns the allowed values, or `undefined` when the decorator declares none this layer can read.
|
|
113
|
+
*/
|
|
114
|
+
function enumValuesOf(entry) {
|
|
115
|
+
if (entry.name === "isIn") {
|
|
116
|
+
const values = entry.constraints?.[0];
|
|
117
|
+
return Array.isArray(values) ? values : undefined;
|
|
118
|
+
}
|
|
119
|
+
if (entry.name !== "isEnum") {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const declared = entry.constraints?.[1];
|
|
123
|
+
if (Array.isArray(declared)) {
|
|
124
|
+
return declared;
|
|
125
|
+
}
|
|
126
|
+
return memberEntriesOf(entry)?.map(([, value]) => value);
|
|
127
|
+
}
|
|
128
|
+
function memberEntriesOf(entry) {
|
|
129
|
+
const entity = entry.constraints?.[0];
|
|
130
|
+
if (typeof entity !== "object" || entity === null) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return Object.entries(entity).filter(([key]) => Number.isNaN(Number.parseInt(key, 10)));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Derives {@link EnumFacts} from the first readable enum-shaped validator on a member.
|
|
137
|
+
*
|
|
138
|
+
* class-validator hands `@IsEnum` the enum object in `constraints[0]` and the already
|
|
139
|
+
* reverse-mapping-filtered value list in `constraints[1]`; the second is what the schema needs, and
|
|
140
|
+
* recomputing it from the first is only a fallback for a class-validator that predates it.
|
|
141
|
+
*
|
|
142
|
+
* A numeric enum resolves to the **integer** wire form even though `isEnum` also accepts the member
|
|
143
|
+
* name at runtime (it validates against `Object.keys(entity).map(k => entity[k])`, which on a
|
|
144
|
+
* numeric enum contains the names too). Reporting `unresolved` would publish an `anyOf` that invites
|
|
145
|
+
* the agent to send `"Active"`, which passes validation and then compares unequal to `Status.Active`
|
|
146
|
+
* everywhere in the handler. The narrower form is the honest one.
|
|
147
|
+
*
|
|
148
|
+
* @returns the facts, or `undefined` when no member list is readable or a value is not a string or
|
|
149
|
+
* an integer — a float or an object cannot be written as a JSON Schema `enum` of a single type.
|
|
150
|
+
*/
|
|
151
|
+
function enumFactsOf(entries) {
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const values = enumValuesOf(entry);
|
|
154
|
+
if (values === undefined || values.length === 0) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const names = new Set();
|
|
158
|
+
const numbers = new Set();
|
|
159
|
+
let readable = true;
|
|
160
|
+
for (const value of values) {
|
|
161
|
+
if (typeof value === "string") {
|
|
162
|
+
names.add(value);
|
|
163
|
+
}
|
|
164
|
+
else if (typeof value === "number" && Number.isInteger(value)) {
|
|
165
|
+
numbers.add(value);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
readable = false;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!readable) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (names.size > 0 && numbers.size > 0) {
|
|
176
|
+
return {
|
|
177
|
+
wireForm: "unresolved",
|
|
178
|
+
names: [...names],
|
|
179
|
+
numbers: [...numbers],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (numbers.size > 0) {
|
|
183
|
+
return {
|
|
184
|
+
wireForm: "integer",
|
|
185
|
+
names: memberEntriesOf(entry)?.map(([key]) => key) ?? [],
|
|
186
|
+
numbers: [...numbers],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { wireForm: "string", names: [...names], numbers: [] };
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
function validatorScalar(name) {
|
|
194
|
+
switch (name) {
|
|
195
|
+
case "isString":
|
|
196
|
+
case "isEmail":
|
|
197
|
+
case "isUrl":
|
|
198
|
+
return scalar("string");
|
|
199
|
+
case "isInt":
|
|
200
|
+
return scalar("integer");
|
|
201
|
+
case "isNumber":
|
|
202
|
+
return scalar("number");
|
|
203
|
+
case "isBoolean":
|
|
204
|
+
return scalar("boolean");
|
|
205
|
+
case "isUuid":
|
|
206
|
+
return scalar("string", "uuid");
|
|
207
|
+
case "isDate":
|
|
208
|
+
case "isDateString":
|
|
209
|
+
return scalar("string", "date-time");
|
|
210
|
+
default:
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function constraintsOf(entries) {
|
|
215
|
+
const found = {};
|
|
216
|
+
const number = (value) => typeof value === "number" ? value : undefined;
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const first = entry.constraints?.[0];
|
|
219
|
+
switch (entry.name) {
|
|
220
|
+
case "minLength":
|
|
221
|
+
case "arrayMinSize":
|
|
222
|
+
found["minSize"] = number(first) ?? found["minSize"];
|
|
223
|
+
break;
|
|
224
|
+
case "maxLength":
|
|
225
|
+
case "arrayMaxSize":
|
|
226
|
+
found["maxSize"] = number(first) ?? found["maxSize"];
|
|
227
|
+
break;
|
|
228
|
+
case "min":
|
|
229
|
+
found["minimum"] = number(first) ?? found["minimum"];
|
|
230
|
+
break;
|
|
231
|
+
case "max":
|
|
232
|
+
found["maximum"] = number(first) ?? found["maximum"];
|
|
233
|
+
break;
|
|
234
|
+
case "matches":
|
|
235
|
+
if (first instanceof RegExp) {
|
|
236
|
+
found["pattern"] = first.source;
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case "isEmail":
|
|
240
|
+
found["format"] = "email";
|
|
241
|
+
break;
|
|
242
|
+
case "isUrl":
|
|
243
|
+
found["format"] = "uri";
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const key of Object.keys(found)) {
|
|
250
|
+
if (found[key] === undefined) {
|
|
251
|
+
delete found[key];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return Object.keys(found).length === 0 ? undefined : found;
|
|
255
|
+
}
|
|
256
|
+
let validators;
|
|
257
|
+
let transforms;
|
|
258
|
+
function groupValidators(target) {
|
|
259
|
+
const grouped = new Map();
|
|
260
|
+
if (validators === undefined) {
|
|
261
|
+
try {
|
|
262
|
+
validators = load("class-validator").getMetadataStorage();
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
validators = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (validators === null) {
|
|
269
|
+
return grouped;
|
|
270
|
+
}
|
|
271
|
+
const entries = validators.getTargetValidationMetadatas(target, target.name, true, false);
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
const list = grouped.get(entry.propertyName);
|
|
274
|
+
if (list === undefined) {
|
|
275
|
+
grouped.set(entry.propertyName, [entry]);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
list.push(entry);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return grouped;
|
|
282
|
+
}
|
|
283
|
+
function nestedType(target, property) {
|
|
284
|
+
if (transforms === undefined) {
|
|
285
|
+
try {
|
|
286
|
+
transforms = load("class-transformer/cjs/storage").defaultMetadataStorage;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
transforms = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (transforms === null) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const metadata = transforms.findTypeMetadata(target, property);
|
|
296
|
+
return metadata?.typeFunction?.();
|
|
297
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HttpAdapterHost } from "@nestjs/core";
|
|
2
|
+
import { type DispatchDeadline, type RequestTemplate } from "@sezzlee/core";
|
|
3
|
+
import { SezzleeOptions, type InvokeTarget, type OuterRequest } from "./options.js";
|
|
4
|
+
export interface DispatchResult {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly body: string;
|
|
7
|
+
readonly contentType?: string;
|
|
8
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
9
|
+
}
|
|
10
|
+
export interface ProbeResult extends DispatchResult {
|
|
11
|
+
readonly shortCircuited: boolean;
|
|
12
|
+
}
|
|
13
|
+
export type { DispatchDeadline };
|
|
14
|
+
/** What an invocation's body needs beyond its arguments: the budgets and whom a `ref` is resolved for. */
|
|
15
|
+
export interface DispatchFiles {
|
|
16
|
+
readonly target: InvokeTarget;
|
|
17
|
+
readonly maxInlineFileBytes: number;
|
|
18
|
+
readonly maxFileBytes: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class SezzleeDispatcher {
|
|
21
|
+
private readonly adapterHost;
|
|
22
|
+
private readonly options;
|
|
23
|
+
constructor(adapterHost: HttpAdapterHost, options: SezzleeOptions);
|
|
24
|
+
dispatch(method: string, path: string, outer?: OuterRequest, deadline?: DispatchDeadline): Promise<DispatchResult>;
|
|
25
|
+
dispatch(template: RequestTemplate, args: unknown, outer?: OuterRequest, deferred?: Readonly<Record<string, unknown>>, deadline?: DispatchDeadline, files?: DispatchFiles): Promise<DispatchResult>;
|
|
26
|
+
probe(method: string, path: string, outer?: OuterRequest, deadline?: DispatchDeadline): Promise<ProbeResult>;
|
|
27
|
+
private run;
|
|
28
|
+
private refResolver;
|
|
29
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
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 { HttpAdapterHost } from "@nestjs/core";
|
|
15
|
+
import { armDeadline, compose, mergeCookieHeader, SezzleeDispatchAborted, untilAbandoned, writeBody, } from "@sezzlee/core";
|
|
16
|
+
import { SezzleeFileRefused } from "./files.js";
|
|
17
|
+
import { callerOf, SEZZLEE_OPTIONS, SezzleeOptions, } from "./options.js";
|
|
18
|
+
import { markSyntheticRequest, wasShortCircuited } from "./markers.js";
|
|
19
|
+
import { createSyntheticContext } from "./synthetic-context.js";
|
|
20
|
+
const safeFilename = /^[^"\r\n\0/\\]+$/;
|
|
21
|
+
const safeMediaType = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*(?:\s*;\s*[A-Za-z0-9!#$&^_.+-]+=(?:[A-Za-z0-9!#$&^_.+-]+|"[^"\r\n\\]*"))*$/;
|
|
22
|
+
/**
|
|
23
|
+
* Guard: a resolver is host code, but its filename and media type land in a part header. One that
|
|
24
|
+
* would break the header, or carries a path, is skipped for the next rung of the ladder rather than
|
|
25
|
+
* written.
|
|
26
|
+
*/
|
|
27
|
+
function usableFilename(value) {
|
|
28
|
+
return value !== undefined && safeFilename.test(value) ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
function usableMediaType(value) {
|
|
31
|
+
return value !== undefined && safeMediaType.test(value) ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
const defaultUserAgent = "sezzlee/0.0.0";
|
|
34
|
+
let SezzleeDispatcher = class SezzleeDispatcher {
|
|
35
|
+
adapterHost;
|
|
36
|
+
options;
|
|
37
|
+
constructor(adapterHost, options) {
|
|
38
|
+
this.adapterHost = adapterHost;
|
|
39
|
+
this.options = options;
|
|
40
|
+
}
|
|
41
|
+
async dispatch(target, second, outer, third, fourth, files) {
|
|
42
|
+
let method;
|
|
43
|
+
let composed;
|
|
44
|
+
let deadline;
|
|
45
|
+
if (typeof target === "string") {
|
|
46
|
+
method = target.toUpperCase();
|
|
47
|
+
composed = { pathAndQuery: second, headers: {} };
|
|
48
|
+
deadline = third;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
method = target.method;
|
|
52
|
+
composed = compose(target, second, third, files === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: { maxInlineFileBytes: files.maxInlineFileBytes });
|
|
55
|
+
deadline = fourth;
|
|
56
|
+
}
|
|
57
|
+
return this.run(method, composed, outer, false, deadline, files);
|
|
58
|
+
}
|
|
59
|
+
async probe(method, path, outer, deadline) {
|
|
60
|
+
const result = await this.run(method.toUpperCase(), { pathAndQuery: path, headers: {} }, outer, true, deadline);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
async run(method, composed, outer, probe, deadline, files) {
|
|
64
|
+
const pipeline = this.adapterHost.httpAdapter?.getInstance();
|
|
65
|
+
if (!pipeline) {
|
|
66
|
+
throw new Error("sezzlee: HTTP adapter is not available; initialize the Nest application before dispatching.");
|
|
67
|
+
}
|
|
68
|
+
const synthetic = this.options.synthetic;
|
|
69
|
+
const scheme = synthetic.scheme ?? (outer?.protocol || "http");
|
|
70
|
+
const outerHost = typeof outer?.headers.host === "string" ? outer.headers.host : undefined;
|
|
71
|
+
const host = synthetic.host ?? outerHost ?? "localhost";
|
|
72
|
+
const headers = { host };
|
|
73
|
+
if (synthetic.accept) {
|
|
74
|
+
headers["accept"] = synthetic.accept;
|
|
75
|
+
}
|
|
76
|
+
headers["user-agent"] = synthetic.userAgent ?? defaultUserAgent;
|
|
77
|
+
if (outer) {
|
|
78
|
+
for (const trace of ["traceparent", "tracestate"]) {
|
|
79
|
+
const value = outer.headers[trace];
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
headers[trace] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const carrier of this.options.identity.carriers) {
|
|
85
|
+
const value = outer.headers[carrier];
|
|
86
|
+
if (value !== undefined) {
|
|
87
|
+
headers[carrier] = Array.isArray(value) ? value.join(", ") : value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.options.identity.projector?.(outer, headers);
|
|
91
|
+
}
|
|
92
|
+
for (const [name, value] of Object.entries(composed.headers)) {
|
|
93
|
+
const key = name.toLowerCase();
|
|
94
|
+
if (key === "cookie") {
|
|
95
|
+
const merged = mergeCookieHeader(headers.cookie, value);
|
|
96
|
+
if (merged !== undefined) {
|
|
97
|
+
headers.cookie = merged;
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
headers[key] = value;
|
|
102
|
+
}
|
|
103
|
+
const deadlineState = armDeadline(deadline);
|
|
104
|
+
try {
|
|
105
|
+
/**
|
|
106
|
+
* Guard: the deadline is armed before any `ref` is resolved, so a resolver that hangs is
|
|
107
|
+
* bounded exactly like a backend that hangs. Resolving earlier — during the validating
|
|
108
|
+
* composition — would let it outlive the call it serves.
|
|
109
|
+
*/
|
|
110
|
+
const written = composed.body === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: await untilAbandoned(writeBody(composed.body, this.refResolver(outer, files, deadlineState.signal)), deadlineState.signal, deadlineState.reason);
|
|
113
|
+
const abandoned = deadlineState.reason();
|
|
114
|
+
if (abandoned !== undefined) {
|
|
115
|
+
throw new SezzleeDispatchAborted(abandoned);
|
|
116
|
+
}
|
|
117
|
+
if (written !== undefined) {
|
|
118
|
+
headers["content-type"] = written.contentType;
|
|
119
|
+
}
|
|
120
|
+
const { req, res, result, abort } = createSyntheticContext(method, composed.pathAndQuery, headers, scheme, written?.bytes, outer?.connection);
|
|
121
|
+
markSyntheticRequest(req, probe);
|
|
122
|
+
deadlineState.onAbandon(abort);
|
|
123
|
+
try {
|
|
124
|
+
pipeline(req, res);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
abort("pipeline");
|
|
128
|
+
/**
|
|
129
|
+
* Settling the context rejects `result`, which nothing awaits on this path because the
|
|
130
|
+
* pipeline's own error is the useful one. Without this no-op handler that rejection is
|
|
131
|
+
* unhandled and crashes the process under Node's default policy.
|
|
132
|
+
*/
|
|
133
|
+
result.catch(() => undefined);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const dispatched = await result;
|
|
137
|
+
return { ...dispatched, shortCircuited: wasShortCircuited(req) };
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
deadlineState.dispose();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
refResolver(outer, files, signal) {
|
|
144
|
+
return async (file, field) => {
|
|
145
|
+
const resolver = this.options.files.resolver;
|
|
146
|
+
if (resolver === undefined || files === undefined) {
|
|
147
|
+
throw new Error(`sezzlee: file argument '${field}' is a ref but no file resolver is bound.`);
|
|
148
|
+
}
|
|
149
|
+
const limit = files.maxFileBytes;
|
|
150
|
+
const outcome = await resolver.resolve({
|
|
151
|
+
ref: file.ref,
|
|
152
|
+
field,
|
|
153
|
+
target: files.target,
|
|
154
|
+
caller: callerOf(outer),
|
|
155
|
+
maxBytes: limit,
|
|
156
|
+
signal,
|
|
157
|
+
});
|
|
158
|
+
if (!outcome.ok) {
|
|
159
|
+
throw new SezzleeFileRefused(field, outcome.reason, limit);
|
|
160
|
+
}
|
|
161
|
+
if (outcome.bytes.byteLength > limit) {
|
|
162
|
+
throw new SezzleeFileRefused(field, "too_large", limit);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
bytes: outcome.bytes,
|
|
166
|
+
filename: file.filename ??
|
|
167
|
+
usableFilename(outcome.filename) ??
|
|
168
|
+
file.fallbackFilename,
|
|
169
|
+
mediaType: file.mediaType ??
|
|
170
|
+
usableMediaType(outcome.mediaType) ??
|
|
171
|
+
file.fallbackMediaType,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
SezzleeDispatcher = __decorate([
|
|
177
|
+
Injectable(),
|
|
178
|
+
__param(1, Inject(SEZZLEE_OPTIONS)),
|
|
179
|
+
__metadata("design:paramtypes", [HttpAdapterHost,
|
|
180
|
+
SezzleeOptions])
|
|
181
|
+
], SezzleeDispatcher);
|
|
182
|
+
export { SezzleeDispatcher };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { InjectionToken, Provider, Type } from "@nestjs/common";
|
|
2
|
+
import type { SezzleeCache, ToolRanker } from "@sezzlee/core";
|
|
3
|
+
import type { CallerScopeResolver } from "./cache.js";
|
|
4
|
+
import type { InvokeResultMapper } from "./invoke-result-mapper.js";
|
|
5
|
+
import type { VisibilityEvaluator } from "./visibility/evaluator.js";
|
|
6
|
+
import type { ProbeEvaluator } from "./visibility/probe.js";
|
|
7
|
+
export interface ExtensionPoints {
|
|
8
|
+
cache: SezzleeCache;
|
|
9
|
+
callerScopeResolver: CallerScopeResolver;
|
|
10
|
+
invokeResultMapper: InvokeResultMapper;
|
|
11
|
+
visibilityEvaluator: VisibilityEvaluator;
|
|
12
|
+
probeEvaluator: ProbeEvaluator;
|
|
13
|
+
toolRanker: ToolRanker | null;
|
|
14
|
+
}
|
|
15
|
+
export declare const extensionTokens: {
|
|
16
|
+
readonly cache: symbol;
|
|
17
|
+
readonly callerScopeResolver: symbol;
|
|
18
|
+
readonly invokeResultMapper: symbol;
|
|
19
|
+
readonly visibilityEvaluator: symbol;
|
|
20
|
+
readonly probeEvaluator: symbol;
|
|
21
|
+
readonly toolRanker: symbol;
|
|
22
|
+
};
|
|
23
|
+
export type OverrideProvider<T> = {
|
|
24
|
+
useClass: Type<T>;
|
|
25
|
+
} | {
|
|
26
|
+
useFactory: (...args: never[]) => T | Promise<T>;
|
|
27
|
+
inject?: InjectionToken[];
|
|
28
|
+
} | {
|
|
29
|
+
useValue: T;
|
|
30
|
+
};
|
|
31
|
+
export type ExtensionOverrides = Partial<{
|
|
32
|
+
[K in keyof ExtensionPoints]: OverrideProvider<ExtensionPoints[K]>;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function toProviders(overrides?: ExtensionOverrides): Provider[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const extensionTokens = {
|
|
2
|
+
cache: Symbol("SEZZLEE_CACHE"),
|
|
3
|
+
callerScopeResolver: Symbol("SEZZLEE_CALLER_SCOPE_RESOLVER"),
|
|
4
|
+
invokeResultMapper: Symbol("SEZZLEE_INVOKE_RESULT_MAPPER"),
|
|
5
|
+
visibilityEvaluator: Symbol("SEZZLEE_VISIBILITY_EVALUATOR"),
|
|
6
|
+
probeEvaluator: Symbol("SEZZLEE_PROBE_EVALUATOR"),
|
|
7
|
+
toolRanker: Symbol("SEZZLEE_TOOL_RANKER"),
|
|
8
|
+
};
|
|
9
|
+
export function toProviders(overrides) {
|
|
10
|
+
if (overrides === undefined) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const providers = [];
|
|
14
|
+
for (const key of Object.keys(extensionTokens)) {
|
|
15
|
+
const override = overrides[key];
|
|
16
|
+
if (override === undefined) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const token = extensionTokens[key];
|
|
20
|
+
if ("useValue" in override) {
|
|
21
|
+
providers.push({ provide: token, useValue: override.useValue });
|
|
22
|
+
}
|
|
23
|
+
else if ("useFactory" in override) {
|
|
24
|
+
providers.push({
|
|
25
|
+
provide: token,
|
|
26
|
+
useFactory: override.useFactory,
|
|
27
|
+
inject: override.inject ?? [],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
providers.push({ provide: token, useClass: override.useClass });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return providers;
|
|
35
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type CatalogDiagnostic, type JsonSchemaObject, type ToolFamily, type ToolVariant } from "@sezzlee/core";
|
|
2
|
+
import type { McpToolEffect, McpVariantOptions } from "./decorators.js";
|
|
3
|
+
/**
|
|
4
|
+
* One capability behind a dispatching operation, as a host's member source returns it.
|
|
5
|
+
*
|
|
6
|
+
* @param key the value written into the dispatch parameter; it never reaches the agent
|
|
7
|
+
* @param body the member's own request body, published as its input schema
|
|
8
|
+
*/
|
|
9
|
+
export type McpFamilyMember = {
|
|
10
|
+
readonly key: string | number;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly body: JsonSchemaObject;
|
|
14
|
+
readonly bodyRequired?: boolean;
|
|
15
|
+
} & McpToolEffect;
|
|
16
|
+
export type McpFamilySource = (context: {
|
|
17
|
+
readonly signal: AbortSignal;
|
|
18
|
+
}) => Promise<readonly McpFamilyMember[]> | readonly McpFamilyMember[];
|
|
19
|
+
export type FamilyLoad = {
|
|
20
|
+
readonly state: "loaded";
|
|
21
|
+
readonly members: readonly McpFamilyMember[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly state: "stale";
|
|
24
|
+
readonly members: readonly McpFamilyMember[];
|
|
25
|
+
readonly error: unknown;
|
|
26
|
+
} | {
|
|
27
|
+
readonly state: "failed";
|
|
28
|
+
readonly error: unknown;
|
|
29
|
+
};
|
|
30
|
+
export type FamilyLoads = ReadonlyMap<string, FamilyLoad>;
|
|
31
|
+
export declare class ToolFamilyOptions {
|
|
32
|
+
private readonly registered;
|
|
33
|
+
loadTimeoutMs: number;
|
|
34
|
+
get sources(): ReadonlyMap<string, McpFamilySource>;
|
|
35
|
+
provide(name: string, source: McpFamilySource): this;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Loads every registered source once.
|
|
39
|
+
*
|
|
40
|
+
* A failing source keeps the members it last returned: dropping a family because its source was
|
|
41
|
+
* briefly unreachable would take working tools offline on every transient error.
|
|
42
|
+
*
|
|
43
|
+
* @param previous the loads the current catalog was built from, or `undefined` before the first
|
|
44
|
+
*/
|
|
45
|
+
export declare function loadFamilies(options: ToolFamilyOptions, previous: FamilyLoads | undefined): Promise<FamilyLoads>;
|
|
46
|
+
export type FamilyResolution = {
|
|
47
|
+
readonly kind: "none";
|
|
48
|
+
} | {
|
|
49
|
+
readonly kind: "members";
|
|
50
|
+
readonly family: ToolFamily;
|
|
51
|
+
readonly variants: readonly ToolVariant[];
|
|
52
|
+
readonly diagnostics: readonly CatalogDiagnostic[];
|
|
53
|
+
} | {
|
|
54
|
+
readonly kind: "refused";
|
|
55
|
+
readonly diagnostic: CatalogDiagnostic;
|
|
56
|
+
};
|
|
57
|
+
export declare const sdkFamilySeverities: {
|
|
58
|
+
readonly unknown_family_source: "endpointDropped";
|
|
59
|
+
readonly family_source_failed: "endpointDropped";
|
|
60
|
+
readonly family_not_loaded: "endpointDropped";
|
|
61
|
+
readonly family_source_stale: "warning";
|
|
62
|
+
readonly family_member_rejected: "warning";
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Lowers one operation's family declaration into the variants its members become.
|
|
66
|
+
*
|
|
67
|
+
* A defect in one member is that member's alone: the rest of the family stays published and the
|
|
68
|
+
* member is reported, because the members are host data and one bad row should not take every
|
|
69
|
+
* sibling offline. Only a defect in the declaration itself refuses the operation.
|
|
70
|
+
*
|
|
71
|
+
* @param owner the operation's identity in diagnostics
|
|
72
|
+
* @param declared the handler's own `@McpVariant`s; a member may not reuse their names
|
|
73
|
+
* @param loads the member loads, or `undefined` when the sources have not loaded yet
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveFamily(family: ToolFamily & {
|
|
76
|
+
readonly source?: string;
|
|
77
|
+
}, owner: string, declared: readonly McpVariantOptions[], options: ToolFamilyOptions, loads: FamilyLoads | undefined): FamilyResolution;
|