import { Effect, Match, Option, Schema } from "effect";import type { Layer } from "effect";import { FetchHttpClient, HttpClient } from "effect/unstable/http"; import { authToolFailure, AuthTemplateSlug, definePlugin, IntegrationAlreadyExistsError, IntegrationDetectionResult, IntegrationSlug, mergeAuthTemplates, ToolName, ToolResult, type AuthMethodDescriptor, type AuthPlacementDescriptor, type IntegrationConfig, type IntegrationRecord, type PluginCtx, type StorageFailure, type ToolAnnotations, type ToolDef,} from "@executor-js/sdk/core"; import { introspect, parseIntrospectionJson, type IntrospectionResult, type IntrospectionType, type IntrospectionField, type IntrospectionTypeRef,} from "./introspect";import { extract } from "./extract";import { GraphqlAuthRequiredError, GraphqlIntrospectionError, GraphqlInvocationError,} from "./errors";import { invokeWithLayer } from "./invoke";import { graphqlPresets } from "./presets";import { makeDefaultGraphqlStore, type GraphqlStore, type StoredOperation } from "./store";import { AuthTemplate, decodeGraphqlIntegrationConfig, decodeGraphqlIntegrationConfigOption, ExtractedField, GraphqlIntegrationConfig, OperationBinding, type GraphqlOperationKind,} from "./types"; // ---------------------------------------------------------------------------// GraphQL error-body decoding (for invocation responses)// --------------------------------------------------------------------------- const GraphqlErrorBody = Schema.Struct({ message: Schema.String });const GraphqlErrorsBody = Schema.Array(Schema.Unknown);const decodeGraphqlErrorBody = Schema.decodeUnknownOption(GraphqlErrorBody);const decodeGraphqlErrorsBody = Schema.decodeUnknownOption(GraphqlErrorsBody); const decodeGraphqlErrors = (errors: unknown): readonly unknown[] | undefined => Option.getOrUndefined(decodeGraphqlErrorsBody(errors)); const extractGraphqlErrorMessage = (errors: readonly unknown[]): string | undefined => errors .map((error) => Option.getOrUndefined(decodeGraphqlErrorBody(error))?.message) .find((message) => message !== undefined && message.length > 0); const GRAPHQL_PLUGIN_ID = "graphql"; // ---------------------------------------------------------------------------// Extension input shapes// --------------------------------------------------------------------------- const AuthTemplateSchema = AuthTemplate; /** Register a GraphQL integration in the catalog. `endpoint` is the GraphQL URL; * `slug` (defaulted from the endpoint) is the catalog id; `introspectionJson` * supplies the schema when the endpoint disables live introspection; `headers` * / `queryParams` are static and also applied to add-time introspection; * `authenticationTemplate` declares the auth methods a connection can apply * through. */const GraphqlAddIntegrationInputSchema = Schema.Struct({ endpoint: Schema.String, slug: Schema.optional(Schema.String), name: Schema.optional(Schema.String), introspectionJson: 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(AuthTemplateSchema)),});export type GraphqlAddIntegrationInput = typeof GraphqlAddIntegrationInputSchema.Type; const GraphqlConfigureInputSchema = Schema.Struct({ name: Schema.optional(Schema.String), endpoint: 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(AuthTemplateSchema)),});export type GraphqlConfigureInput = typeof GraphqlConfigureInputSchema.Type; /** Input for the custom-method-create flow (HTTP `POST /graphql/integrations/ * :slug/config`). Unlike `configure` (which REPLACES the whole config for the * generic repair path), `configureAuth` MERGE-APPENDS these templates onto the * integration's existing `authenticationTemplate`, mirroring OpenAPI's * `configure`. */const GraphqlConfigureAuthInputSchema = Schema.Struct({ authenticationTemplate: Schema.Array(AuthTemplateSchema), mode: Schema.optional(Schema.Literals(["merge", "replace"])),});export type GraphqlConfigureAuthInput = typeof GraphqlConfigureAuthInputSchema.Type; // ---------------------------------------------------------------------------// Static control-tool schemas// --------------------------------------------------------------------------- const StaticAddIntegrationOutputSchema = Schema.Struct({ slug: Schema.String, name: Schema.String,});const StaticGetIntegrationInputSchema = Schema.Struct({ slug: Schema.String,});const StaticGetIntegrationOutputSchema = Schema.Struct({ integration: Schema.NullOr(Schema.Unknown),}); const StaticAddIntegrationInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(GraphqlAddIntegrationInputSchema),);const StaticAddIntegrationOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticAddIntegrationOutputSchema),);const StaticGetIntegrationInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticGetIntegrationInputSchema),);const StaticGetIntegrationOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticGetIntegrationOutputSchema),); const graphqlToolFailure = (code: string, message: string, details?: unknown) => ToolResult.fail({ code, message, ...(details === undefined ? {} : { details }), }); const graphqlAuthToolFailure = (failure: GraphqlAuthRequiredError) => authToolFailure({ code: failure.code, message: failure.message, source: { id: failure.integration, scope: failure.owner }, credential: { kind: failure.credentialKind, ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}), connectionId: failure.connection, }, ...(failure.status !== undefined ? { status: failure.status } : {}), ...(failure.details !== undefined ? { upstream: { ...(failure.status !== undefined ? { status: failure.status } : {}), details: failure.details, }, } : {}), }); // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- /** Match `token` as a separator-bounded run inside a URL hostname or path, * used as a low-confidence detection hint when introspection fails. */const urlMatchesToken = (url: URL, token: string): boolean => { const re = new RegExp(`(?:^|[^a-z0-9])${token}(?:$|[^a-z0-9])`, "i"); return re.test(url.hostname) || re.test(url.pathname);}; /** Derive an integration slug from an endpoint URL. */const slugFromEndpoint = (endpoint: string): string => { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL construction throws; this helper intentionally falls back to the stable default slug try { const url = new URL(endpoint); return url.hostname.replace(/[^a-z0-9]+/gi, "_").toLowerCase(); } catch { return "graphql"; }}; const formatTypeRef = (ref: IntrospectionTypeRef): string => Match.value(ref.kind).pipe( Match.when("NON_NULL", () => (ref.ofType ? `${formatTypeRef(ref.ofType)}!` : "Unknown!")), Match.when("LIST", () => (ref.ofType ? `[${formatTypeRef(ref.ofType)}]` : "[Unknown]")), Match.option, Option.getOrElse(() => ref.name ?? "Unknown"), ); const unwrapTypeName = (ref: IntrospectionTypeRef): string => { if (ref.name) return ref.name; if (ref.ofType) return unwrapTypeName(ref.ofType); return "Unknown";}; const buildSelectionSet = ( ref: IntrospectionTypeRef, types: ReadonlyMap<string, IntrospectionType>, depth: number, seen: Set<string>,): string => { if (depth > 2) return ""; const leafName = unwrapTypeName(ref); if (seen.has(leafName)) return ""; const objectType = types.get(leafName); if (!objectType?.fields) return ""; const kind = objectType.kind; if (kind === "SCALAR" || kind === "ENUM") return ""; seen.add(leafName); const subFields = objectType.fields .filter((f: IntrospectionField) => !f.name.startsWith("__")) .slice(0, 12) .map((f: IntrospectionField) => { const sub = buildSelectionSet(f.type, types, depth + 1, seen); return sub ? `${f.name} ${sub}` : f.name; }); seen.delete(leafName); return subFields.length > 0 ? `{ ${subFields.join(" ")} }` : "";}; // Name every generated operation: some servers reject anonymous operations, and// APM tooling keys traces off the operation name. Field names are already valid// GraphQL name tokens, so the upper-cased field name is a safe operation name.const operationNameForField = (fieldName: string): string => fieldName.charAt(0).toUpperCase() + fieldName.slice(1); const buildOperationStringForField = ( kind: GraphqlOperationKind, field: IntrospectionField, types: ReadonlyMap<string, IntrospectionType>,): string => { const opType = kind === "query" ? "query" : "mutation"; const opName = operationNameForField(field.name); const varDefs = field.args.map((arg) => { const typeName = formatTypeRef(arg.type); return `$${arg.name}: ${typeName}`; }); const argPasses = field.args.map((arg) => `${arg.name}: $${arg.name}`); const selectionSet = buildSelectionSet(field.type, types, 0, new Set()); const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(", ")})` : ""; const argPassStr = argPasses.length > 0 ? `(${argPasses.join(", ")})` : ""; return `${opType} ${opName}${varDefsStr} { ${field.name}${argPassStr}${selectionSet ? ` ${selectionSet}` : ""} }`;}; interface PreparedOperation { readonly toolName: string; readonly description: string; readonly inputSchema: unknown; readonly binding: OperationBinding;} const prepareOperations = ( fields: readonly ExtractedField[], introspection: IntrospectionResult,): readonly PreparedOperation[] => { const typeMap = new Map<string, IntrospectionType>(); for (const t of introspection.__schema.types) { typeMap.set(t.name, t); } const fieldMap = new Map<string, { kind: GraphqlOperationKind; field: IntrospectionField }>(); const schema = introspection.__schema; for (const rootKind of ["query", "mutation"] as const) { const typeName = rootKind === "query" ? schema.queryType?.name : schema.mutationType?.name; if (!typeName) continue; const rootType = typeMap.get(typeName); if (!rootType?.fields) continue; for (const f of rootType.fields) { if (!f.name.startsWith("__")) { fieldMap.set(`${rootKind}.${f.name}`, { kind: rootKind, field: f }); } } } return fields.map((extracted) => { const prefix = extracted.kind === "mutation" ? "mutation" : "query"; // A tool's name keeps its `<kind>.<field>` path (e.g. `query.hello`, // `mutation.setGreeting`). The address grammar treats `<tool>` as the // trailing remainder (see parseToolAddress), so the dot nests naturally. const toolName = `${prefix}.${extracted.fieldName}`; const description = Option.getOrElse( extracted.description, () => `GraphQL ${extracted.kind}: ${extracted.fieldName} -> ${extracted.returnTypeName}`, ); const key = `${extracted.kind}.${extracted.fieldName}`; const entry = fieldMap.get(key); const operationString = entry ? buildOperationStringForField(entry.kind, entry.field, typeMap) : `${extracted.kind} ${operationNameForField(extracted.fieldName)} { ${extracted.fieldName} }`; const binding = OperationBinding.make({ kind: extracted.kind, fieldName: extracted.fieldName, operationString, variableNames: extracted.arguments.map((a) => a.name), }); return { toolName, description, inputSchema: Option.getOrUndefined(extracted.inputSchema), binding, }; });}; const annotationsFor = (binding: OperationBinding): ToolAnnotations => { if (binding.kind === "mutation") { return { requiresApproval: true, approvalDescription: `mutation ${binding.fieldName}`, }; } return {};}; // ---------------------------------------------------------------------------// Auth template rendering (D11) — apply the resolved credential value through// the template the connection references, exactly like an apiKey bearer.// --------------------------------------------------------------------------- interface RenderedAuth { readonly headers: Record<string, string>; readonly queryParams: Record<string, string>;} const renderAuthTemplate = (template: AuthTemplate, value: string): RenderedAuth => { if (template.kind === "oauth2") { const header = template.header ?? "Authorization"; const prefix = template.prefix ?? "Bearer "; return { headers: { [header]: `${prefix}${value}` }, queryParams: {} }; } const rendered = template.prefix ? `${template.prefix}${value}` : value; if (template.in === "query") { return { headers: {}, queryParams: { [template.name]: rendered } }; } return { headers: { [template.name]: rendered }, queryParams: {} };}; // ---------------------------------------------------------------------------// Introspection — produce operations from a config (live or stored JSON).// --------------------------------------------------------------------------- const buildToolDefs = (prepared: readonly PreparedOperation[]): readonly ToolDef[] => prepared.map((p) => ({ name: ToolName.make(p.toolName), description: p.description, inputSchema: p.inputSchema, annotations: annotationsFor(p.binding), })); const toStoredOperations = ( slug: IntegrationSlug, prepared: readonly PreparedOperation[],): StoredOperation[] => prepared.map((p) => ({ toolName: p.toolName, integration: String(slug), binding: p.binding, })); /** Render an integration's static + resolved-credential auth onto introspection * headers/query params. Connection-create / tool-generation introspection runs * with the connection's credential (exactly how its tools are invoked), so an * auth-required endpoint introspects successfully here rather than at add-time. */const introspectHeadersForConnection = ( config: GraphqlIntegrationConfig, credentialValue: string | null, templateSlug: AuthTemplateSlug | null,): RenderedAuth => { const headers: Record<string, string> = { ...(config.headers ?? {}) }; const queryParams: Record<string, string> = { ...(config.queryParams ?? {}) }; if (credentialValue !== null) { // Render the exact template the connection references; with no slug // (connection row not yet persisted) fall back to the first declared. const template = (templateSlug !== null ? config.authenticationTemplate.find((t: AuthTemplate) => t.slug === String(templateSlug)) : undefined) ?? config.authenticationTemplate[0]; if (template) { const rendered = renderAuthTemplate(template, credentialValue); Object.assign(headers, rendered.headers); Object.assign(queryParams, rendered.queryParams); } } return { headers, queryParams };}; /** Introspect a config live or from its stored JSON, applying connection auth. * `parseIntrospectionJson` short-circuits the network when a schema snapshot is * present; otherwise this introspects the endpoint with the rendered credential. */const introspectForConnection = ( config: GraphqlIntegrationConfig, credentialValue: string | null, templateSlug: AuthTemplateSlug | null, httpClientLayer: Layer.Layer<HttpClient.HttpClient>,): Effect.Effect<IntrospectionResult, GraphqlIntrospectionError> => { if (config.introspectionJson) { return parseIntrospectionJson(config.introspectionJson); } const auth = introspectHeadersForConnection(config, credentialValue, templateSlug); return introspect( config.endpoint, Object.keys(auth.headers).length > 0 ? auth.headers : undefined, Object.keys(auth.queryParams).length > 0 ? auth.queryParams : undefined, ).pipe(Effect.provide(httpClientLayer));}; /** Introspect an integration's endpoint (with this connection's credential), * prepare its operations, persist the bindings, and return them. Invoked from * `invokeTool` on a cache miss — i.e. when an integration was registered * without an add-time schema and its bindings haven't been produced yet. */const materializeOperations = ( ctx: PluginCtx<GraphqlStore>, integration: string, config: GraphqlIntegrationConfig, credential: { readonly template: AuthTemplateSlug; readonly value: string | null; }, httpClientLayer: Layer.Layer<HttpClient.HttpClient>,): Effect.Effect<readonly StoredOperation[], GraphqlIntrospectionError | StorageFailure> => Effect.gen(function* () { // Render the exact template this connection references (we have its slug // here, unlike `resolveTools`) so an auth-required endpoint introspects. const template = config.authenticationTemplate.find( (t: AuthTemplate) => t.slug === String(credential.template), ); const headers: Record<string, string> = { ...(config.headers ?? {}) }; const queryParams: Record<string, string> = { ...(config.queryParams ?? {}), }; if (template && credential.value !== null) { const rendered = renderAuthTemplate(template, credential.value); Object.assign(headers, rendered.headers); Object.assign(queryParams, rendered.queryParams); } const introspection = config.introspectionJson ? yield* parseIntrospectionJson(config.introspectionJson) : yield* introspect( config.endpoint, Object.keys(headers).length > 0 ? headers : undefined, Object.keys(queryParams).length > 0 ? queryParams : undefined, ).pipe(Effect.provide(httpClientLayer)); const { result } = yield* extract(introspection).pipe( Effect.catch(() => Effect.succeed({ result: { fields: [] as readonly ExtractedField[] }, } as { readonly result: { readonly fields: readonly ExtractedField[] }; }), ), ); const prepared = prepareOperations(result.fields, introspection); const stored = toStoredOperations(IntegrationSlug.make(integration), prepared); yield* ctx.storage.replaceOperations(integration, stored); return stored; }); // ---------------------------------------------------------------------------// Declared auth methods — project the stored `authenticationTemplate` into the// catalog's plugin-agnostic `AuthMethodDescriptor[]`. Pure/sync and tolerant of// a malformed or foreign config blob (returns `[]`). GraphQL has no accounts// slot of its own, so this projection is what surfaces declared + custom methods// through the catalog's `authMethods` to the hub / Add-account flows. Exported// for tests.//// apiKey → one apikey method carrying a single header/query placement (the// `name` from the template, with the template's `prefix` defaulted to// `""`).// oauth2 → one oauth method (no resolved endpoints; graphql renders the// connection value as a bearer at invoke time).// --------------------------------------------------------------------------- const graphqlApiKeyLabel = (placement: AuthPlacementDescriptor): string => `API key (${placement.name || (placement.carrier === "header" ? "header" : "query")})`;Lines 1–500 of 1071. Load more
import { Effect, Match, Option, Schema } from "effect"; import type { Layer } from "effect"; import { FetchHttpClient, HttpClient } from "effect/unstable/http"; import { authToolFailure, AuthTemplateSlug, definePlugin, IntegrationAlreadyExistsError, IntegrationDetectionResult, IntegrationSlug, mergeAuthTemplates, ToolName, ToolResult, type AuthMethodDescriptor, type AuthPlacementDescriptor, type IntegrationConfig, type IntegrationRecord, type PluginCtx, type StorageFailure, type ToolAnnotations, type ToolDef, } from "@executor-js/sdk/core"; import { introspect, parseIntrospectionJson, type IntrospectionResult, type IntrospectionType, type IntrospectionField, type IntrospectionTypeRef, } from "./introspect"; import { extract } from "./extract"; import { GraphqlAuthRequiredError, GraphqlIntrospectionError, GraphqlInvocationError, } from "./errors"; import { invokeWithLayer } from "./invoke"; import { graphqlPresets } from "./presets"; import { makeDefaultGraphqlStore, type GraphqlStore, type StoredOperation } from "./store"; import { AuthTemplate, decodeGraphqlIntegrationConfig, decodeGraphqlIntegrationConfigOption, ExtractedField, GraphqlIntegrationConfig, OperationBinding, type GraphqlOperationKind, } from "./types"; // --------------------------------------------------------------------------- // GraphQL error-body decoding (for invocation responses) // --------------------------------------------------------------------------- const GraphqlErrorBody = Schema.Struct({ message: Schema.String }); const GraphqlErrorsBody = Schema.Array(Schema.Unknown); const decodeGraphqlErrorBody = Schema.decodeUnknownOption(GraphqlErrorBody); const decodeGraphqlErrorsBody = Schema.decodeUnknownOption(GraphqlErrorsBody); const decodeGraphqlErrors = (errors: unknown): readonly unknown[] | undefined => Option.getOrUndefined(decodeGraphqlErrorsBody(errors)); const extractGraphqlErrorMessage = (errors: readonly unknown[]): string | undefined => errors .map((error) => Option.getOrUndefined(decodeGraphqlErrorBody(error))?.message) .find((message) => message !== undefined && message.length > 0); const GRAPHQL_PLUGIN_ID = "graphql"; // --------------------------------------------------------------------------- // Extension input shapes // --------------------------------------------------------------------------- const AuthTemplateSchema = AuthTemplate; /** Register a GraphQL integration in the catalog. `endpoint` is the GraphQL URL; * `slug` (defaulted from the endpoint) is the catalog id; `introspectionJson` * supplies the schema when the endpoint disables live introspection; `headers` * / `queryParams` are static and also applied to add-time introspection; * `authenticationTemplate` declares the auth methods a connection can apply * through. */ const GraphqlAddIntegrationInputSchema = Schema.Struct({ endpoint: Schema.String, slug: Schema.optional(Schema.String), name: Schema.optional(Schema.String), introspectionJson: 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(AuthTemplateSchema)), }); export type GraphqlAddIntegrationInput = typeof GraphqlAddIntegrationInputSchema.Type; const GraphqlConfigureInputSchema = Schema.Struct({ name: Schema.optional(Schema.String), endpoint: 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(AuthTemplateSchema)), }); export type GraphqlConfigureInput = typeof GraphqlConfigureInputSchema.Type; /** Input for the custom-method-create flow (HTTP `POST /graphql/integrations/ * :slug/config`). Unlike `configure` (which REPLACES the whole config for the * generic repair path), `configureAuth` MERGE-APPENDS these templates onto the * integration's existing `authenticationTemplate`, mirroring OpenAPI's * `configure`. */ const GraphqlConfigureAuthInputSchema = Schema.Struct({ authenticationTemplate: Schema.Array(AuthTemplateSchema), mode: Schema.optional(Schema.Literals(["merge", "replace"])), }); export type GraphqlConfigureAuthInput = typeof GraphqlConfigureAuthInputSchema.Type; // --------------------------------------------------------------------------- // Static control-tool schemas // --------------------------------------------------------------------------- const StaticAddIntegrationOutputSchema = Schema.Struct({ slug: Schema.String, name: Schema.String, }); const StaticGetIntegrationInputSchema = Schema.Struct({ slug: Schema.String, }); const StaticGetIntegrationOutputSchema = Schema.Struct({ integration: Schema.NullOr(Schema.Unknown), }); const StaticAddIntegrationInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(GraphqlAddIntegrationInputSchema), ); const StaticAddIntegrationOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticAddIntegrationOutputSchema), ); const StaticGetIntegrationInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticGetIntegrationInputSchema), ); const StaticGetIntegrationOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticGetIntegrationOutputSchema), ); const graphqlToolFailure = (code: string, message: string, details?: unknown) => ToolResult.fail({ code, message, ...(details === undefined ? {} : { details }), }); const graphqlAuthToolFailure = (failure: GraphqlAuthRequiredError) => authToolFailure({ code: failure.code, message: failure.message, source: { id: failure.integration, scope: failure.owner }, credential: { kind: failure.credentialKind, ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}), connectionId: failure.connection, }, ...(failure.status !== undefined ? { status: failure.status } : {}), ...(failure.details !== undefined ? { upstream: { ...(failure.status !== undefined ? { status: failure.status } : {}), details: failure.details, }, } : {}), }); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Match `token` as a separator-bounded run inside a URL hostname or path, * used as a low-confidence detection hint when introspection fails. */ const urlMatchesToken = (url: URL, token: string): boolean => { const re = new RegExp(`(?:^|[^a-z0-9])${token}(?:$|[^a-z0-9])`, "i"); return re.test(url.hostname) || re.test(url.pathname); }; /** Derive an integration slug from an endpoint URL. */ const slugFromEndpoint = (endpoint: string): string => { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL construction throws; this helper intentionally falls back to the stable default slug try { const url = new URL(endpoint); return url.hostname.replace(/[^a-z0-9]+/gi, "_").toLowerCase(); } catch { return "graphql"; } }; const formatTypeRef = (ref: IntrospectionTypeRef): string => Match.value(ref.kind).pipe( Match.when("NON_NULL", () => (ref.ofType ? `${formatTypeRef(ref.ofType)}!` : "Unknown!")), Match.when("LIST", () => (ref.ofType ? `[${formatTypeRef(ref.ofType)}]` : "[Unknown]")), Match.option, Option.getOrElse(() => ref.name ?? "Unknown"), ); const unwrapTypeName = (ref: IntrospectionTypeRef): string => { if (ref.name) return ref.name; if (ref.ofType) return unwrapTypeName(ref.ofType); return "Unknown"; }; const buildSelectionSet = ( ref: IntrospectionTypeRef, types: ReadonlyMap<string, IntrospectionType>, depth: number, seen: Set<string>, ): string => { if (depth > 2) return ""; const leafName = unwrapTypeName(ref); if (seen.has(leafName)) return ""; const objectType = types.get(leafName); if (!objectType?.fields) return ""; const kind = objectType.kind; if (kind === "SCALAR" || kind === "ENUM") return ""; seen.add(leafName); const subFields = objectType.fields .filter((f: IntrospectionField) => !f.name.startsWith("__")) .slice(0, 12) .map((f: IntrospectionField) => { const sub = buildSelectionSet(f.type, types, depth + 1, seen); return sub ? `${f.name} ${sub}` : f.name; }); seen.delete(leafName); return subFields.length > 0 ? `{ ${subFields.join(" ")} }` : ""; }; // Name every generated operation: some servers reject anonymous operations, and // APM tooling keys traces off the operation name. Field names are already valid // GraphQL name tokens, so the upper-cased field name is a safe operation name. const operationNameForField = (fieldName: string): string => fieldName.charAt(0).toUpperCase() + fieldName.slice(1); const buildOperationStringForField = ( kind: GraphqlOperationKind, field: IntrospectionField, types: ReadonlyMap<string, IntrospectionType>, ): string => { const opType = kind === "query" ? "query" : "mutation"; const opName = operationNameForField(field.name); const varDefs = field.args.map((arg) => { const typeName = formatTypeRef(arg.type); return `$${arg.name}: ${typeName}`; }); const argPasses = field.args.map((arg) => `${arg.name}: $${arg.name}`); const selectionSet = buildSelectionSet(field.type, types, 0, new Set()); const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(", ")})` : ""; const argPassStr = argPasses.length > 0 ? `(${argPasses.join(", ")})` : ""; return `${opType} ${opName}${varDefsStr} { ${field.name}${argPassStr}${selectionSet ? ` ${selectionSet}` : ""} }`; }; interface PreparedOperation { readonly toolName: string; readonly description: string; readonly inputSchema: unknown; readonly binding: OperationBinding; } const prepareOperations = ( fields: readonly ExtractedField[], introspection: IntrospectionResult, ): readonly PreparedOperation[] => { const typeMap = new Map<string, IntrospectionType>(); for (const t of introspection.__schema.types) { typeMap.set(t.name, t); } const fieldMap = new Map<string, { kind: GraphqlOperationKind; field: IntrospectionField }>(); const schema = introspection.__schema; for (const rootKind of ["query", "mutation"] as const) { const typeName = rootKind === "query" ? schema.queryType?.name : schema.mutationType?.name; if (!typeName) continue; const rootType = typeMap.get(typeName); if (!rootType?.fields) continue; for (const f of rootType.fields) { if (!f.name.startsWith("__")) { fieldMap.set(`${rootKind}.${f.name}`, { kind: rootKind, field: f }); } } } return fields.map((extracted) => { const prefix = extracted.kind === "mutation" ? "mutation" : "query"; // A tool's name keeps its `<kind>.<field>` path (e.g. `query.hello`, // `mutation.setGreeting`). The address grammar treats `<tool>` as the // trailing remainder (see parseToolAddress), so the dot nests naturally. const toolName = `${prefix}.${extracted.fieldName}`; const description = Option.getOrElse( extracted.description, () => `GraphQL ${extracted.kind}: ${extracted.fieldName} -> ${extracted.returnTypeName}`, ); const key = `${extracted.kind}.${extracted.fieldName}`; const entry = fieldMap.get(key); const operationString = entry ? buildOperationStringForField(entry.kind, entry.field, typeMap) : `${extracted.kind} ${operationNameForField(extracted.fieldName)} { ${extracted.fieldName} }`; const binding = OperationBinding.make({ kind: extracted.kind, fieldName: extracted.fieldName, operationString, variableNames: extracted.arguments.map((a) => a.name), }); return { toolName, description, inputSchema: Option.getOrUndefined(extracted.inputSchema), binding, }; }); }; const annotationsFor = (binding: OperationBinding): ToolAnnotations => { if (binding.kind === "mutation") { return { requiresApproval: true, approvalDescription: `mutation ${binding.fieldName}`, }; } return {}; }; // --------------------------------------------------------------------------- // Auth template rendering (D11) — apply the resolved credential value through // the template the connection references, exactly like an apiKey bearer. // --------------------------------------------------------------------------- interface RenderedAuth { readonly headers: Record<string, string>; readonly queryParams: Record<string, string>; } const renderAuthTemplate = (template: AuthTemplate, value: string): RenderedAuth => { if (template.kind === "oauth2") { const header = template.header ?? "Authorization"; const prefix = template.prefix ?? "Bearer "; return { headers: { [header]: `${prefix}${value}` }, queryParams: {} }; } const rendered = template.prefix ? `${template.prefix}${value}` : value; if (template.in === "query") { return { headers: {}, queryParams: { [template.name]: rendered } }; } return { headers: { [template.name]: rendered }, queryParams: {} }; }; // --------------------------------------------------------------------------- // Introspection — produce operations from a config (live or stored JSON). // --------------------------------------------------------------------------- const buildToolDefs = (prepared: readonly PreparedOperation[]): readonly ToolDef[] => prepared.map((p) => ({ name: ToolName.make(p.toolName), description: p.description, inputSchema: p.inputSchema, annotations: annotationsFor(p.binding), })); const toStoredOperations = ( slug: IntegrationSlug, prepared: readonly PreparedOperation[], ): StoredOperation[] => prepared.map((p) => ({ toolName: p.toolName, integration: String(slug), binding: p.binding, })); /** Render an integration's static + resolved-credential auth onto introspection * headers/query params. Connection-create / tool-generation introspection runs * with the connection's credential (exactly how its tools are invoked), so an * auth-required endpoint introspects successfully here rather than at add-time. */ const introspectHeadersForConnection = ( config: GraphqlIntegrationConfig, credentialValue: string | null, templateSlug: AuthTemplateSlug | null, ): RenderedAuth => { const headers: Record<string, string> = { ...(config.headers ?? {}) }; const queryParams: Record<string, string> = { ...(config.queryParams ?? {}) }; if (credentialValue !== null) { // Render the exact template the connection references; with no slug // (connection row not yet persisted) fall back to the first declared. const template = (templateSlug !== null ? config.authenticationTemplate.find((t: AuthTemplate) => t.slug === String(templateSlug)) : undefined) ?? config.authenticationTemplate[0]; if (template) { const rendered = renderAuthTemplate(template, credentialValue); Object.assign(headers, rendered.headers); Object.assign(queryParams, rendered.queryParams); } } return { headers, queryParams }; }; /** Introspect a config live or from its stored JSON, applying connection auth. * `parseIntrospectionJson` short-circuits the network when a schema snapshot is * present; otherwise this introspects the endpoint with the rendered credential. */ const introspectForConnection = ( config: GraphqlIntegrationConfig, credentialValue: string | null, templateSlug: AuthTemplateSlug | null, httpClientLayer: Layer.Layer<HttpClient.HttpClient>, ): Effect.Effect<IntrospectionResult, GraphqlIntrospectionError> => { if (config.introspectionJson) { return parseIntrospectionJson(config.introspectionJson); } const auth = introspectHeadersForConnection(config, credentialValue, templateSlug); return introspect( config.endpoint, Object.keys(auth.headers).length > 0 ? auth.headers : undefined, Object.keys(auth.queryParams).length > 0 ? auth.queryParams : undefined, ).pipe(Effect.provide(httpClientLayer)); }; /** Introspect an integration's endpoint (with this connection's credential), * prepare its operations, persist the bindings, and return them. Invoked from * `invokeTool` on a cache miss — i.e. when an integration was registered * without an add-time schema and its bindings haven't been produced yet. */ const materializeOperations = ( ctx: PluginCtx<GraphqlStore>, integration: string, config: GraphqlIntegrationConfig, credential: { readonly template: AuthTemplateSlug; readonly value: string | null; }, httpClientLayer: Layer.Layer<HttpClient.HttpClient>, ): Effect.Effect<readonly StoredOperation[], GraphqlIntrospectionError | StorageFailure> => Effect.gen(function* () { // Render the exact template this connection references (we have its slug // here, unlike `resolveTools`) so an auth-required endpoint introspects. const template = config.authenticationTemplate.find( (t: AuthTemplate) => t.slug === String(credential.template), ); const headers: Record<string, string> = { ...(config.headers ?? {}) }; const queryParams: Record<string, string> = { ...(config.queryParams ?? {}), }; if (template && credential.value !== null) { const rendered = renderAuthTemplate(template, credential.value); Object.assign(headers, rendered.headers); Object.assign(queryParams, rendered.queryParams); } const introspection = config.introspectionJson ? yield* parseIntrospectionJson(config.introspectionJson) : yield* introspect( config.endpoint, Object.keys(headers).length > 0 ? headers : undefined, Object.keys(queryParams).length > 0 ? queryParams : undefined, ).pipe(Effect.provide(httpClientLayer)); const { result } = yield* extract(introspection).pipe( Effect.catch(() => Effect.succeed({ result: { fields: [] as readonly ExtractedField[] }, } as { readonly result: { readonly fields: readonly ExtractedField[] }; }), ), ); const prepared = prepareOperations(result.fields, introspection); const stored = toStoredOperations(IntegrationSlug.make(integration), prepared); yield* ctx.storage.replaceOperations(integration, stored); return stored; }); // --------------------------------------------------------------------------- // Declared auth methods — project the stored `authenticationTemplate` into the // catalog's plugin-agnostic `AuthMethodDescriptor[]`. Pure/sync and tolerant of // a malformed or foreign config blob (returns `[]`). GraphQL has no accounts // slot of its own, so this projection is what surfaces declared + custom methods // through the catalog's `authMethods` to the hub / Add-account flows. Exported // for tests. // // apiKey → one apikey method carrying a single header/query placement (the // `name` from the template, with the template's `prefix` defaulted to // `""`). // oauth2 → one oauth method (no resolved endpoints; graphql renders the // connection value as a bearer at invoke time). // --------------------------------------------------------------------------- const graphqlApiKeyLabel = (placement: AuthPlacementDescriptor): string => `API key (${placement.name || (placement.carrier === "header" ? "header" : "query")})`;