1214 lines · 49.1 KB · TypeScript
RawDownload
import { Effect, Option, Schema } from "effect";import type { Layer } from "effect";import { HttpClient } from "effect/unstable/http"; import {  IntegrationAlreadyExistsError,  IntegrationDetectionResult,  IntegrationSlug,  ToolName,  ToolResult,  authToolFailure,  definePlugin,  mergeAuthTemplates,  sha256Hex,  tool,  type AuthMethodDescriptor,  type Integration,  type IntegrationConfig,  type IntegrationRecord,  type PluginCtx,  type ResolveToolsResult,  type StorageFailure,  type ToolDef,  type ToolInvocationCredential,} from "@executor-js/sdk/core"; import {  decodeOpenApiIntegrationConfig,  renderAuthTemplate,  requiredTemplateVariables,  type OpenApiIntegrationConfig,} from "./config";import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError } from "./errors";import { parse, resolveSpecText } from "./parse";import {  convertGoogleDiscoveryBundleToOpenApi,  convertGoogleDiscoveryToOpenApi,  fetchGoogleDiscoveryDocument,  isGoogleDiscoveryUrl,} from "./google-discovery";import { extract } from "./extract";import { compileToolDefinitions, type ToolDefinition } from "./definitions";import { annotationsForOperation, invokeWithLayer } from "./invoke";import { previewSpec, previewSpecText, type SpecPreview } from "./preview";import { deriveAuthenticationTemplateFromPreview, firstBaseUrlForPreview } from "./derive-auth";import { openApiPresets } from "./presets";import { makeDefaultOpenapiStore, type OpenapiStore, type StoredOperation } from "./store";import type { Authentication } from "./types";import { OperationBinding, normalizeOpenApiAuthInputs, type AuthenticationInput } from "./types";import { ApiKeyAuthTemplate, describeApiKeyAuthMethod } from "@executor-js/sdk/http-auth"; // ---------------------------------------------------------------------------// Plugin config// --------------------------------------------------------------------------- const STRINGIFIED_BODY_CAP = 1024;const UpstreamMessageBody = Schema.Struct({ message: Schema.String });const UpstreamErrorMessageBody = Schema.Struct({ errorMessage: Schema.String });const UpstreamNestedErrorBody = Schema.Struct({ error: UpstreamMessageBody });const UpstreamErrorsArrayBody = Schema.Struct({  errors: Schema.Array(    Schema.Struct({      detail: Schema.optional(Schema.String),      message: Schema.optional(Schema.String),      title: Schema.optional(Schema.String),    }),  ),});const UpstreamDescriptionBody = Schema.Struct({  detail: Schema.optional(Schema.String),  title: Schema.optional(Schema.String),  description: Schema.optional(Schema.String),}); const decodeUpstreamMessageBody = Schema.decodeUnknownOption(UpstreamMessageBody);const decodeUpstreamErrorMessageBody = Schema.decodeUnknownOption(UpstreamErrorMessageBody);const decodeUpstreamNestedErrorBody = Schema.decodeUnknownOption(UpstreamNestedErrorBody);const decodeUpstreamErrorsArrayBody = Schema.decodeUnknownOption(UpstreamErrorsArrayBody);const decodeUpstreamDescriptionBody = Schema.decodeUnknownOption(UpstreamDescriptionBody); const clampedStringify = (value: unknown): string => {  let s: string;  // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: JSON.stringify may throw on cycles; fall back to String() so the upstream body can still be surfaced as ToolError.details fallback text  try {    s = JSON.stringify(value);  } catch {    s = String(value);  }  return s.length > STRINGIFIED_BODY_CAP ? `${s.slice(0, STRINGIFIED_BODY_CAP)}…` : s;}; const firstNonEmpty = (...values: readonly (string | undefined)[]): string | undefined =>  values.find((value) => value !== undefined && value.length > 0); // Walk known upstream error-body shapes so ToolError.message stays concise// while ToolError.details preserves the original body.const extractUpstreamMessage = (body: unknown, status: number): string => {  if (typeof body === "string") {    return body.length > 0 ? body : `Upstream returned HTTP ${status}`;  }  const nested = Option.getOrUndefined(decodeUpstreamNestedErrorBody(body));  const messageBody = Option.getOrUndefined(decodeUpstreamMessageBody(body));  const errorMessageBody = Option.getOrUndefined(decodeUpstreamErrorMessageBody(body));  const errorsBody = Option.getOrUndefined(decodeUpstreamErrorsArrayBody(body));  const descriptionBody = Option.getOrUndefined(decodeUpstreamDescriptionBody(body));  const arrayMessage = errorsBody?.errors    .map(      ({        detail,        message: upstreamMessage,        title,      }: {        detail?: string;        message?: string;        title?: string;      }) => firstNonEmpty(detail, upstreamMessage, title),    )    .find((message: string | undefined) => message !== undefined);  const message = firstNonEmpty(    nested?.error.message,    messageBody?.message,    errorMessageBody?.errorMessage,    arrayMessage,    descriptionBody?.detail,    descriptionBody?.title,    descriptionBody?.description,  );  if (message !== undefined) return message;  if (body !== null && typeof body === "object") {    return clampedStringify(body);  }  return `Upstream returned HTTP ${status}`;}; // ---------------------------------------------------------------------------// Extension input shapes// --------------------------------------------------------------------------- export type OpenApiSpecInput = typeof OpenApiSpecInputSchema.Type; export interface OpenApiPreviewInput {  readonly spec: string;} /** Add an OpenAPI integration to the catalog. The integration is the API *  surface; connections (the credentials) are attached separately and resolve *  their value through the declared `authenticationTemplate`. */export interface OpenApiSpecConfig {  readonly spec: OpenApiSpecInput;  /** The catalog slug for the new integration (the `<integration>` segment). */  readonly slug: string;  /** Human description (defaults to the spec title). */  readonly description?: string;  readonly baseUrl?: string;  /** Static headers applied to every request (no secret material). */  readonly headers?: Record<string, string>;  /** Static query params applied to every request. */  readonly queryParams?: Record<string, string>;  /** Auth methods a connection's value renders through — canonical   *  placements or the request-shaped authoring dialect. */  readonly authenticationTemplate?: readonly AuthenticationInput[];} export interface OpenApiExtensionFailure {  readonly _tag: string;} /** Add / merge custom auth methods onto an existing OpenAPI integration's *  `authenticationTemplate`. Mirrors the GraphQL plugin's `configure`. */export interface OpenApiConfigureInput {  /** The auth methods to add. Each entry is appended to (or, when its `slug`   *  already exists, replaces) the integration's existing template array. A   *  custom apiKey method with no `slug` is assigned a generated `custom_<id>`   *  slug that is collision-checked against the existing template. */  readonly authenticationTemplate: readonly AuthenticationInput[];  readonly mode?: "merge" | "replace";} export interface OpenApiPluginExtension {  readonly previewSpec: (    input: string | OpenApiPreviewInput,  ) => Effect.Effect<    SpecPreview,    OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure  >;  readonly addSpec: (    config: OpenApiSpecConfig,  ) => Effect.Effect<    { readonly slug: IntegrationSlug; readonly toolCount: number },    | OpenApiParseError    | OpenApiExtractionError    | OpenApiOAuthError    | IntegrationAlreadyExistsError    | StorageFailure  >;  readonly removeSpec: (slug: string) => Effect.Effect<void, StorageFailure>;  readonly getIntegration: (slug: string) => Effect.Effect<Integration | null, StorageFailure>;  /** Read the integration's full opaque config, including its   *  `authenticationTemplate`. Returns null when the integration is absent. */  readonly getConfig: (    slug: string,  ) => Effect.Effect<OpenApiIntegrationConfig | null, StorageFailure>;  /** Add / merge custom auth methods onto the integration's   *  `authenticationTemplate`. Returns the resulting template array. */  readonly configure: (    slug: string,    input: OpenApiConfigureInput,  ) => Effect.Effect<readonly Authentication[], StorageFailure>;} // ---------------------------------------------------------------------------// Control-tool input/output schemas// --------------------------------------------------------------------------- const PreviewSpecInputSchema = Schema.Struct({  spec: Schema.String,}); const StaticPreviewServerVariableSchema = Schema.Struct({  default: Schema.String,  enum: Schema.NullOr(Schema.Array(Schema.String)),  description: Schema.NullOr(Schema.String),});const StaticPreviewServerSchema = Schema.Struct({  url: Schema.String,  description: Schema.NullOr(Schema.String),  variables: Schema.NullOr(Schema.Record(Schema.String, StaticPreviewServerVariableSchema)),});const StaticPreviewOAuthAuthorizationCodeFlowSchema = Schema.Struct({  authorizationUrl: Schema.String,  tokenUrl: Schema.String,  refreshUrl: Schema.NullOr(Schema.String),  scopes: Schema.Record(Schema.String, Schema.String),});const StaticPreviewOAuthClientCredentialsFlowSchema = Schema.Struct({  tokenUrl: Schema.String,  refreshUrl: Schema.NullOr(Schema.String),  scopes: Schema.Record(Schema.String, Schema.String),});const StaticPreviewOAuthFlowsSchema = Schema.Struct({  authorizationCode: Schema.NullOr(StaticPreviewOAuthAuthorizationCodeFlowSchema),  clientCredentials: Schema.NullOr(StaticPreviewOAuthClientCredentialsFlowSchema),});const StaticPreviewSecuritySchemeSchema = Schema.Struct({  name: Schema.String,  type: Schema.Literals(["http", "apiKey", "oauth2", "openIdConnect"]),  scheme: Schema.NullOr(Schema.String),  bearerFormat: Schema.NullOr(Schema.String),  in: Schema.NullOr(Schema.Literals(["header", "query", "cookie"])),  headerName: Schema.NullOr(Schema.String),  description: Schema.NullOr(Schema.String),  flows: Schema.NullOr(StaticPreviewOAuthFlowsSchema),  openIdConnectUrl: Schema.NullOr(Schema.String),});const StaticPreviewOAuth2PresetSchema = Schema.Struct({  label: Schema.String,  securitySchemeName: Schema.String,  flow: Schema.Literals(["authorizationCode", "clientCredentials"]),  authorizationUrl: Schema.NullOr(Schema.String),  tokenUrl: Schema.String,  refreshUrl: Schema.NullOr(Schema.String),  scopes: Schema.Record(Schema.String, Schema.String),  identityScopes: Schema.Union([    Schema.Literal("auto"),    Schema.Literal(false),    Schema.Array(Schema.String),  ]),});const StaticPreviewSpecOutputSchema = Schema.Struct({  title: Schema.NullOr(Schema.String),  version: Schema.NullOr(Schema.String),  servers: Schema.Array(StaticPreviewServerSchema),  operationCount: Schema.Number,  tags: Schema.Array(Schema.String),  securitySchemes: Schema.Array(StaticPreviewSecuritySchemeSchema),  authStrategies: Schema.Array(Schema.Struct({ schemes: Schema.Array(Schema.String) })),  headerPresets: Schema.Array(    Schema.Struct({      label: Schema.String,      headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)),      secretHeaders: Schema.Array(Schema.String),    }),  ),  oauth2Presets: Schema.Array(StaticPreviewOAuth2PresetSchema),});type StaticPreviewSpecOutput = typeof StaticPreviewSpecOutputSchema.Type; const OpenApiSpecInputSchema = Schema.Union([  Schema.Struct({ kind: Schema.Literal("url"), url: Schema.String }),  Schema.Struct({ kind: Schema.Literal("blob"), value: Schema.String }),  Schema.Struct({    kind: Schema.Literal("googleDiscovery"),    url: Schema.String,  }),  Schema.Struct({    kind: Schema.Literal("googleDiscoveryBundle"),    urls: Schema.Array(Schema.String),  }),]); const AuthenticationSchema = Schema.Union([  Schema.Struct({    slug: Schema.String,    kind: Schema.Literal("oauth2"),    authorizationUrl: Schema.String,    tokenUrl: Schema.String,    scopes: Schema.Array(Schema.String),  }),  // Credential methods are authored request-shaped — the ONE apikey input  // dialect: `{ type: "apiKey", headers: { Authorization: ["Bearer ",  // variable("token")] }, queryParams: { … } }`.  ApiKeyAuthTemplate,]); const AddSourceInputSchema = Schema.Struct({  spec: OpenApiSpecInputSchema,  slug: Schema.String,  description: Schema.optional(Schema.String),  baseUrl: Schema.optional(Schema.String),  headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),  queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),  authenticationTemplate: Schema.optional(Schema.Array(AuthenticationSchema)),}); const AddSourceOutputSchema = Schema.Struct({  slug: Schema.String,  toolCount: Schema.Number,}); const PreviewSpecInputStandardSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(PreviewSpecInputSchema),);const PreviewSpecOutputStandardSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(StaticPreviewSpecOutputSchema),);const AddSourceInputStandardSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(AddSourceInputSchema),);const AddSourceOutputStandardSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(AddSourceOutputSchema),); const openApiToolFailure = (code: string, message: string, details?: unknown) =>  ToolResult.fail({    code,    message,    ...(details === undefined ? {} : { details }),  }); const openApiAuthToolFailure = (failure: {  readonly code: string;  readonly message: string;  readonly owner: "org" | "user";  readonly integration: string;  readonly connection: string;  readonly credentialKind: "secret" | "oauth" | "upstream";  readonly credentialLabel?: string;  readonly status?: number;  readonly details?: unknown;}) =>  authToolFailure({    // The auth-tool-failure helper's code set is shared with v1; keep the    // string but reference the connection rather than v1's source/slot.    code: failure.code as Parameters<typeof authToolFailure>[0]["code"],    message: failure.message,    source: { id: failure.integration, scope: failure.owner },    credential: {      kind: failure.credentialKind,      ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}),    },    ...(failure.status !== undefined ? { status: failure.status } : {}),    ...(failure.details !== undefined      ? {          upstream: {            ...(failure.status !== undefined ? { status: failure.status } : {}),            details: failure.details,          },        }      : {}),  }); const staticPreviewOutput = (preview: SpecPreview): StaticPreviewSpecOutput => ({  title: Option.getOrNull(preview.title),  version: Option.getOrNull(preview.version),  servers: preview.servers.map((server) => ({    url: server.url,    description: Option.getOrNull(server.description),    variables: Option.getOrNull(server.variables)      ? Object.fromEntries(          Object.entries(Option.getOrNull(server.variables) ?? {}).map(([name, variable]) => [            name,            {              default: variable.default,              enum: Option.getOrNull(variable.enum),              description: Option.getOrNull(variable.description),            },          ]),        )      : null,  })),  operationCount: preview.operationCount,  tags: preview.tags,  securitySchemes: preview.securitySchemes.map((scheme) => ({    name: scheme.name,    type: scheme.type,    scheme: Option.getOrNull(scheme.scheme),    bearerFormat: Option.getOrNull(scheme.bearerFormat),    in: Option.getOrNull(scheme.in),    headerName: Option.getOrNull(scheme.headerName),    description: Option.getOrNull(scheme.description),    flows: Option.isSome(scheme.flows)      ? {          authorizationCode: Option.isSome(scheme.flows.value.authorizationCode)            ? {                authorizationUrl: scheme.flows.value.authorizationCode.value.authorizationUrl,                tokenUrl: scheme.flows.value.authorizationCode.value.tokenUrl,                refreshUrl: Option.getOrNull(scheme.flows.value.authorizationCode.value.refreshUrl),                scopes: scheme.flows.value.authorizationCode.value.scopes,              }            : null,          clientCredentials: Option.isSome(scheme.flows.value.clientCredentials)            ? {                tokenUrl: scheme.flows.value.clientCredentials.value.tokenUrl,                refreshUrl: Option.getOrNull(scheme.flows.value.clientCredentials.value.refreshUrl),                scopes: scheme.flows.value.clientCredentials.value.scopes,              }            : null,        }      : null,    openIdConnectUrl: Option.getOrNull(scheme.openIdConnectUrl),  })),  authStrategies: preview.authStrategies,  headerPresets: preview.headerPresets,  oauth2Presets: preview.oauth2Presets.map((preset) => ({    label: preset.label,    securitySchemeName: preset.securitySchemeName,    flow: preset.flow,    authorizationUrl: Option.getOrNull(preset.authorizationUrl),    tokenUrl: preset.tokenUrl,    refreshUrl: Option.getOrNull(preset.refreshUrl),    scopes: preset.scopes,    identityScopes: preset.identityScopes,  })),}); // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- /** Rewrite OpenAPI `#/components/schemas/X` refs to standard `#/$defs/X`. */const normalizeOpenApiRefs = (node: unknown): unknown => {  if (node == null || typeof node !== "object") return node;  if (Array.isArray(node)) {    let changed = false;    const out = node.map((item) => {      const n = normalizeOpenApiRefs(item);      if (n !== item) changed = true;      return n;    });    return changed ? out : node;  }   const obj = node as Record<string, unknown>;   if (typeof obj.$ref === "string") {    const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/);    if (match) return { ...obj, $ref: `#/$defs/${match[1]}` };    return obj;  }   let changed = false;  const result: Record<string, unknown> = {};  for (const [k, v] of Object.entries(obj)) {    const n = normalizeOpenApiRefs(v);    if (n !== v) changed = true;    result[k] = n;  }  return changed ? result : obj;}; const toBinding = (def: ToolDefinition): OperationBinding =>  OperationBinding.make({    method: def.operation.method,    servers: def.operation.servers,    pathTemplate: def.operation.pathTemplate,    parameters: [...def.operation.parameters],    requestBody: def.operation.requestBody,  }); const descriptionFor = (def: ToolDefinition): string => {  const op = def.operation;  return Option.getOrElse(op.description, () =>    Option.getOrElse(op.summary, () => `${op.method.toUpperCase()} ${op.pathTemplate}`),  );}; const specInputToSourceUrl = (spec: OpenApiSpecInput): string | undefined =>  spec.kind === "url" || spec.kind === "googleDiscovery" ? spec.url : undefined; const specInputToGoogleBundle = (spec: OpenApiSpecInput): readonly string[] | undefined =>

Lines 1–500 of 1214.