@knitli/openapi-mcp 0.0.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-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +362 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +249 -0
- package/dist/compile.d.ts +14 -0
- package/dist/compile.js +132 -0
- package/dist/compiler.d.ts +13 -0
- package/dist/compiler.js +7 -0
- package/dist/conformance/fixtures.d.ts +24 -0
- package/dist/conformance/fixtures.js +119 -0
- package/dist/conformance/index.d.ts +2 -0
- package/dist/conformance/index.js +2 -0
- package/dist/conformance/runtime-suite.d.ts +31 -0
- package/dist/conformance/runtime-suite.js +343 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/load.d.ts +31 -0
- package/dist/load.js +20 -0
- package/dist/operations.d.ts +27 -0
- package/dist/operations.js +231 -0
- package/dist/permissions.d.ts +31 -0
- package/dist/permissions.js +89 -0
- package/dist/release/compile-release.d.ts +33 -0
- package/dist/release/compile-release.js +1620 -0
- package/dist/release/load-v4.d.ts +29 -0
- package/dist/release/load-v4.js +297 -0
- package/dist/release/manifest-builder.d.ts +19 -0
- package/dist/release/manifest-builder.js +28 -0
- package/dist/release/publish.d.ts +45 -0
- package/dist/release/publish.js +346 -0
- package/dist/release/reference-map.d.ts +53 -0
- package/dist/release/reference-map.js +347 -0
- package/dist/release/schema-v4.d.ts +5 -0
- package/dist/release/schema-v4.js +91 -0
- package/dist/runtime/action-permit.d.ts +11 -0
- package/dist/runtime/action-permit.js +1 -0
- package/dist/runtime/classify.d.ts +16 -0
- package/dist/runtime/classify.js +279 -0
- package/dist/runtime/credential-binding.d.ts +3 -0
- package/dist/runtime/credential-binding.js +101 -0
- package/dist/runtime/digest.d.ts +8 -0
- package/dist/runtime/digest.js +80 -0
- package/dist/runtime/errors.d.ts +14 -0
- package/dist/runtime/errors.js +15 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +14 -0
- package/dist/runtime/manifest.d.ts +33 -0
- package/dist/runtime/manifest.js +489 -0
- package/dist/runtime/prepared-call.d.ts +39 -0
- package/dist/runtime/prepared-call.js +566 -0
- package/dist/runtime/references.d.ts +7 -0
- package/dist/runtime/references.js +197 -0
- package/dist/runtime/runtime.d.ts +25 -0
- package/dist/runtime/runtime.js +1272 -0
- package/dist/runtime/schema-keywords.d.ts +2 -0
- package/dist/runtime/schema-keywords.js +30 -0
- package/dist/runtime/schema-resolver.d.ts +10 -0
- package/dist/runtime/schema-resolver.js +237 -0
- package/dist/runtime/serialize.d.ts +12 -0
- package/dist/runtime/serialize.js +877 -0
- package/dist/runtime/store.d.ts +52 -0
- package/dist/runtime/store.js +582 -0
- package/dist/runtime/strict-json.d.ts +19 -0
- package/dist/runtime/strict-json.js +539 -0
- package/dist/runtime/types.d.ts +457 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/verify-record.d.ts +6 -0
- package/dist/runtime/verify-record.js +492 -0
- package/dist/runtime/versions.d.ts +63 -0
- package/dist/runtime/versions.js +86 -0
- package/dist/safety.d.ts +28 -0
- package/dist/safety.js +60 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +52 -0
- package/dist/schemas.d.ts +9 -0
- package/dist/schemas.js +16 -0
- package/dist/sign.d.ts +20 -0
- package/dist/sign.js +55 -0
- package/dist/sqlite/auth.d.ts +33 -0
- package/dist/sqlite/auth.js +973 -0
- package/dist/sqlite/catalog-store.d.ts +20 -0
- package/dist/sqlite/catalog-store.js +370 -0
- package/dist/sqlite/destination-guard.d.ts +24 -0
- package/dist/sqlite/destination-guard.js +140 -0
- package/dist/sqlite/generation-store.d.ts +12 -0
- package/dist/sqlite/generation-store.js +721 -0
- package/dist/sqlite/guarded-fetch.d.ts +24 -0
- package/dist/sqlite/guarded-fetch.js +626 -0
- package/dist/sqlite/index.d.ts +5 -0
- package/dist/sqlite/index.js +5 -0
- package/dist/sqlite/oauth-resource.d.ts +2 -0
- package/dist/sqlite/oauth-resource.js +15 -0
- package/dist/sqlite/release-completion.d.ts +5 -0
- package/dist/sqlite/release-completion.js +80 -0
- package/dist/stdio/action-broker.d.ts +15 -0
- package/dist/stdio/action-broker.js +136 -0
- package/dist/stdio/authorizer.d.ts +30 -0
- package/dist/stdio/authorizer.js +484 -0
- package/dist/stdio/config.d.ts +77 -0
- package/dist/stdio/config.js +144 -0
- package/dist/stdio/exact-policy.d.ts +41 -0
- package/dist/stdio/exact-policy.js +361 -0
- package/dist/stdio/index.d.ts +6 -0
- package/dist/stdio/index.js +148 -0
- package/dist/stdio/render.d.ts +8 -0
- package/dist/stdio/render.js +253 -0
- package/dist/stdio/request-state.d.ts +18 -0
- package/dist/stdio/request-state.js +79 -0
- package/dist/stdio/search.d.ts +10 -0
- package/dist/stdio/search.js +76 -0
- package/dist/stdio/server.d.ts +29 -0
- package/dist/stdio/server.js +309 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { sha256 } from "./digest.js";
|
|
2
|
+
import { OpenApiMcpError } from "./errors.js";
|
|
3
|
+
import { parseTypedRecordId } from "./references.js";
|
|
4
|
+
import { schemaChildKind } from "./schema-keywords.js";
|
|
5
|
+
import { canonicalJsonBounded, parseJsonStrict } from "./strict-json.js";
|
|
6
|
+
import { MAX_OPERATION_TAG_BYTES, MAX_OPERATION_TAG_BYTES_TOTAL, MAX_OPERATION_TAGS, resolveRuntimeLimits, } from "./versions.js";
|
|
7
|
+
const operationKeys = [
|
|
8
|
+
"advisory",
|
|
9
|
+
"api",
|
|
10
|
+
"deprecated",
|
|
11
|
+
"id",
|
|
12
|
+
"method",
|
|
13
|
+
"operationId",
|
|
14
|
+
"origin",
|
|
15
|
+
"parameters",
|
|
16
|
+
"path",
|
|
17
|
+
"requestBody",
|
|
18
|
+
"schemaIds",
|
|
19
|
+
"summary",
|
|
20
|
+
"tags",
|
|
21
|
+
];
|
|
22
|
+
const legacyOperationKeys = operationKeys.filter((key) => key !== "tags");
|
|
23
|
+
const schemaKeys = ["id", "schema"];
|
|
24
|
+
const wrapperKeys = ["id", "logicalDigest", "record"];
|
|
25
|
+
const parameterKeys = [
|
|
26
|
+
"allowReserved",
|
|
27
|
+
"deprecated",
|
|
28
|
+
"explode",
|
|
29
|
+
"in",
|
|
30
|
+
"name",
|
|
31
|
+
"required",
|
|
32
|
+
"style",
|
|
33
|
+
"value",
|
|
34
|
+
];
|
|
35
|
+
const schemaUseSchemaKeys = ["kind", "schemaId"];
|
|
36
|
+
const schemaUseContentKeys = ["kind", "mediaType", "schemaId"];
|
|
37
|
+
const requestBodyKeys = ["content", "required"];
|
|
38
|
+
const requestBodyMediaKeys = ["encoding", "mediaType", "schemaId"];
|
|
39
|
+
const encodingKeys = [
|
|
40
|
+
"allowReserved",
|
|
41
|
+
"contentType",
|
|
42
|
+
"explode",
|
|
43
|
+
"headers",
|
|
44
|
+
"property",
|
|
45
|
+
"style",
|
|
46
|
+
];
|
|
47
|
+
const encodingHeaderKeys = ["name", "required", "value"];
|
|
48
|
+
const digestPattern = /^[0-9a-f]{64}$/;
|
|
49
|
+
const mediaTypePattern = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/;
|
|
50
|
+
const headerPattern = /^[a-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
51
|
+
const locations = ["path", "query", "header", "cookie"];
|
|
52
|
+
const styles = [
|
|
53
|
+
"matrix",
|
|
54
|
+
"label",
|
|
55
|
+
"form",
|
|
56
|
+
"simple",
|
|
57
|
+
"spaceDelimited",
|
|
58
|
+
"pipeDelimited",
|
|
59
|
+
"deepObject",
|
|
60
|
+
];
|
|
61
|
+
function mismatch(message) {
|
|
62
|
+
return new OpenApiMcpError("RECORD_DIGEST_MISMATCH", message);
|
|
63
|
+
}
|
|
64
|
+
function exactObject(value, keys, label) {
|
|
65
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
66
|
+
throw mismatch(`${label} is not an object`);
|
|
67
|
+
const prototype = Object.getPrototypeOf(value);
|
|
68
|
+
if (prototype !== null && prototype !== Object.prototype)
|
|
69
|
+
throw mismatch(`${label} prototype is invalid`);
|
|
70
|
+
const actual = Reflect.ownKeys(value);
|
|
71
|
+
if (actual.some((key) => typeof key !== "string"))
|
|
72
|
+
throw mismatch(`${label} has a symbol property`);
|
|
73
|
+
const sorted = actual.sort();
|
|
74
|
+
const expected = [...keys].sort();
|
|
75
|
+
if (sorted.length !== expected.length ||
|
|
76
|
+
sorted.some((key, index) => key !== expected[index]))
|
|
77
|
+
throw mismatch(`${label} shape is invalid`);
|
|
78
|
+
for (const key of sorted) {
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
80
|
+
if (descriptor === undefined ||
|
|
81
|
+
!("value" in descriptor) ||
|
|
82
|
+
!descriptor.enumerable)
|
|
83
|
+
throw mismatch(`${label} properties must be own data`);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function extractStoredRecord(value) {
|
|
88
|
+
try {
|
|
89
|
+
const object = exactObject(value, wrapperKeys, "stored row");
|
|
90
|
+
const id = Object.getOwnPropertyDescriptor(object, "id");
|
|
91
|
+
const logicalDigest = Object.getOwnPropertyDescriptor(object, "logicalDigest");
|
|
92
|
+
const record = Object.getOwnPropertyDescriptor(object, "record");
|
|
93
|
+
if (id === undefined ||
|
|
94
|
+
!("value" in id) ||
|
|
95
|
+
logicalDigest === undefined ||
|
|
96
|
+
!("value" in logicalDigest) ||
|
|
97
|
+
record === undefined ||
|
|
98
|
+
!("value" in record))
|
|
99
|
+
throw mismatch("Stored row properties must be own data");
|
|
100
|
+
return {
|
|
101
|
+
id: id.value,
|
|
102
|
+
logicalDigest: logicalDigest.value,
|
|
103
|
+
record: record.value,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof OpenApiMcpError &&
|
|
108
|
+
error.code === "RECORD_DIGEST_MISMATCH")
|
|
109
|
+
throw error;
|
|
110
|
+
throw mismatch("Stored row wrapper cannot be inspected");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function schemaId(value, label) {
|
|
114
|
+
try {
|
|
115
|
+
if (typeof value !== "string" ||
|
|
116
|
+
!parseTypedRecordId(value).startsWith("schema:"))
|
|
117
|
+
throw new Error();
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw mismatch(`${label} is not a typed schema ID`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function validateOperationTags(value) {
|
|
125
|
+
if (!Array.isArray(value) || value.length > MAX_OPERATION_TAGS)
|
|
126
|
+
throw mismatch("Operation record tags are invalid");
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
let totalBytes = 0;
|
|
129
|
+
for (const tag of value) {
|
|
130
|
+
if (typeof tag !== "string" ||
|
|
131
|
+
tag.length === 0 ||
|
|
132
|
+
tag.length > MAX_OPERATION_TAG_BYTES)
|
|
133
|
+
throw mismatch("Operation record tags are invalid");
|
|
134
|
+
const tagBytes = new TextEncoder().encode(tag).byteLength;
|
|
135
|
+
if (tagBytes > MAX_OPERATION_TAG_BYTES ||
|
|
136
|
+
totalBytes + tagBytes > MAX_OPERATION_TAG_BYTES_TOTAL ||
|
|
137
|
+
seen.has(tag))
|
|
138
|
+
throw mismatch("Operation record tags are invalid");
|
|
139
|
+
seen.add(tag);
|
|
140
|
+
totalBytes += tagBytes;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function sortedUnique(values, label) {
|
|
144
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
145
|
+
if (values[index - 1] >= values[index])
|
|
146
|
+
throw mismatch(`${label} must be sorted and unique`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function validateSchemaUse(value, label) {
|
|
150
|
+
const candidate = exactObject(value, value?.kind === "content"
|
|
151
|
+
? schemaUseContentKeys
|
|
152
|
+
: schemaUseSchemaKeys, label);
|
|
153
|
+
if (candidate.kind !== "schema" && candidate.kind !== "content")
|
|
154
|
+
throw mismatch(`${label} discriminant is invalid`);
|
|
155
|
+
if (candidate.kind === "content" &&
|
|
156
|
+
(typeof candidate.mediaType !== "string" ||
|
|
157
|
+
!mediaTypePattern.test(candidate.mediaType))) {
|
|
158
|
+
throw mismatch(`${label} media type is invalid`);
|
|
159
|
+
}
|
|
160
|
+
return schemaId(candidate.schemaId, `${label} schema ID`);
|
|
161
|
+
}
|
|
162
|
+
function validateParameters(value) {
|
|
163
|
+
if (!Array.isArray(value))
|
|
164
|
+
throw mismatch("Operation parameters are invalid");
|
|
165
|
+
const schemaIds = [];
|
|
166
|
+
const pathNames = [];
|
|
167
|
+
let previousLocation = -1;
|
|
168
|
+
let previousName = "";
|
|
169
|
+
for (const [index, raw] of value.entries()) {
|
|
170
|
+
const parameter = exactObject(raw, parameterKeys, `parameter ${index}`);
|
|
171
|
+
if (typeof parameter.name !== "string" ||
|
|
172
|
+
parameter.name.length === 0 ||
|
|
173
|
+
typeof parameter.in !== "string" ||
|
|
174
|
+
!locations.includes(parameter.in) ||
|
|
175
|
+
typeof parameter.required !== "boolean" ||
|
|
176
|
+
typeof parameter.deprecated !== "boolean" ||
|
|
177
|
+
typeof parameter.style !== "string" ||
|
|
178
|
+
!styles.includes(parameter.style) ||
|
|
179
|
+
typeof parameter.explode !== "boolean" ||
|
|
180
|
+
typeof parameter.allowReserved !== "boolean")
|
|
181
|
+
throw mismatch(`parameter ${index} fields are invalid`);
|
|
182
|
+
if (parameter.in === "path" && parameter.required !== true)
|
|
183
|
+
throw mismatch("Path parameters must be required");
|
|
184
|
+
if (parameter.in === "path")
|
|
185
|
+
pathNames.push(parameter.name);
|
|
186
|
+
const location = locations.indexOf(parameter.in);
|
|
187
|
+
if (location < previousLocation ||
|
|
188
|
+
(location === previousLocation && parameter.name <= previousName))
|
|
189
|
+
throw mismatch("Operation parameters must be sorted and unique");
|
|
190
|
+
previousLocation = location;
|
|
191
|
+
previousName = parameter.name;
|
|
192
|
+
schemaIds.push(validateSchemaUse(parameter.value, `parameter ${index} value`));
|
|
193
|
+
}
|
|
194
|
+
return { schemaIds, pathNames };
|
|
195
|
+
}
|
|
196
|
+
export function parseOperationPathTemplateV4(path) {
|
|
197
|
+
if (!path.startsWith("/") ||
|
|
198
|
+
path.startsWith("//") ||
|
|
199
|
+
!/^[\x21-\x7e]+$/.test(path) ||
|
|
200
|
+
path.includes("\\") ||
|
|
201
|
+
path.includes("?") ||
|
|
202
|
+
path.includes("#"))
|
|
203
|
+
throw new Error("operation path must be a root-relative request target");
|
|
204
|
+
const matches = [...path.matchAll(/\{([^{}]+)\}/g)];
|
|
205
|
+
const withoutValidTemplates = path.replace(/\{([^{}]+)\}/g, "");
|
|
206
|
+
if (withoutValidTemplates.includes("{") ||
|
|
207
|
+
withoutValidTemplates.includes("}")) {
|
|
208
|
+
throw new Error("operation path template is malformed");
|
|
209
|
+
}
|
|
210
|
+
const variables = matches.map((match) => match[1]);
|
|
211
|
+
if (new Set(variables).size !== variables.length) {
|
|
212
|
+
throw new Error("operation path template variables must be unique");
|
|
213
|
+
}
|
|
214
|
+
const comparablePath = path.replace(/\{([^{}]+)\}/g, "openapi-template");
|
|
215
|
+
const normalized = new URL(comparablePath, "https://openapi.invalid");
|
|
216
|
+
if (normalized.origin !== "https://openapi.invalid" ||
|
|
217
|
+
normalized.pathname !== comparablePath ||
|
|
218
|
+
normalized.search !== "" ||
|
|
219
|
+
normalized.hash !== "")
|
|
220
|
+
throw new Error("operation path must be normalized");
|
|
221
|
+
return variables;
|
|
222
|
+
}
|
|
223
|
+
function validatePathTemplate(path, pathNames) {
|
|
224
|
+
let variables;
|
|
225
|
+
try {
|
|
226
|
+
variables = parseOperationPathTemplateV4(path);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
throw mismatch("Operation path template is invalid");
|
|
230
|
+
}
|
|
231
|
+
const sortedVariables = [...variables].sort();
|
|
232
|
+
const sortedPathNames = [...pathNames].sort();
|
|
233
|
+
if (sortedVariables.length !== sortedPathNames.length ||
|
|
234
|
+
sortedVariables.some((name, index) => name !== sortedPathNames[index])) {
|
|
235
|
+
throw mismatch("Operation path parameters must exactly match path-template variables");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function validateRequestBody(value) {
|
|
239
|
+
if (value === null)
|
|
240
|
+
return [];
|
|
241
|
+
const body = exactObject(value, requestBodyKeys, "request body");
|
|
242
|
+
if (typeof body.required !== "boolean" ||
|
|
243
|
+
!Array.isArray(body.content) ||
|
|
244
|
+
body.content.length === 0)
|
|
245
|
+
throw mismatch("Request body fields are invalid");
|
|
246
|
+
const schemaIds = [];
|
|
247
|
+
let previousMedia = "";
|
|
248
|
+
for (const [mediaIndex, rawMedia] of body.content.entries()) {
|
|
249
|
+
const media = exactObject(rawMedia, requestBodyMediaKeys, `request body media ${mediaIndex}`);
|
|
250
|
+
if (typeof media.mediaType !== "string" ||
|
|
251
|
+
!mediaTypePattern.test(media.mediaType) ||
|
|
252
|
+
media.mediaType <= previousMedia ||
|
|
253
|
+
!Array.isArray(media.encoding)) {
|
|
254
|
+
throw mismatch("Request body media entries are invalid or unsorted");
|
|
255
|
+
}
|
|
256
|
+
previousMedia = media.mediaType;
|
|
257
|
+
schemaIds.push(schemaId(media.schemaId, "request body schema ID"));
|
|
258
|
+
let previousProperty = "";
|
|
259
|
+
for (const [encodingIndex, rawEncoding] of media.encoding.entries()) {
|
|
260
|
+
const encoding = exactObject(rawEncoding, encodingKeys, `encoding ${encodingIndex}`);
|
|
261
|
+
if (typeof encoding.property !== "string" ||
|
|
262
|
+
encoding.property.length === 0 ||
|
|
263
|
+
encoding.property <= previousProperty ||
|
|
264
|
+
!(encoding.contentType === null ||
|
|
265
|
+
(typeof encoding.contentType === "string" &&
|
|
266
|
+
mediaTypePattern.test(encoding.contentType))) ||
|
|
267
|
+
!(encoding.style === null ||
|
|
268
|
+
(typeof encoding.style === "string" &&
|
|
269
|
+
styles.includes(encoding.style))) ||
|
|
270
|
+
!(encoding.explode === null || typeof encoding.explode === "boolean") ||
|
|
271
|
+
typeof encoding.allowReserved !== "boolean" ||
|
|
272
|
+
!Array.isArray(encoding.headers))
|
|
273
|
+
throw mismatch("Request body encoding fields are invalid or unsorted");
|
|
274
|
+
previousProperty = encoding.property;
|
|
275
|
+
let previousHeader = "";
|
|
276
|
+
for (const [headerIndex, rawHeader] of encoding.headers.entries()) {
|
|
277
|
+
const header = exactObject(rawHeader, encodingHeaderKeys, `encoding header ${headerIndex}`);
|
|
278
|
+
if (typeof header.name !== "string" ||
|
|
279
|
+
!headerPattern.test(header.name) ||
|
|
280
|
+
header.name <= previousHeader ||
|
|
281
|
+
typeof header.required !== "boolean") {
|
|
282
|
+
throw mismatch("Encoding headers are invalid or unsorted");
|
|
283
|
+
}
|
|
284
|
+
previousHeader = header.name;
|
|
285
|
+
schemaIds.push(validateSchemaUse(header.value, `encoding header ${headerIndex} value`));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return schemaIds;
|
|
290
|
+
}
|
|
291
|
+
function validateSchemaReferences(value) {
|
|
292
|
+
if (typeof value === "boolean")
|
|
293
|
+
return;
|
|
294
|
+
const stack = [
|
|
295
|
+
{ label: "schema", value },
|
|
296
|
+
];
|
|
297
|
+
const pushSubschema = (child, label) => {
|
|
298
|
+
if (typeof child === "boolean")
|
|
299
|
+
return;
|
|
300
|
+
if (typeof child !== "object" || child === null || Array.isArray(child))
|
|
301
|
+
throw mismatch(`${label} must be a boolean or object schema`);
|
|
302
|
+
stack.push({ label, value: child });
|
|
303
|
+
};
|
|
304
|
+
while (stack.length > 0) {
|
|
305
|
+
const current = stack.pop();
|
|
306
|
+
if (!current)
|
|
307
|
+
continue;
|
|
308
|
+
if (typeof current.value !== "object" ||
|
|
309
|
+
current.value === null ||
|
|
310
|
+
Array.isArray(current.value))
|
|
311
|
+
throw mismatch(`${current.label} must be an object schema`);
|
|
312
|
+
for (const [key, child] of Object.entries(current.value)) {
|
|
313
|
+
if (["$dynamicRef", "$recursiveRef", "$anchor", "$dynamicAnchor"].includes(key))
|
|
314
|
+
throw mismatch(`${key} is unsupported in a schema record`);
|
|
315
|
+
if (key === "$ref")
|
|
316
|
+
schemaId(child, "schema $ref");
|
|
317
|
+
else if (schemaChildKind(key) === "single")
|
|
318
|
+
pushSubschema(child, `schema ${key}`);
|
|
319
|
+
else if (schemaChildKind(key) === "array") {
|
|
320
|
+
if (!Array.isArray(child))
|
|
321
|
+
throw mismatch(`schema ${key} must be an array`);
|
|
322
|
+
child.forEach((entry, index) => {
|
|
323
|
+
pushSubschema(entry, `schema ${key}[${index}]`);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else if (schemaChildKind(key) === "map") {
|
|
327
|
+
if (typeof child !== "object" || child === null || Array.isArray(child))
|
|
328
|
+
throw mismatch(`schema ${key} must be an object`);
|
|
329
|
+
for (const [name, entry] of Object.entries(child))
|
|
330
|
+
pushSubschema(entry, `schema ${key}.${name}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function validateRecord(record, wrapperId, format) {
|
|
336
|
+
let id;
|
|
337
|
+
try {
|
|
338
|
+
if (typeof wrapperId !== "string")
|
|
339
|
+
throw new Error();
|
|
340
|
+
id = parseTypedRecordId(wrapperId);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
throw mismatch("Stored row ID is malformed");
|
|
344
|
+
}
|
|
345
|
+
const isOperation = id.startsWith("operation:");
|
|
346
|
+
const object = exactObject(record, isOperation
|
|
347
|
+
? format === 5
|
|
348
|
+
? operationKeys
|
|
349
|
+
: legacyOperationKeys
|
|
350
|
+
: schemaKeys, "logical record");
|
|
351
|
+
if (object.id !== id)
|
|
352
|
+
throw mismatch("Stored row and logical record IDs disagree");
|
|
353
|
+
if (isOperation) {
|
|
354
|
+
if (typeof object.api !== "string" ||
|
|
355
|
+
typeof object.operationId !== "string" ||
|
|
356
|
+
![
|
|
357
|
+
"GET",
|
|
358
|
+
"HEAD",
|
|
359
|
+
"POST",
|
|
360
|
+
"PUT",
|
|
361
|
+
"PATCH",
|
|
362
|
+
"DELETE",
|
|
363
|
+
"OPTIONS",
|
|
364
|
+
"TRACE",
|
|
365
|
+
].includes(object.method) ||
|
|
366
|
+
typeof object.path !== "string" ||
|
|
367
|
+
typeof object.origin !== "string" ||
|
|
368
|
+
!(object.summary === null || typeof object.summary === "string") ||
|
|
369
|
+
typeof object.deprecated !== "boolean" ||
|
|
370
|
+
!Array.isArray(object.schemaIds) ||
|
|
371
|
+
!(typeof object.advisory === "object" &&
|
|
372
|
+
object.advisory !== null &&
|
|
373
|
+
!Array.isArray(object.advisory)))
|
|
374
|
+
throw mismatch("Operation record fields are invalid");
|
|
375
|
+
const tags = format === 5 ? object.tags : [];
|
|
376
|
+
validateOperationTags(tags);
|
|
377
|
+
if (`operation:${object.api}:${object.operationId}` !== id) {
|
|
378
|
+
throw mismatch("Operation record ID does not match its API and operation ID");
|
|
379
|
+
}
|
|
380
|
+
const parameters = validateParameters(object.parameters);
|
|
381
|
+
validatePathTemplate(object.path, parameters.pathNames);
|
|
382
|
+
const directSchemaIds = [
|
|
383
|
+
...parameters.schemaIds,
|
|
384
|
+
...validateRequestBody(object.requestBody),
|
|
385
|
+
].sort();
|
|
386
|
+
const declaredSchemaIds = object.schemaIds.map((value) => schemaId(value, "Operation schema ID"));
|
|
387
|
+
sortedUnique(declaredSchemaIds, "Operation schema IDs");
|
|
388
|
+
const expectedSchemaIds = [...new Set(directSchemaIds)].sort();
|
|
389
|
+
if (declaredSchemaIds.length !== expectedSchemaIds.length ||
|
|
390
|
+
declaredSchemaIds.some((value, index) => value !== expectedSchemaIds[index])) {
|
|
391
|
+
throw mismatch("Operation schema IDs do not equal direct schema uses");
|
|
392
|
+
}
|
|
393
|
+
return (format === 5
|
|
394
|
+
? object
|
|
395
|
+
: { ...object, tags });
|
|
396
|
+
}
|
|
397
|
+
else if (typeof object.schema !== "boolean" &&
|
|
398
|
+
(typeof object.schema !== "object" ||
|
|
399
|
+
object.schema === null ||
|
|
400
|
+
Array.isArray(object.schema))) {
|
|
401
|
+
throw mismatch("Schema record is invalid");
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
validateSchemaReferences(object.schema);
|
|
405
|
+
}
|
|
406
|
+
return object;
|
|
407
|
+
}
|
|
408
|
+
function detached(record, limits) {
|
|
409
|
+
let json;
|
|
410
|
+
try {
|
|
411
|
+
json = canonicalJsonBounded(record, {
|
|
412
|
+
maxBytes: limits.maxRecordBytes,
|
|
413
|
+
maxDepth: limits.maxJsonDepth,
|
|
414
|
+
maxNodes: Math.max(1, limits.maxRecordBytes),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
throw mismatch("Logical record cannot be bounded and canonicalized");
|
|
419
|
+
}
|
|
420
|
+
return parseJsonStrict(json, {
|
|
421
|
+
maxBytes: limits.maxRecordBytes,
|
|
422
|
+
maxDepth: limits.maxJsonDepth,
|
|
423
|
+
maxKeys: limits.maxManifestRecords,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function deepFreeze(value) {
|
|
427
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value))
|
|
428
|
+
return value;
|
|
429
|
+
for (const key of Reflect.ownKeys(value))
|
|
430
|
+
deepFreeze(value[key]);
|
|
431
|
+
return Object.freeze(value);
|
|
432
|
+
}
|
|
433
|
+
/** Reconstruct, hash, admit, detach, and freeze one untrusted store row. */
|
|
434
|
+
export async function verifyStoredRecord(admitted, rowValue, limitOverrides) {
|
|
435
|
+
const limits = resolveRuntimeLimits(limitOverrides);
|
|
436
|
+
const row = extractStoredRecord(rowValue);
|
|
437
|
+
let wrapperId;
|
|
438
|
+
try {
|
|
439
|
+
if (typeof row.id !== "string")
|
|
440
|
+
throw new Error();
|
|
441
|
+
wrapperId = parseTypedRecordId(row.id);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
throw mismatch("Stored row ID is malformed");
|
|
445
|
+
}
|
|
446
|
+
if (!Object.hasOwn(admitted.manifest.records, wrapperId)) {
|
|
447
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Logical record is absent from the admitted manifest");
|
|
448
|
+
}
|
|
449
|
+
if (typeof row.logicalDigest !== "string" ||
|
|
450
|
+
!digestPattern.test(row.logicalDigest))
|
|
451
|
+
throw mismatch("Stored row digest is invalid");
|
|
452
|
+
let digestInput;
|
|
453
|
+
let copy;
|
|
454
|
+
try {
|
|
455
|
+
digestInput = detached(row.record, limits);
|
|
456
|
+
copy = validateRecord(digestInput, wrapperId, admitted.manifest.format);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
if (error instanceof OpenApiMcpError &&
|
|
460
|
+
error.code === "RECORD_DIGEST_MISMATCH")
|
|
461
|
+
throw error;
|
|
462
|
+
throw mismatch("Logical record cannot be canonicalized");
|
|
463
|
+
}
|
|
464
|
+
const recordKind = copy.id.startsWith("operation:") ? "operation" : "schema";
|
|
465
|
+
const domain = `knitli.openapi-mcp.${recordKind}-record.v${admitted.manifest.format}`;
|
|
466
|
+
const digest = await sha256(domain, digestInput);
|
|
467
|
+
if (digest !== row.logicalDigest)
|
|
468
|
+
throw mismatch("Stored logical digest does not match the row");
|
|
469
|
+
if (admitted.manifest.records[copy.id] !== digest) {
|
|
470
|
+
throw mismatch("Logical record digest does not match the admitted manifest");
|
|
471
|
+
}
|
|
472
|
+
if (copy.id.startsWith("operation:")) {
|
|
473
|
+
const operation = copy;
|
|
474
|
+
let origin;
|
|
475
|
+
try {
|
|
476
|
+
origin = new URL(operation.origin);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
throw mismatch("Operation origin is invalid");
|
|
480
|
+
}
|
|
481
|
+
if (origin.protocol !== "https:" ||
|
|
482
|
+
origin.username !== "" ||
|
|
483
|
+
origin.password !== "" ||
|
|
484
|
+
origin.pathname !== "/" ||
|
|
485
|
+
origin.search !== "" ||
|
|
486
|
+
origin.hash !== "" ||
|
|
487
|
+
origin.origin !== operation.origin ||
|
|
488
|
+
!admitted.manifest.allowedOrigins.includes(operation.origin))
|
|
489
|
+
throw mismatch("Operation origin is not admitted by the manifest");
|
|
490
|
+
}
|
|
491
|
+
return deepFreeze(copy);
|
|
492
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Version of the portable runtime API and its persisted contract. */
|
|
2
|
+
export declare const RUNTIME_CONTRACT_VERSION: 1;
|
|
3
|
+
/** Version of immutable, signed logical release artifacts. */
|
|
4
|
+
export declare const ARTIFACT_FORMAT_VERSION: 5;
|
|
5
|
+
/** Version of the credential-free prepared-call representation. */
|
|
6
|
+
export declare const PREPARED_CALL_VERSION: 2;
|
|
7
|
+
/** Fixed UTF-8 envelope for a single catalog search query. */
|
|
8
|
+
export declare const MAX_SEARCH_QUERY_BYTES: number;
|
|
9
|
+
/** Fixed bounds for authenticated OpenAPI operation tags. */
|
|
10
|
+
export declare const MAX_OPERATION_TAGS = 32;
|
|
11
|
+
export declare const MAX_OPERATION_TAG_BYTES = 128;
|
|
12
|
+
export declare const MAX_OPERATION_TAG_BYTES_TOTAL: number;
|
|
13
|
+
/** Application security and availability limits for every runtime adapter. */
|
|
14
|
+
export interface RuntimeLimits {
|
|
15
|
+
/**
|
|
16
|
+
* Combined canonical operation/schema bytes admitted per release and search.
|
|
17
|
+
* Optional so complete RuntimeLimits objects written before this limit was
|
|
18
|
+
* introduced remain source-compatible.
|
|
19
|
+
*/
|
|
20
|
+
readonly maxReleaseInventoryBytes?: number;
|
|
21
|
+
readonly maxManifestBytes: number;
|
|
22
|
+
readonly maxManifestRecords: number;
|
|
23
|
+
readonly maxRecordBytes: number;
|
|
24
|
+
readonly maxJsonDepth: number;
|
|
25
|
+
readonly maxSchemaClosureBytes: number;
|
|
26
|
+
readonly maxSchemaRefHops: number;
|
|
27
|
+
readonly maxSearchResults: number;
|
|
28
|
+
readonly defaultSearchResults: number;
|
|
29
|
+
readonly maxArgumentsBytes: number;
|
|
30
|
+
readonly maxResponseBytes: number;
|
|
31
|
+
readonly maxPages: number;
|
|
32
|
+
readonly maxPaginationBytes: number;
|
|
33
|
+
readonly maxRedirects: number;
|
|
34
|
+
readonly requestDeadlineMs: number;
|
|
35
|
+
}
|
|
36
|
+
/** Fully defaulted limits used by runtime internals. */
|
|
37
|
+
interface ResolvedRuntimeLimits extends RuntimeLimits {
|
|
38
|
+
readonly maxReleaseInventoryBytes: number;
|
|
39
|
+
}
|
|
40
|
+
export declare const DEFAULT_RUNTIME_LIMITS: Readonly<{
|
|
41
|
+
readonly maxManifestBytes: number;
|
|
42
|
+
readonly maxManifestRecords: 100000;
|
|
43
|
+
readonly maxRecordBytes: number;
|
|
44
|
+
readonly maxReleaseInventoryBytes: number;
|
|
45
|
+
readonly maxJsonDepth: 64;
|
|
46
|
+
readonly maxSchemaClosureBytes: number;
|
|
47
|
+
readonly maxSchemaRefHops: 16;
|
|
48
|
+
readonly maxSearchResults: 50;
|
|
49
|
+
readonly defaultSearchResults: 10;
|
|
50
|
+
readonly maxArgumentsBytes: number;
|
|
51
|
+
readonly maxResponseBytes: number;
|
|
52
|
+
readonly maxPages: 10;
|
|
53
|
+
readonly maxPaginationBytes: number;
|
|
54
|
+
readonly maxRedirects: 3;
|
|
55
|
+
readonly requestDeadlineMs: 30000;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Apply operator limits without allowing a caller to relax the signed-runtime
|
|
59
|
+
* availability envelope. Every supplied value must be a positive safe integer
|
|
60
|
+
* no greater than the portable default.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveRuntimeLimits(overrides?: Partial<RuntimeLimits>): ResolvedRuntimeLimits;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Version of the portable runtime API and its persisted contract. */
|
|
2
|
+
export const RUNTIME_CONTRACT_VERSION = 1;
|
|
3
|
+
/** Version of immutable, signed logical release artifacts. */
|
|
4
|
+
export const ARTIFACT_FORMAT_VERSION = 5;
|
|
5
|
+
/** Version of the credential-free prepared-call representation. */
|
|
6
|
+
export const PREPARED_CALL_VERSION = 2;
|
|
7
|
+
/** Fixed UTF-8 envelope for a single catalog search query. */
|
|
8
|
+
export const MAX_SEARCH_QUERY_BYTES = 4 * 1024;
|
|
9
|
+
/** Fixed bounds for authenticated OpenAPI operation tags. */
|
|
10
|
+
export const MAX_OPERATION_TAGS = 32;
|
|
11
|
+
export const MAX_OPERATION_TAG_BYTES = 128;
|
|
12
|
+
export const MAX_OPERATION_TAG_BYTES_TOTAL = 2 * 1024;
|
|
13
|
+
export const DEFAULT_RUNTIME_LIMITS = Object.freeze({
|
|
14
|
+
maxManifestBytes: 8 * 1024 * 1024,
|
|
15
|
+
maxManifestRecords: 100_000,
|
|
16
|
+
maxRecordBytes: 1 * 1024 * 1024,
|
|
17
|
+
maxReleaseInventoryBytes: 128 * 1024 * 1024,
|
|
18
|
+
maxJsonDepth: 64,
|
|
19
|
+
maxSchemaClosureBytes: 4 * 1024 * 1024,
|
|
20
|
+
maxSchemaRefHops: 16,
|
|
21
|
+
maxSearchResults: 50,
|
|
22
|
+
defaultSearchResults: 10,
|
|
23
|
+
maxArgumentsBytes: 256 * 1024,
|
|
24
|
+
maxResponseBytes: 8 * 1024 * 1024,
|
|
25
|
+
maxPages: 10,
|
|
26
|
+
maxPaginationBytes: 16 * 1024 * 1024,
|
|
27
|
+
maxRedirects: 3,
|
|
28
|
+
requestDeadlineMs: 30_000,
|
|
29
|
+
});
|
|
30
|
+
const malformedOverridesMessage = "Runtime limits overrides must be an exact plain data object";
|
|
31
|
+
function malformedOverrides() {
|
|
32
|
+
return new RangeError(malformedOverridesMessage);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Apply operator limits without allowing a caller to relax the signed-runtime
|
|
36
|
+
* availability envelope. Every supplied value must be a positive safe integer
|
|
37
|
+
* no greater than the portable default.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveRuntimeLimits(overrides = {}) {
|
|
40
|
+
const resolved = {
|
|
41
|
+
...DEFAULT_RUNTIME_LIMITS,
|
|
42
|
+
};
|
|
43
|
+
let supplied;
|
|
44
|
+
try {
|
|
45
|
+
if (overrides === null ||
|
|
46
|
+
typeof overrides !== "object" ||
|
|
47
|
+
Array.isArray(overrides)) {
|
|
48
|
+
throw malformedOverrides();
|
|
49
|
+
}
|
|
50
|
+
const prototype = Object.getPrototypeOf(overrides);
|
|
51
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
52
|
+
throw malformedOverrides();
|
|
53
|
+
}
|
|
54
|
+
supplied = Reflect.ownKeys(overrides).map((candidate) => {
|
|
55
|
+
if (typeof candidate !== "string" ||
|
|
56
|
+
!Object.hasOwn(DEFAULT_RUNTIME_LIMITS, candidate)) {
|
|
57
|
+
throw malformedOverrides();
|
|
58
|
+
}
|
|
59
|
+
const descriptor = Object.getOwnPropertyDescriptor(overrides, candidate);
|
|
60
|
+
if (descriptor === undefined ||
|
|
61
|
+
!descriptor.enumerable ||
|
|
62
|
+
!("value" in descriptor)) {
|
|
63
|
+
throw malformedOverrides();
|
|
64
|
+
}
|
|
65
|
+
return [
|
|
66
|
+
candidate,
|
|
67
|
+
descriptor.value,
|
|
68
|
+
];
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw malformedOverrides();
|
|
73
|
+
}
|
|
74
|
+
for (const [key, value] of supplied) {
|
|
75
|
+
const maximum = DEFAULT_RUNTIME_LIMITS[key];
|
|
76
|
+
if (value === undefined ||
|
|
77
|
+
typeof value !== "number" ||
|
|
78
|
+
!Number.isSafeInteger(value) ||
|
|
79
|
+
value < 1 ||
|
|
80
|
+
value > maximum) {
|
|
81
|
+
throw new RangeError(`Runtime limit ${key} must only lower its default`);
|
|
82
|
+
}
|
|
83
|
+
resolved[key] = value;
|
|
84
|
+
}
|
|
85
|
+
return Object.freeze(resolved);
|
|
86
|
+
}
|
package/dist/safety.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Risk, Safety } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Operation-id suffixes that are POSTs by protocol but reads by semantics,
|
|
4
|
+
* keyed by the API they were derived from. Scoping is load-bearing, not
|
|
5
|
+
* tidiness: `preview` and `query` are generic enough that another mounted API
|
|
6
|
+
* will have a genuinely mutating `POST .../query`, and a global list would
|
|
7
|
+
* classify it `read` — reachable through the no-approval read tool. An API
|
|
8
|
+
* with no entry here gets no overrides, so mutating methods stay `write`.
|
|
9
|
+
*
|
|
10
|
+
* This table is engine code, not artifact data: the server recomputes safety
|
|
11
|
+
* at load and honours a stored `read` on a mutating method only if the same
|
|
12
|
+
* `(api, suffix)` pair appears here.
|
|
13
|
+
*/
|
|
14
|
+
export declare const READ_OVERRIDES: Record<string, readonly string[]>;
|
|
15
|
+
/** A batch endpoint bundles arbitrary sub-request methods, so it is never a read. */
|
|
16
|
+
export declare function isBatch(path: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Classifies an operation as read or write. Method is the default; the
|
|
19
|
+
* API's own override list corrects semantically-read POSTs. `$batch` always
|
|
20
|
+
* wins, and an unrecognised API never gets an override.
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifySafety(method: string, path: string, operationId: string, api: string): Safety;
|
|
23
|
+
/**
|
|
24
|
+
* Risk tier drives which write tool an operation is reachable through.
|
|
25
|
+
* Unknown privilege is treated as high: 22% of Graph has no permission
|
|
26
|
+
* mapping, and unmapped must not silently mean "safe".
|
|
27
|
+
*/
|
|
28
|
+
export declare function riskFor(safety: Safety, privilegeLevel: number | null, path: string): Risk;
|