1395 lines · 50.0 KB · TypeScript
RawDownload
import { Effect, Option, Predicate, Schema } from "effect";import type { Layer } from "effect";import { HttpClient } from "effect/unstable/http"; import {  ScopeId,  SecretId,  SourceDetectionResult,  StorageError,  definePlugin,  tool,  resolveSecretBackedMap,  type CredentialBindingRef,  type PluginCtx,  type StorageFailure,  type ToolAnnotations,  type ToolRow,} from "@executor-js/sdk/core"; import {  headersToConfigValues,  type ConfigFileSink,  type OpenApiSourceConfig,} from "@executor-js/config"; import { OpenApiExtractionError, OpenApiOAuthError, OpenApiParseError } from "./errors";import { parse, resolveSpecText } from "./parse";import { extract } from "./extract";import { compileToolDefinitions, type ToolDefinition } from "./definitions";import { annotationsForOperation, invokeWithLayer } from "./invoke";import { resolveBaseUrl } from "./openapi-utils";import { previewSpec, SpecPreview } from "./preview";import {  makeDefaultOpenapiStore,  openapiSchema,  type OpenapiStore,  type SourceConfig,  type StoredOperation,  type StoredSource,} from "./store";import {  HeaderValue as HeaderValueSchema,  ConfiguredHeaderValue as ConfiguredHeaderValueSchema,  ConfiguredHeaderBinding,  OAuth2SourceConfig,  OpenApiCredentialInput as OpenApiCredentialInputSchema,  OpenApiSourceBindingInput,  OpenApiSourceBindingRef,  type OpenApiCredentialInput as OpenApiCredentialInputValue,  type OpenApiSourceBindingValue,  OperationBinding,  type ConfiguredHeaderValue as ConfiguredHeaderValueValue,  type HeaderValue as HeaderValueValue,} from "./types"; // ---------------------------------------------------------------------------// Plugin config// --------------------------------------------------------------------------- export type HeaderValue = HeaderValueValue;export type ConfiguredHeaderValue = ConfiguredHeaderValueValue;export type OpenApiHeaderInput = HeaderValue | ConfiguredHeaderValue;export type OpenApiCredentialInput = OpenApiCredentialInputValue;export type OpenApiOAuthInput = OAuth2SourceConfig; export interface OpenApiSpecFetchCredentialsInput {  readonly headers?: Record<string, HeaderValue>;  readonly queryParams?: Record<string, HeaderValue>;} export interface OpenApiPreviewInput {  readonly spec: string;  readonly specFetchCredentials?: OpenApiSpecFetchCredentialsInput;} export interface OpenApiSpecConfig {  readonly spec: string;  readonly specFetchCredentials?: OpenApiSpecFetchCredentialsInput;  /**   * 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;  readonly name?: string;  readonly baseUrl?: string;  readonly namespace?: string;  readonly headers?: Record<string, OpenApiCredentialInput>;  readonly queryParams?: Record<string, OpenApiCredentialInput>;  readonly oauth2?: OpenApiOAuthInput;  readonly credentialTargetScope?: string;} export interface OpenApiUpdateSourceInput {  readonly name?: string;  readonly baseUrl?: string;  readonly headers?: Record<string, OpenApiCredentialInput>;  readonly queryParams?: Record<string, OpenApiCredentialInput>;  readonly credentialTargetScope?: string;  /** Refresh the source's stored OAuth2 metadata after a successful   *  re-authenticate. */  readonly oauth2?: OpenApiOAuthInput;} /** * Errors any OpenAPI extension method may surface. The first three are * plugin-domain tagged errors that flow directly to clients (4xx, each * carrying its own `HttpApiSchema` status). `StorageFailure` covers * raw backend failures (`StorageError`) plus `UniqueViolationError`; * the HTTP edge (`@executor-js/api`'s `withCapture`) translates * `StorageError` to the opaque `InternalError({ traceId })` at Layer * composition. `UniqueViolationError` passes through — plugins can * `Effect.catchTag` it if they want a friendlier user-facing error. */export type OpenApiExtensionFailure =  | OpenApiParseError  | OpenApiExtractionError  | OpenApiOAuthError  | StorageFailure; export interface OpenApiPluginExtension {  readonly previewSpec: (    input: string | OpenApiPreviewInput,  ) => Effect.Effect<    SpecPreview,    OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure  >;  readonly addSpec: (    config: OpenApiSpecConfig,  ) => Effect.Effect<    { readonly sourceId: string; readonly toolCount: number },    OpenApiParseError | OpenApiExtractionError | OpenApiOAuthError | StorageFailure  >;  readonly removeSpec: (namespace: string, scope: string) => Effect.Effect<void, StorageFailure>;  readonly getSource: (    namespace: string,    scope: string,  ) => Effect.Effect<StoredSource | null, StorageFailure>;  readonly updateSource: (    namespace: string,    scope: string,    input: OpenApiUpdateSourceInput,  ) => Effect.Effect<void, StorageFailure>;  readonly listSourceBindings: (    sourceId: string,    sourceScope: string,  ) => Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure>;  readonly setSourceBinding: (    input: OpenApiSourceBindingInput,  ) => Effect.Effect<OpenApiSourceBindingRef, StorageFailure>;  readonly removeSourceBinding: (    sourceId: string,    sourceScope: string,    slot: string,    scope: string,  ) => Effect.Effect<void, StorageFailure>;} // ---------------------------------------------------------------------------// Control-tool input/output schemas// --------------------------------------------------------------------------- const PreviewSpecInputSchema = Schema.Struct({  spec: Schema.String,  specFetchCredentials: Schema.optional(    Schema.Struct({      headers: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)),      queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)),    }),  ),}); const OpenApiHeaderInputSchema = Schema.Union([HeaderValueSchema, ConfiguredHeaderValueSchema]);const OpenApiOAuthInputSchema = OAuth2SourceConfig; const AddSourceInputSchema = Schema.Struct({  scope: Schema.String,  spec: Schema.String,  name: Schema.optional(Schema.String),  baseUrl: Schema.optional(Schema.String),  namespace: Schema.optional(Schema.String),  headers: Schema.optional(Schema.Record(Schema.String, OpenApiHeaderInputSchema)),  queryParams: Schema.optional(Schema.Record(Schema.String, OpenApiCredentialInputSchema)),  oauth2: Schema.optional(OpenApiOAuthInputSchema),  credentialTargetScope: Schema.optional(Schema.String),  specFetchCredentials: Schema.optional(    Schema.Struct({      headers: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)),      queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValueSchema)),    }),  ),}); const AddSourceOutputSchema = Schema.Struct({  sourceId: Schema.String,  toolCount: Schema.Number,}); const PreviewSpecInputStandardSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(PreviewSpecInputSchema),);const AddSourceInputStandardSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(AddSourceInputSchema),);const AddSourceOutputStandardSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(AddSourceOutputSchema),); // ---------------------------------------------------------------------------// Helpers// --------------------------------------------------------------------------- /** Rewrite OpenAPI `#/components/schemas/X` refs to standard `#/$defs/X`. */const normalizeOpenApiRefs = (node: unknown): unknown => {  if (node == null || typeof node !== "object") return node;  if (Array.isArray(node)) {    let changed = false;    const out = node.map((item) => {      const n = normalizeOpenApiRefs(item);      if (n !== item) changed = true;      return n;    });    return changed ? out : node;  }   const obj = node as Record<string, unknown>;   if (typeof obj.$ref === "string") {    const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/);    if (match) return { ...obj, $ref: `#/$defs/${match[1]}` };    return obj;  }   let changed = false;  const result: Record<string, unknown> = {};  for (const [k, v] of Object.entries(obj)) {    const n = normalizeOpenApiRefs(v);    if (n !== v) changed = true;    result[k] = n;  }  return changed ? result : obj;}; const toBinding = (def: ToolDefinition): OperationBinding =>  OperationBinding.make({    method: def.operation.method,    pathTemplate: def.operation.pathTemplate,    parameters: [...def.operation.parameters],    requestBody: def.operation.requestBody,  }); const descriptionFor = (def: ToolDefinition): string => {  const op = def.operation;  return Option.getOrElse(op.description, () =>    Option.getOrElse(op.summary, () => `${op.method.toUpperCase()} ${op.pathTemplate}`),  );}; const slotPart = (value: string): string =>  value    .trim()    .toLowerCase()    .replace(/[^a-z0-9]+/g, "-")    .replace(/^-+|-+$/g, "") || "default"; const headerSlotFromName = (name: string): string => `header:${slotPart(name)}`; const queryParamSlotFromName = (name: string): string => `query_param:${slotPart(name)}`; const specFetchHeaderSlotFromName = (name: string): string => `spec_fetch_header:${slotPart(name)}`; const specFetchQueryParamSlotFromName = (name: string): string =>  `spec_fetch_query_param:${slotPart(name)}`; const canonicalizeHeaders = (  headers: Record<string, OpenApiCredentialInput> | undefined,): {  readonly headers: Record<string, ConfiguredHeaderValue>;  readonly bindings: ReadonlyArray<{    readonly slot: string;    readonly value: OpenApiSourceBindingValue;    readonly targetScope?: string;  }>;} => {  const nextHeaders: Record<string, ConfiguredHeaderValue> = {};  const bindings: Array<{    slot: string;    value: OpenApiSourceBindingValue;    targetScope?: string;  }> = [];  for (const [name, value] of Object.entries(headers ?? {})) {    if (typeof value === "string") {      nextHeaders[name] = value;      continue;    }    if ("kind" in value) {      nextHeaders[name] = value;      continue;    }    const slot = headerSlotFromName(name);    nextHeaders[name] = ConfiguredHeaderBinding.make({      kind: "binding",      slot,      prefix: value.prefix,    });    bindings.push({      slot,      targetScope: "targetScope" in value ? value.targetScope : undefined,      value: {        kind: "secret",        secretId: SecretId.make(value.secretId),        ...("secretScopeId" in value && value.secretScopeId          ? { secretScopeId: value.secretScopeId }          : {}),      },    });  }  return { headers: nextHeaders, bindings };}; const canonicalizeCredentialMap = (  values: Record<string, OpenApiCredentialInput> | undefined,  slotForName: (name: string) => string,): {  readonly values: Record<string, ConfiguredHeaderValue>;  readonly bindings: ReadonlyArray<{    readonly slot: string;    readonly value: OpenApiSourceBindingValue;    readonly targetScope?: string;  }>;} => {  const nextValues: Record<string, ConfiguredHeaderValue> = {};  const bindings: Array<{    slot: string;    value: OpenApiSourceBindingValue;    targetScope?: string;  }> = [];  for (const [name, value] of Object.entries(values ?? {})) {    if (typeof value === "string") {      nextValues[name] = value;      continue;    }    if ("kind" in value) {      nextValues[name] = value;      continue;    }    const slot = slotForName(name);    nextValues[name] = ConfiguredHeaderBinding.make({      kind: "binding",      slot,      prefix: value.prefix,    });    bindings.push({      slot,      targetScope: "targetScope" in value ? value.targetScope : undefined,      value: {        kind: "secret",        secretId: SecretId.make(value.secretId),        ...("secretScopeId" in value && value.secretScopeId          ? { secretScopeId: value.secretScopeId }          : {}),      },    });  }  return { values: nextValues, bindings };}; const canonicalizeSpecFetchCredentials = (  credentials:    | {        readonly headers?: Record<string, OpenApiCredentialInput>;        readonly queryParams?: Record<string, OpenApiCredentialInput>;      }    | undefined,): {  readonly credentials?: SourceConfig["specFetchCredentials"];  readonly bindings: ReadonlyArray<{    readonly slot: string;    readonly value: OpenApiSourceBindingValue;    readonly targetScope?: string;  }>;} => {  const headers = canonicalizeCredentialMap(credentials?.headers, specFetchHeaderSlotFromName);  const queryParams = canonicalizeCredentialMap(    credentials?.queryParams,    specFetchQueryParamSlotFromName,  );  const nextCredentials =    Object.keys(headers.values).length === 0 && Object.keys(queryParams.values).length === 0      ? undefined      : {          ...(Object.keys(headers.values).length > 0 ? { headers: headers.values } : {}),          ...(Object.keys(queryParams.values).length > 0            ? { queryParams: queryParams.values }            : {}),        };  return {    credentials: nextCredentials,    bindings: [...headers.bindings, ...queryParams.bindings],  };}; const canonicalizeOAuth2 = (  oauth2: OpenApiOAuthInput | undefined,): {  readonly oauth2?: OAuth2SourceConfig;  readonly bindings: ReadonlyArray<{    readonly slot: string;    readonly value: OpenApiSourceBindingValue;  }>;} => {  if (!oauth2) return { bindings: [] };  return {    oauth2,    bindings: [],  };}; interface EffectiveSourceConfig {  readonly config: SourceConfig;  readonly headersSource: StoredSource;  readonly queryParamsSource: StoredSource;  readonly specFetchCredentialsSource: StoredSource;  readonly oauth2Source: StoredSource;} const OPENAPI_PLUGIN_ID = "openapi"; const scopeRanks = (ctx: PluginCtx<OpenapiStore>): 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 coreBindingToOpenApiBinding = (binding: CredentialBindingRef): OpenApiSourceBindingRef =>  OpenApiSourceBindingRef.make({    sourceId: binding.sourceId,    sourceScopeId: binding.sourceScopeId,    scopeId: binding.scopeId,    slot: binding.slotKey,    value: binding.value,    createdAt: binding.createdAt,    updatedAt: binding.updatedAt,  }); const listOpenApiSourceBindings = (  ctx: PluginCtx<OpenapiStore>,  sourceId: string,  sourceScope: string,): Effect.Effect<readonly OpenApiSourceBindingRef[], StorageFailure> =>  Effect.gen(function* () {    const ranks = scopeRanks(ctx);    const sourceSourceRank = scopeRank(ranks, sourceScope);    if (sourceSourceRank === Infinity) return [];    const bindings = yield* ctx.credentialBindings.listForSource({      pluginId: OPENAPI_PLUGIN_ID,      sourceId,      sourceScope: ScopeId.make(sourceScope),    });    return bindings      .filter((binding) => scopeRank(ranks, binding.scopeId) <= sourceSourceRank)      .map(coreBindingToOpenApiBinding);  }); const resolveOpenApiSourceBinding = (  ctx: PluginCtx<OpenapiStore>,  sourceId: string,  sourceScope: string,  slot: string,): Effect.Effect<OpenApiSourceBindingRef | 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: OPENAPI_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 ? coreBindingToOpenApiBinding(binding) : null;  }); const validateOpenApiBindingTarget = (  ctx: PluginCtx<OpenapiStore>,  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);

Lines 1–500 of 1395.