// 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 DiscoveryMediaUploadProtocol = Schema.Struct({ path: TextOption, multipart: Schema.optional(Schema.Boolean),}); const DiscoveryMediaUploadProtocols = Schema.Struct({ simple: Schema.optional(DiscoveryMediaUploadProtocol), resumable: Schema.optional(DiscoveryMediaUploadProtocol),}); const DiscoveryMediaUpload = Schema.Struct({ accept: Schema.optional(TextArray), maxSize: Schema.optional(Schema.String), protocols: Schema.optional(DiscoveryMediaUploadProtocols),}); 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, supportsMediaUpload: Schema.optional(Schema.Boolean), mediaUpload: Schema.optional(DiscoveryMediaUpload), 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), }; } Lines 1–500 of 1258. Load more
// 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 DiscoveryMediaUploadProtocol = Schema.Struct({ path: TextOption, multipart: Schema.optional(Schema.Boolean), }); const DiscoveryMediaUploadProtocols = Schema.Struct({ simple: Schema.optional(DiscoveryMediaUploadProtocol), resumable: Schema.optional(DiscoveryMediaUploadProtocol), }); const DiscoveryMediaUpload = Schema.Struct({ accept: Schema.optional(TextArray), maxSize: Schema.optional(Schema.String), protocols: Schema.optional(DiscoveryMediaUploadProtocols), }); 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, supportsMediaUpload: Schema.optional(Schema.Boolean), mediaUpload: Schema.optional(DiscoveryMediaUpload), 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), }; }