@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.
Files changed (116) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +362 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +249 -0
  6. package/dist/compile.d.ts +14 -0
  7. package/dist/compile.js +132 -0
  8. package/dist/compiler.d.ts +13 -0
  9. package/dist/compiler.js +7 -0
  10. package/dist/conformance/fixtures.d.ts +24 -0
  11. package/dist/conformance/fixtures.js +119 -0
  12. package/dist/conformance/index.d.ts +2 -0
  13. package/dist/conformance/index.js +2 -0
  14. package/dist/conformance/runtime-suite.d.ts +31 -0
  15. package/dist/conformance/runtime-suite.js +343 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/load.d.ts +31 -0
  19. package/dist/load.js +20 -0
  20. package/dist/operations.d.ts +27 -0
  21. package/dist/operations.js +231 -0
  22. package/dist/permissions.d.ts +31 -0
  23. package/dist/permissions.js +89 -0
  24. package/dist/release/compile-release.d.ts +33 -0
  25. package/dist/release/compile-release.js +1620 -0
  26. package/dist/release/load-v4.d.ts +29 -0
  27. package/dist/release/load-v4.js +297 -0
  28. package/dist/release/manifest-builder.d.ts +19 -0
  29. package/dist/release/manifest-builder.js +28 -0
  30. package/dist/release/publish.d.ts +45 -0
  31. package/dist/release/publish.js +346 -0
  32. package/dist/release/reference-map.d.ts +53 -0
  33. package/dist/release/reference-map.js +347 -0
  34. package/dist/release/schema-v4.d.ts +5 -0
  35. package/dist/release/schema-v4.js +91 -0
  36. package/dist/runtime/action-permit.d.ts +11 -0
  37. package/dist/runtime/action-permit.js +1 -0
  38. package/dist/runtime/classify.d.ts +16 -0
  39. package/dist/runtime/classify.js +279 -0
  40. package/dist/runtime/credential-binding.d.ts +3 -0
  41. package/dist/runtime/credential-binding.js +101 -0
  42. package/dist/runtime/digest.d.ts +8 -0
  43. package/dist/runtime/digest.js +80 -0
  44. package/dist/runtime/errors.d.ts +14 -0
  45. package/dist/runtime/errors.js +15 -0
  46. package/dist/runtime/index.d.ts +20 -0
  47. package/dist/runtime/index.js +14 -0
  48. package/dist/runtime/manifest.d.ts +33 -0
  49. package/dist/runtime/manifest.js +489 -0
  50. package/dist/runtime/prepared-call.d.ts +39 -0
  51. package/dist/runtime/prepared-call.js +566 -0
  52. package/dist/runtime/references.d.ts +7 -0
  53. package/dist/runtime/references.js +197 -0
  54. package/dist/runtime/runtime.d.ts +25 -0
  55. package/dist/runtime/runtime.js +1272 -0
  56. package/dist/runtime/schema-keywords.d.ts +2 -0
  57. package/dist/runtime/schema-keywords.js +30 -0
  58. package/dist/runtime/schema-resolver.d.ts +10 -0
  59. package/dist/runtime/schema-resolver.js +237 -0
  60. package/dist/runtime/serialize.d.ts +12 -0
  61. package/dist/runtime/serialize.js +877 -0
  62. package/dist/runtime/store.d.ts +52 -0
  63. package/dist/runtime/store.js +582 -0
  64. package/dist/runtime/strict-json.d.ts +19 -0
  65. package/dist/runtime/strict-json.js +539 -0
  66. package/dist/runtime/types.d.ts +457 -0
  67. package/dist/runtime/types.js +1 -0
  68. package/dist/runtime/verify-record.d.ts +6 -0
  69. package/dist/runtime/verify-record.js +492 -0
  70. package/dist/runtime/versions.d.ts +63 -0
  71. package/dist/runtime/versions.js +86 -0
  72. package/dist/safety.d.ts +28 -0
  73. package/dist/safety.js +60 -0
  74. package/dist/schema.d.ts +5 -0
  75. package/dist/schema.js +52 -0
  76. package/dist/schemas.d.ts +9 -0
  77. package/dist/schemas.js +16 -0
  78. package/dist/sign.d.ts +20 -0
  79. package/dist/sign.js +55 -0
  80. package/dist/sqlite/auth.d.ts +33 -0
  81. package/dist/sqlite/auth.js +973 -0
  82. package/dist/sqlite/catalog-store.d.ts +20 -0
  83. package/dist/sqlite/catalog-store.js +370 -0
  84. package/dist/sqlite/destination-guard.d.ts +24 -0
  85. package/dist/sqlite/destination-guard.js +140 -0
  86. package/dist/sqlite/generation-store.d.ts +12 -0
  87. package/dist/sqlite/generation-store.js +721 -0
  88. package/dist/sqlite/guarded-fetch.d.ts +24 -0
  89. package/dist/sqlite/guarded-fetch.js +626 -0
  90. package/dist/sqlite/index.d.ts +5 -0
  91. package/dist/sqlite/index.js +5 -0
  92. package/dist/sqlite/oauth-resource.d.ts +2 -0
  93. package/dist/sqlite/oauth-resource.js +15 -0
  94. package/dist/sqlite/release-completion.d.ts +5 -0
  95. package/dist/sqlite/release-completion.js +80 -0
  96. package/dist/stdio/action-broker.d.ts +15 -0
  97. package/dist/stdio/action-broker.js +136 -0
  98. package/dist/stdio/authorizer.d.ts +30 -0
  99. package/dist/stdio/authorizer.js +484 -0
  100. package/dist/stdio/config.d.ts +77 -0
  101. package/dist/stdio/config.js +144 -0
  102. package/dist/stdio/exact-policy.d.ts +41 -0
  103. package/dist/stdio/exact-policy.js +361 -0
  104. package/dist/stdio/index.d.ts +6 -0
  105. package/dist/stdio/index.js +148 -0
  106. package/dist/stdio/render.d.ts +8 -0
  107. package/dist/stdio/render.js +253 -0
  108. package/dist/stdio/request-state.d.ts +18 -0
  109. package/dist/stdio/request-state.js +79 -0
  110. package/dist/stdio/search.d.ts +10 -0
  111. package/dist/stdio/search.js +76 -0
  112. package/dist/stdio/server.d.ts +29 -0
  113. package/dist/stdio/server.js +309 -0
  114. package/dist/types.d.ts +32 -0
  115. package/dist/types.js +1 -0
  116. package/package.json +131 -0
@@ -0,0 +1,973 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { createServer, } from "node:http";
3
+ import { digestCredentialAuthorizationBinding, snapshotCredentialAuthorizationBinding, } from "../runtime/credential-binding.js";
4
+ import { sha256 } from "../runtime/digest.js";
5
+ import { OpenApiMcpError } from "../runtime/errors.js";
6
+ import { canonicalJsonBounded, parseJsonStrict, } from "../runtime/strict-json.js";
7
+ import { isOAuthResource } from "./oauth-resource.js";
8
+ const PROFILE_DIGEST_DOMAIN = "knitli.openapi-mcp.credential-profile.v1";
9
+ const SLOT_DIGEST_DOMAIN = "knitli.openapi-mcp.credential-slots.v1";
10
+ const PROFILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
11
+ const ENV_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
12
+ const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
13
+ const FORBIDDEN_API_KEY_HEADERS = new Set([
14
+ "accept",
15
+ "authorization",
16
+ "connection",
17
+ "cookie",
18
+ "forwarded",
19
+ "host",
20
+ "proxy-authenticate",
21
+ "proxy-authorization",
22
+ "set-cookie",
23
+ "transfer-encoding",
24
+ "upgrade",
25
+ "x-forwarded-for",
26
+ "x-forwarded-host",
27
+ "x-forwarded-proto",
28
+ "x-real-ip",
29
+ ]);
30
+ const CALLBACK_PATH = "/oauth/callback";
31
+ const DEFAULT_AUTHORIZATION_DEADLINE_MS = 120_000;
32
+ const DEFAULT_TOKEN_DEADLINE_MS = 30_000;
33
+ const DEFAULT_MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
34
+ const MAX_CALLBACK_PARAMETERS = 64;
35
+ const MAX_CALLBACK_PARAMETER_NAME_LENGTH = 128;
36
+ const MAX_CALLBACK_EXTENSION_VALUE_LENGTH = 2_048;
37
+ let providerInstanceSequence = 0;
38
+ export class MemorySecretStore {
39
+ #values = new Map();
40
+ async get(key) {
41
+ return this.#values.get(key) ?? null;
42
+ }
43
+ async set(key, value) {
44
+ this.#values.set(key, value);
45
+ }
46
+ async delete(key) {
47
+ this.#values.delete(key);
48
+ }
49
+ }
50
+ function invalidProfile() {
51
+ return new OpenApiMcpError("AUTH_PROFILE_INVALID", "Authentication profile is invalid");
52
+ }
53
+ function snapshotObject(value) {
54
+ try {
55
+ const json = canonicalJsonBounded(value, {
56
+ maxBytes: 32 * 1024,
57
+ maxDepth: 6,
58
+ maxNodes: 512,
59
+ });
60
+ const snapshot = parseJsonStrict(json, {
61
+ maxBytes: 32 * 1024,
62
+ maxDepth: 6,
63
+ maxKeys: 512,
64
+ });
65
+ if (snapshot === null ||
66
+ Array.isArray(snapshot) ||
67
+ typeof snapshot !== "object") {
68
+ throw invalidProfile();
69
+ }
70
+ return snapshot;
71
+ }
72
+ catch {
73
+ throw invalidProfile();
74
+ }
75
+ }
76
+ function exactKeys(value, required, optional = []) {
77
+ const allowed = new Set([...required, ...optional]);
78
+ const keys = Object.keys(value);
79
+ if (required.some((key) => !Object.hasOwn(value, key)) ||
80
+ keys.some((key) => !allowed.has(key))) {
81
+ throw invalidProfile();
82
+ }
83
+ }
84
+ function validateOrigin(value) {
85
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048) {
86
+ throw invalidProfile();
87
+ }
88
+ try {
89
+ const url = new URL(value);
90
+ if (url.protocol !== "https:" ||
91
+ url.username !== "" ||
92
+ url.password !== "" ||
93
+ url.pathname !== "/" ||
94
+ url.search !== "" ||
95
+ url.hash !== "" ||
96
+ url.origin !== value) {
97
+ throw invalidProfile();
98
+ }
99
+ return value;
100
+ }
101
+ catch {
102
+ throw invalidProfile();
103
+ }
104
+ }
105
+ function validateStringArray(value, maximum, allowSpace) {
106
+ if (!Array.isArray(value) || value.length > maximum)
107
+ throw invalidProfile();
108
+ const normalized = [];
109
+ for (const entry of value) {
110
+ if (typeof entry !== "string" ||
111
+ entry.length === 0 ||
112
+ entry.length > 256 ||
113
+ [...entry].some((character) => {
114
+ const code = character.charCodeAt(0);
115
+ return code <= (allowSpace ? 0x1f : 0x20) || code === 0x7f;
116
+ }) ||
117
+ normalized.includes(entry)) {
118
+ throw invalidProfile();
119
+ }
120
+ normalized.push(entry);
121
+ }
122
+ normalized.sort();
123
+ return Object.freeze(normalized);
124
+ }
125
+ function validateOptionalText(value) {
126
+ if (value === undefined)
127
+ return undefined;
128
+ if (typeof value !== "string" ||
129
+ value.length === 0 ||
130
+ value.length > 2048 ||
131
+ [...value].some((character) => {
132
+ const code = character.charCodeAt(0);
133
+ return code < 0x20 || code === 0x7f;
134
+ })) {
135
+ throw invalidProfile();
136
+ }
137
+ return value;
138
+ }
139
+ function validateResource(value) {
140
+ if (value === undefined)
141
+ return undefined;
142
+ if (!isOAuthResource(value))
143
+ throw invalidProfile();
144
+ return value;
145
+ }
146
+ function validateEndpoint(value) {
147
+ if (typeof value !== "string" || value.length > 2048)
148
+ throw invalidProfile();
149
+ try {
150
+ const url = new URL(value);
151
+ if (url.protocol !== "https:" ||
152
+ url.username !== "" ||
153
+ url.password !== "" ||
154
+ url.hash !== "" ||
155
+ url.toString() !== value) {
156
+ throw invalidProfile();
157
+ }
158
+ return value;
159
+ }
160
+ catch {
161
+ throw invalidProfile();
162
+ }
163
+ }
164
+ function validateAuth(value) {
165
+ if (value === null || Array.isArray(value) || typeof value !== "object") {
166
+ throw invalidProfile();
167
+ }
168
+ const auth = value;
169
+ if (auth.type === "bearer-env") {
170
+ exactKeys(auth, ["env", "type"]);
171
+ if (typeof auth.env !== "string" || !ENV_PATTERN.test(auth.env)) {
172
+ throw invalidProfile();
173
+ }
174
+ return Object.freeze({ type: "bearer-env", env: auth.env });
175
+ }
176
+ if (auth.type === "api-key-env") {
177
+ exactKeys(auth, ["env", "name", "placement", "type"]);
178
+ if (typeof auth.env !== "string" ||
179
+ !ENV_PATTERN.test(auth.env) ||
180
+ (auth.placement !== "header" && auth.placement !== "query") ||
181
+ typeof auth.name !== "string" ||
182
+ auth.name.length === 0 ||
183
+ auth.name.length > 256) {
184
+ throw invalidProfile();
185
+ }
186
+ if (auth.placement === "header" &&
187
+ (!HEADER_NAME_PATTERN.test(auth.name) ||
188
+ FORBIDDEN_API_KEY_HEADERS.has(auth.name.toLowerCase()) ||
189
+ auth.name.toLowerCase().startsWith("x-forwarded-"))) {
190
+ throw invalidProfile();
191
+ }
192
+ if (auth.placement === "query" &&
193
+ [...auth.name].some((character) => {
194
+ const code = character.charCodeAt(0);
195
+ return code <= 0x20 || code === 0x7f;
196
+ })) {
197
+ throw invalidProfile();
198
+ }
199
+ return Object.freeze({
200
+ type: "api-key-env",
201
+ env: auth.env,
202
+ placement: auth.placement,
203
+ name: auth.placement === "header" ? auth.name.toLowerCase() : auth.name,
204
+ });
205
+ }
206
+ if (auth.type === "oauth2-pkce") {
207
+ exactKeys(auth, ["authorizationEndpoint", "clientId", "scopes", "tokenEndpoint", "type"], ["resource"]);
208
+ if (typeof auth.clientId !== "string" ||
209
+ auth.clientId.length === 0 ||
210
+ auth.clientId.length > 2048) {
211
+ throw invalidProfile();
212
+ }
213
+ const scopes = validateStringArray(auth.scopes, 64, false);
214
+ const resource = validateResource(auth.resource);
215
+ const validated = {
216
+ type: "oauth2-pkce",
217
+ authorizationEndpoint: validateEndpoint(auth.authorizationEndpoint),
218
+ tokenEndpoint: validateEndpoint(auth.tokenEndpoint),
219
+ clientId: auth.clientId,
220
+ scopes,
221
+ ...(resource === undefined ? {} : { resource }),
222
+ };
223
+ return Object.freeze(validated);
224
+ }
225
+ throw invalidProfile();
226
+ }
227
+ function validateProfile(raw, manifestOrigins) {
228
+ const profile = snapshotObject(raw);
229
+ exactKeys(profile, ["allowedOrigins", "auth", "profileId", "revision"], ["audience", "scopes"]);
230
+ if (typeof profile.profileId !== "string" ||
231
+ !PROFILE_PATTERN.test(profile.profileId) ||
232
+ typeof profile.revision !== "number" ||
233
+ !Number.isSafeInteger(profile.revision) ||
234
+ profile.revision <= 0 ||
235
+ !Array.isArray(profile.allowedOrigins) ||
236
+ profile.allowedOrigins.length === 0 ||
237
+ profile.allowedOrigins.length > 64) {
238
+ throw invalidProfile();
239
+ }
240
+ const allowedOrigins = profile.allowedOrigins.map(validateOrigin);
241
+ if (new Set(allowedOrigins).size !== allowedOrigins.length) {
242
+ throw invalidProfile();
243
+ }
244
+ allowedOrigins.sort();
245
+ const manifests = manifestOrigins.map(validateOrigin);
246
+ const effectiveOrigins = new Set(allowedOrigins.filter((origin) => manifests.includes(origin)));
247
+ if (effectiveOrigins.size === 0)
248
+ throw invalidProfile();
249
+ const auth = validateAuth(profile.auth);
250
+ const audience = validateOptionalText(profile.audience);
251
+ let scopes;
252
+ if (auth.type === "oauth2-pkce") {
253
+ if (Object.hasOwn(profile, "scopes"))
254
+ throw invalidProfile();
255
+ scopes = auth.scopes;
256
+ }
257
+ else {
258
+ scopes = Object.hasOwn(profile, "scopes")
259
+ ? validateStringArray(profile.scopes, 64, false)
260
+ : Object.freeze([]);
261
+ }
262
+ const slots = Object.freeze([
263
+ Object.freeze(auth.type === "api-key-env"
264
+ ? { placement: auth.placement, name: auth.name }
265
+ : { placement: "header", name: "authorization" }),
266
+ ]);
267
+ const value = Object.freeze({
268
+ profileId: profile.profileId,
269
+ revision: profile.revision,
270
+ allowedOrigins: Object.freeze(allowedOrigins),
271
+ auth,
272
+ ...(audience === undefined ? {} : { audience }),
273
+ ...(auth.type === "oauth2-pkce" ? {} : { scopes }),
274
+ });
275
+ return {
276
+ value,
277
+ auth,
278
+ allowedOrigins: value.allowedOrigins,
279
+ effectiveOrigins,
280
+ scopes,
281
+ audience: audience ?? (auth.type === "oauth2-pkce" ? auth.resource : undefined),
282
+ slots,
283
+ };
284
+ }
285
+ function defaultRandom(size) {
286
+ return globalThis.crypto.getRandomValues(new Uint8Array(size));
287
+ }
288
+ function randomIdentifier(randomBytes) {
289
+ let bytes;
290
+ try {
291
+ bytes = randomBytes(24);
292
+ }
293
+ catch {
294
+ throw invalidProfile();
295
+ }
296
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength !== 24) {
297
+ throw invalidProfile();
298
+ }
299
+ return Buffer.from(bytes).toString("base64url");
300
+ }
301
+ async function digestSlots(slots) {
302
+ const canonicalSlots = slots
303
+ .map((slot) => ({ placement: slot.placement, name: slot.name }))
304
+ .sort((left, right) => `${left.placement}:${left.name}`.localeCompare(`${right.placement}:${right.name}`));
305
+ return sha256(SLOT_DIGEST_DOMAIN, canonicalSlots);
306
+ }
307
+ /** Validate local profile semantics without resolving credentials or performing I/O. */
308
+ export function validateLocalAuthProfile(profile) {
309
+ const snapshot = snapshotObject(profile);
310
+ if (!Array.isArray(snapshot.allowedOrigins))
311
+ throw invalidProfile();
312
+ return validateProfile(snapshot, snapshot.allowedOrigins).value;
313
+ }
314
+ export async function digestCredentialProfile(profile) {
315
+ return sha256(PROFILE_DIGEST_DOMAIN, snapshotObject(validateLocalAuthProfile(profile)));
316
+ }
317
+ async function createBinding(profile, profileDigest, slotsDigest, grantId, scopes) {
318
+ const payload = {
319
+ profileId: profile.value.profileId,
320
+ profileDigest,
321
+ grantId,
322
+ ...(profile.audience === undefined ? {} : { audience: profile.audience }),
323
+ scopes,
324
+ slotsDigest,
325
+ };
326
+ return snapshotCredentialAuthorizationBinding({
327
+ ...payload,
328
+ bindingDigest: await digestCredentialAuthorizationBinding(payload),
329
+ });
330
+ }
331
+ function createBindingResolver(profile, profileDigest) {
332
+ const frozenSlots = profile.slots;
333
+ const binding = Object.freeze({
334
+ profileId: profile.value.profileId,
335
+ profileDigest,
336
+ slots: frozenSlots,
337
+ });
338
+ return Object.freeze({
339
+ async resolve(context) {
340
+ if (!profile.effectiveOrigins.has(context.origin)) {
341
+ throw new OpenApiMcpError("DESTINATION_DENIED", "Credential profile does not allow destination");
342
+ }
343
+ return binding;
344
+ },
345
+ });
346
+ }
347
+ function freezeCredential(credential) {
348
+ return Object.freeze({ ...credential });
349
+ }
350
+ function freezeSnapshot(credential, binding) {
351
+ return Object.freeze({ credential: freezeCredential(credential), binding });
352
+ }
353
+ function boundedPositiveOption(value, defaultValue) {
354
+ if (value === undefined)
355
+ return defaultValue;
356
+ if (!Number.isSafeInteger(value) || value <= 0 || value > defaultValue) {
357
+ throw invalidProfile();
358
+ }
359
+ return value;
360
+ }
361
+ function randomOpaque(randomBytes, size = 32) {
362
+ let bytes;
363
+ try {
364
+ bytes = randomBytes(size);
365
+ }
366
+ catch {
367
+ throw invalidProfile();
368
+ }
369
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength !== size) {
370
+ throw invalidProfile();
371
+ }
372
+ return Buffer.from(bytes).toString("base64url");
373
+ }
374
+ function equalState(left, right) {
375
+ const leftBytes = Buffer.from(left, "utf8");
376
+ const rightBytes = Buffer.from(right, "utf8");
377
+ if (leftBytes.byteLength !== rightBytes.byteLength)
378
+ return false;
379
+ return timingSafeEqual(leftBytes, rightBytes);
380
+ }
381
+ function callbackReply(response, status, message) {
382
+ response.writeHead(status, {
383
+ "cache-control": "no-store",
384
+ "content-type": "text/plain; charset=utf-8",
385
+ "x-content-type-options": "nosniff",
386
+ });
387
+ response.end(message);
388
+ }
389
+ async function readBoundedResponse(response, maximumBytes) {
390
+ const declared = response.headers.get("content-length");
391
+ if (declared !== null) {
392
+ const length = Number(declared);
393
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximumBytes) {
394
+ throw new Error("token response rejected");
395
+ }
396
+ }
397
+ if (response.body === null)
398
+ return "";
399
+ const reader = response.body.getReader();
400
+ const chunks = [];
401
+ let bytes = 0;
402
+ try {
403
+ while (true) {
404
+ const item = await reader.read();
405
+ if (item.done)
406
+ break;
407
+ bytes += item.value.byteLength;
408
+ if (bytes > maximumBytes) {
409
+ await reader.cancel();
410
+ throw new Error("token response rejected");
411
+ }
412
+ chunks.push(item.value);
413
+ }
414
+ }
415
+ finally {
416
+ reader.releaseLock();
417
+ }
418
+ const joined = new Uint8Array(bytes);
419
+ let offset = 0;
420
+ for (const chunk of chunks) {
421
+ joined.set(chunk, offset);
422
+ offset += chunk.byteLength;
423
+ }
424
+ return new TextDecoder("utf-8", { fatal: true }).decode(joined);
425
+ }
426
+ function parseTokenScopes(value, requested) {
427
+ if (value === undefined)
428
+ return Object.freeze([...requested].sort());
429
+ if (typeof value !== "string")
430
+ throw new Error("token response rejected");
431
+ const scopes = value === "" ? [] : value.split(" ");
432
+ if (scopes.some((scope, index) => scope.length === 0 ||
433
+ scope.length > 256 ||
434
+ scopes.indexOf(scope) !== index ||
435
+ !requested.includes(scope))) {
436
+ throw new Error("token response rejected");
437
+ }
438
+ return Object.freeze([...scopes].sort());
439
+ }
440
+ async function parseTokenResponse(response, requestedScopes, now, maximumBytes) {
441
+ if (response.redirected ||
442
+ (response.status >= 300 && response.status < 400)) {
443
+ throw new Error("token response rejected");
444
+ }
445
+ if (!response.ok)
446
+ throw new Error("token response rejected");
447
+ const text = await readBoundedResponse(response, maximumBytes);
448
+ const parsed = parseJsonStrict(text, {
449
+ maxBytes: maximumBytes,
450
+ maxDepth: 4,
451
+ maxKeys: 64,
452
+ });
453
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
454
+ throw new Error("token response rejected");
455
+ }
456
+ const token = parsed;
457
+ if (typeof token.access_token !== "string" ||
458
+ token.access_token.length === 0 ||
459
+ token.access_token.length > maximumBytes ||
460
+ typeof token.token_type !== "string" ||
461
+ token.token_type.toLowerCase() !== "bearer" ||
462
+ (Object.hasOwn(token, "refresh_token") &&
463
+ (typeof token.refresh_token !== "string" ||
464
+ token.refresh_token.length === 0 ||
465
+ token.refresh_token.length > maximumBytes)) ||
466
+ (Object.hasOwn(token, "expires_in") &&
467
+ (typeof token.expires_in !== "number" ||
468
+ !Number.isSafeInteger(token.expires_in) ||
469
+ token.expires_in <= 0))) {
470
+ throw new Error("token response rejected");
471
+ }
472
+ const expiresAt = Object.hasOwn(token, "expires_in")
473
+ ? now() + token.expires_in * 1000
474
+ : Number.MAX_SAFE_INTEGER;
475
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= now()) {
476
+ throw new Error("token response rejected");
477
+ }
478
+ return Object.freeze({
479
+ accessToken: token.access_token,
480
+ ...(typeof token.refresh_token === "string"
481
+ ? { refreshToken: token.refresh_token }
482
+ : {}),
483
+ expiresAt,
484
+ scopes: parseTokenScopes(token.scope, requestedScopes),
485
+ });
486
+ }
487
+ function closeServer(server) {
488
+ try {
489
+ server.close();
490
+ }
491
+ catch {
492
+ // Closing an already-closed listener is intentionally idempotent.
493
+ }
494
+ }
495
+ async function listenLoopback(server) {
496
+ return new Promise((resolve, reject) => {
497
+ const onError = () => reject(new OpenApiMcpError("AUTH_REQUIRED", "Authorization could not start"));
498
+ server.once("error", onError);
499
+ server.listen(0, "127.0.0.1", () => {
500
+ server.off("error", onError);
501
+ const address = server.address();
502
+ if (address === null ||
503
+ typeof address === "string" ||
504
+ address.address !== "127.0.0.1") {
505
+ closeServer(server);
506
+ reject(new OpenApiMcpError("AUTH_REQUIRED", "Authorization could not start"));
507
+ return;
508
+ }
509
+ resolve(address.port);
510
+ });
511
+ });
512
+ }
513
+ function createOAuthProvider(profile, profileDigest, slotsDigest, bindingResolver, options, randomBytes) {
514
+ const secretStore = options.secretStore ?? new MemorySecretStore();
515
+ const fetchToken = options.fetch ?? globalThis.fetch;
516
+ const now = options.now ?? Date.now;
517
+ const authorizationDeadlineMs = boundedPositiveOption(options.authorizationDeadlineMs, DEFAULT_AUTHORIZATION_DEADLINE_MS);
518
+ const tokenDeadlineMs = boundedPositiveOption(options.tokenDeadlineMs, DEFAULT_TOKEN_DEADLINE_MS);
519
+ const maxTokenResponseBytes = boundedPositiveOption(options.maxTokenResponseBytes, DEFAULT_MAX_TOKEN_RESPONSE_BYTES);
520
+ providerInstanceSequence += 1;
521
+ if (!Number.isSafeInteger(providerInstanceSequence))
522
+ throw invalidProfile();
523
+ const storageNamespace = `${randomOpaque(randomBytes)}.${providerInstanceSequence.toString(36)}`;
524
+ const tokenKeys = (ownedGeneration) => Object.freeze({
525
+ access: `oauth:${profile.value.profileId}:${storageNamespace}:${ownedGeneration}:access`,
526
+ refresh: `oauth:${profile.value.profileId}:${storageNamespace}:${ownedGeneration}:refresh`,
527
+ });
528
+ let generation = 0;
529
+ let closed = false;
530
+ let pending;
531
+ let authorizationPromise;
532
+ let refreshPromise;
533
+ let grantId;
534
+ let liveScopes = Object.freeze([...profile.scopes]);
535
+ let expiresAt = 0;
536
+ const safeGet = async (key) => {
537
+ try {
538
+ return await secretStore.get(key);
539
+ }
540
+ catch {
541
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential storage is unavailable");
542
+ }
543
+ };
544
+ const safeSet = async (key, value) => {
545
+ try {
546
+ await secretStore.set(key, value);
547
+ }
548
+ catch {
549
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential storage is unavailable");
550
+ }
551
+ };
552
+ const safeDelete = async (key) => {
553
+ try {
554
+ await secretStore.delete(key);
555
+ }
556
+ catch {
557
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential storage is unavailable");
558
+ }
559
+ };
560
+ const deleteTokens = async (ownedGeneration) => {
561
+ const keys = tokenKeys(ownedGeneration);
562
+ let deletionFailed = false;
563
+ try {
564
+ await safeDelete(keys.access);
565
+ }
566
+ catch {
567
+ deletionFailed = true;
568
+ }
569
+ try {
570
+ await safeDelete(keys.refresh);
571
+ }
572
+ catch {
573
+ deletionFailed = true;
574
+ }
575
+ if (deletionFailed) {
576
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential storage is unavailable");
577
+ }
578
+ };
579
+ const commitToken = async (token, expectedGeneration, refreshFallback) => {
580
+ try {
581
+ if (generation !== expectedGeneration || closed)
582
+ return false;
583
+ const keys = tokenKeys(expectedGeneration);
584
+ await safeSet(keys.access, token.accessToken);
585
+ if (generation !== expectedGeneration || closed) {
586
+ await deleteTokens(expectedGeneration);
587
+ return false;
588
+ }
589
+ const nextRefresh = token.refreshToken ?? refreshFallback;
590
+ if (nextRefresh === undefined)
591
+ await safeDelete(keys.refresh);
592
+ else
593
+ await safeSet(keys.refresh, nextRefresh);
594
+ if (generation !== expectedGeneration || closed) {
595
+ await deleteTokens(expectedGeneration);
596
+ return false;
597
+ }
598
+ expiresAt = token.expiresAt;
599
+ liveScopes = token.scopes;
600
+ return true;
601
+ }
602
+ catch {
603
+ try {
604
+ await deleteTokens(expectedGeneration);
605
+ }
606
+ catch {
607
+ // Best effort: an unreliable SecretStore may retain physical bytes.
608
+ }
609
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential storage is unavailable");
610
+ }
611
+ };
612
+ const exchange = async (body, requestedScopes, expectedGeneration, refreshFallback) => {
613
+ const response = await fetchToken(profile.auth.tokenEndpoint, {
614
+ method: "POST",
615
+ headers: {
616
+ accept: "application/json",
617
+ "content-type": "application/x-www-form-urlencoded",
618
+ },
619
+ body: body.toString(),
620
+ redirect: "manual",
621
+ signal: AbortSignal.timeout(tokenDeadlineMs),
622
+ });
623
+ if (generation !== expectedGeneration || closed)
624
+ return false;
625
+ const token = await parseTokenResponse(response, requestedScopes, now, maxTokenResponseBytes);
626
+ if (generation !== expectedGeneration || closed)
627
+ return false;
628
+ return commitToken(token, expectedGeneration, refreshFallback);
629
+ };
630
+ const snapshot = async (accessToken, expectedGeneration) => {
631
+ if (generation !== expectedGeneration || closed) {
632
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
633
+ }
634
+ if (grantId === undefined)
635
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization is required");
636
+ const binding = await createBinding(profile, profileDigest, slotsDigest, grantId, liveScopes);
637
+ if (generation !== expectedGeneration || closed) {
638
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
639
+ }
640
+ return Object.freeze({
641
+ status: "ready",
642
+ snapshot: freezeSnapshot({ type: "bearer", token: accessToken }, binding),
643
+ });
644
+ };
645
+ const beginAuthorization = async () => {
646
+ if (closed)
647
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential provider is closed");
648
+ if (pending !== undefined) {
649
+ return Object.freeze({
650
+ status: "auth-required",
651
+ authorizationUrl: pending.authorizationUrl,
652
+ expiresAt: pending.expiresAt,
653
+ });
654
+ }
655
+ const expectedGeneration = generation;
656
+ const state = randomOpaque(randomBytes);
657
+ const verifier = randomOpaque(randomBytes);
658
+ const challengeDigest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
659
+ if (generation !== expectedGeneration || closed) {
660
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
661
+ }
662
+ let currentPending;
663
+ const handler = (request, response) => {
664
+ if (currentPending === undefined ||
665
+ pending !== currentPending ||
666
+ currentPending.terminal ||
667
+ request.method !== "GET" ||
668
+ typeof request.url !== "string" ||
669
+ request.url.length > 8192 ||
670
+ request.headers.host !==
671
+ `127.0.0.1:${new URL(currentPending.redirectUri).port}`) {
672
+ callbackReply(response, 400, "OAuth callback rejected");
673
+ return;
674
+ }
675
+ let callback;
676
+ try {
677
+ callback = new URL(request.url, currentPending.redirectUri);
678
+ }
679
+ catch {
680
+ callbackReply(response, 400, "OAuth callback rejected");
681
+ return;
682
+ }
683
+ const states = callback.searchParams.getAll("state");
684
+ const codes = callback.searchParams.getAll("code");
685
+ const errors = callback.searchParams.getAll("error");
686
+ const callbackParameters = [...callback.searchParams.entries()];
687
+ const code = codes.length === 1 ? codes[0] : undefined;
688
+ const oauthError = errors.length === 1 ? errors[0] : undefined;
689
+ if (callback.pathname !== CALLBACK_PATH ||
690
+ callback.origin !== new URL(currentPending.redirectUri).origin ||
691
+ callback.hash !== "" ||
692
+ callbackParameters.length > MAX_CALLBACK_PARAMETERS ||
693
+ callbackParameters.some(([key, value]) => {
694
+ const maximumValueLength = key === "code"
695
+ ? 4_096
696
+ : key === "error"
697
+ ? 256
698
+ : key === "state"
699
+ ? 128
700
+ : MAX_CALLBACK_EXTENSION_VALUE_LENGTH;
701
+ return (key.length === 0 ||
702
+ key.length > MAX_CALLBACK_PARAMETER_NAME_LENGTH ||
703
+ value.length > maximumValueLength);
704
+ }) ||
705
+ states.length !== 1 ||
706
+ !equalState(states[0] ?? "", currentPending.state) ||
707
+ ((code === undefined || code === "" || code.length > 4096) &&
708
+ (oauthError === undefined ||
709
+ oauthError === "" ||
710
+ oauthError.length > 256)) ||
711
+ (code !== undefined && oauthError !== undefined) ||
712
+ codes.length > 1 ||
713
+ errors.length > 1) {
714
+ callbackReply(response, 400, "OAuth callback rejected");
715
+ return;
716
+ }
717
+ currentPending.terminal = true;
718
+ clearTimeout(currentPending.timer);
719
+ closeServer(currentPending.server);
720
+ if (oauthError !== undefined || code === undefined) {
721
+ pending = undefined;
722
+ callbackReply(response, 400, "Authorization failed");
723
+ return;
724
+ }
725
+ const body = new URLSearchParams({
726
+ grant_type: "authorization_code",
727
+ code,
728
+ redirect_uri: currentPending.redirectUri,
729
+ client_id: profile.auth.clientId,
730
+ code_verifier: currentPending.verifier,
731
+ });
732
+ if (profile.auth.resource !== undefined) {
733
+ body.set("resource", profile.auth.resource);
734
+ }
735
+ void exchange(body, profile.scopes, expectedGeneration)
736
+ .then((committed) => {
737
+ if (!committed || generation !== expectedGeneration || closed) {
738
+ callbackReply(response, 409, "Authorization failed");
739
+ return;
740
+ }
741
+ grantId = randomIdentifier(randomBytes);
742
+ callbackReply(response, 200, "Authorization complete");
743
+ })
744
+ .catch(() => callbackReply(response, 502, "Authorization failed"))
745
+ .finally(() => {
746
+ if (pending === currentPending)
747
+ pending = undefined;
748
+ });
749
+ };
750
+ const server = createServer(handler);
751
+ const port = await listenLoopback(server);
752
+ if (generation !== expectedGeneration || closed) {
753
+ closeServer(server);
754
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
755
+ }
756
+ const redirectUri = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
757
+ const authorization = new URL(profile.auth.authorizationEndpoint);
758
+ authorization.searchParams.set("response_type", "code");
759
+ authorization.searchParams.set("client_id", profile.auth.clientId);
760
+ authorization.searchParams.set("redirect_uri", redirectUri);
761
+ authorization.searchParams.set("scope", profile.scopes.join(" "));
762
+ authorization.searchParams.set("state", state);
763
+ authorization.searchParams.set("code_challenge", Buffer.from(challengeDigest).toString("base64url"));
764
+ authorization.searchParams.set("code_challenge_method", "S256");
765
+ if (profile.auth.resource !== undefined) {
766
+ authorization.searchParams.set("resource", profile.auth.resource);
767
+ }
768
+ const deadline = now() + authorizationDeadlineMs;
769
+ if (!Number.isSafeInteger(deadline)) {
770
+ closeServer(server);
771
+ throw invalidProfile();
772
+ }
773
+ currentPending = {
774
+ generation: expectedGeneration,
775
+ server,
776
+ authorizationUrl: authorization.toString(),
777
+ expiresAt: new Date(deadline).toISOString(),
778
+ state,
779
+ verifier,
780
+ redirectUri,
781
+ terminal: false,
782
+ timer: setTimeout(() => {
783
+ const flow = currentPending;
784
+ if (flow !== undefined && pending === flow && !flow.terminal) {
785
+ flow.terminal = true;
786
+ pending = undefined;
787
+ closeServer(server);
788
+ }
789
+ }, authorizationDeadlineMs),
790
+ };
791
+ pending = currentPending;
792
+ return Object.freeze({
793
+ status: "auth-required",
794
+ authorizationUrl: currentPending.authorizationUrl,
795
+ expiresAt: currentPending.expiresAt,
796
+ });
797
+ };
798
+ const startAuthorization = () => {
799
+ if (pending !== undefined) {
800
+ return Promise.resolve(Object.freeze({
801
+ status: "auth-required",
802
+ authorizationUrl: pending.authorizationUrl,
803
+ expiresAt: pending.expiresAt,
804
+ }));
805
+ }
806
+ authorizationPromise ??= beginAuthorization().finally(() => {
807
+ authorizationPromise = undefined;
808
+ });
809
+ return authorizationPromise;
810
+ };
811
+ const refresh = async (refreshToken) => {
812
+ const expectedGeneration = generation;
813
+ const keys = tokenKeys(expectedGeneration);
814
+ const body = new URLSearchParams({
815
+ grant_type: "refresh_token",
816
+ refresh_token: refreshToken,
817
+ client_id: profile.auth.clientId,
818
+ });
819
+ if (liveScopes.length > 0)
820
+ body.set("scope", liveScopes.join(" "));
821
+ if (profile.auth.resource !== undefined)
822
+ body.set("resource", profile.auth.resource);
823
+ try {
824
+ const committed = await exchange(body, liveScopes, expectedGeneration, refreshToken);
825
+ if (!committed || generation !== expectedGeneration || closed) {
826
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
827
+ }
828
+ const access = await safeGet(keys.access);
829
+ if (generation !== expectedGeneration || closed || access === null) {
830
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
831
+ }
832
+ return snapshot(access, expectedGeneration);
833
+ }
834
+ catch {
835
+ if (generation === expectedGeneration && !closed) {
836
+ await deleteTokens(expectedGeneration);
837
+ if (generation !== expectedGeneration || closed) {
838
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
839
+ }
840
+ expiresAt = 0;
841
+ grantId = undefined;
842
+ liveScopes = Object.freeze([...profile.scopes]);
843
+ return startAuthorization();
844
+ }
845
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
846
+ }
847
+ };
848
+ const provider = {
849
+ bindingResolver,
850
+ async resolve() {
851
+ if (closed)
852
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential provider is closed");
853
+ if (pending !== undefined || authorizationPromise !== undefined)
854
+ return startAuthorization();
855
+ const expectedGeneration = generation;
856
+ const keys = tokenKeys(expectedGeneration);
857
+ const access = await safeGet(keys.access);
858
+ if (generation !== expectedGeneration || closed) {
859
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
860
+ }
861
+ if (access !== null && grantId !== undefined && expiresAt > now()) {
862
+ return snapshot(access, expectedGeneration);
863
+ }
864
+ const refreshToken = await safeGet(keys.refresh);
865
+ if (generation !== expectedGeneration || closed) {
866
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
867
+ }
868
+ if (refreshToken !== null && grantId !== undefined) {
869
+ refreshPromise ??= refresh(refreshToken).finally(() => {
870
+ refreshPromise = undefined;
871
+ });
872
+ return refreshPromise;
873
+ }
874
+ return startAuthorization();
875
+ },
876
+ async forget() {
877
+ const obsoleteGeneration = generation;
878
+ generation += 1;
879
+ grantId = undefined;
880
+ expiresAt = 0;
881
+ liveScopes = Object.freeze([...profile.scopes]);
882
+ if (pending !== undefined) {
883
+ clearTimeout(pending.timer);
884
+ pending.terminal = true;
885
+ closeServer(pending.server);
886
+ pending = undefined;
887
+ }
888
+ await deleteTokens(obsoleteGeneration);
889
+ },
890
+ async close() {
891
+ const obsoleteGeneration = generation;
892
+ generation += 1;
893
+ closed = true;
894
+ grantId = undefined;
895
+ expiresAt = 0;
896
+ if (pending !== undefined) {
897
+ clearTimeout(pending.timer);
898
+ pending.terminal = true;
899
+ closeServer(pending.server);
900
+ pending = undefined;
901
+ }
902
+ await deleteTokens(obsoleteGeneration);
903
+ },
904
+ };
905
+ return Object.freeze(provider);
906
+ }
907
+ export async function createCredentialProvider(rawProfile, options) {
908
+ if (!options || !Array.isArray(options.manifestOrigins))
909
+ throw invalidProfile();
910
+ const profile = validateProfile(rawProfile, options.manifestOrigins);
911
+ const profileDigest = await digestCredentialProfile(profile.value);
912
+ const slotsDigest = await digestSlots(profile.slots);
913
+ const bindingResolver = createBindingResolver(profile, profileDigest);
914
+ const randomBytes = options.randomBytes ?? defaultRandom;
915
+ if (profile.auth.type === "oauth2-pkce") {
916
+ return createOAuthProvider(profile, profileDigest, slotsDigest, bindingResolver, options, randomBytes);
917
+ }
918
+ const environmentAuth = profile.auth;
919
+ const environment = options.environment ?? process.env;
920
+ let previousSecret;
921
+ let grantId;
922
+ let generation = 0;
923
+ let closed = false;
924
+ return Object.freeze({
925
+ bindingResolver,
926
+ async resolve() {
927
+ if (closed)
928
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential provider is closed");
929
+ const expectedGeneration = generation;
930
+ let secret;
931
+ try {
932
+ secret = environment[environmentAuth.env];
933
+ }
934
+ catch {
935
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential is not available");
936
+ }
937
+ if (typeof secret !== "string" || secret.length === 0) {
938
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Credential is not available");
939
+ }
940
+ if (secret !== previousSecret || grantId === undefined) {
941
+ previousSecret = secret;
942
+ grantId = randomIdentifier(randomBytes);
943
+ }
944
+ const binding = await createBinding(profile, profileDigest, slotsDigest, grantId, profile.scopes);
945
+ if (generation !== expectedGeneration || closed) {
946
+ throw new OpenApiMcpError("AUTH_REQUIRED", "Authorization was cancelled");
947
+ }
948
+ const credential = environmentAuth.type === "bearer-env"
949
+ ? { type: "bearer", token: secret }
950
+ : {
951
+ type: "api-key",
952
+ placement: environmentAuth.placement,
953
+ name: environmentAuth.name,
954
+ value: secret,
955
+ };
956
+ return {
957
+ status: "ready",
958
+ snapshot: freezeSnapshot(credential, binding),
959
+ };
960
+ },
961
+ async forget() {
962
+ generation += 1;
963
+ previousSecret = undefined;
964
+ grantId = undefined;
965
+ },
966
+ async close() {
967
+ generation += 1;
968
+ closed = true;
969
+ previousSecret = undefined;
970
+ grantId = undefined;
971
+ },
972
+ });
973
+ }