684 lines · 20.7 KB · TypeScript
RawDownload
import {  FetchHttpClient,  HttpClient,  HttpClientRequest,} from "@effect/platform";import {  applyCookiePlacementsToHeaders,  applyHttpQueryPlacementsToUrl,  applyJsonBodyPlacements,} from "@executor/codemode-core";import { buildOpenApiToolPresentation } from "./tool-presentation";import { compileOpenApiToolDefinitions } from "./definitions";import { detectOpenApiSource } from "./discovery";import {  httpBodyModeFromContentType,  serializeOpenApiParameterValue,  serializeOpenApiRequestBody,  withSerializedQueryEntries,} from "./http-serialization";import { OpenApiLocalConfigBindingSchema } from "./local-config";import {  OpenApiToolProviderDataSchema,  type OpenApiRefHintTable,  type OpenApiToolProviderData,} from "./types";import {  createOpenApiCatalogFragment,  type OpenApiCatalogOperationInput,} from "./catalog";import { extractOpenApiManifest } from "./extraction";import {  ConnectHttpAuthSchema,  ConnectHttpImportAuthSchema,  createSourceCatalogSyncResult,  decodeBindingConfig,  decodeExecutableBindingPayload,  decodeSourceBindingPayload,  emptySourceBindingState,  encodeBindingConfig,  isSourceCredentialRequiredError,  looksLikeGraphqlEndpoint,  OptionalNullableStringSchema,  SourceCredentialRequiredError,  SourceConnectCommonFieldsSchema,  StringMapSchema,  createCatalogImportMetadata,  EXECUTABLE_BINDING_VERSION,  sourceCoreEffectError,  type Source,  type SourceAdapter,} from "@executor/source-core";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema"; const OpenApiConnectPayloadSchema = Schema.extend(  SourceConnectCommonFieldsSchema,  Schema.extend(    ConnectHttpImportAuthSchema,    Schema.Struct({      kind: Schema.Literal("openapi"),      specUrl: Schema.Trim.pipe(Schema.nonEmptyString()),      auth: Schema.optional(ConnectHttpAuthSchema),    }),  ),); const OpenApiExecutorAddInputSchema = Schema.extend(  ConnectHttpImportAuthSchema,  Schema.Struct({    kind: Schema.Literal("openapi"),    endpoint: Schema.String,    specUrl: Schema.String,    name: OptionalNullableStringSchema,    namespace: OptionalNullableStringSchema,    auth: Schema.optional(ConnectHttpAuthSchema),  }),); const OpenApiBindingConfigSchema = Schema.Struct({  specUrl: Schema.Trim.pipe(Schema.nonEmptyString()),  defaultHeaders: Schema.optional(Schema.NullOr(StringMapSchema)),}); const OpenApiSourceBindingPayloadSchema = Schema.Struct({  specUrl: Schema.optional(Schema.String),  defaultHeaders: Schema.optional(Schema.NullOr(StringMapSchema)),}); type OpenApiBindingConfig = {  specUrl: string;  defaultHeaders: typeof StringMapSchema.Type | null;}; const OPENAPI_BINDING_CONFIG_VERSION = 1; const bindingHasAnyField = (  value: unknown,  fields: readonly string[],): boolean =>  value !== null &&  typeof value === "object" &&  !Array.isArray(value) &&  fields.some((field) => Object.prototype.hasOwnProperty.call(value, field)); const openApiBindingConfigFromSource = (  source: Pick<Source, "id" | "bindingVersion" | "binding">,): Effect.Effect<OpenApiBindingConfig, Error, never> =>  Effect.gen(function* () {    if (      bindingHasAnyField(source.binding, [        "transport",        "queryParams",        "headers",      ])    ) {      return yield* sourceCoreEffectError("openapi/adapter", "OpenAPI sources cannot define MCP transport settings");    }     const bindingConfig = yield* decodeSourceBindingPayload({      sourceId: source.id,      label: "OpenAPI",      version: source.bindingVersion,      expectedVersion: OPENAPI_BINDING_CONFIG_VERSION,      schema: OpenApiSourceBindingPayloadSchema,      value: source.binding,      allowedKeys: ["specUrl", "defaultHeaders"],    });     const specUrl =      typeof bindingConfig.specUrl === "string"        ? bindingConfig.specUrl.trim()        : "";    if (specUrl.length === 0) {      return yield* sourceCoreEffectError("openapi/adapter", "OpenAPI sources require specUrl");    }     return {      specUrl,      defaultHeaders: bindingConfig.defaultHeaders ?? null,    } satisfies OpenApiBindingConfig;  }); const fetchOpenApiDocumentWithHeaders = (input: {  url: string;  headers?: Readonly<Record<string, string>>;  queryParams?: Readonly<Record<string, string>>;  cookies?: Readonly<Record<string, string>>;}): Effect.Effect<string, Error, never> =>  Effect.gen(function* () {    const client = yield* HttpClient.HttpClient;    const request = HttpClientRequest.get(      applyHttpQueryPlacementsToUrl({        url: input.url,        queryParams: input.queryParams,      }).toString(),    ).pipe(      HttpClientRequest.setHeaders(        applyCookiePlacementsToHeaders({          headers: input.headers ?? {},          cookies: input.cookies,        }),      ),    );    const response = yield* client      .execute(request)      .pipe(        Effect.mapError((cause) =>          cause instanceof Error ? cause : new Error(String(cause)),        ),      );     if (response.status === 401 || response.status === 403) {      return yield* new SourceCredentialRequiredError(          "import",          `OpenAPI spec fetch requires credentials (status ${response.status})`,        );    }    if (response.status < 200 || response.status >= 300) {      return yield* sourceCoreEffectError("openapi/adapter", `OpenAPI spec fetch failed with status ${response.status}`);    }     return yield* response.text.pipe(      Effect.mapError((cause) =>        cause instanceof Error ? cause : new Error(String(cause)),      ),    );  }).pipe(Effect.provide(FetchHttpClient.layer)); const asRecord = (value: unknown): Record<string, unknown> =>  typeof value === "object" && value !== null && !Array.isArray(value)    ? (value as Record<string, unknown>)    : {}; type OpenApiToolArgs = Record<string, unknown>;type OpenApiToolParameter =  OpenApiToolProviderData["invocation"]["parameters"][number]; const parameterContainerKeys: Record<  OpenApiToolParameter["location"],  Array<string>> = {  path: ["path", "pathParams", "params"],  query: ["query", "queryParams", "params"],  header: ["headers", "header"],  cookie: ["cookies", "cookie"],}; const readParameterValue = (  args: OpenApiToolArgs,  parameter: OpenApiToolParameter,): unknown => {  const directValue = args[parameter.name];  if (directValue !== undefined) {    return directValue;  }   for (const key of parameterContainerKeys[parameter.location]) {    const container = args[key];    if (      typeof container !== "object" ||      container === null ||      Array.isArray(container)    ) {      continue;    }     const nestedValue = (container as Record<string, unknown>)[parameter.name];    if (nestedValue !== undefined) {      return nestedValue;    }  }   return undefined;}; const replacePathTemplate = (  pathTemplate: string,  args: OpenApiToolArgs,  payload: OpenApiToolProviderData["invocation"],): string => {  let resolvedPath = pathTemplate;   for (const parameter of payload.parameters) {    if (parameter.location !== "path") {      continue;    }     const parameterValue = readParameterValue(args, parameter);    if (parameterValue === undefined || parameterValue === null) {      if (parameter.required) {        throw new Error(`Missing required path parameter: ${parameter.name}`);      }      continue;    }     const serialized = serializeOpenApiParameterValue(      parameter,      parameterValue,    );    resolvedPath = resolvedPath.replaceAll(      `{${parameter.name}}`,      serialized.kind === "path"        ? serialized.value        : encodeURIComponent(String(parameterValue)),    );  }   const unresolvedPathParameters = [...resolvedPath.matchAll(/\{([^{}]+)\}/g)]    .map((match) => match[1])    .filter(      (value): value is string => typeof value === "string" && value.length > 0,    );   for (const parameterName of unresolvedPathParameters) {    const parameterValue =      args[parameterName] ??      (typeof args.path === "object" &&      args.path !== null &&      !Array.isArray(args.path)        ? (args.path as Record<string, unknown>)[parameterName]        : undefined) ??      (typeof args.pathParams === "object" &&      args.pathParams !== null &&      !Array.isArray(args.pathParams)        ? (args.pathParams as Record<string, unknown>)[parameterName]        : undefined) ??      (typeof args.params === "object" &&      args.params !== null &&      !Array.isArray(args.params)        ? (args.params as Record<string, unknown>)[parameterName]        : undefined);     if (parameterValue === undefined || parameterValue === null) {      continue;    }     resolvedPath = resolvedPath.replaceAll(      `{${parameterName}}`,      encodeURIComponent(String(parameterValue)),    );  }   const stillUnresolved = [...resolvedPath.matchAll(/\{([^{}]+)\}/g)]    .map((match) => match[1])    .filter(      (value): value is string => typeof value === "string" && value.length > 0,    );  if (stillUnresolved.length > 0) {    const names = [...new Set(stillUnresolved)].sort().join(", ");    throw new Error(`Unresolved path parameters after substitution: ${names}`);  }   return resolvedPath;}; const responseHeadersRecord = (response: Response): Record<string, string> => {  const headers: Record<string, string> = {};  response.headers.forEach((value, key) => {    headers[key] = value;  });  return headers;}; const decodeResponseBody = async (response: Response): Promise<unknown> => {  if (response.status === 204) {    return null;  }   const bodyMode = httpBodyModeFromContentType(    response.headers.get("content-type"),  );  if (bodyMode === "json") {    return response.json();  }  if (bodyMode === "bytes") {    return new Uint8Array(await response.arrayBuffer());  }   return response.text();}; const resolveOpenApiBaseUrl = (input: {  endpoint: string;  providerData: OpenApiToolProviderData;}): string => {  const server =    input.providerData.servers?.[0] ?? input.providerData.documentServers?.[0];  if (!server) {    return new URL(input.endpoint).toString();  }   const expanded = Object.entries(server.variables ?? {}).reduce(    (url, [name, value]) => url.replaceAll(`{${name}}`, value),    server.url,  );   return new URL(expanded, input.endpoint).toString();}; const resolveRequestUrl = (baseUrl: string, resolvedPath: string): URL => {  try {    return new URL(resolvedPath);  } catch {    const resolved = new URL(baseUrl);    const basePath =      resolved.pathname === "/"        ? ""        : resolved.pathname.endsWith("/")          ? resolved.pathname.slice(0, -1)          : resolved.pathname;    const pathPart = resolvedPath.startsWith("/")      ? resolvedPath      : `/${resolvedPath}`;     resolved.pathname = `${basePath}${pathPart}`.replace(/\/{2,}/g, "/");    resolved.search = "";    resolved.hash = "";    return resolved;  }}; const openApiCatalogOperationFromDefinition = (input: {  definition: ReturnType<typeof compileOpenApiToolDefinitions>[number];  refHintTable?: Readonly<OpenApiRefHintTable>;}): OpenApiCatalogOperationInput => {  const presentation = buildOpenApiToolPresentation({    definition: input.definition,    refHintTable: input.refHintTable,  });  const method = input.definition.method.toUpperCase();   return {    toolId: input.definition.toolId,    title: input.definition.name,    description: input.definition.description,    effect:      method === "GET" || method === "HEAD"        ? "read"        : method === "DELETE"          ? "delete"          : "write",    inputSchema: presentation.inputSchema,    outputSchema: presentation.outputSchema,    providerData: presentation.providerData as OpenApiToolProviderData,  };}; export const openApiSourceAdapter = {  key: "openapi",  displayName: "OpenAPI",  catalogKind: "imported",  connectStrategy: "direct",  credentialStrategy: "credential_managed",  bindingConfigVersion: OPENAPI_BINDING_CONFIG_VERSION,  providerKey: "generic_http",  defaultImportAuthPolicy: "reuse_runtime",  connectPayloadSchema: OpenApiConnectPayloadSchema,  executorAddInputSchema: OpenApiExecutorAddInputSchema,  executorAddHelpText: [    "endpoint is the base API URL. specUrl is the OpenAPI document URL.",  ],  executorAddInputSignatureWidth: 420,  localConfigBindingSchema: OpenApiLocalConfigBindingSchema,  localConfigBindingFromSource: (source) =>    Effect.runSync(      Effect.map(openApiBindingConfigFromSource(source), (bindingConfig) => ({        specUrl: bindingConfig.specUrl,        defaultHeaders: bindingConfig.defaultHeaders,      })),    ),  serializeBindingConfig: (source) =>    encodeBindingConfig({      adapterKey: "openapi",      version: OPENAPI_BINDING_CONFIG_VERSION,      payloadSchema: OpenApiBindingConfigSchema,      payload: Effect.runSync(openApiBindingConfigFromSource(source)),    }),  deserializeBindingConfig: ({ id, bindingConfigJson }) =>    Effect.map(      decodeBindingConfig({        sourceId: id,        label: "OpenAPI",        adapterKey: "openapi",        version: OPENAPI_BINDING_CONFIG_VERSION,        payloadSchema: OpenApiBindingConfigSchema,        value: bindingConfigJson,      }),      ({ version, payload }) => ({        version,        payload: {          specUrl: payload.specUrl,          defaultHeaders: payload.defaultHeaders ?? null,        },      }),    ),  bindingStateFromSource: (source) =>    Effect.map(openApiBindingConfigFromSource(source), (bindingConfig) => ({      ...emptySourceBindingState,      specUrl: bindingConfig.specUrl,      defaultHeaders: bindingConfig.defaultHeaders,    })),  sourceConfigFromSource: (source) =>    Effect.runSync(      Effect.map(openApiBindingConfigFromSource(source), (bindingConfig) => ({        kind: "openapi",        endpoint: source.endpoint,        specUrl: bindingConfig.specUrl,        defaultHeaders: bindingConfig.defaultHeaders,      })),    ),  validateSource: (source) =>    Effect.gen(function* () {      const bindingConfig = yield* openApiBindingConfigFromSource(source);      return {        ...source,        bindingVersion: OPENAPI_BINDING_CONFIG_VERSION,        binding: {          specUrl: bindingConfig.specUrl,          defaultHeaders: bindingConfig.defaultHeaders,        },      };    }),  shouldAutoProbe: (source) =>    source.enabled &&    (source.status === "draft" || source.status === "probing"),  discoveryPriority: ({ normalizedUrl }) =>    looksLikeGraphqlEndpoint(normalizedUrl) ? 50 : 250,  detectSource: ({ normalizedUrl, headers }) =>    detectOpenApiSource({      normalizedUrl,      headers,    }),  syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>    Effect.gen(function* () {      const bindingConfig = yield* openApiBindingConfigFromSource(source);       const auth = yield* resolveAuthMaterialForSlot("import");      const openApiDocument = yield* fetchOpenApiDocumentWithHeaders({        url: bindingConfig.specUrl,        headers: {

Lines 1–500 of 684.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t