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 { 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`. */ 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 }),]); 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.url : 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>;} export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { const resolveSpecForInput = ( spec: OpenApiSpecInput, httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>, ): Effect.Effect< { readonly specText: string; }, OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError > => Effect.gen(function* () { 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); // Defaults the add page derives from its preview, applied here so // headless callers (MCP, API) get the same integration the UI's // add flow would produce - see e2e/scenarios/connect-handoff.test.ts: // - effectiveBaseUrl: the spec's first server, used to anchor the // derived auth template's absolute URLs. It is NOT stored as the // connection baseUrl - the request host is resolved per call from // the operation's extracted `servers`. // - authenticationTemplate: the spec's declared security schemes // (else the Add-connection modal is a dead "No authentication" // end with nowhere to paste a credential) // An explicit input always wins; for auth, an explicit EMPTY array // means "no auth methods" and suppresses the derivation. const explicitBaseUrl = config.baseUrl; const needsDerivedBaseUrl = explicitBaseUrl == null; const needsDerivedAuth = config.authenticationTemplate == null; const preview = needsDerivedBaseUrl || needsDerivedAuth ? yield* previewSpecText(resolved.specText) : undefined; const derivedBaseUrl = needsDerivedBaseUrl && preview ? firstBaseUrlForPreview(preview) : undefined; const effectiveBaseUrl = explicitBaseUrl ?? (derivedBaseUrl || undefined); const derivedAuthenticationTemplate = needsDerivedAuth && preview ? deriveAuthenticationTemplateFromPreview(preview, effectiveBaseUrl) : undefined; const slug = IntegrationSlug.make(config.slug); // Block re-adding an existing slug. The core `integrations.register` // primitive upserts (so boot re-registration is idempotent), but an // explicit add must NOT silently clobber an existing integration's // tools, connections, and policies. To add more auth, update the // existing integration instead. const existing = yield* ctx.core.integrations.get(slug); if (existing) { return yield* new IntegrationAlreadyExistsError({ slug }); } const specHash = yield* sha256Hex(resolved.specText); const integrationConfig: OpenApiIntegrationConfig = { specHash, ...(specInputToSourceUrl(config.spec) !== undefined ? { sourceUrl: specInputToSourceUrl(config.spec) } : {}), // baseUrl is an optional override only. The host is otherwise // resolved per call from the operation's `servers` (extracted from // the spec), so we never bake a derived base URL into the config. ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}), ...(config.headers ? { headers: config.headers } : {}), ...(config.queryParams ? { queryParams: config.queryParams } : {}), // Prefer the caller's explicit template; otherwise derive from the // spec's declared security schemes. ...(config.authenticationTemplate ? { authenticationTemplate: normalizeOpenApiAuthInputs(config.authenticationTemplate), } : derivedAuthenticationTemplate && derivedAuthenticationTemplate.length > 0 ? { authenticationTemplate: derivedAuthenticationTemplate } : {}), }; // The spec blob is written OUTSIDE the transaction: it'sLines 1–500 of 857. Load more
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 { 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`. */ 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 }), ]); 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.url : 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>; } export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { const resolveSpecForInput = ( spec: OpenApiSpecInput, httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>, ): Effect.Effect< { readonly specText: string; }, OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError > => Effect.gen(function* () { 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); // Defaults the add page derives from its preview, applied here so // headless callers (MCP, API) get the same integration the UI's // add flow would produce - see e2e/scenarios/connect-handoff.test.ts: // - effectiveBaseUrl: the spec's first server, used to anchor the // derived auth template's absolute URLs. It is NOT stored as the // connection baseUrl - the request host is resolved per call from // the operation's extracted `servers`. // - authenticationTemplate: the spec's declared security schemes // (else the Add-connection modal is a dead "No authentication" // end with nowhere to paste a credential) // An explicit input always wins; for auth, an explicit EMPTY array // means "no auth methods" and suppresses the derivation. const explicitBaseUrl = config.baseUrl; const needsDerivedBaseUrl = explicitBaseUrl == null; const needsDerivedAuth = config.authenticationTemplate == null; const preview = needsDerivedBaseUrl || needsDerivedAuth ? yield* previewSpecText(resolved.specText) : undefined; const derivedBaseUrl = needsDerivedBaseUrl && preview ? firstBaseUrlForPreview(preview) : undefined; const effectiveBaseUrl = explicitBaseUrl ?? (derivedBaseUrl || undefined); const derivedAuthenticationTemplate = needsDerivedAuth && preview ? deriveAuthenticationTemplateFromPreview(preview, effectiveBaseUrl) : undefined; const slug = IntegrationSlug.make(config.slug); // Block re-adding an existing slug. The core `integrations.register` // primitive upserts (so boot re-registration is idempotent), but an // explicit add must NOT silently clobber an existing integration's // tools, connections, and policies. To add more auth, update the // existing integration instead. const existing = yield* ctx.core.integrations.get(slug); if (existing) { return yield* new IntegrationAlreadyExistsError({ slug }); } const specHash = yield* sha256Hex(resolved.specText); const integrationConfig: OpenApiIntegrationConfig = { specHash, ...(specInputToSourceUrl(config.spec) !== undefined ? { sourceUrl: specInputToSourceUrl(config.spec) } : {}), // baseUrl is an optional override only. The host is otherwise // resolved per call from the operation's `servers` (extracted from // the spec), so we never bake a derived base URL into the config. ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}), ...(config.headers ? { headers: config.headers } : {}), ...(config.queryParams ? { queryParams: config.queryParams } : {}), // Prefer the caller's explicit template; otherwise derive from the // spec's declared security schemes. ...(config.authenticationTemplate ? { authenticationTemplate: normalizeOpenApiAuthInputs(config.authenticationTemplate), } : derivedAuthenticationTemplate && derivedAuthenticationTemplate.length > 0 ? { authenticationTemplate: derivedAuthenticationTemplate } : {}), }; // The spec blob is written OUTSIDE the transaction: it's