@knitli/openapi-mcp 1.0.0 → 1.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/README.md +53 -0
- package/dist/cli.js +43 -0
- package/dist/runtime/admission.d.ts +8 -0
- package/dist/runtime/admission.js +9 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/slice.d.ts +14 -0
- package/dist/slice.js +220 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,48 @@ base64url SPKI public key for the config with:
|
|
|
75
75
|
node --input-type=module -e 'import {readFileSync} from "node:fs"; import {createPublicKey} from "node:crypto"; console.log(createPublicKey(readFileSync("operator/keys/openapi-mcp.pub")).export({type:"spki",format:"der"}).toString("base64url"))'
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### Slice a large document down to one deployment's catalog
|
|
79
|
+
|
|
80
|
+
`compile-release` enforces `maxDocumentKeys` (default 250,000) and
|
|
81
|
+
`maxDocumentNodes` (default 1,000,000) on the whole spec. A document like
|
|
82
|
+
Microsoft Graph's full v1.0 (over 1M nodes, served from a single `/v1.0`) will
|
|
83
|
+
never fit, no matter how few operations one deployment actually exposes. `slice`
|
|
84
|
+
filters an OpenAPI document down to the operations you select by `--tag` and/or
|
|
85
|
+
`--operation`, prunes every `components` entry nothing kept still references,
|
|
86
|
+
and rewrites `servers` to the bare origin `compile-release` requires (it rejects
|
|
87
|
+
any `servers[0].url` with a path component):
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
./node_modules/.bin/openapi-mcp slice \
|
|
91
|
+
--spec ./graph-v1.0.yaml --out ./sliced/mail.json \
|
|
92
|
+
--tag me.message --operation me.sendMail \
|
|
93
|
+
--max-document-keys 5000000 --max-document-nodes 20000000
|
|
94
|
+
./node_modules/.bin/openapi-mcp compile-release \
|
|
95
|
+
--spec ./sliced/mail.json --source-label graph-mail --source-revision revision-1 \
|
|
96
|
+
--catalog mail --release mail-1 --generation 1 \
|
|
97
|
+
--issuer example-operator --key-id signing-1 --policy-id local-policy-1 \
|
|
98
|
+
--allowed-origin https://graph.microsoft.com \
|
|
99
|
+
--out ./operator/releases --sign-key ./operator/keys/openapi-mcp.key
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`--max-document-keys` and `--max-document-nodes` only raise the limits `slice`
|
|
103
|
+
itself loads the source document under (`loadSpecV4`'s `CompilerLimits`); they
|
|
104
|
+
have no effect on the sliced output's own size, which is whatever fits under
|
|
105
|
+
`compile-release`'s (unraised) defaults. On success `slice` prints a summary
|
|
106
|
+
line, for example:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
paths=23 operations=33 schemas=30 keys=3027 (fits compile-release's default maxDocumentKeys=250000)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`keys=` is the sliced document's own key count, counted the same way the
|
|
113
|
+
compiler counts `maxDocumentKeys`; the number in parentheses is
|
|
114
|
+
`compile-release`'s own (unraised) default, so you can tell before running it
|
|
115
|
+
whether the selection is small enough. An `--operation` or `--tag` that matched
|
|
116
|
+
nothing is an error, not a silently smaller release — fix the typo it names.
|
|
117
|
+
OpenAPI 3.1 `webhooks` are not part of `paths` and are left untouched, dangling
|
|
118
|
+
refs and all; Graph's v1.0 document (3.0) has none.
|
|
119
|
+
|
|
78
120
|
## Configure and start the local MCP server
|
|
79
121
|
|
|
80
122
|
Save this as `operator/config.json`, substituting absolute paths, the public key,
|
|
@@ -217,6 +259,17 @@ restart with the remaining valid catalogs. Config requires at least one catalog;
|
|
|
217
259
|
leave the server stopped if disabling the last one. Removing a file while the
|
|
218
260
|
process is using it is not a reliable disable procedure.
|
|
219
261
|
|
|
262
|
+
## Complete release admission
|
|
263
|
+
|
|
264
|
+
`admitCatalogRelease(options, catalogId, releaseId)` from
|
|
265
|
+
`@knitli/openapi-mcp/runtime` verifies every signed inventory operation and
|
|
266
|
+
schema before admitting the generation through the configured `GenerationStore`.
|
|
267
|
+
It revalidates the manifest and inventory around generation CAS retries. Use
|
|
268
|
+
this supported portable API when activating an executable catalog; `admitManifest`
|
|
269
|
+
retains its signed-envelope-only contract. Storage must preserve immutable release
|
|
270
|
+
identities. Admission does not guarantee future availability, so runtime reads
|
|
271
|
+
continue verifying records at use time.
|
|
272
|
+
|
|
220
273
|
## Recover from a bad release
|
|
221
274
|
|
|
222
275
|
Before release, record the prior **observed package version**, its tarball digest,
|
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,10 @@ import { join } from "node:path";
|
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { compile } from "./compile.js";
|
|
6
6
|
import { compileRelease } from "./release/compile-release.js";
|
|
7
|
+
import { DEFAULT_COMPILER_LIMITS, loadSpecV4 } from "./release/load-v4.js";
|
|
7
8
|
import { publishRelease } from "./release/publish.js";
|
|
8
9
|
import { generateKeypair, signArtifact, verifyArtifact } from "./sign.js";
|
|
10
|
+
import { countKeys, HTTP_METHODS, keyBudgetNote, sliceSpec } from "./slice.js";
|
|
9
11
|
const USAGE = `openapi-mcp — compile OpenAPI documents into signed MCP artifacts
|
|
10
12
|
|
|
11
13
|
compile --spec <path> --api <name> --out <path> [--append] [--permissions <path>] [--sign-key <path>] (legacy v3)
|
|
@@ -13,6 +15,7 @@ const USAGE = `openapi-mcp — compile OpenAPI documents into signed MCP artifac
|
|
|
13
15
|
--catalog <id> --release <id> --generation <n> --issuer <id> --key-id <id>
|
|
14
16
|
--policy-id <id> --allowed-origin <https-origin> --out <directory> --sign-key <path>
|
|
15
17
|
[--permissions <path>] [--reference-root <path> --reference-map <path>]
|
|
18
|
+
slice --spec <openapi.yaml|json> --out <sliced.json> [--tag <tag>]... [--operation <operationId>]... [--max-document-keys <n>] [--max-document-nodes <n>]
|
|
16
19
|
verify --artifact <path> --sig <path> --pub <path> (legacy v3 exact-file signature)
|
|
17
20
|
keygen [--out <dir>]
|
|
18
21
|
serve --config <absolute-config-path>
|
|
@@ -203,6 +206,46 @@ if (command === "compile-release") {
|
|
|
203
206
|
}
|
|
204
207
|
process.exit(0);
|
|
205
208
|
}
|
|
209
|
+
if (command === "slice") {
|
|
210
|
+
const { values } = parseOrFail({
|
|
211
|
+
args: rest,
|
|
212
|
+
options: {
|
|
213
|
+
spec: { type: "string" },
|
|
214
|
+
out: { type: "string" },
|
|
215
|
+
tag: { type: "string", multiple: true },
|
|
216
|
+
operation: { type: "string", multiple: true },
|
|
217
|
+
"max-document-keys": { type: "string" },
|
|
218
|
+
"max-document-nodes": { type: "string" },
|
|
219
|
+
},
|
|
220
|
+
strict: true,
|
|
221
|
+
});
|
|
222
|
+
if (!values.spec)
|
|
223
|
+
fail("--spec is required");
|
|
224
|
+
if (!values.out)
|
|
225
|
+
fail("--out is required");
|
|
226
|
+
try {
|
|
227
|
+
const document = (await loadSpecV4(values.spec, {
|
|
228
|
+
maxDocumentKeys: Number(values["max-document-keys"] ?? 5_000_000),
|
|
229
|
+
maxDocumentNodes: Number(values["max-document-nodes"] ?? 20_000_000),
|
|
230
|
+
}));
|
|
231
|
+
const sliced = sliceSpec(document, {
|
|
232
|
+
tags: values.tag ?? [],
|
|
233
|
+
operations: values.operation ?? [],
|
|
234
|
+
});
|
|
235
|
+
await writeFile(values.out, `${JSON.stringify(sliced, null, 2)}\n`);
|
|
236
|
+
const paths = sliced.paths;
|
|
237
|
+
const operationCount = Object.values(paths).reduce((n, item) => n +
|
|
238
|
+
Object.keys(item).filter((k) => HTTP_METHODS.includes(k)).length, 0);
|
|
239
|
+
const schemaCount = Object.keys((sliced.components.schemas ?? {})).length;
|
|
240
|
+
const keys = countKeys(sliced);
|
|
241
|
+
console.log(`paths=${Object.keys(paths).length} operations=${operationCount} schemas=${schemaCount} ` +
|
|
242
|
+
`keys=${keys} (${keyBudgetNote(keys, DEFAULT_COMPILER_LIMITS.maxDocumentKeys)})`);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
fail(err instanceof Error ? err.message : String(err), { usage: false });
|
|
246
|
+
}
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
206
249
|
if (command !== "serve") {
|
|
207
250
|
if (command !== "compile") {
|
|
208
251
|
fail(command ? `unknown command "${command}"` : "no command given");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AdmittedManifest } from "./manifest.ts";
|
|
2
|
+
import { type OpenApiRuntimeOptions } from "./runtime.ts";
|
|
3
|
+
import type { CatalogId, ReleaseId } from "./types.ts";
|
|
4
|
+
/** Verify the complete release inventory before atomically admitting its generation.
|
|
5
|
+
* Storage must preserve immutable release identities; later runtime reads still
|
|
6
|
+
* verify records because this check cannot guarantee future storage availability.
|
|
7
|
+
*/
|
|
8
|
+
export declare function admitCatalogRelease(options: OpenApiRuntimeOptions, catalogId: CatalogId, releaseId: ReleaseId): Promise<AdmittedManifest>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { admitExecutableRelease, verifyExecutableRelease, } from "./runtime.js";
|
|
2
|
+
/** Verify the complete release inventory before atomically admitting its generation.
|
|
3
|
+
* Storage must preserve immutable release identities; later runtime reads still
|
|
4
|
+
* verify records because this check cannot guarantee future storage availability.
|
|
5
|
+
*/
|
|
6
|
+
export async function admitCatalogRelease(options, catalogId, releaseId) {
|
|
7
|
+
const preflight = await verifyExecutableRelease(options, catalogId, releaseId);
|
|
8
|
+
return admitExecutableRelease(options, preflight);
|
|
9
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { ActionDispatchPermit } from "./action-permit.ts";
|
|
2
|
+
export { admitCatalogRelease } from "./admission.ts";
|
|
2
3
|
export { classifyOperation, type OperationClassification, } from "./classify.ts";
|
|
3
4
|
export { digestCredentialAuthorizationBinding, snapshotCredentialAuthorizationBinding, } from "./credential-binding.ts";
|
|
4
5
|
export { sha256, verifyEd25519 } from "./digest.ts";
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { admitCatalogRelease } from "./admission.js";
|
|
1
2
|
export { classifyOperation, } from "./classify.js";
|
|
2
3
|
export { digestCredentialAuthorizationBinding, snapshotCredentialAuthorizationBinding, } from "./credential-binding.js";
|
|
3
4
|
export { sha256, verifyEd25519 } from "./digest.js";
|
package/dist/slice.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const HTTP_METHODS: readonly ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
2
|
+
type Json = Record<string, unknown>;
|
|
3
|
+
/** Which operations survive: any of these tags, or any of these operation ids. */
|
|
4
|
+
export interface SliceSelection {
|
|
5
|
+
readonly tags: readonly string[];
|
|
6
|
+
readonly operations: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** Number of object keys in a JSON value; compare with the compiler's `maxDocumentKeys` (250,000). */
|
|
9
|
+
export declare function countKeys(value: unknown): number;
|
|
10
|
+
/** Describes a key count against compile-release's default `maxDocumentKeys`, and what to do if it doesn't fit. */
|
|
11
|
+
export declare function keyBudgetNote(keys: number, limit: number): string;
|
|
12
|
+
/** Pure: returns a new document containing only the selected operations and what they reference. */
|
|
13
|
+
export declare function sliceSpec(document: Json, selection: SliceSelection): Json;
|
|
14
|
+
export {};
|
package/dist/slice.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Slice an OpenAPI 3.x document down to the operations a deployment will expose, prune the
|
|
2
|
+
// components nothing left references, and rewrite `servers` to the bare origin `compile-release`
|
|
3
|
+
// requires. This is what lets a document far beyond `maxDocumentKeys` (Microsoft Graph's full
|
|
4
|
+
// v1.0, served from `/v1.0`) be compiled as one catalog per deployment.
|
|
5
|
+
//
|
|
6
|
+
// OpenAPI 3.1 `webhooks` are deliberately left untouched: they are not part of `paths` and are
|
|
7
|
+
// spread through as-is, dangling refs and all, same as any other top-level key. Graph is 3.0 and
|
|
8
|
+
// has none.
|
|
9
|
+
export const HTTP_METHODS = [
|
|
10
|
+
"get",
|
|
11
|
+
"put",
|
|
12
|
+
"post",
|
|
13
|
+
"delete",
|
|
14
|
+
"options",
|
|
15
|
+
"head",
|
|
16
|
+
"patch",
|
|
17
|
+
"trace",
|
|
18
|
+
];
|
|
19
|
+
/** Number of object keys in a JSON value; compare with the compiler's `maxDocumentKeys` (250,000). */
|
|
20
|
+
export function countKeys(value) {
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.reduce((sum, item) => sum + countKeys(item), 0);
|
|
23
|
+
if (value && typeof value === "object") {
|
|
24
|
+
return Object.entries(value).reduce((sum, [, item]) => sum + 1 + countKeys(item), 0);
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
/** Describes a key count against compile-release's default `maxDocumentKeys`, and what to do if it doesn't fit. */
|
|
29
|
+
export function keyBudgetNote(keys, limit) {
|
|
30
|
+
return keys <= limit
|
|
31
|
+
? `fits compile-release's default maxDocumentKeys=${limit}`
|
|
32
|
+
: `exceeds compile-release's default maxDocumentKeys=${limit}; pass --max-document-keys to compile-release`;
|
|
33
|
+
}
|
|
34
|
+
function collectRefs(value, into) {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
for (const item of value)
|
|
37
|
+
collectRefs(item, into);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!value || typeof value !== "object")
|
|
41
|
+
return;
|
|
42
|
+
for (const [key, item] of Object.entries(value)) {
|
|
43
|
+
if (key === "$ref" &&
|
|
44
|
+
typeof item === "string" &&
|
|
45
|
+
item.startsWith("#/components/"))
|
|
46
|
+
into.add(item);
|
|
47
|
+
else
|
|
48
|
+
collectRefs(item, into);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** A `$ref` path segment is a URI-fragment-encoded JSON pointer token: decode `%xx`, then `~1`
|
|
52
|
+
* (`/`) and `~0` (`~`), in that order, so a component named e.g. `a/b` round-trips. */
|
|
53
|
+
function decodeRefSegment(raw) {
|
|
54
|
+
return decodeURIComponent(raw).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
55
|
+
}
|
|
56
|
+
function refParts(ref) {
|
|
57
|
+
const [, , kind, name] = ref.split("/");
|
|
58
|
+
if (kind === undefined || name === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
return { kind, name: decodeRefSegment(name) };
|
|
61
|
+
}
|
|
62
|
+
function securitySchemeNames(requirements, into) {
|
|
63
|
+
if (!Array.isArray(requirements))
|
|
64
|
+
return;
|
|
65
|
+
for (const requirement of requirements) {
|
|
66
|
+
if (!requirement || typeof requirement !== "object")
|
|
67
|
+
continue;
|
|
68
|
+
for (const name of Object.keys(requirement))
|
|
69
|
+
into.add(name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Walks `document.paths`, keeping only operations matching `selection`, and reports which
|
|
73
|
+
* selectors actually matched something (an unmatched selector is the caller's problem, not
|
|
74
|
+
* this function's — checked by `sliceSpec`). */
|
|
75
|
+
function selectOperations(document, selection) {
|
|
76
|
+
const tags = new Set(selection.tags);
|
|
77
|
+
const operations = new Set(selection.operations);
|
|
78
|
+
const matchedTags = new Set();
|
|
79
|
+
const matchedOperations = new Set();
|
|
80
|
+
const keptOperations = [];
|
|
81
|
+
const paths = {};
|
|
82
|
+
for (const [path, item] of Object.entries((document.paths ?? {}))) {
|
|
83
|
+
const source = item;
|
|
84
|
+
const target = { ...source };
|
|
85
|
+
let keptAny = false;
|
|
86
|
+
for (const method of HTTP_METHODS) {
|
|
87
|
+
const operation = source[method];
|
|
88
|
+
if (!operation)
|
|
89
|
+
continue;
|
|
90
|
+
const operationTags = Array.isArray(operation.tags)
|
|
91
|
+
? operation.tags
|
|
92
|
+
: [];
|
|
93
|
+
const matchingTags = operationTags.filter((tag) => tags.has(tag));
|
|
94
|
+
const operationId = operation.operationId;
|
|
95
|
+
const matchesOperation = operationId !== undefined && operations.has(operationId);
|
|
96
|
+
if (matchingTags.length === 0 && !matchesOperation) {
|
|
97
|
+
delete target[method];
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const tag of matchingTags)
|
|
101
|
+
matchedTags.add(tag);
|
|
102
|
+
if (matchesOperation && operationId !== undefined)
|
|
103
|
+
matchedOperations.add(operationId);
|
|
104
|
+
keptAny = true;
|
|
105
|
+
keptOperations.push(operation);
|
|
106
|
+
}
|
|
107
|
+
if (!keptAny)
|
|
108
|
+
continue;
|
|
109
|
+
paths[path] = target;
|
|
110
|
+
}
|
|
111
|
+
if (matchedTags.size === 0 && matchedOperations.size === 0)
|
|
112
|
+
throw new Error("The selection selected no operations.");
|
|
113
|
+
const unmatched = [
|
|
114
|
+
...[...tags].filter((tag) => !matchedTags.has(tag)),
|
|
115
|
+
...[...operations].filter((id) => !matchedOperations.has(id)),
|
|
116
|
+
];
|
|
117
|
+
if (unmatched.length > 0)
|
|
118
|
+
throw new Error(`Selection matched nothing for: ${unmatched.join(", ")}`);
|
|
119
|
+
return { paths, keptOperations };
|
|
120
|
+
}
|
|
121
|
+
/** Transitive closure over `$ref` keys only, starting from the kept paths. Discriminator
|
|
122
|
+
* mappings are plain strings and are followed separately (`pruneDiscriminatorMapping`), so a
|
|
123
|
+
* pruned mapping target can be dropped instead of resolved. */
|
|
124
|
+
function resolveReachableRefs(paths, components) {
|
|
125
|
+
const reachable = new Set();
|
|
126
|
+
const pending = new Set();
|
|
127
|
+
collectRefs(paths, pending);
|
|
128
|
+
while (pending.size) {
|
|
129
|
+
const ref = pending.values().next().value;
|
|
130
|
+
pending.delete(ref);
|
|
131
|
+
if (reachable.has(ref))
|
|
132
|
+
continue;
|
|
133
|
+
reachable.add(ref);
|
|
134
|
+
const parts = refParts(ref);
|
|
135
|
+
const component = parts !== undefined ? components[parts.kind]?.[parts.name] : undefined;
|
|
136
|
+
if (component === undefined)
|
|
137
|
+
throw new Error(`Unresolvable reference ${ref}`);
|
|
138
|
+
const next = new Set();
|
|
139
|
+
collectRefs(component, next);
|
|
140
|
+
for (const item of next)
|
|
141
|
+
if (!reachable.has(item))
|
|
142
|
+
pending.add(item);
|
|
143
|
+
}
|
|
144
|
+
return reachable;
|
|
145
|
+
}
|
|
146
|
+
/** Security schemes are referenced by name in `security` requirements, not by `$ref`, so the
|
|
147
|
+
* closure in `resolveReachableRefs` never finds them; collect the names the kept operations (or
|
|
148
|
+
* the root, when an operation has no override) actually use. */
|
|
149
|
+
function collectUsedSecuritySchemes(document, keptOperations) {
|
|
150
|
+
const usedSchemes = new Set();
|
|
151
|
+
securitySchemeNames(document.security, usedSchemes);
|
|
152
|
+
for (const operation of keptOperations)
|
|
153
|
+
securitySchemeNames(operation.security, usedSchemes);
|
|
154
|
+
return usedSchemes;
|
|
155
|
+
}
|
|
156
|
+
function buildPrunedComponents(components, reachable, usedSchemes) {
|
|
157
|
+
const prunedComponents = {};
|
|
158
|
+
for (const ref of reachable) {
|
|
159
|
+
const parts = refParts(ref);
|
|
160
|
+
if (parts === undefined)
|
|
161
|
+
continue;
|
|
162
|
+
const bucket = components[parts.kind];
|
|
163
|
+
if (bucket === undefined)
|
|
164
|
+
continue;
|
|
165
|
+
let component = bucket[parts.name];
|
|
166
|
+
if (parts.kind === "schemas" &&
|
|
167
|
+
component &&
|
|
168
|
+
typeof component === "object") {
|
|
169
|
+
component = pruneDiscriminatorMapping(component, reachable);
|
|
170
|
+
}
|
|
171
|
+
const target = prunedComponents[parts.kind] ?? {};
|
|
172
|
+
target[parts.name] = component;
|
|
173
|
+
prunedComponents[parts.kind] = target;
|
|
174
|
+
}
|
|
175
|
+
if (usedSchemes.size > 0) {
|
|
176
|
+
const schemes = components.securitySchemes ?? {};
|
|
177
|
+
const target = {};
|
|
178
|
+
for (const name of usedSchemes)
|
|
179
|
+
if (name in schemes)
|
|
180
|
+
target[name] = schemes[name];
|
|
181
|
+
if (Object.keys(target).length > 0)
|
|
182
|
+
prunedComponents.securitySchemes = target;
|
|
183
|
+
}
|
|
184
|
+
return prunedComponents;
|
|
185
|
+
}
|
|
186
|
+
/** Pure: returns a new document containing only the selected operations and what they reference. */
|
|
187
|
+
export function sliceSpec(document, selection) {
|
|
188
|
+
const { paths, keptOperations } = selectOperations(document, selection);
|
|
189
|
+
const components = (document.components ?? {});
|
|
190
|
+
const reachable = resolveReachableRefs(paths, components);
|
|
191
|
+
const usedSchemes = collectUsedSecuritySchemes(document, keptOperations);
|
|
192
|
+
const prunedComponents = buildPrunedComponents(components, reachable, usedSchemes);
|
|
193
|
+
const server = document.servers?.[0]?.url;
|
|
194
|
+
if (!server)
|
|
195
|
+
throw new Error("The document names no server.");
|
|
196
|
+
return {
|
|
197
|
+
...document,
|
|
198
|
+
servers: [{ url: new URL(server).origin }],
|
|
199
|
+
paths,
|
|
200
|
+
components: prunedComponents,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Drops `discriminator.mapping` entries whose target is not in the kept closure, on a shallow
|
|
204
|
+
* copy — the schema itself is left aliased to the source document when there is nothing to prune. */
|
|
205
|
+
function pruneDiscriminatorMapping(schema, reachable) {
|
|
206
|
+
const discriminator = schema.discriminator;
|
|
207
|
+
const mapping = discriminator?.mapping;
|
|
208
|
+
if (!discriminator || !mapping)
|
|
209
|
+
return schema;
|
|
210
|
+
const prunedMapping = {};
|
|
211
|
+
for (const [key, target] of Object.entries(mapping))
|
|
212
|
+
if (reachable.has(target))
|
|
213
|
+
prunedMapping[key] = target;
|
|
214
|
+
if (Object.keys(prunedMapping).length === Object.keys(mapping).length)
|
|
215
|
+
return schema;
|
|
216
|
+
return {
|
|
217
|
+
...schema,
|
|
218
|
+
discriminator: { ...discriminator, mapping: prunedMapping },
|
|
219
|
+
};
|
|
220
|
+
}
|
package/package.json
CHANGED