970 lines · 40.1 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 {  convertGoogleDiscoveryBundleToOpenApi,  convertGoogleDiscoveryToOpenApi,  fetchGoogleDiscoveryDocument,  isGoogleDiscoveryUrl,} from "./google-discovery";import { extract } from "./extract";import { 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 {  compileOpenApiSpec,  invokeOpenApiBackedTool,  openApiStoredOperationsFromCompiled,  resolveOpenApiBackedAnnotations,  resolveOpenApiBackedTools,} from "./backing"; // ---------------------------------------------------------------------------// 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` / Google Discovery bundle URLs. */  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,  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 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,  })),}); const specInputToSourceUrl = (spec: OpenApiSpecInput): string | undefined =>  spec.kind === "url" || spec.kind === "googleDiscovery" ? spec.url : undefined; const specInputToGoogleBundle = (spec: OpenApiSpecInput): readonly string[] | undefined =>  spec.kind === "googleDiscoveryBundle" ? spec.urls : undefined; // ---------------------------------------------------------------------------// Declared auth methods — project the stored `authenticationTemplate` into the// catalog's plugin-agnostic `AuthMethodDescriptor[]`. This mirrors the client's// `authMethodsFromConfig` (in the React auth-method-config module) on the// server so the catalog field is consistent. apikey/none projection comes from// the shared model; the oauth method carries the stored endpoints + scopes.// --------------------------------------------------------------------------- export const describeOpenApiAuthMethods = (  record: IntegrationRecord,): readonly AuthMethodDescriptor[] => {  const config = decodeOpenApiIntegrationConfig(record.config);  if (!config) return [];  return (config.authenticationTemplate ?? []).map(    (template: Authentication): AuthMethodDescriptor => {      if (template.kind === "oauth2") {        return {          id: String(template.slug),          label: "OAuth2",          kind: "oauth",          template: String(template.slug),          oauth: {            authorizationUrl: template.authorizationUrl,            tokenUrl: template.tokenUrl,            scopes: template.scopes,          },        };      }      return describeApiKeyAuthMethod(template);    },  );}; export const describeOpenApiIntegrationDisplay = (  record: IntegrationRecord,): { readonly url?: string } => {  const config = decodeOpenApiIntegrationConfig(record.config);  return { url: config?.baseUrl ?? config?.sourceUrl };}; // ---------------------------------------------------------------------------// Plugin factory// --------------------------------------------------------------------------- export interface OpenApiPluginOptions {  readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;} const fetchGoogleDiscoveryBundleConversion = (  urls: readonly string[],  httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>,) =>  Effect.forEach(    urls,    (url) =>      fetchGoogleDiscoveryDocument(url).pipe(        Effect.provide(httpClientLayer),        Effect.map((documentText) => ({ discoveryUrl: url, documentText })),      ),    { concurrency: 4 },  ).pipe(Effect.flatMap((documents) => convertGoogleDiscoveryBundleToOpenApi({ documents }))); export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => {  const resolveSpecForInput = (    spec: OpenApiSpecInput,    httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>,  ): Effect.Effect<    {      readonly specText: string;      readonly baseUrl?: string;      // The Google Discovery converters derive the `googleOAuth2` oauth template      // straight from the spec's declared scopes. `addSpec` adopts it when the      // caller didn't pass an explicit `authenticationTemplate` (the bundle add      // path has no preview to detect auth from).      readonly authenticationTemplate?: readonly Authentication[];    },    OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError  > =>    Effect.gen(function* () {      if (spec.kind === "googleDiscovery") {        const conversion = yield* fetchGoogleDiscoveryDocument(spec.url).pipe(          Effect.provide(httpClientLayer),          Effect.flatMap((documentText) =>            convertGoogleDiscoveryToOpenApi({              discoveryUrl: spec.url,              documentText,            }),          ),        );        return {          specText: conversion.specText,          baseUrl: conversion.baseUrl,          ...(conversion.authenticationTemplate            ? { authenticationTemplate: conversion.authenticationTemplate }            : {}),        };      }      if (spec.kind === "googleDiscoveryBundle") {        const conversion = yield* fetchGoogleDiscoveryBundleConversion(spec.urls, httpClientLayer);        return {          specText: conversion.specText,          baseUrl: conversion.baseUrl,          ...(conversion.authenticationTemplate            ? { authenticationTemplate: conversion.authenticationTemplate }            : {}),        };      }      if (spec.kind === "url") {        const specText = yield* resolveSpecText(spec.url).pipe(Effect.provide(httpClientLayer));        return { specText };      }      return { specText: spec.value };    });   return {    id: "openapi" as const,    packageName: "@executor-js/plugin-openapi",    integrationPresets: openApiPresets.map((preset) => ({      id: preset.id,      name: preset.name,      summary: preset.summary,      ...(preset.url ? { url: preset.url } : {}),      ...(preset.icon ? { icon: preset.icon } : {}),      ...(preset.featured ? { featured: preset.featured } : {}),    })),    storage: (deps): OpenapiStore => makeDefaultOpenapiStore(deps),     extension: (ctx: PluginCtx<OpenapiStore>) => {      const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;       const addSpec = (config: OpenApiSpecConfig) =>        Effect.gen(function* () {          // Resolve URL → text and parse BEFORE opening a transaction. Holding          // `BEGIN` across a network fetch is the Hyperdrive deadlock path.          const resolved = yield* resolveSpecForInput(config.spec, httpClientLayer);          const compiled = yield* compileOpenApiSpec(resolved.specText);

Lines 1–500 of 970.