Rhys Sullivangoogle oauth works2e328ab8 months agoHistory
989 lines · 30.4 KB · TypeScript
RawDownload
import {  FetchHttpClient,  HttpClient,  HttpClientRequest,} from "@effect/platform";import {  createSdkMcpConnector,  discoverMcpToolsFromConnector,} from "@executor/codemode-mcp";import {  extractGoogleDiscoveryManifest,} from "@executor/codemode-google-discovery";import {  extractOpenApiManifest,  parseOpenApiDocument,} from "@executor/codemode-openapi";import {  type SourceAuthInference,  type SourceDiscoveryResult,  type SourceProbeAuth,} from "#schema";import * as Effect from "effect/Effect";import { getIntrospectionQuery } from "graphql";import { startMcpOAuthAuthorization } from "./mcp-oauth";import { namespaceFromSourceName } from "./source-names"; const GRAPHQL_INTROSPECTION_QUERY = getIntrospectionQuery(); type HttpProbeResponse = {  status: number;  headers: Readonly<Record<string, string>>;  text: string;}; type OpenApiProbeResult = {  result: SourceDiscoveryResult;}; type GoogleDiscoveryProbeResult = {  result: SourceDiscoveryResult;}; type GraphqlProbeResult = {  result: SourceDiscoveryResult;}; type McpProbeResult = {  result: SourceDiscoveryResult;}; type OpenApiSecurityCandidate = {  name: string;  kind: "bearer" | "oauth2" | "apiKey" | "basic" | "unknown";  supported: boolean;  headerName: string | null;  prefix: string | null;  parameterName: string | null;  parameterLocation: "header" | "query" | "cookie" | null;  oauthAuthorizationUrl: string | null;  oauthTokenUrl: string | null;  oauthScopes: string[];  reason: string;}; const isRecord = (value: unknown): value is Record<string, unknown> =>  typeof value === "object" && value !== null && !Array.isArray(value); const asString = (value: unknown): string | null =>  typeof value === "string" && value.length > 0 ? value : null; const trimOrNull = (value: string | null | undefined): string | null => {  if (value == null) {    return null;  }   const trimmed = value.trim();  return trimmed.length > 0 ? trimmed : null;}; const defaultNameFromEndpoint = (endpoint: string): string => new URL(endpoint).hostname; const normalizeUrl = (value: string): string => {  const trimmed = value.trim();  if (trimmed.length === 0) {    throw new Error("Source URL is required");  }   const parsed = new URL(trimmed);  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {    throw new Error("Source URL must use http or https");  }   return parsed.toString();}; const unsupportedAuthInference = (  kind: SourceAuthInference["suggestedKind"],  input: Omit<SourceAuthInference, "suggestedKind" | "supported">,): SourceAuthInference => ({  ...input,  suggestedKind: kind,  supported: false,}); const supportedAuthInference = (  kind: Extract<SourceAuthInference["suggestedKind"], "none" | "bearer" | "oauth2">,  input: Omit<SourceAuthInference, "suggestedKind" | "supported">,): SourceAuthInference => ({  ...input,  suggestedKind: kind,  supported: true,}); const unknownAuthInference = (reason: string): SourceAuthInference => ({  suggestedKind: "unknown",  confidence: "low",  supported: false,  reason,  headerName: null,  prefix: null,  parameterName: null,  parameterLocation: null,  oauthAuthorizationUrl: null,  oauthTokenUrl: null,  oauthScopes: [],}); const noneAuthInference = (  reason: string,  confidence: SourceAuthInference["confidence"] = "high",): SourceAuthInference => supportedAuthInference("none", {  confidence,  reason,  headerName: null,  prefix: null,  parameterName: null,  parameterLocation: null,  oauthAuthorizationUrl: null,  oauthTokenUrl: null,  oauthScopes: [],}); const parseChallengeAuthInference = (  headers: Readonly<Record<string, string>>,  fallbackReason: string,): SourceAuthInference => {  const challenge = headers["www-authenticate"] ?? headers["WWW-Authenticate"];  if (!challenge) {    return unknownAuthInference(fallbackReason);  }   const normalized = challenge.toLowerCase();  if (normalized.includes("bearer")) {    return supportedAuthInference("bearer", {      confidence: normalized.includes("realm=") ? "medium" : "low",      reason: `Derived from HTTP challenge: ${challenge}`,      headerName: "Authorization",      prefix: "Bearer ",      parameterName: null,      parameterLocation: null,      oauthAuthorizationUrl: null,      oauthTokenUrl: null,      oauthScopes: [],    });  }   if (normalized.includes("basic")) {    return unsupportedAuthInference("basic", {      confidence: "medium",      reason: `Derived from HTTP challenge: ${challenge}`,      headerName: "Authorization",      prefix: "Basic ",      parameterName: null,      parameterLocation: null,      oauthAuthorizationUrl: null,      oauthTokenUrl: null,      oauthScopes: [],    });  }   return unknownAuthInference(fallbackReason);}; const probeHeadersFromAuth = (probeAuth: SourceProbeAuth | null | undefined): Record<string, string> => {  if (probeAuth == null || probeAuth.kind === "none") {    return {};  }   if (probeAuth.kind === "headers") {    return { ...probeAuth.headers };  }   if (probeAuth.kind === "basic") {    return {      Authorization: `Basic ${Buffer.from(`${probeAuth.username}:${probeAuth.password}`).toString("base64")}`,    };  }   return {    [trimOrNull(probeAuth.headerName) ?? "Authorization"]: `${probeAuth.prefix ?? "Bearer "}${probeAuth.token}`,  };}; const executeHttpProbe = (input: {  method: "GET" | "POST";  url: string;  headers?: Readonly<Record<string, string>>;  body?: string;}): Effect.Effect<HttpProbeResponse, Error, never> =>  Effect.gen(function* () {    const client = yield* HttpClient.HttpClient;    let request = HttpClientRequest.make(input.method)(input.url, {      headers: input.headers ?? {},    });     if (input.body !== undefined) {      request = HttpClientRequest.bodyText(        request,        input.body,        input.headers?.["content-type"] ?? input.headers?.["Content-Type"] ?? "application/json",      );    }     const response = yield* client.execute(request).pipe(      Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause))),    );     const text = yield* response.text.pipe(      Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause))),    );     return {      status: response.status,      headers: response.headers,      text,    } satisfies HttpProbeResponse;  }).pipe(Effect.provide(FetchHttpClient.layer)); const parseStructuredDocument = (value: string): unknown =>  parseOpenApiDocument(value) as unknown; const readLocalRef = (document: Record<string, unknown>, ref: string): unknown => {  if (!ref.startsWith("#/")) {    return undefined;  }   let current: unknown = document;  for (const rawSegment of ref.slice(2).split("/")) {    const segment = rawSegment.replaceAll("~1", "/").replaceAll("~0", "~");    if (!isRecord(current)) {      return undefined;    }    current = current[segment];  }   return current;}; const resolveSecurityScheme = (  document: Record<string, unknown>,  input: unknown,  depth = 0,): Record<string, unknown> | null => {  if (!isRecord(input)) {    return null;  }   const ref = asString(input["$ref"]);  if (ref && depth < 5) {    return resolveSecurityScheme(document, readLocalRef(document, ref), depth + 1);  }   return input;}; const collectAppliedSecurityCandidates = (document: Record<string, unknown>): Array<{  name: string;  scopes: string[];}> => {  const seen = new Set<string>();  const candidates: Array<{ name: string; scopes: string[] }> = [];   const addRequirementArray = (value: unknown) => {    if (!Array.isArray(value)) {      return;    }     for (const requirement of value) {      if (!isRecord(requirement)) {        continue;      }       for (const [name, scopesValue] of Object.entries(requirement)) {        if (name.length === 0 || seen.has(name)) {          continue;        }         seen.add(name);        candidates.push({          name,          scopes: Array.isArray(scopesValue)            ? scopesValue.filter((scope): scope is string => typeof scope === "string")            : [],        });      }    }  };   addRequirementArray(document.security);   const paths = document.paths;  if (!isRecord(paths)) {    return candidates;  }   for (const pathItem of Object.values(paths)) {    if (!isRecord(pathItem)) {      continue;    }     addRequirementArray(pathItem.security);     for (const operation of Object.values(pathItem)) {      if (!isRecord(operation)) {        continue;      }      addRequirementArray(operation.security);    }  }   return candidates;}; const securityCandidateFromScheme = (input: {  name: string;  scopes: string[];  scheme: Record<string, unknown>;}): OpenApiSecurityCandidate => {  const type = asString(input.scheme.type)?.toLowerCase() ?? "";   if (type === "oauth2") {    const flows = isRecord(input.scheme.flows) ? input.scheme.flows : {};    const flow = Object.values(flows).find(isRecord) ?? null;    const declaredScopes = flow && isRecord(flow.scopes)      ? Object.keys(flow.scopes).filter((scope) => scope.length > 0)      : [];    const oauthScopes = [...new Set([...input.scopes, ...declaredScopes])].sort();     return {      name: input.name,      kind: "oauth2",      supported: true,      headerName: "Authorization",      prefix: "Bearer ",      parameterName: null,      parameterLocation: null,      oauthAuthorizationUrl: flow ? trimOrNull(asString(flow.authorizationUrl)) : null,      oauthTokenUrl: flow ? trimOrNull(asString(flow.tokenUrl)) : null,      oauthScopes,      reason: `OpenAPI security scheme \"${input.name}\" declares OAuth2`,    };  }   if (type === "http") {    const scheme = asString(input.scheme.scheme)?.toLowerCase() ?? "";    if (scheme === "bearer") {      return {        name: input.name,        kind: "bearer",        supported: true,        headerName: "Authorization",        prefix: "Bearer ",        parameterName: null,        parameterLocation: null,        oauthAuthorizationUrl: null,        oauthTokenUrl: null,        oauthScopes: input.scopes,        reason: `OpenAPI security scheme \"${input.name}\" declares HTTP bearer auth`,      };    }     if (scheme === "basic") {      return {        name: input.name,        kind: "basic",        supported: false,        headerName: "Authorization",        prefix: "Basic ",        parameterName: null,        parameterLocation: null,        oauthAuthorizationUrl: null,        oauthTokenUrl: null,        oauthScopes: input.scopes,        reason: `OpenAPI security scheme \"${input.name}\" declares HTTP basic auth`,      };    }  }   if (type === "apiKey") {    const location = asString(input.scheme.in);    const parameterLocation = location === "header" || location === "query" || location === "cookie"      ? location      : null;     return {      name: input.name,      kind: "apiKey",      supported: false,      headerName: parameterLocation === "header" ? trimOrNull(asString(input.scheme.name)) : null,      prefix: null,      parameterName: trimOrNull(asString(input.scheme.name)),      parameterLocation,      oauthAuthorizationUrl: null,      oauthTokenUrl: null,      oauthScopes: input.scopes,      reason: `OpenAPI security scheme \"${input.name}\" declares API key auth`,    };  }   return {    name: input.name,    kind: "unknown",    supported: false,    headerName: null,    prefix: null,    parameterName: null,    parameterLocation: null,    oauthAuthorizationUrl: null,    oauthTokenUrl: null,    oauthScopes: input.scopes,    reason: `OpenAPI security scheme \"${input.name}\" uses unsupported type ${type || "unknown"}`,  };}; const inferOpenApiAuth = (document: Record<string, unknown>): SourceAuthInference => {  const components = isRecord(document.components) ? document.components : {};  const securitySchemes = isRecord(components.securitySchemes)    ? components.securitySchemes    : {};  const appliedCandidates = collectAppliedSecurityCandidates(document);   if (appliedCandidates.length === 0) {    if (Object.keys(securitySchemes).length === 0) {      return noneAuthInference("OpenAPI document does not declare security requirements");    }     const fallbackCandidate = Object.entries(securitySchemes)      .map(([name, value]) => securityCandidateFromScheme({        name,        scopes: [],        scheme: resolveSecurityScheme(document, value) ?? {},      }))      .sort((left, right) => {        const priority = { oauth2: 0, bearer: 1, apiKey: 2, basic: 3, unknown: 4 } as const;        return priority[left.kind] - priority[right.kind] || left.name.localeCompare(right.name);      })[0];     if (!fallbackCandidate) {      return noneAuthInference("OpenAPI document does not declare security requirements");    }     const confidence = fallbackCandidate.kind === "unknown" ? "low" : "medium";    if (fallbackCandidate.kind === "oauth2") {      return supportedAuthInference("oauth2", {        confidence,        reason: `${fallbackCandidate.reason}; scheme is defined but not explicitly applied to operations`,        headerName: fallbackCandidate.headerName,        prefix: fallbackCandidate.prefix,        parameterName: fallbackCandidate.parameterName,        parameterLocation: fallbackCandidate.parameterLocation,        oauthAuthorizationUrl: fallbackCandidate.oauthAuthorizationUrl,        oauthTokenUrl: fallbackCandidate.oauthTokenUrl,        oauthScopes: fallbackCandidate.oauthScopes,      });    }     if (fallbackCandidate.kind === "bearer") {      return supportedAuthInference("bearer", {        confidence,        reason: `${fallbackCandidate.reason}; scheme is defined but not explicitly applied to operations`,        headerName: fallbackCandidate.headerName,        prefix: fallbackCandidate.prefix,        parameterName: fallbackCandidate.parameterName,        parameterLocation: fallbackCandidate.parameterLocation,        oauthAuthorizationUrl: fallbackCandidate.oauthAuthorizationUrl,        oauthTokenUrl: fallbackCandidate.oauthTokenUrl,        oauthScopes: fallbackCandidate.oauthScopes,      });    }     if (fallbackCandidate.kind === "apiKey") {      return unsupportedAuthInference("apiKey", {        confidence,        reason: `${fallbackCandidate.reason}; scheme is defined but not explicitly applied to operations`,        headerName: fallbackCandidate.headerName,        prefix: fallbackCandidate.prefix,        parameterName: fallbackCandidate.parameterName,        parameterLocation: fallbackCandidate.parameterLocation,        oauthAuthorizationUrl: fallbackCandidate.oauthAuthorizationUrl,        oauthTokenUrl: fallbackCandidate.oauthTokenUrl,

Lines 1–500 of 989.