1090 lines · 37.9 KB · TypeScript
RawDownload
// Converts Google Discovery documents directly into OpenAPI 3.x. Public// Discovery converters currently target Swagger 2.0 or a broad conversion// pipeline; this adapter emits the shape Executor parses while preserving// Executor-specific tool ids and query semantics.import { Effect, Option, Predicate, Schema, SchemaGetter } from "effect";import { HttpClient, HttpClientRequest } from "effect/unstable/http"; import { OpenApiParseError, type Authentication } from "@executor-js/plugin-openapi";import { compactGoogleOAuthScopes } from "./oauth-scopes";import { AuthTemplateSlug } from "@executor-js/sdk/shared"; interface SpecFetchCredentials {  readonly headers?: Record<string, string>;  readonly queryParams?: Record<string, string>;} const DISCOVERY_SERVICE_HOST = "https://www.googleapis.com/discovery/v1/apis";const GOOGLE_BUNDLE_BASE_URL = "https://www.googleapis.com/";const GOOGLE_OAUTH_AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth";const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";const GOOGLE_PHOTOS_PICKER_SERVICE = "photospicker";const GOOGLE_PHOTOS_PICKER_SCOPE =  "https://www.googleapis.com/auth/photospicker.mediaitems.readonly";const GOOGLE_PHOTOS_PICKER_SCOPE_DESCRIPTION = "Read selected Google Photos media";const OPENAPI_SCHEMA_TYPES = new Set([  "array",  "boolean",  "integer",  "null",  "number",  "object",  "string",]); type GoogleDiscoveryServiceOverride = {  readonly preserveServiceHostedUrl?: true;  readonly scopes?: Record<string, string>;  readonly fallbackMethodScopes?: readonly string[];}; const GOOGLE_DISCOVERY_SERVICE_OVERRIDES: Record<string, GoogleDiscoveryServiceOverride> = {  forms: { preserveServiceHostedUrl: true },  keep: { preserveServiceHostedUrl: true },  [GOOGLE_PHOTOS_PICKER_SERVICE]: {    preserveServiceHostedUrl: true,    scopes: {      [GOOGLE_PHOTOS_PICKER_SCOPE]: GOOGLE_PHOTOS_PICKER_SCOPE_DESCRIPTION,    },    fallbackMethodScopes: [GOOGLE_PHOTOS_PICKER_SCOPE],  },}; const googleDiscoveryUrlForService = (  service: string,  version: string,  host = `${service}.googleapis.com`,): string => {  const override = GOOGLE_DISCOVERY_SERVICE_OVERRIDES[service];  return override?.preserveServiceHostedUrl === true    ? `https://${host}/$discovery/rest?version=${version}`    : `${DISCOVERY_SERVICE_HOST}/${service}/${version}/rest`;}; type JsonPrimitive = string | number | boolean | null;type JsonValue = JsonPrimitive | readonly JsonValue[] | { readonly [key: string]: JsonValue }; type OpenApiSchemaObject = {  readonly $ref?: string;  readonly type?: "array" | "boolean" | "integer" | "null" | "number" | "object" | "string";  readonly description?: string;  readonly title?: string;  readonly format?: string;  readonly readOnly?: boolean;  readonly default?: JsonValue;  readonly enum?: readonly JsonValue[];  readonly items?: OpenApiSchemaObject;  readonly properties?: Record<string, OpenApiSchemaObject>;  readonly required?: readonly string[];  readonly additionalProperties?: boolean | OpenApiSchemaObject;}; type OpenApiParameterObject = {  readonly name: string;  readonly in: "path" | "query" | "header";  readonly required: boolean;  readonly description?: string;  readonly schema: OpenApiSchemaObject;  readonly style?: "form";  readonly explode?: boolean;  readonly allowReserved?: boolean;}; type OpenApiOperationObject = {  readonly operationId: string;  readonly "x-executor-toolPath": string;  readonly "x-executor-pathTemplate"?: string;  readonly tags?: readonly string[];  readonly description?: string;  readonly servers?: readonly { readonly url: string }[];  readonly parameters: readonly OpenApiParameterObject[];  readonly requestBody?: {    readonly required: boolean;    readonly content: Record<      string,      {        readonly schema: OpenApiSchemaObject;      }    >;  };  readonly responses: {    readonly "200": {      readonly description: "Successful response";      readonly content: Record<        string,        {          readonly schema: OpenApiSchemaObject;        }      >;    };  };  readonly security?: readonly Record<string, readonly string[]>[];  readonly "x-google-scopes": readonly string[];}; type OpenApiDocument = {  readonly openapi: "3.1.0";  readonly info: {    readonly title: string;    readonly version: string;  };  readonly servers: readonly { readonly url: string }[];  readonly paths: Record<string, Record<string, OpenApiOperationObject>>;  readonly components: {    readonly schemas: Record<string, OpenApiSchemaObject>;    readonly securitySchemes?: Record<      string,      {        readonly type: "oauth2";        readonly flows: {          readonly authorizationCode: {            readonly authorizationUrl: string;            readonly tokenUrl: string;            readonly scopes: Record<string, string>;          };        };      }    >;  };  readonly security?: readonly Record<string, readonly string[]>[];  readonly "x-executor-origin":    | {        readonly kind: "googleDiscovery";        readonly discoveryUrl: string;        readonly service: string;        readonly version: string;      }    | {        readonly kind: "googleDiscoveryBundle";        readonly services: readonly {          readonly discoveryUrl: string;          readonly service: string;          readonly version: string;        }[];      };}; const TextOption = Schema.OptionFromOptional(Schema.Trim).pipe(  Schema.decode({    decode: SchemaGetter.transform((value) => Option.filter(value, (text) => text.length > 0)),    encode: SchemaGetter.transform((value) => value),  }),  Schema.withDecodingDefaultType(Effect.succeed(Option.none())),);const TextArray = Schema.optional(Schema.Array(Schema.String)).pipe(  Schema.withDecodingDefaultType(Effect.succeed([] as string[])),);const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown);const UnknownRecordWithDefault = Schema.optional(UnknownRecord).pipe(  Schema.withDecodingDefaultType(Effect.succeed({})),); const DiscoveryParameter = Schema.Struct({  type: Schema.optional(Schema.String),  description: TextOption,  properties: UnknownRecordWithDefault,  items: Schema.optional(Schema.Unknown),  additionalProperties: Schema.optional(Schema.Union([Schema.Boolean, Schema.Unknown])),  enum: TextArray,  format: Schema.optional(Schema.String),  readOnly: Schema.optional(Schema.Boolean),  default: Schema.optional(Schema.Union([Schema.String, Schema.Number, Schema.Boolean])),  $ref: Schema.optional(Schema.String),  location: Schema.optional(Schema.Literals(["path", "query", "header"])),  required: Schema.optional(Schema.Boolean),  repeated: Schema.optional(Schema.Boolean),});type DiscoveryParameter = typeof DiscoveryParameter.Type; const DiscoveryRef = Schema.Struct({  $ref: Schema.optional(Schema.String),}); const DiscoveryMethod = Schema.Struct({  id: TextOption,  description: TextOption,  httpMethod: Schema.optional(Schema.String),  path: TextOption,  parameters: UnknownRecordWithDefault,  request: Schema.optional(DiscoveryRef),  response: Schema.optional(DiscoveryRef),  scopes: TextArray,  supportsMediaDownload: Schema.optional(Schema.Boolean),  useMediaDownloadService: Schema.optional(Schema.Boolean),});type DiscoveryMethod = typeof DiscoveryMethod.Type; const DiscoveryResource = Schema.Struct({  methods: UnknownRecordWithDefault,  resources: UnknownRecordWithDefault,}); const DiscoveryDocument = Schema.Struct({  name: TextOption,  version: TextOption,  title: TextOption,  rootUrl: TextOption,  servicePath: Schema.optional(Schema.Trim).pipe(    Schema.withDecodingDefaultType(Effect.succeed("")),  ),  parameters: UnknownRecordWithDefault,  methods: UnknownRecordWithDefault,  resources: UnknownRecordWithDefault,  schemas: UnknownRecordWithDefault,  auth: Schema.optional(    Schema.Struct({      oauth2: Schema.optional(        Schema.Struct({          scopes: Schema.optional(            Schema.Record(              Schema.String,              Schema.Struct({                description: TextOption,              }),            ),          ).pipe(Schema.withDecodingDefaultType(Effect.succeed({}))),        }),      ),    }),  ),});type DiscoveryDocument = typeof DiscoveryDocument.Type; export interface GoogleDiscoveryOpenApiConversion {  readonly specText: string;  readonly baseUrl: string;  readonly title: string;  readonly service: string;  readonly version: string;  readonly discoveryUrls?: readonly string[];  /** The v2 oauth auth template the converted integration declares, when the   *  Discovery document advertises OAuth2 scopes. */  readonly authenticationTemplate?: readonly Authentication[];} const decodeDiscoveryDocument = Schema.decodeUnknownSync(DiscoveryDocument);const decodeDiscoveryParameter = Schema.decodeUnknownSync(DiscoveryParameter);const decodeDiscoveryMethod = Schema.decodeUnknownSync(DiscoveryMethod);const decodeDiscoveryResource = Schema.decodeUnknownSync(DiscoveryResource);const parseJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); const DISCOVERY_SERVICE_PATH_RE =  /^\/discovery\/v1\/apis\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)\/rest\/?$/;const DISCOVERY_VERSION_RE = /^[A-Za-z0-9._-]+$/; const serviceFromGoogleApisHost = (host: string): string | null => {  if (!host.endsWith(".googleapis.com")) return null;  const rawService = host.slice(0, -".googleapis.com".length);  if (!rawService || rawService.includes(".")) return null;  const service =    rawService === "calendar-json"      ? "calendar"      : rawService.endsWith("-json")        ? rawService.slice(0, -5)        : rawService;  return /^[a-z0-9][a-z0-9-]*$/.test(service) ? service : null;}; export const normalizeGoogleDiscoveryUrl = (discoveryUrl: string): string | null => {  const trimmed = discoveryUrl.trim();  if (!URL.canParse(trimmed)) return null;  const parsed = new URL(trimmed);  if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.hash) {    return null;  }   const host = parsed.hostname.toLowerCase();  if (host === "www.googleapis.com") {    if (parsed.search) return null;    const match = parsed.pathname.match(DISCOVERY_SERVICE_PATH_RE);    const service = match?.[1];    const version = match?.[2];    return service && version ? googleDiscoveryUrlForService(service, version) : null;  }   const service = serviceFromGoogleApisHost(host);  if (!service || !["/$discovery/rest", "/$discovery/rest/"].includes(parsed.pathname)) {    return null;  }  const keys = [...parsed.searchParams.keys()];  const version = parsed.searchParams.get("version")?.trim();  if (    keys.length !== 1 ||    keys[0] !== "version" ||    !version ||    !DISCOVERY_VERSION_RE.test(version)  ) {    return null;  }  return googleDiscoveryUrlForService(service, version, host);}; const normalizeDiscoveryUrl = (discoveryUrl: string): string => {  return normalizeGoogleDiscoveryUrl(discoveryUrl) ?? discoveryUrl.trim();}; export const isGoogleDiscoveryUrl = (url: string): boolean => {  return normalizeGoogleDiscoveryUrl(url) !== null;}; export const fetchGoogleDiscoveryDocument = Effect.fn("OpenApi.fetchGoogleDiscoveryDocument")(  function* (discoveryUrl: string, credentials?: SpecFetchCredentials) {    const normalizedDiscoveryUrl = normalizeGoogleDiscoveryUrl(discoveryUrl);    if (!normalizedDiscoveryUrl) {      return yield* new OpenApiParseError({        message:          "Google Discovery document URL must be a supported googleapis.com HTTPS Discovery endpoint",      });    }    const client = yield* HttpClient.HttpClient;    const requestUrl = new URL(normalizedDiscoveryUrl);    for (const [name, value] of Object.entries(credentials?.queryParams ?? {})) {      requestUrl.searchParams.set(name, value);    }    let request = HttpClientRequest.get(requestUrl.toString()).pipe(      HttpClientRequest.setHeader("Accept", "application/json, */*"),    );    for (const [name, value] of Object.entries(credentials?.headers ?? {})) {      request = HttpClientRequest.setHeader(request, name, value);    }    const response = yield* client.execute(request).pipe(      Effect.mapError(        () =>          new OpenApiParseError({            message: "Failed to fetch Google Discovery document",          }),      ),    );    if (response.status < 200 || response.status >= 300) {      return yield* new OpenApiParseError({        message: `Failed to fetch Google Discovery document: HTTP ${response.status}`,      });    }    return yield* response.text.pipe(      Effect.mapError(        () =>          new OpenApiParseError({            message: "Failed to read Google Discovery document body",          }),      ),    );  },); const schemaRef = (name: string) => `#/components/schemas/${name}`; const identitySchemaName = (name: string): string => name; const schemaComponentPart = (value: string): string =>  value    .trim()    .replace(/[^A-Za-z0-9._-]+/g, "_")    .replace(/^_+|_+$/g, "") || "schema"; const normalizeDiscoveryPathTemplate = (pathTemplate: string): string =>  pathTemplate.replaceAll(/\{\+([^{}]+)\}/g, "{$1}"); const pathUsesReservedExpansion = (pathTemplate: string, parameterName: string): boolean =>  pathTemplate.includes(`{+${parameterName}}`); const uniquePathKey = (  paths: Record<string, Record<string, OpenApiOperationObject>>,  preferredPath: string,  method: string,  toolPath: string,): string => {  if (!paths[preferredPath]?.[method]) return preferredPath;  const disambiguated = `/${toolPath.replace(/[^A-Za-z0-9._~-]+/g, "/")}`;  if (!paths[disambiguated]?.[method]) return disambiguated;  let index = 2;  while (paths[`${disambiguated}/${index}`]?.[method]) index += 1;  return `${disambiguated}/${index}`;}; const discoveryDescription = (value: unknown): string | undefined =>  typeof value === "string"    ? value    : Option.isOption(value) && Option.isSome(value)      ? typeof value.value === "string"        ? value.value        : undefined      : undefined; const isRecord = (value: unknown): value is Record<string, unknown> =>  typeof value === "object" && value !== null && !Array.isArray(value); const jsonValue = (value: unknown): JsonValue | undefined => {  if (    value === null ||    typeof value === "string" ||    typeof value === "number" ||    typeof value === "boolean"  ) {    return value;  }  if (Array.isArray(value)) {    const values = value.map(jsonValue);    return values.every(Predicate.isNotUndefined) ? values : undefined;  }  if (!isRecord(value)) return undefined;  const entries = Object.entries(value).flatMap(([key, item]) => {    const converted = jsonValue(item);    return converted === undefined ? [] : [[key, converted] as const];  });  return Object.fromEntries(entries);}; const stringArray = (value: unknown): readonly string[] | undefined => {  if (!Array.isArray(value)) return undefined;  const strings = value.filter((item): item is string => typeof item === "string");  return strings.length === value.length ? strings : undefined;}; const schemaType = (value: unknown): OpenApiSchemaObject["type"] | undefined =>  typeof value === "string" && OPENAPI_SCHEMA_TYPES.has(value)    ? (value as OpenApiSchemaObject["type"])    : undefined; const discoverySchemaToOpenApiSchema = (  raw: unknown,  schemaNameForRef: (name: string) => string = identitySchemaName,): OpenApiSchemaObject => {  if (!isRecord(raw)) return {};  const schema = raw;  if (typeof schema.$ref === "string") return { $ref: schemaRef(schemaNameForRef(schema.$ref)) };   const description = discoveryDescription(schema.description);  const title = discoveryDescription(schema.title);  const defaultValue = jsonValue(schema.default);  const enumValues = Array.isArray(schema.enum)    ? schema.enum.map(jsonValue).filter(Predicate.isNotUndefined)    : [];  const format = typeof schema.format === "string" ? schema.format : undefined;  const readOnly = typeof schema.readOnly === "boolean" ? schema.readOnly : undefined;  const type = schemaType(schema.type);   const base = {    ...(description !== undefined ? { description } : {}),    ...(title !== undefined ? { title } : {}),    ...(format !== undefined ? { format } : {}),    ...(readOnly !== undefined ? { readOnly } : {}),    ...(defaultValue !== undefined ? { default: defaultValue } : {}),    ...(enumValues.length > 0 ? { enum: enumValues } : {}),  } satisfies OpenApiSchemaObject;   if (type === "array") {    return {      ...base,      type: "array",      items: discoverySchemaToOpenApiSchema(schema.items, schemaNameForRef),    };  }   const properties = schema.properties;  if (    type === "object" ||    (isRecord(properties) && Object.keys(properties).length > 0) ||    schema.additionalProperties !== undefined  ) {    const convertedProperties = isRecord(properties)      ? Object.fromEntries(          Object.entries(properties).map(([name, value]) => [            name,            discoverySchemaToOpenApiSchema(value, schemaNameForRef),          ]),        )      : undefined;    const required = stringArray(schema.required);    const additionalProperties =      schema.additionalProperties === undefined        ? undefined

Lines 1–500 of 1090.