@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,8 @@
|
|
|
1
|
+
import type { CallerFacts } from "@sezzlee/core";
|
|
2
|
+
import type { OuterRequest } from "../options.js";
|
|
3
|
+
export interface VisibilityEvaluator {
|
|
4
|
+
resolve(outer: OuterRequest | undefined, policyNames: ReadonlySet<string>): Promise<CallerFacts>;
|
|
5
|
+
}
|
|
6
|
+
export declare class DeclarativeVisibilityEvaluator implements VisibilityEvaluator {
|
|
7
|
+
resolve(): Promise<CallerFacts>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CallHandler, type ExecutionContext, type NestInterceptor } from "@nestjs/common";
|
|
2
|
+
import { type Observable } from "rxjs";
|
|
3
|
+
import type { VisibilityDecision } from "@sezzlee/core";
|
|
4
|
+
import type { CatalogEntry } from "../catalog.js";
|
|
5
|
+
import { SezzleeDispatcher } from "../dispatcher.js";
|
|
6
|
+
import { wasShortCircuited } from "../markers.js";
|
|
7
|
+
import type { OuterRequest, SezzleeOptions } from "../options.js";
|
|
8
|
+
export declare class SezzleeProbeInterceptor implements NestInterceptor {
|
|
9
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface ProbeEvaluator {
|
|
12
|
+
canProbe(entry: CatalogEntry): boolean;
|
|
13
|
+
probe(entry: CatalogEntry, outer: OuterRequest | undefined): Promise<VisibilityDecision>;
|
|
14
|
+
}
|
|
15
|
+
export declare class SezzleeProbeEvaluator implements ProbeEvaluator {
|
|
16
|
+
private readonly dispatcher;
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly disabled;
|
|
19
|
+
constructor(dispatcher: SezzleeDispatcher, options: SezzleeOptions);
|
|
20
|
+
clearDisabled(): void;
|
|
21
|
+
canProbe(entry: CatalogEntry): boolean;
|
|
22
|
+
probe(entry: CatalogEntry, outer: OuterRequest | undefined): Promise<VisibilityDecision>;
|
|
23
|
+
private pathFor;
|
|
24
|
+
}
|
|
25
|
+
export { wasShortCircuited };
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
import { Injectable, } from "@nestjs/common";
|
|
8
|
+
import { of } from "rxjs";
|
|
9
|
+
import { SezzleeDispatchAborted } from "../synthetic-context.js";
|
|
10
|
+
import { isSezzleeProbe, markShortCircuited, wasShortCircuited, } from "../markers.js";
|
|
11
|
+
let SezzleeProbeInterceptor = class SezzleeProbeInterceptor {
|
|
12
|
+
intercept(context, next) {
|
|
13
|
+
const request = context.switchToHttp().getRequest();
|
|
14
|
+
if (!isSezzleeProbe(request)) {
|
|
15
|
+
return next.handle();
|
|
16
|
+
}
|
|
17
|
+
markShortCircuited(request);
|
|
18
|
+
return of(undefined);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
SezzleeProbeInterceptor = __decorate([
|
|
22
|
+
Injectable()
|
|
23
|
+
], SezzleeProbeInterceptor);
|
|
24
|
+
export { SezzleeProbeInterceptor };
|
|
25
|
+
export class SezzleeProbeEvaluator {
|
|
26
|
+
dispatcher;
|
|
27
|
+
options;
|
|
28
|
+
disabled = new Map();
|
|
29
|
+
constructor(dispatcher, options) {
|
|
30
|
+
this.dispatcher = dispatcher;
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
clearDisabled() {
|
|
34
|
+
this.disabled.clear();
|
|
35
|
+
}
|
|
36
|
+
canProbe(entry) {
|
|
37
|
+
return entry.template !== undefined && !this.disabled.has(entry.tool.name);
|
|
38
|
+
}
|
|
39
|
+
async probe(entry, outer) {
|
|
40
|
+
const path = this.pathFor(entry);
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
result = await this.dispatcher.probe(entry.descriptor.method, path, outer);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!(error instanceof SezzleeDispatchAborted)) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
this.disabled.set(entry.tool.name, "the probe was abandoned before the pipeline produced a response");
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
if (result.status === 401 || result.status === 403) {
|
|
53
|
+
return "deny";
|
|
54
|
+
}
|
|
55
|
+
if (result.shortCircuited && result.status < 400) {
|
|
56
|
+
return "allow";
|
|
57
|
+
}
|
|
58
|
+
this.disabled.set(entry.tool.name, result.status === 404
|
|
59
|
+
? "the probe path did not route; declare a value in visibility.probeValues"
|
|
60
|
+
: "the response came back without the short-circuit marker");
|
|
61
|
+
return "unknown";
|
|
62
|
+
}
|
|
63
|
+
pathFor(entry) {
|
|
64
|
+
return entry.descriptor.route.replaceAll(/\{([^}]+)\}/g, (_match, name) => encodeURIComponent(constantFor(entry, name) ??
|
|
65
|
+
this.options.visibility.probeValues.get(name.toLowerCase()) ??
|
|
66
|
+
placeholderFor(entry, name)));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Guard: a family member is reachable only through its own key, so a placeholder or a host probe
|
|
71
|
+
* value would probe a different member, or none, and report its verdict for this one.
|
|
72
|
+
*/
|
|
73
|
+
function constantFor(entry, name) {
|
|
74
|
+
const fill = entry.template?.parameters.find((parameter) => parameter.name === name && parameter.location === "path")?.fill;
|
|
75
|
+
if (fill?.kind !== "constant") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const value = fill.value;
|
|
79
|
+
return typeof value === "string" ||
|
|
80
|
+
typeof value === "number" ||
|
|
81
|
+
typeof value === "boolean"
|
|
82
|
+
? String(value)
|
|
83
|
+
: undefined;
|
|
84
|
+
}
|
|
85
|
+
function placeholderFor(entry, name) {
|
|
86
|
+
const parameter = entry.descriptor.parameters?.find((candidate) => candidate.name === name && candidate.in === "path");
|
|
87
|
+
switch (parameter?.schema.type) {
|
|
88
|
+
case "integer":
|
|
89
|
+
case "number":
|
|
90
|
+
return "1";
|
|
91
|
+
case "boolean":
|
|
92
|
+
return "true";
|
|
93
|
+
default:
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (parameter?.schema.format === "uuid") {
|
|
97
|
+
return "00000000-0000-0000-0000-000000000000";
|
|
98
|
+
}
|
|
99
|
+
if (parameter?.schema.format === "date-time") {
|
|
100
|
+
return "2000-01-01";
|
|
101
|
+
}
|
|
102
|
+
return "probe";
|
|
103
|
+
}
|
|
104
|
+
export { wasShortCircuited };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type CallerFacts, type CallerScope, type SezzleeCache, type VisibilityDecision } from "@sezzlee/core";
|
|
2
|
+
import type { CatalogEntry } from "../catalog.js";
|
|
3
|
+
import type { OuterRequest, SezzleeOptions } from "../options.js";
|
|
4
|
+
import type { ProbeEvaluator } from "./probe.js";
|
|
5
|
+
import type { VisibilityEvaluator } from "./evaluator.js";
|
|
6
|
+
export declare class CallerVisibilityProvider {
|
|
7
|
+
private readonly evaluator;
|
|
8
|
+
private readonly prober;
|
|
9
|
+
private readonly cache;
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly factsFlight;
|
|
12
|
+
private readonly probeFlight;
|
|
13
|
+
private epoch;
|
|
14
|
+
constructor(evaluator: VisibilityEvaluator, prober: ProbeEvaluator, cache: SezzleeCache, options: SezzleeOptions);
|
|
15
|
+
bump(): void;
|
|
16
|
+
canProbe(entry: CatalogEntry): boolean;
|
|
17
|
+
facts(scope: CallerScope, outer: OuterRequest | undefined, policyNames: ReadonlySet<string>): Promise<CallerFacts>;
|
|
18
|
+
probe(scope: CallerScope, entry: CatalogEntry, outer: OuterRequest | undefined): Promise<VisibilityDecision>;
|
|
19
|
+
decide(auth: CatalogEntry["descriptor"]["auth"], facts: CallerFacts): VisibilityDecision;
|
|
20
|
+
visible(decision: VisibilityDecision): boolean;
|
|
21
|
+
private read;
|
|
22
|
+
private write;
|
|
23
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { evaluateVisibility, SingleFlight, } from "@sezzlee/core";
|
|
2
|
+
export class CallerVisibilityProvider {
|
|
3
|
+
evaluator;
|
|
4
|
+
prober;
|
|
5
|
+
cache;
|
|
6
|
+
options;
|
|
7
|
+
factsFlight = new SingleFlight();
|
|
8
|
+
probeFlight = new SingleFlight();
|
|
9
|
+
epoch = 0;
|
|
10
|
+
constructor(evaluator, prober, cache, options) {
|
|
11
|
+
this.evaluator = evaluator;
|
|
12
|
+
this.prober = prober;
|
|
13
|
+
this.cache = cache;
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
bump() {
|
|
17
|
+
this.epoch += 1;
|
|
18
|
+
}
|
|
19
|
+
canProbe(entry) {
|
|
20
|
+
return this.prober.canProbe(entry);
|
|
21
|
+
}
|
|
22
|
+
async facts(scope, outer, policyNames) {
|
|
23
|
+
if (this.options.cache.lifetimeMs <= 0) {
|
|
24
|
+
return this.evaluator.resolve(outer, policyNames);
|
|
25
|
+
}
|
|
26
|
+
const key = { scope, kind: "facts" };
|
|
27
|
+
const observed = this.epoch;
|
|
28
|
+
const cached = await this.read(key);
|
|
29
|
+
if (cached !== undefined) {
|
|
30
|
+
return decodeFacts(cached);
|
|
31
|
+
}
|
|
32
|
+
const resolved = await this.factsFlight.run(String(scope.key), () => this.evaluator.resolve(outer, policyNames));
|
|
33
|
+
if (observed === this.epoch) {
|
|
34
|
+
await this.write(key, encodeFacts(resolved));
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
async probe(scope, entry, outer) {
|
|
39
|
+
const key = { scope, kind: "probe", subkey: entry.tool.name };
|
|
40
|
+
if (this.options.cache.lifetimeMs <= 0) {
|
|
41
|
+
return this.prober.probe(entry, outer);
|
|
42
|
+
}
|
|
43
|
+
const observed = this.epoch;
|
|
44
|
+
const cached = await this.read(key);
|
|
45
|
+
if (cached === "A") {
|
|
46
|
+
return "allow";
|
|
47
|
+
}
|
|
48
|
+
if (cached === "D") {
|
|
49
|
+
return "deny";
|
|
50
|
+
}
|
|
51
|
+
const decision = await this.probeFlight.run(`${scope.key}:${entry.tool.name}`, () => this.prober.probe(entry, outer));
|
|
52
|
+
if (decision !== "unknown" && observed === this.epoch) {
|
|
53
|
+
await this.write(key, decision === "allow" ? "A" : "D");
|
|
54
|
+
}
|
|
55
|
+
return decision;
|
|
56
|
+
}
|
|
57
|
+
decide(auth, facts) {
|
|
58
|
+
return evaluateVisibility(auth, facts);
|
|
59
|
+
}
|
|
60
|
+
visible(decision) {
|
|
61
|
+
if (decision === "allow") {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (decision === "deny") {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return this.options.visibility.onUnknown === "show";
|
|
68
|
+
}
|
|
69
|
+
async read(key) {
|
|
70
|
+
try {
|
|
71
|
+
return await this.cache.get(key);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async write(key, value) {
|
|
78
|
+
try {
|
|
79
|
+
await this.cache.set(key, value);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function encodeFacts(facts) {
|
|
87
|
+
const identity = facts.identity === "present"
|
|
88
|
+
? "P"
|
|
89
|
+
: facts.identity === "absent"
|
|
90
|
+
? "A"
|
|
91
|
+
: "U";
|
|
92
|
+
const results = Object.entries(facts.policyResults ?? {})
|
|
93
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
94
|
+
.map(([name, decision]) => `${name}=${decision.charAt(0).toUpperCase()}`)
|
|
95
|
+
.join(";");
|
|
96
|
+
return `${identity}|${results}`;
|
|
97
|
+
}
|
|
98
|
+
function decodeFacts(encoded) {
|
|
99
|
+
const [identityCode, results = ""] = encoded.split("|");
|
|
100
|
+
const identity = identityCode === "P"
|
|
101
|
+
? "present"
|
|
102
|
+
: identityCode === "A"
|
|
103
|
+
? "absent"
|
|
104
|
+
: "unknown";
|
|
105
|
+
const policyResults = {};
|
|
106
|
+
for (const pair of results.split(";")) {
|
|
107
|
+
if (pair.length === 0) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const [name, code] = pair.split("=");
|
|
111
|
+
if (name === undefined || code === undefined) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
policyResults[name] =
|
|
115
|
+
code === "A" ? "allow" : code === "D" ? "deny" : "unknown";
|
|
116
|
+
}
|
|
117
|
+
return { identity, policyResults };
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,102 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sezzlee/sdk-nestjs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Embeds an MCP layer into an existing NestJS backend: search-first tool discovery over your real endpoints, dispatched through your own guards, pipes and interceptors.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"nestjs",
|
|
9
|
+
"sezzlee",
|
|
10
|
+
"agents",
|
|
11
|
+
"ai",
|
|
12
|
+
"llm",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/sezzlee/mcp.git",
|
|
19
|
+
"directory": "sdks/nestjs"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/sezzlee/mcp/tree/main/sdks/nestjs#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/sezzlee/mcp/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@sezzlee/core": "^0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@modelcontextprotocol/express": ">=2.0.0",
|
|
48
|
+
"@modelcontextprotocol/node": ">=2.0.0",
|
|
49
|
+
"@modelcontextprotocol/server": ">=2.0.0",
|
|
50
|
+
"@nestjs/common": ">=10",
|
|
51
|
+
"@nestjs/core": ">=10",
|
|
52
|
+
"@types/express": ">=4",
|
|
53
|
+
"class-transformer": ">=0.5",
|
|
54
|
+
"class-validator": ">=0.14",
|
|
55
|
+
"rxjs": "^7.1.0",
|
|
56
|
+
"zod": "^4.5.4"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@types/express": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"class-transformer": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"class-validator": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@modelcontextprotocol/client": "2.0.0",
|
|
71
|
+
"@modelcontextprotocol/express": "2.0.0",
|
|
72
|
+
"@modelcontextprotocol/node": "2.0.0",
|
|
73
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
74
|
+
"@nestjs/common": "^11.1.6",
|
|
75
|
+
"@nestjs/core": "^11.1.6",
|
|
76
|
+
"@nestjs/platform-express": "^11.1.6",
|
|
77
|
+
"@nestjs/testing": "^11.1.6",
|
|
78
|
+
"@sezzlee/oxlint-config": "0.0.0",
|
|
79
|
+
"@sezzlee/typescript-config": "0.0.0",
|
|
80
|
+
"@swc/core": "^1.13.0",
|
|
81
|
+
"@types/express": "^5.0.0",
|
|
82
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
83
|
+
"@types/node": "^24.3.0",
|
|
84
|
+
"class-transformer": "^0.5.1",
|
|
85
|
+
"class-validator": "^0.14.2",
|
|
86
|
+
"express": "^5.2.1",
|
|
87
|
+
"hono": "^4.11.4",
|
|
88
|
+
"jsonwebtoken": "^9.0.2",
|
|
89
|
+
"oxlint": "1.85.0",
|
|
90
|
+
"reflect-metadata": "^0.2.2",
|
|
91
|
+
"rxjs": "^7.8.2",
|
|
92
|
+
"typescript": "7.0.2",
|
|
93
|
+
"unplugin-swc": "^1.5.5",
|
|
94
|
+
"vitest": "^3.2.4"
|
|
95
|
+
},
|
|
96
|
+
"scripts": {
|
|
97
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
98
|
+
"lint": "oxlint --deny-warnings",
|
|
99
|
+
"check-types": "tsc -p tsconfig.test.json",
|
|
100
|
+
"test": "vitest run"
|
|
101
|
+
}
|
|
6
102
|
}
|