@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,24 @@
1
+ import type { ActionDispatchPermit } from "../runtime/action-permit.ts";
2
+ import type { ActionAuthorizer, AuthorizedTransport, CredentialAuthorizationBinding, LocalAuthProfile, PaginationTokenCodec, PreparedCall } from "../runtime/types.ts";
3
+ import { type RuntimeLimits } from "../runtime/versions.ts";
4
+ import { type DestinationGuardOptions } from "./destination-guard.ts";
5
+ export interface LocalDispatchOptions {
6
+ readonly profile: LocalAuthProfile;
7
+ /** Host authority must consult verified manifests/current catalog policy. */
8
+ readonly allowsManifestOrigin: (context: Readonly<Pick<PreparedCall, "catalogId" | "releaseId" | "manifestDigest" | "origin">>) => boolean | Promise<boolean>;
9
+ readonly lookup?: DestinationGuardOptions["lookup"];
10
+ readonly now?: () => number;
11
+ readonly planTtlMs?: number;
12
+ readonly tokenTtlMs?: number;
13
+ readonly tokenCapacity?: number;
14
+ readonly limits?: Partial<RuntimeLimits>;
15
+ }
16
+ /** Only this factory pairs permit issuance with the private HTTP implementation. */
17
+ export declare function createLocalDispatchBoundary(authorizer: ActionAuthorizer, options: LocalDispatchOptions): Readonly<{
18
+ broker: {
19
+ consume(decision: import("../runtime/types.ts").AuthorizedActionDecision, call: PreparedCall, binding: CredentialAuthorizationBinding): Promise<ActionDispatchPermit>;
20
+ };
21
+ transport: AuthorizedTransport;
22
+ paginationTokenCodec: PaginationTokenCodec;
23
+ close: () => Promise<void>;
24
+ }>;
@@ -0,0 +1,626 @@
1
+ import { createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import tls from "node:tls";
4
+ import { Client, Dispatcher, fetch } from "undici";
5
+ import { snapshotCredentialAuthorizationBinding } from "../runtime/credential-binding.js";
6
+ import { sha256 } from "../runtime/digest.js";
7
+ import { OpenApiMcpError } from "../runtime/errors.js";
8
+ import { snapshotPaginationTokenState, verifyAndSnapshotPreparedCall, } from "../runtime/prepared-call.js";
9
+ import { canonicalJsonBounded, parseJsonStrict, } from "../runtime/strict-json.js";
10
+ import { resolveRuntimeLimits, } from "../runtime/versions.js";
11
+ import { createActionAuthorizationBoundary } from "../stdio/action-broker.js";
12
+ import { digestCredentialProfile } from "./auth.js";
13
+ import { NodeDestinationGuard, } from "./destination-guard.js";
14
+ function denied() {
15
+ return new OpenApiMcpError("ACTION_DENIED", "Dispatch capability is invalid or expired");
16
+ }
17
+ function invalidCredential() {
18
+ return new OpenApiMcpError("AUTH_PROFILE_INVALID", "Credential snapshot does not match its profile and call");
19
+ }
20
+ function equal(left, right) {
21
+ const a = Buffer.from(left);
22
+ const b = Buffer.from(right);
23
+ return a.byteLength === b.byteLength && timingSafeEqual(a, b);
24
+ }
25
+ function canonical(value) {
26
+ return canonicalJsonBounded(value, {
27
+ maxBytes: 1024 * 1024,
28
+ maxDepth: 64,
29
+ maxNodes: 100_000,
30
+ });
31
+ }
32
+ function detach(value) {
33
+ return parseJsonStrict(canonical(value), {
34
+ maxBytes: 1024 * 1024,
35
+ maxDepth: 64,
36
+ maxKeys: 100_000,
37
+ });
38
+ }
39
+ function fingerprint(call) {
40
+ const { body, normalizedArguments, ...metadata } = call;
41
+ // Keep independently bounded payloads out of the envelope's JSON traversal:
42
+ // raw bytes must not become JSON nodes, nor may metadata consume the admitted
43
+ // arguments' node/depth budget. Named fields and null preserve framing.
44
+ const bodyCommitment = body === null
45
+ ? null
46
+ : {
47
+ byteLength: body.byteLength,
48
+ sha256: createHash("sha256").update(body).digest("hex"),
49
+ };
50
+ const argumentCommitment = createHash("sha256")
51
+ .update(canonical(normalizedArguments))
52
+ .digest("hex");
53
+ return createHash("sha256")
54
+ .update("knitli.openapi-mcp.dispatch-fingerprint.v1\0")
55
+ .update(canonical({
56
+ ...metadata,
57
+ body: bodyCommitment,
58
+ normalizedArguments: argumentCommitment,
59
+ }))
60
+ .digest("hex");
61
+ }
62
+ // Fetch does not forward an `idempotent` RequestInit field. This private
63
+ // dispatcher imposes it at the actual Client boundary on every attempt.
64
+ class SingleAttemptDispatcher extends Dispatcher {
65
+ #client;
66
+ constructor(client) {
67
+ super();
68
+ this.#client = client;
69
+ }
70
+ dispatch(options, handler) {
71
+ return this.#client.dispatch({ ...options, idempotent: false }, handler);
72
+ }
73
+ }
74
+ function upstreamError() {
75
+ return new OpenApiMcpError("UPSTREAM_ERROR", "Upstream request failed");
76
+ }
77
+ async function boundedBody(response, maximum, limitCode) {
78
+ const length = response.headers.get("content-length");
79
+ if (length !== null &&
80
+ (!/^\d+$/.test(length) ||
81
+ !Number.isSafeInteger(Number(length)) ||
82
+ Number(length) > maximum))
83
+ throw new OpenApiMcpError(limitCode);
84
+ const encodings = (response.headers.get("content-encoding") ?? "identity")
85
+ .split(",")
86
+ .map((value) => value.trim().toLowerCase());
87
+ if (encodings.some((value) => !["identity", "gzip", "x-gzip", "deflate", "br", "zstd"].includes(value)) ||
88
+ (encodings.length > 1 && encodings.includes("identity")))
89
+ throw upstreamError();
90
+ const reader = response.body?.getReader();
91
+ if (!reader)
92
+ return new Uint8Array();
93
+ let size = 0;
94
+ const chunks = [];
95
+ try {
96
+ for (;;) {
97
+ const { done, value } = await reader.read();
98
+ if (done)
99
+ break;
100
+ size += value.byteLength;
101
+ if (size > maximum)
102
+ throw new OpenApiMcpError(limitCode);
103
+ chunks.push(value);
104
+ }
105
+ }
106
+ finally {
107
+ await reader.cancel().catch(() => { });
108
+ reader.releaseLock();
109
+ }
110
+ const result = new Uint8Array(size);
111
+ let offset = 0;
112
+ for (const chunk of chunks) {
113
+ result.set(chunk, offset);
114
+ offset += chunk.byteLength;
115
+ }
116
+ return result;
117
+ }
118
+ function nextLink(header) {
119
+ if (header === null)
120
+ return null;
121
+ if (header.length > 8192)
122
+ throw new OpenApiMcpError("INPUT_INVALID", "Continuation header is invalid");
123
+ let next = null;
124
+ for (const part of header.split(/,(?=\s*<)/)) {
125
+ const match = /^\s*<([^<>]*)>\s*(.*)$/.exec(part);
126
+ if (!match)
127
+ throw new OpenApiMcpError("INPUT_INVALID", "Continuation header is invalid");
128
+ const rel = /(?:^|;)\s*rel\s*=\s*(?:"([^"]*)"|([^;\s,]+))/i.exec(match[2] ?? "");
129
+ if (!(rel?.[1] ?? rel?.[2] ?? "").toLowerCase().split(/\s+/).includes("next"))
130
+ continue;
131
+ if (next !== null)
132
+ throw new OpenApiMcpError("INPUT_INVALID", "Continuation header is ambiguous");
133
+ next = match[1] ?? "";
134
+ }
135
+ return next;
136
+ }
137
+ /** Only this factory pairs permit issuance with the private HTTP implementation. */
138
+ export function createLocalDispatchBoundary(authorizer, options) {
139
+ // Detach host configuration synchronously before digest or DNS awaits.
140
+ const profile = detach(options.profile);
141
+ const allowsManifestOrigin = options.allowsManifestOrigin;
142
+ if (typeof allowsManifestOrigin !== "function")
143
+ throw new OpenApiMcpError("DESTINATION_DENIED", "Verified manifest origin policy is required");
144
+ const limits = resolveRuntimeLimits(options.limits);
145
+ const now = options.now ?? Date.now;
146
+ const ttl = options.planTtlMs ?? 30_000;
147
+ const tokenTtl = options.tokenTtlMs ?? 120_000;
148
+ const capacity = options.tokenCapacity ?? 1024;
149
+ if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > 30_000)
150
+ throw new RangeError("Plan TTL must only lower 30000ms");
151
+ if (!Number.isSafeInteger(tokenTtl) ||
152
+ tokenTtl < 1 ||
153
+ tokenTtl > 120_000 ||
154
+ !Number.isSafeInteger(capacity) ||
155
+ capacity < 1 ||
156
+ capacity > 1024)
157
+ throw new RangeError("Token bounds must only lower their defaults");
158
+ const guard = new NodeDestinationGuard({
159
+ allowedOrigins: profile.allowedOrigins,
160
+ lookup: options.lookup,
161
+ now,
162
+ ttlMs: Math.min(ttl, limits.requestDeadlineMs),
163
+ });
164
+ const boundary = createActionAuthorizationBoundary(authorizer, { now });
165
+ let plans = new WeakMap();
166
+ let closed = false;
167
+ const shutdown = new AbortController();
168
+ const activeClients = new Set();
169
+ const connectingSockets = new Set();
170
+ const tokenKey = randomBytes(32);
171
+ const tokens = new Map();
172
+ const tokenMac = (id, tokenState) => createHmac("sha256", tokenKey)
173
+ .update(`knitli.openapi-mcp.page.v1\0${id}\0${canonical(tokenState)}`)
174
+ .digest("base64url");
175
+ const paginationTokenCodec = Object.freeze({
176
+ async encode(input) {
177
+ if (closed)
178
+ throw denied();
179
+ const time = now();
180
+ const tokenState = snapshotPaginationTokenState(detach(input), limits, time);
181
+ if (Date.parse(tokenState.expiresAt) > time + tokenTtl)
182
+ throw new OpenApiMcpError("INPUT_INVALID");
183
+ for (const [key, value] of tokens)
184
+ if (Date.parse(value.state.expiresAt) <= time)
185
+ tokens.delete(key);
186
+ while (tokens.size >= capacity) {
187
+ const oldest = tokens.keys().next().value;
188
+ if (oldest === undefined)
189
+ throw new OpenApiMcpError("INPUT_INVALID");
190
+ tokens.delete(oldest);
191
+ }
192
+ const id = randomBytes(24).toString("base64url");
193
+ const mac = tokenMac(id, tokenState);
194
+ tokens.set(id, { state: tokenState, mac });
195
+ return `page.v1.${id}.${mac}`;
196
+ },
197
+ async decode(token) {
198
+ if (closed)
199
+ throw denied();
200
+ if (typeof token !== "string" ||
201
+ !/^page\.v1\.[A-Za-z0-9_-]{32}\.[A-Za-z0-9_-]{43}$/.test(token))
202
+ throw new OpenApiMcpError("INPUT_INVALID");
203
+ const [, , id = "", mac = ""] = token.split(".");
204
+ const entry = tokens.get(id);
205
+ if (!entry ||
206
+ !equal(entry.mac, mac) ||
207
+ !equal(tokenMac(id, entry.state), mac))
208
+ throw new OpenApiMcpError("INPUT_INVALID");
209
+ try {
210
+ return snapshotPaginationTokenState(entry.state, limits, now());
211
+ }
212
+ catch (error) {
213
+ tokens.delete(id);
214
+ throw error;
215
+ }
216
+ },
217
+ });
218
+ function state(plan) {
219
+ if (closed)
220
+ throw denied();
221
+ const value = typeof plan === "object" && plan !== null ? plans.get(plan) : undefined;
222
+ const time = now();
223
+ if (!value || !Number.isFinite(time) || time >= value.expiresAt)
224
+ throw denied();
225
+ return value;
226
+ }
227
+ async function authorizeDestination(call, url, signal) {
228
+ if (closed)
229
+ throw denied();
230
+ const context = Object.freeze({
231
+ catalogId: call.catalogId,
232
+ releaseId: call.releaseId,
233
+ manifestDigest: call.manifestDigest,
234
+ origin: url.origin,
235
+ });
236
+ try {
237
+ if ((await withDeadline(Promise.resolve(allowsManifestOrigin(context)), signal)) !== true)
238
+ throw new Error();
239
+ }
240
+ catch {
241
+ throw new OpenApiMcpError("DESTINATION_DENIED", "Verified manifest origin policy denied the destination");
242
+ }
243
+ if (closed)
244
+ throw denied();
245
+ return guard.authorize(url, signal);
246
+ }
247
+ const transport = Object.freeze({
248
+ async prepareDispatch(untrustedCall, untrustedSnapshot) {
249
+ if (closed)
250
+ throw denied();
251
+ const signal = AbortSignal.any([
252
+ shutdown.signal,
253
+ AbortSignal.timeout(limits.requestDeadlineMs),
254
+ ]);
255
+ let snapshot;
256
+ try {
257
+ snapshot = detach(untrustedSnapshot);
258
+ }
259
+ catch {
260
+ throw invalidCredential();
261
+ }
262
+ const call = await verifyAndSnapshotPreparedCall(untrustedCall);
263
+ if (["GET", "HEAD"].includes(call.method) && call.body !== null)
264
+ throw new OpenApiMcpError("INPUT_INVALID", "The local HTTP adapter does not support GET or HEAD request bodies");
265
+ const binding = await snapshotCredentialAuthorizationBinding(snapshot.binding);
266
+ const profileDigest = await digestCredentialProfile(profile);
267
+ const credential = snapshot.credential;
268
+ const auth = profile.auth;
269
+ let slot;
270
+ if (auth.type === "api-key-env") {
271
+ if (credential.type !== "api-key" ||
272
+ credential.placement !== auth.placement ||
273
+ credential.name !== auth.name)
274
+ throw invalidCredential();
275
+ slot = {
276
+ placement: credential.placement,
277
+ name: credential.placement === "header"
278
+ ? credential.name.toLowerCase()
279
+ : credential.name,
280
+ };
281
+ }
282
+ else {
283
+ if (credential.type !== "bearer")
284
+ throw invalidCredential();
285
+ slot = { placement: "header", name: "authorization" };
286
+ }
287
+ const secret = credential.type === "bearer" ? credential.token : credential.value;
288
+ const expectedKeys = credential.type === "bearer"
289
+ ? ["token", "type"]
290
+ : ["name", "placement", "type", "value"];
291
+ if (Object.keys(snapshot).sort().join() !== "binding,credential" ||
292
+ Object.keys(credential).sort().join() !== expectedKeys.join() ||
293
+ typeof secret !== "string" ||
294
+ secret.length < 1 ||
295
+ secret.length > 16_384 ||
296
+ Array.from(secret).some((character) => character.charCodeAt(0) <= 32 || character.charCodeAt(0) === 127))
297
+ throw invalidCredential();
298
+ const slotsDigest = await sha256("knitli.openapi-mcp.credential-slots.v1", [slot]);
299
+ const configuredScopes = [
300
+ ...(auth.type === "oauth2-pkce" ? auth.scopes : (profile.scopes ?? [])),
301
+ ].sort();
302
+ const audience = profile.audience ??
303
+ (auth.type === "oauth2-pkce" ? auth.resource : undefined);
304
+ if (!equal(profile.profileId, binding.profileId) ||
305
+ !equal(profileDigest, binding.profileDigest) ||
306
+ !equal(call.credentialProfileId, binding.profileId) ||
307
+ !equal(call.credentialProfileDigest, binding.profileDigest) ||
308
+ !equal(call.reservedSlotsDigest, slotsDigest) ||
309
+ !equal(binding.slotsDigest, slotsDigest) ||
310
+ audience !== binding.audience ||
311
+ binding.scopes.some((scope) => !configuredScopes.includes(scope)) ||
312
+ (auth.type !== "oauth2-pkce" &&
313
+ configuredScopes.join("\0") !== binding.scopes.join("\0")))
314
+ throw invalidCredential();
315
+ const url = new URL(call.relativeUrl, call.origin);
316
+ const pagination = call.pageToken === null
317
+ ? null
318
+ : await paginationTokenCodec.decode(call.pageToken);
319
+ if (pagination &&
320
+ (pagination.catalogId !== call.catalogId ||
321
+ pagination.releaseId !== call.releaseId ||
322
+ pagination.operationId !== call.operationId ||
323
+ pagination.origin !== call.origin ||
324
+ pagination.nextRelativeUrl !== call.relativeUrl ||
325
+ !equal(pagination.manifestDigest, call.manifestDigest) ||
326
+ !equal(pagination.inputDigest, call.inputDigest)))
327
+ throw new OpenApiMcpError("INPUT_INVALID");
328
+ if ((slot.placement === "query" &&
329
+ [...url.searchParams.keys()].some((key) => key.toLowerCase() === slot.name.toLowerCase())) ||
330
+ (slot.placement === "header" &&
331
+ Object.keys(call.headers).some((key) => key.toLowerCase() === slot.name.toLowerCase())))
332
+ throw invalidCredential();
333
+ const destination = await authorizeDestination(call, url, signal);
334
+ if (closed)
335
+ throw denied();
336
+ const plan = Object.freeze(Object.create(null));
337
+ plans.set(plan, {
338
+ call,
339
+ fingerprint: fingerprint(call),
340
+ snapshot: { credential, binding },
341
+ destination,
342
+ pagination,
343
+ expiresAt: Math.min(destination.expiresAt, now() + ttl, pagination ? Date.parse(pagination.expiresAt) : Infinity),
344
+ });
345
+ return plan;
346
+ },
347
+ verifyPlan(plan, call, binding) {
348
+ try {
349
+ const value = state(plan);
350
+ if (!equal(value.fingerprint, fingerprint(call)) ||
351
+ !equal(canonical(value.snapshot.binding), canonical(binding)))
352
+ throw denied();
353
+ }
354
+ catch {
355
+ throw denied();
356
+ }
357
+ },
358
+ async dispatchRead(plan) {
359
+ const value = state(plan);
360
+ if (value.call.safety !== "read" ||
361
+ !["GET", "HEAD", "OPTIONS"].includes(value.call.method))
362
+ throw denied();
363
+ plans.delete(plan);
364
+ return dispatch(value);
365
+ },
366
+ async dispatchAction(plan, permit) {
367
+ const value = state(plan);
368
+ if (value.call.safety !== "action")
369
+ throw denied();
370
+ boundary.permits.consume(permit, value.call.preparedCallDigest, value.snapshot.binding.bindingDigest);
371
+ plans.delete(plan);
372
+ return dispatch(value);
373
+ },
374
+ });
375
+ async function dispatch(value) {
376
+ const { call, snapshot } = value;
377
+ const deadline = Date.now() + limits.requestDeadlineMs;
378
+ const signal = AbortSignal.any([
379
+ shutdown.signal,
380
+ AbortSignal.timeout(limits.requestDeadlineMs),
381
+ ]);
382
+ let destination = value.destination;
383
+ let url = new URL(call.relativeUrl, call.origin);
384
+ let retries = 0;
385
+ let redirects = 0;
386
+ let actionConnectionBegan = false;
387
+ const readEligible = call.safety === "read" &&
388
+ ["GET", "HEAD", "OPTIONS"].includes(call.method);
389
+ for (;;) {
390
+ let connectionBegan = false;
391
+ let positivelyPreConnect = false;
392
+ let client;
393
+ let response;
394
+ let retryWait;
395
+ try {
396
+ signal.throwIfAborted();
397
+ if (now() >= destination.expiresAt)
398
+ throw new OpenApiMcpError("DESTINATION_DENIED");
399
+ const remaining = deadline - Date.now();
400
+ if (remaining <= 0)
401
+ throw upstreamError();
402
+ client = new Client(url.origin, {
403
+ allowH2: false,
404
+ pipelining: 0,
405
+ connect(options, callback) {
406
+ connectionBegan = true;
407
+ if (call.safety === "action")
408
+ actionConnectionBegan = true;
409
+ // Observe TCP establishment directly: a TLS failure happens after
410
+ // TCP connect and must never be reported as safe action failure.
411
+ let connected = false;
412
+ const connectOptions = {
413
+ host: options.hostname,
414
+ servername: options.servername || options.hostname,
415
+ port: Number(options.port) || 443,
416
+ lookup: destination.lookup,
417
+ autoSelectFamily: false,
418
+ rejectUnauthorized: true,
419
+ ALPNProtocols: ["http/1.1"],
420
+ };
421
+ const socket = tls.connect(connectOptions);
422
+ connectingSockets.add(socket);
423
+ socket.once("close", () => connectingSockets.delete(socket));
424
+ const timeout = setTimeout(() => socket.destroy(upstreamError()), remaining);
425
+ socket.once("connect", () => {
426
+ connected = true;
427
+ });
428
+ const failed = (error) => {
429
+ clearTimeout(timeout);
430
+ if (!connected &&
431
+ ["ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH"].includes(error.code ?? "")) {
432
+ positivelyPreConnect = true;
433
+ actionConnectionBegan = false;
434
+ }
435
+ callback(error, null);
436
+ };
437
+ socket.once("error", failed);
438
+ socket.once("secureConnect", () => {
439
+ clearTimeout(timeout);
440
+ connectingSockets.delete(socket);
441
+ socket.removeListener("error", failed);
442
+ callback(null, socket);
443
+ });
444
+ },
445
+ });
446
+ activeClients.add(client);
447
+ const requestUrl = new URL(url);
448
+ const headers = { ...call.headers };
449
+ const credential = snapshot.credential;
450
+ if (credential.type === "bearer")
451
+ headers.authorization = `Bearer ${credential.token}`;
452
+ else if (credential.placement === "header")
453
+ headers[credential.name] = credential.value;
454
+ else
455
+ requestUrl.searchParams.set(credential.name, credential.value);
456
+ response = await fetch(requestUrl, {
457
+ method: call.method,
458
+ headers,
459
+ body: call.body,
460
+ redirect: "manual",
461
+ dispatcher: new SingleAttemptDispatcher(client),
462
+ signal,
463
+ });
464
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
465
+ const location = response.headers.get("location");
466
+ if (call.safety === "action" ||
467
+ !location ||
468
+ location.length > 8192 ||
469
+ redirects >= limits.maxRedirects)
470
+ return { kind: "redirect-blocked", location: null };
471
+ let next;
472
+ try {
473
+ next = new URL(location, url);
474
+ }
475
+ catch {
476
+ return { kind: "redirect-blocked", location: null };
477
+ }
478
+ if (next.origin !== call.origin ||
479
+ next.protocol !== "https:" ||
480
+ next.username ||
481
+ next.password ||
482
+ next.hash)
483
+ return { kind: "redirect-blocked", location: null };
484
+ if (credential.type === "api-key" &&
485
+ credential.placement === "query" &&
486
+ [...next.searchParams.keys()].some((key) => key.toLowerCase() === credential.name.toLowerCase()))
487
+ return { kind: "redirect-blocked", location: null };
488
+ redirects++;
489
+ destination = await authorizeDestination(call, next, signal);
490
+ url = next;
491
+ continue;
492
+ }
493
+ if (readEligible &&
494
+ retries < 2 &&
495
+ [429, 502, 503, 504].includes(response.status)) {
496
+ const after = response.headers.get("retry-after");
497
+ const wait = after === null
498
+ ? 0
499
+ : /^\d+$/.test(after)
500
+ ? Number(after) * 1000
501
+ : Date.parse(after) - Date.now();
502
+ if (Number.isFinite(wait) &&
503
+ Math.max(0, wait) < deadline - Date.now())
504
+ retryWait = Math.max(0, wait);
505
+ }
506
+ if (retryWait === undefined) {
507
+ if (response.status === 304)
508
+ return { kind: "not-modified" };
509
+ const paginationRemaining = limits.maxPaginationBytes -
510
+ (value.pagination?.cumulativeBytes ?? 0);
511
+ const maximum = Math.min(limits.maxResponseBytes, paginationRemaining);
512
+ const body = call.method === "HEAD"
513
+ ? new Uint8Array()
514
+ : await boundedBody(response, maximum, paginationRemaining < limits.maxResponseBytes
515
+ ? "PAGINATION_LIMIT_EXCEEDED"
516
+ : "RESPONSE_LIMIT_EXCEEDED");
517
+ let pageToken;
518
+ const link = call.safety === "read"
519
+ ? nextLink(response.headers.get("link"))
520
+ : null;
521
+ if (link !== null) {
522
+ const next = new URL(link, url);
523
+ if (next.origin !== call.origin ||
524
+ next.username ||
525
+ next.password ||
526
+ next.hash)
527
+ throw new OpenApiMcpError("DESTINATION_DENIED");
528
+ if (credential.type === "api-key" &&
529
+ credential.placement === "query" &&
530
+ [...next.searchParams.keys()].some((key) => key.toLowerCase() === credential.name.toLowerCase()))
531
+ throw new OpenApiMcpError("INPUT_INVALID");
532
+ await authorizeDestination(call, next, signal);
533
+ pageToken = await paginationTokenCodec.encode({
534
+ catalogId: call.catalogId,
535
+ releaseId: call.releaseId,
536
+ manifestDigest: call.manifestDigest,
537
+ operationId: call.operationId,
538
+ inputDigest: call.inputDigest,
539
+ origin: call.origin,
540
+ nextRelativeUrl: `${next.pathname}${next.search}`,
541
+ expiresAt: new Date(now() + tokenTtl).toISOString(),
542
+ pageCount: (value.pagination?.pageCount ?? 0) + 1,
543
+ cumulativeBytes: (value.pagination?.cumulativeBytes ?? 0) + body.byteLength,
544
+ });
545
+ }
546
+ // A small allowlist excludes cookies, Location, and Link, which can
547
+ // contain authenticated URLs. Pagination is carried only by tokens.
548
+ const safeHeaders = {};
549
+ for (const name of ["content-type", "etag", "last-modified"]) {
550
+ const header = response.headers.get(name);
551
+ if (header !== null && header.length <= 4096)
552
+ safeHeaders[name] = header;
553
+ }
554
+ return {
555
+ kind: "success",
556
+ statusCode: response.status,
557
+ headers: safeHeaders,
558
+ body,
559
+ ...(pageToken === undefined ? {} : { pageToken }),
560
+ };
561
+ }
562
+ }
563
+ catch (error) {
564
+ if (call.safety === "action" && actionConnectionBegan)
565
+ throw new OpenApiMcpError("UPSTREAM_OUTCOME_UNKNOWN", "Upstream action outcome is unknown", { details: { preparedCallDigest: call.preparedCallDigest } });
566
+ if (readEligible &&
567
+ (!connectionBegan || positivelyPreConnect) &&
568
+ retries < 2 &&
569
+ !signal.aborted &&
570
+ !(error instanceof OpenApiMcpError))
571
+ retryWait = 0;
572
+ else if (error instanceof OpenApiMcpError)
573
+ throw error;
574
+ else
575
+ throw upstreamError();
576
+ }
577
+ finally {
578
+ await response?.body?.cancel().catch(() => { });
579
+ await client?.destroy().catch(() => { });
580
+ if (client)
581
+ activeClients.delete(client);
582
+ }
583
+ if (retryWait !== undefined) {
584
+ retries++;
585
+ try {
586
+ await delay(retryWait, undefined, { signal });
587
+ }
588
+ catch {
589
+ throw upstreamError();
590
+ }
591
+ }
592
+ }
593
+ }
594
+ async function close() {
595
+ closed = true;
596
+ plans = new WeakMap();
597
+ tokens.clear();
598
+ tokenKey.fill(0);
599
+ shutdown.abort();
600
+ for (const socket of connectingSockets)
601
+ socket.destroy(upstreamError());
602
+ await Promise.allSettled([...activeClients].map((client) => client.destroy()));
603
+ }
604
+ return Object.freeze({
605
+ broker: boundary.broker,
606
+ transport,
607
+ paginationTokenCodec,
608
+ close,
609
+ });
610
+ }
611
+ async function withDeadline(operation, signal) {
612
+ signal.throwIfAborted();
613
+ let abort = () => { };
614
+ try {
615
+ return await Promise.race([
616
+ operation,
617
+ new Promise((_, reject) => {
618
+ abort = () => reject(upstreamError());
619
+ signal.addEventListener("abort", abort, { once: true });
620
+ }),
621
+ ]);
622
+ }
623
+ finally {
624
+ signal.removeEventListener("abort", abort);
625
+ }
626
+ }
@@ -0,0 +1,5 @@
1
+ export { type CredentialProviderOptions, createCredentialProvider, digestCredentialProfile, type LocalCredentialProvider, MemorySecretStore, } from "./auth.ts";
2
+ export { type LegacyV3CatalogIdentity, SqliteCatalogStore, type SqliteCatalogStoreOptions, } from "./catalog-store.ts";
3
+ export { type DestinationGuardOptions, NodeDestinationGuard, } from "./destination-guard.ts";
4
+ export { FileGenerationStore, GenerationStoreContentionError, } from "./generation-store.ts";
5
+ export { createLocalDispatchBoundary, type LocalDispatchOptions, } from "./guarded-fetch.ts";
@@ -0,0 +1,5 @@
1
+ export { createCredentialProvider, digestCredentialProfile, MemorySecretStore, } from "./auth.js";
2
+ export { SqliteCatalogStore, } from "./catalog-store.js";
3
+ export { NodeDestinationGuard, } from "./destination-guard.js";
4
+ export { FileGenerationStore, GenerationStoreContentionError, } from "./generation-store.js";
5
+ export { createLocalDispatchBoundary, } from "./guarded-fetch.js";
@@ -0,0 +1,2 @@
1
+ /** Fixed OAuth audience metadata; this URI is never a credential destination. */
2
+ export declare function isOAuthResource(value: unknown): value is string;
@@ -0,0 +1,15 @@
1
+ /** Fixed OAuth audience metadata; this URI is never a credential destination. */
2
+ export function isOAuthResource(value) {
3
+ if (typeof value !== "string" ||
4
+ value.length === 0 ||
5
+ value.length > 2048 ||
6
+ /[\s#]/u.test(value) ||
7
+ [...value].some((character) => character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f))
8
+ return false;
9
+ try {
10
+ return new URL(value).protocol.length > 1;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }