@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,279 @@
|
|
|
1
|
+
const MAX_TOKEN_SOURCE_LENGTH = 1_024;
|
|
2
|
+
const MAX_TOKENS = 48;
|
|
3
|
+
const MAX_TOKEN_LENGTH = 48;
|
|
4
|
+
const communicationTokens = new Set([
|
|
5
|
+
"communicate",
|
|
6
|
+
"email",
|
|
7
|
+
"invite",
|
|
8
|
+
"invitation",
|
|
9
|
+
"mail",
|
|
10
|
+
"message",
|
|
11
|
+
"notify",
|
|
12
|
+
"notification",
|
|
13
|
+
"send",
|
|
14
|
+
"share",
|
|
15
|
+
"sharing",
|
|
16
|
+
]);
|
|
17
|
+
const authorityTokens = new Set([
|
|
18
|
+
"access",
|
|
19
|
+
"authority",
|
|
20
|
+
"grant",
|
|
21
|
+
"membership",
|
|
22
|
+
"member",
|
|
23
|
+
"permission",
|
|
24
|
+
"privilege",
|
|
25
|
+
"role",
|
|
26
|
+
]);
|
|
27
|
+
const transactionTokens = new Set([
|
|
28
|
+
"charge",
|
|
29
|
+
"invoice",
|
|
30
|
+
"payment",
|
|
31
|
+
"purchase",
|
|
32
|
+
"refund",
|
|
33
|
+
"transaction",
|
|
34
|
+
"transfer",
|
|
35
|
+
]);
|
|
36
|
+
const executeTokens = new Set([
|
|
37
|
+
"deploy",
|
|
38
|
+
"execute",
|
|
39
|
+
"invoke",
|
|
40
|
+
"run",
|
|
41
|
+
"start",
|
|
42
|
+
"trigger",
|
|
43
|
+
]);
|
|
44
|
+
const createTokens = new Set(["add", "create", "provision", "register"]);
|
|
45
|
+
const updateTokens = new Set(["modify", "patch", "replace", "set", "update"]);
|
|
46
|
+
const bulkTokens = new Set([
|
|
47
|
+
"all",
|
|
48
|
+
"batch",
|
|
49
|
+
"bulk",
|
|
50
|
+
"each",
|
|
51
|
+
"everyone",
|
|
52
|
+
"many",
|
|
53
|
+
]);
|
|
54
|
+
function tokensForSource(source) {
|
|
55
|
+
const inspected = source.slice(0, MAX_TOKEN_SOURCE_LENGTH);
|
|
56
|
+
const words = inspected
|
|
57
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.match(/[a-z0-9]+/g);
|
|
60
|
+
const tokenSource = words ?? [];
|
|
61
|
+
return {
|
|
62
|
+
tokens: tokenSource
|
|
63
|
+
.slice(0, MAX_TOKENS)
|
|
64
|
+
.map((word) => word.slice(0, MAX_TOKEN_LENGTH)),
|
|
65
|
+
truncated: source.length > MAX_TOKEN_SOURCE_LENGTH ||
|
|
66
|
+
tokenSource.length > MAX_TOKENS ||
|
|
67
|
+
tokenSource.some((word) => word.length > MAX_TOKEN_LENGTH),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function normalizedTokens(operation) {
|
|
71
|
+
// Each signed source has an independent budget. A source that does not fit
|
|
72
|
+
// cannot be safely classified as routine, because omitted later evidence
|
|
73
|
+
// could establish a dangerous kind or batch cardinality.
|
|
74
|
+
const tokens = [];
|
|
75
|
+
let truncated = false;
|
|
76
|
+
for (const source of [
|
|
77
|
+
operation.operationId,
|
|
78
|
+
operation.path,
|
|
79
|
+
...operation.tags,
|
|
80
|
+
]) {
|
|
81
|
+
const evidence = tokensForSource(source);
|
|
82
|
+
tokens.push(...evidence.tokens);
|
|
83
|
+
truncated ||= evidence.truncated;
|
|
84
|
+
}
|
|
85
|
+
return { tokens, truncated };
|
|
86
|
+
}
|
|
87
|
+
function hasAny(tokens, candidates) {
|
|
88
|
+
return tokens.some((token) => candidates.has(token) ||
|
|
89
|
+
(token.length > 4 &&
|
|
90
|
+
token.endsWith("ies") &&
|
|
91
|
+
candidates.has(`${token.slice(0, -3)}y`)) ||
|
|
92
|
+
(token.length > 4 &&
|
|
93
|
+
token.endsWith("es") &&
|
|
94
|
+
candidates.has(token.slice(0, -2))) ||
|
|
95
|
+
(token.length > 3 &&
|
|
96
|
+
token.endsWith("s") &&
|
|
97
|
+
candidates.has(token.slice(0, -1))));
|
|
98
|
+
}
|
|
99
|
+
function hasBatchSemantics(operation, tokens) {
|
|
100
|
+
return (operation.path === "/$batch" ||
|
|
101
|
+
operation.path.endsWith("/$batch") ||
|
|
102
|
+
tokens.includes("batch"));
|
|
103
|
+
}
|
|
104
|
+
function actionKindFor(operation, evidence) {
|
|
105
|
+
if (operation.method === "DELETE")
|
|
106
|
+
return "delete";
|
|
107
|
+
if (evidence.truncated)
|
|
108
|
+
return "unknown";
|
|
109
|
+
const dangerousMatches = [];
|
|
110
|
+
for (const [kind, candidates] of [
|
|
111
|
+
["communicate", communicationTokens],
|
|
112
|
+
["authority", authorityTokens],
|
|
113
|
+
["transaction", transactionTokens],
|
|
114
|
+
["execute", executeTokens],
|
|
115
|
+
]) {
|
|
116
|
+
if (hasAny(evidence.tokens, candidates))
|
|
117
|
+
dangerousMatches.push(kind);
|
|
118
|
+
}
|
|
119
|
+
if (dangerousMatches.length > 0)
|
|
120
|
+
return dangerousMatches.length === 1 ? dangerousMatches[0] : "unknown";
|
|
121
|
+
const routineMatches = [];
|
|
122
|
+
for (const [kind, candidates] of [
|
|
123
|
+
["create", createTokens],
|
|
124
|
+
["update", updateTokens],
|
|
125
|
+
]) {
|
|
126
|
+
if (hasAny(evidence.tokens, candidates))
|
|
127
|
+
routineMatches.push(kind);
|
|
128
|
+
}
|
|
129
|
+
return routineMatches.length === 1 ? routineMatches[0] : "unknown";
|
|
130
|
+
}
|
|
131
|
+
function isVerifiedArrayBound(schema) {
|
|
132
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema))
|
|
133
|
+
return null;
|
|
134
|
+
if (schema.type !== "array")
|
|
135
|
+
return null;
|
|
136
|
+
const maxItems = schema.maxItems;
|
|
137
|
+
return typeof maxItems === "number" &&
|
|
138
|
+
Number.isSafeInteger(maxItems) &&
|
|
139
|
+
maxItems > 0
|
|
140
|
+
? maxItems
|
|
141
|
+
: null;
|
|
142
|
+
}
|
|
143
|
+
function argumentSection(arguments_, section) {
|
|
144
|
+
const value = arguments_[section];
|
|
145
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
146
|
+
? value
|
|
147
|
+
: undefined;
|
|
148
|
+
}
|
|
149
|
+
function presentParameterSelector(arguments_, parameter) {
|
|
150
|
+
if (parameter.in === "cookie")
|
|
151
|
+
return undefined;
|
|
152
|
+
const section = argumentSection(arguments_, parameter.in === "header" ? "headers" : parameter.in);
|
|
153
|
+
return section !== undefined && Object.hasOwn(section, parameter.name)
|
|
154
|
+
? section[parameter.name]
|
|
155
|
+
: undefined;
|
|
156
|
+
}
|
|
157
|
+
function boundedCardinality(operation, schemas, normalizedArguments) {
|
|
158
|
+
const schemaById = new Map();
|
|
159
|
+
const ambiguousSchemaIds = new Set();
|
|
160
|
+
for (const schema of schemas) {
|
|
161
|
+
if (schemaById.has(schema.id))
|
|
162
|
+
ambiguousSchemaIds.add(schema.id);
|
|
163
|
+
else
|
|
164
|
+
schemaById.set(schema.id, schema);
|
|
165
|
+
}
|
|
166
|
+
let maximum = 0;
|
|
167
|
+
let found = false;
|
|
168
|
+
let unprovenArray = false;
|
|
169
|
+
const containsArray = (value) => {
|
|
170
|
+
if (Array.isArray(value))
|
|
171
|
+
return true;
|
|
172
|
+
return (typeof value === "object" &&
|
|
173
|
+
value !== null &&
|
|
174
|
+
Object.values(value).some(containsArray));
|
|
175
|
+
};
|
|
176
|
+
const addBound = (schemaId, selector) => {
|
|
177
|
+
if (!Array.isArray(selector)) {
|
|
178
|
+
unprovenArray ||= containsArray(selector);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Independent selectors may multiply effects. A single array bound also
|
|
182
|
+
// does not bound nested arrays, so neither shape proves a routine limit.
|
|
183
|
+
if (found || selector.some(containsArray)) {
|
|
184
|
+
unprovenArray = true;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (ambiguousSchemaIds.has(schemaId)) {
|
|
188
|
+
unprovenArray = true;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const schema = schemaById.get(schemaId);
|
|
192
|
+
if (schema === undefined) {
|
|
193
|
+
unprovenArray = true;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const maxItems = isVerifiedArrayBound(schema.schema);
|
|
197
|
+
if (maxItems === null) {
|
|
198
|
+
unprovenArray = true;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
maximum = maxItems;
|
|
202
|
+
found = true;
|
|
203
|
+
};
|
|
204
|
+
for (const parameter of operation.parameters) {
|
|
205
|
+
addBound(parameter.value.schemaId, presentParameterSelector(normalizedArguments, parameter));
|
|
206
|
+
}
|
|
207
|
+
const requestBody = operation.requestBody;
|
|
208
|
+
if (Array.isArray(normalizedArguments.body)) {
|
|
209
|
+
if (requestBody === null || requestBody.content.length !== 1) {
|
|
210
|
+
unprovenArray = true;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
addBound(requestBody.content[0].schemaId, normalizedArguments.body);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (containsArray(normalizedArguments.body)) {
|
|
217
|
+
unprovenArray = true;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
cardinality: !unprovenArray && found && Number.isSafeInteger(maximum) && maximum > 0
|
|
221
|
+
? { kind: "bounded", maxAffected: maximum }
|
|
222
|
+
: null,
|
|
223
|
+
unprovenArray,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function hasTerminalRequiredResourceIdentifier(operation) {
|
|
227
|
+
const terminalSegment = operation.path.split("/").at(-1);
|
|
228
|
+
return operation.parameters.some((parameter) => parameter.in === "path" &&
|
|
229
|
+
parameter.required &&
|
|
230
|
+
terminalSegment === `{${parameter.name}}` &&
|
|
231
|
+
/^(?:[iI][dD]|[A-Za-z0-9]+(?:Id|ID)|[A-Za-z0-9]+[-_.][iI][dD])$/.test(parameter.name));
|
|
232
|
+
}
|
|
233
|
+
function cardinalityFor(operation, tokens, schemas, normalizedArguments, truncated) {
|
|
234
|
+
if (truncated)
|
|
235
|
+
return { kind: "unknown" };
|
|
236
|
+
if (hasAny(tokens, bulkTokens))
|
|
237
|
+
return { kind: "unbounded" };
|
|
238
|
+
const bounded = boundedCardinality(operation, schemas, normalizedArguments);
|
|
239
|
+
if (bounded.unprovenArray)
|
|
240
|
+
return { kind: "unknown" };
|
|
241
|
+
if (bounded.cardinality !== null)
|
|
242
|
+
return bounded.cardinality;
|
|
243
|
+
if (hasTerminalRequiredResourceIdentifier(operation))
|
|
244
|
+
return { kind: "single" };
|
|
245
|
+
return { kind: "unknown" };
|
|
246
|
+
}
|
|
247
|
+
function isDangerous(kind) {
|
|
248
|
+
return kind !== "create" && kind !== "update";
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Recompute safety, action kind, cardinality, and risk from verified records.
|
|
252
|
+
* Compiler-supplied advisory fields intentionally do not participate.
|
|
253
|
+
*/
|
|
254
|
+
export function classifyOperation(operation, schemas = [], normalizedArguments = {}) {
|
|
255
|
+
const evidence = normalizedTokens(operation);
|
|
256
|
+
if ((operation.method === "GET" || operation.method === "HEAD") &&
|
|
257
|
+
!evidence.truncated &&
|
|
258
|
+
!hasBatchSemantics(operation, evidence.tokens)) {
|
|
259
|
+
return {
|
|
260
|
+
safety: "read",
|
|
261
|
+
actionKind: null,
|
|
262
|
+
cardinality: null,
|
|
263
|
+
dangerous: false,
|
|
264
|
+
highRisk: false,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const actionKind = actionKindFor(operation, evidence);
|
|
268
|
+
const cardinality = cardinalityFor(operation, evidence.tokens, schemas, normalizedArguments, evidence.truncated);
|
|
269
|
+
const dangerous = isDangerous(actionKind);
|
|
270
|
+
return {
|
|
271
|
+
safety: "action",
|
|
272
|
+
actionKind,
|
|
273
|
+
cardinality,
|
|
274
|
+
dangerous,
|
|
275
|
+
highRisk: dangerous ||
|
|
276
|
+
cardinality.kind === "unknown" ||
|
|
277
|
+
cardinality.kind === "unbounded",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CredentialAuthorizationBinding, PreparedCall, Sha256 } from "./types.ts";
|
|
2
|
+
export declare function digestCredentialAuthorizationBinding(value: Omit<CredentialAuthorizationBinding, "bindingDigest">): Promise<Sha256>;
|
|
3
|
+
export declare function snapshotCredentialAuthorizationBinding(value: CredentialAuthorizationBinding, call?: Pick<PreparedCall, "credentialProfileId" | "credentialProfileDigest" | "reservedSlotsDigest">): Promise<CredentialAuthorizationBinding>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { sha256 } from "./digest.js";
|
|
2
|
+
import { OpenApiMcpError } from "./errors.js";
|
|
3
|
+
import { canonicalJsonBounded, parseJsonStrict } from "./strict-json.js";
|
|
4
|
+
const domain = "knitli.openapi-mcp.credential-binding.v1";
|
|
5
|
+
const digestPattern = /^[0-9a-f]{64}$/;
|
|
6
|
+
function hasDisallowedCharacter(value, forbidSpace) {
|
|
7
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
8
|
+
const code = value.charCodeAt(index);
|
|
9
|
+
if (code <= (forbidSpace ? 0x20 : 0x1f) || code === 0x7f)
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
function snapshot(value, hasDigest) {
|
|
15
|
+
try {
|
|
16
|
+
const json = canonicalJsonBounded(value, {
|
|
17
|
+
maxBytes: 16384,
|
|
18
|
+
maxDepth: 4,
|
|
19
|
+
maxNodes: 256,
|
|
20
|
+
});
|
|
21
|
+
const data = parseJsonStrict(json, {
|
|
22
|
+
maxBytes: 16384,
|
|
23
|
+
maxDepth: 4,
|
|
24
|
+
maxKeys: 256,
|
|
25
|
+
});
|
|
26
|
+
if (data === null || Array.isArray(data) || typeof data !== "object")
|
|
27
|
+
throw new Error();
|
|
28
|
+
const expected = [
|
|
29
|
+
"profileId",
|
|
30
|
+
"profileDigest",
|
|
31
|
+
"grantId",
|
|
32
|
+
"scopes",
|
|
33
|
+
"slotsDigest",
|
|
34
|
+
];
|
|
35
|
+
if (hasDigest)
|
|
36
|
+
expected.push("bindingDigest");
|
|
37
|
+
if (Object.hasOwn(data, "audience"))
|
|
38
|
+
expected.push("audience");
|
|
39
|
+
if (Object.keys(data).sort().join(",") !== expected.sort().join(","))
|
|
40
|
+
throw new Error();
|
|
41
|
+
if (typeof data.profileId !== "string" ||
|
|
42
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(data.profileId))
|
|
43
|
+
throw new Error();
|
|
44
|
+
if (typeof data.grantId !== "string" ||
|
|
45
|
+
!/^[A-Za-z0-9_-]{16,128}$/.test(data.grantId))
|
|
46
|
+
throw new Error();
|
|
47
|
+
for (const key of hasDigest
|
|
48
|
+
? ["profileDigest", "slotsDigest", "bindingDigest"]
|
|
49
|
+
: ["profileDigest", "slotsDigest"]) {
|
|
50
|
+
if (typeof data[key] !== "string" || !digestPattern.test(data[key]))
|
|
51
|
+
throw new Error();
|
|
52
|
+
}
|
|
53
|
+
if (Object.hasOwn(data, "audience") &&
|
|
54
|
+
(typeof data.audience !== "string" ||
|
|
55
|
+
data.audience.length === 0 ||
|
|
56
|
+
data.audience.length > 2048 ||
|
|
57
|
+
hasDisallowedCharacter(data.audience, false)))
|
|
58
|
+
throw new Error();
|
|
59
|
+
if (!Array.isArray(data.scopes) || data.scopes.length > 64)
|
|
60
|
+
throw new Error();
|
|
61
|
+
const scopes = [];
|
|
62
|
+
for (const scope of data.scopes) {
|
|
63
|
+
if (typeof scope !== "string" ||
|
|
64
|
+
scope.length === 0 ||
|
|
65
|
+
scope.length > 256 ||
|
|
66
|
+
hasDisallowedCharacter(scope, true) ||
|
|
67
|
+
scopes.includes(scope))
|
|
68
|
+
throw new Error();
|
|
69
|
+
scopes.push(scope);
|
|
70
|
+
}
|
|
71
|
+
data.scopes = scopes.sort();
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new OpenApiMcpError("AUTH_PROFILE_INVALID", "Credential binding is invalid");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function digestCredentialAuthorizationBinding(value) {
|
|
79
|
+
return sha256(domain, snapshot(value, false));
|
|
80
|
+
}
|
|
81
|
+
export async function snapshotCredentialAuthorizationBinding(value, call) {
|
|
82
|
+
const data = snapshot(value, true);
|
|
83
|
+
const callCommitments = call === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: {
|
|
86
|
+
profileId: call.credentialProfileId,
|
|
87
|
+
profileDigest: call.credentialProfileDigest,
|
|
88
|
+
slotsDigest: call.reservedSlotsDigest,
|
|
89
|
+
};
|
|
90
|
+
const { bindingDigest, ...payload } = data;
|
|
91
|
+
const expected = await sha256(domain, payload);
|
|
92
|
+
if (expected !== bindingDigest ||
|
|
93
|
+
(callCommitments !== undefined &&
|
|
94
|
+
(data.profileId !== callCommitments.profileId ||
|
|
95
|
+
data.profileDigest !== callCommitments.profileDigest ||
|
|
96
|
+
data.slotsDigest !== callCommitments.slotsDigest))) {
|
|
97
|
+
throw new OpenApiMcpError("AUTH_PROFILE_INVALID", "Credential binding does not match the prepared call");
|
|
98
|
+
}
|
|
99
|
+
Object.freeze(data.scopes);
|
|
100
|
+
return Object.freeze(data);
|
|
101
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JsonValue, Sha256 } from "./types.ts";
|
|
2
|
+
/** Hash domain-separated canonical JSON using portable Web Crypto. */
|
|
3
|
+
export declare function sha256(domain: string, value: JsonValue): Promise<Sha256>;
|
|
4
|
+
/**
|
|
5
|
+
* Verify a base64url Ed25519 signature with a base64url SPKI public key.
|
|
6
|
+
* Malformed material is deliberately an ordinary verification failure.
|
|
7
|
+
*/
|
|
8
|
+
export declare function verifyEd25519(payload: Uint8Array | string, signature: Uint8Array | string, key: Uint8Array | string): Promise<boolean>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { OpenApiMcpError } from "./errors.js";
|
|
2
|
+
import { canonicalJson } from "./strict-json.js";
|
|
3
|
+
const base64urlAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
4
|
+
function decodeBase64Url(value) {
|
|
5
|
+
if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1)
|
|
6
|
+
return null;
|
|
7
|
+
let accumulator = 0;
|
|
8
|
+
let bits = 0;
|
|
9
|
+
const bytes = [];
|
|
10
|
+
for (const character of value) {
|
|
11
|
+
const digit = base64urlAlphabet.indexOf(character);
|
|
12
|
+
if (digit < 0)
|
|
13
|
+
return null;
|
|
14
|
+
accumulator = (accumulator << 6) | digit;
|
|
15
|
+
bits += 6;
|
|
16
|
+
while (bits >= 8) {
|
|
17
|
+
bits -= 8;
|
|
18
|
+
bytes.push((accumulator >> bits) & 0xff);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (bits > 0 && (accumulator & ((1 << bits) - 1)) !== 0)
|
|
22
|
+
return null;
|
|
23
|
+
const decoded = Uint8Array.from(bytes);
|
|
24
|
+
return encodeBase64Url(decoded) === value ? decoded : null;
|
|
25
|
+
}
|
|
26
|
+
function encodeBase64Url(bytes) {
|
|
27
|
+
let accumulator = 0;
|
|
28
|
+
let bits = 0;
|
|
29
|
+
let encoded = "";
|
|
30
|
+
for (const byte of bytes) {
|
|
31
|
+
accumulator = (accumulator << 8) | byte;
|
|
32
|
+
bits += 8;
|
|
33
|
+
while (bits >= 6) {
|
|
34
|
+
bits -= 6;
|
|
35
|
+
encoded += base64urlAlphabet[(accumulator >> bits) & 0x3f];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (bits > 0)
|
|
39
|
+
encoded += base64urlAlphabet[(accumulator << (6 - bits)) & 0x3f];
|
|
40
|
+
return encoded;
|
|
41
|
+
}
|
|
42
|
+
function bytesFor(value) {
|
|
43
|
+
return typeof value === "string"
|
|
44
|
+
? decodeBase64Url(value)
|
|
45
|
+
: new Uint8Array(value);
|
|
46
|
+
}
|
|
47
|
+
function toArrayBuffer(bytes) {
|
|
48
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
49
|
+
copy.set(bytes);
|
|
50
|
+
return copy.buffer;
|
|
51
|
+
}
|
|
52
|
+
/** Hash domain-separated canonical JSON using portable Web Crypto. */
|
|
53
|
+
export async function sha256(domain, value) {
|
|
54
|
+
if (domain.length === 0 || domain.includes("\0")) {
|
|
55
|
+
throw new OpenApiMcpError("MANIFEST_INVALID", "Digest domain must be non-empty and NUL-free");
|
|
56
|
+
}
|
|
57
|
+
const bytes = new TextEncoder().encode(`${domain}\0${canonicalJson(value)}`);
|
|
58
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", toArrayBuffer(bytes));
|
|
59
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify a base64url Ed25519 signature with a base64url SPKI public key.
|
|
63
|
+
* Malformed material is deliberately an ordinary verification failure.
|
|
64
|
+
*/
|
|
65
|
+
export async function verifyEd25519(payload, signature, key) {
|
|
66
|
+
try {
|
|
67
|
+
const signatureBytes = bytesFor(signature);
|
|
68
|
+
const keyBytes = bytesFor(key);
|
|
69
|
+
if (signatureBytes === null || keyBytes === null)
|
|
70
|
+
return false;
|
|
71
|
+
const payloadBytes = typeof payload === "string"
|
|
72
|
+
? new TextEncoder().encode(payload)
|
|
73
|
+
: new Uint8Array(payload);
|
|
74
|
+
const publicKey = await globalThis.crypto.subtle.importKey("spki", toArrayBuffer(keyBytes), { name: "Ed25519" }, false, ["verify"]);
|
|
75
|
+
return await globalThis.crypto.subtle.verify("Ed25519", publicKey, toArrayBuffer(signatureBytes), toArrayBuffer(payloadBytes));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OpenApiValue } from "./types.ts";
|
|
2
|
+
export type OpenApiMcpErrorCode = "ARTIFACT_FORMAT_UNSUPPORTED" | "MANIFEST_INVALID" | "MANIFEST_SIGNATURE_INVALID" | "MANIFEST_ROLLBACK_REJECTED" | "MANIFEST_GENERATION_CONFLICT" | "RECORD_NOT_ADMITTED" | "RECORD_DIGEST_MISMATCH" | "OPERATION_REF_INVALID" | "OPERATION_NOT_FOUND" | "SCHEMA_RESOLUTION_LIMIT" | "INPUT_INVALID" | "TOOL_SAFETY_MISMATCH" | "ACTION_CONFIRMATION_REQUIRED" | "ACTION_DENIED" | "ACTION_CONFIRMATION_EXPIRED" | "AUTH_PROFILE_INVALID" | "AUTH_REQUIRED" | "DESTINATION_DENIED" | "RESPONSE_LIMIT_EXCEEDED" | "PAGINATION_LIMIT_EXCEEDED" | "UPSTREAM_ERROR" | "UPSTREAM_OUTCOME_UNKNOWN";
|
|
3
|
+
/**
|
|
4
|
+
* A stable, model-safe error. Callers must provide only pre-redacted details.
|
|
5
|
+
*/
|
|
6
|
+
export declare class OpenApiMcpError extends Error {
|
|
7
|
+
readonly code: OpenApiMcpErrorCode;
|
|
8
|
+
readonly retryable: boolean;
|
|
9
|
+
readonly details: Readonly<Record<string, OpenApiValue>>;
|
|
10
|
+
constructor(code: OpenApiMcpErrorCode, message?: string, options?: {
|
|
11
|
+
retryable?: boolean;
|
|
12
|
+
details?: Readonly<Record<string, OpenApiValue>>;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stable, model-safe error. Callers must provide only pre-redacted details.
|
|
3
|
+
*/
|
|
4
|
+
export class OpenApiMcpError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
retryable;
|
|
7
|
+
details;
|
|
8
|
+
constructor(code, message = code, options = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "OpenApiMcpError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.retryable = options.retryable ?? false;
|
|
13
|
+
this.details = options.details ?? {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type { ActionDispatchPermit } from "./action-permit.ts";
|
|
2
|
+
export { classifyOperation, type OperationClassification, } from "./classify.ts";
|
|
3
|
+
export { digestCredentialAuthorizationBinding, snapshotCredentialAuthorizationBinding, } from "./credential-binding.ts";
|
|
4
|
+
export { sha256, verifyEd25519 } from "./digest.ts";
|
|
5
|
+
export type { OpenApiMcpErrorCode } from "./errors.ts";
|
|
6
|
+
export { OpenApiMcpError } from "./errors.ts";
|
|
7
|
+
export type { AdmittedManifest, ManifestTrust, TrustedManifestKey, } from "./manifest.ts";
|
|
8
|
+
export { admitManifest } from "./manifest.ts";
|
|
9
|
+
export { createPreparedCall, digestBytes, digestPreparedCall, type PreparedCallInput, verifyPreparedCall, } from "./prepared-call.ts";
|
|
10
|
+
export { decodeOperationRef, encodeOperationRef, parseTypedRecordId, } from "./references.ts";
|
|
11
|
+
export { createOpenApiRuntime, type OpenApiRuntimeOptions } from "./runtime.ts";
|
|
12
|
+
export { resolveSchemaClosure } from "./schema-resolver.ts";
|
|
13
|
+
export { type SerializedArguments, serializeArguments } from "./serialize.ts";
|
|
14
|
+
export { CATALOG_STORE_PUBLIC_MESSAGES, createD1CatalogStore, type D1CatalogDatabase, type D1CatalogPreparedStatement, type D1CatalogResult, type D1CatalogValue, } from "./store.ts";
|
|
15
|
+
export type { StrictJsonLimits } from "./strict-json.ts";
|
|
16
|
+
export { canonicalJson, DEFAULT_STRICT_JSON_LIMITS, parseJsonStrict, } from "./strict-json.ts";
|
|
17
|
+
export type { ActionAuthorizer, ActionCardinality, ActionKind, AuthorizationContext, AuthorizationDecision, AuthorizationId, AuthorizedActionDecision, AuthorizedTransport, AuthProfile, CallOutcome, CandidateRef, CatalogId, CatalogStore, Credential, CredentialAuthorizationBinding, CredentialBindingResolver, CredentialProfileBinding, CredentialProvider, CredentialResolution, CredentialSlot, CredentialSlotContext, CredentialSnapshot, DestinationPolicy, GenerationState, GenerationStore, GenerationTransition, HttpMethod, JsonObject, JsonValue, LocalAuthProfile, ManifestEnvelope, ManifestSignature, OpenApiArguments, OpenApiRuntime, OpenApiValue, OperationRecordV4, OperationRef, OperationRefPayload, PaginationTokenCodec, PaginationTokenState, PreparedCall, PreparedDispatch, PrepareInput, ReleaseId, ReleaseManifestV4, RollbackAuthorization, SafeApprovalPresentation, SchemaRecordV4, SearchInput, SearchQuery, SearchResult, SearchResultItem, SearchWarning, SecretStore, Sha256, StoredRecord, TypedOperationId, TypedRecordId, TypedSchemaId, VerifiedActionRequestState, } from "./types.ts";
|
|
18
|
+
export { verifyStoredRecord } from "./verify-record.ts";
|
|
19
|
+
export type { RuntimeLimits } from "./versions.ts";
|
|
20
|
+
export { ARTIFACT_FORMAT_VERSION, DEFAULT_RUNTIME_LIMITS, MAX_SEARCH_QUERY_BYTES, PREPARED_CALL_VERSION, RUNTIME_CONTRACT_VERSION, resolveRuntimeLimits, } from "./versions.ts";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { classifyOperation, } from "./classify.js";
|
|
2
|
+
export { digestCredentialAuthorizationBinding, snapshotCredentialAuthorizationBinding, } from "./credential-binding.js";
|
|
3
|
+
export { sha256, verifyEd25519 } from "./digest.js";
|
|
4
|
+
export { OpenApiMcpError } from "./errors.js";
|
|
5
|
+
export { admitManifest } from "./manifest.js";
|
|
6
|
+
export { createPreparedCall, digestBytes, digestPreparedCall, verifyPreparedCall, } from "./prepared-call.js";
|
|
7
|
+
export { decodeOperationRef, encodeOperationRef, parseTypedRecordId, } from "./references.js";
|
|
8
|
+
export { createOpenApiRuntime } from "./runtime.js";
|
|
9
|
+
export { resolveSchemaClosure } from "./schema-resolver.js";
|
|
10
|
+
export { serializeArguments } from "./serialize.js";
|
|
11
|
+
export { CATALOG_STORE_PUBLIC_MESSAGES, createD1CatalogStore, } from "./store.js";
|
|
12
|
+
export { canonicalJson, DEFAULT_STRICT_JSON_LIMITS, parseJsonStrict, } from "./strict-json.js";
|
|
13
|
+
export { verifyStoredRecord } from "./verify-record.js";
|
|
14
|
+
export { ARTIFACT_FORMAT_VERSION, DEFAULT_RUNTIME_LIMITS, MAX_SEARCH_QUERY_BYTES, PREPARED_CALL_VERSION, RUNTIME_CONTRACT_VERSION, resolveRuntimeLimits, } from "./versions.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GenerationState, GenerationStore, ManifestEnvelope, ReleaseManifestV4, RollbackAuthorization, Sha256 } from "./types.ts";
|
|
2
|
+
import { type RuntimeLimits } from "./versions.ts";
|
|
3
|
+
export interface TrustedManifestKey {
|
|
4
|
+
issuer: string;
|
|
5
|
+
keyId: string;
|
|
6
|
+
/** Base64url-encoded Ed25519 SPKI public key. */
|
|
7
|
+
publicKey: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ManifestTrust {
|
|
10
|
+
releaseKeys: readonly TrustedManifestKey[];
|
|
11
|
+
rollbackKeys: readonly TrustedManifestKey[];
|
|
12
|
+
now?: () => Date;
|
|
13
|
+
}
|
|
14
|
+
export interface AdmittedManifest {
|
|
15
|
+
readonly manifest: ReleaseManifestV4;
|
|
16
|
+
readonly manifestDigest: Sha256;
|
|
17
|
+
}
|
|
18
|
+
export interface AuthenticatedManifest extends AdmittedManifest {
|
|
19
|
+
readonly rollback: RollbackAuthorization | undefined;
|
|
20
|
+
readonly rollbackMalformed: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Authenticate a v4/v5 manifest without reading or mutating generation state. */
|
|
23
|
+
export declare function authenticateManifest(envelope: ManifestEnvelope, trust: ManifestTrust, limitOverrides?: Partial<RuntimeLimits>): Promise<AuthenticatedManifest>;
|
|
24
|
+
/** Atomically commit the generation transition for an authenticated manifest. */
|
|
25
|
+
/**
|
|
26
|
+
* Commit only the generation decision made against `state`.
|
|
27
|
+
* A null result is a CAS miss; callers must reread and re-plan rather than
|
|
28
|
+
* reinterpreting this manifest under a newer generation policy.
|
|
29
|
+
*/
|
|
30
|
+
export declare function commitAuthenticatedManifestAtState(authenticated: AuthenticatedManifest, trust: ManifestTrust, generations: GenerationStore, state: GenerationState | null): Promise<AdmittedManifest | null>;
|
|
31
|
+
export declare function admitAuthenticatedManifest(authenticated: AuthenticatedManifest, trust: ManifestTrust, generations: GenerationStore): Promise<AdmittedManifest>;
|
|
32
|
+
/** Verify a v4/v5 manifest and atomically admit its generation transition. */
|
|
33
|
+
export declare function admitManifest(envelope: ManifestEnvelope, trust: ManifestTrust, generations: GenerationStore, limitOverrides?: Partial<RuntimeLimits>): Promise<AdmittedManifest>;
|