725 lines · 30.0 KB · TypeScript
RawDownload
import { Effect, Option, Predicate } from "effect";import { Schema } from "effect"; import {  HealthCheckCandidate,  compareHealthCheckCandidates,  projectResponseFields,} from "@executor-js/sdk/core"; import { parse, resolveSpecText, type ParsedDocument } from "./parse";import {  extract,  streamOutputSchemas,  streamPreviewOperations,  type StreamedPreviewOperation,} from "./extract";import { compileToolDefinitions } from "./definitions";import { normalizeOpenApiRefs } from "./backing";import { OpenApiExtractionError } from "./errors";import { DocResolver } from "./openapi-utils";import {  collectReferencedSchemas,  indexSchemas,  structuralSplit,  type KeepPathItem,} from "./split";import { HttpMethod, ServerInfo, type ExtractedOperation, type ExtractionResult } from "./types";import { getHealthCheckParameters } from "./health-check-operation"; // Keep potentially mutating methods ranked below ordinary reads. This mirrors// REQUIRE_APPROVAL without importing the HTTP execution path into the browser.const DESTRUCTIVE_METHODS = new Set(["post", "put", "patch", "delete"]); // Cap on health-check candidate METADATA carried in the preview, so the add// screen's operation picker can search the whole spec (not just a top few).// Building this is cheap (the rank/sort already runs over every operation); only// the payload grows, so the cap bounds Graph-sized specs (16k+ ops) to the// top-ranked 1000. Beyond that, the picker stays freeform (type an exact op).const MAX_PREVIEW_CANDIDATES = 1000; // Cap on how many carried candidates get their response schema WALKED for the// typed identity picker. Walking is the only expensive part, so it stays small:// the top-ranked survivors get typed identity fields; the long tail is// metadata-only and its identity picker falls back to a freeform dot-path.const MAX_PREVIEW_RESPONSE_FIELD_CANDIDATES = 50; // ---------------------------------------------------------------------------// OAuth 2.0 flows — one entry per supported grant type// --------------------------------------------------------------------------- /** Scopes declared by a flow: `{ scopeName: description }` */const OAuth2Scopes = Schema.Record(Schema.String, Schema.String);const SecuritySchemeType = Schema.Literals(["http", "apiKey", "oauth2", "openIdConnect"]);type SecuritySchemeType = typeof SecuritySchemeType.Type; const decodeSecuritySchemeType = Schema.decodeUnknownOption(SecuritySchemeType); export const OAuth2AuthorizationCodeFlow = Schema.Struct({  authorizationUrl: Schema.String,  tokenUrl: Schema.String,  refreshUrl: Schema.OptionFromOptional(Schema.String),  scopes: OAuth2Scopes,});export type OAuth2AuthorizationCodeFlow = typeof OAuth2AuthorizationCodeFlow.Type; export const OAuth2ClientCredentialsFlow = Schema.Struct({  tokenUrl: Schema.String,  refreshUrl: Schema.OptionFromOptional(Schema.String),  scopes: OAuth2Scopes,});export type OAuth2ClientCredentialsFlow = typeof OAuth2ClientCredentialsFlow.Type; export const OAuth2Flows = Schema.Struct({  authorizationCode: Schema.OptionFromOptional(OAuth2AuthorizationCodeFlow),  clientCredentials: Schema.OptionFromOptional(OAuth2ClientCredentialsFlow),});export type OAuth2Flows = typeof OAuth2Flows.Type; // ---------------------------------------------------------------------------// Security scheme — what the spec declares it needs// --------------------------------------------------------------------------- export const SecurityScheme = Schema.Struct({  /** Key name in components.securitySchemes (e.g. "api_token") */  name: Schema.String,  /** OpenAPI security scheme type */  type: SecuritySchemeType,  /** For type: "http" — e.g. "bearer", "basic" */  scheme: Schema.OptionFromOptional(Schema.String),  /** For type: "http" with scheme "bearer" — e.g. "JWT" */  bearerFormat: Schema.OptionFromOptional(Schema.String),  /** For type: "apiKey" — where the key goes */  in: Schema.OptionFromOptional(Schema.Literals(["header", "query", "cookie"])),  /** For type: "apiKey" — the header/query/cookie name */  headerName: Schema.OptionFromOptional(Schema.String),  description: Schema.OptionFromOptional(Schema.String),  /** For type: "oauth2" — declared flows (authorizationCode / clientCredentials only; implicit and password are deprecated). */  flows: Schema.OptionFromOptional(OAuth2Flows),  /** For type: "openIdConnect" — the discovery URL. */  openIdConnectUrl: Schema.OptionFromOptional(Schema.String),});export type SecurityScheme = typeof SecurityScheme.Type; // ---------------------------------------------------------------------------// Auth strategy — a valid combination of security schemes// --------------------------------------------------------------------------- export const AuthStrategy = Schema.Struct({  /** The security schemes required together for this strategy */  schemes: Schema.Array(Schema.String),});export type AuthStrategy = typeof AuthStrategy.Type; // ---------------------------------------------------------------------------// Header preset — derived from an auth strategy// --------------------------------------------------------------------------- export const HeaderPreset = Schema.Struct({  /** Human-readable label for the UI (e.g. "Bearer Token", "API Key + Email") */  label: Schema.String,  /** Headers this strategy needs. Value is null when the user must provide it. */  headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)),  /** Which headers should be stored as secrets */  secretHeaders: Schema.Array(Schema.String),  /** Query parameters the strategy sends the secret in (apiKey in=query,   *  e.g. Viator's legacy `?apiKey=`). Absent on older stored previews. */  secretQueryParams: Schema.optional(Schema.Array(Schema.String)),});export type HeaderPreset = typeof HeaderPreset.Type; // ---------------------------------------------------------------------------// OAuth2 preset — derived from an oauth2 security scheme + a flow choice// --------------------------------------------------------------------------- export const OAuth2Preset = Schema.Struct({  /** Human-readable label for the UI (e.g. "OAuth2 (Authorization Code) — oauth_app") */  label: Schema.String,  /** The source security scheme this preset came from (components.securitySchemes key). */  securitySchemeName: Schema.String,  /** Which OAuth2 flow this preset uses. */  flow: Schema.Literals(["authorizationCode", "clientCredentials"]),  /** For authorizationCode: user-agent redirect URL (from the spec). */  authorizationUrl: Schema.OptionFromOptional(Schema.String),  /** Token endpoint to exchange the code / refresh. */  tokenUrl: Schema.String,  /** RFC 8707 resource indicator discovered from protected-resource metadata. */  resource: Schema.OptionFromOptional(Schema.String),  /** Optional refresh endpoint if the spec declares one separately. */  refreshUrl: Schema.OptionFromOptional(Schema.String),  /** Declared scopes for this flow: `{ scope: description }`. */  scopes: Schema.Record(Schema.String, Schema.String),  /** Identity scopes to request alongside API scopes. `"auto"` discovers standard OIDC scopes. */  identityScopes: Schema.Union([    Schema.Literal("auto"),    Schema.Literal(false),    Schema.Array(Schema.String),  ]),  /** Provider metadata advertised Client ID Metadata Document support. */  supportsClientIdMetadataDocument: Schema.optional(Schema.Boolean),});export type OAuth2Preset = typeof OAuth2Preset.Type; // ---------------------------------------------------------------------------// Preview operation — lightweight shape for the add-integration UI list// --------------------------------------------------------------------------- export const PreviewOperation = Schema.Struct({  operationId: Schema.String,  method: HttpMethod,  path: Schema.String,  summary: Schema.OptionFromOptional(Schema.String),  tags: Schema.Array(Schema.String),  deprecated: Schema.Boolean,});export type PreviewOperation = typeof PreviewOperation.Type; // ---------------------------------------------------------------------------// Spec preview — everything the frontend needs// --------------------------------------------------------------------------- export const SpecPreview = Schema.Struct({  title: Schema.OptionFromOptional(Schema.String),  /** The spec's `info.description` — prefills the add form's description field. */  description: Schema.OptionFromOptional(Schema.String),  version: Schema.OptionFromOptional(Schema.String),  /** Reuses ServerInfo from extraction */  servers: Schema.Array(ServerInfo),  operationCount: Schema.Number,  /** Lightweight operation list for the add-integration UI */  operations: Schema.Array(PreviewOperation),  tags: Schema.Array(Schema.String),  securitySchemes: Schema.Array(SecurityScheme),  /** Valid auth strategies (each is a set of schemes used together) */  authStrategies: Schema.Array(AuthStrategy),  /** Pre-built header presets derived from auth strategies */  headerPresets: Schema.Array(HeaderPreset),  /** OAuth2 presets — one per (oauth2 scheme × supported flow) combination */  oauth2Presets: Schema.Array(OAuth2Preset),  /** Top-ranked health-check candidates (bounded), so the add screen can offer a   *  typed operation + identity picker before the integration is registered. */  healthCheckCandidates: Schema.Array(HealthCheckCandidate),});export type SpecPreview = typeof SpecPreview.Type; // HTTP/UI preview deliberately omits the per-operation list. Graph-sized specs// can define 16k+ operations, while the add flow only needs counts, tags,// servers, and auth metadata before registration.export const SpecPreviewSummary = Schema.Struct({  title: Schema.OptionFromOptional(Schema.String),  description: Schema.OptionFromOptional(Schema.String),  version: Schema.OptionFromOptional(Schema.String),  servers: Schema.Array(ServerInfo),  operationCount: Schema.Number,  tags: Schema.Array(Schema.String),  securitySchemes: Schema.Array(SecurityScheme),  authStrategies: Schema.Array(AuthStrategy),  headerPresets: Schema.Array(HeaderPreset),  oauth2Presets: Schema.Array(OAuth2Preset),  healthCheckCandidates: Schema.Array(HealthCheckCandidate),});export type SpecPreviewSummary = typeof SpecPreviewSummary.Type; export const specPreviewSummary = (preview: SpecPreview): SpecPreviewSummary =>  SpecPreviewSummary.make({    title: preview.title,    description: preview.description,    version: preview.version,    servers: preview.servers,    operationCount: preview.operationCount,    tags: preview.tags,    securitySchemes: preview.securitySchemes,    authStrategies: preview.authStrategies,    headerPresets: preview.headerPresets,    oauth2Presets: preview.oauth2Presets,    healthCheckCandidates: preview.healthCheckCandidates,  }); // ---------------------------------------------------------------------------// Security scheme extraction// --------------------------------------------------------------------------- const stringRecord = (value: unknown): Record<string, string> => {  if (!value || typeof value !== "object" || Array.isArray(value)) return {};  const out: Record<string, string> = {};  for (const [k, v] of Object.entries(value as Record<string, unknown>)) {    if (typeof v === "string") out[k] = v;  }  return out;}; const extractFlows = (rawFlows: unknown): Option.Option<OAuth2Flows> => {  if (!rawFlows || typeof rawFlows !== "object") return Option.none();  const flows = rawFlows as Record<string, unknown>;   const parseFlow = <K extends "authorizationCode" | "clientCredentials">(key: K): unknown =>    flows[key];   let authorizationCode: Option.Option<OAuth2AuthorizationCodeFlow> = Option.none();  const authCodeRaw = parseFlow("authorizationCode");  if (authCodeRaw && typeof authCodeRaw === "object") {    const f = authCodeRaw as Record<string, unknown>;    const authUrl = typeof f.authorizationUrl === "string" ? f.authorizationUrl : null;    const tokenUrl = typeof f.tokenUrl === "string" ? f.tokenUrl : null;    if (authUrl && tokenUrl) {      authorizationCode = Option.some(        OAuth2AuthorizationCodeFlow.make({          authorizationUrl: authUrl,          tokenUrl,          refreshUrl: Option.fromNullishOr(            typeof f.refreshUrl === "string" ? f.refreshUrl : undefined,          ),          scopes: stringRecord(f.scopes),        }),      );    }  }   let clientCredentials: Option.Option<OAuth2ClientCredentialsFlow> = Option.none();  const ccRaw = parseFlow("clientCredentials");  if (ccRaw && typeof ccRaw === "object") {    const f = ccRaw as Record<string, unknown>;    const tokenUrl = typeof f.tokenUrl === "string" ? f.tokenUrl : null;    if (tokenUrl) {      clientCredentials = Option.some(        OAuth2ClientCredentialsFlow.make({          tokenUrl,          refreshUrl: Option.fromNullishOr(            typeof f.refreshUrl === "string" ? f.refreshUrl : undefined,          ),          scopes: stringRecord(f.scopes),        }),      );    }  }   if (Option.isNone(authorizationCode) && Option.isNone(clientCredentials)) {    return Option.none();  }  return Option.some(OAuth2Flows.make({ authorizationCode, clientCredentials }));}; const extractSecuritySchemes = (  rawSchemes: Record<string, unknown>,  resolver: DocResolver,): SecurityScheme[] =>  Object.entries(rawSchemes).flatMap(([name, schemeOrRef]) => {    if (!schemeOrRef || typeof schemeOrRef !== "object") return [];    // Resolve $ref so schemes defined via `$ref` aren't silently dropped.    const resolved = resolver.resolve<Record<string, unknown>>(      schemeOrRef as Record<string, unknown>,    );    if (!resolved || typeof resolved !== "object") return [];    const scheme = resolved;     const type = decodeSecuritySchemeType(scheme.type);    if (Option.isNone(type)) return [];    const schemeType = type.value;     return [      SecurityScheme.make({        name,        type: schemeType,        scheme: Option.fromNullishOr(scheme.scheme as string | undefined),        bearerFormat: Option.fromNullishOr(scheme.bearerFormat as string | undefined),        in: Option.fromNullishOr(scheme.in as "header" | "query" | "cookie" | undefined),        headerName: Option.fromNullishOr(scheme.name as string | undefined),        description: Option.fromNullishOr(scheme.description as string | undefined),        flows: schemeType === "oauth2" ? extractFlows(scheme.flows) : Option.none(),        openIdConnectUrl: Option.fromNullishOr(scheme.openIdConnectUrl as string | undefined),      }),    ];  }); // ---------------------------------------------------------------------------// Header preset builder// --------------------------------------------------------------------------- const buildHeaderPresets = (  schemes: readonly SecurityScheme[],  strategies: readonly AuthStrategy[],): HeaderPreset[] => {  const schemeMap = new Map(schemes.map((s) => [s.name, s]));   return strategies.flatMap((strategy) => {    const resolved = strategy.schemes      .map((name) => schemeMap.get(name))      .filter(Predicate.isNotUndefined);     if (resolved.length === 0) return [];     const headers: Record<string, string | null> = {};    const secretHeaders: string[] = [];    const secretQueryParams: string[] = [];    const labelParts: string[] = [];     for (const scheme of resolved) {      if (scheme.type === "http" && Option.getOrElse(scheme.scheme, () => "") === "bearer") {        headers["Authorization"] = null;        secretHeaders.push("Authorization");        labelParts.push("Bearer Token");      } else if (scheme.type === "http" && Option.getOrElse(scheme.scheme, () => "") === "basic") {        headers["Authorization"] = null;        secretHeaders.push("Authorization");        labelParts.push("Basic Auth");      } else if (scheme.type === "apiKey" && Option.getOrElse(scheme.in, () => "") === "header") {        const headerName = Option.getOrElse(scheme.headerName, () => scheme.name);        headers[headerName] = null;        secretHeaders.push(headerName);        labelParts.push(scheme.name);      } else if (scheme.type === "apiKey" && Option.getOrElse(scheme.in, () => "") === "query") {        secretQueryParams.push(Option.getOrElse(scheme.headerName, () => scheme.name));        labelParts.push(`${scheme.name} (query)`);      } else if (scheme.type === "apiKey") {        // Cookie (and unknown) locations are not renderable as a stored        // method — auth placements carry header|query — and a cookie scheme        // is usually the vendor console's own session, not a mintable        // credential. Contributing a label here used to produce a method        // with zero placements: an empty, unfillable card in the add flow.        continue;      } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {        return [];      } else {        labelParts.push(scheme.name);      }    }     // A strategy in which nothing is renderable (cookie-only, or an exotic    // scheme type) yields no preset rather than an empty one.    if (secretHeaders.length === 0 && secretQueryParams.length === 0) return [];     return [      HeaderPreset.make({        label: labelParts.join(" + "),        headers,        secretHeaders,        ...(secretQueryParams.length > 0 ? { secretQueryParams } : {}),      }),    ];  });}; // ---------------------------------------------------------------------------// OAuth2 preset builder// --------------------------------------------------------------------------- const buildOAuth2Presets = (schemes: readonly SecurityScheme[]): OAuth2Preset[] => {  const presets: OAuth2Preset[] = [];  for (const scheme of schemes) {    if (scheme.type !== "oauth2") continue;    if (Option.isNone(scheme.flows)) continue;    const flows = scheme.flows.value;     if (Option.isSome(flows.authorizationCode)) {      const flow = flows.authorizationCode.value;      presets.push(        OAuth2Preset.make({          label: `OAuth2 Authorization Code · ${scheme.name}`,          securitySchemeName: scheme.name,          flow: "authorizationCode",          authorizationUrl: Option.some(flow.authorizationUrl),          tokenUrl: flow.tokenUrl,          resource: Option.none(),          refreshUrl: flow.refreshUrl,          scopes: flow.scopes,          identityScopes: "auto",        }),      );    }     if (Option.isSome(flows.clientCredentials)) {      const flow = flows.clientCredentials.value;      presets.push(        OAuth2Preset.make({          label: `OAuth2 Client Credentials · ${scheme.name}`,          securitySchemeName: scheme.name,          flow: "clientCredentials",          authorizationUrl: Option.none(),          tokenUrl: flow.tokenUrl,          resource: Option.none(),          refreshUrl: flow.refreshUrl,          scopes: flow.scopes,          identityScopes: false,        }),      );    }  }  return presets;}; // ---------------------------------------------------------------------------// Collect unique tags from extraction result// --------------------------------------------------------------------------- const collectTags = (result: ExtractionResult): string[] => {  const tagSet = new Set<string>();  for (const op of result.operations) {    for (const tag of op.tags) tagSet.add(tag);  }  return [...tagSet].sort();}; // ---------------------------------------------------------------------------// Health-check candidates (bounded) for the add screen// --------------------------------------------------------------------------- /** * Project the top-ranked health-check candidates from a parsed doc + its * extracted operations, so the add screen can offer a typed operation/identity * picker before registration. * * Tool paths are computed on the FULL operation set (`compileToolDefinitions` * collision resolution is stateful, so they must match the paths the operations * get at registration). Candidates are ranked, sliced to `MAX_PREVIEW_CANDIDATES` * (enough for the operation picker to search the whole spec), and only the * top `MAX_PREVIEW_RESPONSE_FIELD_CANDIDATES` get their response schema walked for * the typed identity field; the rest stay metadata-only (freeform identity). */const buildPreviewHealthCheckCandidates = (  doc: ParsedDocument,  operations: readonly ExtractedOperation[],): HealthCheckCandidate[] => {  if (operations.length === 0) return [];   const definitions = compileToolDefinitions(operations);   const ranked = definitions    .map((def): HealthCheckCandidate => {      const op = def.operation;      const method = op.method.toLowerCase();      const parameters = getHealthCheckParameters(op);      return {        operation: def.toolPath,        method,        requiredArgCount: parameters.filter((parameter) => parameter.required).length,        destructive: DESTRUCTIVE_METHODS.has(method),        summary:          Option.getOrUndefined(op.summary) ??          Option.getOrUndefined(op.description) ??          `${method.toUpperCase()} ${op.pathTemplate}`,        ...(parameters.length > 0 ? { parameters } : {}),

Lines 1–500 of 725.