@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,566 @@
|
|
|
1
|
+
import { sha256 } from "./digest.js";
|
|
2
|
+
import { OpenApiMcpError } from "./errors.js";
|
|
3
|
+
import { parseTypedRecordId } from "./references.js";
|
|
4
|
+
import { canonicalJsonBounded } from "./strict-json.js";
|
|
5
|
+
import { DEFAULT_RUNTIME_LIMITS, PREPARED_CALL_VERSION, } from "./versions.js";
|
|
6
|
+
const preparedCallKeys = [
|
|
7
|
+
"actionKind",
|
|
8
|
+
"body",
|
|
9
|
+
"cardinality",
|
|
10
|
+
"catalogId",
|
|
11
|
+
"credentialProfileDigest",
|
|
12
|
+
"credentialProfileId",
|
|
13
|
+
"reservedSlotsDigest",
|
|
14
|
+
"headers",
|
|
15
|
+
"inputDigest",
|
|
16
|
+
"manifestDigest",
|
|
17
|
+
"method",
|
|
18
|
+
"normalizedArguments",
|
|
19
|
+
"operationDigest",
|
|
20
|
+
"operationId",
|
|
21
|
+
"origin",
|
|
22
|
+
"pageToken",
|
|
23
|
+
"preparedCallDigest",
|
|
24
|
+
"relativeUrl",
|
|
25
|
+
"releaseId",
|
|
26
|
+
"safety",
|
|
27
|
+
"version",
|
|
28
|
+
];
|
|
29
|
+
const inputKeys = preparedCallKeys.filter((key) => key !== "inputDigest" && key !== "preparedCallDigest");
|
|
30
|
+
const digestPattern = /^[0-9a-f]{64}$/;
|
|
31
|
+
const headerNamePattern = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
32
|
+
const shortIdPattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
33
|
+
const forbiddenJsonKeys = new Set(["__proto__", "constructor", "prototype"]);
|
|
34
|
+
const methods = new Set([
|
|
35
|
+
"GET",
|
|
36
|
+
"HEAD",
|
|
37
|
+
"POST",
|
|
38
|
+
"PUT",
|
|
39
|
+
"PATCH",
|
|
40
|
+
"DELETE",
|
|
41
|
+
"OPTIONS",
|
|
42
|
+
"TRACE",
|
|
43
|
+
]);
|
|
44
|
+
const actionKinds = new Set([
|
|
45
|
+
"create",
|
|
46
|
+
"update",
|
|
47
|
+
"delete",
|
|
48
|
+
"communicate",
|
|
49
|
+
"authority",
|
|
50
|
+
"transaction",
|
|
51
|
+
"execute",
|
|
52
|
+
"unknown",
|
|
53
|
+
]);
|
|
54
|
+
const forbiddenTransportHeaders = new Set([
|
|
55
|
+
"authorization",
|
|
56
|
+
"cf-access-authenticated-user-email",
|
|
57
|
+
"cf-access-client-id",
|
|
58
|
+
"cf-access-client-secret",
|
|
59
|
+
"cf-connecting-ip",
|
|
60
|
+
"cf-connecting-ipv6",
|
|
61
|
+
"cf-pseudo-ipv4",
|
|
62
|
+
"cloudfront-viewer-address",
|
|
63
|
+
"connection",
|
|
64
|
+
"content-length",
|
|
65
|
+
"cookie",
|
|
66
|
+
"fastly-client-ip",
|
|
67
|
+
"fly-client-ip",
|
|
68
|
+
"forwarded",
|
|
69
|
+
"host",
|
|
70
|
+
"keep-alive",
|
|
71
|
+
"proxy-authenticate",
|
|
72
|
+
"proxy-authorization",
|
|
73
|
+
"proxy-connection",
|
|
74
|
+
"sec-fetch-dest",
|
|
75
|
+
"sec-fetch-mode",
|
|
76
|
+
"sec-fetch-site",
|
|
77
|
+
"sec-fetch-user",
|
|
78
|
+
"set-cookie",
|
|
79
|
+
"te",
|
|
80
|
+
"trailer",
|
|
81
|
+
"transfer-encoding",
|
|
82
|
+
"true-client-ip",
|
|
83
|
+
"upgrade",
|
|
84
|
+
"via",
|
|
85
|
+
"www-authenticate",
|
|
86
|
+
"x-api-key",
|
|
87
|
+
"x-amz-security-token",
|
|
88
|
+
"x-auth-request-user",
|
|
89
|
+
"x-auth-token",
|
|
90
|
+
"x-azure-clientip",
|
|
91
|
+
"x-aws-principal",
|
|
92
|
+
"x-client-ip",
|
|
93
|
+
"x-client-id",
|
|
94
|
+
"x-client-secret",
|
|
95
|
+
"x-cluster-client-ip",
|
|
96
|
+
"x-envoy-external-address",
|
|
97
|
+
"x-appengine-user-ip",
|
|
98
|
+
"x-forwarded-for",
|
|
99
|
+
"x-forwarded-host",
|
|
100
|
+
"x-forwarded-port",
|
|
101
|
+
"x-forwarded-proto",
|
|
102
|
+
"x-forwarded-server",
|
|
103
|
+
"x-goog-authenticated-user-email",
|
|
104
|
+
"x-ms-client-principal",
|
|
105
|
+
"x-original-forwarded-for",
|
|
106
|
+
"x-provider-id",
|
|
107
|
+
"x-real-ip",
|
|
108
|
+
]);
|
|
109
|
+
const maxHeaderCount = 64;
|
|
110
|
+
const maxHeaderNameLength = 128;
|
|
111
|
+
const maxHeaderValueLength = 4096;
|
|
112
|
+
const maxRelativeUrlLength = 8192;
|
|
113
|
+
// An owned call exposes body copies through its public getter while retaining
|
|
114
|
+
// the immutable dispatch snapshot privately. Untrusted lookalikes never enter
|
|
115
|
+
// this map and must pass the strict own-data parser below.
|
|
116
|
+
const ownedPreparedCalls = new WeakMap();
|
|
117
|
+
function invalid() {
|
|
118
|
+
return new OpenApiMcpError("INPUT_INVALID", "Prepared call is invalid");
|
|
119
|
+
}
|
|
120
|
+
function integrityFailure() {
|
|
121
|
+
return new OpenApiMcpError("INPUT_INVALID", "Prepared call integrity check failed");
|
|
122
|
+
}
|
|
123
|
+
function hasControlCharacter(value) {
|
|
124
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
125
|
+
const code = value.charCodeAt(index);
|
|
126
|
+
if (code <= 0x1f || code === 0x7f)
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
function hasForbiddenHeaderValueCharacter(value) {
|
|
132
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
133
|
+
const code = value.charCodeAt(index);
|
|
134
|
+
if ((code <= 0x1f && code !== 0x09) || code === 0x7f)
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
function forbiddenHeader(name) {
|
|
140
|
+
const lower = name.toLowerCase();
|
|
141
|
+
return forbiddenTransportHeaders.has(lower);
|
|
142
|
+
}
|
|
143
|
+
function ownDataObject(value, keys) {
|
|
144
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
145
|
+
throw invalid();
|
|
146
|
+
const prototype = Object.getPrototypeOf(value);
|
|
147
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
148
|
+
throw invalid();
|
|
149
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
150
|
+
if (ownKeys.some((key) => typeof key !== "string"))
|
|
151
|
+
throw invalid();
|
|
152
|
+
const stringKeys = ownKeys;
|
|
153
|
+
if (keys !== null &&
|
|
154
|
+
(stringKeys.length !== keys.length ||
|
|
155
|
+
stringKeys.some((key) => !keys.includes(key)))) {
|
|
156
|
+
throw invalid();
|
|
157
|
+
}
|
|
158
|
+
for (const key of stringKeys) {
|
|
159
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
160
|
+
if (descriptor === undefined ||
|
|
161
|
+
!("value" in descriptor) ||
|
|
162
|
+
!descriptor.enumerable) {
|
|
163
|
+
throw invalid();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
function ownValue(object, key) {
|
|
169
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
170
|
+
if (descriptor === undefined || !("value" in descriptor))
|
|
171
|
+
throw invalid();
|
|
172
|
+
return descriptor.value;
|
|
173
|
+
}
|
|
174
|
+
function validShortId(value) {
|
|
175
|
+
return (typeof value === "string" &&
|
|
176
|
+
value.length > 0 &&
|
|
177
|
+
value.length <= 128 &&
|
|
178
|
+
value !== "." &&
|
|
179
|
+
value !== ".." &&
|
|
180
|
+
shortIdPattern.test(value));
|
|
181
|
+
}
|
|
182
|
+
function digest(value) {
|
|
183
|
+
if (typeof value !== "string" || !digestPattern.test(value))
|
|
184
|
+
throw invalid();
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
function operationId(value) {
|
|
188
|
+
if (typeof value !== "string")
|
|
189
|
+
throw invalid();
|
|
190
|
+
try {
|
|
191
|
+
const parsed = parseTypedRecordId(value);
|
|
192
|
+
if (!parsed.startsWith("operation:"))
|
|
193
|
+
throw invalid();
|
|
194
|
+
return parsed;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw invalid();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function origin(value) {
|
|
201
|
+
if (typeof value !== "string" || value.length > 2048)
|
|
202
|
+
throw invalid();
|
|
203
|
+
try {
|
|
204
|
+
const parsed = new URL(value);
|
|
205
|
+
if (parsed.protocol !== "https:" ||
|
|
206
|
+
parsed.username !== "" ||
|
|
207
|
+
parsed.password !== "" ||
|
|
208
|
+
parsed.origin !== value) {
|
|
209
|
+
throw invalid();
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error instanceof OpenApiMcpError)
|
|
215
|
+
throw error;
|
|
216
|
+
throw invalid();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function relativeUrl(value, callOrigin) {
|
|
220
|
+
if (typeof value !== "string" ||
|
|
221
|
+
value.length === 0 ||
|
|
222
|
+
value.length > maxRelativeUrlLength ||
|
|
223
|
+
!value.startsWith("/") ||
|
|
224
|
+
value.startsWith("//") ||
|
|
225
|
+
value.includes("#") ||
|
|
226
|
+
value.includes("\\") ||
|
|
227
|
+
hasControlCharacter(value)) {
|
|
228
|
+
throw invalid();
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
if (new URL(value, callOrigin).origin !== callOrigin)
|
|
232
|
+
throw invalid();
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error instanceof OpenApiMcpError)
|
|
237
|
+
throw error;
|
|
238
|
+
throw invalid();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function cloneHeaders(value) {
|
|
242
|
+
const source = ownDataObject(value, null);
|
|
243
|
+
const keys = Object.keys(source).sort();
|
|
244
|
+
if (keys.length > maxHeaderCount)
|
|
245
|
+
throw invalid();
|
|
246
|
+
const result = Object.create(null);
|
|
247
|
+
for (const key of keys) {
|
|
248
|
+
const headerValue = ownValue(source, key);
|
|
249
|
+
if (key.length === 0 ||
|
|
250
|
+
key.length > maxHeaderNameLength ||
|
|
251
|
+
!headerNamePattern.test(key) ||
|
|
252
|
+
forbiddenHeader(key) ||
|
|
253
|
+
typeof headerValue !== "string" ||
|
|
254
|
+
headerValue.length > maxHeaderValueLength ||
|
|
255
|
+
hasForbiddenHeaderValueCharacter(headerValue)) {
|
|
256
|
+
throw invalid();
|
|
257
|
+
}
|
|
258
|
+
const normalizedName = key.toLowerCase();
|
|
259
|
+
if (Object.hasOwn(result, normalizedName))
|
|
260
|
+
throw invalid();
|
|
261
|
+
result[normalizedName] = headerValue;
|
|
262
|
+
}
|
|
263
|
+
return Object.freeze(result);
|
|
264
|
+
}
|
|
265
|
+
function cloneJson(value, state, depth = 0) {
|
|
266
|
+
if (state.nodes >= DEFAULT_RUNTIME_LIMITS.maxManifestRecords || depth >= 64)
|
|
267
|
+
throw invalid();
|
|
268
|
+
state.nodes += 1;
|
|
269
|
+
if (value === null ||
|
|
270
|
+
typeof value === "boolean" ||
|
|
271
|
+
typeof value === "string") {
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
if (typeof value === "number") {
|
|
275
|
+
if (!Number.isFinite(value))
|
|
276
|
+
throw invalid();
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
const keys = Reflect.ownKeys(value);
|
|
281
|
+
if (keys.length !== value.length + 1 ||
|
|
282
|
+
keys.some((key) => key !== "length" &&
|
|
283
|
+
(typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/.test(key)))) {
|
|
284
|
+
throw invalid();
|
|
285
|
+
}
|
|
286
|
+
const result = [];
|
|
287
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
288
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
289
|
+
if (descriptor === undefined ||
|
|
290
|
+
!("value" in descriptor) ||
|
|
291
|
+
!descriptor.enumerable) {
|
|
292
|
+
throw invalid();
|
|
293
|
+
}
|
|
294
|
+
result.push(cloneJson(descriptor.value, state, depth + 1));
|
|
295
|
+
}
|
|
296
|
+
return Object.freeze(result);
|
|
297
|
+
}
|
|
298
|
+
const source = ownDataObject(value, null);
|
|
299
|
+
const result = Object.create(null);
|
|
300
|
+
for (const key of Object.keys(source).sort()) {
|
|
301
|
+
if (forbiddenJsonKeys.has(key))
|
|
302
|
+
throw invalid();
|
|
303
|
+
result[key] = cloneJson(ownValue(source, key), state, depth + 1);
|
|
304
|
+
}
|
|
305
|
+
return Object.freeze(result);
|
|
306
|
+
}
|
|
307
|
+
function cloneBody(value) {
|
|
308
|
+
if (value === null)
|
|
309
|
+
return null;
|
|
310
|
+
if (!(value instanceof Uint8Array) ||
|
|
311
|
+
value.byteLength > DEFAULT_RUNTIME_LIMITS.maxArgumentsBytes)
|
|
312
|
+
throw invalid();
|
|
313
|
+
const copy = new Uint8Array(value);
|
|
314
|
+
// Typed arrays with elements cannot be frozen. Preventing extension and
|
|
315
|
+
// binding their byte digest still makes any later byte mutation detectable.
|
|
316
|
+
Object.preventExtensions(copy);
|
|
317
|
+
return copy;
|
|
318
|
+
}
|
|
319
|
+
function cardinality(value) {
|
|
320
|
+
if (value === null)
|
|
321
|
+
return null;
|
|
322
|
+
const source = ownDataObject(value, null);
|
|
323
|
+
const kind = ownValue(source, "kind");
|
|
324
|
+
if (kind === "bounded") {
|
|
325
|
+
if (Reflect.ownKeys(source).length !== 2)
|
|
326
|
+
throw invalid();
|
|
327
|
+
const maxAffected = ownValue(source, "maxAffected");
|
|
328
|
+
if (typeof maxAffected !== "number" ||
|
|
329
|
+
!Number.isSafeInteger(maxAffected) ||
|
|
330
|
+
maxAffected < 1) {
|
|
331
|
+
throw invalid();
|
|
332
|
+
}
|
|
333
|
+
return Object.freeze({ kind, maxAffected });
|
|
334
|
+
}
|
|
335
|
+
if ((kind === "single" || kind === "unbounded" || kind === "unknown") &&
|
|
336
|
+
Reflect.ownKeys(source).length === 1) {
|
|
337
|
+
return Object.freeze({ kind });
|
|
338
|
+
}
|
|
339
|
+
throw invalid();
|
|
340
|
+
}
|
|
341
|
+
function parseUntrustedPreparedCall(value) {
|
|
342
|
+
const source = ownDataObject(value, preparedCallKeys);
|
|
343
|
+
const version = ownValue(source, "version");
|
|
344
|
+
const catalogId = ownValue(source, "catalogId");
|
|
345
|
+
const releaseId = ownValue(source, "releaseId");
|
|
346
|
+
const parsedOperationId = operationId(ownValue(source, "operationId"));
|
|
347
|
+
const method = ownValue(source, "method");
|
|
348
|
+
const parsedOrigin = origin(ownValue(source, "origin"));
|
|
349
|
+
const safety = ownValue(source, "safety");
|
|
350
|
+
const pageToken = ownValue(source, "pageToken");
|
|
351
|
+
const actionKind = ownValue(source, "actionKind");
|
|
352
|
+
const parsedCardinality = cardinality(ownValue(source, "cardinality"));
|
|
353
|
+
if (version !== PREPARED_CALL_VERSION ||
|
|
354
|
+
!validShortId(catalogId) ||
|
|
355
|
+
!validShortId(releaseId) ||
|
|
356
|
+
typeof method !== "string" ||
|
|
357
|
+
!methods.has(method) ||
|
|
358
|
+
(safety !== "read" && safety !== "action") ||
|
|
359
|
+
(pageToken !== null &&
|
|
360
|
+
(typeof pageToken !== "string" ||
|
|
361
|
+
pageToken.length === 0 ||
|
|
362
|
+
pageToken.length > 512 ||
|
|
363
|
+
hasControlCharacter(pageToken) ||
|
|
364
|
+
safety !== "read")) ||
|
|
365
|
+
(actionKind !== null &&
|
|
366
|
+
(typeof actionKind !== "string" ||
|
|
367
|
+
!actionKinds.has(actionKind))) ||
|
|
368
|
+
(safety === "read" &&
|
|
369
|
+
(actionKind !== null || parsedCardinality !== null)) ||
|
|
370
|
+
(safety === "action" && (actionKind === null || parsedCardinality === null))) {
|
|
371
|
+
throw invalid();
|
|
372
|
+
}
|
|
373
|
+
const normalized = cloneJson(ownValue(source, "normalizedArguments"), {
|
|
374
|
+
nodes: 0,
|
|
375
|
+
});
|
|
376
|
+
if (typeof normalized !== "object" ||
|
|
377
|
+
normalized === null ||
|
|
378
|
+
Array.isArray(normalized)) {
|
|
379
|
+
throw invalid();
|
|
380
|
+
}
|
|
381
|
+
// This also rejects malformed Unicode and enforces the same byte limit used
|
|
382
|
+
// for operation arguments before a canonical digest is computed.
|
|
383
|
+
canonicalJsonBounded(normalized, {
|
|
384
|
+
maxBytes: DEFAULT_RUNTIME_LIMITS.maxArgumentsBytes,
|
|
385
|
+
maxDepth: DEFAULT_RUNTIME_LIMITS.maxJsonDepth,
|
|
386
|
+
maxNodes: DEFAULT_RUNTIME_LIMITS.maxManifestRecords,
|
|
387
|
+
});
|
|
388
|
+
const credentialProfileId = ownValue(source, "credentialProfileId");
|
|
389
|
+
if (!validShortId(credentialProfileId))
|
|
390
|
+
throw invalid();
|
|
391
|
+
return {
|
|
392
|
+
version,
|
|
393
|
+
catalogId: catalogId,
|
|
394
|
+
releaseId: releaseId,
|
|
395
|
+
operationId: parsedOperationId,
|
|
396
|
+
operationDigest: digest(ownValue(source, "operationDigest")),
|
|
397
|
+
manifestDigest: digest(ownValue(source, "manifestDigest")),
|
|
398
|
+
credentialProfileId,
|
|
399
|
+
credentialProfileDigest: digest(ownValue(source, "credentialProfileDigest")),
|
|
400
|
+
reservedSlotsDigest: digest(ownValue(source, "reservedSlotsDigest")),
|
|
401
|
+
method: method,
|
|
402
|
+
origin: parsedOrigin,
|
|
403
|
+
relativeUrl: relativeUrl(ownValue(source, "relativeUrl"), parsedOrigin),
|
|
404
|
+
pageToken: pageToken,
|
|
405
|
+
headers: cloneHeaders(ownValue(source, "headers")),
|
|
406
|
+
body: cloneBody(ownValue(source, "body")),
|
|
407
|
+
normalizedArguments: normalized,
|
|
408
|
+
safety,
|
|
409
|
+
actionKind: actionKind,
|
|
410
|
+
cardinality: parsedCardinality,
|
|
411
|
+
inputDigest: digest(ownValue(source, "inputDigest")),
|
|
412
|
+
preparedCallDigest: digest(ownValue(source, "preparedCallDigest")),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function parsePreparedCall(value) {
|
|
416
|
+
try {
|
|
417
|
+
if (typeof value === "object" && value !== null) {
|
|
418
|
+
const owned = ownedPreparedCalls.get(value);
|
|
419
|
+
if (owned !== undefined)
|
|
420
|
+
return owned;
|
|
421
|
+
}
|
|
422
|
+
return parseUntrustedPreparedCall(value);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// Reflection and proxy traps are untrusted input. Do not expose their
|
|
426
|
+
// messages or allow them to escape the stable runtime error boundary.
|
|
427
|
+
throw invalid();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function canonicalPayload(call, bodyDigest) {
|
|
431
|
+
return {
|
|
432
|
+
version: call.version,
|
|
433
|
+
catalogId: call.catalogId,
|
|
434
|
+
releaseId: call.releaseId,
|
|
435
|
+
operationId: call.operationId,
|
|
436
|
+
operationDigest: call.operationDigest,
|
|
437
|
+
manifestDigest: call.manifestDigest,
|
|
438
|
+
credentialProfileId: call.credentialProfileId,
|
|
439
|
+
credentialProfileDigest: call.credentialProfileDigest,
|
|
440
|
+
reservedSlotsDigest: call.reservedSlotsDigest,
|
|
441
|
+
method: call.method,
|
|
442
|
+
origin: call.origin,
|
|
443
|
+
relativeUrl: call.relativeUrl,
|
|
444
|
+
pageToken: call.pageToken,
|
|
445
|
+
headers: call.headers,
|
|
446
|
+
body: bodyDigest,
|
|
447
|
+
normalizedArguments: call.normalizedArguments,
|
|
448
|
+
safety: call.safety,
|
|
449
|
+
actionKind: call.actionKind,
|
|
450
|
+
cardinality: call.cardinality,
|
|
451
|
+
inputDigest: call.inputDigest,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** Package-private defensive snapshot shared by the runtime and local codec. */
|
|
455
|
+
export function snapshotPaginationTokenState(value, limits, now = Date.now()) {
|
|
456
|
+
const data = ownDataObject(value, [
|
|
457
|
+
"catalogId",
|
|
458
|
+
"releaseId",
|
|
459
|
+
"manifestDigest",
|
|
460
|
+
"operationId",
|
|
461
|
+
"inputDigest",
|
|
462
|
+
"origin",
|
|
463
|
+
"nextRelativeUrl",
|
|
464
|
+
"expiresAt",
|
|
465
|
+
"pageCount",
|
|
466
|
+
"cumulativeBytes",
|
|
467
|
+
]);
|
|
468
|
+
const parsedOrigin = origin(data.origin);
|
|
469
|
+
const next = relativeUrl(data.nextRelativeUrl, parsedOrigin);
|
|
470
|
+
const url = new URL(next, parsedOrigin);
|
|
471
|
+
const expiresAt = typeof data.expiresAt === "string" ? Date.parse(data.expiresAt) : NaN;
|
|
472
|
+
if (!validShortId(data.catalogId) ||
|
|
473
|
+
!validShortId(data.releaseId) ||
|
|
474
|
+
!Number.isFinite(now) ||
|
|
475
|
+
!Number.isFinite(expiresAt) ||
|
|
476
|
+
expiresAt <= now ||
|
|
477
|
+
expiresAt > now + 120_000 ||
|
|
478
|
+
new Date(expiresAt).toISOString() !== data.expiresAt ||
|
|
479
|
+
`${url.pathname}${url.search}` !== next ||
|
|
480
|
+
!Number.isSafeInteger(data.pageCount) ||
|
|
481
|
+
Number(data.pageCount) < 1 ||
|
|
482
|
+
!Number.isSafeInteger(data.cumulativeBytes) ||
|
|
483
|
+
Number(data.cumulativeBytes) < 0)
|
|
484
|
+
throw invalid();
|
|
485
|
+
const state = {
|
|
486
|
+
catalogId: data.catalogId,
|
|
487
|
+
releaseId: data.releaseId,
|
|
488
|
+
operationId: operationId(data.operationId),
|
|
489
|
+
manifestDigest: digest(data.manifestDigest),
|
|
490
|
+
inputDigest: digest(data.inputDigest),
|
|
491
|
+
origin: parsedOrigin,
|
|
492
|
+
nextRelativeUrl: next,
|
|
493
|
+
expiresAt: data.expiresAt,
|
|
494
|
+
pageCount: data.pageCount,
|
|
495
|
+
cumulativeBytes: data.cumulativeBytes,
|
|
496
|
+
};
|
|
497
|
+
if (state.pageCount >= limits.maxPages ||
|
|
498
|
+
state.cumulativeBytes >= limits.maxPaginationBytes)
|
|
499
|
+
throw new OpenApiMcpError("PAGINATION_LIMIT_EXCEEDED");
|
|
500
|
+
return Object.freeze(state);
|
|
501
|
+
}
|
|
502
|
+
function timingSafeEqual(left, right) {
|
|
503
|
+
if (left.length !== right.length)
|
|
504
|
+
return false;
|
|
505
|
+
let difference = 0;
|
|
506
|
+
for (let index = 0; index < left.length; index += 1)
|
|
507
|
+
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
508
|
+
return difference === 0;
|
|
509
|
+
}
|
|
510
|
+
async function digestNormalizedArguments(value) {
|
|
511
|
+
return await sha256("knitli.openapi-mcp.input.v1", value);
|
|
512
|
+
}
|
|
513
|
+
/** SHA-256 of raw request bytes, independent of JSON canonicalization. */
|
|
514
|
+
export async function digestBytes(value) {
|
|
515
|
+
const copy = new Uint8Array(value);
|
|
516
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", copy);
|
|
517
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
518
|
+
}
|
|
519
|
+
/** Compute the domain-separated call digest, deliberately omitting its own key. */
|
|
520
|
+
export async function digestPreparedCall(call) {
|
|
521
|
+
const parsed = parsePreparedCall(call);
|
|
522
|
+
return await sha256("knitli.openapi-mcp.prepared-call.v2", canonicalPayload(parsed, parsed.body === null ? null : await digestBytes(parsed.body)));
|
|
523
|
+
}
|
|
524
|
+
/** Verify a prepared call locally; storage admission is rechecked separately. */
|
|
525
|
+
export async function verifyAndSnapshotPreparedCall(call) {
|
|
526
|
+
const parsed = parsePreparedCall(call);
|
|
527
|
+
const expectedInput = await digestNormalizedArguments(parsed.normalizedArguments);
|
|
528
|
+
const expectedCall = await sha256("knitli.openapi-mcp.prepared-call.v2", canonicalPayload(parsed, parsed.body === null ? null : await digestBytes(parsed.body)));
|
|
529
|
+
if (!timingSafeEqual(expectedInput, parsed.inputDigest) ||
|
|
530
|
+
!timingSafeEqual(expectedCall, parsed.preparedCallDigest)) {
|
|
531
|
+
throw integrityFailure();
|
|
532
|
+
}
|
|
533
|
+
return parsed;
|
|
534
|
+
}
|
|
535
|
+
/** Verify a prepared call locally; storage admission is rechecked separately. */
|
|
536
|
+
export async function verifyPreparedCall(call) {
|
|
537
|
+
await verifyAndSnapshotPreparedCall(call);
|
|
538
|
+
}
|
|
539
|
+
/** Build an owned, detached prepared call and bind all fields to its digests. */
|
|
540
|
+
export async function createPreparedCall(input) {
|
|
541
|
+
try {
|
|
542
|
+
const source = ownDataObject(input, inputKeys);
|
|
543
|
+
const provisional = {
|
|
544
|
+
...Object.fromEntries(inputKeys.map((key) => [key, ownValue(source, key)])),
|
|
545
|
+
inputDigest: "0".repeat(64),
|
|
546
|
+
preparedCallDigest: "0".repeat(64),
|
|
547
|
+
};
|
|
548
|
+
const parsed = parsePreparedCall(provisional);
|
|
549
|
+
const inputDigest = await digestNormalizedArguments(parsed.normalizedArguments);
|
|
550
|
+
const withInput = { ...parsed, inputDigest };
|
|
551
|
+
const preparedCallDigest = await sha256("knitli.openapi-mcp.prepared-call.v2", canonicalPayload(withInput, parsed.body === null ? null : await digestBytes(parsed.body)));
|
|
552
|
+
const snapshot = Object.freeze({ ...withInput, preparedCallDigest });
|
|
553
|
+
const { body: _body, ...publicCall } = snapshot;
|
|
554
|
+
const ownedCall = Object.freeze({
|
|
555
|
+
...publicCall,
|
|
556
|
+
get body() {
|
|
557
|
+
return snapshot.body === null ? null : new Uint8Array(snapshot.body);
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
ownedPreparedCalls.set(ownedCall, snapshot);
|
|
561
|
+
return ownedCall;
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
throw invalid();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { OperationRef, OperationRefPayload, TypedRecordId } from "./types.ts";
|
|
2
|
+
/** Validate a typed operation or schema record identifier before it is used as a key. */
|
|
3
|
+
export declare function parseTypedRecordId(value: string): TypedRecordId;
|
|
4
|
+
/** Encode the exact, canonical, manifest-bound operation-reference wire format. */
|
|
5
|
+
export declare function encodeOperationRef(payload: OperationRefPayload): OperationRef;
|
|
6
|
+
/** Decode only canonical `opref.v1` data; authorization remains a later runtime concern. */
|
|
7
|
+
export declare function decodeOperationRef(reference: string): OperationRefPayload;
|