@onyxsecurity/mcp-gateway 2.1.21 → 2.1.22

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.
@@ -1 +1 @@
1
- {"version":3,"file":"main-B_ZamBxF.js","names":["config","config","credentials: StoredCredentials","bearer","refreshInFlight: null | Promise<string | undefined>","msg: { id?: unknown }","reconnecting: null | Promise<boolean>","notificationHandlers: SetNotificationHandlerArgs[]","jsonLines: string[]","spawnedAt: number | undefined","spawnedPid: number | undefined","messageType: string","result: string","parsedResult: unknown","repoDigests: unknown[]","digests: string[]","activeStdioTransport: StdioClientTransport | undefined","yargs","proxyType: ProxyType","finalCommand: string | undefined","finalArgs: string[] | undefined","remoteUrl: string | undefined","usedTransport: TransportType | undefined","remoteHeaders: Record<string, string>","customEnv: Record<string, string>","missingVar: string | undefined","val","reason","containerMCPCommand: ContainerMCPCommand | undefined","containerImageDigests: string[] | undefined","sessionDataOnce: ReturnType<typeof getSessionData> | undefined","accessControlClient: AccessControlClient | undefined","recentStderr: string[]","headers: Record<string, string>","connectedClient: Client","dynAuth: DynamicAuthHandle","discoveryError: string | undefined","trafficMirrorInit: Promise<unknown> | undefined","upstream: undefined | UpstreamClient","connected","serverVersion","serverCapabilities","server","upstream"],"sources":["../node_modules/eventsource/dist/index.js","../src/accessControl/authorizer.ts","../src/accessControl/client.ts","../src/auth/OAuthCredentialStore.ts","../src/auth/dynamicAuth.ts","../src/lib/initializeErrorResponder.ts","../src/lib/upstreamErrors.ts","../src/lib/connectWithRetry.ts","../src/lib/reconnectingClient.ts","../src/JSONFilterTransform.ts","../src/StdioClientTransport.ts","../src/utils/imageReference.ts","../src/utils/dockerImageDigest.ts","../src/bin/main.ts"],"sourcesContent":["import { createParser } from \"eventsource-parser\";\nclass ErrorEvent extends Event {\n /**\n * Constructs a new `ErrorEvent` instance. This is typically not called directly,\n * but rather emitted by the `EventSource` object when an error occurs.\n *\n * @param type - The type of the event (should be \"error\")\n * @param errorEventInitDict - Optional properties to include in the error event\n */\n constructor(type, errorEventInitDict) {\n var _a, _b;\n super(type), this.code = (_a = errorEventInitDict == null ? void 0 : errorEventInitDict.code) != null ? _a : void 0, this.message = (_b = errorEventInitDict == null ? void 0 : errorEventInitDict.message) != null ? _b : void 0;\n }\n /**\n * Node.js \"hides\" the `message` and `code` properties of the `ErrorEvent` instance,\n * when it is `console.log`'ed. This makes it harder to debug errors. To ease debugging,\n * we explicitly include the properties in the `inspect` method.\n *\n * This is automatically called by Node.js when you `console.log` an instance of this class.\n *\n * @param _depth - The current depth\n * @param options - The options passed to `util.inspect`\n * @param inspect - The inspect function to use (prevents having to import it from `util`)\n * @returns A string representation of the error\n */\n [Symbol.for(\"nodejs.util.inspect.custom\")](_depth, options, inspect) {\n return inspect(inspectableError(this), options);\n }\n /**\n * Deno \"hides\" the `message` and `code` properties of the `ErrorEvent` instance,\n * when it is `console.log`'ed. This makes it harder to debug errors. To ease debugging,\n * we explicitly include the properties in the `inspect` method.\n *\n * This is automatically called by Deno when you `console.log` an instance of this class.\n *\n * @param inspect - The inspect function to use (prevents having to import it from `util`)\n * @param options - The options passed to `Deno.inspect`\n * @returns A string representation of the error\n */\n [Symbol.for(\"Deno.customInspect\")](inspect, options) {\n return inspect(inspectableError(this), options);\n }\n}\nfunction syntaxError(message) {\n const DomException = globalThis.DOMException;\n return typeof DomException == \"function\" ? new DomException(message, \"SyntaxError\") : new SyntaxError(message);\n}\nfunction flattenError(err) {\n return err instanceof Error ? \"errors\" in err && Array.isArray(err.errors) ? err.errors.map(flattenError).join(\", \") : \"cause\" in err && err.cause instanceof Error ? `${err}: ${flattenError(err.cause)}` : err.message : `${err}`;\n}\nfunction inspectableError(err) {\n return {\n type: err.type,\n message: err.message,\n code: err.code,\n defaultPrevented: err.defaultPrevented,\n cancelable: err.cancelable,\n timeStamp: err.timeStamp\n };\n}\nvar __typeError = (msg) => {\n throw TypeError(msg);\n}, __accessCheck = (obj, member, msg) => member.has(obj) || __typeError(\"Cannot \" + msg), __privateGet = (obj, member, getter) => (__accessCheck(obj, member, \"read from private field\"), getter ? getter.call(obj) : member.get(obj)), __privateAdd = (obj, member, value) => member.has(obj) ? __typeError(\"Cannot add the same private member more than once\") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value), __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, \"write to private field\"), member.set(obj, value), value), __privateMethod = (obj, member, method) => (__accessCheck(obj, member, \"access private method\"), method), _readyState, _url, _redirectUrl, _withCredentials, _fetch, _reconnectInterval, _reconnectTimer, _lastEventId, _controller, _parser, _onError, _onMessage, _onOpen, _EventSource_instances, connect_fn, _onFetchResponse, _onFetchError, getRequestOptions_fn, _onEvent, _onRetryChange, failConnection_fn, scheduleReconnect_fn, _reconnect;\nclass EventSource extends EventTarget {\n constructor(url, eventSourceInitDict) {\n var _a, _b;\n super(), __privateAdd(this, _EventSource_instances), this.CONNECTING = 0, this.OPEN = 1, this.CLOSED = 2, __privateAdd(this, _readyState), __privateAdd(this, _url), __privateAdd(this, _redirectUrl), __privateAdd(this, _withCredentials), __privateAdd(this, _fetch), __privateAdd(this, _reconnectInterval), __privateAdd(this, _reconnectTimer), __privateAdd(this, _lastEventId, null), __privateAdd(this, _controller), __privateAdd(this, _parser), __privateAdd(this, _onError, null), __privateAdd(this, _onMessage, null), __privateAdd(this, _onOpen, null), __privateAdd(this, _onFetchResponse, async (response) => {\n var _a2;\n __privateGet(this, _parser).reset();\n const { body, redirected, status, headers } = response;\n if (status === 204) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, \"Server sent HTTP 204, not reconnecting\", 204), this.close();\n return;\n }\n if (redirected ? __privateSet(this, _redirectUrl, new URL(response.url)) : __privateSet(this, _redirectUrl, void 0), status !== 200) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, `Non-200 status code (${status})`, status);\n return;\n }\n if (!(headers.get(\"content-type\") || \"\").startsWith(\"text/event-stream\")) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, 'Invalid content type, expected \"text/event-stream\"', status);\n return;\n }\n if (__privateGet(this, _readyState) === this.CLOSED)\n return;\n __privateSet(this, _readyState, this.OPEN);\n const openEvent = new Event(\"open\");\n if ((_a2 = __privateGet(this, _onOpen)) == null || _a2.call(this, openEvent), this.dispatchEvent(openEvent), typeof body != \"object\" || !body || !(\"getReader\" in body)) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, \"Invalid response body, expected a web ReadableStream\", status), this.close();\n return;\n }\n const decoder = new TextDecoder(), reader = body.getReader();\n let open = !0;\n do {\n const { done, value } = await reader.read();\n value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open = !1, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));\n } while (open);\n }), __privateAdd(this, _onFetchError, (err) => {\n __privateSet(this, _controller, void 0), !(err.name === \"AbortError\" || err.type === \"aborted\") && __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this, flattenError(err));\n }), __privateAdd(this, _onEvent, (event) => {\n typeof event.id == \"string\" && __privateSet(this, _lastEventId, event.id);\n const messageEvent = new MessageEvent(event.event || \"message\", {\n data: event.data,\n origin: __privateGet(this, _redirectUrl) ? __privateGet(this, _redirectUrl).origin : __privateGet(this, _url).origin,\n lastEventId: event.id || \"\"\n });\n __privateGet(this, _onMessage) && (!event.event || event.event === \"message\") && __privateGet(this, _onMessage).call(this, messageEvent), this.dispatchEvent(messageEvent);\n }), __privateAdd(this, _onRetryChange, (value) => {\n __privateSet(this, _reconnectInterval, value);\n }), __privateAdd(this, _reconnect, () => {\n __privateSet(this, _reconnectTimer, void 0), __privateGet(this, _readyState) === this.CONNECTING && __privateMethod(this, _EventSource_instances, connect_fn).call(this);\n });\n try {\n if (url instanceof URL)\n __privateSet(this, _url, url);\n else if (typeof url == \"string\")\n __privateSet(this, _url, new URL(url, getBaseURL()));\n else\n throw new Error(\"Invalid URL\");\n } catch {\n throw syntaxError(\"An invalid or illegal string was specified\");\n }\n __privateSet(this, _parser, createParser({\n onEvent: __privateGet(this, _onEvent),\n onRetry: __privateGet(this, _onRetryChange)\n })), __privateSet(this, _readyState, this.CONNECTING), __privateSet(this, _reconnectInterval, 3e3), __privateSet(this, _fetch, (_a = eventSourceInitDict == null ? void 0 : eventSourceInitDict.fetch) != null ? _a : globalThis.fetch), __privateSet(this, _withCredentials, (_b = eventSourceInitDict == null ? void 0 : eventSourceInitDict.withCredentials) != null ? _b : !1), __privateMethod(this, _EventSource_instances, connect_fn).call(this);\n }\n /**\n * Returns the state of this EventSource object's connection. It can have the values described below.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/readyState)\n *\n * Note: typed as `number` instead of `0 | 1 | 2` for compatibility with the `EventSource` interface,\n * defined in the TypeScript `dom` library.\n *\n * @public\n */\n get readyState() {\n return __privateGet(this, _readyState);\n }\n /**\n * Returns the URL providing the event stream.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/url)\n *\n * @public\n */\n get url() {\n return __privateGet(this, _url).href;\n }\n /**\n * Returns true if the credentials mode for connection requests to the URL providing the event stream is set to \"include\", and false otherwise.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/withCredentials)\n */\n get withCredentials() {\n return __privateGet(this, _withCredentials);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */\n get onerror() {\n return __privateGet(this, _onError);\n }\n set onerror(value) {\n __privateSet(this, _onError, value);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */\n get onmessage() {\n return __privateGet(this, _onMessage);\n }\n set onmessage(value) {\n __privateSet(this, _onMessage, value);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */\n get onopen() {\n return __privateGet(this, _onOpen);\n }\n set onopen(value) {\n __privateSet(this, _onOpen, value);\n }\n addEventListener(type, listener, options) {\n const listen = listener;\n super.addEventListener(type, listen, options);\n }\n removeEventListener(type, listener, options) {\n const listen = listener;\n super.removeEventListener(type, listen, options);\n }\n /**\n * Aborts any instances of the fetch algorithm started for this EventSource object, and sets the readyState attribute to CLOSED.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close)\n *\n * @public\n */\n close() {\n __privateGet(this, _reconnectTimer) && clearTimeout(__privateGet(this, _reconnectTimer)), __privateGet(this, _readyState) !== this.CLOSED && (__privateGet(this, _controller) && __privateGet(this, _controller).abort(), __privateSet(this, _readyState, this.CLOSED), __privateSet(this, _controller, void 0));\n }\n}\n_readyState = /* @__PURE__ */ new WeakMap(), _url = /* @__PURE__ */ new WeakMap(), _redirectUrl = /* @__PURE__ */ new WeakMap(), _withCredentials = /* @__PURE__ */ new WeakMap(), _fetch = /* @__PURE__ */ new WeakMap(), _reconnectInterval = /* @__PURE__ */ new WeakMap(), _reconnectTimer = /* @__PURE__ */ new WeakMap(), _lastEventId = /* @__PURE__ */ new WeakMap(), _controller = /* @__PURE__ */ new WeakMap(), _parser = /* @__PURE__ */ new WeakMap(), _onError = /* @__PURE__ */ new WeakMap(), _onMessage = /* @__PURE__ */ new WeakMap(), _onOpen = /* @__PURE__ */ new WeakMap(), _EventSource_instances = /* @__PURE__ */ new WeakSet(), /**\n* Connect to the given URL and start receiving events\n*\n* @internal\n*/\nconnect_fn = function() {\n __privateSet(this, _readyState, this.CONNECTING), __privateSet(this, _controller, new AbortController()), __privateGet(this, _fetch)(__privateGet(this, _url), __privateMethod(this, _EventSource_instances, getRequestOptions_fn).call(this)).then(__privateGet(this, _onFetchResponse)).catch(__privateGet(this, _onFetchError));\n}, _onFetchResponse = /* @__PURE__ */ new WeakMap(), _onFetchError = /* @__PURE__ */ new WeakMap(), /**\n* Get request options for the `fetch()` request\n*\n* @returns The request options\n* @internal\n*/\ngetRequestOptions_fn = function() {\n var _a;\n const init = {\n // [spec] Let `corsAttributeState` be `Anonymous`…\n // [spec] …will have their mode set to \"cors\"…\n mode: \"cors\",\n redirect: \"follow\",\n headers: { Accept: \"text/event-stream\", ...__privateGet(this, _lastEventId) ? { \"Last-Event-ID\": __privateGet(this, _lastEventId) } : void 0 },\n cache: \"no-store\",\n signal: (_a = __privateGet(this, _controller)) == null ? void 0 : _a.signal\n };\n return \"window\" in globalThis && (init.credentials = this.withCredentials ? \"include\" : \"same-origin\"), init;\n}, _onEvent = /* @__PURE__ */ new WeakMap(), _onRetryChange = /* @__PURE__ */ new WeakMap(), /**\n* Handles the process referred to in the EventSource specification as \"failing a connection\".\n*\n* @param error - The error causing the connection to fail\n* @param code - The HTTP status code, if available\n* @internal\n*/\nfailConnection_fn = function(message, code) {\n var _a;\n __privateGet(this, _readyState) !== this.CLOSED && __privateSet(this, _readyState, this.CLOSED);\n const errorEvent = new ErrorEvent(\"error\", { code, message });\n (_a = __privateGet(this, _onError)) == null || _a.call(this, errorEvent), this.dispatchEvent(errorEvent);\n}, /**\n* Schedules a reconnection attempt against the EventSource endpoint.\n*\n* @param message - The error causing the connection to fail\n* @param code - The HTTP status code, if available\n* @internal\n*/\nscheduleReconnect_fn = function(message, code) {\n var _a;\n if (__privateGet(this, _readyState) === this.CLOSED)\n return;\n __privateSet(this, _readyState, this.CONNECTING);\n const errorEvent = new ErrorEvent(\"error\", { code, message });\n (_a = __privateGet(this, _onError)) == null || _a.call(this, errorEvent), this.dispatchEvent(errorEvent), __privateSet(this, _reconnectTimer, setTimeout(__privateGet(this, _reconnect), __privateGet(this, _reconnectInterval)));\n}, _reconnect = /* @__PURE__ */ new WeakMap(), /**\n* ReadyState representing an EventSource currently trying to connect\n*\n* @public\n*/\nEventSource.CONNECTING = 0, /**\n* ReadyState representing an EventSource connection that is open (eg connected)\n*\n* @public\n*/\nEventSource.OPEN = 1, /**\n* ReadyState representing an EventSource connection that is closed (eg disconnected)\n*\n* @public\n*/\nEventSource.CLOSED = 2;\nfunction getBaseURL() {\n const doc = \"document\" in globalThis ? globalThis.document : void 0;\n return doc && typeof doc == \"object\" && \"baseURI\" in doc && typeof doc.baseURI == \"string\" ? doc.baseURI : void 0;\n}\nexport {\n ErrorEvent,\n EventSource\n};\n//# sourceMappingURL=index.js.map\n","import type { AccessControlClient } from \"./client.js\";\n\nimport { logger } from \"../lib/logger.js\";\n\nexport interface AccessControlConfig {\n client?: AccessControlClient;\n enabled: boolean;\n serverName?: string;\n}\n\n/**\n * Authorizes MCP servers based on access control configuration.\n * Queries backend with full client info for authorization decisions.\n */\nexport class AccessControlAuthorizer {\n private client?: AccessControlClient;\n private enabled: boolean;\n private lastBlockReason?: string;\n private serverName?: string;\n\n constructor(config: AccessControlConfig) {\n this.enabled = config.enabled;\n this.client = config.client;\n this.serverName = config.serverName;\n\n logger.info(\"AccessControlAuthorizer initialized\", {\n enabled: this.enabled,\n hasClient: !!this.client,\n serverName: this.serverName,\n });\n }\n\n /**\n * Get human-readable reason for why a server was blocked.\n * Returns the reason from the last authorization check, or a default message.\n *\n * @returns Reason message\n */\n getBlockReason(): string {\n if (this.lastBlockReason) {\n return this.lastBlockReason;\n }\n // Default message with server name if available\n const serverPart = this.serverName ? ` '${this.serverName}'` : \"\";\n return `MCP server${serverPart} is not authorized for use in your organization and has been blocked by Onyx.`;\n }\n\n /**\n * Check if access is allowed by querying the backend.\n * Always fails open (allows) if backend is unavailable or not configured.\n * Stores the block reason from the backend for later retrieval.\n *\n * @returns Promise resolving to true if allowed, false if blocked\n */\n async isAllowed(): Promise<boolean> {\n // If access control not enabled, allow everything\n if (!this.enabled) {\n return true;\n }\n\n // Check with backend if client is available\n if (this.client) {\n try {\n const response = await this.client.authorize();\n if (response.action === \"block\") {\n // Store the reason from backend for user-facing error messages, or use default with server name\n if (response.reason) {\n this.lastBlockReason = response.reason;\n } else {\n const serverPart = this.serverName ? ` '${this.serverName}'` : \"\";\n this.lastBlockReason = `MCP server${serverPart} is not authorized for use in your organization and has been blocked by Onyx.`;\n }\n return false;\n }\n return true;\n } catch (error) {\n logger.error(\"Access control authorization failed with unexpected error\", {\n error: String(error),\n });\n // DEFENSIVE: client.authorize() handles all errors internally and always\n // returns an AccessControlResponse, so this catch is normally unreachable.\n // However, we keep it as defensive fail-open behavior for unexpected errors.\n return true;\n }\n }\n\n // No client configured - allow by default (shouldn't happen if enabled=true with URL)\n logger.warn(\"No access control client configured, allowing by default\");\n return true;\n }\n}\n","import type { SessionData } from \"../utils/getUserData.js\";\n\nimport { compressClientInfo } from \"../lib/compress-client-info.js\";\nimport { logger } from \"../lib/logger.js\";\n\nexport interface AccessControlClientConfig {\n apiKey: string;\n headers: Record<string, string>;\n sessionData: SessionData;\n timeoutMs: number;\n url: string;\n}\n\nexport interface AccessControlResponse {\n action: \"allow\" | \"block\";\n reason?: string;\n}\n\n/**\n * Client for checking MCP server access control with backend API.\n * Always fails open (allows) when backend is unreachable.\n */\nexport class AccessControlClient {\n private clientInfoBase64: string;\n private config: AccessControlClientConfig;\n\n constructor(config: AccessControlClientConfig) {\n this.config = config;\n this.clientInfoBase64 = this.getClientInfoBase64();\n logger.info(\"AccessControlClient initialized\", {\n timeoutMs: config.timeoutMs,\n url: config.url,\n });\n }\n\n /**\n * Authorize by querying the backend with full client info.\n * Always fails open (allows) on any error.\n *\n * @returns Access control response\n */\n async authorize(): Promise<AccessControlResponse> {\n try {\n // Try to get decision from backend\n const response = await this.sendAuthorizeRequest();\n\n logger.debug(\"Access control check successful\", {\n action: response.action,\n });\n\n return response;\n } catch (error) {\n logger.warn(\"Access control check failed, failing open (allowing by default)\", {\n error: String(error),\n });\n\n // Always fail open (allow by default for better UX)\n return { action: \"allow\" };\n }\n }\n\n /**\n * Compress and base64-encode client info for URL.\n * Matches scanner pattern.\n */\n private getClientInfoBase64(): string {\n return compressClientInfo(this.config.sessionData);\n }\n\n /**\n * Send authorization request to backend with compressed client info in URL.\n * Matches scanner URL pattern: ${url}/${apiKey}/mcp/${clientInfoBase64}\n *\n * @returns Access control response\n * @throws Error if request fails\n */\n private async sendAuthorizeRequest(): Promise<AccessControlResponse> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);\n\n try {\n // Build URL with API key and compressed client info (matching scanner pattern)\n const checkUrl = `${this.config.url}/${this.config.apiKey}/mcp/${this.clientInfoBase64}`;\n\n const response = await fetch(checkUrl, {\n headers: {\n ...this.config.headers,\n },\n method: \"POST\",\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Access control service returned ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as AccessControlResponse;\n\n // Validate response format\n if (!result.action || ![\"allow\", \"block\"].includes(result.action)) {\n throw new Error(`Invalid access control response format: action=\"${result.action}\"`);\n }\n\n return result;\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new Error(`Access control check timed out after ${this.config.timeoutMs}ms`);\n }\n throw error;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n}\n","import type { OAuthClientInformationFull, OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\nimport { createHash } from \"node:crypto\";\nimport { chmod, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { chownToRealUser } from \"../utils/chownToRealUser.js\";\nimport { normalizeUrl } from \"../utils/normalizeUrl.js\";\n\nexport interface OAuthCredentialStoreOptions {\n /**\n * Directory to store OAuth credentials.\n * Defaults to ~/.onyx/mcp-gateway/oauth/\n */\n storageDir: string;\n}\n\n/**\n * Stored credentials structure persisted to disk.\n */\nexport interface StoredCredentials {\n /**\n * DCR client registration information.\n */\n clientInfo?: OAuthClientInformationFull;\n /**\n * Timestamp when the credentials were stored (ISO string).\n */\n createdAt: string;\n /**\n * The original server URL these credentials are for.\n */\n serverUrl: string;\n /**\n * OAuth tokens (access_token, refresh_token, etc.).\n */\n tokens?: OAuthTokens;\n /**\n * Timestamp when tokens were obtained (ISO string).\n * Used to calculate token expiration.\n */\n tokensObtainedAt?: string;\n /**\n * Last update timestamp (ISO string).\n */\n updatedAt: string;\n}\n\n/**\n * File-based credential store for OAuth credentials.\n * Stores credentials in JSON files keyed by server URL hash.\n */\nexport class OAuthCredentialStore {\n private readonly storageDir: string;\n\n constructor(options: OAuthCredentialStoreOptions) {\n this.storageDir = options.storageDir;\n }\n\n /**\n * Deletes stored credentials for a server URL.\n */\n async delete(serverUrl: string): Promise<void> {\n // Normalize URL to ensure consistent key generation\n const normalizedUrl = normalizeUrl(serverUrl);\n const filePath = this.getFilePath(normalizedUrl);\n\n try {\n await rm(filePath);\n logger.debug(\"Deleted stored credentials\", { filePath, serverUrl: normalizedUrl });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n logger.warn(\"Failed to delete stored credentials\", {\n error: String(error),\n filePath,\n serverUrl: normalizedUrl,\n });\n }\n }\n }\n\n /**\n * Checks if stored credentials have a refresh token.\n */\n hasRefreshToken(credentials: StoredCredentials): boolean {\n return !!credentials.tokens?.refresh_token;\n }\n\n /**\n * Checks if stored credentials have a valid (non-expired) access token.\n */\n hasValidAccessToken(credentials: StoredCredentials): boolean {\n return !!credentials.tokens?.access_token && !this.isAccessTokenExpired(credentials);\n }\n\n /**\n * Checks if the stored access token is expired.\n * Returns true if expired or if expiration cannot be determined.\n */\n isAccessTokenExpired(credentials: StoredCredentials): boolean {\n if (!credentials.tokens || !credentials.tokensObtainedAt) {\n return true;\n }\n\n const { expires_in } = credentials.tokens;\n if (!expires_in) {\n // No expiration info - assume not expired\n return false;\n }\n\n const obtainedAt = new Date(credentials.tokensObtainedAt).getTime();\n const expiresAt = obtainedAt + expires_in * 1000;\n const now = Date.now();\n\n // Add 30 second buffer to avoid edge cases\n const isExpired = now >= expiresAt - 30000;\n\n logger.debug(\"Checked access token expiration\", {\n expiresAt: new Date(expiresAt).toISOString(),\n expiresIn: expires_in,\n isExpired,\n now: new Date(now).toISOString(),\n obtainedAt: credentials.tokensObtainedAt,\n });\n\n return isExpired;\n }\n\n /**\n * Loads stored credentials for a server URL.\n * Returns undefined if no credentials exist or if they're invalid.\n */\n async load(serverUrl: string): Promise<StoredCredentials | undefined> {\n // Normalize URL to ensure consistent key generation\n const normalizedUrl = normalizeUrl(serverUrl);\n const filePath = this.getFilePath(normalizedUrl);\n\n try {\n const content = await readFile(filePath, \"utf-8\");\n const credentials = JSON.parse(content) as StoredCredentials;\n\n // Validate the stored credentials match the requested server (using normalized URL)\n // Also accept credentials stored with the original URL format for backward compatibility\n const storedUrlNormalized = normalizeUrl(credentials.serverUrl);\n if (storedUrlNormalized !== normalizedUrl) {\n logger.warn(\"Credential file server URL mismatch\", {\n expected: normalizedUrl,\n filePath,\n found: credentials.serverUrl,\n });\n return undefined;\n }\n\n logger.debug(\"Loaded stored credentials\", {\n clientId: credentials.clientInfo?.client_id,\n hasRefreshToken: !!credentials.tokens?.refresh_token,\n hasTokens: !!credentials.tokens,\n serverUrl: normalizedUrl,\n });\n\n return credentials;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n // File doesn't exist - no stored credentials\n logger.debug(\"No stored credentials found\", { serverUrl: normalizedUrl });\n return undefined;\n }\n\n logger.warn(\"Failed to load stored credentials\", {\n error: String(error),\n filePath,\n serverUrl: normalizedUrl,\n });\n return undefined;\n }\n }\n\n /**\n * Saves credentials to disk.\n */\n async save(credentials: StoredCredentials): Promise<void> {\n await this.ensureStorageDir();\n\n const filePath = this.getFilePath(credentials.serverUrl);\n const content = JSON.stringify(credentials, null, 2);\n\n try {\n await writeFile(filePath, content, { encoding: \"utf-8\", mode: 0o600 });\n // Ensure permissions are correct even if file existed\n await chmod(filePath, 0o600);\n\n logger.debug(\"Saved credentials to disk\", {\n clientId: credentials.clientInfo?.client_id,\n filePath,\n hasTokens: !!credentials.tokens,\n serverUrl: credentials.serverUrl,\n });\n } catch (error) {\n logger.error(\"Failed to save credentials\", {\n error: String(error),\n filePath,\n serverUrl: credentials.serverUrl,\n });\n throw error;\n }\n }\n\n /**\n * Updates specific fields in stored credentials.\n * Creates new credentials if none exist.\n */\n async update(\n serverUrl: string,\n updates: Partial<Pick<StoredCredentials, \"clientInfo\" | \"tokens\" | \"tokensObtainedAt\">>\n ): Promise<StoredCredentials> {\n // Normalize URL to ensure consistent key generation and storage\n const normalizedUrl = normalizeUrl(serverUrl);\n const existing = await this.load(normalizedUrl);\n const now = new Date().toISOString();\n\n const credentials: StoredCredentials = existing\n ? {\n ...existing,\n ...updates,\n // Update serverUrl to normalized form for consistency\n serverUrl: normalizedUrl,\n updatedAt: now,\n }\n : {\n createdAt: now,\n serverUrl: normalizedUrl,\n updatedAt: now,\n ...updates,\n };\n\n await this.save(credentials);\n return credentials;\n }\n\n /**\n * Ensures the storage directory exists with proper permissions.\n */\n private async ensureStorageDir(): Promise<void> {\n try {\n await mkdir(this.storageDir, { mode: 0o700, recursive: true });\n } catch (error) {\n // Directory may already exist\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") {\n throw error;\n }\n }\n\n // Best-effort ownership repair; should not block auth flows\n try {\n await chownToRealUser(this.storageDir);\n } catch (error) {\n logger.warn(\"Failed to fix OAuth storage directory ownership\", {\n error: String(error),\n storageDir: this.storageDir,\n });\n }\n }\n\n /**\n * Generates a URL-safe filename from a server URL.\n * Uses SHA-256 hash truncated to 16 chars for brevity.\n */\n private generateKey(serverUrl: string): string {\n const hash = createHash(\"sha256\").update(serverUrl).digest(\"hex\");\n return hash.substring(0, 16);\n }\n\n /**\n * Gets the file path for a server's credentials.\n */\n private getFilePath(serverUrl: string): string {\n const key = this.generateKey(serverUrl);\n return join(this.storageDir, `${key}.json`);\n }\n}\n\n","/**\n * Dynamic OAuth bearer for the gateway's HTTP transports (PRDCT-3666 Tier 2).\n *\n * The gateway's first cut bound the access_token into the HTTP transport's\n * Authorization header ONCE, at connect time (`main.ts:606`). Once the gateway\n * had successfully connected, an upstream-side token rotation, revocation, or\n * clock-skew-driven expiry would 401 every subsequent request, with no\n * recovery — the user's session was effectively dead until they restarted the\n * MCP client.\n *\n * `createDynamicAuth` returns a handle whose `getCurrentBearer()` is read at\n * fetch time (not connect time). `createAuthenticatingFetch` wraps the\n * transport's underlying `fetch` so that:\n *\n * 1. Each outbound request reads the current bearer just-in-time and\n * attaches it as `Authorization: Bearer ...`.\n * 2. If the upstream returns 401 to a request we sent with a bearer, the\n * wrapper triggers a single silent refresh attempt and retries the SAME\n * request once. Concurrent 401s share the same in-flight refresh\n * (single-flight) to avoid burning the refresh_token via rotation races.\n * 3. Before refreshing, the handle re-reads the on-disk credentials —\n * another gateway process may already have refreshed the token, in which\n * case we use the disk value instead of burning another refresh.\n * 4. If the refresh ultimately fails (`invalid_grant`, no refresh_token,\n * transient error), the wrapper does NOT open a browser — that decision\n * is left to the connect-time `main.ts` handler (which can fall back to\n * interactive OAuth when appropriate). The mid-stream contract is: a\n * browser must never open as a direct reaction to a 401.\n */\n\nimport type { FetchLike } from \"@modelcontextprotocol/sdk/shared/transport.js\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { tryRefreshCachedToken } from \"./oauthFlow.js\";\nimport type { OAuthCredentialStore } from \"./OAuthCredentialStore.js\";\n\nexport interface DynamicAuthHandle {\n /** Synchronous read of the in-memory bearer. May be undefined. */\n getCurrentBearer(): string | undefined;\n /**\n * Attempt a silent refresh. Deduplicates concurrent calls via a single-flight\n * promise. Before issuing an HTTP refresh, reads the on-disk credentials to\n * pick up any rotation done by a parallel gateway process.\n *\n * `sentBearer` is the bearer that just got 401'd. If the disk now holds a\n * different bearer, the on-disk value is adopted and returned without a new\n * HTTP call (multi-process race recovery).\n *\n * Returns the new bearer on success, undefined if refresh failed for any\n * reason (`invalid_grant`, transient, no refresh_token on disk).\n */\n refreshIfPossible(sentBearer?: string): Promise<string | undefined>;\n /** Replace the in-memory bearer (e.g., after a successful `performOAuthFlow`). */\n setBearer(bearer: string | undefined): void;\n}\n\n/**\n * Wraps `fetch` to attach the dynamic Authorization header and recover from\n * 401 responses via a single silent-refresh-and-retry. Pass the returned\n * function to the SDK transport's `fetch` option.\n *\n * Transport coverage notes — including the one case we deliberately do NOT\n * support (silent mid-stream drop on an open SSE event-stream) — are\n * documented in detail above `createTransport` in\n * `src/lib/transportDetection.ts`. Read it before changing this wrapper.\n */\nexport function createAuthenticatingFetch(\n auth: DynamicAuthHandle,\n baseFetch: typeof fetch = (...args) => globalThis.fetch(...args),\n): FetchLike {\n return async (url, init) => {\n // Whether the caller supplied a static Authorization (api-key / Basic /\n // static bearer via --env-http-headers, --headers, or --bearer-token-env).\n // The wrapper owns only the OAuth bearer; it must not clobber a credential\n // the caller explicitly provided. Without this guard, non-OAuth auth modes\n // break in any environment without a cached OAuth token (PRDCT-5136).\n const callerSuppliedAuth = new Headers(init?.headers).has(\"Authorization\");\n\n const sendInit = (bearer: string | undefined): RequestInit => {\n const headers = new Headers(init?.headers);\n if (bearer) {\n // OAuth bearer takes precedence and is refreshed mid-session.\n headers.set(\"Authorization\", `Bearer ${bearer}`);\n } else if (!callerSuppliedAuth) {\n // No bearer and no caller-supplied credential: clear only the slot the\n // wrapper would otherwise manage, so we never send a stale OAuth token.\n headers.delete(\"Authorization\");\n }\n // else: no bearer but the caller supplied a static Authorization — leave\n // it intact so the upstream can authenticate it (api-key / Basic / etc.).\n return { ...init, headers };\n };\n\n const bearer = auth.getCurrentBearer();\n let response = await baseFetch(url, sendInit(bearer));\n\n // Diagnostic: capture the wire-level details of any non-2xx upstream\n // response. The SDK transport only surfaces \"HTTP <code>: <body>\" and drops\n // response headers — but WWW-Authenticate is exactly the signal we need\n // post-mortem to tell a non-compliant 500 from a spec-correct 401 + OAuth\n // discovery pointer. The body is bounded and only read on error (a streaming\n // 2xx is never cloned). A 401 is the expected OAuth-challenge case (info);\n // any other non-2xx is surprising (warn).\n if (!response.ok) {\n let bodySnippet = \"\";\n try {\n bodySnippet = (await response.clone().text()).slice(0, 500);\n } catch {\n /* body not readable (already consumed or streaming) */\n }\n const diag = {\n bodySnippet,\n contentType: response.headers.get(\"content-type\") ?? undefined,\n eventType: \"mcp_upstream_non_2xx\",\n method: String(init?.method ?? \"GET\").toUpperCase(),\n requestAccept: new Headers(init?.headers).get(\"accept\") ?? undefined,\n requestHadAuth: Boolean(bearer) || callerSuppliedAuth,\n status: response.status,\n url: sanitizeUrlForLog(url),\n wwwAuthenticate: response.headers.get(\"www-authenticate\") ?? undefined,\n };\n if (response.status === 401) {\n logger.info(\"Upstream returned non-2xx (auth challenge)\", diag);\n } else {\n logger.warn(\"Upstream returned non-2xx\", diag);\n }\n }\n\n // Only attempt refresh-and-retry when we actually sent a bearer that was\n // rejected. A 401 without a bearer means the upstream is asking us to\n // authenticate from scratch — that path is handled by the connect-time\n // logic in main.ts, not here.\n if (response.status === 401 && bearer) {\n logger.info(\"Upstream returned 401; attempting mid-session silent refresh\", {\n eventType: \"mcp_auth_mid_session_refresh_attempt\",\n });\n const refreshed = await auth.refreshIfPossible(bearer);\n if (refreshed && refreshed !== bearer) {\n logger.info(\"Silent refresh succeeded mid-session; retrying request\", {\n eventType: \"mcp_auth_mid_session_refresh_succeeded\",\n });\n response = await baseFetch(url, sendInit(refreshed));\n } else {\n logger.warn(\"Silent refresh produced no new bearer; passing 401 through\", {\n eventType: \"mcp_auth_mid_session_refresh_failed\",\n });\n }\n }\n\n return response;\n };\n}\n\nexport function createDynamicAuth(\n serverUrl: string,\n credentialStore: OAuthCredentialStore,\n initialBearer: string | undefined,\n): DynamicAuthHandle {\n let currentBearer = initialBearer;\n let refreshInFlight: null | Promise<string | undefined> = null;\n\n return {\n getCurrentBearer: () => currentBearer,\n refreshIfPossible: (sentBearer) => {\n // Single-flight: concurrent callers share the same refresh promise so\n // that N parallel 401s yield ONE refresh HTTP call (not N), avoiding\n // refresh_token rotation races within this process.\n if (refreshInFlight) return refreshInFlight;\n refreshInFlight = (async () => {\n try {\n // Multi-process recovery: another gateway process may already have\n // refreshed since we loaded our in-memory bearer. Re-read disk\n // before committing to a refresh HTTP call.\n const onDisk = await credentialStore.load(serverUrl);\n const diskBearer = onDisk?.tokens?.access_token;\n if (diskBearer && diskBearer !== sentBearer) {\n logger.info(\"Adopting newer access_token from disk (other process refreshed)\", {\n eventType: \"mcp_auth_disk_bearer_adopted\",\n serverUrl,\n });\n currentBearer = diskBearer;\n return diskBearer;\n }\n // Disk has the same bearer we tried (or none). Issue a refresh.\n const result = await tryRefreshCachedToken(serverUrl, credentialStore);\n if (result?.tokens.access_token) {\n currentBearer = result.tokens.access_token;\n return currentBearer;\n }\n // Refresh failed (invalid_grant cleared the tokens, invalid_client\n // cleared the whole record, transient, or no refresh_token on disk).\n // Leave currentBearer as-is so the connect-time handler can take over\n // when this gateway process restarts; callers must NOT open a browser\n // from inside the fetch wrapper.\n return undefined;\n } finally {\n refreshInFlight = null;\n }\n })();\n return refreshInFlight;\n },\n setBearer: (bearer) => {\n currentBearer = bearer;\n },\n };\n}\n\n/**\n * Origin + pathname only — drops the query string / fragment, which can carry\n * tokens or other sensitive material. Used for diagnostic logging of upstream\n * requests so a log line never leaks a credential.\n */\nfunction sanitizeUrlForLog(url: unknown): string {\n try {\n const s = typeof url === \"string\" ? url : String(url);\n const u = new URL(s);\n return `${u.origin}${u.pathname}`;\n } catch {\n return \"[unparseable-url]\";\n }\n}\n","import { createInterface } from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\n\n/**\n * Surface a startup failure to the parent MCP client (e.g. OpenCode, Cursor,\n * Claude Code) as a specific message instead of the generic\n * `MCP error -32000: Connection closed`.\n *\n * Why this is needed: the gateway connects to its upstream BEFORE it opens the\n * client-facing stdio transport, so every startup failure happens before the\n * MCP `Server` exists. Exiting at that point closes the stdio pipe with no\n * JSON-RPC reply, and the client renders that as `-32000`. The only thing a\n * stdio MCP client renders as a readable reason is a JSON-RPC **error reply to\n * the `initialize` request** (an error on `tools/list` is shown only as a\n * generic \"Failed to get tools\" — verified against OpenCode).\n *\n * So on a startup failure we briefly read stdin, wait for the client's first\n * request (`initialize`), reply with a JSON-RPC error carrying `reason`, then\n * let the caller exit. This must run ONLY before the real `Server` transport is\n * connected (otherwise two readers would fight over stdin) and ONLY in stdio\n * mode with a real client attached — hence the `isTTY` short-circuit.\n *\n * `input`/`output` default to process.stdin/stdout; they exist for unit tests.\n *\n * Resolves `true` if it replied to a request, `false` otherwise (TTY / no\n * client / timeout / stdin closed). Never rejects.\n */\nexport async function respondInitializeError(\n reason: string,\n opts: { code?: number; input?: Readable; output?: Writable; timeoutMs?: number } = {},\n): Promise<boolean> {\n const code = opts.code ?? -32603; // JSON-RPC InternalError; clients show `message`\n const timeoutMs = opts.timeoutMs ?? 4000;\n const input = opts.input ?? process.stdin;\n const output = opts.output ?? process.stdout;\n\n // Run directly in a terminal (no MCP client over the pipe) → nothing to answer.\n if ((input as { isTTY?: boolean }).isTTY) return false;\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n let replied = false;\n // Claimed synchronously the moment we decide to answer a request, so several\n // id-bearing requests buffered before the (async) write callback runs can't\n // each emit their own reply — exactly one JSON-RPC error ever hits stdout.\n let claimed = false;\n const rl = createInterface({ input });\n\n const finish = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n rl.removeAllListeners(\"line\");\n rl.close();\n resolve(replied);\n };\n\n // Cap the wait: a piped-but-silent stdin (no client speaking) must not hang.\n const timer = setTimeout(finish, timeoutMs);\n timer.unref?.();\n\n rl.on(\"line\", (line) => {\n if (claimed) return; // a reply is already in flight — ignore the rest\n const trimmed = line.trim();\n if (!trimmed) return;\n let msg: { id?: unknown };\n try {\n msg = JSON.parse(trimmed) as { id?: unknown };\n } catch {\n return; // ignore non-JSON noise\n }\n // The first request carrying an id is `initialize`. Reply to it with the\n // error so the client shows `reason` rather than `-32000`.\n if (msg && msg.id !== undefined && msg.id !== null) {\n claimed = true; // set BEFORE the async write; stop processing buffered lines now\n // We've committed to replying (the bytes are handed to output.write below).\n // Set `replied` here too, so a stdin end/timeout racing the write callback\n // can't resolve false despite the reply having been written.\n replied = true;\n rl.removeAllListeners(\"line\");\n output.write(\n `${JSON.stringify({ error: { code, message: reason }, id: msg.id, jsonrpc: \"2.0\" })}\\n`,\n () => {\n finish();\n },\n );\n }\n });\n\n input.once(\"end\", finish);\n input.once(\"error\", finish);\n });\n}\n","/**\n * Classification of errors from the upstream MCP server connection.\n *\n * The gateway connects to a remote MCP server whose reachability can flap\n * (DNS/VPN/tailscale blips, brief outages). undici's global `fetch` surfaces\n * such failures as `TypeError: fetch failed` with the real OS error on\n * `err.cause` (e.g. `Error: connect ECONNREFUSED` carrying `.code`). These are\n * transient: retrying the connection usually succeeds once connectivity\n * returns. This module decides which errors are worth retrying/reconnecting on,\n * and extracts the OS errno for diagnostics — the fatal-startup path previously\n * logged only the opaque `\"fetch failed\"` message and dropped the errno, which\n * made this class of failure hard to triage.\n *\n * `is401Error` is the single source of truth for \"the upstream is asking us to\n * authenticate\" (also consumed by the connect-time OAuth logic in main.ts). A\n * 401 is deliberately NOT a recoverable connection error — it needs an auth\n * refresh / OAuth flow, not a blind retry.\n */\n\nimport { UnauthorizedError } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport { McpError } from \"@modelcontextprotocol/sdk/types.js\";\n\nimport { AccessControlBlockError } from \"./errors.js\";\n\n/**\n * OS / undici error codes that indicate a transport-level failure the gateway\n * can recover from by retrying (or reconnecting) the upstream connection. This\n * spans both connect-phase failures (connection never established) and\n * mid-flight drops; the retry-vs-reconnect decision keys off recoverability,\n * while retry SAFETY for side-effecting calls is a separate concern.\n */\nconst RECOVERABLE_ERRNOS = new Set<string>([\n \"EAI_AGAIN\",\n \"ECONNABORTED\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"EHOSTDOWN\",\n \"EHOSTUNREACH\",\n \"ENETDOWN\",\n \"ENETUNREACH\",\n \"ENOTFOUND\",\n \"EPIPE\",\n \"EPROTO\",\n \"ETIMEDOUT\",\n \"UND_ERR_BODY_TIMEOUT\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n]);\n\n/**\n * Connection-phase errnos: the TCP connection was never established, so the\n * request provably never reached the upstream (let alone a tool). Safe to retry\n * even a side-effecting call — it cannot have executed. A mid-flight reset\n * (ECONNRESET/EPIPE) or timeout is deliberately NOT here: the request may have\n * been received and acted on, so retrying it is unsafe (see isPreDispatchError).\n */\nconst PRE_DISPATCH_ERRNOS = new Set<string>([\n \"EAI_AGAIN\",\n \"ECONNREFUSED\",\n \"EHOSTDOWN\",\n \"EHOSTUNREACH\",\n \"ENETDOWN\",\n \"ENETUNREACH\",\n \"ENOTFOUND\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n]);\n\n/**\n * Message fragments that identify a transport failure when no structured errno\n * is available (e.g. the MCP SDK stringifies transport errors into a plain\n * `Error.message`, and `ProxyConnectionError` embeds the underlying message).\n * Kept to unambiguous transport/timeout phrases to avoid misclassifying an\n * application-level error as a connection problem.\n */\n// A pure client-side request timeout (\"MCP error -32001: Request timed out\") is\n// deliberately NOT here: a tool merely exceeding the forward timeout on a healthy\n// connection would otherwise trigger a needless reconnect that closes the client\n// and aborts sibling in-flight calls. Genuine connection death still surfaces as\n// an errno or a \"connection closed\"/\"not connected\" message, which ARE covered.\nconst RECOVERABLE_MESSAGE_RE =\n /\\bfetch failed\\b|socket hang up|other side closed|\\bterminated\\b|network (?:error|is unreachable)|econnrefused|econnreset|econnaborted|etimedout|enotfound|eai_again|enetunreach|ehostunreach|und_err|not connected|transport (?:is )?closed|connection closed/i;\n\n/**\n * Message fragments meaning the request was NOT transmitted — the transport was\n * already closed when we tried to send. Like a connect-phase errno, this is\n * safe to retry even for a side-effecting call: it provably never executed.\n * (The MCP SDK throws \"Not connected\" when its transport is closed.)\n */\nconst PRE_DISPATCH_MESSAGE_RE = /\\bnot connected\\b|transport (?:is )?closed/i;\n\n// MCP-session-expiry wording. The strong set is safe to trust even inside an\n// McpError (a JSON-RPC error from a tool); the general set may appear in\n// app-level tool text and is only trusted for transport-level (non-McpError)\n// errors — see isStaleSessionError.\nconst SESSION_PHRASE_RE =\n /session (?:not found|expired|has expired|is invalid|invalid|mismatch|does not|no longer)|no valid session|unknown session|mcp-session-id/i;\nconst STRONG_SESSION_PHRASE_RE = /no valid session|unknown session|mcp-session-id|session not found/i;\n\n/**\n * Walks the common nested error shapes (`cause`, `response`, `data`, `body`) to\n * find an OS/undici errno string like `ECONNREFUSED`. undici puts the real\n * cause of a `fetch failed` on `err.cause`. Returns undefined when no errno is\n * present. Mirrors the `.cause`-walking idiom in `src/auth/errors.ts`.\n */\nexport function getUpstreamErrno(error: unknown): string | undefined {\n return walkForErrno(error, new WeakSet<object>());\n}\n\n/**\n * Extracts an HTTP-ish status code from the common nested error shapes. The MCP\n * SDK's Streamable-HTTP transport surfaces some failures as an error carrying a\n * numeric `status`/`statusCode`/`code`. Returns undefined when none is present\n * (e.g. a plain `Error POSTing to endpoint: <body>` that dropped the status).\n */\nexport function getUpstreamHttpStatus(error: unknown): number | undefined {\n return walkForStatus(error, new WeakSet<object>());\n}\n\n/**\n * Whether the upstream is asking us to authenticate (HTTP 401 / SDK\n * UnauthorizedError). Extracted from main.ts so the connect-time OAuth handler\n * and the recoverable-error classifier share one definition.\n */\nexport function is401Error(error: unknown): boolean {\n if (error instanceof UnauthorizedError) return true;\n if (error && typeof error === \"object\") {\n const status = (error as { status?: number }).status;\n const code = (error as { code?: number }).code;\n const message = (error as { message?: string }).message;\n if (status === 401 || code === 401) return true;\n // Require auth/HTTP context around a textual 401 so an unrelated transport\n // error whose text merely contains \"401\" (a port like `:401`, \"401 bytes\")\n // isn't classified as auth — this now gates retry-vs-fail-fast, and the\n // structured status/code checks above already cover real 401 responses.\n if (message && /\\bunauthorized\\b|\\b(?:http|status)(?:\\s+(?:status|code))?\\s*[:=]?\\s*401\\b/i.test(String(message)))\n return true;\n }\n return false;\n}\n\n/**\n * Whether an error is safe to retry even for a side-effecting call (tools/call):\n * the request provably never reached tool execution. True for connect-phase\n * network failures (connection never established) and stale-session rejections\n * (rejected at the session gate). Mid-flight resets/timeouts and 5xx are NOT\n * pre-dispatch — the request may already have executed, so the reconnecting\n * client reconnects but does NOT retry those for side-effecting calls.\n */\nexport function isPreDispatchError(error: unknown): boolean {\n // A JSON-RPC McpError is a response from the server's protocol layer, which\n // proves the request was dispatched and processed — so it can NEVER be\n // pre-dispatch, and a side-effecting tools/call must not be retried on it\n // (that is the double-execution bug). The SDK's closed-transport signal\n // (\"Not connected\") is a plain Error, so the intended path below is unaffected.\n if (error instanceof McpError) return false;\n const errno = getUpstreamErrno(error);\n if (errno && PRE_DISPATCH_ERRNOS.has(errno)) return true;\n if (isStaleSessionError(error)) return true;\n return PRE_DISPATCH_MESSAGE_RE.test(messageOf(error));\n}\n\n/**\n * Whether an upstream connection/forwarding error is a transient transport\n * failure worth retrying the connect or reconnecting the session for.\n *\n * Explicitly NOT recoverable: access-control blocks (a policy decision, not a\n * connection problem) and 401s (need auth, not a retry).\n */\nexport function isRecoverableUpstreamError(error: unknown): boolean {\n if (error instanceof AccessControlBlockError) return false;\n if (is401Error(error)) return false;\n\n const errno = getUpstreamErrno(error);\n if (errno && RECOVERABLE_ERRNOS.has(errno)) return true;\n\n // A terminated/expired MCP session is recoverable by re-initializing.\n if (isStaleSessionError(error)) return true;\n\n // 5xx from the upstream is transient (deploy, overload) — worth a reconnect.\n const status = getUpstreamHttpStatus(error);\n if (status !== undefined && status >= 500) return true;\n\n const message = messageOf(error);\n if (message.length > 0 && RECOVERABLE_MESSAGE_RE.test(message)) return true;\n\n return false;\n}\n\n/**\n * Whether the upstream rejected the request because its MCP session is no longer\n * valid — the spec's 404-on-terminated-session, the 400 \"no valid session\"\n * variant some servers return, or an explicit session-mismatch message. Such a\n * rejection happens at the session gate, BEFORE the tool runs, so re-initializing\n * and retrying is safe.\n */\nexport function isStaleSessionError(error: unknown): boolean {\n const message = messageOf(error);\n // A tool's own McpError may mention \"session\" in app-level text (e.g. surfacing\n // a backing-API auth error) — that is not a stale MCP *session*. Only strong\n // transport-session wording is trusted inside an McpError. A genuine terminated\n // session arrives as a StreamableHTTPError (HTTP 404), handled below.\n if (error instanceof McpError) return STRONG_SESSION_PHRASE_RE.test(message);\n const status = getUpstreamHttpStatus(error);\n if (status === 404) return true; // spec: server 404s a request bearing a terminated session id\n if (status === 400 && SESSION_PHRASE_RE.test(message)) return true;\n // The status is frequently absent on the thrown error; fall back to the phrase.\n return SESSION_PHRASE_RE.test(message);\n}\n\nfunction messageOf(error: unknown): string {\n if (error instanceof Error) return error.message ?? \"\";\n if (typeof error === \"string\") return error;\n if (error && typeof error === \"object\" && \"message\" in error) {\n return String((error as { message?: unknown }).message ?? \"\");\n }\n return \"\";\n}\n\nfunction walkForErrno(error: unknown, seen: WeakSet<object>): string | undefined {\n if (error == null || typeof error !== \"object\") return undefined;\n if (seen.has(error)) return undefined;\n seen.add(error);\n\n const code = (error as NodeJS.ErrnoException).code;\n // Node errno codes are uppercase strings (ECONNREFUSED, UND_ERR_SOCKET). A\n // numeric `code` here is an HTTP-ish status, not an errno — skip it.\n if (typeof code === \"string\" && /^[A-Z][A-Z0-9_]*$/.test(code)) {\n return code;\n }\n\n for (const key of [\"cause\", \"response\", \"data\", \"body\"] as const) {\n const found = walkForErrno((error as Record<string, unknown>)[key], seen);\n if (found) return found;\n }\n return undefined;\n}\n\nfunction walkForStatus(error: unknown, seen: WeakSet<object>): number | undefined {\n if (error == null || typeof error !== \"object\") return undefined;\n if (seen.has(error)) return undefined;\n seen.add(error);\n\n // An McpError's `code` is a JSON-RPC error code, not an HTTP status. A tool\n // that returns McpError(404, ...) already executed, so reading its code as an\n // HTTP/stale-session signal would retry a side-effecting call (double exec). A\n // genuine transport HTTP status arrives as a StreamableHTTPError (not McpError).\n const keys =\n error instanceof McpError ? ([\"status\", \"statusCode\"] as const) : ([\"status\", \"statusCode\", \"code\"] as const);\n for (const key of keys) {\n const value = (error as Record<string, unknown>)[key];\n // Only treat 3-digit HTTP-range numbers as a status. A Node errno (string\n // code) or a negative JSON-RPC code (e.g. -32001) is not an HTTP status.\n if (typeof value === \"number\" && value >= 100 && value <= 599) return value;\n }\n\n for (const key of [\"cause\", \"response\", \"data\", \"body\"] as const) {\n const found = walkForStatus((error as Record<string, unknown>)[key], seen);\n if (found !== undefined) return found;\n }\n return undefined;\n}\n","/**\n * Bounded retry-with-backoff for the initial upstream connection.\n *\n * The gateway connects to the upstream MCP server once at startup and treats a\n * failure as fatal (\"MCP server failed to start\") — so a brief reachability\n * blip at the moment the MCP client spawns the gateway kills the whole session\n * (the tools never appear, and the client won't respawn a failed stdio server).\n * Because upstream reachability flaps and self-recovers within seconds, a small\n * bounded retry turns most of these transient failures into a successful start.\n *\n * Retries ONLY on recoverable transport errors (default:\n * `isRecoverableUpstreamError`); a policy block / 401 / config error fails fast\n * so the existing connect-time handlers (interactive OAuth, clear error) still\n * run. Bounded by both `maxRetries` and a hard wall-clock cap (`maxElapsedMs`)\n * so the retry budget never blows the MCP client's own server-startup timeout.\n *\n * Mirrors the retry shape of `src/probe/index.ts`, adding exponential backoff\n * with jitter.\n */\n\nimport { isRecoverableUpstreamError } from \"./upstreamErrors.js\";\n\nexport interface ConnectRetryHooks {\n /** Decide whether an error is worth retrying. Default: recoverable transport errors. */\n isRetryable?: (error: unknown) => boolean;\n /** Injectable clock/sleep for tests. Default: real setTimeout. */\n now?: () => number;\n /** Called before each backoff sleep — used to emit a retry log event. */\n onRetry?: (info: { attempt: number; delayMs: number; error: unknown }) => void;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface ConnectRetryOptions {\n /** Delay before the first retry, in ms (grows exponentially per attempt). */\n initialDelayMs: number;\n /** Ceiling for a single backoff delay, in ms. */\n maxDelayMs: number;\n /** Hard wall-clock cap across all attempts, in ms. 0 disables the cap. */\n maxElapsedMs: number;\n /** Retries after the first attempt. 0 disables retry (single attempt). */\n maxRetries: number;\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Runs `connectFn`, retrying recoverable failures with exponential backoff +\n * jitter until it succeeds, `maxRetries` is exhausted, a non-retryable error is\n * thrown, or the `maxElapsedMs` wall-clock budget runs out. Rethrows the last\n * error on give-up so the caller's existing fatal-startup handling applies.\n */\nexport async function connectWithRetry<T>(\n connectFn: () => Promise<T>,\n options: ConnectRetryOptions,\n hooks: ConnectRetryHooks = {}\n): Promise<T> {\n const isRetryable = hooks.isRetryable ?? isRecoverableUpstreamError;\n const sleep = hooks.sleep ?? defaultSleep;\n const now = hooks.now ?? Date.now;\n\n const start = now();\n let attempt = 0;\n\n for (;;) {\n try {\n return await connectFn();\n } catch (error) {\n attempt += 1;\n if (attempt > options.maxRetries || !isRetryable(error)) {\n throw error;\n }\n\n let delayMs = backoffDelayMs(attempt, options);\n\n if (options.maxElapsedMs > 0) {\n const remaining = options.maxElapsedMs - (now() - start);\n // No budget left for another attempt after the wait — give up now.\n if (remaining <= 0) throw error;\n delayMs = Math.min(delayMs, remaining);\n }\n\n hooks.onRetry?.({ attempt, delayMs, error });\n await sleep(delayMs);\n }\n }\n}\n\n/**\n * Exponential backoff (initialDelayMs * 2^(attempt-1), capped at maxDelayMs)\n * with ±20% jitter so concurrent gateways don't retry in lockstep.\n */\nfunction backoffDelayMs(attempt: number, options: ConnectRetryOptions): number {\n const base = Math.min(options.initialDelayMs * 2 ** (attempt - 1), options.maxDelayMs);\n const jitter = base * 0.2 * (Math.random() * 2 - 1);\n return Math.max(0, Math.round(base + jitter));\n}\n","/**\n * Mid-session reconnect for the upstream MCP connection.\n *\n * The gateway connects to the upstream once and hands that client to\n * `proxyServer`, whose handlers capture it in closures. When the upstream drops\n * mid-session (a reachability flap, or a terminated/expired session) every\n * subsequent forwarded call throws forever — the session is dead until the MCP\n * client restarts the gateway process. That mid-session death is what this fixes.\n *\n * `createReconnectingClient` returns a STABLE façade with the exact method set\n * `proxyServer` uses. It holds the live client in a mutable slot; on a\n * recoverable failure it rebuilds the connection (via the injected `reconnect`\n * factory), replays the notification handlers onto the new client, swaps it in,\n * and retries the failed call — all transparently, so `proxyServer` never knows\n * the connection was replaced. Because the façade identity is stable, the\n * handler closures always hit the current client.\n *\n * Contract:\n * - Reconnect only on `isRecoverableUpstreamError` (transport failure / stale\n * session / 5xx). Never on a policy block or 401.\n * - Single-flight + generation guard: N concurrent failures trigger ONE\n * reconnect; a call that failed on an already-replaced client just retries on\n * the new one instead of reconnecting again.\n * - Cooldown collapses bursts; reconnect is driven by traffic (one attempt per\n * failing call), never a background loop — so a sustained outage self-heals on\n * the next call once connectivity returns, without hammering.\n * - Retry SAFETY is method-aware: idempotent reads retry on any recoverable\n * error; a side-effecting `tools/call` retries ONLY on a pre-dispatch error\n * (connection never established / stale-session reject), never on an ambiguous\n * mid-flight failure that may already have executed.\n *\n * The `reconnect` factory MUST NOT open an interactive OAuth browser (see\n * main.ts `allowInteractiveAuth`); a silent token refresh still happens inside\n * the transport's fetch wrapper.\n */\n\nimport type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\nimport { logger } from \"./logger.js\";\nimport { getUpstreamErrno, isPreDispatchError, isRecoverableUpstreamError } from \"./upstreamErrors.js\";\n\nexport interface ReconnectingClientOptions {\n /** Minimum gap between reconnect attempts (ms) — collapses bursts. */\n cooldownMs: number;\n /** The already-connected client to start from. */\n initial: Client;\n /** Injectable clock for tests. Defaults to Date.now. */\n now?: () => number;\n /**\n * Builds a fresh, connected client. Called on a recoverable mid-session\n * failure. MUST NOT trigger interactive OAuth (no browser mid-session).\n */\n reconnect: () => Promise<Client>;\n}\n\n/**\n * The subset of the MCP SDK `Client` that `proxyServer` forwards through. The\n * real `Client` structurally satisfies this, so the reconnecting façade can\n * stand in for it wherever `proxyServer` expects a client.\n */\nexport type UpstreamClient = Pick<\n Client,\n | \"callTool\"\n | \"close\"\n | \"complete\"\n | \"getPrompt\"\n | \"listPrompts\"\n | \"listResources\"\n | \"listResourceTemplates\"\n | \"listTools\"\n | \"notification\"\n | \"readResource\"\n | \"setNotificationHandler\"\n | \"subscribeResource\"\n | \"unsubscribeResource\"\n>;\n\ntype SetNotificationHandlerArgs = Parameters<Client[\"setNotificationHandler\"]>;\n\nexport function createReconnectingClient(options: ReconnectingClientOptions): UpstreamClient {\n const now = options.now ?? Date.now;\n\n let current = options.initial;\n // Bumped on every successful swap. A forwarded call captures the generation it\n // dispatched under; if it later fails but the generation has moved on, another\n // call already reconnected — so it just retries on the new client.\n let generation = 0;\n let lastAttemptAt = Number.NEGATIVE_INFINITY;\n let reconnecting: null | Promise<boolean> = null;\n let closed = false;\n let sawSubscribe = false;\n const notificationHandlers: SetNotificationHandlerArgs[] = [];\n\n const doReconnect = async (): Promise<boolean> => {\n logger.info(\"Upstream connection lost mid-session; reconnecting\", {\n eventType: \"mcp_upstream_reconnect_attempt\",\n });\n if (sawSubscribe) {\n // A fresh session has no subscriptions; surface it rather than silently\n // dropping resource-update delivery. (Subscription replay is a later step.)\n logger.warn(\"Reconnecting with resource subscriptions active; they will not survive the new session\", {\n eventType: \"mcp_upstream_reconnect_subscriptions_dropped\",\n });\n }\n try {\n const next = await options.reconnect();\n // close() may have run while this reconnect was in flight. Don't publish a\n // fresh client after shutdown (it would leak a live connection) — close it\n // and report failure.\n if (closed) {\n await Promise.resolve()\n .then(() => next.close())\n .catch(() => {});\n return false;\n }\n // Replay recorded notification handlers onto the new client BEFORE swapping\n // so an upstream notification arriving right after the swap is handled.\n for (const args of notificationHandlers) {\n next.setNotificationHandler(...args);\n }\n const old = current;\n current = next;\n generation += 1;\n // Close the old client after publishing the new one, so no new dispatch\n // lands on a closing connection. Best-effort; errors are irrelevant here.\n void Promise.resolve()\n .then(() => old.close())\n .catch(() => {});\n logger.info(\"Upstream reconnected\", { eventType: \"mcp_upstream_reconnect_succeeded\" });\n return true;\n } catch (error) {\n logger.warn(\"Upstream reconnect failed\", {\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_upstream_reconnect_failed\",\n });\n return false;\n }\n };\n\n const ensureReconnected = (genAtDispatch: number): Promise<boolean> => {\n // Someone already reconnected past the generation this call dispatched under.\n if (generation > genAtDispatch) return Promise.resolve(true);\n // Join an in-flight reconnect (single-flight dedup of concurrent failures).\n if (reconnecting) return reconnecting;\n // Cooldown: don't hammer a still-down upstream from a burst of failed calls.\n if (now() - lastAttemptAt < options.cooldownMs) return Promise.resolve(false);\n lastAttemptAt = now();\n reconnecting = doReconnect().finally(() => {\n reconnecting = null;\n });\n return reconnecting;\n };\n\n const forward = async <T>(idempotent: boolean, invoke: (client: Client) => Promise<T>): Promise<T> => {\n const gen = generation;\n try {\n return await invoke(current);\n } catch (error) {\n const swappedUnderUs = generation > gen;\n if (closed || (!swappedUnderUs && !isRecoverableUpstreamError(error))) throw error;\n const reconnected = await ensureReconnected(gen);\n // Retry once, on the (possibly new) current client. A side-effecting call\n // is retried only when the error proves the request never executed.\n if (reconnected && (idempotent || isPreDispatchError(error))) {\n return await invoke(current);\n }\n throw error;\n }\n };\n\n const facade = {\n callTool: (...args: Parameters<Client[\"callTool\"]>) => forward(false, (c) => c.callTool(...args)),\n close: (...args: Parameters<Client[\"close\"]>) => {\n closed = true;\n return current.close(...args);\n },\n complete: (...args: Parameters<Client[\"complete\"]>) => forward(true, (c) => c.complete(...args)),\n getPrompt: (...args: Parameters<Client[\"getPrompt\"]>) => forward(true, (c) => c.getPrompt(...args)),\n listPrompts: (...args: Parameters<Client[\"listPrompts\"]>) => forward(true, (c) => c.listPrompts(...args)),\n listResources: (...args: Parameters<Client[\"listResources\"]>) => forward(true, (c) => c.listResources(...args)),\n listResourceTemplates: (...args: Parameters<Client[\"listResourceTemplates\"]>) =>\n forward(true, (c) => c.listResourceTemplates(...args)),\n listTools: (...args: Parameters<Client[\"listTools\"]>) => forward(true, (c) => c.listTools(...args)),\n // A notification is one-way (no response). Treat it as non-idempotent so it\n // is only resent on a pre-dispatch failure (provably never sent) — never on\n // an ambiguous mid-flight failure, which could duplicate a delivered message.\n notification: (...args: Parameters<Client[\"notification\"]>) => forward(false, (c) => c.notification(...args)),\n readResource: (...args: Parameters<Client[\"readResource\"]>) => forward(true, (c) => c.readResource(...args)),\n setNotificationHandler: (...args: SetNotificationHandlerArgs) => {\n notificationHandlers.push(args);\n return current.setNotificationHandler(...args);\n },\n subscribeResource: (...args: Parameters<Client[\"subscribeResource\"]>) => {\n sawSubscribe = true;\n return forward(true, (c) => c.subscribeResource(...args));\n },\n unsubscribeResource: (...args: Parameters<Client[\"unsubscribeResource\"]>) =>\n forward(true, (c) => c.unsubscribeResource(...args)),\n };\n\n return facade as UpstreamClient;\n}\n","import { Transform } from \"node:stream\";\n\nimport { logger } from \"./lib/logger.js\";\n\nconst MAX_DROPPED_LINES_LOGGED = 50;\n\n/**\n * Filters out lines that do not start with '{' from the input stream.\n * We use this to drop anything that is obviously not a JSON-RPC message.\n */\nexport class JSONFilterTransform extends Transform {\n private buffer = \"\";\n private droppedLinesLogged = 0;\n\n constructor() {\n super({ objectMode: false });\n }\n\n _flush(callback: (error: Error | null, chunk: Buffer | null) => void) {\n // Handle any remaining data in buffer\n if (this.buffer.trim().startsWith(\"{\")) {\n callback(null, Buffer.from(this.buffer));\n } else {\n if (this.buffer.trim().length > 0) {\n this.logDroppedLine(this.buffer);\n }\n callback(null, null);\n }\n }\n\n _transform(chunk: Buffer, _encoding: string, callback: (error: Error | null, chunk: Buffer | null) => void) {\n this.buffer += chunk.toString();\n const lines = this.buffer.split(\"\\n\");\n\n // Keep the last incomplete line in the buffer\n this.buffer = lines.pop() || \"\";\n\n const jsonLines: string[] = [];\n for (const line of lines) {\n if (line.trim().startsWith(\"{\")) {\n jsonLines.push(line);\n } else if (line.trim().length > 0) {\n this.logDroppedLine(line);\n }\n }\n\n if (jsonLines.length > 0) {\n // Send filtered lines with newlines\n const output = `${jsonLines.join(\"\\n\")}\\n`;\n\n callback(null, Buffer.from(output));\n } else {\n callback(null, null);\n }\n }\n\n private logDroppedLine(line: string) {\n // Already past the suppression sentinel — stay silent.\n if (this.droppedLinesLogged > MAX_DROPPED_LINES_LOGGED) return;\n\n // First line past the cap: emit the sentinel exactly once. The increment\n // moves us into the `>` branch above, so subsequent calls fall through.\n if (this.droppedLinesLogged === MAX_DROPPED_LINES_LOGGED) {\n this.droppedLinesLogged += 1;\n logger.info(\"Suppressing further non-JSON stdout lines\", {\n eventType: \"mcp_server_stdout_dropped_suppressed\",\n loggedSoFar: MAX_DROPPED_LINES_LOGGED,\n });\n return;\n }\n\n this.droppedLinesLogged += 1;\n logger.info(\"MCP server stdout (non-JSON, dropped)\", {\n eventType: \"mcp_server_stdout_dropped\",\n line: line.length > 1024 ? `${line.slice(0, 1024)}…` : line,\n });\n }\n}\n","/**\n * Forked from https://github.com/modelcontextprotocol/typescript-sdk/blob/a1608a6513d18eb965266286904760f830de96fe/src/client/stdio.ts\n */\n\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport type { JSONRPCMessage } from \"@modelcontextprotocol/sdk/types.js\";\n\nimport {\n ReadBuffer,\n serializeMessage,\n} from \"@modelcontextprotocol/sdk/shared/stdio.js\";\nimport { type ChildProcess, type IOType, spawn, spawnSync } from \"node:child_process\";\nimport { PassThrough, type Stream } from \"node:stream\";\n\nimport { JSONFilterTransform } from \"./JSONFilterTransform.js\";\nimport { logger } from \"./lib/logger.js\";\nimport { redactCliArgs } from \"./utils/redactCliArgs.js\";\n\nexport type StdioServerParameters = {\n\t/**\n\t * Command line arguments to pass to the executable.\n\t */\n\targs?: string[];\n\n\t/**\n\t * The executable to run to start the server.\n\t */\n\tcommand: string;\n\n\t/**\n\t * The working directory to use when spawning the process.\n\t *\n\t * If not specified, the current working directory will be inherited.\n\t */\n\tcwd?: string;\n\n\t/**\n\t * The environment to use when spawning the process.\n\t *\n\t * If not specified, the result of getDefaultEnvironment() will be used.\n\t */\n\tenv: Record<string, string>;\n\n\t/**\n\t * A function to call when an event occurs.\n\t */\n\tonEvent?: (event: TransportEvent) => void;\n\n\t/**\n\t * When true, spawn the child process using the user's shell.\n\t */\n\tshell?: boolean;\n\n\t/**\n\t * How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`.\n\t *\n\t * The default is \"inherit\", meaning messages to stderr will be printed to the parent process's stderr.\n\t */\n\tstderr?: IOType | number | Stream;\n};\n\ntype TransportEvent =\n\t| {\n\t\t\tchunk: string;\n\t\t\ttype: \"data\";\n\t }\n\t| {\n\t\t\terror: Error;\n\t\t\ttype: \"error\";\n\t }\n\t| {\n\t\t\tmessage: JSONRPCMessage;\n\t\t\ttype: \"message\";\n\t }\n\t| {\n\t\t\ttype: \"close\";\n\t };\n\n/**\n * Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout.\n *\n * This transport is only available in Node.js environments.\n */\nexport class StdioClientTransport implements Transport {\n\tonclose?: () => void;\n\n\tonerror?: (error: Error) => void;\n\tonmessage?: (message: JSONRPCMessage) => void;\n\t/**\n\t * The child process pid spawned by this transport.\n\t *\n\t * This is only available after the transport has been started.\n\t */\n\tget pid(): null | number {\n\t\treturn this._process?.pid ?? null;\n\t}\n\t/**\n\t * PID of the MCP server child captured at \"spawn\" time. Unlike `pid`, this\n\t * survives close() nulling `this._process`, so session-metadata assembly can\n\t * report the child even after it exits. Undefined until spawn. On win32 with\n\t * shell:true this is the cmd wrapper the server runs under — still the direct\n\t * child the OS process tree shows between the gateway and the server.\n\t */\n\tget spawnedServerPid(): number | undefined {\n\t\treturn this._spawnedPid;\n\t}\n\t/**\n\t * Epoch-ms timestamp captured at the child's \"spawn\" event. Undefined until\n\t * spawn. Callers convert to ISO-8601 for the wire.\n\t */\n\tget spawnedServerStartedAt(): number | undefined {\n\t\treturn this._spawnedAt;\n\t}\n\t/**\n\t * The stderr stream of the child process, if `StdioServerParameters.stderr` was set to \"pipe\" or \"overlapped\".\n\t *\n\t * If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to\n\t * attach listeners before the start method is invoked. This prevents loss of any early\n\t * error output emitted by the child process.\n\t */\n\tget stderr(): null | Stream {\n\t\tif (this._stderrStream) {\n\t\t\treturn this._stderrStream;\n\t\t}\n\n\t\treturn this._process?.stderr ?? null;\n\t}\n\tprivate _abortController: AbortController = new AbortController();\n\tprivate _process?: ChildProcess;\n\tprivate _readBuffer: ReadBuffer = new ReadBuffer();\n\tprivate _serverParams: StdioServerParameters;\n\tprivate _spawnedAt?: number;\n\tprivate _spawnedPid?: number;\n\tprivate _stderrStream: null | PassThrough = null;\n\n\tprivate onEvent?: (event: TransportEvent) => void;\n\n\tconstructor(server: StdioServerParameters) {\n\t\tthis._serverParams = server;\n\t\tif (server.stderr === \"pipe\" || server.stderr === \"overlapped\") {\n\t\t\tthis._stderrStream = new PassThrough();\n\t\t}\n\t\tthis.onEvent = server.onEvent;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.onEvent?.({\n\t\t\ttype: \"close\",\n\t\t});\n\n\t\t// Kill the entire process tree to avoid orphaned grandchildren.\n\t\t// AbortController only kills the direct child, but MCP servers\n\t\t// may spawn subprocesses (workers, file watchers, etc.) that\n\t\t// would otherwise be left running as orphans.\n\t\tif (this._process?.pid) {\n\t\t\ttry {\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\t// Windows doesn't support negative-PID process group kills.\n\t\t\t\t\t// Use taskkill /T to terminate the entire process tree.\n\t\t\t\t\tconst result = spawnSync(\"taskkill\", [\"/PID\", String(this._process.pid), \"/T\", \"/F\"], {\n\t\t\t\t\t\tstdio: \"ignore\",\n\t\t\t\t\t});\n\t\t\t\t\tif (result.error) {\n\t\t\t\t\t\tthrow result.error;\n\t\t\t\t\t}\n\t\t\t\t\tif (result.status !== 0) {\n\t\t\t\t\t\tthrow new Error(`taskkill exited with status ${result.status}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.kill(-this._process.pid, \"SIGTERM\");\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tlogger.debug(\"Process tree cleanup failed (process may already be dead)\", {\n\t\t\t\t\terror: (error as Error).message,\n\t\t\t\t\tpid: this._process.pid,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tthis._abortController.abort();\n\t\tthis._process = undefined;\n\t\tthis._readBuffer.clear();\n\t}\n\n\tsend(message: JSONRPCMessage): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (!this._process?.stdin) {\n\t\t\t\tthrow new Error(\"Not connected\");\n\t\t\t}\n\n\t\t\tconst json = serializeMessage(message);\n\t\t\tif (this._process.stdin.write(json)) {\n\t\t\t\tresolve();\n\t\t\t} else {\n\t\t\t\tthis._process.stdin.once(\"drain\", resolve);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Starts the server process and prepares to communicate with it.\n\t */\n\tasync start(): Promise<void> {\n\t\tif (this._process) {\n\t\t\tthrow new Error(\n\t\t\t\t\"StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.\",\n\t\t\t);\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst useShell = this._serverParams.shell ?? false;\n\t\t\t// Node's spawn with shell:true on Windows runs\n\t\t\t// `cmd.exe /d /s /c \"<command> <arg1> <arg2>...\"` by joining the\n\t\t\t// command + args array with single spaces, with no per-token\n\t\t\t// escaping. Any segment containing whitespace (e.g. a path like\n\t\t\t// `C:\\Users\\Foo\\OneDrive - Acme Corp\\bin\\python.exe`) is then\n\t\t\t// re-split by cmd.exe at every space — the child sees a\n\t\t\t// truncated command (`C:\\Users\\Foo\\OneDrive`) plus a few stray\n\t\t\t// tokens and dies with \"not recognized as an internal or\n\t\t\t// external command\" (or, for `python <prefix> ...`, Python\n\t\t\t// follows the first arg as a script directory and reports\n\t\t\t// `can't find '__main__' module in '...'`). Pre-quote each\n\t\t\t// segment containing whitespace or cmd.exe metacharacters so\n\t\t\t// the joined command line round-trips through the parser.\n\t\t\tconst shouldQuote = useShell && process.platform === \"win32\";\n\t\t\tconst spawnCommand = shouldQuote\n\t\t\t\t? quoteForWindowsShell(this._serverParams.command)\n\t\t\t\t: this._serverParams.command;\n\t\t\tconst spawnArgs = shouldQuote\n\t\t\t\t? (this._serverParams.args ?? []).map(quoteForWindowsShell)\n\t\t\t\t: this._serverParams.args ?? [];\n\n\t\t\tthis._process = spawn(\n\t\t\t\tspawnCommand,\n\t\t\t\tspawnArgs,\n\t\t\t\t{\n\t\t\t\t\tcwd: this._serverParams.cwd,\n\t\t\t\t\t// On POSIX, detached creates a new process group (setsid) so we\n\t\t\t\t\t// can kill the entire tree with kill(-pid). On Windows, detached\n\t\t\t\t\t// breaks the child out of the parent's job object, which would\n\t\t\t\t\t// prevent automatic cleanup if the parent crashes — so we skip it.\n\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\tenv: this._serverParams.env,\n\t\t\t\t\tshell: useShell,\n\t\t\t\t\tsignal: this._abortController.signal,\n\t\t\t\t\tstdio: [\"pipe\", \"pipe\", this._serverParams.stderr ?? \"inherit\"],\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tthis._process.on(\"error\", (error) => {\n\t\t\t\tif (error.name === \"AbortError\") {\n\t\t\t\t\t// Expected when close() is called.\n\t\t\t\t\tthis.onclose?.();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlogger.error(\"Process spawn error\", {\n\t\t\t\t\targs: this._serverParams.args ? redactCliArgs(this._serverParams.args) : undefined,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\tcwd: this._serverParams.cwd,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\terrorCode: (error as NodeJS.ErrnoException).code,\n\t\t\t\t\terrorErrno: (error as NodeJS.ErrnoException).errno,\n\t\t\t\t\terrorSyscall: (error as NodeJS.ErrnoException).syscall,\n\t\t\t\t\toperation: \"spawn\",\n\t\t\t\t\tshell: this._serverParams.shell,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\treject(error);\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\t// Capture pid + spawn time at \"spawn\" time so they remain available in\n\t\t\t// the \"close\" handler. close() nulls `this._process` synchronously\n\t\t\t// before the OS fires the close event, so reading `this._process?.pid`\n\t\t\t// from the close handler would always return undefined on that path.\n\t\t\tlet spawnedAt: number | undefined;\n\t\t\tlet spawnedPid: number | undefined;\n\t\t\tthis._process.on(\"spawn\", () => {\n\t\t\t\tspawnedAt = Date.now();\n\t\t\t\tspawnedPid = this._process?.pid;\n\t\t\t\t// Mirror onto the instance so session-metadata assembly can read the\n\t\t\t\t// spawned child via the public getters.\n\t\t\t\tthis._spawnedAt = spawnedAt;\n\t\t\t\tthis._spawnedPid = spawnedPid;\n\t\t\t\t// Log argCount, not args — MCP servers commonly take secrets as\n\t\t\t\t// CLI args (--api-key, --token, etc.) and these logs land in the\n\t\t\t\t// file transport and remote ingest endpoint.\n\t\t\t\tlogger.info(\"MCP server process spawned\", {\n\t\t\t\t\targCount: this._serverParams.args?.length,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\teventType: \"mcp_server_process_spawned\",\n\t\t\t\t\tpid: spawnedPid,\n\t\t\t\t});\n\t\t\t\tresolve();\n\t\t\t});\n\n\t\t\tthis._process.on(\"close\", (code, signal) => {\n\t\t\t\tconst uptimeMs = spawnedAt === undefined ? undefined : Date.now() - spawnedAt;\n\t\t\t\tconst cleanExit = code === 0 && signal === null;\n\t\t\t\tconst logLevel = cleanExit ? \"info\" : \"warn\";\n\t\t\t\tlogger[logLevel](\"MCP server process closed\", {\n\t\t\t\t\targCount: this._serverParams.args?.length,\n\t\t\t\t\tcode,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\teventType: \"mcp_server_process_closed\",\n\t\t\t\t\tpid: spawnedPid,\n\t\t\t\t\tsignal,\n\t\t\t\t\tuptimeMs,\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\ttype: \"close\",\n\t\t\t\t});\n\n\t\t\t\tthis._process = undefined;\n\t\t\t\tthis.onclose?.();\n\t\t\t});\n\n\t\t\tthis._process.stdin?.on(\"error\", (error) => {\n\t\t\t\tlogger.error(\"Stdin error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\toperation: \"write\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\tconst jsonFilterTransform = new JSONFilterTransform();\n\n\t\t\tthis._process.stdout?.pipe(jsonFilterTransform);\n\n\t\t\tjsonFilterTransform.on(\"data\", (chunk) => {\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\tchunk: chunk.toString(),\n\t\t\t\t\ttype: \"data\",\n\t\t\t\t});\n\n\t\t\t\tthis._readBuffer.append(chunk);\n\t\t\t\tthis.processReadBuffer();\n\t\t\t});\n\n\t\t\tjsonFilterTransform.on(\"error\", (error) => {\n\t\t\t\tlogger.error(\"JSON filter error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\toperation: \"parse\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\tif (this._stderrStream && this._process.stderr) {\n\t\t\t\tthis._process.stderr.pipe(this._stderrStream);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate processReadBuffer() {\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\tconst message = this._readBuffer.readMessage();\n\n\t\t\t\tif (message === null) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\tmessage,\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t});\n\n\t\t\t\tthis.onmessage?.(message);\n\t\t\t} catch (error) {\n\t\t\t\t// Try to read message for context (may fail)\n\t\t\t\tlet messageType: string = \"unknown\";\n\t\t\t\ttry {\n\t\t\t\t\tconst msg = this._readBuffer.readMessage();\n\t\t\t\t\tif (msg) {\n\t\t\t\t\t\tmessageType = typeof msg;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore error when trying to read message for context\n\t\t\t\t}\n\n\t\t\t\tlogger.error(\"Message processing error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: (error as Error).message,\n\t\t\t\t\tmessageType,\n\t\t\t\t\toperation: \"process\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror: error as Error,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error as Error);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Quote a token for Node's `spawn(..., { shell: true })` on Windows.\n *\n * Returns `s` unchanged when no quoting is needed — preserves the pre-fix\n * shape of the command line for common cases (`npx`, `python`, `node`, etc.),\n * so existing installs aren't re-quoted unnecessarily. Wraps in `\"...\"` and\n * doubles embedded quotes (cmd.exe convention inside a quoted token) when\n * the token contains whitespace or any cmd.exe metacharacter.\n *\n * Exported for the unit test; production code reaches this through\n * `StdioClientTransport.start()`.\n */\nexport function quoteForWindowsShell(s: string): string {\n\tif (s.length === 0) return s;\n\tif (!/[\\s\"&|<>^()]/.test(s)) return s;\n\t// Double embedded quotes (cmd.exe convention inside a quoted token), then\n\t// double any run of trailing backslashes. Under the Windows/MSVC argv rules\n\t// (CommandLineToArgvW), backslashes are only special immediately before a\n\t// quote: an odd count escapes the closing `\"`, so a token ending in `\\` like\n\t// `C:\\Path\\` would otherwise corrupt argv. Doubling the trailing run makes the\n\t// count even, preserving the backslash and terminating the quote correctly\n\t// (e.g. `C:\\Path\\` -> `\"C:\\Path\\\\\"`).\n\tconst escaped = s.replace(/\"/g, '\"\"').replace(/(\\\\+)$/, \"$1$1\");\n\treturn `\"${escaped}\"`;\n}\n","/**\n * OCI image-reference validation for container MCP images.\n *\n * DEFENSE IN DEPTH — not the primary control. The primary control is that the\n * digest lookup passes the image reference as a single argv element to the\n * container runtime (`execFileSync`, no shell — see\n * {@link ./dockerImageDigest.ts}), so no metacharacter can be interpreted by a\n * shell in the first place. This validator additionally refuses to hand a\n * reference that is not a well-formed OCI image reference to the runtime at all,\n * so a malformed/hostile value is rejected and logged before any local process\n * is spawned.\n *\n * The grammar mirrors the OCI distribution spec (the same one\n * `github.com/distribution/reference` implements):\n *\n * reference := name [ \":\" tag ] [ \"@\" digest ]\n * name := [domain \"/\"] path-component [\"/\" path-component]*\n * domain := host [\":\" port]\n * host := domain-name | IPv4 | \"[\" IPv6 \"]\"\n * path-component := [a-z0-9]+ (separator [a-z0-9]+)*\n * separator := \"_\" | \"__\" | \".\" | \"-\"+\n * tag := [A-Za-z0-9_][A-Za-z0-9_.-]{0,127}\n * digest := algorithm \":\" hex\n */\n\n/** The outcome of validating an image reference. `reason` is set iff invalid. */\nexport interface ImageReferenceCheck {\n /** Bounded, log-safe label naming why the reference was rejected. */\n reason?: string;\n valid: boolean;\n}\n\n/**\n * Longest reference we accept. The OCI spec caps a repository name at 255\n * characters; 512 leaves room for a registry host, a tag and a digest while\n * still bounding what reaches the runtime (and the log line).\n */\nconst MAX_REFERENCE_LENGTH = 512;\n\n/**\n * Characters that are meaningful to a POSIX or Windows shell. Used ONLY to\n * describe why a reference the grammar already rejected is dangerous, so the log\n * line names the metacharacter instead of a generic grammar failure.\n */\nconst SHELL_METACHARACTERS = [\n \"$\",\n \"`\",\n \";\",\n \"|\",\n \"&\",\n \">\",\n \"<\",\n \"(\",\n \")\",\n \"{\",\n \"}\",\n \"'\",\n '\"',\n \"\\\\\",\n \"!\",\n \"*\",\n \"?\",\n \"~\",\n \"#\",\n] as const;\n\nconst ALPHA_NUMERIC = \"[a-z0-9]+\";\nconst SEPARATOR = \"(?:[._]|__|-+)\";\nconst PATH_COMPONENT = `${ALPHA_NUMERIC}(?:${SEPARATOR}${ALPHA_NUMERIC})*`;\nconst DOMAIN_COMPONENT = \"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\";\nconst DOMAIN_NAME = `${DOMAIN_COMPONENT}(?:\\\\.${DOMAIN_COMPONENT})*`;\nconst IPV6_HOST = \"\\\\[[a-fA-F0-9:]+\\\\]\";\nconst DOMAIN = `(?:${DOMAIN_NAME}|${IPV6_HOST})(?::[0-9]+)?`;\nconst NAME = `(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*`;\nconst TAG = \"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}\";\nconst DIGEST = \"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[0-9a-fA-F]{32,}\";\n\nconst IMAGE_REFERENCE = new RegExp(`^${NAME}(?::${TAG})?(?:@${DIGEST})?$`);\n\n/**\n * Validate a container image reference against the OCI image-reference grammar.\n *\n * @param imageName - The raw, untrusted image reference from the MCP server config\n * @returns `{ valid: true }`, or `{ valid: false, reason }` naming the defect\n */\nexport function validateImageReference(imageName: string): ImageReferenceCheck {\n if (typeof imageName !== \"string\" || imageName.trim() === \"\") {\n return { reason: \"empty image reference\", valid: false };\n }\n\n if (imageName.length > MAX_REFERENCE_LENGTH) {\n return { reason: `image reference longer than ${MAX_REFERENCE_LENGTH} characters`, valid: false };\n }\n\n if (!IMAGE_REFERENCE.test(imageName)) {\n return { reason: describeDefect(imageName), valid: false };\n }\n\n return { valid: true };\n}\n\n/**\n * Describe why a reference that FAILED the grammar is malformed. Diagnostic\n * only — the grammar above is the single authority on validity, so this can\n * never widen what is accepted.\n *\n * @param imageName - A reference already known to be invalid\n * @returns A bounded, log-safe reason that never embeds the reference itself\n */\nfunction describeDefect(imageName: string): string {\n // eslint-disable-next-line no-control-regex -- deliberately matching C0 control characters\n if (/[\\u0000-\\u001f\\u007f]/.test(imageName)) {\n return \"contains a control character\";\n }\n\n if (/\\s/.test(imageName)) {\n return \"contains whitespace\";\n }\n\n const metacharacter = SHELL_METACHARACTERS.find((character) => imageName.includes(character));\n if (metacharacter) {\n return `contains the shell metacharacter ${metacharacter}`;\n }\n\n // An argv element starting with \"-\" is read as an option by the runtime CLI,\n // never as an image name.\n if (imageName.startsWith(\"-\")) {\n return \"starts with '-' and would be parsed as a runtime option\";\n }\n\n return \"does not match the OCI image-reference grammar\";\n}\n","import { execFileSync } from \"node:child_process\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { validateImageReference } from \"./imageReference.js\";\n\n/**\n * A container-based MCP command, as parsed from the CLI arguments.\n *\n * Deliberately carries NO image digests: resolving those invokes the container\n * runtime, which the gateway must not do until the access-control decision has\n * allowed the server. Detection is pure string parsing and safe to run at startup;\n * resolution is {@link getContainerImageDigests}, called after the gate. The Go\n * twin (scanner/internal/container/container.go) keeps them in one struct because\n * the scanner has no access-control gate to run first.\n */\nexport interface ContainerMCPCommand {\n /**\n * The image name/tag used in the command. Untrusted: it comes verbatim from\n * the MCP server config and is validated before it reaches the runtime.\n */\n imageName: string;\n /**\n * Whether the command is a container MCP (docker, podman, etc.)\n */\n isContainerMCP: boolean;\n /**\n * The detected container runtime (docker, podman, nerdctl, finch, etc.)\n */\n runtime: ContainerRuntime;\n}\n\n/**\n * Supported container runtimes\n */\nexport type ContainerRuntime = \"docker\" | \"finch\" | \"nerdctl\" | \"podman\" | \"unknown\";\n\n/**\n * Detect the container runtime from a command string.\n *\n * Supports:\n * - docker / docker.exe\n * - podman / podman.exe\n * - nerdctl / nerdctl.exe\n * - finch / finch.exe\n *\n * @param command - The executable command\n * @returns The detected container runtime or \"unknown\"\n */\nexport function detectContainerRuntime(command: string): ContainerRuntime {\n // Normalize command to lowercase for comparison\n const normalizedCommand = command.toLowerCase();\n\n // Get the base command name (without path)\n const baseName = normalizedCommand.split(/[/\\\\]/).pop() || \"\";\n\n // Check for known container runtimes\n if (baseName === \"docker\" || baseName === \"docker.exe\") {\n return \"docker\";\n }\n if (baseName === \"podman\" || baseName === \"podman.exe\") {\n return \"podman\";\n }\n if (baseName === \"nerdctl\" || baseName === \"nerdctl.exe\") {\n return \"nerdctl\";\n }\n if (baseName === \"finch\" || baseName === \"finch.exe\") {\n return \"finch\";\n }\n\n return \"unknown\";\n}\n\n/**\n * Extract the image name from container run arguments.\n *\n * Docker/Podman/nerdctl run syntax: <runtime> run [OPTIONS] IMAGE [COMMAND] [ARG...]\n *\n * The image is the first argument after all options (which start with - or --).\n * Some options take values: -e, --env, -v, --volume, -p, --publish, etc.\n *\n * @param args - The run arguments (after \"<runtime> run\")\n * @returns The image name or undefined if not found\n */\nexport function extractContainerImageName(args: string[]): string | undefined {\n // Find the index of \"run\" in args\n const runIndex = args.indexOf(\"run\");\n if (runIndex === -1) {\n return undefined;\n }\n\n // Args after \"run\"\n const runArgs = args.slice(runIndex + 1);\n\n // Options that take a value (the next arg is not the image)\n // These are common across Docker, Podman, nerdctl, and finch\n // Sorted alphabetically for linter\n const optionsWithValue = new Set([\n \"--add-host\",\n \"--annotation\",\n \"--arch\",\n \"--attach\",\n \"--blkio-weight\",\n \"--blkio-weight-device\",\n \"--cap-add\",\n \"--cap-drop\",\n \"--cgroup-parent\",\n \"--cgroupns\",\n \"--cidfile\",\n \"--cosign-key\",\n \"--cpu-period\",\n \"--cpu-quota\",\n \"--cpu-rt-period\",\n \"--cpu-rt-runtime\",\n \"--cpu-shares\",\n \"--cpus\",\n \"--cpuset-cpus\",\n \"--cpuset-mems\",\n \"--decryption-key\",\n \"--device\",\n \"--device-cgroup-rule\",\n \"--device-read-bps\",\n \"--device-read-iops\",\n \"--device-write-bps\",\n \"--device-write-iops\",\n \"--dns\",\n \"--dns-option\",\n \"--dns-search\",\n \"--domainname\",\n \"--entrypoint\",\n \"--env\",\n \"--env-file\",\n \"--env-host\",\n \"--expose\",\n \"--gidmap\",\n \"--gpus\",\n \"--group-add\",\n \"--health-cmd\",\n \"--health-interval\",\n \"--health-retries\",\n \"--health-start-period\",\n \"--health-timeout\",\n \"--hostname\",\n \"--image-volume\",\n \"--ip\",\n \"--ip6\",\n \"--ipc\",\n \"--isolation\",\n \"--kernel-memory\",\n \"--label\",\n \"--label-file\",\n \"--link\",\n \"--link-local-ip\",\n \"--log-driver\",\n \"--log-opt\",\n \"--mac-address\",\n \"--memory\",\n \"--memory-reservation\",\n \"--memory-swap\",\n \"--memory-swappiness\",\n \"--mount\",\n \"--name\",\n \"--network\",\n \"--network-alias\",\n \"--oom-kill-disable\",\n \"--oom-score-adj\",\n \"--os\",\n \"--pid\",\n \"--pidfile\",\n \"--pids-limit\",\n \"--platform\",\n \"--pod\",\n \"--privileged\",\n \"--publish\",\n \"--publish-all\",\n \"--pull\",\n \"--requires\",\n \"--restart\",\n \"--rootfs\",\n \"--runtime\",\n \"--secret\",\n \"--security-opt\",\n \"--shm-size\",\n \"--stop-signal\",\n \"--stop-timeout\",\n \"--storage-opt\",\n \"--subgidname\",\n \"--subuidname\",\n \"--sysctl\",\n \"--systemd\",\n \"--timeout\",\n \"--tmpfs\",\n \"--tz\",\n \"--uidmap\",\n \"--ulimit\",\n \"--user\",\n \"--userns\",\n \"--uts\",\n \"--variant\",\n \"--volume\",\n \"--volume-driver\",\n \"--volumes-from\",\n \"--workdir\",\n \"-a\",\n \"-c\",\n \"-e\",\n \"-h\",\n \"-l\",\n \"-m\",\n \"-p\",\n \"-P\",\n \"-u\",\n \"-v\",\n \"-w\",\n ]);\n\n let i = 0;\n while (i < runArgs.length) {\n const arg = runArgs[i];\n\n // Skip options\n if (arg.startsWith(\"-\")) {\n // Check if this option takes a value\n // Handle --option=value format\n if (arg.includes(\"=\")) {\n i++;\n continue;\n }\n\n // Check if the option (without leading dashes) is in our set\n if (optionsWithValue.has(arg)) {\n // Skip this option and its value\n i += 2;\n continue;\n }\n\n // Boolean option, just skip it\n i++;\n continue;\n }\n\n // This is the image name\n return arg;\n }\n\n return undefined;\n}\n\n/** Bound on how long the runtime's `image inspect` may take. */\nconst DIGEST_LOOKUP_TIMEOUT_MS = 5000;\n\n/**\n * Detect whether a command runs a container (Docker, Podman, nerdctl, finch) and\n * extract the image reference from its arguments.\n *\n * PURE string parsing: it spawns no process and touches no shell, so it is safe\n * to run before the access-control decision. Resolving the digests — the step\n * that invokes the container runtime — is {@link getContainerImageDigests}, which\n * runs only after the server has been allowed.\n *\n * @param command - The executable command\n * @param args - Command arguments\n * @returns The detected runtime and (untrusted) image reference\n */\nexport function detectContainerMCP(command: string, args?: string[]): ContainerMCPCommand {\n const runtime = detectContainerRuntime(command);\n\n if (runtime === \"unknown\") {\n return {\n imageName: \"\",\n isContainerMCP: false,\n runtime: \"unknown\",\n };\n }\n\n // Check if there's a \"run\" subcommand in the args\n if (!args || !args.includes(\"run\")) {\n return {\n imageName: \"\",\n isContainerMCP: false,\n runtime,\n };\n }\n\n const imageName = extractContainerImageName(args);\n\n if (!imageName) {\n logger.debug(\"Container command detected but could not extract image name\", {\n args,\n command,\n runtime,\n });\n return {\n imageName: \"\",\n isContainerMCP: true,\n runtime,\n };\n }\n\n return {\n imageName,\n isContainerMCP: true,\n runtime,\n };\n}\n\n/**\n * Get all container image digests using the appropriate runtime's inspect command.\n *\n * The digests are sha256 hashes of the image manifest, which are consistent across\n * registries and uniquely identify specific image versions.\n * An image may have multiple digests if it exists in multiple registries.\n *\n * SECURITY: `imageName` is attacker-controllable — it comes from a (possibly\n * source-controlled) MCP server config. It is therefore\n * 1. validated against the OCI image-reference grammar and skipped-with-a-log if\n * it does not match, and\n * 2. passed to the runtime as a single argv element via `execFileSync` — NEVER\n * interpolated into a shell command string. `execSync` always spawns\n * `/bin/sh -c`, where `$(...)` and backticks stay live inside double quotes, so\n * a crafted reference could execute arbitrary commands as the local user.\n * This mirrors the Go twin's argv call (scanner/internal/container/container.go).\n * This function MUST only be called after the access-control decision (main.ts):\n * it runs a local binary on a value the MCP config controls, so a server the\n * policy denies must never reach it.\n *\n * @param imageName - The untrusted image name/tag to inspect\n * @param runtime - The container runtime to use\n * @returns Array of image digests (sha256 hashes), empty if rejected, not found or locally built\n */\nexport function getContainerImageDigests(imageName: string, runtime: ContainerRuntime): string[] {\n // All supported runtimes use the same inspect syntax\n const runtimeCommand = runtime === \"unknown\" ? \"docker\" : runtime;\n\n const check = validateImageReference(imageName);\n if (!check.valid) {\n // A deliberate skip is an outcome someone has to be able to see: log it at warn\n // with the offending reference. The session/machine ids and the trace id ride on\n // every line via the logger's session context.\n logger.warn(\"Rejected container image reference; skipping digest lookup\", {\n eventType: \"mcp_container_image_ref_rejected\",\n imageName,\n reason: check.reason,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n let result: string;\n try {\n // Get RepoDigests as JSON array - this contains the manifest digests\n // Format: [\"repo@sha256:abc123...\", \"other-repo@sha256:def456...\"] or [] if image was built locally\n result = execFileSync(\n runtimeCommand,\n [\"image\", \"inspect\", \"--format\", \"{{json .RepoDigests}}\", imageName],\n {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: DIGEST_LOOKUP_TIMEOUT_MS,\n }\n );\n } catch (error) {\n // Image not found locally, or runtime not available\n logger.warn(\"Container image digest lookup failed\", {\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n const trimmedResult = result.trim();\n\n // Parse the JSON array of digests. The lookup must stay TOTAL: whatever the\n // runtime prints, it degrades to \"no digests\" and never throws — a digest is\n // metadata, so a surprising `image inspect` output must not take the MCP server\n // down with it. Note a Go nil slice marshals to `null`, not `[]`, and\n // `--format '{{json …}}'` does print `null` for a nil field (docker 29.6.2 on\n // `.Config.Entrypoint`); whether RepoDigests is nil or empty is a daemon\n // implementation detail, so `null` is treated as \"none\", not iterated.\n let parsedResult: unknown;\n try {\n parsedResult = JSON.parse(trimmedResult);\n } catch {\n logger.warn(\"Could not parse RepoDigests JSON\", {\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n result: trimmedResult,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n if (!Array.isArray(parsedResult)) {\n logger.warn(\"RepoDigests was not a JSON array\", {\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n result: trimmedResult,\n runtime: runtimeCommand,\n });\n return [];\n }\n const repoDigests: unknown[] = parsedResult;\n\n // Extract sha256 digests from all entries\n // Format is typically: \"repo@sha256:abc123...\" or just \"sha256:...\"\n const digests: string[] = [];\n for (const repoDigest of repoDigests) {\n if (typeof repoDigest !== \"string\") {\n continue;\n }\n // Extract just the sha256:... portion\n const digestMatch = repoDigest.match(/sha256:[a-f0-9]{64}/i);\n if (digestMatch) {\n digests.push(digestMatch[0]);\n }\n }\n\n if (digests.length > 0) {\n logger.info(\"Container image digests resolved\", {\n digests,\n eventType: \"mcp_container_image_digests_resolved\",\n imageName,\n runtime: runtimeCommand,\n });\n return digests;\n }\n\n // No digest available (likely a locally built image that was never pushed)\n logger.info(\"No container image digests available (image may be locally built)\", {\n eventType: \"mcp_container_image_digests_empty\",\n imageName,\n runtime: runtimeCommand,\n });\n return [];\n}\n","import { StringDecoder } from \"node:string_decoder\";\nimport { clearInterval, setInterval, setTimeout } from \"node:timers\";\nimport util from \"node:util\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { type ServerCapabilities } from \"@modelcontextprotocol/sdk/types.js\";\nimport { EventSource } from \"eventsource\";\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\n\nimport { AccessControlAuthorizer } from \"../accessControl/authorizer.js\";\nimport { AccessControlClient } from \"../accessControl/client.js\";\nimport { OAuthCredentialStore, performOAuthFlow } from \"../auth/index.js\";\nimport { discoverAuthServer } from \"../auth/discovery.js\";\nimport { createAuthenticatingFetch, createDynamicAuth, type DynamicAuthHandle } from \"../auth/dynamicAuth.js\";\nimport { initializeOAuthProviders } from \"../auth/oauthProvidersClient.js\";\nimport { config } from \"../config/env.js\";\nimport { AccessControlBlockError, ConfigurationError, ProxyConnectionError } from \"../lib/errors.js\";\nimport { respondInitializeError } from \"../lib/initializeErrorResponder.js\";\nimport { emitKeychainOutcomeLog } from \"../lib/keychainOutcome.js\";\nimport { extractTraceIdFromHeaders, flushLogs, logger } from \"../lib/logger.js\";\nimport { emitProxySetupLog } from \"../lib/proxySetup.js\";\nimport { initializeTrafficMirror } from \"../lib/trafficMirror.js\";\n\n// The live stdio MCP-server transport, registered at creation so the traffic\n// mirror's per-event metadata can report the spawned child's pid + start time.\n// Reconnects re-register, so a respawned server's new pid is picked up on the\n// next scan. Undefined for remote proxies (no spawned child).\nlet activeStdioTransport: StdioClientTransport | undefined;\n\nconst spawnedServerInfoProvider = () => ({\n pid: activeStdioTransport?.spawnedServerPid,\n startedAtMs: activeStdioTransport?.spawnedServerStartedAt,\n});\nimport { connectWithRetry } from \"../lib/connectWithRetry.js\";\nimport { createReconnectingClient, type UpstreamClient } from \"../lib/reconnectingClient.js\";\nimport { connectWithTransportFallback, type TransportType } from \"../lib/transportDetection.js\";\nimport { getUpstreamErrno, is401Error } from \"../lib/upstreamErrors.js\";\nimport { logRequestCounts, proxyServer } from \"../proxyServer.js\";\nimport { StdioClientTransport } from \"../StdioClientTransport.js\";\nimport { startHTTPServer } from \"../startHTTPServer.js\";\nimport { type ContainerMCPCommand, detectContainerMCP, getContainerImageDigests } from \"../utils/dockerImageDigest.js\";\nimport { getSessionData, type RemoteSessionCliArgs, type StdioSessionCliArgs } from \"../utils/getUserData.js\";\nimport { normalizeUrl } from \"../utils/normalizeUrl.js\";\nimport { redactCliArgs } from \"../utils/redactCliArgs.js\";\n\nutil.inspect.defaultOptions.depth = 8;\n\nif (!(\"EventSource\" in global)) {\n // @ts-expect-error - figure out how to use --experimental-eventsource with vitest\n global.EventSource = EventSource;\n}\n\n// Export ProxyType for use by other modules\nexport type ProxyType = \"remote\" | \"stdio\";\n\n// sysexits EX_CONFIG (78): a required auth env var resolved empty/unset. Distinct\n// from the generic exit 1 so wrappers and parent agents can tell a configuration\n// error apart from a crash.\nconst EXIT_AUTH_CONFIG = 78;\n\n// PRDCT-6638: make a startup failure visible to ALL three audiences instead of\n// the silent process.exit() that the parent agent only sees as\n// \"-32000: Connection closed\":\n// - the parent AGENT (OpenCode/Cursor/Claude Code) — reply to its `initialize`\n// with a JSON-RPC error whose message is `reason`, so it shows the specific\n// cause instead of -32000 (see lib/initializeErrorResponder). Gated on\n// `respondToClient`, which the caller sets only in stdio mode with the real\n// Server transport NOT yet connected, so we never fight it for stdin;\n// - GROUNDCOVER (+ the local file) — logger.error then flushLogs() BEFORE exit,\n// because process.exit otherwise drops the async transports;\n// - a developer running the gateway directly — an actionable stderr line.\n// keepAlive: flushLogs' timers are .unref()'d and there may be no other pending\n// I/O at this pre-connect exit, so without a ref'd timer Node would drain and\n// exit 13 (unsettled top-level await) before the flush completes. A flush failure\n// must not bypass the intended exit code.\nconst failStartupVisibly = async (opts: {\n exitCode: number;\n logMessage: string;\n meta: Record<string, unknown>;\n reason: string;\n respondToClient: boolean;\n stderrLine: string;\n}): Promise<never> => {\n logger.error(opts.logMessage, opts.meta);\n // Await the stderr write so it drains BEFORE process.exit: writes to a pipe/TTY\n // are async on POSIX, so an un-awaited write can be truncated on exit.\n await new Promise<void>((resolve) => {\n process.stderr.write(`${opts.stderrLine}\\n`, () => resolve());\n });\n if (opts.respondToClient) {\n await respondInitializeError(opts.reason);\n }\n const keepAlive = setInterval(() => {}, 50);\n try {\n await flushLogs(2000);\n } catch {\n // still fail loud with opts.exitCode rather than a generic unhandled rejection\n } finally {\n clearInterval(keepAlive);\n }\n process.exit(opts.exitCode);\n};\n\n// A synthetic auth env var (the scanner lifts a remote server's header value into\n// MCP_GATEWAY_HEADER_*) resolved empty/unset.\nconst exitAuthEnvMissing = (args: {\n envVar: string;\n eventType: string;\n header?: string;\n logMessage: string;\n respondToClient: boolean;\n}): Promise<never> => {\n const target = args.header ? `HTTP header \"${args.header}\"` : \"the bearer token\";\n const reason =\n `Onyx MCP gateway: required auth env var \"${args.envVar}\" for ${target} is empty or unset. ` +\n `Export the source value your agent config references (e.g. the {env:...} / \\${...} token) in the launch environment.`;\n return failStartupVisibly({\n exitCode: EXIT_AUTH_CONFIG,\n logMessage: args.logMessage,\n meta: { envVar: args.envVar, eventType: args.eventType, ...(args.header ? { header: args.header } : {}) },\n reason,\n respondToClient: args.respondToClient,\n stderrLine: `[mcp-gateway] FATAL: ${reason}`,\n });\n};\n\nexport const main = async () => {\n // Emit the proxy-dispatcher install/failure record captured during phase 0\n // (see lib/proxySetup.ts). Done here, not at the install site, so it goes\n // through the structured logger (file + ingest) instead of stdio.\n emitProxySetupLog(logger);\n\n // Same deal for the startup api-key resolution outcome captured during\n // phase 2b (see lib/keychainOutcome.ts): re-emit it here so the reader-side\n // `keychain_outcome` event reaches groundcover via the ingest transport,\n // instead of dying on the pre-logger stderr write.\n emitKeychainOutcomeLog(logger);\n\n const argv = await yargs(hideBin(process.argv))\n .scriptName(\"mcp-gateway\")\n .command(\"stdio [command] [args...]\", \"Run a local command with stdio transport (default)\", (yargs) => {\n return yargs\n .positional(\"command\", {\n describe: \"The command to run\",\n type: \"string\",\n })\n .positional(\"args\", {\n array: true,\n describe: \"The arguments to pass to the command\",\n type: \"string\",\n });\n })\n .command(\"remote\", \"Connect to a remote MCP server\", (yargs) => {\n return yargs.options({\n \"bearer-token-env\": {\n describe:\n \"Environment variable name containing a bearer token for remote authentication\",\n type: \"string\",\n },\n \"env-http-headers\": {\n describe:\n \"HTTP headers resolved from environment variables (format: HeaderName:ENV_VAR_NAME)\",\n string: true,\n type: \"array\",\n },\n headers: {\n deprecated: \"Use --env-http-headers with env var names instead. --headers exposes values in process listings.\",\n describe: \"Legacy: headers with literal values (format: key:value)\",\n string: true,\n type: \"array\",\n },\n \"remote-transport\": {\n choices: [\"sse\", \"stream\"],\n deprecated: \"Transport is now auto-detected per MCP spec. This option is ignored.\",\n describe: \"(Deprecated) The transport type - now auto-detected\",\n type: \"string\",\n },\n url: {\n demandOption: true,\n describe: \"The URL of the remote MCP server\",\n type: \"string\",\n },\n });\n })\n .command(\"$0 [command] [args...]\", \"Run a command with MCP arguments (backward compatible)\", (yargs) => {\n return yargs\n .positional(\"command\", {\n describe: \"The command to run\",\n type: \"string\",\n })\n .positional(\"args\", {\n array: true,\n describe: \"The arguments to pass to the command\",\n type: \"string\",\n });\n })\n .env(\"MCP_GATEWAY\")\n .parserConfiguration({\n \"populate--\": true,\n \"unknown-options-as-args\": true,\n })\n .options({\n // NEW: Scanner/Gateway configuration CLI args (these override env vars)\n // Note: These are pre-parsed in mcp-gateway.ts and set as env vars before this file loads\n \"access-control-url\": {\n describe: \"URL of the access control service (overrides MCP_GATEWAY_ACCESS_CONTROL_URL)\",\n type: \"string\",\n },\n debug: {\n default: false,\n describe: \"Enable debug logging\",\n type: \"boolean\",\n },\n env: {\n describe: \"Environment variables to pass to the command (format: KEY=value)\",\n string: true,\n type: \"array\",\n },\n gracefulShutdownTimeout: {\n default: 5000,\n describe: \"The timeout (in milliseconds) for graceful shutdown\",\n type: \"number\",\n },\n \"oauth-providers-url\": {\n describe: \"URL of the OAuth providers config endpoint (overrides MCP_GATEWAY_OAUTH_PROVIDERS_URL)\",\n type: \"string\",\n },\n port: {\n describe: \"Run as HTTP server on the specified port instead of stdio\",\n type: \"number\",\n },\n \"scanner-api-key\": {\n describe: \"API key for scanner authentication (overrides MCP_GATEWAY_SCANNER_API_KEY)\",\n type: \"string\",\n },\n \"scanner-fail-open\": {\n describe: \"Allow requests when scanner is unavailable (overrides MCP_GATEWAY_SCANNER_FAIL_OPEN)\",\n type: \"boolean\",\n },\n \"scanner-timeout-ms\": {\n describe: \"Scanner request timeout in milliseconds (overrides MCP_GATEWAY_SCANNER_TIMEOUT_MS)\",\n type: \"number\",\n },\n \"scanner-url\": {\n describe: \"URL of the scanner/evaluator service (overrides MCP_GATEWAY_SCANNER_URL)\",\n type: \"string\",\n },\n shell: {\n default: process.platform === \"win32\",\n describe: \"Spawn the server via the user's shell (defaults to true on Windows)\",\n type: \"boolean\",\n },\n })\n .help()\n .parseAsync();\n\n // Fetch OAuth provider config from the web extension service before any\n // OAuth flow can run. Fail-safe: on error, the in-memory provider list\n // stays empty and OAuth requests fall through to Dynamic Client\n // Registration. See auth/oauthProvidersClient.ts.\n //\n // Placed AFTER yargs parsing so commands like `--help` / `--version` exit\n // immediately without paying the OAuth fetch latency cost.\n await initializeOAuthProviders(config.oauthProviders);\n\n // Determine the proxy type based on the command\n let proxyType: ProxyType;\n // True once the client-facing stdio transport is connected. The startup-failure\n // path checks this so it only answers `initialize` itself BEFORE the real Server\n // owns stdin (never both — they would fight over the same pipe).\n let clientConnected = false;\n let finalCommand: string | undefined;\n let finalArgs: string[] | undefined;\n let remoteUrl: string | undefined;\n // Transport type used for connection (for logging)\n let usedTransport: TransportType | undefined;\n const remoteHeaders: Record<string, string> = {};\n\n // Parse --env KEY=VALUE args for stdio child processes (general-purpose flag).\n const customEnv: Record<string, string> = {};\n if (argv.env) {\n for (const envVar of argv.env as string[]) {\n const [key, ...valueParts] = envVar.split(\"=\");\n if (key && valueParts.length > 0) {\n customEnv[key.trim()] = valueParts.join(\"=\").trim();\n }\n }\n }\n\n // Resolve an env var reference from process.env. Handles plain env var names\n // (used by --env-http-headers and --bearer-token-env) and ${VAR} template\n // patterns (defense-in-depth, not used by current scanner output).\n // Returns the resolved string, or the name of the first missing/empty var.\n const resolveEnvValue = (value: string): { missingVar?: string; resolved: string } => {\n if (value.includes(\"${\")) {\n let missingVar: string | undefined;\n const resolved = value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_, varName: string) => {\n const val = process.env[varName];\n if (!val) {\n missingVar = varName;\n return \"\";\n }\n return val;\n });\n return missingVar ? { missingVar, resolved: \"\" } : { resolved };\n }\n // Plain env var name\n const val = process.env[value];\n if (!val) {\n return { missingVar: value, resolved: \"\" };\n }\n return { resolved: val };\n };\n\n // Legacy: --headers with literal values (old installs). New installs use\n // --env-http-headers with synthetic env var names instead. Passthrough only.\n if (argv.headers) {\n for (const header of argv.headers as string[]) {\n const [key, ...valueParts] = header.split(\":\");\n if (key && valueParts.length > 0) {\n remoteHeaders[key.trim()] = valueParts.join(\":\").trim();\n }\n }\n }\n\n // Resolve bearer token from environment variable if specified\n if (argv.bearerTokenEnv) {\n const envVarName = argv.bearerTokenEnv as string;\n const { missingVar, resolved: token } = resolveEnvValue(envVarName);\n if (missingVar) {\n await exitAuthEnvMissing({\n envVar: missingVar,\n eventType: \"mcp_auth_bearer_env_missing\",\n logMessage: \"Bearer token env var specified but not set\",\n respondToClient: !argv.port,\n });\n }\n remoteHeaders[\"Authorization\"] = `Bearer ${token}`;\n logger.info(\"Bearer token loaded from environment variable\", {\n authType: \"bearer_token\",\n envVar: envVarName,\n eventType: \"mcp_auth_bearer_env\",\n remoteUrl: argv.url as string,\n });\n }\n\n // Resolve env-http-headers: each entry is either:\n // \"HeaderName:ENV_VAR_NAME\" — full value from a single env var\n // \"HeaderName:Bearer ${ENV_VAR_NAME}\" — template with ${VAR} references resolved from env\n if (argv.envHttpHeaders) {\n for (const entry of argv.envHttpHeaders as string[]) {\n const [headerName, ...valueParts] = entry.split(\":\");\n const valueOrTemplate = valueParts.join(\":\").trim();\n if (headerName && valueOrTemplate) {\n const trimmedHeader = headerName.trim();\n const { missingVar, resolved } = resolveEnvValue(valueOrTemplate);\n\n if (missingVar) {\n await exitAuthEnvMissing({\n envVar: missingVar,\n eventType: \"mcp_auth_env_header_missing\",\n header: trimmedHeader,\n logMessage: \"Env var for HTTP header not set\",\n respondToClient: !argv.port,\n });\n }\n\n remoteHeaders[trimmedHeader] = resolved;\n logger.info(\"HTTP header loaded from environment\", {\n eventType: \"mcp_auth_env_header\",\n header: trimmedHeader,\n remoteUrl: argv.url as string,\n });\n }\n }\n }\n\n // Extract and set trace ID from final resolved headers (legacy + env-backed)\n const headerTraceId = extractTraceIdFromHeaders(remoteHeaders);\n if (headerTraceId) {\n logger.setTraceId(headerTraceId);\n }\n\n if (remoteHeaders.Authorization) {\n const authType = remoteHeaders.Authorization.startsWith(\"Bearer \")\n ? \"bearer_token\"\n : remoteHeaders.Authorization.startsWith(\"Basic \")\n ? \"basic\"\n : \"api_key\";\n\n logger.info(\"Authorization header provided\", {\n authType,\n eventType: \"mcp_auth_header_provided\",\n remoteUrl: argv.url as string,\n });\n }\n\n // Initialize traffic scanner from config\n const scannerHeaders = { ...config.scanner.headers };\n\n logger.debug(\"Initializing traffic scanner\", {\n failOpen: config.scanner.failOpen,\n hasApiKey: !!config.scanner.apiKey,\n scannerEnabled: config.scanner.enabled,\n scannerUrl: config.scanner.url,\n timeoutMs: config.scanner.timeoutMs,\n });\n\n // Centralized handling for ANY startup failure (config/usage error, upstream\n // unreachable, access-control block). Each surfaces a SPECIFIC reason to the\n // agent via an `initialize` error reply (never the opaque -32000), to\n // Groundcover, and to stderr — then exits. Called both from the proxy-type\n // determination below (pre-connect config errors) and the post-connect catch;\n // `clientConnected` decides whether we may still answer the client's stdin.\n const handleStartupFailure = async (error: unknown): Promise<never> => {\n const respondToClient = !argv.port && !clientConnected;\n // A remote URL may carry credentials/query tokens — never surface them. Use\n // origin-only, and scrub any verbatim occurrence the upstream error embedded.\n const safeRemoteUrl = remoteUrl\n ? (() => {\n try {\n return new URL(remoteUrl).origin;\n } catch {\n return \"[invalid remote URL]\";\n }\n })()\n : undefined;\n const scrub = (text: string): string =>\n remoteUrl && safeRemoteUrl ? text.split(remoteUrl).join(safeRemoteUrl) : text;\n\n if (error instanceof AccessControlBlockError) {\n // Onyx access-control block at connect time. error.message carries the\n // org's policy reason (or a default naming Onyx).\n const reason =\n error.message ||\n \"This MCP server has been blocked by your organization's Onyx access-control policy. Contact your administrator if you believe this is an error.\";\n // Prominent multi-line banner on stderr — in addition to the agent-facing\n // initialize error and the Groundcover event — so a developer running the\n // gateway directly (the ticket's --debug flow) sees the block clearly.\n const banner = [\n \"\",\n \"❌ ACCESS DENIED\",\n \"━\".repeat(60),\n reason,\n \"Please contact your administrator if you believe this is an error.\",\n \"\",\n ].join(\"\\n\");\n return failStartupVisibly({\n exitCode: 1,\n logMessage: \"MCP server blocked by access control policy\",\n meta: { error: error.message, eventType: \"mcp_access_control_block\", reason: error.reason },\n reason,\n respondToClient,\n stderrLine: banner,\n });\n }\n\n // Any other startup failure (config/usage error, upstream unreachable, …).\n const reason = scrub(\n error instanceof ConfigurationError\n ? `Onyx MCP gateway: configuration error — ${error.message}`\n : error instanceof ProxyConnectionError\n ? `Onyx MCP gateway: cannot reach the upstream MCP server${safeRemoteUrl ? ` at ${safeRemoteUrl}` : \"\"} — ${error.message}`\n : `Onyx MCP gateway: failed to start — ${error instanceof Error ? error.message : String(error)}`,\n );\n return failStartupVisibly({\n exitCode: 1,\n logMessage: \"MCP server failed to start\",\n meta: {\n args: finalArgs ? redactCliArgs(finalArgs) : undefined,\n command: finalCommand,\n errno: getUpstreamErrno(error),\n error: scrub(`Could not start the proxy: ${error}`),\n eventType: \"mcp_server_start_failed\",\n proxyType,\n remoteUrl: safeRemoteUrl,\n transport: usedTransport,\n },\n reason,\n respondToClient,\n stderrLine: `[mcp-gateway] FATAL: ${reason}`,\n });\n };\n\n // Determine proxy type based on command\n if (argv._[0] === \"remote\") {\n proxyType = \"remote\";\n\n // Warn if deprecated remote-transport argument is used\n if (argv[\"remote-transport\"]) {\n logger.warn(\n \"The --remote-transport option is deprecated and will be ignored. \" +\n \"Transport is now auto-detected per MCP spec (Streamable HTTP with SSE fallback).\",\n { providedTransport: argv[\"remote-transport\"] }\n );\n }\n\n const rawUrl = argv.url as string;\n if (!rawUrl) {\n // Route through the visible-startup handler (not a bare throw) so the agent\n // gets a specific reason rather than -32000.\n await handleStartupFailure(new ConfigurationError(\"Remote URL is required for remote proxy type\", \"url\"));\n }\n // Normalize URL to avoid redirect issues on Windows\n // Windows may convert POST to GET on 301/302 redirects (e.g., trailing slash differences)\n remoteUrl = normalizeUrl(rawUrl);\n if (remoteUrl !== rawUrl) {\n logger.debug(\"Normalized remote URL\", { normalizedUrl: remoteUrl, originalUrl: rawUrl });\n }\n } else {\n // Default to stdio for backward compatibility\n proxyType = \"stdio\";\n\n // If -- separator was used, first item in argv[\"--\"] is the command\n if (argv[\"--\"] && (argv[\"--\"] as string[]).length > 0) {\n const dashDashArgs = argv[\"--\"] as string[];\n finalCommand = dashDashArgs[0];\n // Ensure all args are strings (yargs may parse numbers as numeric types)\n finalArgs = dashDashArgs.slice(1).map(String);\n } else {\n // Use positional arguments\n finalCommand = argv.command as string;\n // Ensure all args are strings (yargs may parse numbers as numeric types)\n finalArgs = ((argv.args as unknown[]) || []).map(String);\n }\n\n if (!finalCommand) {\n // Route through the visible-startup handler (not a bare throw) so the agent\n // gets a specific reason rather than -32000.\n await handleStartupFailure(new ConfigurationError(\"No command specified for stdio proxy\", \"command\"));\n }\n }\n\n // Detect a container-based MCP (Docker, Podman, nerdctl, finch). This is PURE\n // string parsing — no process is spawned. Resolving the image DIGESTS invokes\n // the container runtime with a value the (possibly source-controlled) MCP\n // config controls, so that step is deferred to\n // resolveContainerImageDigests() below, which runs only after the\n // access-control decision has allowed this server.\n let containerMCPCommand: ContainerMCPCommand | undefined;\n if (proxyType === \"stdio\" && finalCommand) {\n containerMCPCommand = detectContainerMCP(finalCommand, finalArgs);\n if (containerMCPCommand.isContainerMCP) {\n logger.info(\"Container MCP detected\", {\n imageName: containerMCPCommand.imageName,\n runtime: containerMCPCommand.runtime,\n });\n }\n }\n\n // Resolved (post-gate) container image digests. `undefined` until\n // resolveContainerImageDigests() has run — never resolved for a server the\n // access-control decision denied.\n let containerImageDigests: string[] | undefined;\n\n // Session identity (machine/session ids), read at most once per process.\n // Resolved LAZILY and awaited ONLY by the paths that need it: the probe behind it\n // is slow on Windows, and a plain stdio server must not pay for it on the way to\n // being spawned (that latency is what a client sees as a hung start). Two paths\n // do await it: the access-control request, which awaited it before this was lazy,\n // and the container digest lookup, which needs the ids on its own log lines — so\n // a CONTAINER server does wait for it pre-spawn. That is cheap in the deployed\n // shape (access control configured ⇒ already resolved) and only a first read in\n // dev runs without access control. Keep it that way if you reorder this.\n let sessionDataOnce: ReturnType<typeof getSessionData> | undefined;\n const attachSessionDataToLogger = async (): Promise<void> => {\n sessionDataOnce ??= getSessionData();\n logger.setSessionData({ ...(await sessionDataOnce), proxyType });\n };\n\n /**\n * Invoke the container runtime to resolve the MCP server's image digests.\n *\n * SECURITY: this is the one step that runs a local binary\n * with a value taken from the MCP server config, so it MUST NOT run before\n * accessControlAuthorizer.isAllowed() has allowed the server — otherwise a\n * server Onyx policy denies still causes local execution. Idempotent, so a\n * mid-session reconnect does not re-inspect the image.\n */\n const resolveContainerImageDigests = async (): Promise<void> => {\n if (containerImageDigests !== undefined || !containerMCPCommand?.isContainerMCP || !containerMCPCommand.imageName) {\n return;\n }\n // Attach the machine/session ids first, so the lookup's own outcome logs —\n // the validator's deliberate skip, the failure path and the success path —\n // carry ids that tie the line to this host and session.\n await attachSessionDataToLogger();\n containerImageDigests = getContainerImageDigests(\n containerMCPCommand.imageName,\n containerMCPCommand.runtime\n );\n };\n\n const buildSessionCliArgs = (): RemoteSessionCliArgs | StdioSessionCliArgs =>\n proxyType === \"stdio\"\n ? {\n args: finalArgs as string[],\n command: finalCommand as string,\n dockerImageDigests: containerImageDigests,\n dockerImageName: containerMCPCommand?.imageName,\n proxyType,\n }\n : {\n proxyType,\n url: remoteUrl as string,\n };\n\n // Initialize the access control client if configured. NOTE: the session data\n // sent with the authorize request carries dockerImageName but not the digests —\n // they are resolved only after this decision. The backend's MCP access-control\n // path keys on the command+args cache hash and resolves the version from its\n // own mcp_latest_version cache, so the decision is unaffected.\n let accessControlClient: AccessControlClient | undefined;\n if (config.accessControl.url && config.scanner.apiKey) {\n const cliArgs = buildSessionCliArgs();\n // This path already awaited the session identity before the fix, so reading\n // it here adds no startup latency. It also makes the ids available to every\n // log line from here on, digest lookup included.\n await attachSessionDataToLogger();\n const sessionDataBase = await sessionDataOnce;\n\n const sessionData =\n cliArgs.proxyType === \"stdio\"\n ? {\n ...sessionDataBase,\n proxyType: \"stdio\" as const,\n stdioCliArgs: { ...cliArgs, args: redactCliArgs(cliArgs.args) },\n }\n : {\n ...sessionDataBase,\n proxyType: \"remote\" as const,\n remoteCliArgs: cliArgs,\n };\n\n accessControlClient = new AccessControlClient({\n apiKey: config.scanner.apiKey,\n headers: config.accessControl.headers,\n sessionData,\n timeoutMs: config.accessControl.timeoutMs,\n url: config.accessControl.url,\n });\n }\n\n // Determine server name for error messages\n const serverName = proxyType === \"remote\" ? remoteUrl : finalCommand;\n\n // Initialize access control authorizer\n const accessControlAuthorizer = new AccessControlAuthorizer({\n client: accessControlClient,\n enabled: config.accessControl.enabled,\n serverName,\n });\n\n // Initialize OAuth credential store for remote connections\n const oauthCredentialStore = new OAuthCredentialStore({\n storageDir: config.oauthDir,\n });\n\n // Factory function to create a new Client instance\n const createClient = (): Client =>\n new Client(\n {\n name: \"mcp-gateway\",\n version: config.appVersion,\n },\n {\n capabilities: {},\n }\n );\n\n const connect = async (\n client: Client,\n { allowInteractiveAuth = true }: { allowInteractiveAuth?: boolean } = {}\n ): Promise<Client> => {\n // Check access control (async - may query backend)\n const isAllowed = await accessControlAuthorizer.isAllowed();\n if (!isAllowed) {\n const reason = accessControlAuthorizer.getBlockReason();\n logger.warn(\"MCP server blocked by access control\", {\n proxyType,\n reason,\n });\n\n throw new AccessControlBlockError(reason, \"policy_block\");\n }\n\n logger.info(\"MCP server allowed by access control\");\n\n // ONLY NOW may the container runtime be invoked for this server's image\n // digests: the access-control decision has resolved and allowed it. Running it\n // earlier is what let a policy-denied server achieve local host execution\n // before the gateway enforced its own control.\n await resolveContainerImageDigests();\n\n if (proxyType === \"stdio\") {\n // Merge custom CLI environment variables with process.env for child processes\n // Note: This is for MCP server processes, not the proxy's internal configuration\n const mergedEnv = {\n ...process.env,\n ...customEnv,\n } as Record<string, string>;\n\n const transport = new StdioClientTransport({\n args: finalArgs,\n command: finalCommand as string,\n env: mergedEnv,\n onEvent: (event) => {\n if (argv.debug) {\n logger.debug(\"Transport event\", { event });\n }\n },\n shell: argv.shell,\n // Pipe stderr so we can both forward to the parent agent client (preserving\n // the previous \"inherit\" behavior) AND log each line into the gateway's\n // structured log. Without this we have no record of why a spawned MCP\n // server crashed during startup.\n stderr: \"pipe\",\n });\n activeStdioTransport = transport;\n\n // Cap each captured stderr line so a misbehaving server emitting multi-MB\n // stack dumps or base64 blobs to stderr can't balloon log entries or the\n // in-memory tail buffer. Matches JSONFilterTransform.logDroppedLine.\n const STDERR_LINE_MAX = 1024;\n // If the child never emits a newline we still need an upper bound on the\n // pending line buffer — otherwise a binary blob to stderr would grow\n // memory unbounded and defeat the per-line cap.\n const STDERR_BUFFER_MAX = STDERR_LINE_MAX * 8;\n const truncate = (line: string): string =>\n line.length > STDERR_LINE_MAX ? `${line.slice(0, STDERR_LINE_MAX)}…` : line;\n\n let stderrLineBuffer = \"\";\n const recentStderr: string[] = [];\n const logStderrLine = (line: string) => {\n const truncated = truncate(line);\n recentStderr.push(truncated);\n if (recentStderr.length > 100) recentStderr.shift();\n logger.info(\"MCP server stderr\", {\n command: finalCommand,\n eventType: \"mcp_server_stderr\",\n line: truncated,\n });\n };\n const flushBufferAsLine = () => {\n if (stderrLineBuffer.length === 0) return;\n logStderrLine(stderrLineBuffer);\n stderrLineBuffer = \"\";\n };\n // Decoder buffers incomplete multi-byte UTF-8 sequences across chunk\n // boundaries — without it `chunk.toString()` would emit replacement\n // characters when a codepoint straddles a chunk.\n const stderrDecoder = new StringDecoder(\"utf8\");\n // Drain any pending decoder bytes + remaining line buffer. Idempotent —\n // both `end` and `close` may fire, and the connect-failure catch below\n // also drains explicitly so we don't depend on the SDK ordering.\n let stderrTailDrained = false;\n const drainStderrTail = () => {\n if (stderrTailDrained) return;\n stderrTailDrained = true;\n const decoderTail = stderrDecoder.end();\n if (decoderTail) {\n stderrLineBuffer += decoderTail;\n }\n flushBufferAsLine();\n };\n transport.stderr?.on(\"data\", (chunk: Buffer) => {\n // Forward raw bytes to the parent's stderr to preserve the byte-fidelity\n // the previous `stderr: \"inherit\"` provided. The structured-log path\n // below uses StringDecoder for proper UTF-8 handling.\n process.stderr.write(chunk);\n stderrLineBuffer += stderrDecoder.write(chunk);\n const lines = stderrLineBuffer.split(\"\\n\");\n stderrLineBuffer = lines.pop() ?? \"\";\n for (const line of lines) {\n if (line.length === 0) continue;\n logStderrLine(line);\n }\n // Cap residual partial line — only after extracting any complete lines\n // above, otherwise a chunk containing newlines + an oversize tail would\n // silently discard the parseable lines together with the tail.\n if (stderrLineBuffer.length > STDERR_BUFFER_MAX) {\n flushBufferAsLine();\n }\n });\n transport.stderr?.once(\"end\", drainStderrTail);\n transport.stderr?.once(\"close\", drainStderrTail);\n\n try {\n await client.connect(transport);\n return client;\n } catch (error) {\n drainStderrTail();\n logger.warn(\"Failed original server access\", {\n command: finalCommand,\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_server_access_failed\",\n proxyType,\n stderrTail: recentStderr.slice(-20),\n });\n // Tear down the spawned child before rethrowing. connectWithStartupRetry\n // re-runs connect() on a recoverable failure, so without this each retry\n // would orphan the previous attempt's stdio subprocess.\n await transport.close().catch(() => {});\n throw new ProxyConnectionError(`Failed to connect to stdio process: ${error}`, undefined, \"stdio\", {\n argCount: finalArgs?.length,\n command: finalCommand,\n }, error);\n }\n } else if (proxyType === \"remote\") {\n // Connect to remote server\n // Strategy: Try Streamable HTTP first, fall back to SSE if not supported (404/405)\n // Handle OAuth (401) separately - if auth fails, get token and retry the whole process\n\n const url = remoteUrl as string;\n\n // Build request init carrying the caller-supplied headers. An OAuth\n // bearer is NOT set here — it's injected per-request by the dynamic-auth\n // fetch wrapper below, so that mid-session refreshes pick up the latest\n // bearer without needing to recreate the transport (PRDCT-3666 Tier 2).\n // A static Authorization (api-key / Basic) MAY be present in remoteHeaders;\n // the wrapper preserves it when there is no OAuth bearer (PRDCT-5136).\n const buildNonAuthRequestInit = (): RequestInit | undefined => {\n const headers: Record<string, string> = { ...remoteHeaders };\n return Object.keys(headers).length > 0 ? { headers } : undefined;\n };\n\n // Get OAuth token (from cache or via flow)\n const getOAuthToken = async (): Promise<string> => {\n const oauthResult = await performOAuthFlow(url, { credentialStore: oauthCredentialStore });\n logger.info(\"Authentication performed\", {\n authType: \"oauth\",\n eventType: \"mcp_auth_performed\",\n fromCache: oauthResult.fromCache,\n remoteUrl: url,\n });\n return oauthResult.tokens.access_token;\n };\n\n let connectedClient: Client;\n try {\n // Check for cached OAuth token\n const cached = await oauthCredentialStore.load(url);\n const initialBearer =\n cached && oauthCredentialStore.hasValidAccessToken(cached) ? cached.tokens?.access_token : undefined;\n\n // Dynamic auth handle: holds the in-memory bearer. The transport's\n // custom fetch reads from this handle on every request, refreshes\n // mid-session on 401, and dedupes concurrent refreshes (PRDCT-3666\n // Tier 2). When `performOAuthFlow` produces fresh tokens (cold start\n // or browser fallback), we feed them back into the handle so future\n // requests use the new bearer too.\n const dynAuth: DynamicAuthHandle = createDynamicAuth(url, oauthCredentialStore, initialBearer);\n const authFetch = createAuthenticatingFetch(dynAuth);\n\n let usedFreshOAuth = false; // Track if we've already tried with a fresh token\n\n try {\n // Try connecting with transport fallback (Streamable HTTP → SSE)\n // Uses a client factory to create fresh clients for each transport attempt\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server\", { hasAuth: !!initialBearer, transport: usedTransport });\n } catch (err) {\n // Handle 401 errors - need OAuth authentication or token refresh\n if (is401Error(err)) {\n // If we already tried with a fresh OAuth token, don't retry again (avoid infinite loop)\n if (usedFreshOAuth) {\n throw err;\n }\n\n // PRDCT-3666: when the cached token was rejected by upstream and\n // the dynamic-auth fetch wrapper couldn't silently recover (no\n // refresh_token on disk, or invalid_grant, or transient failure\n // mid-retry), fall through to the full interactive OAuth flow.\n // Tier 1 already preserved refresh_token across transient errors;\n // Tier 2 added the mid-session refresh inside `authFetch`. By the\n // time we land here, both fast paths have been exhausted and a\n // browser popup is the spec-correct next step (matches what a\n // non-gateway MCP client would do).\n if (dynAuth.getCurrentBearer() ?? initialBearer) {\n logger.info(\"Cached + refresh both rejected; falling back to full OAuth flow\");\n\n // Mark the cached access_token as expired BEFORE falling back.\n //\n // Without this, `performOAuthFlow` → `tryLoadExistingCredentials`\n // would happily return the SAME dead access_token (its\n // time-based `hasValidAccessToken` check can't know upstream\n // rejected it), we'd set that dead token as the bearer, retry,\n // get 401 again, and the `usedFreshOAuth` guard would\n // propagate the error WITHOUT ever opening a browser. Users\n // on no-refresh_token servers (ClickUp, GitHub Copilot,\n // Sentry) would be permanently stuck.\n //\n // Clearing `tokensObtainedAt` is enough — `isAccessTokenExpired`\n // returns true when it's missing, so `hasValidAccessToken`\n // returns false. We deliberately preserve:\n // - `clientInfo` (so we don't have to re-DCR)\n // - `tokens.refresh_token` (a transient refresh failure may\n // have just left it on disk; the next attempt may succeed)\n await oauthCredentialStore.update(url, { tokensObtainedAt: undefined });\n } else {\n logger.info(\"Server requires OAuth, starting authentication flow...\");\n }\n\n // A mid-session reconnect must never open a browser (the dynamicAuth\n // contract). The silent refresh already ran inside authFetch; a\n // surviving 401 is surfaced (non-recoverable) and the user re-auths\n // interactively on the next fresh connect.\n if (!allowInteractiveAuth) {\n logger.info(\"Skipping interactive OAuth on reconnect; surfacing 401\", {\n eventType: \"mcp_reconnect_skip_interactive_auth\",\n });\n throw err;\n }\n\n const freshToken = await getOAuthToken();\n dynAuth.setBearer(freshToken);\n usedFreshOAuth = true;\n // If even the freshly-issued OAuth token is then rejected, the\n // error propagates to the outer catch (wrapped as\n // ProxyConnectionError) — usedFreshOAuth gates further retries.\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server with OAuth\", { transport: usedTransport });\n } else if (\n !usedFreshOAuth &&\n !(dynAuth.getCurrentBearer() ?? initialBearer) &&\n !new Headers(remoteHeaders).has(\"Authorization\")\n ) {\n // COLD-START non-401 recovery. We sent NO credential at all — neither\n // an OAuth bearer NOR a caller-supplied static Authorization (api-key\n // / Basic / bearer via --bearer-token-env / --headers /\n // --env-http-headers) — and the upstream rejected with a non-401. It\n // may be an OAuth-protected server that answers an unauthenticated\n // request non-compliantly — e.g. Okta's llm.atko.ai returns 500\n // instead of a spec-correct 401. `is401Error` is false here and the\n // transport layer only falls back on 404/405, so without this branch\n // the gateway would give up without ever authenticating, even though\n // the server exposes standard, discoverable OAuth metadata.\n //\n // Deliberately scoped to the NO-CREDENTIAL case (cronyx + coderabbit,\n // PR #8418). If we had ALREADY sent a credential (a cached OAuth token\n // OR a static Authorization) and got a non-401 (500/502/503/429), that\n // is NOT evidence the credential is bad — a genuine rejection is a\n // 401, handled by the branch above. A non-401 on an authenticated\n // request is most likely transient (deploy, rate-limit), so re-authing\n // there would wipe a still-valid token on disk and pop a browser on a\n // blip. We let that case fall through to `throw err` and propagate the\n // original error unchanged.\n //\n // Probe the well-known OAuth metadata directly (independent of any\n // 401). Only if an authorization server is actually advertised do we\n // run the OAuth flow once and retry; otherwise propagate the ORIGINAL\n // error (just +1 well-known probe of latency on an already-doomed\n // connection). Because we only get here with no bearer, there is no\n // cached token to invalidate first — `performOAuthFlow` does a clean\n // fresh acquisition.\n let advertisesOAuth = false;\n let discoveryError: string | undefined;\n try {\n const { authMetadata } = await discoverAuthServer(new URL(url), { logger });\n advertisesOAuth = Boolean(authMetadata);\n } catch (discoveryErr) {\n discoveryError = discoveryErr instanceof Error ? discoveryErr.message : String(discoveryErr);\n }\n if (!advertisesOAuth) {\n // Surface at warn so a production recurrence shows WHY recovery did\n // not happen: the upstream rejected us with a non-401 AND exposed\n // no discoverable OAuth metadata, so there is nothing to bootstrap\n // from. (discoverAuthServer logs the individual metadata-probe\n // results — protected-resource + auth-server + registration_endpoint\n // — separately.)\n logger.warn(\"Non-401 failure and no discoverable OAuth metadata; propagating original error\", {\n discoveryError,\n eventType: \"mcp_non401_no_oauth_discovery\",\n originalError: err instanceof Error ? err.message : String(err),\n remoteUrl: url,\n });\n throw err;\n }\n logger.info(\"Cold-start non-401 but server advertises OAuth; attempting OAuth flow\", {\n originalError: err instanceof Error ? err.message : String(err),\n });\n if (!allowInteractiveAuth) {\n logger.info(\"Skipping interactive OAuth on reconnect; surfacing error\", {\n eventType: \"mcp_reconnect_skip_interactive_auth\",\n });\n throw err;\n }\n const freshToken = await getOAuthToken();\n dynAuth.setBearer(freshToken);\n usedFreshOAuth = true;\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server with OAuth (recovered from non-401)\", {\n transport: usedTransport,\n });\n } else {\n throw err;\n }\n }\n return connectedClient;\n } catch (error) {\n logger.warn(\"Failed original server access\", {\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_server_access_failed\",\n proxyType,\n remoteUrl: url,\n });\n if (error instanceof ProxyConnectionError) throw error;\n throw new ProxyConnectionError(\n `Failed to connect to remote server: ${error instanceof Error ? error.message : String(error)}`,\n url,\n usedTransport,\n { headerCount: Object.keys(remoteHeaders).length },\n error\n );\n }\n }\n\n // Should never reach here, but satisfy TypeScript\n return client;\n };\n\n // Connect to the upstream with bounded retry on transient transport failures,\n // so a brief reachability blip when the gateway starts doesn't fail the whole\n // session. connect() returns a fresh client per attempt via createClient().\n const connectWithStartupRetry = (): Promise<Client> =>\n connectWithRetry(() => connect(createClient()), config.upstream.connect, {\n onRetry: ({ attempt, delayMs, error }) => {\n logger.warn(\"Upstream connect failed; retrying\", {\n attempt,\n delayMs,\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_upstream_connect_retry\",\n proxyType,\n });\n },\n });\n\n // Whether to wrap the upstream in the mid-session reconnecting façade. Remote\n // by default; stdio is opt-in (a stdio \"reconnect\" respawns the child and\n // loses its in-memory state). The reconnect factory passes\n // allowInteractiveAuth:false so a mid-session reconnect never opens a browser.\n const reconnectEnabled =\n proxyType === \"remote\"\n ? config.upstream.reconnect.enabled\n : config.upstream.reconnect.enabledForStdio;\n const wrapUpstream = (connected: Client): UpstreamClient =>\n reconnectEnabled\n ? createReconnectingClient({\n cooldownMs: config.upstream.reconnect.cooldownMs,\n initial: connected,\n reconnect: () => connect(createClient(), { allowInteractiveAuth: false }),\n })\n : connected;\n\n // The traffic mirror snapshots the session data — including the container image\n // digests — when it is initialized, so it must be initialized AFTER the first\n // connect() resolved them (i.e. after the access-control gate). Memoized: one\n // mirror per process, however many sessions connect.\n let trafficMirrorInit: Promise<unknown> | undefined;\n const ensureTrafficMirror = (): Promise<unknown> => {\n trafficMirrorInit ??= initializeTrafficMirror({\n cliArgs: buildSessionCliArgs(),\n scanApiKey: config.scanner.apiKey,\n // Scanner config from env vars\n scanEnabled: config.scanner.enabled,\n scanFailOpen: config.scanner.failOpen,\n scanHeaders: scannerHeaders,\n scanTimeoutMs: config.scanner.timeoutMs,\n scanUrl: config.scanner.url || \"\",\n spawnedServerInfoProvider,\n });\n return trafficMirrorInit;\n };\n\n const proxy = async () => {\n // If port is specified, start HTTP server mode\n if (argv.port) {\n let upstream: undefined | UpstreamClient;\n const httpServer = await startHTTPServer({\n createServer: async () => {\n // connect() may return a different instance for remote connections;\n // connectWithStartupRetry retries a transient upstream failure.\n const connected = await connectWithStartupRetry();\n\n // Initialize the traffic evaluator once, now that access control has\n // allowed the server and the image digests are resolved.\n await ensureTrafficMirror();\n\n const serverVersion = connected.getServerVersion() as {\n name: string;\n version: string;\n };\n\n const serverCapabilities =\n connected.getServerCapabilities() as ServerCapabilities;\n\n // Wrap so a mid-session upstream drop self-heals instead of killing the\n // session. proxyServer holds this stable façade, not the raw client.\n upstream = wrapUpstream(connected);\n\n const server = new Server(serverVersion, {\n capabilities: serverCapabilities,\n });\n\n proxyServer({\n authorizer: accessControlAuthorizer,\n client: upstream,\n server,\n serverCapabilities,\n });\n\n return server;\n },\n onClose: async (server) => {\n if (upstream) {\n await upstream.close();\n }\n await server.close();\n },\n port: argv.port,\n });\n\n logger.info(\"MCP gateway started\", {\n eventType: \"mcp_gateway_started\",\n mode: \"http\",\n port: argv.port,\n proxyType,\n });\n if (proxyType === \"stdio\") {\n logger.info(\"MCP server configuration\", {\n args: redactCliArgs(finalArgs ?? []).join(\" \"),\n command: finalCommand,\n eventType: \"mcp_server_config\",\n proxyType,\n });\n } else {\n logger.info(\"MCP server configuration\", {\n eventType: \"mcp_server_config\",\n proxyType,\n remoteUrl,\n });\n }\n\n return {\n close: () => {\n return httpServer.close();\n },\n };\n }\n\n // Original stdio mode\n // connect() may return a different instance for remote connections;\n // connectWithStartupRetry retries a transient upstream failure.\n const connected = await connectWithStartupRetry();\n\n const serverVersion = connected.getServerVersion() as {\n name: string;\n version: string;\n };\n\n const serverCapabilities =\n connected.getServerCapabilities() as ServerCapabilities;\n\n // Wrap so a mid-session upstream drop self-heals (remote only by default).\n const upstream = wrapUpstream(connected);\n\n // Always create a stdio server for client-facing interface\n const server = new Server(serverVersion, {\n capabilities: serverCapabilities,\n });\n\n proxyServer({\n authorizer: accessControlAuthorizer,\n client: upstream,\n server,\n serverCapabilities,\n });\n\n const stdioTransport = new StdioServerTransport();\n\n // Initialize traffic evaluator with scanner config (post-gate, so the\n // resolved container image digests are part of the session data it reports).\n await ensureTrafficMirror();\n\n await server.connect(stdioTransport);\n clientConnected = true;\n\n logger.info(\"MCP gateway started\", {\n eventType: \"mcp_gateway_started\",\n mode: \"stdio\",\n proxyType,\n });\n\n // Log MCP server configuration\n logger.info(\"MCP server configuration\", {\n capabilities: Object.keys(serverCapabilities || {}),\n eventType: \"mcp_server_config\",\n proxyType,\n serverName: serverVersion?.name,\n serverVersion: serverVersion?.version,\n ...(proxyType === \"stdio\"\n ? { args: redactCliArgs(finalArgs ?? []).join(\" \"), command: finalCommand }\n : { remoteUrl, transport: usedTransport }),\n });\n\n return {\n close: () => {\n // Close the upstream connection (the façade closes the live client).\n return upstream.close();\n },\n };\n };\n\n const createGracefulShutdown = ({\n server,\n timeout,\n }: {\n server: { close: () => Promise<void> | void };\n timeout: number;\n }) => {\n let shutdownTriggered = false;\n\n const gracefulShutdown = async (trigger: string) => {\n if (shutdownTriggered) {\n return;\n }\n shutdownTriggered = true;\n\n logger.info(\"MCP gateway stopping\", {\n eventType: \"mcp_gateway_stopping\",\n signal: trigger,\n });\n\n try {\n await server.close();\n // Log final request counts before shutdown\n logRequestCounts();\n // Flush logs to ingest endpoint before exit\n await flushLogs(2000);\n process.exit(0);\n } catch (error) {\n const errorMessage = `Error during shutdown: ${error}`;\n logger.error(\"Error during shutdown\", {\n error: errorMessage,\n eventType: \"mcp_gateway_stopped\",\n shutdownTimeout: timeout,\n });\n // Flush logs to ingest endpoint before exit\n await flushLogs(2000);\n process.exit(1);\n }\n };\n\n const timeoutHandler = (trigger: string) => async () => {\n setTimeout(() => {\n logger.error(\"Graceful shutdown timeout exceeded, forcing exit\");\n process.exit(1);\n }, timeout).unref();\n\n await gracefulShutdown(trigger);\n };\n\n process.once(\"SIGTERM\", timeoutHandler(\"SIGTERM\"));\n process.once(\"SIGINT\", timeoutHandler(\"SIGINT\"));\n\n // Stdin EOF — fires when the parent (e.g. claude) closes our stdio pipe.\n // On Windows, parents typically TerminateProcess without graceful signals,\n // but the MCP SDK closes stdio first; this gives us a window to reap our\n // child process tree before being killed. On POSIX it provides a backup\n // path when SIGHUP/SIGTERM doesn't reach us.\n process.stdin.once(\"end\", timeoutHandler(\"stdin-end\"));\n\n return () => {\n return server.close();\n };\n };\n\n // Log startup before attempting to connect\n logger.info(\"MCP gateway starting\", {\n command: finalCommand,\n eventType: \"mcp_gateway_starting\",\n proxyType,\n transport: proxyType === \"remote\" ? \"auto-detect\" : \"stdio\",\n url: remoteUrl,\n });\n\n try {\n const server = await proxy();\n\n createGracefulShutdown({\n server,\n timeout: argv.gracefulShutdownTimeout,\n });\n } catch (error) {\n // Startup failed after proxy() began (upstream unreachable, access-control\n // block, …). Surface a specific reason to the agent + Groundcover + stderr.\n await handleStartupFailure(error);\n }\n};\n"],"x_google_ignoreList":[0],"mappings":"6gDACA,IAAM,EAAN,cAAyB,KAAM,CAQ7B,YAAY,EAAM,EAAoB,CAEpC,MAAM,EAAK,CAAE,KAAK,KAAa,GAAyD,MAAqB,IAAK,GAAG,KAAK,QAAgB,GAAyD,SAAwB,IAAK,GAclO,CAAC,OAAO,IAAI,6BAA6B,EAAE,EAAQ,EAAS,EAAS,CACnE,OAAO,EAAQ,EAAiB,KAAK,CAAE,EAAQ,CAajD,CAAC,OAAO,IAAI,qBAAqB,EAAE,EAAS,EAAS,CACnD,OAAO,EAAQ,EAAiB,KAAK,CAAE,EAAQ,GAGnD,SAAS,GAAY,EAAS,CAC5B,IAAM,EAAe,WAAW,aAChC,OAAO,OAAO,GAAgB,WAAa,IAAI,EAAa,EAAS,cAAc,CAAO,YAAY,EAAQ,CAEhH,SAAS,EAAa,EAAK,CACzB,OAAO,aAAe,MAAQ,WAAY,GAAO,MAAM,QAAQ,EAAI,OAAO,CAAG,EAAI,OAAO,IAAI,EAAa,CAAC,KAAK,KAAK,CAAG,UAAW,GAAO,EAAI,iBAAiB,MAAQ,GAAG,EAAI,IAAI,EAAa,EAAI,MAAM,GAAK,EAAI,QAAU,GAAG,IAEhO,SAAS,EAAiB,EAAK,CAC7B,MAAO,CACL,KAAM,EAAI,KACV,QAAS,EAAI,QACb,KAAM,EAAI,KACV,iBAAkB,EAAI,iBACtB,WAAY,EAAI,WAChB,UAAW,EAAI,UAChB,CAEH,IAAI,EAAe,GAAQ,CACzB,MAAM,UAAU,EAAI,EACnB,GAAiB,EAAK,EAAQ,IAAQ,EAAO,IAAI,EAAI,EAAI,EAAY,UAAY,EAAI,CAAE,GAAgB,EAAK,EAAQ,KAAY,EAAc,EAAK,EAAQ,0BAA0B,CAAE,EAAS,EAAO,KAAK,EAAI,CAAG,EAAO,IAAI,EAAI,EAAG,GAAgB,EAAK,EAAQ,IAAU,EAAO,IAAI,EAAI,CAAG,EAAY,oDAAoD,CAAG,aAAkB,QAAU,EAAO,IAAI,EAAI,CAAG,EAAO,IAAI,EAAK,EAAM,CAAE,GAAgB,EAAK,EAAQ,EAAO,KAAY,EAAc,EAAK,EAAQ,yBAAyB,CAAE,EAAO,IAAI,EAAK,EAAM,CAAE,GAAQ,GAAmB,EAAK,EAAQ,KAAY,EAAc,EAAK,EAAQ,wBAAwB,CAAE,GAAS,EAAa,EAAM,EAAc,EAAkB,EAAQ,EAAoB,EAAiB,EAAc,EAAa,EAAS,EAAU,EAAY,EAAS,EAAwB,GAAY,GAAkB,GAAe,GAAsB,GAAU,GAAgB,EAAmB,GAAsB,GACr9B,GAAN,cAA0B,WAAY,CACpC,YAAY,EAAK,EAAqB,CAEpC,OAAO,CAAE,EAAa,KAAM,EAAuB,CAAE,KAAK,WAAa,EAAG,KAAK,KAAO,EAAG,KAAK,OAAS,EAAG,EAAa,KAAM,EAAY,CAAE,EAAa,KAAM,EAAK,CAAE,EAAa,KAAM,EAAa,CAAE,EAAa,KAAM,EAAiB,CAAE,EAAa,KAAM,EAAO,CAAE,EAAa,KAAM,EAAmB,CAAE,EAAa,KAAM,EAAgB,CAAE,EAAa,KAAM,EAAc,KAAK,CAAE,EAAa,KAAM,EAAY,CAAE,EAAa,KAAM,EAAQ,CAAE,EAAa,KAAM,EAAU,KAAK,CAAE,EAAa,KAAM,EAAY,KAAK,CAAE,EAAa,KAAM,EAAS,KAAK,CAAE,EAAa,KAAM,GAAkB,KAAO,IAAa,CAChmB,IAAI,EACJ,EAAa,KAAM,EAAQ,CAAC,OAAO,CACnC,GAAM,CAAE,OAAM,aAAY,SAAQ,WAAY,EAC9C,GAAI,IAAW,IAAK,CAClB,EAAgB,KAAM,EAAwB,EAAkB,CAAC,KAAK,KAAM,yCAA0C,IAAI,CAAE,KAAK,OAAO,CACxI,OAEF,GAAI,EAAa,EAAa,KAAM,EAAc,IAAI,IAAI,EAAS,IAAI,CAAC,CAAG,EAAa,KAAM,EAAc,IAAK,GAAE,CAAE,IAAW,IAAK,CACnI,EAAgB,KAAM,EAAwB,EAAkB,CAAC,KAAK,KAAM,wBAAwB,EAAO,GAAI,EAAO,CACtH,OAEF,GAAI,EAAE,EAAQ,IAAI,eAAe,EAAI,IAAI,WAAW,oBAAoB,CAAE,CACxE,EAAgB,KAAM,EAAwB,EAAkB,CAAC,KAAK,KAAM,qDAAsD,EAAO,CACzI,OAEF,GAAI,EAAa,KAAM,EAAY,GAAK,KAAK,OAC3C,OACF,EAAa,KAAM,EAAa,KAAK,KAAK,CAC1C,IAAM,EAAY,IAAI,MAAM,OAAO,CACnC,IAAK,EAAM,EAAa,KAAM,EAAQ,GAAK,MAAQ,EAAI,KAAK,KAAM,EAAU,CAAE,KAAK,cAAc,EAAU,CAAE,OAAO,GAAQ,UAAY,CAAC,GAAQ,EAAE,cAAe,GAAO,CACvK,EAAgB,KAAM,EAAwB,EAAkB,CAAC,KAAK,KAAM,uDAAwD,EAAO,CAAE,KAAK,OAAO,CACzJ,OAEF,IAAM,EAAU,IAAI,YAAe,EAAS,EAAK,WAAW,CACxD,EAAO,CAAC,EACZ,EAAG,CACD,GAAM,CAAE,OAAM,SAAU,MAAM,EAAO,MAAM,CAC3C,GAAS,EAAa,KAAM,EAAQ,CAAC,KAAK,EAAQ,OAAO,EAAO,CAAE,OAAQ,CAAC,EAAM,CAAC,CAAC,CAAE,IAAS,EAAO,CAAC,EAAG,EAAa,KAAM,EAAQ,CAAC,OAAO,CAAE,EAAgB,KAAM,EAAwB,GAAqB,CAAC,KAAK,KAAK,QACrN,IACT,CAAE,EAAa,KAAM,GAAgB,GAAQ,CAC7C,EAAa,KAAM,EAAa,IAAK,GAAE,CAAE,EAAE,EAAI,OAAS,cAAgB,EAAI,OAAS,YAAc,EAAgB,KAAM,EAAwB,GAAqB,CAAC,KAAK,KAAM,EAAa,EAAI,CAAC,EACpM,CAAE,EAAa,KAAM,GAAW,GAAU,CAC1C,OAAO,EAAM,IAAM,UAAY,EAAa,KAAM,EAAc,EAAM,GAAG,CACzE,IAAM,EAAe,IAAI,aAAa,EAAM,OAAS,UAAW,CAC9D,KAAM,EAAM,KACZ,OAAQ,EAAa,KAAM,EAAa,CAAG,EAAa,KAAM,EAAa,CAAC,OAAS,EAAa,KAAM,EAAK,CAAC,OAC9G,YAAa,EAAM,IAAM,GAC1B,CAAC,CACF,EAAa,KAAM,EAAW,GAAK,CAAC,EAAM,OAAS,EAAM,QAAU,YAAc,EAAa,KAAM,EAAW,CAAC,KAAK,KAAM,EAAa,CAAE,KAAK,cAAc,EAAa,EAC1K,CAAE,EAAa,KAAM,GAAiB,GAAU,CAChD,EAAa,KAAM,EAAoB,EAAM,EAC7C,CAAE,EAAa,KAAM,OAAkB,CACvC,EAAa,KAAM,EAAiB,IAAK,GAAE,CAAE,EAAa,KAAM,EAAY,GAAK,KAAK,YAAc,EAAgB,KAAM,EAAwB,GAAW,CAAC,KAAK,KAAK,EACxK,CACF,GAAI,CACF,GAAI,aAAe,IACjB,EAAa,KAAM,EAAM,EAAI,SACtB,OAAO,GAAO,SACrB,EAAa,KAAM,EAAM,IAAI,IAAI,EAAK,IAAY,CAAC,CAAC,MAEpD,MAAU,MAAM,cAAc,MAC1B,CACN,MAAM,GAAY,6CAA6C,CAEjE,EAAa,KAAM,EAAS,EAAa,CACvC,QAAS,EAAa,KAAM,GAAS,CACrC,QAAS,EAAa,KAAM,GAAe,CAC5C,CAAC,CAAC,CAAE,EAAa,KAAM,EAAa,KAAK,WAAW,CAAE,EAAa,KAAM,EAAoB,IAAI,CAAE,EAAa,KAAM,EAAc,GAA2D,OAAsB,WAAW,MAAM,CAAE,EAAa,KAAM,EAAwB,GAA2D,iBAAgC,CAAC,EAAE,CAAE,EAAgB,KAAM,EAAwB,GAAW,CAAC,KAAK,KAAK,CAY1b,IAAI,YAAa,CACf,OAAO,EAAa,KAAM,EAAY,CASxC,IAAI,KAAM,CACR,OAAO,EAAa,KAAM,EAAK,CAAC,KAOlC,IAAI,iBAAkB,CACpB,OAAO,EAAa,KAAM,EAAiB,CAG7C,IAAI,SAAU,CACZ,OAAO,EAAa,KAAM,EAAS,CAErC,IAAI,QAAQ,EAAO,CACjB,EAAa,KAAM,EAAU,EAAM,CAGrC,IAAI,WAAY,CACd,OAAO,EAAa,KAAM,EAAW,CAEvC,IAAI,UAAU,EAAO,CACnB,EAAa,KAAM,EAAY,EAAM,CAGvC,IAAI,QAAS,CACX,OAAO,EAAa,KAAM,EAAQ,CAEpC,IAAI,OAAO,EAAO,CAChB,EAAa,KAAM,EAAS,EAAM,CAEpC,iBAAiB,EAAM,EAAU,EAAS,CACxC,IAAM,EAAS,EACf,MAAM,iBAAiB,EAAM,EAAQ,EAAQ,CAE/C,oBAAoB,EAAM,EAAU,EAAS,CAC3C,IAAM,EAAS,EACf,MAAM,oBAAoB,EAAM,EAAQ,EAAQ,CASlD,OAAQ,CACN,EAAa,KAAM,EAAgB,EAAI,aAAa,EAAa,KAAM,EAAgB,CAAC,CAAE,EAAa,KAAM,EAAY,GAAK,KAAK,SAAW,EAAa,KAAM,EAAY,EAAI,EAAa,KAAM,EAAY,CAAC,OAAO,CAAE,EAAa,KAAM,EAAa,KAAK,OAAO,CAAE,EAAa,KAAM,EAAa,IAAK,GAAE,IAGnT,EAA8B,IAAI,QAAW,EAAuB,IAAI,QAAW,EAA+B,IAAI,QAAW,EAAmC,IAAI,QAAW,EAAyB,IAAI,QAAW,EAAqC,IAAI,QAAW,EAAkC,IAAI,QAAW,EAA+B,IAAI,QAAW,EAA8B,IAAI,QAAW,EAA0B,IAAI,QAAW,EAA2B,IAAI,QAAW,EAA6B,IAAI,QAAW,EAA0B,IAAI,QAAW,EAAyC,IAAI,QAKhnB,GAAa,UAAW,CACtB,EAAa,KAAM,EAAa,KAAK,WAAW,CAAE,EAAa,KAAM,EAAa,IAAI,gBAAkB,CAAE,EAAa,KAAM,EAAO,CAAC,EAAa,KAAM,EAAK,CAAE,EAAgB,KAAM,EAAwB,GAAqB,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,EAAa,KAAM,GAAiB,CAAC,CAAC,MAAM,EAAa,KAAM,GAAc,CAAC,EACjU,GAAmC,IAAI,QAAW,GAAgC,IAAI,QAMzF,GAAuB,UAAW,CAEhC,IAAM,EAAO,CAGX,KAAM,OACN,SAAU,SACV,QAAS,CAAE,OAAQ,oBAAqB,GAAG,EAAa,KAAM,EAAa,CAAG,CAAE,gBAAiB,EAAa,KAAM,EAAa,CAAE,CAAG,IAAK,GAAG,CAC9I,MAAO,WACP,OAAc,EAAa,KAAM,EAAY,EAAwB,OACtE,CACD,MAAO,WAAY,aAAe,EAAK,YAAc,KAAK,gBAAkB,UAAY,eAAgB,GACvG,GAA2B,IAAI,QAAW,GAAiC,IAAI,QAOlF,EAAoB,SAAS,EAAS,EAAM,CAC1C,IAAI,EACJ,EAAa,KAAM,EAAY,GAAK,KAAK,QAAU,EAAa,KAAM,EAAa,KAAK,OAAO,CAC/F,IAAM,EAAa,IAAI,EAAW,QAAS,CAAE,OAAM,UAAS,CAAC,EAC5D,EAAK,EAAa,KAAM,EAAS,GAAK,MAAQ,EAAG,KAAK,KAAM,EAAW,CAAE,KAAK,cAAc,EAAW,EAQ1G,GAAuB,SAAS,EAAS,EAAM,CAC7C,IAAI,EACJ,GAAI,EAAa,KAAM,EAAY,GAAK,KAAK,OAC3C,OACF,EAAa,KAAM,EAAa,KAAK,WAAW,CAChD,IAAM,EAAa,IAAI,EAAW,QAAS,CAAE,OAAM,UAAS,CAAC,EAC5D,EAAK,EAAa,KAAM,EAAS,GAAK,MAAQ,EAAG,KAAK,KAAM,EAAW,CAAE,KAAK,cAAc,EAAW,CAAE,EAAa,KAAM,EAAiB,WAAW,EAAa,KAAM,GAAW,CAAE,EAAa,KAAM,EAAmB,CAAC,CAAC,EAChO,GAA6B,IAAI,QAKpC,GAAY,WAAa,EAKzB,GAAY,KAAO,EAKnB,GAAY,OAAS,EACrB,SAAS,IAAa,CACpB,IAAM,EAAM,aAAc,WAAa,WAAW,SAAW,IAAK,GAClE,OAAO,GAAO,OAAO,GAAO,UAAY,YAAa,GAAO,OAAO,EAAI,SAAW,SAAW,EAAI,QAAU,IAAK,GC5PlH,IAAa,GAAb,KAAqC,CACnC,OACA,QACA,gBACA,WAEA,YAAY,EAA6B,CACvC,KAAK,QAAUA,EAAO,QACtB,KAAK,OAASA,EAAO,OACrB,KAAK,WAAaA,EAAO,WAEzB,EAAO,KAAK,sCAAuC,CACjD,QAAS,KAAK,QACd,UAAW,CAAC,CAAC,KAAK,OAClB,WAAY,KAAK,WAClB,CAAC,CASJ,gBAAyB,CAMvB,OALI,KAAK,gBACA,KAAK,gBAIP,aADY,KAAK,WAAa,KAAK,KAAK,WAAW,GAAK,GAChC,+EAUjC,MAAM,WAA8B,CAElC,GAAI,CAAC,KAAK,QACR,MAAO,GAIT,GAAI,KAAK,OACP,GAAI,CACF,IAAM,EAAW,MAAM,KAAK,OAAO,WAAW,CAW9C,OAVI,EAAS,SAAW,SAElB,EAAS,OACX,KAAK,gBAAkB,EAAS,OAGhC,KAAK,gBAAkB,aADJ,KAAK,WAAa,KAAK,KAAK,WAAW,GAAK,GAChB,+EAE1C,IAEF,SACA,EAAO,CAOd,OANA,EAAO,MAAM,4DAA6D,CACxE,MAAO,OAAO,EAAM,CACrB,CAAC,CAIK,GAMX,OADA,EAAO,KAAK,2DAA2D,CAChE,KClEE,GAAb,KAAiC,CAC/B,iBACA,OAEA,YAAY,EAAmC,CAC7C,KAAK,OAASC,EACd,KAAK,iBAAmB,KAAK,qBAAqB,CAClD,EAAO,KAAK,kCAAmC,CAC7C,UAAWA,EAAO,UAClB,IAAKA,EAAO,IACb,CAAC,CASJ,MAAM,WAA4C,CAChD,GAAI,CAEF,IAAM,EAAW,MAAM,KAAK,sBAAsB,CAMlD,OAJA,EAAO,MAAM,kCAAmC,CAC9C,OAAQ,EAAS,OAClB,CAAC,CAEK,QACA,EAAO,CAMd,OALA,EAAO,KAAK,kEAAmE,CAC7E,MAAO,OAAO,EAAM,CACrB,CAAC,CAGK,CAAE,OAAQ,QAAS,EAQ9B,qBAAsC,CACpC,OAAO,EAAmB,KAAK,OAAO,YAAY,CAUpD,MAAc,sBAAuD,CACnE,IAAM,EAAa,IAAI,gBACjB,EAAU,eAAiB,EAAW,OAAO,CAAE,KAAK,OAAO,UAAU,CAE3E,GAAI,CAEF,IAAM,EAAW,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,OAAO,OAAO,OAAO,KAAK,mBAEhE,EAAW,MAAM,MAAM,EAAU,CACrC,QAAS,CACP,GAAG,KAAK,OAAO,QAChB,CACD,OAAQ,OACR,OAAQ,EAAW,OACpB,CAAC,CAEF,GAAI,CAAC,EAAS,GACZ,MAAU,MAAM,mCAAmC,EAAS,OAAO,IAAI,EAAS,aAAa,CAG/F,IAAM,EAAU,MAAM,EAAS,MAAM,CAGrC,GAAI,CAAC,EAAO,QAAU,CAAC,CAAC,QAAS,QAAQ,CAAC,SAAS,EAAO,OAAO,CAC/D,MAAU,MAAM,mDAAmD,EAAO,OAAO,GAAG,CAGtF,OAAO,QACA,EAAO,CAId,MAHI,aAAiB,OAAS,EAAM,OAAS,aACjC,MAAM,wCAAwC,KAAK,OAAO,UAAU,IAAI,CAE9E,SACE,CACR,aAAa,EAAQ,ICzDd,GAAb,KAAkC,CAChC,WAEA,YAAY,EAAsC,CAChD,KAAK,WAAa,EAAQ,WAM5B,MAAM,OAAO,EAAkC,CAE7C,IAAM,EAAgB,EAAa,EAAU,CACvC,EAAW,KAAK,YAAY,EAAc,CAEhD,GAAI,CACF,MAAM,EAAG,EAAS,CAClB,EAAO,MAAM,6BAA8B,CAAE,WAAU,UAAW,EAAe,CAAC,OAC3E,EAAO,CACT,EAAgC,OAAS,UAC5C,EAAO,KAAK,sCAAuC,CACjD,MAAO,OAAO,EAAM,CACpB,WACA,UAAW,EACZ,CAAC,EAQR,gBAAgB,EAAyC,CACvD,MAAO,CAAC,CAAC,EAAY,QAAQ,cAM/B,oBAAoB,EAAyC,CAC3D,MAAO,CAAC,CAAC,EAAY,QAAQ,cAAgB,CAAC,KAAK,qBAAqB,EAAY,CAOtF,qBAAqB,EAAyC,CAC5D,GAAI,CAAC,EAAY,QAAU,CAAC,EAAY,iBACtC,MAAO,GAGT,GAAM,CAAE,cAAe,EAAY,OACnC,GAAI,CAAC,EAEH,MAAO,GAIT,IAAM,EADa,IAAI,KAAK,EAAY,iBAAiB,CAAC,SAAS,CACpC,EAAa,IACtC,EAAM,KAAK,KAAK,CAGhB,EAAY,GAAO,EAAY,IAUrC,OARA,EAAO,MAAM,kCAAmC,CAC9C,UAAW,IAAI,KAAK,EAAU,CAAC,aAAa,CAC5C,UAAW,EACX,YACA,IAAK,IAAI,KAAK,EAAI,CAAC,aAAa,CAChC,WAAY,EAAY,iBACzB,CAAC,CAEK,EAOT,MAAM,KAAK,EAA2D,CAEpE,IAAM,EAAgB,EAAa,EAAU,CACvC,EAAW,KAAK,YAAY,EAAc,CAEhD,GAAI,CACF,IAAM,EAAU,MAAM,EAAS,EAAU,QAAQ,CAC3C,EAAc,KAAK,MAAM,EAAQ,CAKvC,GAD4B,EAAa,EAAY,UAAU,GACnC,EAAe,CACzC,EAAO,KAAK,sCAAuC,CACjD,SAAU,EACV,WACA,MAAO,EAAY,UACpB,CAAC,CACF,OAUF,OAPA,EAAO,MAAM,4BAA6B,CACxC,SAAU,EAAY,YAAY,UAClC,gBAAiB,CAAC,CAAC,EAAY,QAAQ,cACvC,UAAW,CAAC,CAAC,EAAY,OACzB,UAAW,EACZ,CAAC,CAEK,QACA,EAAO,CACd,GAAK,EAAgC,OAAS,SAAU,CAEtD,EAAO,MAAM,8BAA+B,CAAE,UAAW,EAAe,CAAC,CACzE,OAGF,EAAO,KAAK,oCAAqC,CAC/C,MAAO,OAAO,EAAM,CACpB,WACA,UAAW,EACZ,CAAC,CACF,QAOJ,MAAM,KAAK,EAA+C,CACxD,MAAM,KAAK,kBAAkB,CAE7B,IAAM,EAAW,KAAK,YAAY,EAAY,UAAU,CAClD,EAAU,KAAK,UAAU,EAAa,KAAM,EAAE,CAEpD,GAAI,CACF,MAAM,EAAU,EAAU,EAAS,CAAE,SAAU,QAAS,KAAM,IAAO,CAAC,CAEtE,MAAM,GAAM,EAAU,IAAM,CAE5B,EAAO,MAAM,4BAA6B,CACxC,SAAU,EAAY,YAAY,UAClC,WACA,UAAW,CAAC,CAAC,EAAY,OACzB,UAAW,EAAY,UACxB,CAAC,OACK,EAAO,CAMd,MALA,EAAO,MAAM,6BAA8B,CACzC,MAAO,OAAO,EAAM,CACpB,WACA,UAAW,EAAY,UACxB,CAAC,CACI,GAQV,MAAM,OACJ,EACA,EAC4B,CAE5B,IAAM,EAAgB,EAAa,EAAU,CACvC,EAAW,MAAM,KAAK,KAAK,EAAc,CACzC,EAAM,IAAI,MAAM,CAAC,aAAa,CAE9BC,EAAiC,EACnC,CACE,GAAG,EACH,GAAG,EAEH,UAAW,EACX,UAAW,EACZ,CACD,CACE,UAAW,EACX,UAAW,EACX,UAAW,EACX,GAAG,EACJ,CAGL,OADA,MAAM,KAAK,KAAK,EAAY,CACrB,EAMT,MAAc,kBAAkC,CAC9C,GAAI,CACF,MAAM,GAAM,KAAK,WAAY,CAAE,KAAM,IAAO,UAAW,GAAM,CAAC,OACvD,EAAO,CAEd,GAAK,EAAgC,OAAS,SAC5C,MAAM,EAKV,GAAI,CACF,MAAM,EAAgB,KAAK,WAAW,OAC/B,EAAO,CACd,EAAO,KAAK,kDAAmD,CAC7D,MAAO,OAAO,EAAM,CACpB,WAAY,KAAK,WAClB,CAAC,EAQN,YAAoB,EAA2B,CAE7C,OADa,GAAW,SAAS,CAAC,OAAO,EAAU,CAAC,OAAO,MAAM,CACrD,UAAU,EAAG,GAAG,CAM9B,YAAoB,EAA2B,CAC7C,IAAM,EAAM,KAAK,YAAY,EAAU,CACvC,OAAO,GAAK,KAAK,WAAY,GAAG,EAAI,OAAO,GCpN/C,SAAgB,GACd,EACA,GAA2B,GAAG,IAAS,WAAW,MAAM,GAAG,EAAK,CACrD,CACX,OAAO,MAAO,EAAK,IAAS,CAM1B,IAAM,EAAqB,IAAI,QAAQ,GAAM,QAAQ,CAAC,IAAI,gBAAgB,CAEpE,EAAY,GAA4C,CAC5D,IAAM,EAAU,IAAI,QAAQ,GAAM,QAAQ,CAW1C,OAVIC,EAEF,EAAQ,IAAI,gBAAiB,UAAUA,IAAS,CACtC,GAGV,EAAQ,OAAO,gBAAgB,CAI1B,CAAE,GAAG,EAAM,UAAS,EAGvB,EAAS,EAAK,kBAAkB,CAClC,EAAW,MAAM,EAAU,EAAK,EAAS,EAAO,CAAC,CASrD,GAAI,CAAC,EAAS,GAAI,CAChB,IAAI,EAAc,GAClB,GAAI,CACF,GAAe,MAAM,EAAS,OAAO,CAAC,MAAM,EAAE,MAAM,EAAG,IAAI,MACrD,EAGR,IAAM,EAAO,CACX,cACA,YAAa,EAAS,QAAQ,IAAI,eAAe,EAAI,IAAA,GACrD,UAAW,uBACX,OAAQ,OAAO,GAAM,QAAU,MAAM,CAAC,aAAa,CACnD,cAAe,IAAI,QAAQ,GAAM,QAAQ,CAAC,IAAI,SAAS,EAAI,IAAA,GAC3D,eAAgB,EAAQ,GAAW,EACnC,OAAQ,EAAS,OACjB,IAAK,GAAkB,EAAI,CAC3B,gBAAiB,EAAS,QAAQ,IAAI,mBAAmB,EAAI,IAAA,GAC9D,CACG,EAAS,SAAW,IACtB,EAAO,KAAK,6CAA8C,EAAK,CAE/D,EAAO,KAAK,4BAA6B,EAAK,CAQlD,GAAI,EAAS,SAAW,KAAO,EAAQ,CACrC,EAAO,KAAK,+DAAgE,CAC1E,UAAW,uCACZ,CAAC,CACF,IAAM,EAAY,MAAM,EAAK,kBAAkB,EAAO,CAClD,GAAa,IAAc,GAC7B,EAAO,KAAK,yDAA0D,CACpE,UAAW,yCACZ,CAAC,CACF,EAAW,MAAM,EAAU,EAAK,EAAS,EAAU,CAAC,EAEpD,EAAO,KAAK,6DAA8D,CACxE,UAAW,sCACZ,CAAC,CAIN,OAAO,GAIX,SAAgB,GACd,EACA,EACA,EACmB,CACnB,IAAI,EAAgB,EAChBC,EAAsD,KAE1D,MAAO,CACL,qBAAwB,EACxB,kBAAoB,GAId,IACJ,GAAmB,SAAY,CAC7B,GAAI,CAKF,IAAM,GADS,MAAM,EAAgB,KAAK,EAAU,GACzB,QAAQ,aACnC,GAAI,GAAc,IAAe,EAM/B,OALA,EAAO,KAAK,kEAAmE,CAC7E,UAAW,+BACX,YACD,CAAC,CACF,EAAgB,EACT,EAGT,IAAM,EAAS,MAAM,EAAsB,EAAW,EAAgB,CAUtE,OATI,GAAQ,OAAO,cACjB,EAAgB,EAAO,OAAO,aACvB,GAOT,cACQ,CACR,EAAkB,SAElB,CACG,GAET,UAAY,GAAW,CACrB,EAAgB,GAEnB,CAQH,SAAS,GAAkB,EAAsB,CAC/C,GAAI,CACF,IAAM,EAAI,OAAO,GAAQ,SAAW,EAAM,OAAO,EAAI,CAC/C,EAAI,IAAI,IAAI,EAAE,CACpB,MAAO,GAAG,EAAE,SAAS,EAAE,gBACjB,CACN,MAAO,qBC/LX,eAAsB,GACpB,EACA,EAAmF,EAAE,CACnE,CAClB,IAAM,EAAO,EAAK,MAAQ,OACpB,EAAY,EAAK,WAAa,IAC9B,EAAQ,EAAK,OAAS,QAAQ,MAC9B,EAAS,EAAK,QAAU,QAAQ,OAKtC,OAFK,EAA8B,MAAc,GAE1C,IAAI,QAAkB,GAAY,CACvC,IAAI,EAAU,GACV,EAAU,GAIV,EAAU,GACR,EAAK,EAAgB,CAAE,QAAO,CAAC,CAE/B,MAAe,CACf,IACJ,EAAU,GACV,aAAa,EAAM,CACnB,EAAG,mBAAmB,OAAO,CAC7B,EAAG,OAAO,CACV,EAAQ,EAAQ,GAIZ,EAAQ,WAAW,EAAQ,EAAU,CAC3C,EAAM,SAAS,CAEf,EAAG,GAAG,OAAS,GAAS,CACtB,GAAI,EAAS,OACb,IAAM,EAAU,EAAK,MAAM,CAC3B,GAAI,CAAC,EAAS,OACd,IAAIC,EACJ,GAAI,CACF,EAAM,KAAK,MAAM,EAAQ,MACnB,CACN,OAIE,GAAO,EAAI,KAAO,IAAA,IAAa,EAAI,KAAO,OAC5C,EAAU,GAIV,EAAU,GACV,EAAG,mBAAmB,OAAO,CAC7B,EAAO,MACL,GAAG,KAAK,UAAU,CAAE,MAAO,CAAE,OAAM,QAAS,EAAQ,CAAE,GAAI,EAAI,GAAI,QAAS,MAAO,CAAC,CAAC,QAC9E,CACJ,GAAQ,EAEX,GAEH,CAEF,EAAM,KAAK,MAAO,EAAO,CACzB,EAAM,KAAK,QAAS,EAAO,EAC3B,CC5DJ,MAAM,GAAqB,IAAI,IAAY,CACzC,YACA,eACA,eACA,aACA,YACA,eACA,WACA,cACA,YACA,QACA,SACA,YACA,uBACA,0BACA,0BACA,iBACD,CAAC,CASI,GAAsB,IAAI,IAAY,CAC1C,YACA,eACA,YACA,eACA,WACA,cACA,YACA,0BACD,CAAC,CAcI,GACJ,kQAQI,GAA0B,8CAM1B,GACJ,4IACI,GAA2B,qEAQjC,SAAgB,EAAiB,EAAoC,CACnE,OAAO,GAAa,EAAO,IAAI,QAAkB,CASnD,SAAgB,GAAsB,EAAoC,CACxE,OAAO,GAAc,EAAO,IAAI,QAAkB,CAQpD,SAAgB,GAAW,EAAyB,CAClD,GAAI,aAAiB,EAAmB,MAAO,GAC/C,GAAI,GAAS,OAAO,GAAU,SAAU,CACtC,IAAM,EAAU,EAA8B,OACxC,EAAQ,EAA4B,KACpC,EAAW,EAA+B,QAMhD,GALI,IAAW,KAAO,IAAS,KAK3B,GAAW,6EAA6E,KAAK,OAAO,EAAQ,CAAC,CAC/G,MAAO,GAEX,MAAO,GAWT,SAAgB,GAAmB,EAAyB,CAM1D,GAAI,aAAiB,EAAU,MAAO,GACtC,IAAM,EAAQ,EAAiB,EAAM,CAGrC,OAFI,GAAS,GAAoB,IAAI,EAAM,EACvC,GAAoB,EAAM,CAAS,GAChC,GAAwB,KAAK,GAAU,EAAM,CAAC,CAUvD,SAAgB,GAA2B,EAAyB,CAElE,GADI,aAAiB,GACjB,GAAW,EAAM,CAAE,MAAO,GAE9B,IAAM,EAAQ,EAAiB,EAAM,CAIrC,GAHI,GAAS,GAAmB,IAAI,EAAM,EAGtC,GAAoB,EAAM,CAAE,MAAO,GAGvC,IAAM,EAAS,GAAsB,EAAM,CAC3C,GAAI,IAAW,IAAA,IAAa,GAAU,IAAK,MAAO,GAElD,IAAM,EAAU,GAAU,EAAM,CAGhC,MAFA,GAAI,EAAQ,OAAS,GAAK,GAAuB,KAAK,EAAQ,EAYhE,SAAgB,GAAoB,EAAyB,CAC3D,IAAM,EAAU,GAAU,EAAM,CAKhC,GAAI,aAAiB,EAAU,OAAO,GAAyB,KAAK,EAAQ,CAC5E,IAAM,EAAS,GAAsB,EAAM,CAI3C,OAHI,IAAW,KACX,IAAW,KAAO,GAAkB,KAAK,EAAQ,CAAS,GAEvD,GAAkB,KAAK,EAAQ,CAGxC,SAAS,GAAU,EAAwB,CAMzC,OALI,aAAiB,MAAc,EAAM,SAAW,GAChD,OAAO,GAAU,SAAiB,EAClC,GAAS,OAAO,GAAU,UAAY,YAAa,EAC9C,OAAQ,EAAgC,SAAW,GAAG,CAExD,GAGT,SAAS,GAAa,EAAgB,EAA2C,CAE/E,GADqB,OAAO,GAAU,WAAlC,GACA,EAAK,IAAI,EAAM,CAAE,OACrB,EAAK,IAAI,EAAM,CAEf,IAAM,EAAQ,EAAgC,KAG9C,GAAI,OAAO,GAAS,UAAY,oBAAoB,KAAK,EAAK,CAC5D,OAAO,EAGT,IAAK,IAAM,IAAO,CAAC,QAAS,WAAY,OAAQ,OAAO,CAAW,CAChE,IAAM,EAAQ,GAAc,EAAkC,GAAM,EAAK,CACzE,GAAI,EAAO,OAAO,GAKtB,SAAS,GAAc,EAAgB,EAA2C,CAEhF,GADqB,OAAO,GAAU,WAAlC,GACA,EAAK,IAAI,EAAM,CAAE,OACrB,EAAK,IAAI,EAAM,CAMf,IAAM,EACJ,aAAiB,EAAY,CAAC,SAAU,aAAa,CAAc,CAAC,SAAU,aAAc,OAAO,CACrG,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAS,EAAkC,GAGjD,GAAI,OAAO,GAAU,UAAY,GAAS,KAAO,GAAS,IAAK,OAAO,EAGxE,IAAK,IAAM,IAAO,CAAC,QAAS,WAAY,OAAQ,OAAO,CAAW,CAChE,IAAM,EAAQ,GAAe,EAAkC,GAAM,EAAK,CAC1E,GAAI,IAAU,IAAA,GAAW,OAAO,GCvNpC,MAAM,GAAgB,GAA8B,IAAI,QAAS,GAAY,WAAW,EAAS,EAAG,CAAC,CAQrG,eAAsB,GACpB,EACA,EACA,EAA2B,EAAE,CACjB,CACZ,IAAM,EAAc,EAAM,aAAe,GACnC,EAAQ,EAAM,OAAS,GACvB,EAAM,EAAM,KAAO,KAAK,IAExB,EAAQ,GAAK,CACf,EAAU,EAEd,OACE,GAAI,CACF,OAAO,MAAM,GAAW,OACjB,EAAO,CAEd,GADA,GAAW,EACP,EAAU,EAAQ,YAAc,CAAC,EAAY,EAAM,CACrD,MAAM,EAGR,IAAI,EAAU,GAAe,EAAS,EAAQ,CAE9C,GAAI,EAAQ,aAAe,EAAG,CAC5B,IAAM,EAAY,EAAQ,cAAgB,GAAK,CAAG,GAElD,GAAI,GAAa,EAAG,MAAM,EAC1B,EAAU,KAAK,IAAI,EAAS,EAAU,CAGxC,EAAM,UAAU,CAAE,UAAS,UAAS,QAAO,CAAC,CAC5C,MAAM,EAAM,EAAQ,EAS1B,SAAS,GAAe,EAAiB,EAAsC,CAC7E,IAAM,EAAO,KAAK,IAAI,EAAQ,eAAiB,IAAM,EAAU,GAAI,EAAQ,WAAW,CAChF,EAAS,EAAO,IAAO,KAAK,QAAQ,CAAG,EAAI,GACjD,OAAO,KAAK,IAAI,EAAG,KAAK,MAAM,EAAO,EAAO,CAAC,CCf/C,SAAgB,GAAyB,EAAoD,CAC3F,IAAM,EAAM,EAAQ,KAAO,KAAK,IAE5B,EAAU,EAAQ,QAIlB,EAAa,EACb,EAAgB,KAChBC,EAAwC,KACxC,EAAS,GACT,EAAe,GACbC,EAAqD,EAAE,CAEvD,EAAc,SAA8B,CAChD,EAAO,KAAK,qDAAsD,CAChE,UAAW,iCACZ,CAAC,CACE,GAGF,EAAO,KAAK,yFAA0F,CACpG,UAAW,+CACZ,CAAC,CAEJ,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,WAAW,CAItC,GAAI,EAIF,OAHA,MAAM,QAAQ,SAAS,CACpB,SAAW,EAAK,OAAO,CAAC,CACxB,UAAY,GAAG,CACX,GAIT,IAAK,IAAM,KAAQ,EACjB,EAAK,uBAAuB,GAAG,EAAK,CAEtC,IAAM,EAAM,EASZ,MARA,GAAU,EACV,GAAc,EAGT,QAAQ,SAAS,CACnB,SAAW,EAAI,OAAO,CAAC,CACvB,UAAY,GAAG,CAClB,EAAO,KAAK,uBAAwB,CAAE,UAAW,mCAAoC,CAAC,CAC/E,SACA,EAAO,CAMd,OALA,EAAO,KAAK,4BAA6B,CACvC,MAAO,EAAiB,EAAM,CAC9B,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAC7D,UAAW,gCACZ,CAAC,CACK,KAIL,EAAqB,GAErB,EAAa,EAAsB,QAAQ,QAAQ,GAAK,CAExD,IAEA,GAAK,CAAG,EAAgB,EAAQ,WAAmB,QAAQ,QAAQ,GAAM,EAC7E,EAAgB,GAAK,CACrB,EAAe,GAAa,CAAC,YAAc,CACzC,EAAe,MACf,CACK,IAGH,EAAU,MAAU,EAAqB,IAAuD,CACpG,IAAM,EAAM,EACZ,GAAI,CACF,OAAO,MAAM,EAAO,EAAQ,OACrB,EAAO,CAEd,GAAI,GAAW,EADQ,EAAa,IACF,CAAC,GAA2B,EAAM,CAAG,MAAM,EAI7E,GAHoB,MAAM,EAAkB,EAAI,GAG5B,GAAc,GAAmB,EAAM,EACzD,OAAO,MAAM,EAAO,EAAQ,CAE9B,MAAM,IAkCV,MA9Be,CACb,UAAW,GAAG,IAAyC,EAAQ,GAAQ,GAAM,EAAE,SAAS,GAAG,EAAK,CAAC,CACjG,OAAQ,GAAG,KACT,EAAS,GACF,EAAQ,MAAM,GAAG,EAAK,EAE/B,UAAW,GAAG,IAAyC,EAAQ,GAAO,GAAM,EAAE,SAAS,GAAG,EAAK,CAAC,CAChG,WAAY,GAAG,IAA0C,EAAQ,GAAO,GAAM,EAAE,UAAU,GAAG,EAAK,CAAC,CACnG,aAAc,GAAG,IAA4C,EAAQ,GAAO,GAAM,EAAE,YAAY,GAAG,EAAK,CAAC,CACzG,eAAgB,GAAG,IAA8C,EAAQ,GAAO,GAAM,EAAE,cAAc,GAAG,EAAK,CAAC,CAC/G,uBAAwB,GAAG,IACzB,EAAQ,GAAO,GAAM,EAAE,sBAAsB,GAAG,EAAK,CAAC,CACxD,WAAY,GAAG,IAA0C,EAAQ,GAAO,GAAM,EAAE,UAAU,GAAG,EAAK,CAAC,CAInG,cAAe,GAAG,IAA6C,EAAQ,GAAQ,GAAM,EAAE,aAAa,GAAG,EAAK,CAAC,CAC7G,cAAe,GAAG,IAA6C,EAAQ,GAAO,GAAM,EAAE,aAAa,GAAG,EAAK,CAAC,CAC5G,wBAAyB,GAAG,KAC1B,EAAqB,KAAK,EAAK,CACxB,EAAQ,uBAAuB,GAAG,EAAK,EAEhD,mBAAoB,GAAG,KACrB,EAAe,GACR,EAAQ,GAAO,GAAM,EAAE,kBAAkB,GAAG,EAAK,CAAC,EAE3D,qBAAsB,GAAG,IACvB,EAAQ,GAAO,GAAM,EAAE,oBAAoB,GAAG,EAAK,CAAC,CACvD,CC7LH,IAAa,GAAb,cAAyC,CAAU,CACjD,OAAiB,GACjB,mBAA6B,EAE7B,aAAc,CACZ,MAAM,CAAE,WAAY,GAAO,CAAC,CAG9B,OAAO,EAA+D,CAEhE,KAAK,OAAO,MAAM,CAAC,WAAW,IAAI,CACpC,EAAS,KAAM,OAAO,KAAK,KAAK,OAAO,CAAC,EAEpC,KAAK,OAAO,MAAM,CAAC,OAAS,GAC9B,KAAK,eAAe,KAAK,OAAO,CAElC,EAAS,KAAM,KAAK,EAIxB,WAAW,EAAe,EAAmB,EAA+D,CAC1G,KAAK,QAAU,EAAM,UAAU,CAC/B,IAAM,EAAQ,KAAK,OAAO,MAAM;EAAK,CAGrC,KAAK,OAAS,EAAM,KAAK,EAAI,GAE7B,IAAMC,EAAsB,EAAE,CAC9B,IAAK,IAAM,KAAQ,EACb,EAAK,MAAM,CAAC,WAAW,IAAI,CAC7B,EAAU,KAAK,EAAK,CACX,EAAK,MAAM,CAAC,OAAS,GAC9B,KAAK,eAAe,EAAK,CAI7B,GAAI,EAAU,OAAS,EAAG,CAExB,IAAM,EAAS,GAAG,EAAU,KAAK;EAAK,CAAC,IAEvC,EAAS,KAAM,OAAO,KAAK,EAAO,CAAC,MAEnC,EAAS,KAAM,KAAK,CAIxB,eAAuB,EAAc,CAE/B,UAAK,mBAAqB,IAI9B,IAAI,KAAK,qBAAuB,GAA0B,CACxD,KAAK,oBAAsB,EAC3B,EAAO,KAAK,4CAA6C,CACvD,UAAW,uCACX,YAAa,GACd,CAAC,CACF,OAGF,KAAK,oBAAsB,EAC3B,EAAO,KAAK,wCAAyC,CACnD,UAAW,4BACX,KAAM,EAAK,OAAS,KAAO,GAAG,EAAK,MAAM,EAAG,KAAK,CAAC,GAAK,EACxD,CAAC,ICQO,GAAb,KAAuD,CACtD,QAEA,QACA,UAMA,IAAI,KAAqB,CACxB,OAAO,KAAK,UAAU,KAAO,KAS9B,IAAI,kBAAuC,CAC1C,OAAO,KAAK,YAMb,IAAI,wBAA6C,CAChD,OAAO,KAAK,WASb,IAAI,QAAwB,CAK3B,OAJI,KAAK,cACD,KAAK,cAGN,KAAK,UAAU,QAAU,KAEjC,iBAA4C,IAAI,gBAChD,SACA,YAAkC,IAAI,EACtC,cACA,WACA,YACA,cAA4C,KAE5C,QAEA,YAAY,EAA+B,CAC1C,KAAK,cAAgB,GACjB,EAAO,SAAW,QAAU,EAAO,SAAW,gBACjD,KAAK,cAAgB,IAAI,GAE1B,KAAK,QAAU,EAAO,QAGvB,MAAM,OAAuB,CAS5B,GARA,KAAK,UAAU,CACd,KAAM,QACN,CAAC,CAME,KAAK,UAAU,IAClB,GAAI,CACH,GAAI,QAAQ,WAAa,QAAS,CAGjC,IAAM,EAAS,GAAU,WAAY,CAAC,OAAQ,OAAO,KAAK,SAAS,IAAI,CAAE,KAAM,KAAK,CAAE,CACrF,MAAO,SACP,CAAC,CACF,GAAI,EAAO,MACV,MAAM,EAAO,MAEd,GAAI,EAAO,SAAW,EACrB,MAAU,MAAM,+BAA+B,EAAO,SAAS,MAGhE,QAAQ,KAAK,CAAC,KAAK,SAAS,IAAK,UAAU,OAEpC,EAAO,CACf,EAAO,MAAM,4DAA6D,CACzE,MAAQ,EAAgB,QACxB,IAAK,KAAK,SAAS,IACnB,CAAC,CAIJ,KAAK,iBAAiB,OAAO,CAC7B,KAAK,SAAW,IAAA,GAChB,KAAK,YAAY,OAAO,CAGzB,KAAK,EAAwC,CAC5C,OAAO,IAAI,QAAS,GAAY,CAC/B,GAAI,CAAC,KAAK,UAAU,MACnB,MAAU,MAAM,gBAAgB,CAGjC,IAAM,EAAO,EAAiB,EAAQ,CAClC,KAAK,SAAS,MAAM,MAAM,EAAK,CAClC,GAAS,CAET,KAAK,SAAS,MAAM,KAAK,QAAS,EAAQ,EAE1C,CAMH,MAAM,OAAuB,CAC5B,GAAI,KAAK,SACR,MAAU,MACT,gHACA,CAGF,OAAO,IAAI,SAAS,EAAS,IAAW,CACvC,IAAM,EAAW,KAAK,cAAc,OAAS,GAcvC,EAAc,GAAY,QAAQ,WAAa,QAC/C,EAAe,EAClB,GAAqB,KAAK,cAAc,QAAQ,CAChD,KAAK,cAAc,QAChB,EAAY,GACd,KAAK,cAAc,MAAQ,EAAE,EAAE,IAAI,GAAqB,CACzD,KAAK,cAAc,MAAQ,EAAE,CAEhC,KAAK,SAAW,GACf,EACA,EACA,CACC,IAAK,KAAK,cAAc,IAKxB,SAAU,QAAQ,WAAa,QAC/B,IAAK,KAAK,cAAc,IACxB,MAAO,EACP,OAAQ,KAAK,iBAAiB,OAC9B,MAAO,CAAC,OAAQ,OAAQ,KAAK,cAAc,QAAU,UAAU,CAC/D,CACD,CAED,KAAK,SAAS,GAAG,QAAU,GAAU,CACpC,GAAI,EAAM,OAAS,aAAc,CAEhC,KAAK,WAAW,CAChB,OAGD,EAAO,MAAM,sBAAuB,CACnC,KAAM,KAAK,cAAc,KAAO,EAAc,KAAK,cAAc,KAAK,CAAG,IAAA,GACzE,QAAS,KAAK,cAAc,QAC5B,IAAK,KAAK,cAAc,IACxB,MAAO,EAAM,QACb,UAAY,EAAgC,KAC5C,WAAa,EAAgC,MAC7C,aAAe,EAAgC,QAC/C,UAAW,QACX,MAAO,KAAK,cAAc,MAC1B,cAAe,QACf,CAAC,CAEF,EAAO,EAAM,CACb,KAAK,UAAU,EAAM,EACpB,CAMF,IAAIC,EACAC,EACJ,KAAK,SAAS,GAAG,YAAe,CAC/B,EAAY,KAAK,KAAK,CACtB,EAAa,KAAK,UAAU,IAG5B,KAAK,WAAa,EAClB,KAAK,YAAc,EAInB,EAAO,KAAK,6BAA8B,CACzC,SAAU,KAAK,cAAc,MAAM,OACnC,QAAS,KAAK,cAAc,QAC5B,UAAW,6BACX,IAAK,EACL,CAAC,CACF,GAAS,EACR,CAEF,KAAK,SAAS,GAAG,SAAU,EAAM,IAAW,CAC3C,IAAM,EAAW,IAAc,IAAA,GAAY,IAAA,GAAY,KAAK,KAAK,CAAG,EAGpE,EAFkB,IAAS,GAAK,IAAW,KACd,OAAS,QACrB,4BAA6B,CAC7C,SAAU,KAAK,cAAc,MAAM,OACnC,OACA,QAAS,KAAK,cAAc,QAC5B,UAAW,4BACX,IAAK,EACL,SACA,WACA,CAAC,CAEF,KAAK,UAAU,CACd,KAAM,QACN,CAAC,CAEF,KAAK,SAAW,IAAA,GAChB,KAAK,WAAW,EACf,CAEF,KAAK,SAAS,OAAO,GAAG,QAAU,GAAU,CAC3C,EAAO,MAAM,cAAe,CAC3B,QAAS,KAAK,cAAc,QAC5B,MAAO,EAAM,QACb,UAAW,QACX,IAAK,KAAK,UAAU,IACpB,cAAe,QACf,CAAC,CAEF,KAAK,UAAU,CACd,QACA,KAAM,QACN,CAAC,CAEF,KAAK,UAAU,EAAM,EACpB,CAEF,IAAM,EAAsB,IAAI,GAEhC,KAAK,SAAS,QAAQ,KAAK,EAAoB,CAE/C,EAAoB,GAAG,OAAS,GAAU,CACzC,KAAK,UAAU,CACd,MAAO,EAAM,UAAU,CACvB,KAAM,OACN,CAAC,CAEF,KAAK,YAAY,OAAO,EAAM,CAC9B,KAAK,mBAAmB,EACvB,CAEF,EAAoB,GAAG,QAAU,GAAU,CAC1C,EAAO,MAAM,oBAAqB,CACjC,QAAS,KAAK,cAAc,QAC5B,MAAO,EAAM,QACb,UAAW,QACX,IAAK,KAAK,UAAU,IACpB,cAAe,QACf,CAAC,CAEF,KAAK,UAAU,CACd,QACA,KAAM,QACN,CAAC,CAEF,KAAK,UAAU,EAAM,EACpB,CAEE,KAAK,eAAiB,KAAK,SAAS,QACvC,KAAK,SAAS,OAAO,KAAK,KAAK,cAAc,EAE7C,CAGH,mBAA4B,CAC3B,OACC,GAAI,CACH,IAAM,EAAU,KAAK,YAAY,aAAa,CAE9C,GAAI,IAAY,KACf,MAGD,KAAK,UAAU,CACd,UACA,KAAM,UACN,CAAC,CAEF,KAAK,YAAY,EAAQ,OACjB,EAAO,CAEf,IAAIC,EAAsB,UAC1B,GAAI,CACH,IAAM,EAAM,KAAK,YAAY,aAAa,CACtC,IACH,EAAc,OAAO,QAEf,EAIR,EAAO,MAAM,2BAA4B,CACxC,QAAS,KAAK,cAAc,QAC5B,MAAQ,EAAgB,QACxB,cACA,UAAW,UACX,IAAK,KAAK,UAAU,IACpB,cAAe,QACf,CAAC,CAEF,KAAK,UAAU,CACP,QACP,KAAM,QACN,CAAC,CAEF,KAAK,UAAU,EAAe,IAkBlC,SAAgB,GAAqB,EAAmB,CAWvD,OAVI,EAAE,SAAW,GACb,CAAC,eAAe,KAAK,EAAE,CAAS,EAS7B,IADS,EAAE,QAAQ,KAAM,KAAK,CAAC,QAAQ,SAAU,OAAO,CAC5C,GCvZpB,MAOM,GAAuB,CAC3B,IACA,IACA,IACA,IACA,IACA,IACA,IACA,IACA,IACA,IACA,IACA,IACA,IACA,KACA,IACA,IACA,IACA,IACA,IACD,CAEK,GAAgB,YAEhB,GAAiB,GAAG,GAAc,mBAAiB,GAAc,IACjE,GAAmB,sDAInB,GAAO,MADE,MAFK,GAAG,GAAiB,QAAQ,GAAiB,IAEhC,mCACP,KAAK,GAAe,MAAM,GAAe,IAI7D,GAAsB,OAAO,IAAI,GAAK,8FAA8B,CAQ1E,SAAgB,GAAuB,EAAwC,CAa7E,OAZI,OAAO,GAAc,UAAY,EAAU,MAAM,GAAK,GACjD,CAAE,OAAQ,wBAAyB,MAAO,GAAO,CAGtD,EAAU,OAAS,IACd,CAAE,OAAQ,6CAAkE,MAAO,GAAO,CAG9F,GAAgB,KAAK,EAAU,CAI7B,CAAE,MAAO,GAAM,CAHb,CAAE,OAAQ,GAAe,EAAU,CAAE,MAAO,GAAO,CAc9D,SAAS,GAAe,EAA2B,CAEjD,GAAI,wBAAwB,KAAK,EAAU,CACzC,MAAO,+BAGT,GAAI,KAAK,KAAK,EAAU,CACtB,MAAO,sBAGT,IAAM,EAAgB,GAAqB,KAAM,GAAc,EAAU,SAAS,EAAU,CAAC,CAW7F,OAVI,EACK,oCAAoC,IAKzC,EAAU,WAAW,IAAI,CACpB,0DAGF,iDClFT,SAAgB,GAAuB,EAAmC,CAKxE,IAAM,EAHoB,EAAQ,aAAa,CAGZ,MAAM,QAAQ,CAAC,KAAK,EAAI,GAgB3D,OAbI,IAAa,UAAY,IAAa,aACjC,SAEL,IAAa,UAAY,IAAa,aACjC,SAEL,IAAa,WAAa,IAAa,cAClC,UAEL,IAAa,SAAW,IAAa,YAChC,QAGF,UAcT,SAAgB,GAA0B,EAAoC,CAE5E,IAAM,EAAW,EAAK,QAAQ,MAAM,CACpC,GAAI,IAAa,GACf,OAIF,IAAM,EAAU,EAAK,MAAM,EAAW,EAAE,CAKlC,EAAmB,IAAI,IAAI,yxCAqHhC,CAAC,CAEE,EAAI,EACR,KAAO,EAAI,EAAQ,QAAQ,CACzB,IAAM,EAAM,EAAQ,GAGpB,GAAI,EAAI,WAAW,IAAI,CAAE,CAGvB,GAAI,EAAI,SAAS,IAAI,CAAE,CACrB,IACA,SAIF,GAAI,EAAiB,IAAI,EAAI,CAAE,CAE7B,GAAK,EACL,SAIF,IACA,SAIF,OAAO,GAsBX,SAAgB,GAAmB,EAAiB,EAAsC,CACxF,IAAM,EAAU,GAAuB,EAAQ,CAE/C,GAAI,IAAY,UACd,MAAO,CACL,UAAW,GACX,eAAgB,GAChB,QAAS,UACV,CAIH,GAAI,CAAC,GAAQ,CAAC,EAAK,SAAS,MAAM,CAChC,MAAO,CACL,UAAW,GACX,eAAgB,GAChB,UACD,CAGH,IAAM,EAAY,GAA0B,EAAK,CAejD,OAbK,EAaE,CACL,YACA,eAAgB,GAChB,UACD,EAhBC,EAAO,MAAM,8DAA+D,CAC1E,OACA,UACA,UACD,CAAC,CACK,CACL,UAAW,GACX,eAAgB,GAChB,UACD,EAkCL,SAAgB,GAAyB,EAAmB,EAAqC,CAE/F,IAAM,EAAiB,IAAY,UAAY,SAAW,EAEpD,EAAQ,GAAuB,EAAU,CAC/C,GAAI,CAAC,EAAM,MAUT,OANA,EAAO,KAAK,6DAA8D,CACxE,UAAW,mCACX,YACA,OAAQ,EAAM,OACd,QAAS,EACV,CAAC,CACK,EAAE,CAGX,IAAIC,EACJ,GAAI,CAGF,EAAS,GACP,EACA,CAAC,QAAS,UAAW,WAAY,wBAAyB,EAAU,CACpE,CACE,SAAU,QACV,MAAO,CAAC,OAAQ,OAAQ,OAAO,CAC/B,QAAS,IACV,CACF,OACM,EAAO,CAQd,OANA,EAAO,KAAK,uCAAwC,CAClD,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAC7D,UAAW,2CACX,YACA,QAAS,EACV,CAAC,CACK,EAAE,CAGX,IAAM,EAAgB,EAAO,MAAM,CAS/BC,EACJ,GAAI,CACF,EAAe,KAAK,MAAM,EAAc,MAClC,CAON,OANA,EAAO,KAAK,mCAAoC,CAC9C,UAAW,2CACX,YACA,OAAQ,EACR,QAAS,EACV,CAAC,CACK,EAAE,CAGX,GAAI,CAAC,MAAM,QAAQ,EAAa,CAO9B,OANA,EAAO,KAAK,mCAAoC,CAC9C,UAAW,2CACX,YACA,OAAQ,EACR,QAAS,EACV,CAAC,CACK,EAAE,CAEX,IAAMC,EAAyB,EAIzBC,EAAoB,EAAE,CAC5B,IAAK,IAAM,KAAc,EAAa,CACpC,GAAI,OAAO,GAAe,SACxB,SAGF,IAAM,EAAc,EAAW,MAAM,uBAAuB,CACxD,GACF,EAAQ,KAAK,EAAY,GAAG,CAoBhC,OAhBI,EAAQ,OAAS,GACnB,EAAO,KAAK,mCAAoC,CAC9C,UACA,UAAW,uCACX,YACA,QAAS,EACV,CAAC,CACK,IAIT,EAAO,KAAK,oEAAqE,CAC/E,UAAW,oCACX,YACA,QAAS,EACV,CAAC,CACK,EAAE,ECrZX,IAAIC,GAEJ,MAAM,QAAmC,CACvC,IAAK,IAAsB,iBAC3B,YAAa,IAAsB,uBACpC,EAaD,GAAK,QAAQ,eAAe,MAAQ,EAE9B,gBAAiB,SAErB,OAAO,YAAc,IASvB,MAiBM,GAAqB,KAAO,IAOZ,CACpB,EAAO,MAAM,EAAK,WAAY,EAAK,KAAK,CAGxC,MAAM,IAAI,QAAe,GAAY,CACnC,QAAQ,OAAO,MAAM,GAAG,EAAK,WAAW,QAAW,GAAS,CAAC,EAC7D,CACE,EAAK,iBACP,MAAM,GAAuB,EAAK,OAAO,CAE3C,IAAM,EAAY,OAAkB,GAAI,GAAG,CAC3C,GAAI,CACF,MAAM,GAAU,IAAK,MACf,SAEE,CACR,GAAc,EAAU,CAE1B,QAAQ,KAAK,EAAK,SAAS,EAKvB,GAAsB,GAMN,CACpB,IAAM,EAAS,EAAK,OAAS,gBAAgB,EAAK,OAAO,GAAK,mBACxD,EACJ,4CAA4C,EAAK,OAAO,QAAQ,EAAO,0IAEzE,OAAO,GAAmB,CACxB,SAAU,GACV,WAAY,EAAK,WACjB,KAAM,CAAE,OAAQ,EAAK,OAAQ,UAAW,EAAK,UAAW,GAAI,EAAK,OAAS,CAAE,OAAQ,EAAK,OAAQ,CAAG,EAAE,CAAG,CACzG,SACA,gBAAiB,EAAK,gBACtB,WAAY,wBAAwB,IACrC,CAAC,EAGS,GAAO,SAAY,CAI9B,GAAkB,EAAO,CAMzB,GAAuB,EAAO,CAE9B,IAAM,EAAO,MAAMC,GAAM,GAAQ,QAAQ,KAAK,CAAC,CAC5C,WAAW,cAAc,CACzB,QAAQ,4BAA6B,qDAAuD,GACpF,EACJ,WAAW,UAAW,CACrB,SAAU,qBACV,KAAM,SACP,CAAC,CACD,WAAW,OAAQ,CAClB,MAAO,GACP,SAAU,uCACV,KAAM,SACP,CAAC,CACJ,CACD,QAAQ,SAAU,iCAAmC,GAC7C,EAAM,QAAQ,CACnB,mBAAoB,CAClB,SACE,gFACF,KAAM,SACP,CACD,mBAAoB,CAClB,SACE,qFACF,OAAQ,GACR,KAAM,QACP,CACD,QAAS,CACP,WAAY,mGACZ,SAAU,0DACV,OAAQ,GACR,KAAM,QACP,CACD,mBAAoB,CAClB,QAAS,CAAC,MAAO,SAAS,CAC1B,WAAY,uEACZ,SAAU,sDACV,KAAM,SACP,CACD,IAAK,CACH,aAAc,GACd,SAAU,mCACV,KAAM,SACP,CACF,CAAC,CACF,CACD,QAAQ,yBAA0B,yDAA2D,GACrF,EACJ,WAAW,UAAW,CACrB,SAAU,qBACV,KAAM,SACP,CAAC,CACD,WAAW,OAAQ,CAClB,MAAO,GACP,SAAU,uCACV,KAAM,SACP,CAAC,CACJ,CACD,IAAI,cAAc,CAClB,oBAAoB,CACnB,aAAc,GACd,0BAA2B,GAC5B,CAAC,CACD,QAAQ,CAGP,qBAAsB,CACpB,SAAU,+EACV,KAAM,SACP,CACD,MAAO,CACL,QAAS,GACT,SAAU,uBACV,KAAM,UACP,CACD,IAAK,CACH,SAAU,mEACV,OAAQ,GACR,KAAM,QACP,CACD,wBAAyB,CACvB,QAAS,IACT,SAAU,sDACV,KAAM,SACP,CACD,sBAAuB,CACrB,SAAU,yFACV,KAAM,SACP,CACD,KAAM,CACJ,SAAU,4DACV,KAAM,SACP,CACD,kBAAmB,CACjB,SAAU,6EACV,KAAM,SACP,CACD,oBAAqB,CACnB,SAAU,uFACV,KAAM,UACP,CACD,qBAAsB,CACpB,SAAU,qFACV,KAAM,SACP,CACD,cAAe,CACb,SAAU,2EACV,KAAM,SACP,CACD,MAAO,CACL,QAAS,QAAQ,WAAa,QAC9B,SAAU,sEACV,KAAM,UACP,CACF,CAAC,CACD,MAAM,CACN,YAAY,CASf,MAAM,GAAyB,EAAO,eAAe,CAGrD,IAAIC,EAIA,EAAkB,GAClBC,EACAC,EACAC,EAEAC,EACEC,EAAwC,EAAE,CAG1CC,GAAoC,EAAE,CAC5C,GAAI,EAAK,IACP,IAAK,IAAM,KAAU,EAAK,IAAiB,CACzC,GAAM,CAAC,EAAK,GAAG,GAAc,EAAO,MAAM,IAAI,CAC1C,GAAO,EAAW,OAAS,IAC7B,GAAU,EAAI,MAAM,EAAI,EAAW,KAAK,IAAI,CAAC,MAAM,EASzD,IAAM,GAAmB,GAA6D,CACpF,GAAI,EAAM,SAAS,KAAK,CAAE,CACxB,IAAIC,EACE,EAAW,EAAM,QAAQ,mCAAoC,EAAG,IACxD,QAAQ,IAAI,KAEtB,EAAa,EACN,IAGT,CACF,OAAO,EAAa,CAAE,aAAY,SAAU,GAAI,CAAG,CAAE,WAAU,CAGjE,IAAM,EAAM,QAAQ,IAAI,GAIxB,OAHK,EAGE,CAAE,SAAU,EAAK,CAFf,CAAE,WAAY,EAAO,SAAU,GAAI,EAO9C,GAAI,EAAK,QACP,IAAK,IAAM,KAAU,EAAK,QAAqB,CAC7C,GAAM,CAAC,EAAK,GAAG,GAAc,EAAO,MAAM,IAAI,CAC1C,GAAO,EAAW,OAAS,IAC7B,EAAc,EAAI,MAAM,EAAI,EAAW,KAAK,IAAI,CAAC,MAAM,EAM7D,GAAI,EAAK,eAAgB,CACvB,IAAM,EAAa,EAAK,eAClB,CAAE,aAAY,SAAU,GAAU,GAAgB,EAAW,CAC/D,GACF,MAAM,GAAmB,CACvB,OAAQ,EACR,UAAW,8BACX,WAAY,6CACZ,gBAAiB,CAAC,EAAK,KACxB,CAAC,CAEJ,EAAc,cAAmB,UAAU,IAC3C,EAAO,KAAK,gDAAiD,CAC3D,SAAU,eACV,OAAQ,EACR,UAAW,sBACX,UAAW,EAAK,IACjB,CAAC,CAMJ,GAAI,EAAK,eACP,IAAK,IAAM,KAAS,EAAK,eAA4B,CACnD,GAAM,CAAC,EAAY,GAAG,GAAc,EAAM,MAAM,IAAI,CAC9C,EAAkB,EAAW,KAAK,IAAI,CAAC,MAAM,CACnD,GAAI,GAAc,EAAiB,CACjC,IAAM,EAAgB,EAAW,MAAM,CACjC,CAAE,aAAY,YAAa,GAAgB,EAAgB,CAE7D,GACF,MAAM,GAAmB,CACvB,OAAQ,EACR,UAAW,8BACX,OAAQ,EACR,WAAY,kCACZ,gBAAiB,CAAC,EAAK,KACxB,CAAC,CAGJ,EAAc,GAAiB,EAC/B,EAAO,KAAK,sCAAuC,CACjD,UAAW,sBACX,OAAQ,EACR,UAAW,EAAK,IACjB,CAAC,EAMR,IAAM,GAAgB,GAA0B,EAAc,CAK9D,GAJI,IACF,EAAO,WAAW,GAAc,CAG9B,EAAc,cAAe,CAC/B,IAAM,EAAW,EAAc,cAAc,WAAW,UAAU,CAC9D,eACA,EAAc,cAAc,WAAW,SAAS,CAC9C,QACA,UAEN,EAAO,KAAK,gCAAiC,CAC3C,WACA,UAAW,2BACX,UAAW,EAAK,IACjB,CAAC,CAIJ,IAAM,GAAiB,CAAE,GAAG,EAAO,QAAQ,QAAS,CAEpD,EAAO,MAAM,+BAAgC,CAC3C,SAAU,EAAO,QAAQ,SACzB,UAAW,CAAC,CAAC,EAAO,QAAQ,OAC5B,eAAgB,EAAO,QAAQ,QAC/B,WAAY,EAAO,QAAQ,IAC3B,UAAW,EAAO,QAAQ,UAC3B,CAAC,CAQF,IAAM,EAAuB,KAAO,IAAmC,CACrE,IAAM,EAAkB,CAAC,EAAK,MAAQ,CAAC,EAGjC,EAAgB,OACX,CACL,GAAI,CACF,OAAO,IAAI,IAAI,EAAU,CAAC,YACpB,CACN,MAAO,2BAEP,CACJ,IAAA,GACE,EAAS,GACb,GAAa,EAAgB,EAAK,MAAM,EAAU,CAAC,KAAK,EAAc,CAAG,EAE3E,GAAI,aAAiB,EAAyB,CAG5C,IAAME,EACJ,EAAM,SACN,kJAII,EAAS,CACb,GACA,kBACA,IAAI,OAAO,GAAG,CACdA,EACA,qEACA,GACD,CAAC,KAAK;EAAK,CACZ,OAAO,GAAmB,CACxB,SAAU,EACV,WAAY,8CACZ,KAAM,CAAE,MAAO,EAAM,QAAS,UAAW,2BAA4B,OAAQ,EAAM,OAAQ,CAC3F,OAAA,EACA,kBACA,WAAY,EACb,CAAC,CAIJ,IAAM,EAAS,EACb,aAAiB,EACb,2CAA2C,EAAM,UACjD,aAAiB,EACf,yDAAyD,EAAgB,OAAO,IAAkB,GAAG,KAAK,EAAM,UAChH,uCAAuC,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GACpG,CACD,OAAO,GAAmB,CACxB,SAAU,EACV,WAAY,6BACZ,KAAM,CACJ,KAAM,EAAY,EAAc,EAAU,CAAG,IAAA,GAC7C,QAAS,EACT,MAAO,EAAiB,EAAM,CAC9B,MAAO,EAAM,8BAA8B,IAAQ,CACnD,UAAW,0BACX,YACA,UAAW,EACX,UAAW,EACZ,CACD,SACA,kBACA,WAAY,wBAAwB,IACrC,CAAC,EAIJ,GAAI,EAAK,EAAE,KAAO,SAAU,CAC1B,EAAY,SAGR,EAAK,qBACP,EAAO,KACL,oJAEA,CAAE,kBAAmB,EAAK,oBAAqB,CAChD,CAGH,IAAM,EAAS,EAAK,IACf,GAGH,MAAM,EAAqB,IAAI,EAAmB,+CAAgD,MAAM,CAAC,CAI3G,EAAY,EAAa,EAAO,CAC5B,IAAc,GAChB,EAAO,MAAM,wBAAyB,CAAE,cAAe,EAAW,YAAa,EAAQ,CAAC,KAErF,CAKL,GAHA,EAAY,QAGR,EAAK,OAAU,EAAK,MAAmB,OAAS,EAAG,CACrD,IAAM,EAAe,EAAK,MAC1B,EAAe,EAAa,GAE5B,EAAY,EAAa,MAAM,EAAE,CAAC,IAAI,OAAO,MAG7C,EAAe,EAAK,QAEpB,GAAc,EAAK,MAAsB,EAAE,EAAE,IAAI,OAAO,CAGrD,GAGH,MAAM,EAAqB,IAAI,EAAmB,uCAAwC,UAAU,CAAC,CAUzG,IAAIC,EACA,IAAc,SAAW,IAC3B,EAAsB,GAAmB,EAAc,EAAU,CAC7D,EAAoB,gBACtB,EAAO,KAAK,yBAA0B,CACpC,UAAW,EAAoB,UAC/B,QAAS,EAAoB,QAC9B,CAAC,EAON,IAAIC,EAWAC,GACE,GAA4B,SAA2B,CAC3D,KAAoB,GAAgB,CACpC,EAAO,eAAe,CAAE,GAAI,MAAM,GAAkB,YAAW,CAAC,EAY5D,GAA+B,SAA2B,CAC1D,IAA0B,IAAA,IAAa,CAAC,GAAqB,gBAAkB,CAAC,EAAoB,YAMxG,MAAM,IAA2B,CACjC,EAAwB,GACtB,EAAoB,UACpB,EAAoB,QACrB,GAGG,OACJ,IAAc,QACV,CACA,KAAM,EACN,QAAS,EACT,mBAAoB,EACpB,gBAAiB,GAAqB,UACtC,YACD,CACC,CACA,YACA,IAAK,EACN,CAODC,GACJ,GAAI,EAAO,cAAc,KAAO,EAAO,QAAQ,OAAQ,CACrD,IAAM,EAAU,IAAqB,CAIrC,MAAM,IAA2B,CACjC,IAAM,EAAkB,MAAM,GAExB,EACJ,EAAQ,YAAc,QAClB,CACA,GAAG,EACH,UAAW,QACX,aAAc,CAAE,GAAG,EAAS,KAAM,EAAc,EAAQ,KAAK,CAAE,CAChE,CACC,CACA,GAAG,EACH,UAAW,SACX,cAAe,EAChB,CAEL,GAAsB,IAAI,GAAoB,CAC5C,OAAQ,EAAO,QAAQ,OACvB,QAAS,EAAO,cAAc,QAC9B,cACA,UAAW,EAAO,cAAc,UAChC,IAAK,EAAO,cAAc,IAC3B,CAAC,CAIJ,IAAM,GAAa,IAAc,SAAW,EAAY,EAGlD,EAA0B,IAAI,GAAwB,CAC1D,OAAQ,GACR,QAAS,EAAO,cAAc,QAC9B,cACD,CAAC,CAGI,EAAuB,IAAI,GAAqB,CACpD,WAAY,EAAO,SACpB,CAAC,CAGI,MACJ,IAAI,EACF,CACE,KAAM,cACN,QAAS,EAAO,WACjB,CACD,CACE,aAAc,EAAE,CACjB,CACF,CAEG,EAAU,MACd,EACA,CAAE,uBAAuB,IAA6C,EAAE,GACpD,CAGpB,GAAI,CADc,MAAM,EAAwB,WAAW,CAC3C,CACd,IAAM,EAAS,EAAwB,gBAAgB,CAMvD,MALA,EAAO,KAAK,uCAAwC,CAClD,YACA,SACD,CAAC,CAEI,IAAI,EAAwB,EAAQ,eAAe,CAW3D,GARA,EAAO,KAAK,uCAAuC,CAMnD,MAAM,IAA8B,CAEhC,IAAc,QAAS,CAGzB,IAAM,EAAY,CAChB,GAAG,QAAQ,IACX,GAAG,GACJ,CAEK,EAAY,IAAI,GAAqB,CACzC,KAAM,EACN,QAAS,EACT,IAAK,EACL,QAAU,GAAU,CACd,EAAK,OACP,EAAO,MAAM,kBAAmB,CAAE,QAAO,CAAC,EAG9C,MAAO,EAAK,MAKZ,OAAQ,OACT,CAAC,CACF,GAAuB,EAKvB,IAAM,EAAkB,KAIE,EAAkB,EAJ5C,IAKM,EAAY,GAChB,EAAK,OAAS,EAAkB,GAAG,EAAK,MAAM,EAAG,EAAgB,CAAC,GAAK,EAErE,EAAmB,GACjBC,EAAyB,EAAE,CAC3B,EAAiB,GAAiB,CACtC,IAAM,EAAY,EAAS,EAAK,CAChC,EAAa,KAAK,EAAU,CACxB,EAAa,OAAS,KAAK,EAAa,OAAO,CACnD,EAAO,KAAK,oBAAqB,CAC/B,QAAS,EACT,UAAW,oBACX,KAAM,EACP,CAAC,EAEE,MAA0B,CAC1B,EAAiB,SAAW,IAChC,EAAc,EAAiB,CAC/B,EAAmB,KAKf,EAAgB,IAAI,GAAc,OAAO,CAI3C,EAAoB,GAClB,MAAwB,CAC5B,GAAI,EAAmB,OACvB,EAAoB,GACpB,IAAM,EAAc,EAAc,KAAK,CACnC,IACF,GAAoB,GAEtB,GAAmB,EAErB,EAAU,QAAQ,GAAG,OAAS,GAAkB,CAI9C,QAAQ,OAAO,MAAM,EAAM,CAC3B,GAAoB,EAAc,MAAM,EAAM,CAC9C,IAAM,EAAQ,EAAiB,MAAM;EAAK,CAC1C,EAAmB,EAAM,KAAK,EAAI,GAClC,IAAK,IAAM,KAAQ,EAAO,CACxB,GAAI,EAAK,SAAW,EAAG,SACvB,EAAc,EAAK,CAKjB,EAAiB,OAAS,MAC5B,GAAmB,EAErB,CACF,EAAU,QAAQ,KAAK,MAAO,EAAgB,CAC9C,EAAU,QAAQ,KAAK,QAAS,EAAgB,CAEhD,GAAI,CAEF,OADA,MAAM,EAAO,QAAQ,EAAU,CACxB,QACA,EAAO,CAcd,MAbA,GAAiB,CACjB,EAAO,KAAK,gCAAiC,CAC3C,QAAS,EACT,MAAO,EAAiB,EAAM,CAC9B,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAC7D,UAAW,2BACX,YACA,WAAY,EAAa,MAAM,IAAI,CACpC,CAAC,CAIF,MAAM,EAAU,OAAO,CAAC,UAAY,GAAG,CACjC,IAAI,EAAqB,uCAAuC,IAAS,IAAA,GAAW,QAAS,CACjG,SAAU,GAAW,OACrB,QAAS,EACV,CAAE,EAAM,UAEF,IAAc,SAAU,CAKjC,IAAM,EAAM,EAQN,MAAyD,CAC7D,IAAMC,EAAkC,CAAE,GAAG,EAAe,CAC5D,OAAO,OAAO,KAAK,EAAQ,CAAC,OAAS,EAAI,CAAE,UAAS,CAAG,IAAA,IAInD,EAAgB,SAA6B,CACjD,IAAM,EAAc,MAAM,GAAiB,EAAK,CAAE,gBAAiB,EAAsB,CAAC,CAO1F,OANA,EAAO,KAAK,2BAA4B,CACtC,SAAU,QACV,UAAW,qBACX,UAAW,EAAY,UACvB,UAAW,EACZ,CAAC,CACK,EAAY,OAAO,cAGxBC,EACJ,GAAI,CAEF,IAAM,EAAS,MAAM,EAAqB,KAAK,EAAI,CAC7C,EACJ,GAAU,EAAqB,oBAAoB,EAAO,CAAG,EAAO,QAAQ,aAAe,IAAA,GAQvFC,EAA6B,GAAkB,EAAK,EAAsB,EAAc,CACxF,EAAY,GAA0B,EAAQ,CAEhD,EAAiB,GAErB,GAAI,CAGF,IAAM,EAAS,MAAM,GACnB,EACA,EACA,GAAyB,CACzB,EACD,CACD,EAAkB,EAAO,OACzB,EAAgB,EAAO,cACvB,EAAO,KAAK,6BAA8B,CAAE,QAAS,CAAC,CAAC,EAAe,UAAW,EAAe,CAAC,OAC1F,EAAK,CAEZ,GAAI,GAAW,EAAI,CAAE,CAEnB,GAAI,EACF,MAAM,EAyCR,GA7BI,EAAQ,kBAAkB,EAAI,GAChC,EAAO,KAAK,kEAAkE,CAmB9E,MAAM,EAAqB,OAAO,EAAK,CAAE,iBAAkB,IAAA,GAAW,CAAC,EAEvE,EAAO,KAAK,yDAAyD,CAOnE,CAAC,EAIH,MAHA,EAAO,KAAK,yDAA0D,CACpE,UAAW,sCACZ,CAAC,CACI,EAGR,IAAM,EAAa,MAAM,GAAe,CACxC,EAAQ,UAAU,EAAW,CAC7B,EAAiB,GAIjB,IAAM,EAAS,MAAM,GACnB,EACA,EACA,GAAyB,CACzB,EACD,CACD,EAAkB,EAAO,OACzB,EAAgB,EAAO,cACvB,EAAO,KAAK,wCAAyC,CAAE,UAAW,EAAe,CAAC,SAElF,CAAC,GACD,EAAE,EAAQ,kBAAkB,EAAI,IAChC,CAAC,IAAI,QAAQ,EAAc,CAAC,IAAI,gBAAgB,CAChD,CA6BA,IAAI,EAAkB,GAClBC,EACJ,GAAI,CACF,GAAM,CAAE,gBAAiB,MAAM,GAAmB,IAAI,IAAI,EAAI,CAAE,CAAE,SAAQ,CAAC,CAC3E,EAAkB,EAAQ,QACnB,EAAc,CACrB,EAAiB,aAAwB,MAAQ,EAAa,QAAU,OAAO,EAAa,CAE9F,GAAI,CAAC,EAaH,MANA,EAAO,KAAK,iFAAkF,CAC5F,iBACA,UAAW,gCACX,cAAe,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,CAC/D,UAAW,EACZ,CAAC,CACI,EAKR,GAHA,EAAO,KAAK,wEAAyE,CACnF,cAAe,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,CAChE,CAAC,CACE,CAAC,EAIH,MAHA,EAAO,KAAK,2DAA4D,CACtE,UAAW,sCACZ,CAAC,CACI,EAER,IAAM,EAAa,MAAM,GAAe,CACxC,EAAQ,UAAU,EAAW,CAC7B,EAAiB,GACjB,IAAM,EAAS,MAAM,GACnB,EACA,EACA,GAAyB,CACzB,EACD,CACD,EAAkB,EAAO,OACzB,EAAgB,EAAO,cACvB,EAAO,KAAK,iEAAkE,CAC5E,UAAW,EACZ,CAAC,MAEF,MAAM,EAGV,OAAO,QACA,EAAO,CASd,MARA,EAAO,KAAK,gCAAiC,CAC3C,MAAO,EAAiB,EAAM,CAC9B,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAC7D,UAAW,2BACX,YACA,UAAW,EACZ,CAAC,CACE,aAAiB,EAA4B,EAC3C,IAAI,EACR,uCAAuC,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GAC7F,EACA,EACA,CAAE,YAAa,OAAO,KAAK,EAAc,CAAC,OAAQ,CAClD,EACD,EAKL,OAAO,GAMH,OACJ,OAAuB,EAAQ,GAAc,CAAC,CAAE,EAAO,SAAS,QAAS,CACvE,SAAU,CAAE,UAAS,UAAS,WAAY,CACxC,EAAO,KAAK,oCAAqC,CAC/C,UACA,UACA,MAAO,EAAiB,EAAM,CAC9B,MAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAC7D,UAAW,6BACX,YACD,CAAC,EAEL,CAAC,CAME,EACJ,IAAc,SACV,EAAO,SAAS,UAAU,QAC1B,EAAO,SAAS,UAAU,gBAC1B,EAAgB,GACpB,EACI,GAAyB,CACvB,WAAY,EAAO,SAAS,UAAU,WACtC,QAAS,EACT,cAAiB,EAAQ,GAAc,CAAE,CAAE,qBAAsB,GAAO,CAAC,CAC1E,CAAC,CACF,EAMFC,EACE,OACJ,IAAsB,EAAwB,CAC5C,QAAS,IAAqB,CAC9B,WAAY,EAAO,QAAQ,OAE3B,YAAa,EAAO,QAAQ,QAC5B,aAAc,EAAO,QAAQ,SAC7B,YAAa,GACb,cAAe,EAAO,QAAQ,UAC9B,QAAS,EAAO,QAAQ,KAAO,GAC/B,6BACD,CAAC,CACK,GAGH,EAAQ,SAAY,CAExB,GAAI,EAAK,KAAM,CACb,IAAIC,EACE,EAAa,MAAM,EAAgB,CACvC,aAAc,SAAY,CAGxB,IAAMC,EAAY,MAAM,IAAyB,CAIjD,MAAM,GAAqB,CAE3B,IAAMC,EAAgBD,EAAU,kBAAkB,CAK5CE,EACJF,EAAU,uBAAuB,CAInC,EAAW,EAAaA,EAAU,CAElC,IAAMG,EAAS,IAAI,EAAOF,EAAe,CACvC,aAAcC,EACf,CAAC,CASF,OAPA,EAAY,CACV,WAAY,EACZ,OAAQE,EACR,OAAA,EACA,mBAAA,EACD,CAAC,CAEKD,GAET,QAAS,KAAO,IAAW,CACrBC,GACF,MAAMA,EAAS,OAAO,CAExB,MAAMD,EAAO,OAAO,EAEtB,KAAM,EAAK,KACZ,CAAC,CAuBF,OArBA,EAAO,KAAK,sBAAuB,CACjC,UAAW,sBACX,KAAM,OACN,KAAM,EAAK,KACX,YACD,CAAC,CACE,IAAc,QAChB,EAAO,KAAK,2BAA4B,CACtC,KAAM,EAAc,GAAa,EAAE,CAAC,CAAC,KAAK,IAAI,CAC9C,QAAS,EACT,UAAW,oBACX,YACD,CAAC,CAEF,EAAO,KAAK,2BAA4B,CACtC,UAAW,oBACX,YACA,YACD,CAAC,CAGG,CACL,UACS,EAAW,OAAO,CAE5B,CAMH,IAAM,EAAY,MAAM,IAAyB,CAE3C,EAAgB,EAAU,kBAAkB,CAK5C,EACJ,EAAU,uBAAuB,CAG7B,EAAW,EAAa,EAAU,CAGlC,EAAS,IAAI,EAAO,EAAe,CACvC,aAAc,EACf,CAAC,CAEF,EAAY,CACV,WAAY,EACZ,OAAQ,EACR,SACA,qBACD,CAAC,CAEF,IAAM,EAAiB,IAAI,EA2B3B,OAvBA,MAAM,GAAqB,CAE3B,MAAM,EAAO,QAAQ,EAAe,CACpC,EAAkB,GAElB,EAAO,KAAK,sBAAuB,CACjC,UAAW,sBACX,KAAM,QACN,YACD,CAAC,CAGF,EAAO,KAAK,2BAA4B,CACtC,aAAc,OAAO,KAAK,GAAsB,EAAE,CAAC,CACnD,UAAW,oBACX,YACA,WAAY,GAAe,KAC3B,cAAe,GAAe,QAC9B,GAAI,IAAc,QACd,CAAE,KAAM,EAAc,GAAa,EAAE,CAAC,CAAC,KAAK,IAAI,CAAE,QAAS,EAAc,CACzE,CAAE,YAAW,UAAW,EAAe,CAC5C,CAAC,CAEK,CACL,UAES,EAAS,OAAO,CAE1B,EAGG,GAA0B,CAC9B,SACA,aAII,CACJ,IAAI,EAAoB,GAElB,EAAmB,KAAO,IAAoB,CAC9C,MAKJ,CAFA,EAAoB,GAEpB,EAAO,KAAK,uBAAwB,CAClC,UAAW,uBACX,OAAQ,EACT,CAAC,CAEF,GAAI,CACF,MAAM,EAAO,OAAO,CAEpB,GAAkB,CAElB,MAAM,GAAU,IAAK,CACrB,QAAQ,KAAK,EAAE,OACR,EAAO,CACd,IAAM,EAAe,0BAA0B,IAC/C,EAAO,MAAM,wBAAyB,CACpC,MAAO,EACP,UAAW,sBACX,gBAAiB,EAClB,CAAC,CAEF,MAAM,GAAU,IAAK,CACrB,QAAQ,KAAK,EAAE,IAIb,EAAkB,GAAoB,SAAY,CACtD,OAAiB,CACf,EAAO,MAAM,mDAAmD,CAChE,QAAQ,KAAK,EAAE,EACd,EAAQ,CAAC,OAAO,CAEnB,MAAM,EAAiB,EAAQ,EAajC,OAVA,QAAQ,KAAK,UAAW,EAAe,UAAU,CAAC,CAClD,QAAQ,KAAK,SAAU,EAAe,SAAS,CAAC,CAOhD,QAAQ,MAAM,KAAK,MAAO,EAAe,YAAY,CAAC,KAG7C,EAAO,OAAO,EAKzB,EAAO,KAAK,uBAAwB,CAClC,QAAS,EACT,UAAW,uBACX,YACA,UAAW,IAAc,SAAW,cAAgB,QACpD,IAAK,EACN,CAAC,CAEF,GAAI,CACF,IAAM,EAAS,MAAM,GAAO,CAE5B,EAAuB,CACrB,SACA,QAAS,EAAK,wBACf,CAAC,OACK,EAAO,CAGd,MAAM,EAAqB,EAAM"}
1
+ {"version":3,"file":"main-Brer_L6I.js","names":["config","config","credentials: StoredCredentials","bearer","refreshInFlight: null | Promise<string | undefined>","msg: { id?: unknown }","reconnecting: null | Promise<boolean>","notificationHandlers: SetNotificationHandlerArgs[]","jsonLines: string[]","spawnedAt: number | undefined","spawnedPid: number | undefined","messageType: string","result: string","parsedResult: unknown","repoDigests: unknown[]","digests: string[]","activeStdioTransport: StdioClientTransport | undefined","yargs","proxyType: ProxyType","finalCommand: string | undefined","finalArgs: string[] | undefined","remoteUrl: string | undefined","usedTransport: TransportType | undefined","remoteHeaders: Record<string, string>","customEnv: Record<string, string>","missingVar: string | undefined","val","reason","containerMCPCommand: ContainerMCPCommand | undefined","containerImageDigests: string[] | undefined","sessionDataOnce: ReturnType<typeof getSessionData> | undefined","accessControlClient: AccessControlClient | undefined","recentStderr: string[]","headers: Record<string, string>","connectedClient: Client","dynAuth: DynamicAuthHandle","discoveryError: string | undefined","trafficMirrorInit: Promise<unknown> | undefined","upstream: undefined | UpstreamClient","connected","serverVersion","serverCapabilities","server","upstream"],"sources":["../node_modules/eventsource/dist/index.js","../src/accessControl/authorizer.ts","../src/accessControl/client.ts","../src/auth/OAuthCredentialStore.ts","../src/auth/dynamicAuth.ts","../src/lib/initializeErrorResponder.ts","../src/lib/upstreamErrors.ts","../src/lib/connectWithRetry.ts","../src/lib/reconnectingClient.ts","../src/JSONFilterTransform.ts","../src/StdioClientTransport.ts","../src/utils/imageReference.ts","../src/utils/dockerImageDigest.ts","../src/bin/main.ts"],"sourcesContent":["import { createParser } from \"eventsource-parser\";\nclass ErrorEvent extends Event {\n /**\n * Constructs a new `ErrorEvent` instance. This is typically not called directly,\n * but rather emitted by the `EventSource` object when an error occurs.\n *\n * @param type - The type of the event (should be \"error\")\n * @param errorEventInitDict - Optional properties to include in the error event\n */\n constructor(type, errorEventInitDict) {\n var _a, _b;\n super(type), this.code = (_a = errorEventInitDict == null ? void 0 : errorEventInitDict.code) != null ? _a : void 0, this.message = (_b = errorEventInitDict == null ? void 0 : errorEventInitDict.message) != null ? _b : void 0;\n }\n /**\n * Node.js \"hides\" the `message` and `code` properties of the `ErrorEvent` instance,\n * when it is `console.log`'ed. This makes it harder to debug errors. To ease debugging,\n * we explicitly include the properties in the `inspect` method.\n *\n * This is automatically called by Node.js when you `console.log` an instance of this class.\n *\n * @param _depth - The current depth\n * @param options - The options passed to `util.inspect`\n * @param inspect - The inspect function to use (prevents having to import it from `util`)\n * @returns A string representation of the error\n */\n [Symbol.for(\"nodejs.util.inspect.custom\")](_depth, options, inspect) {\n return inspect(inspectableError(this), options);\n }\n /**\n * Deno \"hides\" the `message` and `code` properties of the `ErrorEvent` instance,\n * when it is `console.log`'ed. This makes it harder to debug errors. To ease debugging,\n * we explicitly include the properties in the `inspect` method.\n *\n * This is automatically called by Deno when you `console.log` an instance of this class.\n *\n * @param inspect - The inspect function to use (prevents having to import it from `util`)\n * @param options - The options passed to `Deno.inspect`\n * @returns A string representation of the error\n */\n [Symbol.for(\"Deno.customInspect\")](inspect, options) {\n return inspect(inspectableError(this), options);\n }\n}\nfunction syntaxError(message) {\n const DomException = globalThis.DOMException;\n return typeof DomException == \"function\" ? new DomException(message, \"SyntaxError\") : new SyntaxError(message);\n}\nfunction flattenError(err) {\n return err instanceof Error ? \"errors\" in err && Array.isArray(err.errors) ? err.errors.map(flattenError).join(\", \") : \"cause\" in err && err.cause instanceof Error ? `${err}: ${flattenError(err.cause)}` : err.message : `${err}`;\n}\nfunction inspectableError(err) {\n return {\n type: err.type,\n message: err.message,\n code: err.code,\n defaultPrevented: err.defaultPrevented,\n cancelable: err.cancelable,\n timeStamp: err.timeStamp\n };\n}\nvar __typeError = (msg) => {\n throw TypeError(msg);\n}, __accessCheck = (obj, member, msg) => member.has(obj) || __typeError(\"Cannot \" + msg), __privateGet = (obj, member, getter) => (__accessCheck(obj, member, \"read from private field\"), getter ? getter.call(obj) : member.get(obj)), __privateAdd = (obj, member, value) => member.has(obj) ? __typeError(\"Cannot add the same private member more than once\") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value), __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, \"write to private field\"), member.set(obj, value), value), __privateMethod = (obj, member, method) => (__accessCheck(obj, member, \"access private method\"), method), _readyState, _url, _redirectUrl, _withCredentials, _fetch, _reconnectInterval, _reconnectTimer, _lastEventId, _controller, _parser, _onError, _onMessage, _onOpen, _EventSource_instances, connect_fn, _onFetchResponse, _onFetchError, getRequestOptions_fn, _onEvent, _onRetryChange, failConnection_fn, scheduleReconnect_fn, _reconnect;\nclass EventSource extends EventTarget {\n constructor(url, eventSourceInitDict) {\n var _a, _b;\n super(), __privateAdd(this, _EventSource_instances), this.CONNECTING = 0, this.OPEN = 1, this.CLOSED = 2, __privateAdd(this, _readyState), __privateAdd(this, _url), __privateAdd(this, _redirectUrl), __privateAdd(this, _withCredentials), __privateAdd(this, _fetch), __privateAdd(this, _reconnectInterval), __privateAdd(this, _reconnectTimer), __privateAdd(this, _lastEventId, null), __privateAdd(this, _controller), __privateAdd(this, _parser), __privateAdd(this, _onError, null), __privateAdd(this, _onMessage, null), __privateAdd(this, _onOpen, null), __privateAdd(this, _onFetchResponse, async (response) => {\n var _a2;\n __privateGet(this, _parser).reset();\n const { body, redirected, status, headers } = response;\n if (status === 204) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, \"Server sent HTTP 204, not reconnecting\", 204), this.close();\n return;\n }\n if (redirected ? __privateSet(this, _redirectUrl, new URL(response.url)) : __privateSet(this, _redirectUrl, void 0), status !== 200) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, `Non-200 status code (${status})`, status);\n return;\n }\n if (!(headers.get(\"content-type\") || \"\").startsWith(\"text/event-stream\")) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, 'Invalid content type, expected \"text/event-stream\"', status);\n return;\n }\n if (__privateGet(this, _readyState) === this.CLOSED)\n return;\n __privateSet(this, _readyState, this.OPEN);\n const openEvent = new Event(\"open\");\n if ((_a2 = __privateGet(this, _onOpen)) == null || _a2.call(this, openEvent), this.dispatchEvent(openEvent), typeof body != \"object\" || !body || !(\"getReader\" in body)) {\n __privateMethod(this, _EventSource_instances, failConnection_fn).call(this, \"Invalid response body, expected a web ReadableStream\", status), this.close();\n return;\n }\n const decoder = new TextDecoder(), reader = body.getReader();\n let open = !0;\n do {\n const { done, value } = await reader.read();\n value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open = !1, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));\n } while (open);\n }), __privateAdd(this, _onFetchError, (err) => {\n __privateSet(this, _controller, void 0), !(err.name === \"AbortError\" || err.type === \"aborted\") && __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this, flattenError(err));\n }), __privateAdd(this, _onEvent, (event) => {\n typeof event.id == \"string\" && __privateSet(this, _lastEventId, event.id);\n const messageEvent = new MessageEvent(event.event || \"message\", {\n data: event.data,\n origin: __privateGet(this, _redirectUrl) ? __privateGet(this, _redirectUrl).origin : __privateGet(this, _url).origin,\n lastEventId: event.id || \"\"\n });\n __privateGet(this, _onMessage) && (!event.event || event.event === \"message\") && __privateGet(this, _onMessage).call(this, messageEvent), this.dispatchEvent(messageEvent);\n }), __privateAdd(this, _onRetryChange, (value) => {\n __privateSet(this, _reconnectInterval, value);\n }), __privateAdd(this, _reconnect, () => {\n __privateSet(this, _reconnectTimer, void 0), __privateGet(this, _readyState) === this.CONNECTING && __privateMethod(this, _EventSource_instances, connect_fn).call(this);\n });\n try {\n if (url instanceof URL)\n __privateSet(this, _url, url);\n else if (typeof url == \"string\")\n __privateSet(this, _url, new URL(url, getBaseURL()));\n else\n throw new Error(\"Invalid URL\");\n } catch {\n throw syntaxError(\"An invalid or illegal string was specified\");\n }\n __privateSet(this, _parser, createParser({\n onEvent: __privateGet(this, _onEvent),\n onRetry: __privateGet(this, _onRetryChange)\n })), __privateSet(this, _readyState, this.CONNECTING), __privateSet(this, _reconnectInterval, 3e3), __privateSet(this, _fetch, (_a = eventSourceInitDict == null ? void 0 : eventSourceInitDict.fetch) != null ? _a : globalThis.fetch), __privateSet(this, _withCredentials, (_b = eventSourceInitDict == null ? void 0 : eventSourceInitDict.withCredentials) != null ? _b : !1), __privateMethod(this, _EventSource_instances, connect_fn).call(this);\n }\n /**\n * Returns the state of this EventSource object's connection. It can have the values described below.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/readyState)\n *\n * Note: typed as `number` instead of `0 | 1 | 2` for compatibility with the `EventSource` interface,\n * defined in the TypeScript `dom` library.\n *\n * @public\n */\n get readyState() {\n return __privateGet(this, _readyState);\n }\n /**\n * Returns the URL providing the event stream.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/url)\n *\n * @public\n */\n get url() {\n return __privateGet(this, _url).href;\n }\n /**\n * Returns true if the credentials mode for connection requests to the URL providing the event stream is set to \"include\", and false otherwise.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/withCredentials)\n */\n get withCredentials() {\n return __privateGet(this, _withCredentials);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */\n get onerror() {\n return __privateGet(this, _onError);\n }\n set onerror(value) {\n __privateSet(this, _onError, value);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */\n get onmessage() {\n return __privateGet(this, _onMessage);\n }\n set onmessage(value) {\n __privateSet(this, _onMessage, value);\n }\n /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */\n get onopen() {\n return __privateGet(this, _onOpen);\n }\n set onopen(value) {\n __privateSet(this, _onOpen, value);\n }\n addEventListener(type, listener, options) {\n const listen = listener;\n super.addEventListener(type, listen, options);\n }\n removeEventListener(type, listener, options) {\n const listen = listener;\n super.removeEventListener(type, listen, options);\n }\n /**\n * Aborts any instances of the fetch algorithm started for this EventSource object, and sets the readyState attribute to CLOSED.\n *\n * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close)\n *\n * @public\n */\n close() {\n __privateGet(this, _reconnectTimer) && clearTimeout(__privateGet(this, _reconnectTimer)), __privateGet(this, _readyState) !== this.CLOSED && (__privateGet(this, _controller) && __privateGet(this, _controller).abort(), __privateSet(this, _readyState, this.CLOSED), __privateSet(this, _controller, void 0));\n }\n}\n_readyState = /* @__PURE__ */ new WeakMap(), _url = /* @__PURE__ */ new WeakMap(), _redirectUrl = /* @__PURE__ */ new WeakMap(), _withCredentials = /* @__PURE__ */ new WeakMap(), _fetch = /* @__PURE__ */ new WeakMap(), _reconnectInterval = /* @__PURE__ */ new WeakMap(), _reconnectTimer = /* @__PURE__ */ new WeakMap(), _lastEventId = /* @__PURE__ */ new WeakMap(), _controller = /* @__PURE__ */ new WeakMap(), _parser = /* @__PURE__ */ new WeakMap(), _onError = /* @__PURE__ */ new WeakMap(), _onMessage = /* @__PURE__ */ new WeakMap(), _onOpen = /* @__PURE__ */ new WeakMap(), _EventSource_instances = /* @__PURE__ */ new WeakSet(), /**\n* Connect to the given URL and start receiving events\n*\n* @internal\n*/\nconnect_fn = function() {\n __privateSet(this, _readyState, this.CONNECTING), __privateSet(this, _controller, new AbortController()), __privateGet(this, _fetch)(__privateGet(this, _url), __privateMethod(this, _EventSource_instances, getRequestOptions_fn).call(this)).then(__privateGet(this, _onFetchResponse)).catch(__privateGet(this, _onFetchError));\n}, _onFetchResponse = /* @__PURE__ */ new WeakMap(), _onFetchError = /* @__PURE__ */ new WeakMap(), /**\n* Get request options for the `fetch()` request\n*\n* @returns The request options\n* @internal\n*/\ngetRequestOptions_fn = function() {\n var _a;\n const init = {\n // [spec] Let `corsAttributeState` be `Anonymous`…\n // [spec] …will have their mode set to \"cors\"…\n mode: \"cors\",\n redirect: \"follow\",\n headers: { Accept: \"text/event-stream\", ...__privateGet(this, _lastEventId) ? { \"Last-Event-ID\": __privateGet(this, _lastEventId) } : void 0 },\n cache: \"no-store\",\n signal: (_a = __privateGet(this, _controller)) == null ? void 0 : _a.signal\n };\n return \"window\" in globalThis && (init.credentials = this.withCredentials ? \"include\" : \"same-origin\"), init;\n}, _onEvent = /* @__PURE__ */ new WeakMap(), _onRetryChange = /* @__PURE__ */ new WeakMap(), /**\n* Handles the process referred to in the EventSource specification as \"failing a connection\".\n*\n* @param error - The error causing the connection to fail\n* @param code - The HTTP status code, if available\n* @internal\n*/\nfailConnection_fn = function(message, code) {\n var _a;\n __privateGet(this, _readyState) !== this.CLOSED && __privateSet(this, _readyState, this.CLOSED);\n const errorEvent = new ErrorEvent(\"error\", { code, message });\n (_a = __privateGet(this, _onError)) == null || _a.call(this, errorEvent), this.dispatchEvent(errorEvent);\n}, /**\n* Schedules a reconnection attempt against the EventSource endpoint.\n*\n* @param message - The error causing the connection to fail\n* @param code - The HTTP status code, if available\n* @internal\n*/\nscheduleReconnect_fn = function(message, code) {\n var _a;\n if (__privateGet(this, _readyState) === this.CLOSED)\n return;\n __privateSet(this, _readyState, this.CONNECTING);\n const errorEvent = new ErrorEvent(\"error\", { code, message });\n (_a = __privateGet(this, _onError)) == null || _a.call(this, errorEvent), this.dispatchEvent(errorEvent), __privateSet(this, _reconnectTimer, setTimeout(__privateGet(this, _reconnect), __privateGet(this, _reconnectInterval)));\n}, _reconnect = /* @__PURE__ */ new WeakMap(), /**\n* ReadyState representing an EventSource currently trying to connect\n*\n* @public\n*/\nEventSource.CONNECTING = 0, /**\n* ReadyState representing an EventSource connection that is open (eg connected)\n*\n* @public\n*/\nEventSource.OPEN = 1, /**\n* ReadyState representing an EventSource connection that is closed (eg disconnected)\n*\n* @public\n*/\nEventSource.CLOSED = 2;\nfunction getBaseURL() {\n const doc = \"document\" in globalThis ? globalThis.document : void 0;\n return doc && typeof doc == \"object\" && \"baseURI\" in doc && typeof doc.baseURI == \"string\" ? doc.baseURI : void 0;\n}\nexport {\n ErrorEvent,\n EventSource\n};\n//# sourceMappingURL=index.js.map\n","import type { AccessControlClient } from \"./client.js\";\n\nimport { logger } from \"../lib/logger.js\";\n\nexport interface AccessControlConfig {\n client?: AccessControlClient;\n enabled: boolean;\n serverName?: string;\n}\n\n/**\n * Authorizes MCP servers based on access control configuration.\n * Queries backend with full client info for authorization decisions.\n */\nexport class AccessControlAuthorizer {\n private client?: AccessControlClient;\n private enabled: boolean;\n private lastBlockReason?: string;\n private serverName?: string;\n\n constructor(config: AccessControlConfig) {\n this.enabled = config.enabled;\n this.client = config.client;\n this.serverName = config.serverName;\n\n logger.info(\"AccessControlAuthorizer initialized\", {\n enabled: this.enabled,\n hasClient: !!this.client,\n serverName: this.serverName,\n });\n }\n\n /**\n * Get human-readable reason for why a server was blocked.\n * Returns the reason from the last authorization check, or a default message.\n *\n * @returns Reason message\n */\n getBlockReason(): string {\n if (this.lastBlockReason) {\n return this.lastBlockReason;\n }\n // Default message with server name if available\n const serverPart = this.serverName ? ` '${this.serverName}'` : \"\";\n return `MCP server${serverPart} is not authorized for use in your organization and has been blocked by Onyx.`;\n }\n\n /**\n * Check if access is allowed by querying the backend.\n * Always fails open (allows) if backend is unavailable or not configured.\n * Stores the block reason from the backend for later retrieval.\n *\n * @returns Promise resolving to true if allowed, false if blocked\n */\n async isAllowed(): Promise<boolean> {\n // If access control not enabled, allow everything\n if (!this.enabled) {\n return true;\n }\n\n // Check with backend if client is available\n if (this.client) {\n try {\n const response = await this.client.authorize();\n if (response.action === \"block\") {\n // Store the reason from backend for user-facing error messages, or use default with server name\n if (response.reason) {\n this.lastBlockReason = response.reason;\n } else {\n const serverPart = this.serverName ? ` '${this.serverName}'` : \"\";\n this.lastBlockReason = `MCP server${serverPart} is not authorized for use in your organization and has been blocked by Onyx.`;\n }\n return false;\n }\n return true;\n } catch (error) {\n logger.error(\"Access control authorization failed with unexpected error\", {\n error: String(error),\n });\n // DEFENSIVE: client.authorize() handles all errors internally and always\n // returns an AccessControlResponse, so this catch is normally unreachable.\n // However, we keep it as defensive fail-open behavior for unexpected errors.\n return true;\n }\n }\n\n // No client configured - allow by default (shouldn't happen if enabled=true with URL)\n logger.warn(\"No access control client configured, allowing by default\");\n return true;\n }\n}\n","import type { SessionData } from \"../utils/getUserData.js\";\n\nimport { compressClientInfo } from \"../lib/compress-client-info.js\";\nimport { logger } from \"../lib/logger.js\";\n\nexport interface AccessControlClientConfig {\n apiKey: string;\n headers: Record<string, string>;\n sessionData: SessionData;\n timeoutMs: number;\n url: string;\n}\n\nexport interface AccessControlResponse {\n action: \"allow\" | \"block\";\n reason?: string;\n}\n\n/**\n * Client for checking MCP server access control with backend API.\n * Always fails open (allows) when backend is unreachable.\n */\nexport class AccessControlClient {\n private clientInfoBase64: string;\n private config: AccessControlClientConfig;\n\n constructor(config: AccessControlClientConfig) {\n this.config = config;\n this.clientInfoBase64 = this.getClientInfoBase64();\n logger.info(\"AccessControlClient initialized\", {\n timeoutMs: config.timeoutMs,\n url: config.url,\n });\n }\n\n /**\n * Authorize by querying the backend with full client info.\n * Always fails open (allows) on any error.\n *\n * @returns Access control response\n */\n async authorize(): Promise<AccessControlResponse> {\n try {\n // Try to get decision from backend\n const response = await this.sendAuthorizeRequest();\n\n logger.debug(\"Access control check successful\", {\n action: response.action,\n });\n\n return response;\n } catch (error) {\n logger.warn(\"Access control check failed, failing open (allowing by default)\", {\n error: String(error),\n });\n\n // Always fail open (allow by default for better UX)\n return { action: \"allow\" };\n }\n }\n\n /**\n * Compress and base64-encode client info for URL.\n * Matches scanner pattern.\n */\n private getClientInfoBase64(): string {\n return compressClientInfo(this.config.sessionData);\n }\n\n /**\n * Send authorization request to backend with compressed client info in URL.\n * Matches scanner URL pattern: ${url}/${apiKey}/mcp/${clientInfoBase64}\n *\n * @returns Access control response\n * @throws Error if request fails\n */\n private async sendAuthorizeRequest(): Promise<AccessControlResponse> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);\n\n try {\n // Build URL with API key and compressed client info (matching scanner pattern)\n const checkUrl = `${this.config.url}/${this.config.apiKey}/mcp/${this.clientInfoBase64}`;\n\n const response = await fetch(checkUrl, {\n headers: {\n ...this.config.headers,\n },\n method: \"POST\",\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`Access control service returned ${response.status}: ${response.statusText}`);\n }\n\n const result = (await response.json()) as AccessControlResponse;\n\n // Validate response format\n if (!result.action || ![\"allow\", \"block\"].includes(result.action)) {\n throw new Error(`Invalid access control response format: action=\"${result.action}\"`);\n }\n\n return result;\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new Error(`Access control check timed out after ${this.config.timeoutMs}ms`);\n }\n throw error;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n}\n","import type { OAuthClientInformationFull, OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\nimport { createHash } from \"node:crypto\";\nimport { chmod, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { chownToRealUser } from \"../utils/chownToRealUser.js\";\nimport { normalizeUrl } from \"../utils/normalizeUrl.js\";\n\nexport interface OAuthCredentialStoreOptions {\n /**\n * Directory to store OAuth credentials.\n * Defaults to ~/.onyx/mcp-gateway/oauth/\n */\n storageDir: string;\n}\n\n/**\n * Stored credentials structure persisted to disk.\n */\nexport interface StoredCredentials {\n /**\n * DCR client registration information.\n */\n clientInfo?: OAuthClientInformationFull;\n /**\n * Timestamp when the credentials were stored (ISO string).\n */\n createdAt: string;\n /**\n * The original server URL these credentials are for.\n */\n serverUrl: string;\n /**\n * OAuth tokens (access_token, refresh_token, etc.).\n */\n tokens?: OAuthTokens;\n /**\n * Timestamp when tokens were obtained (ISO string).\n * Used to calculate token expiration.\n */\n tokensObtainedAt?: string;\n /**\n * Last update timestamp (ISO string).\n */\n updatedAt: string;\n}\n\n/**\n * File-based credential store for OAuth credentials.\n * Stores credentials in JSON files keyed by server URL hash.\n */\nexport class OAuthCredentialStore {\n private readonly storageDir: string;\n\n constructor(options: OAuthCredentialStoreOptions) {\n this.storageDir = options.storageDir;\n }\n\n /**\n * Deletes stored credentials for a server URL.\n */\n async delete(serverUrl: string): Promise<void> {\n // Normalize URL to ensure consistent key generation\n const normalizedUrl = normalizeUrl(serverUrl);\n const filePath = this.getFilePath(normalizedUrl);\n\n try {\n await rm(filePath);\n logger.debug(\"Deleted stored credentials\", { filePath, serverUrl: normalizedUrl });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n logger.warn(\"Failed to delete stored credentials\", {\n error: String(error),\n filePath,\n serverUrl: normalizedUrl,\n });\n }\n }\n }\n\n /**\n * Checks if stored credentials have a refresh token.\n */\n hasRefreshToken(credentials: StoredCredentials): boolean {\n return !!credentials.tokens?.refresh_token;\n }\n\n /**\n * Checks if stored credentials have a valid (non-expired) access token.\n */\n hasValidAccessToken(credentials: StoredCredentials): boolean {\n return !!credentials.tokens?.access_token && !this.isAccessTokenExpired(credentials);\n }\n\n /**\n * Checks if the stored access token is expired.\n * Returns true if expired or if expiration cannot be determined.\n */\n isAccessTokenExpired(credentials: StoredCredentials): boolean {\n if (!credentials.tokens || !credentials.tokensObtainedAt) {\n return true;\n }\n\n const { expires_in } = credentials.tokens;\n if (!expires_in) {\n // No expiration info - assume not expired\n return false;\n }\n\n const obtainedAt = new Date(credentials.tokensObtainedAt).getTime();\n const expiresAt = obtainedAt + expires_in * 1000;\n const now = Date.now();\n\n // Add 30 second buffer to avoid edge cases\n const isExpired = now >= expiresAt - 30000;\n\n logger.debug(\"Checked access token expiration\", {\n expiresAt: new Date(expiresAt).toISOString(),\n expiresIn: expires_in,\n isExpired,\n now: new Date(now).toISOString(),\n obtainedAt: credentials.tokensObtainedAt,\n });\n\n return isExpired;\n }\n\n /**\n * Loads stored credentials for a server URL.\n * Returns undefined if no credentials exist or if they're invalid.\n */\n async load(serverUrl: string): Promise<StoredCredentials | undefined> {\n // Normalize URL to ensure consistent key generation\n const normalizedUrl = normalizeUrl(serverUrl);\n const filePath = this.getFilePath(normalizedUrl);\n\n try {\n const content = await readFile(filePath, \"utf-8\");\n const credentials = JSON.parse(content) as StoredCredentials;\n\n // Validate the stored credentials match the requested server (using normalized URL)\n // Also accept credentials stored with the original URL format for backward compatibility\n const storedUrlNormalized = normalizeUrl(credentials.serverUrl);\n if (storedUrlNormalized !== normalizedUrl) {\n logger.warn(\"Credential file server URL mismatch\", {\n expected: normalizedUrl,\n filePath,\n found: credentials.serverUrl,\n });\n return undefined;\n }\n\n logger.debug(\"Loaded stored credentials\", {\n clientId: credentials.clientInfo?.client_id,\n hasRefreshToken: !!credentials.tokens?.refresh_token,\n hasTokens: !!credentials.tokens,\n serverUrl: normalizedUrl,\n });\n\n return credentials;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n // File doesn't exist - no stored credentials\n logger.debug(\"No stored credentials found\", { serverUrl: normalizedUrl });\n return undefined;\n }\n\n logger.warn(\"Failed to load stored credentials\", {\n error: String(error),\n filePath,\n serverUrl: normalizedUrl,\n });\n return undefined;\n }\n }\n\n /**\n * Saves credentials to disk.\n */\n async save(credentials: StoredCredentials): Promise<void> {\n await this.ensureStorageDir();\n\n const filePath = this.getFilePath(credentials.serverUrl);\n const content = JSON.stringify(credentials, null, 2);\n\n try {\n await writeFile(filePath, content, { encoding: \"utf-8\", mode: 0o600 });\n // Ensure permissions are correct even if file existed\n await chmod(filePath, 0o600);\n\n logger.debug(\"Saved credentials to disk\", {\n clientId: credentials.clientInfo?.client_id,\n filePath,\n hasTokens: !!credentials.tokens,\n serverUrl: credentials.serverUrl,\n });\n } catch (error) {\n logger.error(\"Failed to save credentials\", {\n error: String(error),\n filePath,\n serverUrl: credentials.serverUrl,\n });\n throw error;\n }\n }\n\n /**\n * Updates specific fields in stored credentials.\n * Creates new credentials if none exist.\n */\n async update(\n serverUrl: string,\n updates: Partial<Pick<StoredCredentials, \"clientInfo\" | \"tokens\" | \"tokensObtainedAt\">>\n ): Promise<StoredCredentials> {\n // Normalize URL to ensure consistent key generation and storage\n const normalizedUrl = normalizeUrl(serverUrl);\n const existing = await this.load(normalizedUrl);\n const now = new Date().toISOString();\n\n const credentials: StoredCredentials = existing\n ? {\n ...existing,\n ...updates,\n // Update serverUrl to normalized form for consistency\n serverUrl: normalizedUrl,\n updatedAt: now,\n }\n : {\n createdAt: now,\n serverUrl: normalizedUrl,\n updatedAt: now,\n ...updates,\n };\n\n await this.save(credentials);\n return credentials;\n }\n\n /**\n * Ensures the storage directory exists with proper permissions.\n */\n private async ensureStorageDir(): Promise<void> {\n try {\n await mkdir(this.storageDir, { mode: 0o700, recursive: true });\n } catch (error) {\n // Directory may already exist\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") {\n throw error;\n }\n }\n\n // Best-effort ownership repair; should not block auth flows\n try {\n await chownToRealUser(this.storageDir);\n } catch (error) {\n logger.warn(\"Failed to fix OAuth storage directory ownership\", {\n error: String(error),\n storageDir: this.storageDir,\n });\n }\n }\n\n /**\n * Generates a URL-safe filename from a server URL.\n * Uses SHA-256 hash truncated to 16 chars for brevity.\n */\n private generateKey(serverUrl: string): string {\n const hash = createHash(\"sha256\").update(serverUrl).digest(\"hex\");\n return hash.substring(0, 16);\n }\n\n /**\n * Gets the file path for a server's credentials.\n */\n private getFilePath(serverUrl: string): string {\n const key = this.generateKey(serverUrl);\n return join(this.storageDir, `${key}.json`);\n }\n}\n\n","/**\n * Dynamic OAuth bearer for the gateway's HTTP transports (PRDCT-3666 Tier 2).\n *\n * The gateway's first cut bound the access_token into the HTTP transport's\n * Authorization header ONCE, at connect time (`main.ts:606`). Once the gateway\n * had successfully connected, an upstream-side token rotation, revocation, or\n * clock-skew-driven expiry would 401 every subsequent request, with no\n * recovery — the user's session was effectively dead until they restarted the\n * MCP client.\n *\n * `createDynamicAuth` returns a handle whose `getCurrentBearer()` is read at\n * fetch time (not connect time). `createAuthenticatingFetch` wraps the\n * transport's underlying `fetch` so that:\n *\n * 1. Each outbound request reads the current bearer just-in-time and\n * attaches it as `Authorization: Bearer ...`.\n * 2. If the upstream returns 401 to a request we sent with a bearer, the\n * wrapper triggers a single silent refresh attempt and retries the SAME\n * request once. Concurrent 401s share the same in-flight refresh\n * (single-flight) to avoid burning the refresh_token via rotation races.\n * 3. Before refreshing, the handle re-reads the on-disk credentials —\n * another gateway process may already have refreshed the token, in which\n * case we use the disk value instead of burning another refresh.\n * 4. If the refresh ultimately fails (`invalid_grant`, no refresh_token,\n * transient error), the wrapper does NOT open a browser — that decision\n * is left to the connect-time `main.ts` handler (which can fall back to\n * interactive OAuth when appropriate). The mid-stream contract is: a\n * browser must never open as a direct reaction to a 401.\n */\n\nimport type { FetchLike } from \"@modelcontextprotocol/sdk/shared/transport.js\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { tryRefreshCachedToken } from \"./oauthFlow.js\";\nimport type { OAuthCredentialStore } from \"./OAuthCredentialStore.js\";\n\nexport interface DynamicAuthHandle {\n /** Synchronous read of the in-memory bearer. May be undefined. */\n getCurrentBearer(): string | undefined;\n /**\n * Attempt a silent refresh. Deduplicates concurrent calls via a single-flight\n * promise. Before issuing an HTTP refresh, reads the on-disk credentials to\n * pick up any rotation done by a parallel gateway process.\n *\n * `sentBearer` is the bearer that just got 401'd. If the disk now holds a\n * different bearer, the on-disk value is adopted and returned without a new\n * HTTP call (multi-process race recovery).\n *\n * Returns the new bearer on success, undefined if refresh failed for any\n * reason (`invalid_grant`, transient, no refresh_token on disk).\n */\n refreshIfPossible(sentBearer?: string): Promise<string | undefined>;\n /** Replace the in-memory bearer (e.g., after a successful `performOAuthFlow`). */\n setBearer(bearer: string | undefined): void;\n}\n\n/**\n * Wraps `fetch` to attach the dynamic Authorization header and recover from\n * 401 responses via a single silent-refresh-and-retry. Pass the returned\n * function to the SDK transport's `fetch` option.\n *\n * Transport coverage notes — including the one case we deliberately do NOT\n * support (silent mid-stream drop on an open SSE event-stream) — are\n * documented in detail above `createTransport` in\n * `src/lib/transportDetection.ts`. Read it before changing this wrapper.\n */\nexport function createAuthenticatingFetch(\n auth: DynamicAuthHandle,\n baseFetch: typeof fetch = (...args) => globalThis.fetch(...args),\n): FetchLike {\n return async (url, init) => {\n // Whether the caller supplied a static Authorization (api-key / Basic /\n // static bearer via --env-http-headers, --headers, or --bearer-token-env).\n // The wrapper owns only the OAuth bearer; it must not clobber a credential\n // the caller explicitly provided. Without this guard, non-OAuth auth modes\n // break in any environment without a cached OAuth token (PRDCT-5136).\n const callerSuppliedAuth = new Headers(init?.headers).has(\"Authorization\");\n\n const sendInit = (bearer: string | undefined): RequestInit => {\n const headers = new Headers(init?.headers);\n if (bearer) {\n // OAuth bearer takes precedence and is refreshed mid-session.\n headers.set(\"Authorization\", `Bearer ${bearer}`);\n } else if (!callerSuppliedAuth) {\n // No bearer and no caller-supplied credential: clear only the slot the\n // wrapper would otherwise manage, so we never send a stale OAuth token.\n headers.delete(\"Authorization\");\n }\n // else: no bearer but the caller supplied a static Authorization — leave\n // it intact so the upstream can authenticate it (api-key / Basic / etc.).\n return { ...init, headers };\n };\n\n const bearer = auth.getCurrentBearer();\n let response = await baseFetch(url, sendInit(bearer));\n\n // Diagnostic: capture the wire-level details of any non-2xx upstream\n // response. The SDK transport only surfaces \"HTTP <code>: <body>\" and drops\n // response headers — but WWW-Authenticate is exactly the signal we need\n // post-mortem to tell a non-compliant 500 from a spec-correct 401 + OAuth\n // discovery pointer. The body is bounded and only read on error (a streaming\n // 2xx is never cloned). A 401 is the expected OAuth-challenge case (info);\n // any other non-2xx is surprising (warn).\n if (!response.ok) {\n let bodySnippet = \"\";\n try {\n bodySnippet = (await response.clone().text()).slice(0, 500);\n } catch {\n /* body not readable (already consumed or streaming) */\n }\n const diag = {\n bodySnippet,\n contentType: response.headers.get(\"content-type\") ?? undefined,\n eventType: \"mcp_upstream_non_2xx\",\n method: String(init?.method ?? \"GET\").toUpperCase(),\n requestAccept: new Headers(init?.headers).get(\"accept\") ?? undefined,\n requestHadAuth: Boolean(bearer) || callerSuppliedAuth,\n status: response.status,\n url: sanitizeUrlForLog(url),\n wwwAuthenticate: response.headers.get(\"www-authenticate\") ?? undefined,\n };\n if (response.status === 401) {\n logger.info(\"Upstream returned non-2xx (auth challenge)\", diag);\n } else {\n logger.warn(\"Upstream returned non-2xx\", diag);\n }\n }\n\n // Only attempt refresh-and-retry when we actually sent a bearer that was\n // rejected. A 401 without a bearer means the upstream is asking us to\n // authenticate from scratch — that path is handled by the connect-time\n // logic in main.ts, not here.\n if (response.status === 401 && bearer) {\n logger.info(\"Upstream returned 401; attempting mid-session silent refresh\", {\n eventType: \"mcp_auth_mid_session_refresh_attempt\",\n });\n const refreshed = await auth.refreshIfPossible(bearer);\n if (refreshed && refreshed !== bearer) {\n logger.info(\"Silent refresh succeeded mid-session; retrying request\", {\n eventType: \"mcp_auth_mid_session_refresh_succeeded\",\n });\n response = await baseFetch(url, sendInit(refreshed));\n } else {\n logger.warn(\"Silent refresh produced no new bearer; passing 401 through\", {\n eventType: \"mcp_auth_mid_session_refresh_failed\",\n });\n }\n }\n\n return response;\n };\n}\n\nexport function createDynamicAuth(\n serverUrl: string,\n credentialStore: OAuthCredentialStore,\n initialBearer: string | undefined,\n): DynamicAuthHandle {\n let currentBearer = initialBearer;\n let refreshInFlight: null | Promise<string | undefined> = null;\n\n return {\n getCurrentBearer: () => currentBearer,\n refreshIfPossible: (sentBearer) => {\n // Single-flight: concurrent callers share the same refresh promise so\n // that N parallel 401s yield ONE refresh HTTP call (not N), avoiding\n // refresh_token rotation races within this process.\n if (refreshInFlight) return refreshInFlight;\n refreshInFlight = (async () => {\n try {\n // Multi-process recovery: another gateway process may already have\n // refreshed since we loaded our in-memory bearer. Re-read disk\n // before committing to a refresh HTTP call.\n const onDisk = await credentialStore.load(serverUrl);\n const diskBearer = onDisk?.tokens?.access_token;\n if (diskBearer && diskBearer !== sentBearer) {\n logger.info(\"Adopting newer access_token from disk (other process refreshed)\", {\n eventType: \"mcp_auth_disk_bearer_adopted\",\n serverUrl,\n });\n currentBearer = diskBearer;\n return diskBearer;\n }\n // Disk has the same bearer we tried (or none). Issue a refresh.\n const result = await tryRefreshCachedToken(serverUrl, credentialStore);\n if (result?.tokens.access_token) {\n currentBearer = result.tokens.access_token;\n return currentBearer;\n }\n // Refresh failed (invalid_grant cleared the tokens, invalid_client\n // cleared the whole record, transient, or no refresh_token on disk).\n // Leave currentBearer as-is so the connect-time handler can take over\n // when this gateway process restarts; callers must NOT open a browser\n // from inside the fetch wrapper.\n return undefined;\n } finally {\n refreshInFlight = null;\n }\n })();\n return refreshInFlight;\n },\n setBearer: (bearer) => {\n currentBearer = bearer;\n },\n };\n}\n\n/**\n * Origin + pathname only — drops the query string / fragment, which can carry\n * tokens or other sensitive material. Used for diagnostic logging of upstream\n * requests so a log line never leaks a credential.\n */\nfunction sanitizeUrlForLog(url: unknown): string {\n try {\n const s = typeof url === \"string\" ? url : String(url);\n const u = new URL(s);\n return `${u.origin}${u.pathname}`;\n } catch {\n return \"[unparseable-url]\";\n }\n}\n","import { createInterface } from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\n\n/**\n * Surface a startup failure to the parent MCP client (e.g. OpenCode, Cursor,\n * Claude Code) as a specific message instead of the generic\n * `MCP error -32000: Connection closed`.\n *\n * Why this is needed: the gateway connects to its upstream BEFORE it opens the\n * client-facing stdio transport, so every startup failure happens before the\n * MCP `Server` exists. Exiting at that point closes the stdio pipe with no\n * JSON-RPC reply, and the client renders that as `-32000`. The only thing a\n * stdio MCP client renders as a readable reason is a JSON-RPC **error reply to\n * the `initialize` request** (an error on `tools/list` is shown only as a\n * generic \"Failed to get tools\" — verified against OpenCode).\n *\n * So on a startup failure we briefly read stdin, wait for the client's first\n * request (`initialize`), reply with a JSON-RPC error carrying `reason`, then\n * let the caller exit. This must run ONLY before the real `Server` transport is\n * connected (otherwise two readers would fight over stdin) and ONLY in stdio\n * mode with a real client attached — hence the `isTTY` short-circuit.\n *\n * `input`/`output` default to process.stdin/stdout; they exist for unit tests.\n *\n * Resolves `true` if it replied to a request, `false` otherwise (TTY / no\n * client / timeout / stdin closed). Never rejects.\n */\nexport async function respondInitializeError(\n reason: string,\n opts: { code?: number; input?: Readable; output?: Writable; timeoutMs?: number } = {},\n): Promise<boolean> {\n const code = opts.code ?? -32603; // JSON-RPC InternalError; clients show `message`\n const timeoutMs = opts.timeoutMs ?? 4000;\n const input = opts.input ?? process.stdin;\n const output = opts.output ?? process.stdout;\n\n // Run directly in a terminal (no MCP client over the pipe) → nothing to answer.\n if ((input as { isTTY?: boolean }).isTTY) return false;\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n let replied = false;\n // Claimed synchronously the moment we decide to answer a request, so several\n // id-bearing requests buffered before the (async) write callback runs can't\n // each emit their own reply — exactly one JSON-RPC error ever hits stdout.\n let claimed = false;\n const rl = createInterface({ input });\n\n const finish = () => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n rl.removeAllListeners(\"line\");\n rl.close();\n resolve(replied);\n };\n\n // Cap the wait: a piped-but-silent stdin (no client speaking) must not hang.\n const timer = setTimeout(finish, timeoutMs);\n timer.unref?.();\n\n rl.on(\"line\", (line) => {\n if (claimed) return; // a reply is already in flight — ignore the rest\n const trimmed = line.trim();\n if (!trimmed) return;\n let msg: { id?: unknown };\n try {\n msg = JSON.parse(trimmed) as { id?: unknown };\n } catch {\n return; // ignore non-JSON noise\n }\n // The first request carrying an id is `initialize`. Reply to it with the\n // error so the client shows `reason` rather than `-32000`.\n if (msg && msg.id !== undefined && msg.id !== null) {\n claimed = true; // set BEFORE the async write; stop processing buffered lines now\n // We've committed to replying (the bytes are handed to output.write below).\n // Set `replied` here too, so a stdin end/timeout racing the write callback\n // can't resolve false despite the reply having been written.\n replied = true;\n rl.removeAllListeners(\"line\");\n output.write(\n `${JSON.stringify({ error: { code, message: reason }, id: msg.id, jsonrpc: \"2.0\" })}\\n`,\n () => {\n finish();\n },\n );\n }\n });\n\n input.once(\"end\", finish);\n input.once(\"error\", finish);\n });\n}\n","/**\n * Classification of errors from the upstream MCP server connection.\n *\n * The gateway connects to a remote MCP server whose reachability can flap\n * (DNS/VPN/tailscale blips, brief outages). undici's global `fetch` surfaces\n * such failures as `TypeError: fetch failed` with the real OS error on\n * `err.cause` (e.g. `Error: connect ECONNREFUSED` carrying `.code`). These are\n * transient: retrying the connection usually succeeds once connectivity\n * returns. This module decides which errors are worth retrying/reconnecting on,\n * and extracts the OS errno for diagnostics — the fatal-startup path previously\n * logged only the opaque `\"fetch failed\"` message and dropped the errno, which\n * made this class of failure hard to triage.\n *\n * `is401Error` is the single source of truth for \"the upstream is asking us to\n * authenticate\" (also consumed by the connect-time OAuth logic in main.ts). A\n * 401 is deliberately NOT a recoverable connection error — it needs an auth\n * refresh / OAuth flow, not a blind retry.\n */\n\nimport { UnauthorizedError } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport { McpError } from \"@modelcontextprotocol/sdk/types.js\";\n\nimport { AccessControlBlockError } from \"./errors.js\";\n\n/**\n * OS / undici error codes that indicate a transport-level failure the gateway\n * can recover from by retrying (or reconnecting) the upstream connection. This\n * spans both connect-phase failures (connection never established) and\n * mid-flight drops; the retry-vs-reconnect decision keys off recoverability,\n * while retry SAFETY for side-effecting calls is a separate concern.\n */\nconst RECOVERABLE_ERRNOS = new Set<string>([\n \"EAI_AGAIN\",\n \"ECONNABORTED\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"EHOSTDOWN\",\n \"EHOSTUNREACH\",\n \"ENETDOWN\",\n \"ENETUNREACH\",\n \"ENOTFOUND\",\n \"EPIPE\",\n \"EPROTO\",\n \"ETIMEDOUT\",\n \"UND_ERR_BODY_TIMEOUT\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n]);\n\n/**\n * Connection-phase errnos: the TCP connection was never established, so the\n * request provably never reached the upstream (let alone a tool). Safe to retry\n * even a side-effecting call — it cannot have executed. A mid-flight reset\n * (ECONNRESET/EPIPE) or timeout is deliberately NOT here: the request may have\n * been received and acted on, so retrying it is unsafe (see isPreDispatchError).\n */\nconst PRE_DISPATCH_ERRNOS = new Set<string>([\n \"EAI_AGAIN\",\n \"ECONNREFUSED\",\n \"EHOSTDOWN\",\n \"EHOSTUNREACH\",\n \"ENETDOWN\",\n \"ENETUNREACH\",\n \"ENOTFOUND\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n]);\n\n/**\n * Message fragments that identify a transport failure when no structured errno\n * is available (e.g. the MCP SDK stringifies transport errors into a plain\n * `Error.message`, and `ProxyConnectionError` embeds the underlying message).\n * Kept to unambiguous transport/timeout phrases to avoid misclassifying an\n * application-level error as a connection problem.\n */\n// A pure client-side request timeout (\"MCP error -32001: Request timed out\") is\n// deliberately NOT here: a tool merely exceeding the forward timeout on a healthy\n// connection would otherwise trigger a needless reconnect that closes the client\n// and aborts sibling in-flight calls. Genuine connection death still surfaces as\n// an errno or a \"connection closed\"/\"not connected\" message, which ARE covered.\nconst RECOVERABLE_MESSAGE_RE =\n /\\bfetch failed\\b|socket hang up|other side closed|\\bterminated\\b|network (?:error|is unreachable)|econnrefused|econnreset|econnaborted|etimedout|enotfound|eai_again|enetunreach|ehostunreach|und_err|not connected|transport (?:is )?closed|connection closed/i;\n\n/**\n * Message fragments meaning the request was NOT transmitted — the transport was\n * already closed when we tried to send. Like a connect-phase errno, this is\n * safe to retry even for a side-effecting call: it provably never executed.\n * (The MCP SDK throws \"Not connected\" when its transport is closed.)\n */\nconst PRE_DISPATCH_MESSAGE_RE = /\\bnot connected\\b|transport (?:is )?closed/i;\n\n// MCP-session-expiry wording. The strong set is safe to trust even inside an\n// McpError (a JSON-RPC error from a tool); the general set may appear in\n// app-level tool text and is only trusted for transport-level (non-McpError)\n// errors — see isStaleSessionError.\nconst SESSION_PHRASE_RE =\n /session (?:not found|expired|has expired|is invalid|invalid|mismatch|does not|no longer)|no valid session|unknown session|mcp-session-id/i;\nconst STRONG_SESSION_PHRASE_RE = /no valid session|unknown session|mcp-session-id|session not found/i;\n\n/**\n * Walks the common nested error shapes (`cause`, `response`, `data`, `body`) to\n * find an OS/undici errno string like `ECONNREFUSED`. undici puts the real\n * cause of a `fetch failed` on `err.cause`. Returns undefined when no errno is\n * present. Mirrors the `.cause`-walking idiom in `src/auth/errors.ts`.\n */\nexport function getUpstreamErrno(error: unknown): string | undefined {\n return walkForErrno(error, new WeakSet<object>());\n}\n\n/**\n * Extracts an HTTP-ish status code from the common nested error shapes. The MCP\n * SDK's Streamable-HTTP transport surfaces some failures as an error carrying a\n * numeric `status`/`statusCode`/`code`. Returns undefined when none is present\n * (e.g. a plain `Error POSTing to endpoint: <body>` that dropped the status).\n */\nexport function getUpstreamHttpStatus(error: unknown): number | undefined {\n return walkForStatus(error, new WeakSet<object>());\n}\n\n/**\n * Whether the upstream is asking us to authenticate (HTTP 401 / SDK\n * UnauthorizedError). Extracted from main.ts so the connect-time OAuth handler\n * and the recoverable-error classifier share one definition.\n */\nexport function is401Error(error: unknown): boolean {\n if (error instanceof UnauthorizedError) return true;\n if (error && typeof error === \"object\") {\n const status = (error as { status?: number }).status;\n const code = (error as { code?: number }).code;\n const message = (error as { message?: string }).message;\n if (status === 401 || code === 401) return true;\n // Require auth/HTTP context around a textual 401 so an unrelated transport\n // error whose text merely contains \"401\" (a port like `:401`, \"401 bytes\")\n // isn't classified as auth — this now gates retry-vs-fail-fast, and the\n // structured status/code checks above already cover real 401 responses.\n if (message && /\\bunauthorized\\b|\\b(?:http|status)(?:\\s+(?:status|code))?\\s*[:=]?\\s*401\\b/i.test(String(message)))\n return true;\n }\n return false;\n}\n\n/**\n * Whether an error is safe to retry even for a side-effecting call (tools/call):\n * the request provably never reached tool execution. True for connect-phase\n * network failures (connection never established) and stale-session rejections\n * (rejected at the session gate). Mid-flight resets/timeouts and 5xx are NOT\n * pre-dispatch — the request may already have executed, so the reconnecting\n * client reconnects but does NOT retry those for side-effecting calls.\n */\nexport function isPreDispatchError(error: unknown): boolean {\n // A JSON-RPC McpError is a response from the server's protocol layer, which\n // proves the request was dispatched and processed — so it can NEVER be\n // pre-dispatch, and a side-effecting tools/call must not be retried on it\n // (that is the double-execution bug). The SDK's closed-transport signal\n // (\"Not connected\") is a plain Error, so the intended path below is unaffected.\n if (error instanceof McpError) return false;\n const errno = getUpstreamErrno(error);\n if (errno && PRE_DISPATCH_ERRNOS.has(errno)) return true;\n if (isStaleSessionError(error)) return true;\n return PRE_DISPATCH_MESSAGE_RE.test(messageOf(error));\n}\n\n/**\n * Whether an upstream connection/forwarding error is a transient transport\n * failure worth retrying the connect or reconnecting the session for.\n *\n * Explicitly NOT recoverable: access-control blocks (a policy decision, not a\n * connection problem) and 401s (need auth, not a retry).\n */\nexport function isRecoverableUpstreamError(error: unknown): boolean {\n if (error instanceof AccessControlBlockError) return false;\n if (is401Error(error)) return false;\n\n const errno = getUpstreamErrno(error);\n if (errno && RECOVERABLE_ERRNOS.has(errno)) return true;\n\n // A terminated/expired MCP session is recoverable by re-initializing.\n if (isStaleSessionError(error)) return true;\n\n // 5xx from the upstream is transient (deploy, overload) — worth a reconnect.\n const status = getUpstreamHttpStatus(error);\n if (status !== undefined && status >= 500) return true;\n\n const message = messageOf(error);\n if (message.length > 0 && RECOVERABLE_MESSAGE_RE.test(message)) return true;\n\n return false;\n}\n\n/**\n * Whether the upstream rejected the request because its MCP session is no longer\n * valid — the spec's 404-on-terminated-session, the 400 \"no valid session\"\n * variant some servers return, or an explicit session-mismatch message. Such a\n * rejection happens at the session gate, BEFORE the tool runs, so re-initializing\n * and retrying is safe.\n */\nexport function isStaleSessionError(error: unknown): boolean {\n const message = messageOf(error);\n // A tool's own McpError may mention \"session\" in app-level text (e.g. surfacing\n // a backing-API auth error) — that is not a stale MCP *session*. Only strong\n // transport-session wording is trusted inside an McpError. A genuine terminated\n // session arrives as a StreamableHTTPError (HTTP 404), handled below.\n if (error instanceof McpError) return STRONG_SESSION_PHRASE_RE.test(message);\n const status = getUpstreamHttpStatus(error);\n if (status === 404) return true; // spec: server 404s a request bearing a terminated session id\n if (status === 400 && SESSION_PHRASE_RE.test(message)) return true;\n // The status is frequently absent on the thrown error; fall back to the phrase.\n return SESSION_PHRASE_RE.test(message);\n}\n\nfunction messageOf(error: unknown): string {\n if (error instanceof Error) return error.message ?? \"\";\n if (typeof error === \"string\") return error;\n if (error && typeof error === \"object\" && \"message\" in error) {\n return String((error as { message?: unknown }).message ?? \"\");\n }\n return \"\";\n}\n\nfunction walkForErrno(error: unknown, seen: WeakSet<object>): string | undefined {\n if (error == null || typeof error !== \"object\") return undefined;\n if (seen.has(error)) return undefined;\n seen.add(error);\n\n const code = (error as NodeJS.ErrnoException).code;\n // Node errno codes are uppercase strings (ECONNREFUSED, UND_ERR_SOCKET). A\n // numeric `code` here is an HTTP-ish status, not an errno — skip it.\n if (typeof code === \"string\" && /^[A-Z][A-Z0-9_]*$/.test(code)) {\n return code;\n }\n\n for (const key of [\"cause\", \"response\", \"data\", \"body\"] as const) {\n const found = walkForErrno((error as Record<string, unknown>)[key], seen);\n if (found) return found;\n }\n return undefined;\n}\n\nfunction walkForStatus(error: unknown, seen: WeakSet<object>): number | undefined {\n if (error == null || typeof error !== \"object\") return undefined;\n if (seen.has(error)) return undefined;\n seen.add(error);\n\n // An McpError's `code` is a JSON-RPC error code, not an HTTP status. A tool\n // that returns McpError(404, ...) already executed, so reading its code as an\n // HTTP/stale-session signal would retry a side-effecting call (double exec). A\n // genuine transport HTTP status arrives as a StreamableHTTPError (not McpError).\n const keys =\n error instanceof McpError ? ([\"status\", \"statusCode\"] as const) : ([\"status\", \"statusCode\", \"code\"] as const);\n for (const key of keys) {\n const value = (error as Record<string, unknown>)[key];\n // Only treat 3-digit HTTP-range numbers as a status. A Node errno (string\n // code) or a negative JSON-RPC code (e.g. -32001) is not an HTTP status.\n if (typeof value === \"number\" && value >= 100 && value <= 599) return value;\n }\n\n for (const key of [\"cause\", \"response\", \"data\", \"body\"] as const) {\n const found = walkForStatus((error as Record<string, unknown>)[key], seen);\n if (found !== undefined) return found;\n }\n return undefined;\n}\n","/**\n * Bounded retry-with-backoff for the initial upstream connection.\n *\n * The gateway connects to the upstream MCP server once at startup and treats a\n * failure as fatal (\"MCP server failed to start\") — so a brief reachability\n * blip at the moment the MCP client spawns the gateway kills the whole session\n * (the tools never appear, and the client won't respawn a failed stdio server).\n * Because upstream reachability flaps and self-recovers within seconds, a small\n * bounded retry turns most of these transient failures into a successful start.\n *\n * Retries ONLY on recoverable transport errors (default:\n * `isRecoverableUpstreamError`); a policy block / 401 / config error fails fast\n * so the existing connect-time handlers (interactive OAuth, clear error) still\n * run. Bounded by both `maxRetries` and a hard wall-clock cap (`maxElapsedMs`)\n * so the retry budget never blows the MCP client's own server-startup timeout.\n *\n * Mirrors the retry shape of `src/probe/index.ts`, adding exponential backoff\n * with jitter.\n */\n\nimport { isRecoverableUpstreamError } from \"./upstreamErrors.js\";\n\nexport interface ConnectRetryHooks {\n /** Decide whether an error is worth retrying. Default: recoverable transport errors. */\n isRetryable?: (error: unknown) => boolean;\n /** Injectable clock/sleep for tests. Default: real setTimeout. */\n now?: () => number;\n /** Called before each backoff sleep — used to emit a retry log event. */\n onRetry?: (info: { attempt: number; delayMs: number; error: unknown }) => void;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface ConnectRetryOptions {\n /** Delay before the first retry, in ms (grows exponentially per attempt). */\n initialDelayMs: number;\n /** Ceiling for a single backoff delay, in ms. */\n maxDelayMs: number;\n /** Hard wall-clock cap across all attempts, in ms. 0 disables the cap. */\n maxElapsedMs: number;\n /** Retries after the first attempt. 0 disables retry (single attempt). */\n maxRetries: number;\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Runs `connectFn`, retrying recoverable failures with exponential backoff +\n * jitter until it succeeds, `maxRetries` is exhausted, a non-retryable error is\n * thrown, or the `maxElapsedMs` wall-clock budget runs out. Rethrows the last\n * error on give-up so the caller's existing fatal-startup handling applies.\n */\nexport async function connectWithRetry<T>(\n connectFn: () => Promise<T>,\n options: ConnectRetryOptions,\n hooks: ConnectRetryHooks = {}\n): Promise<T> {\n const isRetryable = hooks.isRetryable ?? isRecoverableUpstreamError;\n const sleep = hooks.sleep ?? defaultSleep;\n const now = hooks.now ?? Date.now;\n\n const start = now();\n let attempt = 0;\n\n for (;;) {\n try {\n return await connectFn();\n } catch (error) {\n attempt += 1;\n if (attempt > options.maxRetries || !isRetryable(error)) {\n throw error;\n }\n\n let delayMs = backoffDelayMs(attempt, options);\n\n if (options.maxElapsedMs > 0) {\n const remaining = options.maxElapsedMs - (now() - start);\n // No budget left for another attempt after the wait — give up now.\n if (remaining <= 0) throw error;\n delayMs = Math.min(delayMs, remaining);\n }\n\n hooks.onRetry?.({ attempt, delayMs, error });\n await sleep(delayMs);\n }\n }\n}\n\n/**\n * Exponential backoff (initialDelayMs * 2^(attempt-1), capped at maxDelayMs)\n * with ±20% jitter so concurrent gateways don't retry in lockstep.\n */\nfunction backoffDelayMs(attempt: number, options: ConnectRetryOptions): number {\n const base = Math.min(options.initialDelayMs * 2 ** (attempt - 1), options.maxDelayMs);\n const jitter = base * 0.2 * (Math.random() * 2 - 1);\n return Math.max(0, Math.round(base + jitter));\n}\n","/**\n * Mid-session reconnect for the upstream MCP connection.\n *\n * The gateway connects to the upstream once and hands that client to\n * `proxyServer`, whose handlers capture it in closures. When the upstream drops\n * mid-session (a reachability flap, or a terminated/expired session) every\n * subsequent forwarded call throws forever — the session is dead until the MCP\n * client restarts the gateway process. That mid-session death is what this fixes.\n *\n * `createReconnectingClient` returns a STABLE façade with the exact method set\n * `proxyServer` uses. It holds the live client in a mutable slot; on a\n * recoverable failure it rebuilds the connection (via the injected `reconnect`\n * factory), replays the notification handlers onto the new client, swaps it in,\n * and retries the failed call — all transparently, so `proxyServer` never knows\n * the connection was replaced. Because the façade identity is stable, the\n * handler closures always hit the current client.\n *\n * Contract:\n * - Reconnect only on `isRecoverableUpstreamError` (transport failure / stale\n * session / 5xx). Never on a policy block or 401.\n * - Single-flight + generation guard: N concurrent failures trigger ONE\n * reconnect; a call that failed on an already-replaced client just retries on\n * the new one instead of reconnecting again.\n * - Cooldown collapses bursts; reconnect is driven by traffic (one attempt per\n * failing call), never a background loop — so a sustained outage self-heals on\n * the next call once connectivity returns, without hammering.\n * - Retry SAFETY is method-aware: idempotent reads retry on any recoverable\n * error; a side-effecting `tools/call` retries ONLY on a pre-dispatch error\n * (connection never established / stale-session reject), never on an ambiguous\n * mid-flight failure that may already have executed.\n *\n * The `reconnect` factory MUST NOT open an interactive OAuth browser (see\n * main.ts `allowInteractiveAuth`); a silent token refresh still happens inside\n * the transport's fetch wrapper.\n */\n\nimport type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\nimport { logger } from \"./logger.js\";\nimport { getUpstreamErrno, isPreDispatchError, isRecoverableUpstreamError } from \"./upstreamErrors.js\";\n\nexport interface ReconnectingClientOptions {\n /** Minimum gap between reconnect attempts (ms) — collapses bursts. */\n cooldownMs: number;\n /** The already-connected client to start from. */\n initial: Client;\n /** Injectable clock for tests. Defaults to Date.now. */\n now?: () => number;\n /**\n * Builds a fresh, connected client. Called on a recoverable mid-session\n * failure. MUST NOT trigger interactive OAuth (no browser mid-session).\n */\n reconnect: () => Promise<Client>;\n}\n\n/**\n * The subset of the MCP SDK `Client` that `proxyServer` forwards through. The\n * real `Client` structurally satisfies this, so the reconnecting façade can\n * stand in for it wherever `proxyServer` expects a client.\n */\nexport type UpstreamClient = Pick<\n Client,\n | \"callTool\"\n | \"close\"\n | \"complete\"\n | \"getPrompt\"\n | \"listPrompts\"\n | \"listResources\"\n | \"listResourceTemplates\"\n | \"listTools\"\n | \"notification\"\n | \"readResource\"\n | \"setNotificationHandler\"\n | \"subscribeResource\"\n | \"unsubscribeResource\"\n>;\n\ntype SetNotificationHandlerArgs = Parameters<Client[\"setNotificationHandler\"]>;\n\nexport function createReconnectingClient(options: ReconnectingClientOptions): UpstreamClient {\n const now = options.now ?? Date.now;\n\n let current = options.initial;\n // Bumped on every successful swap. A forwarded call captures the generation it\n // dispatched under; if it later fails but the generation has moved on, another\n // call already reconnected — so it just retries on the new client.\n let generation = 0;\n let lastAttemptAt = Number.NEGATIVE_INFINITY;\n let reconnecting: null | Promise<boolean> = null;\n let closed = false;\n let sawSubscribe = false;\n const notificationHandlers: SetNotificationHandlerArgs[] = [];\n\n const doReconnect = async (): Promise<boolean> => {\n logger.info(\"Upstream connection lost mid-session; reconnecting\", {\n eventType: \"mcp_upstream_reconnect_attempt\",\n });\n if (sawSubscribe) {\n // A fresh session has no subscriptions; surface it rather than silently\n // dropping resource-update delivery. (Subscription replay is a later step.)\n logger.warn(\"Reconnecting with resource subscriptions active; they will not survive the new session\", {\n eventType: \"mcp_upstream_reconnect_subscriptions_dropped\",\n });\n }\n try {\n const next = await options.reconnect();\n // close() may have run while this reconnect was in flight. Don't publish a\n // fresh client after shutdown (it would leak a live connection) — close it\n // and report failure.\n if (closed) {\n await Promise.resolve()\n .then(() => next.close())\n .catch(() => {});\n return false;\n }\n // Replay recorded notification handlers onto the new client BEFORE swapping\n // so an upstream notification arriving right after the swap is handled.\n for (const args of notificationHandlers) {\n next.setNotificationHandler(...args);\n }\n const old = current;\n current = next;\n generation += 1;\n // Close the old client after publishing the new one, so no new dispatch\n // lands on a closing connection. Best-effort; errors are irrelevant here.\n void Promise.resolve()\n .then(() => old.close())\n .catch(() => {});\n logger.info(\"Upstream reconnected\", { eventType: \"mcp_upstream_reconnect_succeeded\" });\n return true;\n } catch (error) {\n logger.warn(\"Upstream reconnect failed\", {\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_upstream_reconnect_failed\",\n });\n return false;\n }\n };\n\n const ensureReconnected = (genAtDispatch: number): Promise<boolean> => {\n // Someone already reconnected past the generation this call dispatched under.\n if (generation > genAtDispatch) return Promise.resolve(true);\n // Join an in-flight reconnect (single-flight dedup of concurrent failures).\n if (reconnecting) return reconnecting;\n // Cooldown: don't hammer a still-down upstream from a burst of failed calls.\n if (now() - lastAttemptAt < options.cooldownMs) return Promise.resolve(false);\n lastAttemptAt = now();\n reconnecting = doReconnect().finally(() => {\n reconnecting = null;\n });\n return reconnecting;\n };\n\n const forward = async <T>(idempotent: boolean, invoke: (client: Client) => Promise<T>): Promise<T> => {\n const gen = generation;\n try {\n return await invoke(current);\n } catch (error) {\n const swappedUnderUs = generation > gen;\n if (closed || (!swappedUnderUs && !isRecoverableUpstreamError(error))) throw error;\n const reconnected = await ensureReconnected(gen);\n // Retry once, on the (possibly new) current client. A side-effecting call\n // is retried only when the error proves the request never executed.\n if (reconnected && (idempotent || isPreDispatchError(error))) {\n return await invoke(current);\n }\n throw error;\n }\n };\n\n const facade = {\n callTool: (...args: Parameters<Client[\"callTool\"]>) => forward(false, (c) => c.callTool(...args)),\n close: (...args: Parameters<Client[\"close\"]>) => {\n closed = true;\n return current.close(...args);\n },\n complete: (...args: Parameters<Client[\"complete\"]>) => forward(true, (c) => c.complete(...args)),\n getPrompt: (...args: Parameters<Client[\"getPrompt\"]>) => forward(true, (c) => c.getPrompt(...args)),\n listPrompts: (...args: Parameters<Client[\"listPrompts\"]>) => forward(true, (c) => c.listPrompts(...args)),\n listResources: (...args: Parameters<Client[\"listResources\"]>) => forward(true, (c) => c.listResources(...args)),\n listResourceTemplates: (...args: Parameters<Client[\"listResourceTemplates\"]>) =>\n forward(true, (c) => c.listResourceTemplates(...args)),\n listTools: (...args: Parameters<Client[\"listTools\"]>) => forward(true, (c) => c.listTools(...args)),\n // A notification is one-way (no response). Treat it as non-idempotent so it\n // is only resent on a pre-dispatch failure (provably never sent) — never on\n // an ambiguous mid-flight failure, which could duplicate a delivered message.\n notification: (...args: Parameters<Client[\"notification\"]>) => forward(false, (c) => c.notification(...args)),\n readResource: (...args: Parameters<Client[\"readResource\"]>) => forward(true, (c) => c.readResource(...args)),\n setNotificationHandler: (...args: SetNotificationHandlerArgs) => {\n notificationHandlers.push(args);\n return current.setNotificationHandler(...args);\n },\n subscribeResource: (...args: Parameters<Client[\"subscribeResource\"]>) => {\n sawSubscribe = true;\n return forward(true, (c) => c.subscribeResource(...args));\n },\n unsubscribeResource: (...args: Parameters<Client[\"unsubscribeResource\"]>) =>\n forward(true, (c) => c.unsubscribeResource(...args)),\n };\n\n return facade as UpstreamClient;\n}\n","import { Transform } from \"node:stream\";\n\nimport { logger } from \"./lib/logger.js\";\n\nconst MAX_DROPPED_LINES_LOGGED = 50;\n\n/**\n * Filters out lines that do not start with '{' from the input stream.\n * We use this to drop anything that is obviously not a JSON-RPC message.\n */\nexport class JSONFilterTransform extends Transform {\n private buffer = \"\";\n private droppedLinesLogged = 0;\n\n constructor() {\n super({ objectMode: false });\n }\n\n _flush(callback: (error: Error | null, chunk: Buffer | null) => void) {\n // Handle any remaining data in buffer\n if (this.buffer.trim().startsWith(\"{\")) {\n callback(null, Buffer.from(this.buffer));\n } else {\n if (this.buffer.trim().length > 0) {\n this.logDroppedLine(this.buffer);\n }\n callback(null, null);\n }\n }\n\n _transform(chunk: Buffer, _encoding: string, callback: (error: Error | null, chunk: Buffer | null) => void) {\n this.buffer += chunk.toString();\n const lines = this.buffer.split(\"\\n\");\n\n // Keep the last incomplete line in the buffer\n this.buffer = lines.pop() || \"\";\n\n const jsonLines: string[] = [];\n for (const line of lines) {\n if (line.trim().startsWith(\"{\")) {\n jsonLines.push(line);\n } else if (line.trim().length > 0) {\n this.logDroppedLine(line);\n }\n }\n\n if (jsonLines.length > 0) {\n // Send filtered lines with newlines\n const output = `${jsonLines.join(\"\\n\")}\\n`;\n\n callback(null, Buffer.from(output));\n } else {\n callback(null, null);\n }\n }\n\n private logDroppedLine(line: string) {\n // Already past the suppression sentinel — stay silent.\n if (this.droppedLinesLogged > MAX_DROPPED_LINES_LOGGED) return;\n\n // First line past the cap: emit the sentinel exactly once. The increment\n // moves us into the `>` branch above, so subsequent calls fall through.\n if (this.droppedLinesLogged === MAX_DROPPED_LINES_LOGGED) {\n this.droppedLinesLogged += 1;\n logger.info(\"Suppressing further non-JSON stdout lines\", {\n eventType: \"mcp_server_stdout_dropped_suppressed\",\n loggedSoFar: MAX_DROPPED_LINES_LOGGED,\n });\n return;\n }\n\n this.droppedLinesLogged += 1;\n logger.info(\"MCP server stdout (non-JSON, dropped)\", {\n eventType: \"mcp_server_stdout_dropped\",\n line: line.length > 1024 ? `${line.slice(0, 1024)}…` : line,\n });\n }\n}\n","/**\n * Forked from https://github.com/modelcontextprotocol/typescript-sdk/blob/a1608a6513d18eb965266286904760f830de96fe/src/client/stdio.ts\n */\n\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport type { JSONRPCMessage } from \"@modelcontextprotocol/sdk/types.js\";\n\nimport {\n ReadBuffer,\n serializeMessage,\n} from \"@modelcontextprotocol/sdk/shared/stdio.js\";\nimport { type ChildProcess, type IOType, spawn, spawnSync } from \"node:child_process\";\nimport { PassThrough, type Stream } from \"node:stream\";\n\nimport { JSONFilterTransform } from \"./JSONFilterTransform.js\";\nimport { logger } from \"./lib/logger.js\";\nimport { redactCliArgs } from \"./utils/redactCliArgs.js\";\n\nexport type StdioServerParameters = {\n\t/**\n\t * Command line arguments to pass to the executable.\n\t */\n\targs?: string[];\n\n\t/**\n\t * The executable to run to start the server.\n\t */\n\tcommand: string;\n\n\t/**\n\t * The working directory to use when spawning the process.\n\t *\n\t * If not specified, the current working directory will be inherited.\n\t */\n\tcwd?: string;\n\n\t/**\n\t * The environment to use when spawning the process.\n\t *\n\t * If not specified, the result of getDefaultEnvironment() will be used.\n\t */\n\tenv: Record<string, string>;\n\n\t/**\n\t * A function to call when an event occurs.\n\t */\n\tonEvent?: (event: TransportEvent) => void;\n\n\t/**\n\t * When true, spawn the child process using the user's shell.\n\t */\n\tshell?: boolean;\n\n\t/**\n\t * How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`.\n\t *\n\t * The default is \"inherit\", meaning messages to stderr will be printed to the parent process's stderr.\n\t */\n\tstderr?: IOType | number | Stream;\n};\n\ntype TransportEvent =\n\t| {\n\t\t\tchunk: string;\n\t\t\ttype: \"data\";\n\t }\n\t| {\n\t\t\terror: Error;\n\t\t\ttype: \"error\";\n\t }\n\t| {\n\t\t\tmessage: JSONRPCMessage;\n\t\t\ttype: \"message\";\n\t }\n\t| {\n\t\t\ttype: \"close\";\n\t };\n\n/**\n * Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout.\n *\n * This transport is only available in Node.js environments.\n */\nexport class StdioClientTransport implements Transport {\n\tonclose?: () => void;\n\n\tonerror?: (error: Error) => void;\n\tonmessage?: (message: JSONRPCMessage) => void;\n\t/**\n\t * The child process pid spawned by this transport.\n\t *\n\t * This is only available after the transport has been started.\n\t */\n\tget pid(): null | number {\n\t\treturn this._process?.pid ?? null;\n\t}\n\t/**\n\t * PID of the MCP server child captured at \"spawn\" time. Unlike `pid`, this\n\t * survives close() nulling `this._process`, so session-metadata assembly can\n\t * report the child even after it exits. Undefined until spawn. On win32 with\n\t * shell:true this is the cmd wrapper the server runs under — still the direct\n\t * child the OS process tree shows between the gateway and the server.\n\t */\n\tget spawnedServerPid(): number | undefined {\n\t\treturn this._spawnedPid;\n\t}\n\t/**\n\t * Epoch-ms timestamp captured at the child's \"spawn\" event. Undefined until\n\t * spawn. Callers convert to ISO-8601 for the wire.\n\t */\n\tget spawnedServerStartedAt(): number | undefined {\n\t\treturn this._spawnedAt;\n\t}\n\t/**\n\t * The stderr stream of the child process, if `StdioServerParameters.stderr` was set to \"pipe\" or \"overlapped\".\n\t *\n\t * If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to\n\t * attach listeners before the start method is invoked. This prevents loss of any early\n\t * error output emitted by the child process.\n\t */\n\tget stderr(): null | Stream {\n\t\tif (this._stderrStream) {\n\t\t\treturn this._stderrStream;\n\t\t}\n\n\t\treturn this._process?.stderr ?? null;\n\t}\n\tprivate _abortController: AbortController = new AbortController();\n\tprivate _process?: ChildProcess;\n\tprivate _readBuffer: ReadBuffer = new ReadBuffer();\n\tprivate _serverParams: StdioServerParameters;\n\tprivate _spawnedAt?: number;\n\tprivate _spawnedPid?: number;\n\tprivate _stderrStream: null | PassThrough = null;\n\n\tprivate onEvent?: (event: TransportEvent) => void;\n\n\tconstructor(server: StdioServerParameters) {\n\t\tthis._serverParams = server;\n\t\tif (server.stderr === \"pipe\" || server.stderr === \"overlapped\") {\n\t\t\tthis._stderrStream = new PassThrough();\n\t\t}\n\t\tthis.onEvent = server.onEvent;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.onEvent?.({\n\t\t\ttype: \"close\",\n\t\t});\n\n\t\t// Kill the entire process tree to avoid orphaned grandchildren.\n\t\t// AbortController only kills the direct child, but MCP servers\n\t\t// may spawn subprocesses (workers, file watchers, etc.) that\n\t\t// would otherwise be left running as orphans.\n\t\tif (this._process?.pid) {\n\t\t\ttry {\n\t\t\t\tif (process.platform === \"win32\") {\n\t\t\t\t\t// Windows doesn't support negative-PID process group kills.\n\t\t\t\t\t// Use taskkill /T to terminate the entire process tree.\n\t\t\t\t\tconst result = spawnSync(\"taskkill\", [\"/PID\", String(this._process.pid), \"/T\", \"/F\"], {\n\t\t\t\t\t\tstdio: \"ignore\",\n\t\t\t\t\t});\n\t\t\t\t\tif (result.error) {\n\t\t\t\t\t\tthrow result.error;\n\t\t\t\t\t}\n\t\t\t\t\tif (result.status !== 0) {\n\t\t\t\t\t\tthrow new Error(`taskkill exited with status ${result.status}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.kill(-this._process.pid, \"SIGTERM\");\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tlogger.debug(\"Process tree cleanup failed (process may already be dead)\", {\n\t\t\t\t\terror: (error as Error).message,\n\t\t\t\t\tpid: this._process.pid,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tthis._abortController.abort();\n\t\tthis._process = undefined;\n\t\tthis._readBuffer.clear();\n\t}\n\n\tsend(message: JSONRPCMessage): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (!this._process?.stdin) {\n\t\t\t\tthrow new Error(\"Not connected\");\n\t\t\t}\n\n\t\t\tconst json = serializeMessage(message);\n\t\t\tif (this._process.stdin.write(json)) {\n\t\t\t\tresolve();\n\t\t\t} else {\n\t\t\t\tthis._process.stdin.once(\"drain\", resolve);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Starts the server process and prepares to communicate with it.\n\t */\n\tasync start(): Promise<void> {\n\t\tif (this._process) {\n\t\t\tthrow new Error(\n\t\t\t\t\"StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.\",\n\t\t\t);\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst useShell = this._serverParams.shell ?? false;\n\t\t\t// Node's spawn with shell:true on Windows runs\n\t\t\t// `cmd.exe /d /s /c \"<command> <arg1> <arg2>...\"` by joining the\n\t\t\t// command + args array with single spaces, with no per-token\n\t\t\t// escaping. Any segment containing whitespace (e.g. a path like\n\t\t\t// `C:\\Users\\Foo\\OneDrive - Acme Corp\\bin\\python.exe`) is then\n\t\t\t// re-split by cmd.exe at every space — the child sees a\n\t\t\t// truncated command (`C:\\Users\\Foo\\OneDrive`) plus a few stray\n\t\t\t// tokens and dies with \"not recognized as an internal or\n\t\t\t// external command\" (or, for `python <prefix> ...`, Python\n\t\t\t// follows the first arg as a script directory and reports\n\t\t\t// `can't find '__main__' module in '...'`). Pre-quote each\n\t\t\t// segment containing whitespace or cmd.exe metacharacters so\n\t\t\t// the joined command line round-trips through the parser.\n\t\t\tconst shouldQuote = useShell && process.platform === \"win32\";\n\t\t\tconst spawnCommand = shouldQuote\n\t\t\t\t? quoteForWindowsShell(this._serverParams.command)\n\t\t\t\t: this._serverParams.command;\n\t\t\tconst spawnArgs = shouldQuote\n\t\t\t\t? (this._serverParams.args ?? []).map(quoteForWindowsShell)\n\t\t\t\t: this._serverParams.args ?? [];\n\n\t\t\tthis._process = spawn(\n\t\t\t\tspawnCommand,\n\t\t\t\tspawnArgs,\n\t\t\t\t{\n\t\t\t\t\tcwd: this._serverParams.cwd,\n\t\t\t\t\t// On POSIX, detached creates a new process group (setsid) so we\n\t\t\t\t\t// can kill the entire tree with kill(-pid). On Windows, detached\n\t\t\t\t\t// breaks the child out of the parent's job object, which would\n\t\t\t\t\t// prevent automatic cleanup if the parent crashes — so we skip it.\n\t\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\t\tenv: this._serverParams.env,\n\t\t\t\t\tshell: useShell,\n\t\t\t\t\tsignal: this._abortController.signal,\n\t\t\t\t\tstdio: [\"pipe\", \"pipe\", this._serverParams.stderr ?? \"inherit\"],\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tthis._process.on(\"error\", (error) => {\n\t\t\t\tif (error.name === \"AbortError\") {\n\t\t\t\t\t// Expected when close() is called.\n\t\t\t\t\tthis.onclose?.();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlogger.error(\"Process spawn error\", {\n\t\t\t\t\targs: this._serverParams.args ? redactCliArgs(this._serverParams.args) : undefined,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\tcwd: this._serverParams.cwd,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\terrorCode: (error as NodeJS.ErrnoException).code,\n\t\t\t\t\terrorErrno: (error as NodeJS.ErrnoException).errno,\n\t\t\t\t\terrorSyscall: (error as NodeJS.ErrnoException).syscall,\n\t\t\t\t\toperation: \"spawn\",\n\t\t\t\t\tshell: this._serverParams.shell,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\treject(error);\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\t// Capture pid + spawn time at \"spawn\" time so they remain available in\n\t\t\t// the \"close\" handler. close() nulls `this._process` synchronously\n\t\t\t// before the OS fires the close event, so reading `this._process?.pid`\n\t\t\t// from the close handler would always return undefined on that path.\n\t\t\tlet spawnedAt: number | undefined;\n\t\t\tlet spawnedPid: number | undefined;\n\t\t\tthis._process.on(\"spawn\", () => {\n\t\t\t\tspawnedAt = Date.now();\n\t\t\t\tspawnedPid = this._process?.pid;\n\t\t\t\t// Mirror onto the instance so session-metadata assembly can read the\n\t\t\t\t// spawned child via the public getters.\n\t\t\t\tthis._spawnedAt = spawnedAt;\n\t\t\t\tthis._spawnedPid = spawnedPid;\n\t\t\t\t// Log argCount, not args — MCP servers commonly take secrets as\n\t\t\t\t// CLI args (--api-key, --token, etc.) and these logs land in the\n\t\t\t\t// file transport and remote ingest endpoint.\n\t\t\t\tlogger.info(\"MCP server process spawned\", {\n\t\t\t\t\targCount: this._serverParams.args?.length,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\teventType: \"mcp_server_process_spawned\",\n\t\t\t\t\tpid: spawnedPid,\n\t\t\t\t});\n\t\t\t\tresolve();\n\t\t\t});\n\n\t\t\tthis._process.on(\"close\", (code, signal) => {\n\t\t\t\tconst uptimeMs = spawnedAt === undefined ? undefined : Date.now() - spawnedAt;\n\t\t\t\tconst cleanExit = code === 0 && signal === null;\n\t\t\t\tconst logLevel = cleanExit ? \"info\" : \"warn\";\n\t\t\t\tlogger[logLevel](\"MCP server process closed\", {\n\t\t\t\t\targCount: this._serverParams.args?.length,\n\t\t\t\t\tcode,\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\teventType: \"mcp_server_process_closed\",\n\t\t\t\t\tpid: spawnedPid,\n\t\t\t\t\tsignal,\n\t\t\t\t\tuptimeMs,\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\ttype: \"close\",\n\t\t\t\t});\n\n\t\t\t\tthis._process = undefined;\n\t\t\t\tthis.onclose?.();\n\t\t\t});\n\n\t\t\tthis._process.stdin?.on(\"error\", (error) => {\n\t\t\t\tlogger.error(\"Stdin error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\toperation: \"write\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\tconst jsonFilterTransform = new JSONFilterTransform();\n\n\t\t\tthis._process.stdout?.pipe(jsonFilterTransform);\n\n\t\t\tjsonFilterTransform.on(\"data\", (chunk) => {\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\tchunk: chunk.toString(),\n\t\t\t\t\ttype: \"data\",\n\t\t\t\t});\n\n\t\t\t\tthis._readBuffer.append(chunk);\n\t\t\t\tthis.processReadBuffer();\n\t\t\t});\n\n\t\t\tjsonFilterTransform.on(\"error\", (error) => {\n\t\t\t\tlogger.error(\"JSON filter error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: error.message,\n\t\t\t\t\toperation: \"parse\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error);\n\t\t\t});\n\n\t\t\tif (this._stderrStream && this._process.stderr) {\n\t\t\t\tthis._process.stderr.pipe(this._stderrStream);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate processReadBuffer() {\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\tconst message = this._readBuffer.readMessage();\n\n\t\t\t\tif (message === null) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\tmessage,\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t});\n\n\t\t\t\tthis.onmessage?.(message);\n\t\t\t} catch (error) {\n\t\t\t\t// Try to read message for context (may fail)\n\t\t\t\tlet messageType: string = \"unknown\";\n\t\t\t\ttry {\n\t\t\t\t\tconst msg = this._readBuffer.readMessage();\n\t\t\t\t\tif (msg) {\n\t\t\t\t\t\tmessageType = typeof msg;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore error when trying to read message for context\n\t\t\t\t}\n\n\t\t\t\tlogger.error(\"Message processing error\", {\n\t\t\t\t\tcommand: this._serverParams.command,\n\t\t\t\t\terror: (error as Error).message,\n\t\t\t\t\tmessageType,\n\t\t\t\t\toperation: \"process\",\n\t\t\t\t\tpid: this._process?.pid,\n\t\t\t\t\ttransportType: \"stdio\",\n\t\t\t\t});\n\n\t\t\t\tthis.onEvent?.({\n\t\t\t\t\terror: error as Error,\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t});\n\n\t\t\t\tthis.onerror?.(error as Error);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Quote a token for Node's `spawn(..., { shell: true })` on Windows.\n *\n * Returns `s` unchanged when no quoting is needed — preserves the pre-fix\n * shape of the command line for common cases (`npx`, `python`, `node`, etc.),\n * so existing installs aren't re-quoted unnecessarily. Wraps in `\"...\"` and\n * doubles embedded quotes (cmd.exe convention inside a quoted token) when\n * the token contains whitespace or any cmd.exe metacharacter.\n *\n * Exported for the unit test; production code reaches this through\n * `StdioClientTransport.start()`.\n */\nexport function quoteForWindowsShell(s: string): string {\n\tif (s.length === 0) return s;\n\tif (!/[\\s\"&|<>^()]/.test(s)) return s;\n\t// Double embedded quotes (cmd.exe convention inside a quoted token), then\n\t// double any run of trailing backslashes. Under the Windows/MSVC argv rules\n\t// (CommandLineToArgvW), backslashes are only special immediately before a\n\t// quote: an odd count escapes the closing `\"`, so a token ending in `\\` like\n\t// `C:\\Path\\` would otherwise corrupt argv. Doubling the trailing run makes the\n\t// count even, preserving the backslash and terminating the quote correctly\n\t// (e.g. `C:\\Path\\` -> `\"C:\\Path\\\\\"`).\n\tconst escaped = s.replace(/\"/g, '\"\"').replace(/(\\\\+)$/, \"$1$1\");\n\treturn `\"${escaped}\"`;\n}\n","/**\n * OCI image-reference validation for container MCP images.\n *\n * DEFENSE IN DEPTH — not the primary control. The primary control is that the\n * digest lookup passes the image reference as a single argv element to the\n * container runtime (`execFileSync`, no shell — see\n * {@link ./dockerImageDigest.ts}), so no metacharacter can be interpreted by a\n * shell in the first place. This validator additionally refuses to hand a\n * reference that is not a well-formed OCI image reference to the runtime at all,\n * so a malformed/hostile value is rejected and logged before any local process\n * is spawned.\n *\n * The grammar mirrors the OCI distribution spec (the same one\n * `github.com/distribution/reference` implements):\n *\n * reference := name [ \":\" tag ] [ \"@\" digest ]\n * name := [domain \"/\"] path-component [\"/\" path-component]*\n * domain := host [\":\" port]\n * host := domain-name | IPv4 | \"[\" IPv6 \"]\"\n * path-component := [a-z0-9]+ (separator [a-z0-9]+)*\n * separator := \"_\" | \"__\" | \".\" | \"-\"+\n * tag := [A-Za-z0-9_][A-Za-z0-9_.-]{0,127}\n * digest := algorithm \":\" hex\n */\n\n/** The outcome of validating an image reference. `reason` is set iff invalid. */\nexport interface ImageReferenceCheck {\n /** Bounded, log-safe label naming why the reference was rejected. */\n reason?: string;\n valid: boolean;\n}\n\n/**\n * Longest reference we accept. The OCI spec caps a repository name at 255\n * characters; 512 leaves room for a registry host, a tag and a digest while\n * still bounding what reaches the runtime (and the log line).\n */\nconst MAX_REFERENCE_LENGTH = 512;\n\n/**\n * Characters that are meaningful to a POSIX or Windows shell. Used ONLY to\n * describe why a reference the grammar already rejected is dangerous, so the log\n * line names the metacharacter instead of a generic grammar failure.\n */\nconst SHELL_METACHARACTERS = [\n \"$\",\n \"`\",\n \";\",\n \"|\",\n \"&\",\n \">\",\n \"<\",\n \"(\",\n \")\",\n \"{\",\n \"}\",\n \"'\",\n '\"',\n \"\\\\\",\n \"!\",\n \"*\",\n \"?\",\n \"~\",\n \"#\",\n] as const;\n\nconst ALPHA_NUMERIC = \"[a-z0-9]+\";\nconst SEPARATOR = \"(?:[._]|__|-+)\";\nconst PATH_COMPONENT = `${ALPHA_NUMERIC}(?:${SEPARATOR}${ALPHA_NUMERIC})*`;\nconst DOMAIN_COMPONENT = \"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\";\nconst DOMAIN_NAME = `${DOMAIN_COMPONENT}(?:\\\\.${DOMAIN_COMPONENT})*`;\nconst IPV6_HOST = \"\\\\[[a-fA-F0-9:]+\\\\]\";\nconst DOMAIN = `(?:${DOMAIN_NAME}|${IPV6_HOST})(?::[0-9]+)?`;\nconst NAME = `(?:${DOMAIN}/)?${PATH_COMPONENT}(?:/${PATH_COMPONENT})*`;\nconst TAG = \"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}\";\nconst DIGEST = \"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[0-9a-fA-F]{32,}\";\n\nconst IMAGE_REFERENCE = new RegExp(`^${NAME}(?::${TAG})?(?:@${DIGEST})?$`);\n\n/**\n * Validate a container image reference against the OCI image-reference grammar.\n *\n * @param imageName - The raw, untrusted image reference from the MCP server config\n * @returns `{ valid: true }`, or `{ valid: false, reason }` naming the defect\n */\nexport function validateImageReference(imageName: string): ImageReferenceCheck {\n if (typeof imageName !== \"string\" || imageName.trim() === \"\") {\n return { reason: \"empty image reference\", valid: false };\n }\n\n if (imageName.length > MAX_REFERENCE_LENGTH) {\n return { reason: `image reference longer than ${MAX_REFERENCE_LENGTH} characters`, valid: false };\n }\n\n if (!IMAGE_REFERENCE.test(imageName)) {\n return { reason: describeDefect(imageName), valid: false };\n }\n\n return { valid: true };\n}\n\n/**\n * Describe why a reference that FAILED the grammar is malformed. Diagnostic\n * only — the grammar above is the single authority on validity, so this can\n * never widen what is accepted.\n *\n * @param imageName - A reference already known to be invalid\n * @returns A bounded, log-safe reason that never embeds the reference itself\n */\nfunction describeDefect(imageName: string): string {\n // eslint-disable-next-line no-control-regex -- deliberately matching C0 control characters\n if (/[\\u0000-\\u001f\\u007f]/.test(imageName)) {\n return \"contains a control character\";\n }\n\n if (/\\s/.test(imageName)) {\n return \"contains whitespace\";\n }\n\n const metacharacter = SHELL_METACHARACTERS.find((character) => imageName.includes(character));\n if (metacharacter) {\n return `contains the shell metacharacter ${metacharacter}`;\n }\n\n // An argv element starting with \"-\" is read as an option by the runtime CLI,\n // never as an image name.\n if (imageName.startsWith(\"-\")) {\n return \"starts with '-' and would be parsed as a runtime option\";\n }\n\n return \"does not match the OCI image-reference grammar\";\n}\n","import { execFileSync } from \"node:child_process\";\n\nimport { logger } from \"../lib/logger.js\";\nimport { validateImageReference } from \"./imageReference.js\";\n\n/**\n * A container-based MCP command, as parsed from the CLI arguments.\n *\n * Deliberately carries NO image digests: resolving those invokes the container\n * runtime, which the gateway must not do until the access-control decision has\n * allowed the server. Detection is pure string parsing and safe to run at startup;\n * resolution is {@link getContainerImageDigests}, called after the gate. The Go\n * twin (scanner/internal/container/container.go) keeps them in one struct because\n * the scanner has no access-control gate to run first.\n */\nexport interface ContainerMCPCommand {\n /**\n * The image name/tag used in the command. Untrusted: it comes verbatim from\n * the MCP server config and is validated before it reaches the runtime.\n */\n imageName: string;\n /**\n * Whether the command is a container MCP (docker, podman, etc.)\n */\n isContainerMCP: boolean;\n /**\n * The detected container runtime (docker, podman, nerdctl, finch, etc.)\n */\n runtime: ContainerRuntime;\n}\n\n/**\n * Supported container runtimes\n */\nexport type ContainerRuntime = \"docker\" | \"finch\" | \"nerdctl\" | \"podman\" | \"unknown\";\n\n/**\n * Detect the container runtime from a command string.\n *\n * Supports:\n * - docker / docker.exe\n * - podman / podman.exe\n * - nerdctl / nerdctl.exe\n * - finch / finch.exe\n *\n * @param command - The executable command\n * @returns The detected container runtime or \"unknown\"\n */\nexport function detectContainerRuntime(command: string): ContainerRuntime {\n // Normalize command to lowercase for comparison\n const normalizedCommand = command.toLowerCase();\n\n // Get the base command name (without path)\n const baseName = normalizedCommand.split(/[/\\\\]/).pop() || \"\";\n\n // Check for known container runtimes\n if (baseName === \"docker\" || baseName === \"docker.exe\") {\n return \"docker\";\n }\n if (baseName === \"podman\" || baseName === \"podman.exe\") {\n return \"podman\";\n }\n if (baseName === \"nerdctl\" || baseName === \"nerdctl.exe\") {\n return \"nerdctl\";\n }\n if (baseName === \"finch\" || baseName === \"finch.exe\") {\n return \"finch\";\n }\n\n return \"unknown\";\n}\n\n/**\n * Extract the image name from container run arguments.\n *\n * Docker/Podman/nerdctl run syntax: <runtime> run [OPTIONS] IMAGE [COMMAND] [ARG...]\n *\n * The image is the first argument after all options (which start with - or --).\n * Some options take values: -e, --env, -v, --volume, -p, --publish, etc.\n *\n * @param args - The run arguments (after \"<runtime> run\")\n * @returns The image name or undefined if not found\n */\nexport function extractContainerImageName(args: string[]): string | undefined {\n // Find the index of \"run\" in args\n const runIndex = args.indexOf(\"run\");\n if (runIndex === -1) {\n return undefined;\n }\n\n // Args after \"run\"\n const runArgs = args.slice(runIndex + 1);\n\n // Options that take a value (the next arg is not the image)\n // These are common across Docker, Podman, nerdctl, and finch\n // Sorted alphabetically for linter\n const optionsWithValue = new Set([\n \"--add-host\",\n \"--annotation\",\n \"--arch\",\n \"--attach\",\n \"--blkio-weight\",\n \"--blkio-weight-device\",\n \"--cap-add\",\n \"--cap-drop\",\n \"--cgroup-parent\",\n \"--cgroupns\",\n \"--cidfile\",\n \"--cosign-key\",\n \"--cpu-period\",\n \"--cpu-quota\",\n \"--cpu-rt-period\",\n \"--cpu-rt-runtime\",\n \"--cpu-shares\",\n \"--cpus\",\n \"--cpuset-cpus\",\n \"--cpuset-mems\",\n \"--decryption-key\",\n \"--device\",\n \"--device-cgroup-rule\",\n \"--device-read-bps\",\n \"--device-read-iops\",\n \"--device-write-bps\",\n \"--device-write-iops\",\n \"--dns\",\n \"--dns-option\",\n \"--dns-search\",\n \"--domainname\",\n \"--entrypoint\",\n \"--env\",\n \"--env-file\",\n \"--env-host\",\n \"--expose\",\n \"--gidmap\",\n \"--gpus\",\n \"--group-add\",\n \"--health-cmd\",\n \"--health-interval\",\n \"--health-retries\",\n \"--health-start-period\",\n \"--health-timeout\",\n \"--hostname\",\n \"--image-volume\",\n \"--ip\",\n \"--ip6\",\n \"--ipc\",\n \"--isolation\",\n \"--kernel-memory\",\n \"--label\",\n \"--label-file\",\n \"--link\",\n \"--link-local-ip\",\n \"--log-driver\",\n \"--log-opt\",\n \"--mac-address\",\n \"--memory\",\n \"--memory-reservation\",\n \"--memory-swap\",\n \"--memory-swappiness\",\n \"--mount\",\n \"--name\",\n \"--network\",\n \"--network-alias\",\n \"--oom-kill-disable\",\n \"--oom-score-adj\",\n \"--os\",\n \"--pid\",\n \"--pidfile\",\n \"--pids-limit\",\n \"--platform\",\n \"--pod\",\n \"--privileged\",\n \"--publish\",\n \"--publish-all\",\n \"--pull\",\n \"--requires\",\n \"--restart\",\n \"--rootfs\",\n \"--runtime\",\n \"--secret\",\n \"--security-opt\",\n \"--shm-size\",\n \"--stop-signal\",\n \"--stop-timeout\",\n \"--storage-opt\",\n \"--subgidname\",\n \"--subuidname\",\n \"--sysctl\",\n \"--systemd\",\n \"--timeout\",\n \"--tmpfs\",\n \"--tz\",\n \"--uidmap\",\n \"--ulimit\",\n \"--user\",\n \"--userns\",\n \"--uts\",\n \"--variant\",\n \"--volume\",\n \"--volume-driver\",\n \"--volumes-from\",\n \"--workdir\",\n \"-a\",\n \"-c\",\n \"-e\",\n \"-h\",\n \"-l\",\n \"-m\",\n \"-p\",\n \"-P\",\n \"-u\",\n \"-v\",\n \"-w\",\n ]);\n\n let i = 0;\n while (i < runArgs.length) {\n const arg = runArgs[i];\n\n // Skip options\n if (arg.startsWith(\"-\")) {\n // Check if this option takes a value\n // Handle --option=value format\n if (arg.includes(\"=\")) {\n i++;\n continue;\n }\n\n // Check if the option (without leading dashes) is in our set\n if (optionsWithValue.has(arg)) {\n // Skip this option and its value\n i += 2;\n continue;\n }\n\n // Boolean option, just skip it\n i++;\n continue;\n }\n\n // This is the image name\n return arg;\n }\n\n return undefined;\n}\n\n/** Bound on how long the runtime's `image inspect` may take. */\nconst DIGEST_LOOKUP_TIMEOUT_MS = 5000;\n\n/**\n * Detect whether a command runs a container (Docker, Podman, nerdctl, finch) and\n * extract the image reference from its arguments.\n *\n * PURE string parsing: it spawns no process and touches no shell, so it is safe\n * to run before the access-control decision. Resolving the digests — the step\n * that invokes the container runtime — is {@link getContainerImageDigests}, which\n * runs only after the server has been allowed.\n *\n * @param command - The executable command\n * @param args - Command arguments\n * @returns The detected runtime and (untrusted) image reference\n */\nexport function detectContainerMCP(command: string, args?: string[]): ContainerMCPCommand {\n const runtime = detectContainerRuntime(command);\n\n if (runtime === \"unknown\") {\n return {\n imageName: \"\",\n isContainerMCP: false,\n runtime: \"unknown\",\n };\n }\n\n // Check if there's a \"run\" subcommand in the args\n if (!args || !args.includes(\"run\")) {\n return {\n imageName: \"\",\n isContainerMCP: false,\n runtime,\n };\n }\n\n const imageName = extractContainerImageName(args);\n\n if (!imageName) {\n logger.debug(\"Container command detected but could not extract image name\", {\n args,\n command,\n runtime,\n });\n return {\n imageName: \"\",\n isContainerMCP: true,\n runtime,\n };\n }\n\n return {\n imageName,\n isContainerMCP: true,\n runtime,\n };\n}\n\n/**\n * Get all container image digests using the appropriate runtime's inspect command.\n *\n * The digests are sha256 hashes of the image manifest, which are consistent across\n * registries and uniquely identify specific image versions.\n * An image may have multiple digests if it exists in multiple registries.\n *\n * SECURITY: `imageName` is attacker-controllable — it comes from a (possibly\n * source-controlled) MCP server config. It is therefore\n * 1. validated against the OCI image-reference grammar and skipped-with-a-log if\n * it does not match, and\n * 2. passed to the runtime as a single argv element via `execFileSync` — NEVER\n * interpolated into a shell command string. `execSync` always spawns\n * `/bin/sh -c`, where `$(...)` and backticks stay live inside double quotes, so\n * a crafted reference could execute arbitrary commands as the local user.\n * This mirrors the Go twin's argv call (scanner/internal/container/container.go).\n * This function MUST only be called after the access-control decision (main.ts):\n * it runs a local binary on a value the MCP config controls, so a server the\n * policy denies must never reach it.\n *\n * @param imageName - The untrusted image name/tag to inspect\n * @param runtime - The container runtime to use\n * @returns Array of image digests (sha256 hashes), empty if rejected, not found or locally built\n */\nexport function getContainerImageDigests(imageName: string, runtime: ContainerRuntime): string[] {\n // All supported runtimes use the same inspect syntax\n const runtimeCommand = runtime === \"unknown\" ? \"docker\" : runtime;\n\n const check = validateImageReference(imageName);\n if (!check.valid) {\n // A deliberate skip is an outcome someone has to be able to see: log it at warn\n // with the offending reference. The session/machine ids and the trace id ride on\n // every line via the logger's session context.\n logger.warn(\"Rejected container image reference; skipping digest lookup\", {\n eventType: \"mcp_container_image_ref_rejected\",\n imageName,\n reason: check.reason,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n let result: string;\n try {\n // Get RepoDigests as JSON array - this contains the manifest digests\n // Format: [\"repo@sha256:abc123...\", \"other-repo@sha256:def456...\"] or [] if image was built locally\n result = execFileSync(\n runtimeCommand,\n [\"image\", \"inspect\", \"--format\", \"{{json .RepoDigests}}\", imageName],\n {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: DIGEST_LOOKUP_TIMEOUT_MS,\n }\n );\n } catch (error) {\n // Image not found locally, or runtime not available\n logger.warn(\"Container image digest lookup failed\", {\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n const trimmedResult = result.trim();\n\n // Parse the JSON array of digests. The lookup must stay TOTAL: whatever the\n // runtime prints, it degrades to \"no digests\" and never throws — a digest is\n // metadata, so a surprising `image inspect` output must not take the MCP server\n // down with it. Note a Go nil slice marshals to `null`, not `[]`, and\n // `--format '{{json …}}'` does print `null` for a nil field (docker 29.6.2 on\n // `.Config.Entrypoint`); whether RepoDigests is nil or empty is a daemon\n // implementation detail, so `null` is treated as \"none\", not iterated.\n let parsedResult: unknown;\n try {\n parsedResult = JSON.parse(trimmedResult);\n } catch {\n logger.warn(\"Could not parse RepoDigests JSON\", {\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n result: trimmedResult,\n runtime: runtimeCommand,\n });\n return [];\n }\n\n if (!Array.isArray(parsedResult)) {\n logger.warn(\"RepoDigests was not a JSON array\", {\n eventType: \"mcp_container_image_digest_lookup_failed\",\n imageName,\n result: trimmedResult,\n runtime: runtimeCommand,\n });\n return [];\n }\n const repoDigests: unknown[] = parsedResult;\n\n // Extract sha256 digests from all entries\n // Format is typically: \"repo@sha256:abc123...\" or just \"sha256:...\"\n const digests: string[] = [];\n for (const repoDigest of repoDigests) {\n if (typeof repoDigest !== \"string\") {\n continue;\n }\n // Extract just the sha256:... portion\n const digestMatch = repoDigest.match(/sha256:[a-f0-9]{64}/i);\n if (digestMatch) {\n digests.push(digestMatch[0]);\n }\n }\n\n if (digests.length > 0) {\n logger.info(\"Container image digests resolved\", {\n digests,\n eventType: \"mcp_container_image_digests_resolved\",\n imageName,\n runtime: runtimeCommand,\n });\n return digests;\n }\n\n // No digest available (likely a locally built image that was never pushed)\n logger.info(\"No container image digests available (image may be locally built)\", {\n eventType: \"mcp_container_image_digests_empty\",\n imageName,\n runtime: runtimeCommand,\n });\n return [];\n}\n","import { StringDecoder } from \"node:string_decoder\";\nimport { clearInterval, setInterval, setTimeout } from \"node:timers\";\nimport util from \"node:util\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { type ServerCapabilities } from \"@modelcontextprotocol/sdk/types.js\";\nimport { EventSource } from \"eventsource\";\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\n\nimport { AccessControlAuthorizer } from \"../accessControl/authorizer.js\";\nimport { AccessControlClient } from \"../accessControl/client.js\";\nimport { OAuthCredentialStore, performOAuthFlow } from \"../auth/index.js\";\nimport { discoverAuthServer } from \"../auth/discovery.js\";\nimport { createAuthenticatingFetch, createDynamicAuth, type DynamicAuthHandle } from \"../auth/dynamicAuth.js\";\nimport { initializeOAuthProviders } from \"../auth/oauthProvidersClient.js\";\nimport { config } from \"../config/env.js\";\nimport { AccessControlBlockError, ConfigurationError, ProxyConnectionError } from \"../lib/errors.js\";\nimport { respondInitializeError } from \"../lib/initializeErrorResponder.js\";\nimport { emitKeychainOutcomeLog } from \"../lib/keychainOutcome.js\";\nimport { extractTraceIdFromHeaders, flushLogs, logger } from \"../lib/logger.js\";\nimport { emitProxySetupLog } from \"../lib/proxySetup.js\";\nimport { initializeTrafficMirror } from \"../lib/trafficMirror.js\";\n\n// The live stdio MCP-server transport, registered at creation so the traffic\n// mirror's per-event metadata can report the spawned child's pid + start time.\n// Reconnects re-register, so a respawned server's new pid is picked up on the\n// next scan. Undefined for remote proxies (no spawned child).\nlet activeStdioTransport: StdioClientTransport | undefined;\n\nconst spawnedServerInfoProvider = () => ({\n pid: activeStdioTransport?.spawnedServerPid,\n startedAtMs: activeStdioTransport?.spawnedServerStartedAt,\n});\nimport { connectWithRetry } from \"../lib/connectWithRetry.js\";\nimport { createReconnectingClient, type UpstreamClient } from \"../lib/reconnectingClient.js\";\nimport { connectWithTransportFallback, type TransportType } from \"../lib/transportDetection.js\";\nimport { getUpstreamErrno, is401Error } from \"../lib/upstreamErrors.js\";\nimport { logRequestCounts, proxyServer } from \"../proxyServer.js\";\nimport { StdioClientTransport } from \"../StdioClientTransport.js\";\nimport { startHTTPServer } from \"../startHTTPServer.js\";\nimport { type ContainerMCPCommand, detectContainerMCP, getContainerImageDigests } from \"../utils/dockerImageDigest.js\";\nimport { getSessionData, type RemoteSessionCliArgs, type StdioSessionCliArgs } from \"../utils/getUserData.js\";\nimport { normalizeUrl } from \"../utils/normalizeUrl.js\";\nimport { redactCliArgs } from \"../utils/redactCliArgs.js\";\n\nutil.inspect.defaultOptions.depth = 8;\n\nif (!(\"EventSource\" in global)) {\n // @ts-expect-error - figure out how to use --experimental-eventsource with vitest\n global.EventSource = EventSource;\n}\n\n// Export ProxyType for use by other modules\nexport type ProxyType = \"remote\" | \"stdio\";\n\n// sysexits EX_CONFIG (78): a required auth env var resolved empty/unset. Distinct\n// from the generic exit 1 so wrappers and parent agents can tell a configuration\n// error apart from a crash.\nconst EXIT_AUTH_CONFIG = 78;\n\n// PRDCT-6638: make a startup failure visible to ALL three audiences instead of\n// the silent process.exit() that the parent agent only sees as\n// \"-32000: Connection closed\":\n// - the parent AGENT (OpenCode/Cursor/Claude Code) — reply to its `initialize`\n// with a JSON-RPC error whose message is `reason`, so it shows the specific\n// cause instead of -32000 (see lib/initializeErrorResponder). Gated on\n// `respondToClient`, which the caller sets only in stdio mode with the real\n// Server transport NOT yet connected, so we never fight it for stdin;\n// - GROUNDCOVER (+ the local file) — logger.error then flushLogs() BEFORE exit,\n// because process.exit otherwise drops the async transports;\n// - a developer running the gateway directly — an actionable stderr line.\n// keepAlive: flushLogs' timers are .unref()'d and there may be no other pending\n// I/O at this pre-connect exit, so without a ref'd timer Node would drain and\n// exit 13 (unsettled top-level await) before the flush completes. A flush failure\n// must not bypass the intended exit code.\nconst failStartupVisibly = async (opts: {\n exitCode: number;\n logMessage: string;\n meta: Record<string, unknown>;\n reason: string;\n respondToClient: boolean;\n stderrLine: string;\n}): Promise<never> => {\n logger.error(opts.logMessage, opts.meta);\n // Await the stderr write so it drains BEFORE process.exit: writes to a pipe/TTY\n // are async on POSIX, so an un-awaited write can be truncated on exit.\n await new Promise<void>((resolve) => {\n process.stderr.write(`${opts.stderrLine}\\n`, () => resolve());\n });\n if (opts.respondToClient) {\n await respondInitializeError(opts.reason);\n }\n const keepAlive = setInterval(() => {}, 50);\n try {\n await flushLogs(2000);\n } catch {\n // still fail loud with opts.exitCode rather than a generic unhandled rejection\n } finally {\n clearInterval(keepAlive);\n }\n process.exit(opts.exitCode);\n};\n\n// A synthetic auth env var (the scanner lifts a remote server's header value into\n// MCP_GATEWAY_HEADER_*) resolved empty/unset.\nconst exitAuthEnvMissing = (args: {\n envVar: string;\n eventType: string;\n header?: string;\n logMessage: string;\n respondToClient: boolean;\n}): Promise<never> => {\n const target = args.header ? `HTTP header \"${args.header}\"` : \"the bearer token\";\n const reason =\n `Onyx MCP gateway: required auth env var \"${args.envVar}\" for ${target} is empty or unset. ` +\n `Export the source value your agent config references (e.g. the {env:...} / \\${...} token) in the launch environment.`;\n return failStartupVisibly({\n exitCode: EXIT_AUTH_CONFIG,\n logMessage: args.logMessage,\n meta: { envVar: args.envVar, eventType: args.eventType, ...(args.header ? { header: args.header } : {}) },\n reason,\n respondToClient: args.respondToClient,\n stderrLine: `[mcp-gateway] FATAL: ${reason}`,\n });\n};\n\nexport const main = async () => {\n // Emit the proxy-dispatcher install/failure record captured during phase 0\n // (see lib/proxySetup.ts). Done here, not at the install site, so it goes\n // through the structured logger (file + ingest) instead of stdio.\n emitProxySetupLog(logger);\n\n // Same deal for the startup api-key resolution outcome captured during\n // phase 2b (see lib/keychainOutcome.ts): re-emit it here so the reader-side\n // `keychain_outcome` event reaches groundcover via the ingest transport,\n // instead of dying on the pre-logger stderr write.\n emitKeychainOutcomeLog(logger);\n\n const argv = await yargs(hideBin(process.argv))\n .scriptName(\"mcp-gateway\")\n .command(\"stdio [command] [args...]\", \"Run a local command with stdio transport (default)\", (yargs) => {\n return yargs\n .positional(\"command\", {\n describe: \"The command to run\",\n type: \"string\",\n })\n .positional(\"args\", {\n array: true,\n describe: \"The arguments to pass to the command\",\n type: \"string\",\n });\n })\n .command(\"remote\", \"Connect to a remote MCP server\", (yargs) => {\n return yargs.options({\n \"bearer-token-env\": {\n describe:\n \"Environment variable name containing a bearer token for remote authentication\",\n type: \"string\",\n },\n \"env-http-headers\": {\n describe:\n \"HTTP headers resolved from environment variables (format: HeaderName:ENV_VAR_NAME)\",\n string: true,\n type: \"array\",\n },\n headers: {\n deprecated: \"Use --env-http-headers with env var names instead. --headers exposes values in process listings.\",\n describe: \"Legacy: headers with literal values (format: key:value)\",\n string: true,\n type: \"array\",\n },\n \"remote-transport\": {\n choices: [\"sse\", \"stream\"],\n deprecated: \"Transport is now auto-detected per MCP spec. This option is ignored.\",\n describe: \"(Deprecated) The transport type - now auto-detected\",\n type: \"string\",\n },\n url: {\n demandOption: true,\n describe: \"The URL of the remote MCP server\",\n type: \"string\",\n },\n });\n })\n .command(\"$0 [command] [args...]\", \"Run a command with MCP arguments (backward compatible)\", (yargs) => {\n return yargs\n .positional(\"command\", {\n describe: \"The command to run\",\n type: \"string\",\n })\n .positional(\"args\", {\n array: true,\n describe: \"The arguments to pass to the command\",\n type: \"string\",\n });\n })\n .env(\"MCP_GATEWAY\")\n .parserConfiguration({\n \"populate--\": true,\n \"unknown-options-as-args\": true,\n })\n .options({\n // NEW: Scanner/Gateway configuration CLI args (these override env vars)\n // Note: These are pre-parsed in mcp-gateway.ts and set as env vars before this file loads\n \"access-control-url\": {\n describe: \"URL of the access control service (overrides MCP_GATEWAY_ACCESS_CONTROL_URL)\",\n type: \"string\",\n },\n debug: {\n default: false,\n describe: \"Enable debug logging\",\n type: \"boolean\",\n },\n env: {\n describe: \"Environment variables to pass to the command (format: KEY=value)\",\n string: true,\n type: \"array\",\n },\n gracefulShutdownTimeout: {\n default: 5000,\n describe: \"The timeout (in milliseconds) for graceful shutdown\",\n type: \"number\",\n },\n \"oauth-providers-url\": {\n describe: \"URL of the OAuth providers config endpoint (overrides MCP_GATEWAY_OAUTH_PROVIDERS_URL)\",\n type: \"string\",\n },\n port: {\n describe: \"Run as HTTP server on the specified port instead of stdio\",\n type: \"number\",\n },\n \"scanner-api-key\": {\n describe: \"API key for scanner authentication (overrides MCP_GATEWAY_SCANNER_API_KEY)\",\n type: \"string\",\n },\n \"scanner-fail-open\": {\n describe: \"Allow requests when scanner is unavailable (overrides MCP_GATEWAY_SCANNER_FAIL_OPEN)\",\n type: \"boolean\",\n },\n \"scanner-timeout-ms\": {\n describe: \"Scanner request timeout in milliseconds (overrides MCP_GATEWAY_SCANNER_TIMEOUT_MS)\",\n type: \"number\",\n },\n \"scanner-url\": {\n describe: \"URL of the scanner/evaluator service (overrides MCP_GATEWAY_SCANNER_URL)\",\n type: \"string\",\n },\n shell: {\n default: process.platform === \"win32\",\n describe: \"Spawn the server via the user's shell (defaults to true on Windows)\",\n type: \"boolean\",\n },\n })\n .help()\n .parseAsync();\n\n // Fetch OAuth provider config from the web extension service before any\n // OAuth flow can run. Fail-safe: on error, the in-memory provider list\n // stays empty and OAuth requests fall through to Dynamic Client\n // Registration. See auth/oauthProvidersClient.ts.\n //\n // Placed AFTER yargs parsing so commands like `--help` / `--version` exit\n // immediately without paying the OAuth fetch latency cost.\n await initializeOAuthProviders(config.oauthProviders);\n\n // Determine the proxy type based on the command\n let proxyType: ProxyType;\n // True once the client-facing stdio transport is connected. The startup-failure\n // path checks this so it only answers `initialize` itself BEFORE the real Server\n // owns stdin (never both — they would fight over the same pipe).\n let clientConnected = false;\n let finalCommand: string | undefined;\n let finalArgs: string[] | undefined;\n let remoteUrl: string | undefined;\n // Transport type used for connection (for logging)\n let usedTransport: TransportType | undefined;\n const remoteHeaders: Record<string, string> = {};\n\n // Parse --env KEY=VALUE args for stdio child processes (general-purpose flag).\n const customEnv: Record<string, string> = {};\n if (argv.env) {\n for (const envVar of argv.env as string[]) {\n const [key, ...valueParts] = envVar.split(\"=\");\n if (key && valueParts.length > 0) {\n customEnv[key.trim()] = valueParts.join(\"=\").trim();\n }\n }\n }\n\n // Resolve an env var reference from process.env. Handles plain env var names\n // (used by --env-http-headers and --bearer-token-env) and ${VAR} template\n // patterns (defense-in-depth, not used by current scanner output).\n // Returns the resolved string, or the name of the first missing/empty var.\n const resolveEnvValue = (value: string): { missingVar?: string; resolved: string } => {\n if (value.includes(\"${\")) {\n let missingVar: string | undefined;\n const resolved = value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_, varName: string) => {\n const val = process.env[varName];\n if (!val) {\n missingVar = varName;\n return \"\";\n }\n return val;\n });\n return missingVar ? { missingVar, resolved: \"\" } : { resolved };\n }\n // Plain env var name\n const val = process.env[value];\n if (!val) {\n return { missingVar: value, resolved: \"\" };\n }\n return { resolved: val };\n };\n\n // Legacy: --headers with literal values (old installs). New installs use\n // --env-http-headers with synthetic env var names instead. Passthrough only.\n if (argv.headers) {\n for (const header of argv.headers as string[]) {\n const [key, ...valueParts] = header.split(\":\");\n if (key && valueParts.length > 0) {\n remoteHeaders[key.trim()] = valueParts.join(\":\").trim();\n }\n }\n }\n\n // Resolve bearer token from environment variable if specified\n if (argv.bearerTokenEnv) {\n const envVarName = argv.bearerTokenEnv as string;\n const { missingVar, resolved: token } = resolveEnvValue(envVarName);\n if (missingVar) {\n await exitAuthEnvMissing({\n envVar: missingVar,\n eventType: \"mcp_auth_bearer_env_missing\",\n logMessage: \"Bearer token env var specified but not set\",\n respondToClient: !argv.port,\n });\n }\n remoteHeaders[\"Authorization\"] = `Bearer ${token}`;\n logger.info(\"Bearer token loaded from environment variable\", {\n authType: \"bearer_token\",\n envVar: envVarName,\n eventType: \"mcp_auth_bearer_env\",\n remoteUrl: argv.url as string,\n });\n }\n\n // Resolve env-http-headers: each entry is either:\n // \"HeaderName:ENV_VAR_NAME\" — full value from a single env var\n // \"HeaderName:Bearer ${ENV_VAR_NAME}\" — template with ${VAR} references resolved from env\n if (argv.envHttpHeaders) {\n for (const entry of argv.envHttpHeaders as string[]) {\n const [headerName, ...valueParts] = entry.split(\":\");\n const valueOrTemplate = valueParts.join(\":\").trim();\n if (headerName && valueOrTemplate) {\n const trimmedHeader = headerName.trim();\n const { missingVar, resolved } = resolveEnvValue(valueOrTemplate);\n\n if (missingVar) {\n await exitAuthEnvMissing({\n envVar: missingVar,\n eventType: \"mcp_auth_env_header_missing\",\n header: trimmedHeader,\n logMessage: \"Env var for HTTP header not set\",\n respondToClient: !argv.port,\n });\n }\n\n remoteHeaders[trimmedHeader] = resolved;\n logger.info(\"HTTP header loaded from environment\", {\n eventType: \"mcp_auth_env_header\",\n header: trimmedHeader,\n remoteUrl: argv.url as string,\n });\n }\n }\n }\n\n // Extract and set trace ID from final resolved headers (legacy + env-backed)\n const headerTraceId = extractTraceIdFromHeaders(remoteHeaders);\n if (headerTraceId) {\n logger.setTraceId(headerTraceId);\n }\n\n if (remoteHeaders.Authorization) {\n const authType = remoteHeaders.Authorization.startsWith(\"Bearer \")\n ? \"bearer_token\"\n : remoteHeaders.Authorization.startsWith(\"Basic \")\n ? \"basic\"\n : \"api_key\";\n\n logger.info(\"Authorization header provided\", {\n authType,\n eventType: \"mcp_auth_header_provided\",\n remoteUrl: argv.url as string,\n });\n }\n\n // Initialize traffic scanner from config\n const scannerHeaders = { ...config.scanner.headers };\n\n logger.debug(\"Initializing traffic scanner\", {\n failOpen: config.scanner.failOpen,\n hasApiKey: !!config.scanner.apiKey,\n scannerEnabled: config.scanner.enabled,\n scannerUrl: config.scanner.url,\n timeoutMs: config.scanner.timeoutMs,\n });\n\n // Centralized handling for ANY startup failure (config/usage error, upstream\n // unreachable, access-control block). Each surfaces a SPECIFIC reason to the\n // agent via an `initialize` error reply (never the opaque -32000), to\n // Groundcover, and to stderr — then exits. Called both from the proxy-type\n // determination below (pre-connect config errors) and the post-connect catch;\n // `clientConnected` decides whether we may still answer the client's stdin.\n const handleStartupFailure = async (error: unknown): Promise<never> => {\n const respondToClient = !argv.port && !clientConnected;\n // A remote URL may carry credentials/query tokens — never surface them. Use\n // origin-only, and scrub any verbatim occurrence the upstream error embedded.\n const safeRemoteUrl = remoteUrl\n ? (() => {\n try {\n return new URL(remoteUrl).origin;\n } catch {\n return \"[invalid remote URL]\";\n }\n })()\n : undefined;\n const scrub = (text: string): string =>\n remoteUrl && safeRemoteUrl ? text.split(remoteUrl).join(safeRemoteUrl) : text;\n\n if (error instanceof AccessControlBlockError) {\n // Onyx access-control block at connect time. error.message carries the\n // org's policy reason (or a default naming Onyx).\n const reason =\n error.message ||\n \"This MCP server has been blocked by your organization's Onyx access-control policy. Contact your administrator if you believe this is an error.\";\n // Prominent multi-line banner on stderr — in addition to the agent-facing\n // initialize error and the Groundcover event — so a developer running the\n // gateway directly (the ticket's --debug flow) sees the block clearly.\n const banner = [\n \"\",\n \"❌ ACCESS DENIED\",\n \"━\".repeat(60),\n reason,\n \"Please contact your administrator if you believe this is an error.\",\n \"\",\n ].join(\"\\n\");\n return failStartupVisibly({\n exitCode: 1,\n logMessage: \"MCP server blocked by access control policy\",\n meta: { error: error.message, eventType: \"mcp_access_control_block\", reason: error.reason },\n reason,\n respondToClient,\n stderrLine: banner,\n });\n }\n\n // Any other startup failure (config/usage error, upstream unreachable, …).\n const reason = scrub(\n error instanceof ConfigurationError\n ? `Onyx MCP gateway: configuration error — ${error.message}`\n : error instanceof ProxyConnectionError\n ? `Onyx MCP gateway: cannot reach the upstream MCP server${safeRemoteUrl ? ` at ${safeRemoteUrl}` : \"\"} — ${error.message}`\n : `Onyx MCP gateway: failed to start — ${error instanceof Error ? error.message : String(error)}`,\n );\n return failStartupVisibly({\n exitCode: 1,\n logMessage: \"MCP server failed to start\",\n meta: {\n args: finalArgs ? redactCliArgs(finalArgs) : undefined,\n command: finalCommand,\n errno: getUpstreamErrno(error),\n error: scrub(`Could not start the proxy: ${error}`),\n eventType: \"mcp_server_start_failed\",\n proxyType,\n remoteUrl: safeRemoteUrl,\n transport: usedTransport,\n },\n reason,\n respondToClient,\n stderrLine: `[mcp-gateway] FATAL: ${reason}`,\n });\n };\n\n // Determine proxy type based on command\n if (argv._[0] === \"remote\") {\n proxyType = \"remote\";\n\n // Warn if deprecated remote-transport argument is used\n if (argv[\"remote-transport\"]) {\n logger.warn(\n \"The --remote-transport option is deprecated and will be ignored. \" +\n \"Transport is now auto-detected per MCP spec (Streamable HTTP with SSE fallback).\",\n { providedTransport: argv[\"remote-transport\"] }\n );\n }\n\n const rawUrl = argv.url as string;\n if (!rawUrl) {\n // Route through the visible-startup handler (not a bare throw) so the agent\n // gets a specific reason rather than -32000.\n await handleStartupFailure(new ConfigurationError(\"Remote URL is required for remote proxy type\", \"url\"));\n }\n // Normalize URL to avoid redirect issues on Windows\n // Windows may convert POST to GET on 301/302 redirects (e.g., trailing slash differences)\n remoteUrl = normalizeUrl(rawUrl);\n if (remoteUrl !== rawUrl) {\n logger.debug(\"Normalized remote URL\", { normalizedUrl: remoteUrl, originalUrl: rawUrl });\n }\n } else {\n // Default to stdio for backward compatibility\n proxyType = \"stdio\";\n\n // If -- separator was used, first item in argv[\"--\"] is the command\n if (argv[\"--\"] && (argv[\"--\"] as string[]).length > 0) {\n const dashDashArgs = argv[\"--\"] as string[];\n finalCommand = dashDashArgs[0];\n // Ensure all args are strings (yargs may parse numbers as numeric types)\n finalArgs = dashDashArgs.slice(1).map(String);\n } else {\n // Use positional arguments\n finalCommand = argv.command as string;\n // Ensure all args are strings (yargs may parse numbers as numeric types)\n finalArgs = ((argv.args as unknown[]) || []).map(String);\n }\n\n if (!finalCommand) {\n // Route through the visible-startup handler (not a bare throw) so the agent\n // gets a specific reason rather than -32000.\n await handleStartupFailure(new ConfigurationError(\"No command specified for stdio proxy\", \"command\"));\n }\n }\n\n // Detect a container-based MCP (Docker, Podman, nerdctl, finch). This is PURE\n // string parsing — no process is spawned. Resolving the image DIGESTS invokes\n // the container runtime with a value the (possibly source-controlled) MCP\n // config controls, so that step is deferred to\n // resolveContainerImageDigests() below, which runs only after the\n // access-control decision has allowed this server.\n let containerMCPCommand: ContainerMCPCommand | undefined;\n if (proxyType === \"stdio\" && finalCommand) {\n containerMCPCommand = detectContainerMCP(finalCommand, finalArgs);\n if (containerMCPCommand.isContainerMCP) {\n logger.info(\"Container MCP detected\", {\n imageName: containerMCPCommand.imageName,\n runtime: containerMCPCommand.runtime,\n });\n }\n }\n\n // Resolved (post-gate) container image digests. `undefined` until\n // resolveContainerImageDigests() has run — never resolved for a server the\n // access-control decision denied.\n let containerImageDigests: string[] | undefined;\n\n // Session identity (machine/session ids), read at most once per process.\n // Resolved LAZILY and awaited ONLY by the paths that need it: the probe behind it\n // is slow on Windows, and a plain stdio server must not pay for it on the way to\n // being spawned (that latency is what a client sees as a hung start). Two paths\n // do await it: the access-control request, which awaited it before this was lazy,\n // and the container digest lookup, which needs the ids on its own log lines — so\n // a CONTAINER server does wait for it pre-spawn. That is cheap in the deployed\n // shape (access control configured ⇒ already resolved) and only a first read in\n // dev runs without access control. Keep it that way if you reorder this.\n let sessionDataOnce: ReturnType<typeof getSessionData> | undefined;\n const attachSessionDataToLogger = async (): Promise<void> => {\n sessionDataOnce ??= getSessionData();\n logger.setSessionData({ ...(await sessionDataOnce), proxyType });\n };\n\n /**\n * Invoke the container runtime to resolve the MCP server's image digests.\n *\n * SECURITY: this is the one step that runs a local binary\n * with a value taken from the MCP server config, so it MUST NOT run before\n * accessControlAuthorizer.isAllowed() has allowed the server — otherwise a\n * server Onyx policy denies still causes local execution. Idempotent, so a\n * mid-session reconnect does not re-inspect the image.\n */\n const resolveContainerImageDigests = async (): Promise<void> => {\n if (containerImageDigests !== undefined || !containerMCPCommand?.isContainerMCP || !containerMCPCommand.imageName) {\n return;\n }\n // Attach the machine/session ids first, so the lookup's own outcome logs —\n // the validator's deliberate skip, the failure path and the success path —\n // carry ids that tie the line to this host and session.\n await attachSessionDataToLogger();\n containerImageDigests = getContainerImageDigests(\n containerMCPCommand.imageName,\n containerMCPCommand.runtime\n );\n };\n\n const buildSessionCliArgs = (): RemoteSessionCliArgs | StdioSessionCliArgs =>\n proxyType === \"stdio\"\n ? {\n args: finalArgs as string[],\n command: finalCommand as string,\n dockerImageDigests: containerImageDigests,\n dockerImageName: containerMCPCommand?.imageName,\n proxyType,\n }\n : {\n proxyType,\n url: remoteUrl as string,\n };\n\n // Initialize the access control client if configured. NOTE: the session data\n // sent with the authorize request carries dockerImageName but not the digests —\n // they are resolved only after this decision. The backend's MCP access-control\n // path keys on the command+args cache hash and resolves the version from its\n // own mcp_latest_version cache, so the decision is unaffected.\n let accessControlClient: AccessControlClient | undefined;\n if (config.accessControl.url && config.scanner.apiKey) {\n const cliArgs = buildSessionCliArgs();\n // This path already awaited the session identity before the fix, so reading\n // it here adds no startup latency. It also makes the ids available to every\n // log line from here on, digest lookup included.\n await attachSessionDataToLogger();\n const sessionDataBase = await sessionDataOnce;\n\n const sessionData =\n cliArgs.proxyType === \"stdio\"\n ? {\n ...sessionDataBase,\n proxyType: \"stdio\" as const,\n stdioCliArgs: { ...cliArgs, args: redactCliArgs(cliArgs.args) },\n }\n : {\n ...sessionDataBase,\n proxyType: \"remote\" as const,\n remoteCliArgs: cliArgs,\n };\n\n accessControlClient = new AccessControlClient({\n apiKey: config.scanner.apiKey,\n headers: config.accessControl.headers,\n sessionData,\n timeoutMs: config.accessControl.timeoutMs,\n url: config.accessControl.url,\n });\n }\n\n // Determine server name for error messages\n const serverName = proxyType === \"remote\" ? remoteUrl : finalCommand;\n\n // Initialize access control authorizer\n const accessControlAuthorizer = new AccessControlAuthorizer({\n client: accessControlClient,\n enabled: config.accessControl.enabled,\n serverName,\n });\n\n // Initialize OAuth credential store for remote connections\n const oauthCredentialStore = new OAuthCredentialStore({\n storageDir: config.oauthDir,\n });\n\n // Factory function to create a new Client instance\n const createClient = (): Client =>\n new Client(\n {\n name: \"mcp-gateway\",\n version: config.appVersion,\n },\n {\n capabilities: {},\n }\n );\n\n const connect = async (\n client: Client,\n { allowInteractiveAuth = true }: { allowInteractiveAuth?: boolean } = {}\n ): Promise<Client> => {\n // Check access control (async - may query backend)\n const isAllowed = await accessControlAuthorizer.isAllowed();\n if (!isAllowed) {\n const reason = accessControlAuthorizer.getBlockReason();\n logger.warn(\"MCP server blocked by access control\", {\n proxyType,\n reason,\n });\n\n throw new AccessControlBlockError(reason, \"policy_block\");\n }\n\n logger.info(\"MCP server allowed by access control\");\n\n // ONLY NOW may the container runtime be invoked for this server's image\n // digests: the access-control decision has resolved and allowed it. Running it\n // earlier is what let a policy-denied server achieve local host execution\n // before the gateway enforced its own control.\n await resolveContainerImageDigests();\n\n if (proxyType === \"stdio\") {\n // Merge custom CLI environment variables with process.env for child processes\n // Note: This is for MCP server processes, not the proxy's internal configuration\n const mergedEnv = {\n ...process.env,\n ...customEnv,\n } as Record<string, string>;\n\n const transport = new StdioClientTransport({\n args: finalArgs,\n command: finalCommand as string,\n env: mergedEnv,\n onEvent: (event) => {\n if (argv.debug) {\n logger.debug(\"Transport event\", { event });\n }\n },\n shell: argv.shell,\n // Pipe stderr so we can both forward to the parent agent client (preserving\n // the previous \"inherit\" behavior) AND log each line into the gateway's\n // structured log. Without this we have no record of why a spawned MCP\n // server crashed during startup.\n stderr: \"pipe\",\n });\n activeStdioTransport = transport;\n\n // Cap each captured stderr line so a misbehaving server emitting multi-MB\n // stack dumps or base64 blobs to stderr can't balloon log entries or the\n // in-memory tail buffer. Matches JSONFilterTransform.logDroppedLine.\n const STDERR_LINE_MAX = 1024;\n // If the child never emits a newline we still need an upper bound on the\n // pending line buffer — otherwise a binary blob to stderr would grow\n // memory unbounded and defeat the per-line cap.\n const STDERR_BUFFER_MAX = STDERR_LINE_MAX * 8;\n const truncate = (line: string): string =>\n line.length > STDERR_LINE_MAX ? `${line.slice(0, STDERR_LINE_MAX)}…` : line;\n\n let stderrLineBuffer = \"\";\n const recentStderr: string[] = [];\n const logStderrLine = (line: string) => {\n const truncated = truncate(line);\n recentStderr.push(truncated);\n if (recentStderr.length > 100) recentStderr.shift();\n logger.info(\"MCP server stderr\", {\n command: finalCommand,\n eventType: \"mcp_server_stderr\",\n line: truncated,\n });\n };\n const flushBufferAsLine = () => {\n if (stderrLineBuffer.length === 0) return;\n logStderrLine(stderrLineBuffer);\n stderrLineBuffer = \"\";\n };\n // Decoder buffers incomplete multi-byte UTF-8 sequences across chunk\n // boundaries — without it `chunk.toString()` would emit replacement\n // characters when a codepoint straddles a chunk.\n const stderrDecoder = new StringDecoder(\"utf8\");\n // Drain any pending decoder bytes + remaining line buffer. Idempotent —\n // both `end` and `close` may fire, and the connect-failure catch below\n // also drains explicitly so we don't depend on the SDK ordering.\n let stderrTailDrained = false;\n const drainStderrTail = () => {\n if (stderrTailDrained) return;\n stderrTailDrained = true;\n const decoderTail = stderrDecoder.end();\n if (decoderTail) {\n stderrLineBuffer += decoderTail;\n }\n flushBufferAsLine();\n };\n transport.stderr?.on(\"data\", (chunk: Buffer) => {\n // Forward raw bytes to the parent's stderr to preserve the byte-fidelity\n // the previous `stderr: \"inherit\"` provided. The structured-log path\n // below uses StringDecoder for proper UTF-8 handling.\n process.stderr.write(chunk);\n stderrLineBuffer += stderrDecoder.write(chunk);\n const lines = stderrLineBuffer.split(\"\\n\");\n stderrLineBuffer = lines.pop() ?? \"\";\n for (const line of lines) {\n if (line.length === 0) continue;\n logStderrLine(line);\n }\n // Cap residual partial line — only after extracting any complete lines\n // above, otherwise a chunk containing newlines + an oversize tail would\n // silently discard the parseable lines together with the tail.\n if (stderrLineBuffer.length > STDERR_BUFFER_MAX) {\n flushBufferAsLine();\n }\n });\n transport.stderr?.once(\"end\", drainStderrTail);\n transport.stderr?.once(\"close\", drainStderrTail);\n\n try {\n await client.connect(transport);\n return client;\n } catch (error) {\n drainStderrTail();\n logger.warn(\"Failed original server access\", {\n command: finalCommand,\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_server_access_failed\",\n proxyType,\n stderrTail: recentStderr.slice(-20),\n });\n // Tear down the spawned child before rethrowing. connectWithStartupRetry\n // re-runs connect() on a recoverable failure, so without this each retry\n // would orphan the previous attempt's stdio subprocess.\n await transport.close().catch(() => {});\n throw new ProxyConnectionError(`Failed to connect to stdio process: ${error}`, undefined, \"stdio\", {\n argCount: finalArgs?.length,\n command: finalCommand,\n }, error);\n }\n } else if (proxyType === \"remote\") {\n // Connect to remote server\n // Strategy: Try Streamable HTTP first, fall back to SSE if not supported (404/405)\n // Handle OAuth (401) separately - if auth fails, get token and retry the whole process\n\n const url = remoteUrl as string;\n\n // Build request init carrying the caller-supplied headers. An OAuth\n // bearer is NOT set here — it's injected per-request by the dynamic-auth\n // fetch wrapper below, so that mid-session refreshes pick up the latest\n // bearer without needing to recreate the transport (PRDCT-3666 Tier 2).\n // A static Authorization (api-key / Basic) MAY be present in remoteHeaders;\n // the wrapper preserves it when there is no OAuth bearer (PRDCT-5136).\n const buildNonAuthRequestInit = (): RequestInit | undefined => {\n const headers: Record<string, string> = { ...remoteHeaders };\n return Object.keys(headers).length > 0 ? { headers } : undefined;\n };\n\n // Get OAuth token (from cache or via flow)\n const getOAuthToken = async (): Promise<string> => {\n const oauthResult = await performOAuthFlow(url, { credentialStore: oauthCredentialStore });\n logger.info(\"Authentication performed\", {\n authType: \"oauth\",\n eventType: \"mcp_auth_performed\",\n fromCache: oauthResult.fromCache,\n remoteUrl: url,\n });\n return oauthResult.tokens.access_token;\n };\n\n let connectedClient: Client;\n try {\n // Check for cached OAuth token\n const cached = await oauthCredentialStore.load(url);\n const initialBearer =\n cached && oauthCredentialStore.hasValidAccessToken(cached) ? cached.tokens?.access_token : undefined;\n\n // Dynamic auth handle: holds the in-memory bearer. The transport's\n // custom fetch reads from this handle on every request, refreshes\n // mid-session on 401, and dedupes concurrent refreshes (PRDCT-3666\n // Tier 2). When `performOAuthFlow` produces fresh tokens (cold start\n // or browser fallback), we feed them back into the handle so future\n // requests use the new bearer too.\n const dynAuth: DynamicAuthHandle = createDynamicAuth(url, oauthCredentialStore, initialBearer);\n const authFetch = createAuthenticatingFetch(dynAuth);\n\n let usedFreshOAuth = false; // Track if we've already tried with a fresh token\n\n try {\n // Try connecting with transport fallback (Streamable HTTP → SSE)\n // Uses a client factory to create fresh clients for each transport attempt\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server\", { hasAuth: !!initialBearer, transport: usedTransport });\n } catch (err) {\n // Handle 401 errors - need OAuth authentication or token refresh\n if (is401Error(err)) {\n // If we already tried with a fresh OAuth token, don't retry again (avoid infinite loop)\n if (usedFreshOAuth) {\n throw err;\n }\n\n // PRDCT-3666: when the cached token was rejected by upstream and\n // the dynamic-auth fetch wrapper couldn't silently recover (no\n // refresh_token on disk, or invalid_grant, or transient failure\n // mid-retry), fall through to the full interactive OAuth flow.\n // Tier 1 already preserved refresh_token across transient errors;\n // Tier 2 added the mid-session refresh inside `authFetch`. By the\n // time we land here, both fast paths have been exhausted and a\n // browser popup is the spec-correct next step (matches what a\n // non-gateway MCP client would do).\n if (dynAuth.getCurrentBearer() ?? initialBearer) {\n logger.info(\"Cached + refresh both rejected; falling back to full OAuth flow\");\n\n // Mark the cached access_token as expired BEFORE falling back.\n //\n // Without this, `performOAuthFlow` → `tryLoadExistingCredentials`\n // would happily return the SAME dead access_token (its\n // time-based `hasValidAccessToken` check can't know upstream\n // rejected it), we'd set that dead token as the bearer, retry,\n // get 401 again, and the `usedFreshOAuth` guard would\n // propagate the error WITHOUT ever opening a browser. Users\n // on no-refresh_token servers (ClickUp, GitHub Copilot,\n // Sentry) would be permanently stuck.\n //\n // Clearing `tokensObtainedAt` is enough — `isAccessTokenExpired`\n // returns true when it's missing, so `hasValidAccessToken`\n // returns false. We deliberately preserve:\n // - `clientInfo` (so we don't have to re-DCR)\n // - `tokens.refresh_token` (a transient refresh failure may\n // have just left it on disk; the next attempt may succeed)\n await oauthCredentialStore.update(url, { tokensObtainedAt: undefined });\n } else {\n logger.info(\"Server requires OAuth, starting authentication flow...\");\n }\n\n // A mid-session reconnect must never open a browser (the dynamicAuth\n // contract). The silent refresh already ran inside authFetch; a\n // surviving 401 is surfaced (non-recoverable) and the user re-auths\n // interactively on the next fresh connect.\n if (!allowInteractiveAuth) {\n logger.info(\"Skipping interactive OAuth on reconnect; surfacing 401\", {\n eventType: \"mcp_reconnect_skip_interactive_auth\",\n });\n throw err;\n }\n\n const freshToken = await getOAuthToken();\n dynAuth.setBearer(freshToken);\n usedFreshOAuth = true;\n // If even the freshly-issued OAuth token is then rejected, the\n // error propagates to the outer catch (wrapped as\n // ProxyConnectionError) — usedFreshOAuth gates further retries.\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server with OAuth\", { transport: usedTransport });\n } else if (\n !usedFreshOAuth &&\n !(dynAuth.getCurrentBearer() ?? initialBearer) &&\n !new Headers(remoteHeaders).has(\"Authorization\")\n ) {\n // COLD-START non-401 recovery. We sent NO credential at all — neither\n // an OAuth bearer NOR a caller-supplied static Authorization (api-key\n // / Basic / bearer via --bearer-token-env / --headers /\n // --env-http-headers) — and the upstream rejected with a non-401. It\n // may be an OAuth-protected server that answers an unauthenticated\n // request non-compliantly — e.g. Okta's llm.atko.ai returns 500\n // instead of a spec-correct 401. `is401Error` is false here and the\n // transport layer only falls back on 404/405, so without this branch\n // the gateway would give up without ever authenticating, even though\n // the server exposes standard, discoverable OAuth metadata.\n //\n // Deliberately scoped to the NO-CREDENTIAL case (cronyx + coderabbit,\n // PR #8418). If we had ALREADY sent a credential (a cached OAuth token\n // OR a static Authorization) and got a non-401 (500/502/503/429), that\n // is NOT evidence the credential is bad — a genuine rejection is a\n // 401, handled by the branch above. A non-401 on an authenticated\n // request is most likely transient (deploy, rate-limit), so re-authing\n // there would wipe a still-valid token on disk and pop a browser on a\n // blip. We let that case fall through to `throw err` and propagate the\n // original error unchanged.\n //\n // Probe the well-known OAuth metadata directly (independent of any\n // 401). Only if an authorization server is actually advertised do we\n // run the OAuth flow once and retry; otherwise propagate the ORIGINAL\n // error (just +1 well-known probe of latency on an already-doomed\n // connection). Because we only get here with no bearer, there is no\n // cached token to invalidate first — `performOAuthFlow` does a clean\n // fresh acquisition.\n let advertisesOAuth = false;\n let discoveryError: string | undefined;\n try {\n const { authMetadata } = await discoverAuthServer(new URL(url), { logger });\n advertisesOAuth = Boolean(authMetadata);\n } catch (discoveryErr) {\n discoveryError = discoveryErr instanceof Error ? discoveryErr.message : String(discoveryErr);\n }\n if (!advertisesOAuth) {\n // Surface at warn so a production recurrence shows WHY recovery did\n // not happen: the upstream rejected us with a non-401 AND exposed\n // no discoverable OAuth metadata, so there is nothing to bootstrap\n // from. (discoverAuthServer logs the individual metadata-probe\n // results — protected-resource + auth-server + registration_endpoint\n // — separately.)\n logger.warn(\"Non-401 failure and no discoverable OAuth metadata; propagating original error\", {\n discoveryError,\n eventType: \"mcp_non401_no_oauth_discovery\",\n originalError: err instanceof Error ? err.message : String(err),\n remoteUrl: url,\n });\n throw err;\n }\n logger.info(\"Cold-start non-401 but server advertises OAuth; attempting OAuth flow\", {\n originalError: err instanceof Error ? err.message : String(err),\n });\n if (!allowInteractiveAuth) {\n logger.info(\"Skipping interactive OAuth on reconnect; surfacing error\", {\n eventType: \"mcp_reconnect_skip_interactive_auth\",\n });\n throw err;\n }\n const freshToken = await getOAuthToken();\n dynAuth.setBearer(freshToken);\n usedFreshOAuth = true;\n const result = await connectWithTransportFallback(\n createClient,\n url,\n buildNonAuthRequestInit(),\n authFetch,\n );\n connectedClient = result.client;\n usedTransport = result.transportType;\n logger.info(\"Connected to remote server with OAuth (recovered from non-401)\", {\n transport: usedTransport,\n });\n } else {\n throw err;\n }\n }\n return connectedClient;\n } catch (error) {\n logger.warn(\"Failed original server access\", {\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_server_access_failed\",\n proxyType,\n remoteUrl: url,\n });\n if (error instanceof ProxyConnectionError) throw error;\n throw new ProxyConnectionError(\n `Failed to connect to remote server: ${error instanceof Error ? error.message : String(error)}`,\n url,\n usedTransport,\n { headerCount: Object.keys(remoteHeaders).length },\n error\n );\n }\n }\n\n // Should never reach here, but satisfy TypeScript\n return client;\n };\n\n // Connect to the upstream with bounded retry on transient transport failures,\n // so a brief reachability blip when the gateway starts doesn't fail the whole\n // session. connect() returns a fresh client per attempt via createClient().\n const connectWithStartupRetry = (): Promise<Client> =>\n connectWithRetry(() => connect(createClient()), config.upstream.connect, {\n onRetry: ({ attempt, delayMs, error }) => {\n logger.warn(\"Upstream connect failed; retrying\", {\n attempt,\n delayMs,\n errno: getUpstreamErrno(error),\n error: error instanceof Error ? error.message : String(error),\n eventType: \"mcp_upstream_connect_retry\",\n proxyType,\n });\n },\n });\n\n // Whether to wrap the upstream in the mid-session reconnecting façade. Remote\n // by default; stdio is opt-in (a stdio \"reconnect\" respawns the child and\n // loses its in-memory state). The reconnect factory passes\n // allowInteractiveAuth:false so a mid-session reconnect never opens a browser.\n const reconnectEnabled =\n proxyType === \"remote\"\n ? config.upstream.reconnect.enabled\n : config.upstream.reconnect.enabledForStdio;\n const wrapUpstream = (connected: Client): UpstreamClient =>\n reconnectEnabled\n ? createReconnectingClient({\n cooldownMs: config.upstream.reconnect.cooldownMs,\n initial: connected,\n reconnect: () => connect(createClient(), { allowInteractiveAuth: false }),\n })\n : connected;\n\n // The traffic mirror snapshots the session data — including the container image\n // digests — when it is initialized, so it must be initialized AFTER the first\n // connect() resolved them (i.e. after the access-control gate). Memoized: one\n // mirror per process, however many sessions connect.\n let trafficMirrorInit: Promise<unknown> | undefined;\n const ensureTrafficMirror = (): Promise<unknown> => {\n trafficMirrorInit ??= initializeTrafficMirror({\n cliArgs: buildSessionCliArgs(),\n scanApiKey: config.scanner.apiKey,\n // Scanner config from env vars\n scanEnabled: config.scanner.enabled,\n scanFailOpen: config.scanner.failOpen,\n scanHeaders: scannerHeaders,\n scanTimeoutMs: config.scanner.timeoutMs,\n scanUrl: config.scanner.url || \"\",\n spawnedServerInfoProvider,\n });\n return trafficMirrorInit;\n };\n\n const proxy = async () => {\n // If port is specified, start HTTP server mode\n if (argv.port) {\n let upstream: undefined | UpstreamClient;\n const httpServer = await startHTTPServer({\n createServer: async () => {\n // connect() may return a different instance for remote connections;\n // connectWithStartupRetry retries a transient upstream failure.\n const connected = await connectWithStartupRetry();\n\n // Initialize the traffic evaluator once, now that access control has\n // allowed the server and the image digests are resolved.\n await ensureTrafficMirror();\n\n const serverVersion = connected.getServerVersion() as {\n name: string;\n version: string;\n };\n\n const serverCapabilities =\n connected.getServerCapabilities() as ServerCapabilities;\n\n // Wrap so a mid-session upstream drop self-heals instead of killing the\n // session. proxyServer holds this stable façade, not the raw client.\n upstream = wrapUpstream(connected);\n\n const server = new Server(serverVersion, {\n capabilities: serverCapabilities,\n });\n\n proxyServer({\n authorizer: accessControlAuthorizer,\n client: upstream,\n server,\n serverCapabilities,\n });\n\n return server;\n },\n onClose: async (server) => {\n if (upstream) {\n await upstream.close();\n }\n await server.close();\n },\n port: argv.port,\n });\n\n logger.info(\"MCP gateway started\", {\n eventType: \"mcp_gateway_started\",\n mode: \"http\",\n port: argv.port,\n proxyType,\n });\n if (proxyType === \"stdio\") {\n logger.info(\"MCP server configuration\", {\n args: redactCliArgs(finalArgs ?? []).join(\" \"),\n command: finalCommand,\n eventType: \"mcp_server_config\",\n proxyType,\n });\n } else {\n logger.info(\"MCP server configuration\", {\n eventType: \"mcp_server_config\",\n proxyType,\n remoteUrl,\n });\n }\n\n return {\n close: () => {\n return httpServer.close();\n },\n };\n }\n\n // Original stdio mode\n // connect() may return a different instance for remote connections;\n // connectWithStartupRetry retries a transient upstream failure.\n const connected = await connectWithStartupRetry();\n\n const serverVersion = connected.getServerVersion() as {\n name: string;\n version: string;\n };\n\n const serverCapabilities =\n connected.getServerCapabilities() as ServerCapabilities;\n\n // Wrap so a mid-session upstream drop self-heals (remote only by default).\n const upstream = wrapUpstream(connected);\n\n // Always create a stdio server for client-facing interface\n const server = new Server(serverVersion, {\n capabilities: serverCapabilities,\n });\n\n proxyServer({\n authorizer: accessControlAuthorizer,\n client: upstream,\n server,\n serverCapabilities,\n });\n\n const stdioTransport = new StdioServerTransport();\n\n // Initialize traffic evaluator with scanner config (post-gate, so the\n // resolved container image digests are part of the session data it reports).\n await ensureTrafficMirror();\n\n await server.connect(stdioTransport);\n clientConnected = true;\n\n logger.info(\"MCP gateway started\", {\n eventType: \"mcp_gateway_started\",\n mode: \"stdio\",\n proxyType,\n });\n\n // Log MCP server configuration\n logger.info(\"MCP server configuration\", {\n capabilities: Object.keys(serverCapabilities || {}),\n eventType: \"mcp_server_config\",\n proxyType,\n serverName: serverVersion?.name,\n serverVersion: serverVersion?.version,\n ...(proxyType === \"stdio\"\n ? { args: redactCliArgs(finalArgs ?? []).join(\" \"), command: finalCommand }\n : { remoteUrl, transport: usedTransport }),\n });\n\n return {\n close: () => {\n // Close the upstream connection (the façade closes the live client).\n return upstream.close();\n },\n };\n };\n\n const createGracefulShutdown = ({\n server,\n timeout,\n }: {\n server: { close: () => Promise<void> | void };\n timeout: number;\n }) => {\n let shutdownTriggered = false;\n\n const gracefulShutdown = async (trigger: string) => {\n if (shutdownTriggered) {\n return;\n }\n shutdownTriggered = true;\n\n logger.info(\"MCP gateway stopping\", {\n eventType: \"mcp_gateway_stopping\",\n signal: trigger,\n });\n\n try {\n await server.close();\n // Log final request counts before shutdown\n logRequestCounts();\n // Flush logs to ingest endpoint before exit\n await flushLogs(2000);\n process.exit(0);\n } catch (error) {\n const errorMessage = `Error during shutdown: ${error}`;\n logger.error(\"Error during shutdown\", {\n error: errorMessage,\n eventType: \"mcp_gateway_stopped\",\n shutdownTimeout: timeout,\n });\n // Flush logs to ingest endpoint before exit\n await flushLogs(2000);\n process.exit(1);\n }\n };\n\n const timeoutHandler = (trigger: string) => async () => {\n setTimeout(() => {\n logger.error(\"Graceful shutdown timeout exceeded, forcing exit\");\n process.exit(1);\n }, timeout).unref();\n\n await gracefulShutdown(trigger);\n };\n\n process.once(\"SIGTERM\", timeoutHandler(\"SIGTERM\"));\n process.once(\"SIGINT\", timeoutHandler(\"SIGINT\"));\n\n // Stdin EOF — fires when the parent (e.g. claude) closes our stdio pipe.\n // On Windows, parents typically TerminateProcess without graceful signals,\n // but the MCP SDK closes stdio first; this gives us a window to reap our\n // child process tree before being killed. On POSIX it provides a backup\n // path when SIGHUP/SIGTERM doesn't reach us.\n process.stdin.once(\"end\", timeoutHandler(\"stdin-end\"));\n\n return () => {\n return server.close();\n };\n };\n\n // Log startup before attempting to connect\n logger.info(\"MCP gateway starting\", {\n command: finalCommand,\n eventType: \"mcp_gateway_starting\",\n proxyType,\n transport: proxyType === \"remote\" ? \"auto-detect\" : \"stdio\",\n url: remoteUrl,\n });\n\n try {\n const server = await proxy();\n\n createGracefulShutdown({\n server,\n timeout: argv.gracefulShutdownTimeout,\n });\n } catch (error) {\n // Startup failed after proxy() began (upstream unreachable, access-control\n // block, …). Surface a specific reason to the agent + Groundcover + stderr.\n await handleStartupFailure(error);\n }\n};\n"],"x_google_ignoreList":[0],"mappings":"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"}