@knitli/openapi-mcp 1.4.0 → 1.4.1
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/dist/operations.js +5 -1
- package/dist/runtime/manifest.js +6 -2
- package/dist/runtime/references.js +2 -1
- package/dist/runtime/strict-json.js +1 -1
- package/dist/sqlite/generation-store.js +66 -6
- package/dist/stdio/authorizer.js +7 -1
- package/dist/stdio/server.js +39 -6
- package/package.json +2 -2
package/dist/operations.js
CHANGED
|
@@ -90,7 +90,11 @@ export function resolveCanonicalPointer(doc, pointer) {
|
|
|
90
90
|
!Object.hasOwn(node, index)) {
|
|
91
91
|
throw new Error("JSON Pointer array index was not found");
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
const element = Object.getOwnPropertyDescriptor(node, index);
|
|
94
|
+
if (element === undefined || !("value" in element)) {
|
|
95
|
+
throw new Error("JSON Pointer array index was not found");
|
|
96
|
+
}
|
|
97
|
+
node = element.value;
|
|
94
98
|
continue;
|
|
95
99
|
}
|
|
96
100
|
if (typeof node !== "object" ||
|
package/dist/runtime/manifest.js
CHANGED
|
@@ -90,7 +90,8 @@ function requireString(value, label, maximum, pattern) {
|
|
|
90
90
|
if (typeof value !== "string" ||
|
|
91
91
|
value.length === 0 ||
|
|
92
92
|
value.length > maximum ||
|
|
93
|
-
(pattern && !pattern.test(value))
|
|
93
|
+
(pattern && !pattern.test(value)) // nosemgrep -- anchored linear-time literal; length capped by maximum above
|
|
94
|
+
) {
|
|
94
95
|
throw manifestInvalid(`${label} is invalid`);
|
|
95
96
|
}
|
|
96
97
|
return value;
|
|
@@ -107,7 +108,10 @@ function requireInteger(value, label) {
|
|
|
107
108
|
return value;
|
|
108
109
|
}
|
|
109
110
|
function requireDigest(value, label) {
|
|
110
|
-
if (typeof value !== "string" ||
|
|
111
|
+
if (typeof value !== "string" ||
|
|
112
|
+
value.length !== 64 ||
|
|
113
|
+
!digestPattern.test(value) // nosemgrep -- anchored fixed-length linear literal; length pinned to 64 above
|
|
114
|
+
)
|
|
111
115
|
throw manifestInvalid(`${label} is invalid`);
|
|
112
116
|
return value;
|
|
113
117
|
}
|
|
@@ -17,7 +17,8 @@ function assertSegment(value, maximumLength, pattern, label) {
|
|
|
17
17
|
value.length > maximumLength ||
|
|
18
18
|
value === "." ||
|
|
19
19
|
value === ".." ||
|
|
20
|
-
!pattern.test(value)
|
|
20
|
+
!pattern.test(value) // nosemgrep -- anchored linear-time literal; length capped by maximumLength above
|
|
21
|
+
) {
|
|
21
22
|
throw invalidReference(`${label} is invalid`);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
@@ -530,7 +530,7 @@ function serializeCanonical(value, ancestors) {
|
|
|
530
530
|
}
|
|
531
531
|
function isCanonicalArrayIndex(key, length) {
|
|
532
532
|
if (!/^(?:0|[1-9]\d*)$/.test(key))
|
|
533
|
-
return false;
|
|
533
|
+
return false; // nosemgrep -- anchored literal, disjoint alternation, single star: linear-time
|
|
534
534
|
const index = Number(key);
|
|
535
535
|
return (Number.isSafeInteger(index) &&
|
|
536
536
|
index >= 0 &&
|
|
@@ -70,7 +70,7 @@ function requireIdentity(catalogId, issuer) {
|
|
|
70
70
|
catalogId.length > 128 ||
|
|
71
71
|
catalogId === "." ||
|
|
72
72
|
catalogId === ".." ||
|
|
73
|
-
!identityPattern.test(catalogId) ||
|
|
73
|
+
!identityPattern.test(catalogId) || // nosemgrep -- anchored linear-time literal; input length is capped at 128 above
|
|
74
74
|
typeof issuer !== "string" ||
|
|
75
75
|
issuer.length === 0 ||
|
|
76
76
|
issuer.length > 256 ||
|
|
@@ -299,16 +299,76 @@ async function validateMutexMetadata(mutex) {
|
|
|
299
299
|
}
|
|
300
300
|
return before;
|
|
301
301
|
}
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
const mutexInitSuffix = ".mutex-init";
|
|
303
|
+
const mutexInitUuidSegments = [8, 4, 4, 4, 12];
|
|
304
|
+
function isAsciiHexDigit(code) {
|
|
305
|
+
return ((code >= 0x30 && code <= 0x39) ||
|
|
306
|
+
(code >= 0x41 && code <= 0x46) ||
|
|
307
|
+
(code >= 0x61 && code <= 0x66));
|
|
308
|
+
}
|
|
309
|
+
function isMutexInitializerUuid(candidate) {
|
|
310
|
+
// 8-4-4-4-12 lowercase/uppercase hex with version 1-5 and variant 8/9/a/b.
|
|
311
|
+
if (candidate.length !== 36)
|
|
312
|
+
return false;
|
|
313
|
+
let position = 0;
|
|
314
|
+
for (let segment = 0; segment < mutexInitUuidSegments.length; segment += 1) {
|
|
315
|
+
if (segment > 0) {
|
|
316
|
+
if (candidate[position] !== "-")
|
|
317
|
+
return false;
|
|
318
|
+
position += 1;
|
|
319
|
+
}
|
|
320
|
+
const length = mutexInitUuidSegments[segment];
|
|
321
|
+
for (let index = 0; index < length; index += 1) {
|
|
322
|
+
const code = candidate.charCodeAt(position);
|
|
323
|
+
if (segment === 2 && index === 0) {
|
|
324
|
+
if (code < 0x31 || code > 0x35)
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
else if (segment === 3 && index === 0) {
|
|
328
|
+
if (code !== 0x38 &&
|
|
329
|
+
code !== 0x39 &&
|
|
330
|
+
code !== 0x41 &&
|
|
331
|
+
code !== 0x42 &&
|
|
332
|
+
code !== 0x61 &&
|
|
333
|
+
code !== 0x62)
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
else if (!isAsciiHexDigit(code)) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
position += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
function isMutexInitializerAlias(name, targetName) {
|
|
345
|
+
// Matches `.<target>.<digits>.<uuid>.mutex-init` case-insensitively,
|
|
346
|
+
// without building a RegExp from the target name.
|
|
347
|
+
const prefix = `.${targetName}.`;
|
|
348
|
+
if (name.length < prefix.length + 1 + 1 + 36 + mutexInitSuffix.length)
|
|
349
|
+
return false;
|
|
350
|
+
if (name.slice(0, prefix.length).toLowerCase() !== prefix.toLowerCase())
|
|
351
|
+
return false;
|
|
352
|
+
if (name.slice(-mutexInitSuffix.length).toLowerCase() !== mutexInitSuffix)
|
|
353
|
+
return false;
|
|
354
|
+
const middle = name.slice(prefix.length, -mutexInitSuffix.length);
|
|
355
|
+
const separator = middle.indexOf(".");
|
|
356
|
+
if (separator <= 0)
|
|
357
|
+
return false;
|
|
358
|
+
const counter = middle.slice(0, separator);
|
|
359
|
+
for (let index = 0; index < counter.length; index += 1) {
|
|
360
|
+
const code = counter.charCodeAt(index);
|
|
361
|
+
if (code < 0x30 || code > 0x39)
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
return isMutexInitializerUuid(middle.slice(separator + 1));
|
|
304
365
|
}
|
|
305
366
|
async function clearMutexInitializerAlias(location) {
|
|
306
367
|
const final = await lstat(location.mutex);
|
|
307
|
-
const
|
|
308
|
-
"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\\.mutex-init$", "i");
|
|
368
|
+
const targetName = basename(location.target);
|
|
309
369
|
let removed = false;
|
|
310
370
|
for (const name of await readdir(location.parent)) {
|
|
311
|
-
if (!
|
|
371
|
+
if (!isMutexInitializerAlias(name, targetName))
|
|
312
372
|
continue;
|
|
313
373
|
const candidate = join(location.parent, name);
|
|
314
374
|
let metadata;
|
package/dist/stdio/authorizer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1
2
|
import { acceptedContent, inputResponse, } from "@modelcontextprotocol/server";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { snapshotCredentialAuthorizationBinding } from "../runtime/credential-binding.js";
|
|
@@ -54,6 +55,11 @@ function hexRandom(randomBytes) {
|
|
|
54
55
|
}
|
|
55
56
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
56
57
|
}
|
|
58
|
+
function randomIdsEqual(left, right) {
|
|
59
|
+
if (left.length !== right.length)
|
|
60
|
+
return false;
|
|
61
|
+
return timingSafeEqual(Buffer.from(left), Buffer.from(right));
|
|
62
|
+
}
|
|
57
63
|
function exactPayloadMatch(payload, pending) {
|
|
58
64
|
return (payload.pendingId === pending.pendingId &&
|
|
59
65
|
payload.callDigest === pending.callDigest &&
|
|
@@ -416,7 +422,7 @@ class LedgerActionAuthorizer {
|
|
|
416
422
|
const authorizationId = Object.freeze(Object.create(null));
|
|
417
423
|
let collision = false;
|
|
418
424
|
for (const existing of this.#receipts.values()) {
|
|
419
|
-
if (existing.randomId
|
|
425
|
+
if (randomIdsEqual(existing.randomId, token)) {
|
|
420
426
|
collision = true;
|
|
421
427
|
break;
|
|
422
428
|
}
|
package/dist/stdio/server.js
CHANGED
|
@@ -138,12 +138,45 @@ function outcome(value, snapshot) {
|
|
|
138
138
|
]
|
|
139
139
|
.filter(Boolean)
|
|
140
140
|
.sort((left, right) => right.length - left.length);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const redact = (text) =>
|
|
141
|
+
// Literal redaction without a dynamic RegExp: find the earliest occurrence
|
|
142
|
+
// across variants (ties broken longest-first, matching the previous
|
|
143
|
+
// longest-first alternation), redact it, and resume after the match.
|
|
144
|
+
// indexOf skips non-matching regions in native code, so adversarial
|
|
145
|
+
// long-prefix inputs stay linear instead of quadratic per offset.
|
|
146
|
+
const redact = (text) => {
|
|
147
|
+
if (variants.length === 0 || text.length === 0)
|
|
148
|
+
return text;
|
|
149
|
+
const parts = [];
|
|
150
|
+
let cursor = 0;
|
|
151
|
+
let redacted = false;
|
|
152
|
+
for (;;) {
|
|
153
|
+
let at = -1;
|
|
154
|
+
let match = "";
|
|
155
|
+
for (const variant of variants) {
|
|
156
|
+
const found = text.indexOf(variant, cursor);
|
|
157
|
+
if (found === -1)
|
|
158
|
+
continue;
|
|
159
|
+
if (at === -1 ||
|
|
160
|
+
found < at ||
|
|
161
|
+
(found === at && variant.length > match.length)) {
|
|
162
|
+
at = found;
|
|
163
|
+
match = variant;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (at === -1)
|
|
167
|
+
break;
|
|
168
|
+
if (at > cursor)
|
|
169
|
+
parts.push(text.slice(cursor, at));
|
|
170
|
+
parts.push("[REDACTED]");
|
|
171
|
+
cursor = at + match.length;
|
|
172
|
+
redacted = true;
|
|
173
|
+
}
|
|
174
|
+
if (!redacted)
|
|
175
|
+
return text;
|
|
176
|
+
if (cursor < text.length)
|
|
177
|
+
parts.push(text.slice(cursor));
|
|
178
|
+
return parts.join("");
|
|
179
|
+
};
|
|
147
180
|
// Only upstream values are untrusted. Keep protocol fields and opaque tokens intact.
|
|
148
181
|
if (value.kind === "success") {
|
|
149
182
|
try {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knitli/openapi-mcp",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Compile signed OpenAPI catalogs and serve verified search and authorized API calls over MCP.",
|
|
7
7
|
"license": "MIT OR Apache-2.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@modelcontextprotocol/server": "2.0.0",
|
|
75
75
|
"undici": "8.10.2",
|
|
76
|
-
"yaml": "
|
|
76
|
+
"yaml": "2.9.0",
|
|
77
77
|
"zod": "4.5.4"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|