1104 lines · 44.4 KB · TypeScript
RawDownload
import { Effect, Option, Schema } from "effect";import type { Layer } from "effect";import { HttpClient } from "effect/unstable/http"; import {  IntegrationAlreadyExistsError,  IntegrationDetectionResult,  IntegrationNotFoundError,  IntegrationSlug,  ToolResult,  definePlugin,  mergeAuthTemplates,  sha256Hex,  tool,  type AuthMethodDescriptor,  type Integration,  type IntegrationConfig,  type IntegrationRecord,  type PluginCtx,  type StorageFailure,} from "@executor-js/sdk/core"; import { decodeOpenApiIntegrationConfig, type OpenApiIntegrationConfig } from "./config";import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError } from "./errors";import { parse, resolveSpecText } from "./parse";import { extract } from "./extract";import {  OAuth2AuthorizationCodeFlow,  OAuth2Flows,  OAuth2Preset,  SecurityScheme,  previewSpecText,  type SpecPreview,} from "./preview";import { deriveAuthenticationTemplateFromPreview, firstBaseUrlForPreview } from "./derive-auth";import { openApiPresets } from "./presets";import { makeDefaultOpenapiStore, type OpenapiStore } from "./store";import type { Authentication } from "./types";import { normalizeOpenApiAuthInputs, type AuthenticationInput } from "./types";import { ApiKeyAuthTemplate, describeApiKeyAuthMethod } from "@executor-js/sdk/http-auth";import {  checkHealthOpenApi,  compileOpenApiSpec,  invokeOpenApiBackedTool,  listHealthCheckCandidatesOpenApi,  openApiStoredOperationsFromCompiled,  resolveOpenApiBackedAnnotations,  resolveOpenApiBackedTools,  validateOpenApiBackedToolArgs,} from "./backing";import { resolveServerUrl } from "./openapi-utils"; // ---------------------------------------------------------------------------// 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;  /** Display name (defaults to the spec title). */  readonly name?: string;  /** Agent-visible description (defaults to the spec's `info.description`,   *  then the 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";} /** What changed in the tool catalog when a spec was updated in place. Tool *  names, not addresses - the same diff applies to every connection. */export interface UpdateSpecResult {  readonly slug: IntegrationSlug;  readonly toolCount: number;  readonly addedTools: readonly string[];  readonly removedTools: readonly string[];} export interface OpenApiUpdateSpecInput {  /** New spec source. Omit to re-fetch from the integration's stored   *  `sourceUrl`. */  readonly spec?: OpenApiSpecInput;} 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  >;  /** Re-resolve the integration's spec (from its stored source URL, or the   *  provided input) and rebuild its tools IN PLACE - connections,   *  credentials, policies, and the curated description are untouched. */  readonly updateSpec: (    slug: string,    input?: OpenApiUpdateSpecInput,  ) => Effect.Effect<    UpdateSpecResult,    | OpenApiParseError    | OpenApiExtractionError    | OpenApiOAuthError    | IntegrationNotFoundError    | 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,  resource: Schema.NullOr(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),  ]),  supportsClientIdMetadataDocument: Schema.optional(Schema.Boolean),});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 }),]); const AuthenticationSchema = Schema.Union([  Schema.Struct({    slug: Schema.String,    kind: Schema.Literal("oauth2"),    authorizationUrl: Schema.String,    tokenUrl: Schema.String,    resource: Schema.optional(Schema.NullOr(Schema.String)),    scopes: Schema.Array(Schema.String),    supportsClientIdMetadataDocument: Schema.optional(Schema.Boolean),  }),  // 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 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,    resource: Option.getOrNull(preset.resource),    refreshUrl: Option.getOrNull(preset.refreshUrl),    scopes: preset.scopes,    identityScopes: preset.identityScopes,    supportsClientIdMetadataDocument: preset.supportsClientIdMetadataDocument,  })),}); const specInputToSourceUrl = (spec: OpenApiSpecInput): string | undefined =>  spec.kind === "url" ? spec.url : undefined; const OAUTH_DISCOVERED_SCHEME_NAME = "DiscoveredOAuth2";const OPENAPI_HTTP_METHODS = new Set([  "get",  "put",  "post",  "delete",  "patch",  "head",  "options",  "trace",]); const isRecord = (value: unknown): value is Record<string, unknown> =>  typeof value === "object" && value !== null && !Array.isArray(value); const maybeUrl = (value: string): URL | null => {  // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL parsing accepts user-pasted spec/base URLs  try {    return new URL(value);  } catch {    return null;  }}; const addProbeCandidate = (candidates: string[], value: string | undefined): void => {  const trimmed = value?.trim();  if (!trimmed) return;  const parsed = maybeUrl(trimmed);  if (!parsed || (parsed.protocol !== "https:" && parsed.protocol !== "http:")) return;  const normalized = parsed.toString();  const origin = parsed.origin;  if (!candidates.includes(normalized)) candidates.push(normalized);  if (!candidates.includes(origin)) candidates.push(origin);}; const oauthProbeCandidates = (  preview: SpecPreview,  sourceUrl: string | undefined,  baseUrl: string | undefined,): readonly string[] => {  const candidates: string[] = [];  addProbeCandidate(candidates, baseUrl);  for (const server of preview.servers) {    addProbeCandidate(      candidates,      resolveServerUrl(server.url, Option.getOrUndefined(server.variables), {}),    );  }  addProbeCandidate(candidates, sourceUrl);  return candidates;}; const securityRequirementScopes = (  security: unknown,  targetSchemes: ReadonlySet<string>,): readonly string[] => {  if (!Array.isArray(security)) return [];  const scopes = new Set<string>();  for (const requirement of security) {    if (!isRecord(requirement)) continue;    for (const [scheme, rawScopes] of Object.entries(requirement)) {      if (targetSchemes.size > 0 && !targetSchemes.has(scheme)) continue;      if (!Array.isArray(rawScopes)) continue;      for (const scope of rawScopes) {        if (typeof scope === "string" && scope.trim().length > 0) scopes.add(scope.trim());      }    }  }  return [...scopes];}; const collectDeclaredSecurityScopes = (doc: unknown, targetSchemes: ReadonlySet<string>) => {  const scopes = new Set<string>();  if (!isRecord(doc)) return [] as readonly string[];   for (const scope of securityRequirementScopes(doc.security, targetSchemes)) scopes.add(scope);   const paths = doc.paths;  if (!isRecord(paths)) return [...scopes].sort();  for (const pathItem of Object.values(paths)) {    if (!isRecord(pathItem)) continue;    for (const [method, operation] of Object.entries(pathItem)) {      if (!OPENAPI_HTTP_METHODS.has(method.toLowerCase()) || !isRecord(operation)) continue;      for (const scope of securityRequirementScopes(operation.security, targetSchemes)) {        scopes.add(scope);      }    }  }  return [...scopes].sort();}; const nonOAuthSecuritySchemeNames = (preview: SpecPreview): ReadonlySet<string> =>  new Set(    preview.securitySchemes      .filter((scheme) => scheme.type === "http" || scheme.type === "apiKey")      .map((scheme) => scheme.name),  ); const discoveredOAuthPreview = (input: {  readonly preview: SpecPreview;  readonly authorizationUrl: string;  readonly tokenUrl: string;  readonly resource?: string | null;  readonly scopes: readonly string[];  readonly supportsClientIdMetadataDocument?: boolean;}): SpecPreview => {  const scopes = Object.fromEntries(input.scopes.map((scope) => [scope, ""]));  const flow = OAuth2AuthorizationCodeFlow.make({    authorizationUrl: input.authorizationUrl,    tokenUrl: input.tokenUrl,    refreshUrl: Option.none(),    scopes,  });  const flows = OAuth2Flows.make({    authorizationCode: Option.some(flow),    clientCredentials: Option.none(),  });  return {    ...input.preview,    securitySchemes: [      ...input.preview.securitySchemes,      SecurityScheme.make({        name: OAUTH_DISCOVERED_SCHEME_NAME,        type: "oauth2",        scheme: Option.none(),        bearerFormat: Option.none(),        in: Option.none(),        headerName: Option.none(),        description: Option.some("Discovered from OAuth authorization-server metadata"),        flows: Option.some(flows),        openIdConnectUrl: Option.none(),      }),    ],    oauth2Presets: [      ...input.preview.oauth2Presets,      OAuth2Preset.make({

Lines 1–500 of 1104.