import { Effect, Match, Option, Schema } from "effect";import type { Layer } from "effect";import { HttpClient } from "effect/unstable/http"; import { type CredentialBindingRef, type CredentialBindingValue, definePlugin, tool, defaultSourceInstallScopeId, ScopeId, SourceDetectionResult, StorageError, ToolResult, type PluginCtx, type StorageFailure, type ToolAnnotations, type ToolRow,} from "@executor-js/sdk/core";import { compileHttpNamedCredentialMap, OAuth2SourceConfig, httpCredentialInputToBindingValue, type HttpConfiguredValueInput,} from "@executor-js/sdk/http-source"; import { headersToConfigValues, type ConfigFileSink, type GraphqlSourceConfig as GraphqlConfigEntry,} from "@executor-js/config"; import { introspect, parseIntrospectionJson, type IntrospectionResult, type IntrospectionType, type IntrospectionField, type IntrospectionTypeRef,} from "./introspect";import { extract } from "./extract";import { GraphqlIntrospectionError, GraphqlInvocationError } from "./errors";import { invokeWithLayer } from "./invoke";import { graphqlPresets } from "./presets";import { graphqlSchema, makeDefaultGraphqlStore, type GraphqlStore, type StoredGraphqlSource, type StoredOperation,} from "./store";import { ExtractedField, GraphqlConfiguredValueInput as GraphqlConfiguredValueInputSchema, GRAPHQL_OAUTH_CONNECTION_SLOT, GraphqlCredentialInput as GraphqlCredentialInputSchema, GraphqlSourceAuthInput as GraphqlSourceAuthInputSchema, graphqlHeaderSlot, graphqlQueryParamSlot, OperationBinding, type ConfiguredGraphqlCredentialValue, type GraphqlConfiguredValueInput, type GraphqlCredentialInput, type GraphqlSourceAuth, type HeaderValue as HeaderValueValue, type GraphqlSourceAuthInput, type GraphqlOperationKind,} from "./types"; // ---------------------------------------------------------------------------// Plugin config// --------------------------------------------------------------------------- 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); export type HeaderValue = HeaderValueValue;export type GraphqlCredentialValue = ConfiguredGraphqlCredentialValue; export interface GraphqlSourceConfig { /** The GraphQL endpoint URL */ readonly endpoint: string; /** * Executor scope id that owns this source row. Must be one of the * executor's configured scopes. Typical shape: an admin adds the * source at the outermost (organization) scope so it's visible to * every inner (per-user) scope via fall-through reads. */ readonly scope: string; /** Display name for the source. */ readonly name: string; /** Optional: introspection JSON text (if endpoint doesn't support introspection) */ readonly introspectionJson?: string; /** Namespace for the tools. */ readonly namespace: string; /** Headers applied to every request. Secret entries declare source-owned slots. */ readonly headers?: Record<string, GraphqlConfiguredValueInput>; /** Query parameters applied to every request. Secret entries declare source-owned slots. */ readonly queryParams?: Record<string, GraphqlConfiguredValueInput>; /** Optional OAuth2 credential used as a Bearer token for every request. */ readonly oauth2?: OAuth2SourceConfig; /** Initial credential bindings used while adding and introspecting this source. */ readonly credentials?: GraphqlInitialCredentialsInput;} const GraphqlInitialCredentialsInputSchema = Schema.Struct({ scope: Schema.String, headers: Schema.optional(Schema.Record(Schema.String, GraphqlCredentialInputSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, GraphqlCredentialInputSchema)), auth: Schema.optional(GraphqlSourceAuthInputSchema),});type GraphqlInitialCredentialsInput = typeof GraphqlInitialCredentialsInputSchema.Type; const StaticAddSourceInputSchema = Schema.Struct({ endpoint: Schema.String, name: Schema.String, introspectionJson: Schema.optional(Schema.String), namespace: Schema.String, headers: Schema.optional(Schema.Record(Schema.String, GraphqlConfiguredValueInputSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, GraphqlConfiguredValueInputSchema)), oauth2: Schema.optional(OAuth2SourceConfig), credentials: Schema.optional(GraphqlInitialCredentialsInputSchema),});const SourceConfigureInputSchema = Schema.Struct({ name: Schema.optional(Schema.String), endpoint: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, GraphqlCredentialInputSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, GraphqlCredentialInputSchema)), auth: Schema.optional(GraphqlSourceAuthInputSchema),});const StaticConfigureSourceInputSchema = Schema.Struct({ source: Schema.Struct({ id: Schema.String, scope: Schema.String, }), scope: Schema.String, ...SourceConfigureInputSchema.fields,});const StaticConfigureSourceOutputSchema = Schema.Struct({ configured: Schema.Boolean,});const StaticGetSourceInputSchema = Schema.Struct({ namespace: Schema.String, scope: Schema.String,});const StaticGetSourceOutputSchema = Schema.Struct({ source: Schema.NullOr(Schema.Unknown),}); const StaticAddSourceInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticAddSourceInputSchema),);const StaticAddSourceOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1( Schema.Struct({ namespace: Schema.String, source: Schema.Struct({ id: Schema.String, scope: Schema.String, }), toolCount: Schema.Number, }), ),);const StaticGetSourceInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticGetSourceInputSchema),);const StaticGetSourceOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticGetSourceOutputSchema),);const StaticConfigureSourceInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticConfigureSourceInputSchema),);const StaticConfigureSourceOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticConfigureSourceOutputSchema),); const graphqlToolFailure = (code: string, message: string, details?: unknown) => ToolResult.fail({ code, message, ...(details === undefined ? {} : { details }), }); const resolveStaticScopeInput = ( ctx: { readonly scopes: readonly { readonly id: ScopeId; readonly name: string }[] }, value: string,): string => String( ctx.scopes.find((scope) => scope.name === value || String(scope.id) === value)?.id ?? value, ); // ---------------------------------------------------------------------------// Plugin extension// --------------------------------------------------------------------------- export interface GraphqlSourceRef { readonly id: string; readonly scope: string;} export interface GraphqlConfigureSourceInput { readonly scope: string; readonly name?: string; readonly endpoint?: string; readonly headers?: Record<string, GraphqlCredentialInput>; readonly queryParams?: Record<string, GraphqlCredentialInput>; readonly auth?: GraphqlSourceAuthInput;} // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- /** Match `token` as a separator-bounded run inside a URL hostname or path, * used as a low-confidence detection hint when introspection fails. * Boundary chars are everything non-alphanumeric, so `/api/graphql`, * `graphql.example.com`, `graphql-api`, and `graphql_v2` all match while * `graphqlserver` and `/graphqlite` do not. */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 a namespace from an endpoint URL */const namespaceFromEndpoint = (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 namespace 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) => !f.name.startsWith("__")) .slice(0, 12) .map((f) => { 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(" ")} }` : "";}; const buildOperationStringForField = ( kind: GraphqlOperationKind, field: IntrospectionField, types: ReadonlyMap<string, IntrospectionType>,): string => { const opType = kind === "query" ? "query" : "mutation"; 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}${varDefsStr} { ${field.name}${argPassStr}${selectionSet ? ` ${selectionSet}` : ""} }`;}; interface PreparedOperation { readonly toolPath: 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"; const toolPath = `${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} { ${extracted.fieldName} }`; const binding = OperationBinding.make({ kind: extracted.kind, fieldName: extracted.fieldName, operationString, variableNames: extracted.arguments.map((a) => a.name), }); return { toolPath, description, inputSchema: Option.getOrUndefined(extracted.inputSchema), binding, }; });}; const annotationsFor = (binding: OperationBinding): ToolAnnotations => { if (binding.kind === "mutation") { return { requiresApproval: true, approvalDescription: `mutation ${binding.fieldName}`, }; } return {};}; // ---------------------------------------------------------------------------// Plugin factory// --------------------------------------------------------------------------- export interface GraphqlPluginOptions { readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; /** If provided, source add/remove is mirrored to executor.jsonc * (best-effort — file errors are logged, not raised). */ readonly configFile?: ConfigFileSink;} const toGraphqlConfigEntry = ( namespace: string, config: GraphqlSourceConfig,): GraphqlConfigEntry => { const headers: Record<string, HeaderValue> = {}; for (const [name, value] of Object.entries(config.headers ?? {})) { if (typeof value === "string" || !("kind" in value)) { headers[name] = value; } } return { kind: "graphql", endpoint: config.endpoint, introspectionJson: config.introspectionJson, namespace, headers: headersToConfigValues(Object.keys(headers).length > 0 ? headers : undefined), };}; const GRAPHQL_PLUGIN_ID = "graphql"; const scopeRanks = (ctx: PluginCtx<GraphqlStore>): ReadonlyMap<string, number> => new Map(ctx.scopes.map((scope, index) => [String(scope.id), index])); const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: string): number => ranks.get(scopeId) ?? Infinity; const resolveGraphqlSourceBinding = ( ctx: PluginCtx<GraphqlStore>, sourceId: string, sourceScope: string, slot: string,): Effect.Effect<CredentialBindingRef | null, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return null; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: GRAPHQL_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); const binding = bindings .filter( (candidate) => candidate.slotKey === slot && scopeRank(ranks, candidate.scopeId) <= sourceSourceRank, ) .sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0]; return binding ?? null; }); const validateGraphqlBindingTarget = ( ctx: PluginCtx<GraphqlStore>, input: { readonly sourceScope: string; readonly targetScope: string; readonly sourceId: string; },): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, input.sourceScope); const targetRank = scopeRank(ranks, input.targetScope); const scopeList = `[${ctx.scopes.map((s) => s.id).join(", ")}]`; if (sourceSourceRank === Infinity) { return yield* new StorageError({ message: `GraphQL source binding references source scope "${input.sourceScope}" ` + `which is not in the executor's scope stack ${scopeList}.`, cause: undefined, }); } if (targetRank === Infinity) { return yield* new StorageError({ message: `GraphQL source binding targets scope "${input.targetScope}" which is not ` + `in the executor's scope stack ${scopeList}.`, cause: undefined, }); } if (targetRank > sourceSourceRank) { return yield* new StorageError({ message: `GraphQL source bindings for "${input.sourceId}" cannot be written at ` + `outer scope "${input.targetScope}" because the base source lives at ` + `"${input.sourceScope}"`, cause: undefined, }); } }); const canonicalizeCredentialMap = compileHttpNamedCredentialMap; const canonicalizeConfiguredValueMap = ( values: Record<string, GraphqlConfiguredValueInput> | undefined, slotForName: (name: string) => string,): Record<string, ConfiguredGraphqlCredentialValue> => { const next: Record<string, ConfiguredGraphqlCredentialValue> = {}; for (const [name, value] of Object.entries(values ?? {})) { if (typeof value === "string") { next[name] = value; continue; } next[name] = { kind: "binding", slot: slotForName(name), prefix: value.prefix,Lines 1–500 of 1246. Load more
import { Effect, Match, Option, Schema } from "effect"; import type { Layer } from "effect"; import { HttpClient } from "effect/unstable/http"; import { type CredentialBindingRef, type CredentialBindingValue, definePlugin, tool, defaultSourceInstallScopeId, ScopeId, SourceDetectionResult, StorageError, ToolResult, type PluginCtx, type StorageFailure, type ToolAnnotations, type ToolRow, } from "@executor-js/sdk/core"; import { compileHttpNamedCredentialMap, OAuth2SourceConfig, httpCredentialInputToBindingValue, type HttpConfiguredValueInput, } from "@executor-js/sdk/http-source"; import { headersToConfigValues, type ConfigFileSink, type GraphqlSourceConfig as GraphqlConfigEntry, } from "@executor-js/config"; import { introspect, parseIntrospectionJson, type IntrospectionResult, type IntrospectionType, type IntrospectionField, type IntrospectionTypeRef, } from "./introspect"; import { extract } from "./extract"; import { GraphqlIntrospectionError, GraphqlInvocationError } from "./errors"; import { invokeWithLayer } from "./invoke"; import { graphqlPresets } from "./presets"; import { graphqlSchema, makeDefaultGraphqlStore, type GraphqlStore, type StoredGraphqlSource, type StoredOperation, } from "./store"; import { ExtractedField, GraphqlConfiguredValueInput as GraphqlConfiguredValueInputSchema, GRAPHQL_OAUTH_CONNECTION_SLOT, GraphqlCredentialInput as GraphqlCredentialInputSchema, GraphqlSourceAuthInput as GraphqlSourceAuthInputSchema, graphqlHeaderSlot, graphqlQueryParamSlot, OperationBinding, type ConfiguredGraphqlCredentialValue, type GraphqlConfiguredValueInput, type GraphqlCredentialInput, type GraphqlSourceAuth, type HeaderValue as HeaderValueValue, type GraphqlSourceAuthInput, type GraphqlOperationKind, } from "./types"; // --------------------------------------------------------------------------- // Plugin config // --------------------------------------------------------------------------- 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); export type HeaderValue = HeaderValueValue; export type GraphqlCredentialValue = ConfiguredGraphqlCredentialValue; export interface GraphqlSourceConfig { /** The GraphQL endpoint URL */ readonly endpoint: string; /** * Executor scope id that owns this source row. Must be one of the * executor's configured scopes. Typical shape: an admin adds the * source at the outermost (organization) scope so it's visible to * every inner (per-user) scope via fall-through reads. */ readonly scope: string; /** Display name for the source. */ readonly name: string; /** Optional: introspection JSON text (if endpoint doesn't support introspection) */ readonly introspectionJson?: string; /** Namespace for the tools. */ readonly namespace: string; /** Headers applied to every request. Secret entries declare source-owned slots. */ readonly headers?: Record<string, GraphqlConfiguredValueInput>; /** Query parameters applied to every request. Secret entries declare source-owned slots. */ readonly queryParams?: Record<string, GraphqlConfiguredValueInput>; /** Optional OAuth2 credential used as a Bearer token for every request. */ readonly oauth2?: OAuth2SourceConfig; /** Initial credential bindings used while adding and introspecting this source. */ readonly credentials?: GraphqlInitialCredentialsInput; } const GraphqlInitialCredentialsInputSchema = Schema.Struct({ scope: Schema.String, headers: Schema.optional(Schema.Record(Schema.String, GraphqlCredentialInputSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, GraphqlCredentialInputSchema)), auth: Schema.optional(GraphqlSourceAuthInputSchema), }); type GraphqlInitialCredentialsInput = typeof GraphqlInitialCredentialsInputSchema.Type; const StaticAddSourceInputSchema = Schema.Struct({ endpoint: Schema.String, name: Schema.String, introspectionJson: Schema.optional(Schema.String), namespace: Schema.String, headers: Schema.optional(Schema.Record(Schema.String, GraphqlConfiguredValueInputSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, GraphqlConfiguredValueInputSchema)), oauth2: Schema.optional(OAuth2SourceConfig), credentials: Schema.optional(GraphqlInitialCredentialsInputSchema), }); const SourceConfigureInputSchema = Schema.Struct({ name: Schema.optional(Schema.String), endpoint: Schema.optional(Schema.String), headers: Schema.optional(Schema.Record(Schema.String, GraphqlCredentialInputSchema)), queryParams: Schema.optional(Schema.Record(Schema.String, GraphqlCredentialInputSchema)), auth: Schema.optional(GraphqlSourceAuthInputSchema), }); const StaticConfigureSourceInputSchema = Schema.Struct({ source: Schema.Struct({ id: Schema.String, scope: Schema.String, }), scope: Schema.String, ...SourceConfigureInputSchema.fields, }); const StaticConfigureSourceOutputSchema = Schema.Struct({ configured: Schema.Boolean, }); const StaticGetSourceInputSchema = Schema.Struct({ namespace: Schema.String, scope: Schema.String, }); const StaticGetSourceOutputSchema = Schema.Struct({ source: Schema.NullOr(Schema.Unknown), }); const StaticAddSourceInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticAddSourceInputSchema), ); const StaticAddSourceOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1( Schema.Struct({ namespace: Schema.String, source: Schema.Struct({ id: Schema.String, scope: Schema.String, }), toolCount: Schema.Number, }), ), ); const StaticGetSourceInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticGetSourceInputSchema), ); const StaticGetSourceOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticGetSourceOutputSchema), ); const StaticConfigureSourceInputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticConfigureSourceInputSchema), ); const StaticConfigureSourceOutputStandardSchema = Schema.toStandardSchemaV1( Schema.toStandardJSONSchemaV1(StaticConfigureSourceOutputSchema), ); const graphqlToolFailure = (code: string, message: string, details?: unknown) => ToolResult.fail({ code, message, ...(details === undefined ? {} : { details }), }); const resolveStaticScopeInput = ( ctx: { readonly scopes: readonly { readonly id: ScopeId; readonly name: string }[] }, value: string, ): string => String( ctx.scopes.find((scope) => scope.name === value || String(scope.id) === value)?.id ?? value, ); // --------------------------------------------------------------------------- // Plugin extension // --------------------------------------------------------------------------- export interface GraphqlSourceRef { readonly id: string; readonly scope: string; } export interface GraphqlConfigureSourceInput { readonly scope: string; readonly name?: string; readonly endpoint?: string; readonly headers?: Record<string, GraphqlCredentialInput>; readonly queryParams?: Record<string, GraphqlCredentialInput>; readonly auth?: GraphqlSourceAuthInput; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Match `token` as a separator-bounded run inside a URL hostname or path, * used as a low-confidence detection hint when introspection fails. * Boundary chars are everything non-alphanumeric, so `/api/graphql`, * `graphql.example.com`, `graphql-api`, and `graphql_v2` all match while * `graphqlserver` and `/graphqlite` do not. */ 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 a namespace from an endpoint URL */ const namespaceFromEndpoint = (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 namespace 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) => !f.name.startsWith("__")) .slice(0, 12) .map((f) => { 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(" ")} }` : ""; }; const buildOperationStringForField = ( kind: GraphqlOperationKind, field: IntrospectionField, types: ReadonlyMap<string, IntrospectionType>, ): string => { const opType = kind === "query" ? "query" : "mutation"; 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}${varDefsStr} { ${field.name}${argPassStr}${selectionSet ? ` ${selectionSet}` : ""} }`; }; interface PreparedOperation { readonly toolPath: 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"; const toolPath = `${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} { ${extracted.fieldName} }`; const binding = OperationBinding.make({ kind: extracted.kind, fieldName: extracted.fieldName, operationString, variableNames: extracted.arguments.map((a) => a.name), }); return { toolPath, description, inputSchema: Option.getOrUndefined(extracted.inputSchema), binding, }; }); }; const annotationsFor = (binding: OperationBinding): ToolAnnotations => { if (binding.kind === "mutation") { return { requiresApproval: true, approvalDescription: `mutation ${binding.fieldName}`, }; } return {}; }; // --------------------------------------------------------------------------- // Plugin factory // --------------------------------------------------------------------------- export interface GraphqlPluginOptions { readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; /** If provided, source add/remove is mirrored to executor.jsonc * (best-effort — file errors are logged, not raised). */ readonly configFile?: ConfigFileSink; } const toGraphqlConfigEntry = ( namespace: string, config: GraphqlSourceConfig, ): GraphqlConfigEntry => { const headers: Record<string, HeaderValue> = {}; for (const [name, value] of Object.entries(config.headers ?? {})) { if (typeof value === "string" || !("kind" in value)) { headers[name] = value; } } return { kind: "graphql", endpoint: config.endpoint, introspectionJson: config.introspectionJson, namespace, headers: headersToConfigValues(Object.keys(headers).length > 0 ? headers : undefined), }; }; const GRAPHQL_PLUGIN_ID = "graphql"; const scopeRanks = (ctx: PluginCtx<GraphqlStore>): ReadonlyMap<string, number> => new Map(ctx.scopes.map((scope, index) => [String(scope.id), index])); const scopeRank = (ranks: ReadonlyMap<string, number>, scopeId: string): number => ranks.get(scopeId) ?? Infinity; const resolveGraphqlSourceBinding = ( ctx: PluginCtx<GraphqlStore>, sourceId: string, sourceScope: string, slot: string, ): Effect.Effect<CredentialBindingRef | null, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, sourceScope); if (sourceSourceRank === Infinity) return null; const bindings = yield* ctx.credentialBindings.listForSource({ pluginId: GRAPHQL_PLUGIN_ID, sourceId, sourceScope: ScopeId.make(sourceScope), }); const binding = bindings .filter( (candidate) => candidate.slotKey === slot && scopeRank(ranks, candidate.scopeId) <= sourceSourceRank, ) .sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0]; return binding ?? null; }); const validateGraphqlBindingTarget = ( ctx: PluginCtx<GraphqlStore>, input: { readonly sourceScope: string; readonly targetScope: string; readonly sourceId: string; }, ): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { const ranks = scopeRanks(ctx); const sourceSourceRank = scopeRank(ranks, input.sourceScope); const targetRank = scopeRank(ranks, input.targetScope); const scopeList = `[${ctx.scopes.map((s) => s.id).join(", ")}]`; if (sourceSourceRank === Infinity) { return yield* new StorageError({ message: `GraphQL source binding references source scope "${input.sourceScope}" ` + `which is not in the executor's scope stack ${scopeList}.`, cause: undefined, }); } if (targetRank === Infinity) { return yield* new StorageError({ message: `GraphQL source binding targets scope "${input.targetScope}" which is not ` + `in the executor's scope stack ${scopeList}.`, cause: undefined, }); } if (targetRank > sourceSourceRank) { return yield* new StorageError({ message: `GraphQL source bindings for "${input.sourceId}" cannot be written at ` + `outer scope "${input.targetScope}" because the base source lives at ` + `"${input.sourceScope}"`, cause: undefined, }); } }); const canonicalizeCredentialMap = compileHttpNamedCredentialMap; const canonicalizeConfiguredValueMap = ( values: Record<string, GraphqlConfiguredValueInput> | undefined, slotForName: (name: string) => string, ): Record<string, ConfiguredGraphqlCredentialValue> => { const next: Record<string, ConfiguredGraphqlCredentialValue> = {}; for (const [name, value] of Object.entries(values ?? {})) { if (typeof value === "string") { next[name] = value; continue; } next[name] = { kind: "binding", slot: slotForName(name), prefix: value.prefix,