import { Effect } from "effect";import type { Layer } from "effect";import { HttpClient } from "effect/unstable/http"; import { IntegrationAlreadyExistsError, IntegrationDetectionResult, IntegrationNotFoundError, IntegrationSlug, definePlugin, mergeAuthTemplates, sha256Hex, type AuthMethodDescriptor, type HealthCheckSpec, type Integration, type IntegrationConfig, type IntegrationRecord, type PluginCtx,} from "@executor-js/sdk/core";import { describeApiKeyAuthMethod } from "@executor-js/sdk/http-auth";import { checkHealthOpenApi, compileOpenApiSpec, invokeOpenApiBackedTool, listHealthCheckCandidatesOpenApi, makeDefaultOpenapiStore, normalizeOpenApiAuthInputs, OpenApiExtractionError, OpenApiParseError, openApiStoredOperationsFromCompiled, resolveOpenApiBackedAnnotations, resolveOpenApiBackedTools, type Authentication, type AuthenticationInput, type OpenapiStore,} from "@executor-js/plugin-openapi"; import { convertGoogleDiscoveryBundleToOpenApi, fetchGoogleDiscoveryDocument, googleDiscoveryDocumentIdentity, normalizeGoogleDiscoveryUrl, type GoogleDiscoveryDocumentIdentity,} from "./discovery";import { decodeGoogleIntegrationConfig, type GoogleIntegrationConfig } from "./config";import { googleOAuthConsentScopesForPreset, googleOpenApiBundlePreset, googleOpenApiPresets, googlePhotosOpenApiBundlePreset, googlePhotosOpenApiPresets, googleServiceSlug, type GoogleOpenApiPreset,} from "./presets"; const GOOGLE_OAUTH2_DISCOVERY_URL = "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest"; /** The default health check for a Google bundle: prefer the lightweight * OAuth2 userinfo identity call, which every new Google bundle includes. * Older bundles may only have People API, so keep that fallback. */export const defaultGoogleHealthCheck = ( urls: readonly string[], definitions: readonly { readonly toolPath: string; readonly operation: { readonly method: string; readonly pathTemplate: string }; }[],): HealthCheckSpec | undefined => { const userinfoGet = definitions.find( (def) => def.operation.method.toLowerCase() === "get" && (def.toolPath === "oauth2.userinfo.get" || def.operation.pathTemplate === "/oauth2/v2/userinfo"), ); if (userinfoGet) { return { operation: userinfoGet.toolPath, identityField: "email", }; } const hasPeopleApi = urls.some((url) => url.includes("/people/")); if (!hasPeopleApi) return undefined; const peopleGet = definitions.find( (def) => def.operation.method.toLowerCase() === "get" && (def.toolPath === "people.people.get" || def.operation.pathTemplate === "/v1/{+resourceName}"), ); return peopleGet ? { operation: peopleGet.toolPath, args: { resourceName: "people/me", personFields: "names,emailAddresses" }, identityField: "emailAddresses.0.value", } : undefined;}; export const GOOGLE_CUSTOM_SERVICE_ID = "custom"; export interface GooglePresetServiceConfig { readonly presetId: string; readonly slug?: string; readonly name?: string;} export interface GoogleCustomServiceConfig { readonly custom: { /** * Each Discovery URL is added as its own integration. `slug`, `name`, and * `description` are explicit overrides for a single URL; supplying them * with multiple URLs returns a failed row instead of guessing which service * the override belongs to. */ readonly urls: readonly string[]; readonly slug?: string; readonly name?: string; readonly description?: string; };} export type GoogleServiceConfig = GooglePresetServiceConfig | GoogleCustomServiceConfig; export interface GoogleAddServicesInput { readonly services: readonly GoogleServiceConfig[]; readonly baseUrl?: string;} export interface GoogleAddServicesAdded { readonly slug: IntegrationSlug; readonly presetId: string; readonly toolCount: number;} export interface GoogleAddServicesSkipped { readonly slug: IntegrationSlug; readonly presetId: string; readonly reason: "already_exists";} export interface GoogleAddServicesFailed { readonly slug: IntegrationSlug; readonly presetId: string; readonly error: string;} export interface GoogleAddServicesResult { readonly added: readonly GoogleAddServicesAdded[]; readonly skipped: readonly GoogleAddServicesSkipped[]; readonly failed: readonly GoogleAddServicesFailed[];} export interface GoogleConfigureInput { readonly authenticationTemplate: readonly AuthenticationInput[]; readonly mode?: "merge" | "replace";} export interface GoogleUpdateInput { readonly urls?: readonly string[];} export interface GoogleUpdateResult { readonly slug: IntegrationSlug; readonly toolCount: number; readonly addedTools: readonly string[]; readonly removedTools: readonly string[];} export interface GooglePluginOptions { readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;} const DEFAULT_GOOGLE_SLUG = "google";const DEFAULT_GOOGLE_CUSTOM_SLUG = "google_custom";const MAX_GOOGLE_CUSTOM_DESCRIPTION_LENGTH = 500; const googleOpenApiPresetById: ReadonlyMap<string, GoogleOpenApiPreset> = new Map( googleOpenApiPresets.map((preset) => [preset.id, preset]),); const isGooglePresetServiceConfig = ( service: GoogleServiceConfig,): service is GooglePresetServiceConfig => "presetId" in service; const googleServiceEntryId = (service: GoogleServiceConfig): string => isGooglePresetServiceConfig(service) ? service.presetId : GOOGLE_CUSTOM_SERVICE_ID; const googleServiceEntrySlug = (service: GoogleServiceConfig): IntegrationSlug => isGooglePresetServiceConfig(service) ? IntegrationSlug.make(service.slug?.trim() || googleServiceSlug(service.presetId)) : IntegrationSlug.make(service.custom.slug?.trim() || DEFAULT_GOOGLE_CUSTOM_SLUG); type GoogleAddServiceOutcome = { readonly added: readonly GoogleAddServicesAdded[]; readonly skipped: readonly GoogleAddServicesSkipped[]; readonly failed: readonly GoogleAddServicesFailed[];}; const googleAddServiceFailure = (input: { readonly slug: IntegrationSlug; readonly presetId: string; readonly error: string;}): GoogleAddServiceOutcome => ({ added: [], skipped: [], failed: [{ slug: input.slug, presetId: input.presetId, error: input.error }],}); type GoogleFetchedDiscoveryDocument = { readonly discoveryUrl: string; readonly documentText: string;}; const googlePhotosBundlePresetIdByUrl = new Map( googlePhotosOpenApiPresets.flatMap((preset) => preset.url ? [[normalizeGoogleDiscoveryUrl(preset.url) ?? preset.url, preset.id] as const] : [], ),); const googlePhotosBundleConsentScopes = ( urls: readonly string[],): readonly string[] | undefined => { const normalized = new Set(urls); const presetIds = [...googlePhotosBundlePresetIdByUrl.entries()].flatMap(([url, presetId]) => normalized.has(url) ? [presetId] : [], ); return presetIds.length > 0 ? presetIds.flatMap((presetId) => googleOAuthConsentScopesForPreset(presetId)) : undefined;}; const fetchGoogleDiscoveryDocuments = ( urls: readonly string[], httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>,) => Effect.forEach( urls, (url) => fetchGoogleDiscoveryDocument(url).pipe( Effect.provide(httpClientLayer), Effect.map( (documentText): GoogleFetchedDiscoveryDocument => ({ discoveryUrl: url, documentText }), ), ), { concurrency: 4 }, ); const fetchGoogleBundleConversion = ( urls: readonly string[], httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>, consentScopesOverride?: readonly string[],) => fetchGoogleDiscoveryDocuments(urls, httpClientLayer).pipe( Effect.flatMap((documents) => { const consentScopes = consentScopesOverride ?? googlePhotosBundleConsentScopes(urls); return convertGoogleDiscoveryBundleToOpenApi({ documents, ...(consentScopes !== undefined ? { consentScopes } : {}), }); }), ); const uniqueUrls = (urls: readonly string[]): readonly string[] => [ ...new Set(urls.flatMap((url) => normalizeGoogleDiscoveryUrl(url) ?? [])),]; const googleBundleUrlsWithIdentity = ( urls: readonly string[],): Effect.Effect<readonly string[], OpenApiParseError> => Effect.gen(function* () { const normalized: string[] = []; for (const url of urls) { const discoveryUrl = normalizeGoogleDiscoveryUrl(url); if (!discoveryUrl) { return yield* new OpenApiParseError({ message: "Google Discovery document URL must be a supported googleapis.com HTTPS Discovery endpoint", }); } normalized.push(discoveryUrl); } return uniqueUrls([...normalized, GOOGLE_OAUTH2_DISCOVERY_URL]); }); type GoogleServiceAddPlan = | { readonly kind: "preset"; readonly service: GooglePresetServiceConfig; } | { readonly kind: "custom"; readonly service: GoogleCustomServiceConfig; readonly url: string; readonly fallbackSlug: IntegrationSlug; } | { readonly kind: "invalid-custom"; readonly service: GoogleCustomServiceConfig; readonly slug: IntegrationSlug; readonly presetId: string; readonly error: string; }; const hasCustomGoogleServiceOverride = (service: GoogleCustomServiceConfig): boolean => Boolean( service.custom.slug?.trim() || service.custom.name?.trim() || service.custom.description?.trim(), ); const googleCustomFallbackSlug = (index: number): IntegrationSlug => IntegrationSlug.make( index === 0 ? DEFAULT_GOOGLE_CUSTOM_SLUG : `${DEFAULT_GOOGLE_CUSTOM_SLUG}_${index + 1}`, ); const googleCustomResultId = (slug: IntegrationSlug): string => String(slug); const googleDiscoveryNameSlugPart = (name: string): string => name .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, "_") .replace(/^_+|_+$/g, ""); const googleDiscoverySlug = (name: string): IntegrationSlug => IntegrationSlug.make( googleServiceSlug(`google-${googleDiscoveryNameSlugPart(name) || "custom"}`), ); const googleDiscoveryDisplayName = (identity: GoogleDiscoveryDocumentIdentity): string => { const title = identity.title ?.trim() .replace(/\s+API(?:\s+v[0-9][A-Za-z0-9._-]*)?$/i, "") .trim(); if (title && title.length > 0) { return /^google\b/i.test(title) ? title : `Google ${title}`; } return identity.name .trim() .replace(/[-_.]+/g, " ") .replace(/\b\w/g, (char) => char.toUpperCase());}; const googleDiscoveryDescription = ( identity: GoogleDiscoveryDocumentIdentity, displayName: string,): string => { const description = identity.description?.trim(); if (description && description.length > 0) { return description.length > MAX_GOOGLE_CUSTOM_DESCRIPTION_LENGTH ? `${description.slice(0, MAX_GOOGLE_CUSTOM_DESCRIPTION_LENGTH - 3).trimEnd()}...` : description; } return `${displayName}.`;}; const googleServiceAddPlans = ( services: readonly GoogleServiceConfig[],): readonly GoogleServiceAddPlan[] => { const plans: GoogleServiceAddPlan[] = []; let customIndex = 0; for (const service of services) { if (isGooglePresetServiceConfig(service)) { plans.push({ kind: "preset", service }); continue; } if (service.custom.urls.length === 0) { const slug = googleCustomFallbackSlug(customIndex); customIndex += 1; plans.push({ kind: "invalid-custom", service, slug, presetId: googleCustomResultId(slug), error: "Custom Google service requires at least one Discovery URL", }); continue; } if (service.custom.urls.length > 1 && hasCustomGoogleServiceOverride(service)) { const slug = googleServiceEntrySlug(service); plans.push({ kind: "invalid-custom", service, slug, presetId: googleCustomResultId(slug), error: "Custom Google service identity overrides require exactly one Discovery URL", }); continue; } for (const url of service.custom.urls) { const overrideSlug = service.custom.slug?.trim(); const fallbackSlug = overrideSlug && overrideSlug.length > 0 ? IntegrationSlug.make(overrideSlug) : googleCustomFallbackSlug(customIndex); plans.push({ kind: "custom", service, url, fallbackSlug, }); customIndex += 1; } } return plans;}; const describeGoogleAuthMethods = (record: IntegrationRecord): readonly AuthMethodDescriptor[] => { const config = decodeGoogleIntegrationConfig(record.config); if (!config) return []; return (config.authenticationTemplate ?? []).map( (template: Authentication): AuthMethodDescriptor => { if (template.kind === "oauth2") { return { id: String(template.slug), label: "OAuth2", kind: "oauth", template: String(template.slug), oauth: { authorizationUrl: template.authorizationUrl, tokenUrl: template.tokenUrl, scopes: template.scopes, }, }; } return describeApiKeyAuthMethod(template); }, );}; const describeGoogleIntegrationDisplay = (record: IntegrationRecord): { readonly url?: string } => { const config = decodeGoogleIntegrationConfig(record.config); return { url: config?.baseUrl ?? config?.googleDiscoveryUrls?.[0] };}; const makeGooglePluginExtension = ( options: GooglePluginOptions | undefined, ctx: PluginCtx<OpenapiStore>,) => { const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; const addGoogleOpenApiIntegrationFromDocuments = (input: { readonly urls: readonly string[]; readonly documents: readonly GoogleFetchedDiscoveryDocument[]; readonly slug: IntegrationSlug; readonly name: string; readonly description: string; readonly baseUrl?: string; readonly consentScopes?: readonly string[]; }) => Effect.gen(function* () { const consentScopes = input.consentScopes ?? googlePhotosBundleConsentScopes(input.urls); const conversion = yield* convertGoogleDiscoveryBundleToOpenApi({ documents: input.documents, ...(consentScopes !== undefined ? { consentScopes } : {}), }); const compiled = yield* compileOpenApiSpec(conversion.specText); const slug = input.slug; const existing = yield* ctx.core.integrations.get(slug); if (existing) { return yield* new IntegrationAlreadyExistsError({ slug }); } const specHash = yield* sha256Hex(conversion.specText); const integrationConfig: GoogleIntegrationConfig = { specHash, googleDiscoveryUrls: input.urls, ...(input.baseUrl ? { baseUrl: input.baseUrl } : {}), ...(conversion.authenticationTemplate ? { authenticationTemplate: conversion.authenticationTemplate } : {}), }; yield* ctx.storage.putSpec(specHash, conversion.specText); yield* ctx.storage.putDefs(specHash, JSON.stringify(compiled.hoistedDefs)); yield* ctx.transaction( Effect.gen(function* () { yield* ctx.core.integrations.register({ slug, name: input.name, description: input.description, config: integrationConfig satisfies GoogleIntegrationConfig as IntegrationConfig, canRemove: true, canRefresh: true, }); yield* ctx.storage.putOperations( String(slug), openApiStoredOperationsFromCompiled(String(slug), compiled), ); }), ); // Default the health check to the light OAuth2 userinfo identity call // added to every new Google OpenAPI integration. Older integrations // without oauth2/v2 can still fall back to the People API identity // operation.Lines 1–500 of 949. Load more
import { Effect } from "effect"; import type { Layer } from "effect"; import { HttpClient } from "effect/unstable/http"; import { IntegrationAlreadyExistsError, IntegrationDetectionResult, IntegrationNotFoundError, IntegrationSlug, definePlugin, mergeAuthTemplates, sha256Hex, type AuthMethodDescriptor, type HealthCheckSpec, type Integration, type IntegrationConfig, type IntegrationRecord, type PluginCtx, } from "@executor-js/sdk/core"; import { describeApiKeyAuthMethod } from "@executor-js/sdk/http-auth"; import { checkHealthOpenApi, compileOpenApiSpec, invokeOpenApiBackedTool, listHealthCheckCandidatesOpenApi, makeDefaultOpenapiStore, normalizeOpenApiAuthInputs, OpenApiExtractionError, OpenApiParseError, openApiStoredOperationsFromCompiled, resolveOpenApiBackedAnnotations, resolveOpenApiBackedTools, type Authentication, type AuthenticationInput, type OpenapiStore, } from "@executor-js/plugin-openapi"; import { convertGoogleDiscoveryBundleToOpenApi, fetchGoogleDiscoveryDocument, googleDiscoveryDocumentIdentity, normalizeGoogleDiscoveryUrl, type GoogleDiscoveryDocumentIdentity, } from "./discovery"; import { decodeGoogleIntegrationConfig, type GoogleIntegrationConfig } from "./config"; import { googleOAuthConsentScopesForPreset, googleOpenApiBundlePreset, googleOpenApiPresets, googlePhotosOpenApiBundlePreset, googlePhotosOpenApiPresets, googleServiceSlug, type GoogleOpenApiPreset, } from "./presets"; const GOOGLE_OAUTH2_DISCOVERY_URL = "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest"; /** The default health check for a Google bundle: prefer the lightweight * OAuth2 userinfo identity call, which every new Google bundle includes. * Older bundles may only have People API, so keep that fallback. */ export const defaultGoogleHealthCheck = ( urls: readonly string[], definitions: readonly { readonly toolPath: string; readonly operation: { readonly method: string; readonly pathTemplate: string }; }[], ): HealthCheckSpec | undefined => { const userinfoGet = definitions.find( (def) => def.operation.method.toLowerCase() === "get" && (def.toolPath === "oauth2.userinfo.get" || def.operation.pathTemplate === "/oauth2/v2/userinfo"), ); if (userinfoGet) { return { operation: userinfoGet.toolPath, identityField: "email", }; } const hasPeopleApi = urls.some((url) => url.includes("/people/")); if (!hasPeopleApi) return undefined; const peopleGet = definitions.find( (def) => def.operation.method.toLowerCase() === "get" && (def.toolPath === "people.people.get" || def.operation.pathTemplate === "/v1/{+resourceName}"), ); return peopleGet ? { operation: peopleGet.toolPath, args: { resourceName: "people/me", personFields: "names,emailAddresses" }, identityField: "emailAddresses.0.value", } : undefined; }; export const GOOGLE_CUSTOM_SERVICE_ID = "custom"; export interface GooglePresetServiceConfig { readonly presetId: string; readonly slug?: string; readonly name?: string; } export interface GoogleCustomServiceConfig { readonly custom: { /** * Each Discovery URL is added as its own integration. `slug`, `name`, and * `description` are explicit overrides for a single URL; supplying them * with multiple URLs returns a failed row instead of guessing which service * the override belongs to. */ readonly urls: readonly string[]; readonly slug?: string; readonly name?: string; readonly description?: string; }; } export type GoogleServiceConfig = GooglePresetServiceConfig | GoogleCustomServiceConfig; export interface GoogleAddServicesInput { readonly services: readonly GoogleServiceConfig[]; readonly baseUrl?: string; } export interface GoogleAddServicesAdded { readonly slug: IntegrationSlug; readonly presetId: string; readonly toolCount: number; } export interface GoogleAddServicesSkipped { readonly slug: IntegrationSlug; readonly presetId: string; readonly reason: "already_exists"; } export interface GoogleAddServicesFailed { readonly slug: IntegrationSlug; readonly presetId: string; readonly error: string; } export interface GoogleAddServicesResult { readonly added: readonly GoogleAddServicesAdded[]; readonly skipped: readonly GoogleAddServicesSkipped[]; readonly failed: readonly GoogleAddServicesFailed[]; } export interface GoogleConfigureInput { readonly authenticationTemplate: readonly AuthenticationInput[]; readonly mode?: "merge" | "replace"; } export interface GoogleUpdateInput { readonly urls?: readonly string[]; } export interface GoogleUpdateResult { readonly slug: IntegrationSlug; readonly toolCount: number; readonly addedTools: readonly string[]; readonly removedTools: readonly string[]; } export interface GooglePluginOptions { readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>; } const DEFAULT_GOOGLE_SLUG = "google"; const DEFAULT_GOOGLE_CUSTOM_SLUG = "google_custom"; const MAX_GOOGLE_CUSTOM_DESCRIPTION_LENGTH = 500; const googleOpenApiPresetById: ReadonlyMap<string, GoogleOpenApiPreset> = new Map( googleOpenApiPresets.map((preset) => [preset.id, preset]), ); const isGooglePresetServiceConfig = ( service: GoogleServiceConfig, ): service is GooglePresetServiceConfig => "presetId" in service; const googleServiceEntryId = (service: GoogleServiceConfig): string => isGooglePresetServiceConfig(service) ? service.presetId : GOOGLE_CUSTOM_SERVICE_ID; const googleServiceEntrySlug = (service: GoogleServiceConfig): IntegrationSlug => isGooglePresetServiceConfig(service) ? IntegrationSlug.make(service.slug?.trim() || googleServiceSlug(service.presetId)) : IntegrationSlug.make(service.custom.slug?.trim() || DEFAULT_GOOGLE_CUSTOM_SLUG); type GoogleAddServiceOutcome = { readonly added: readonly GoogleAddServicesAdded[]; readonly skipped: readonly GoogleAddServicesSkipped[]; readonly failed: readonly GoogleAddServicesFailed[]; }; const googleAddServiceFailure = (input: { readonly slug: IntegrationSlug; readonly presetId: string; readonly error: string; }): GoogleAddServiceOutcome => ({ added: [], skipped: [], failed: [{ slug: input.slug, presetId: input.presetId, error: input.error }], }); type GoogleFetchedDiscoveryDocument = { readonly discoveryUrl: string; readonly documentText: string; }; const googlePhotosBundlePresetIdByUrl = new Map( googlePhotosOpenApiPresets.flatMap((preset) => preset.url ? [[normalizeGoogleDiscoveryUrl(preset.url) ?? preset.url, preset.id] as const] : [], ), ); const googlePhotosBundleConsentScopes = ( urls: readonly string[], ): readonly string[] | undefined => { const normalized = new Set(urls); const presetIds = [...googlePhotosBundlePresetIdByUrl.entries()].flatMap(([url, presetId]) => normalized.has(url) ? [presetId] : [], ); return presetIds.length > 0 ? presetIds.flatMap((presetId) => googleOAuthConsentScopesForPreset(presetId)) : undefined; }; const fetchGoogleDiscoveryDocuments = ( urls: readonly string[], httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>, ) => Effect.forEach( urls, (url) => fetchGoogleDiscoveryDocument(url).pipe( Effect.provide(httpClientLayer), Effect.map( (documentText): GoogleFetchedDiscoveryDocument => ({ discoveryUrl: url, documentText }), ), ), { concurrency: 4 }, ); const fetchGoogleBundleConversion = ( urls: readonly string[], httpClientLayer: Layer.Layer<HttpClient.HttpClient, never, never>, consentScopesOverride?: readonly string[], ) => fetchGoogleDiscoveryDocuments(urls, httpClientLayer).pipe( Effect.flatMap((documents) => { const consentScopes = consentScopesOverride ?? googlePhotosBundleConsentScopes(urls); return convertGoogleDiscoveryBundleToOpenApi({ documents, ...(consentScopes !== undefined ? { consentScopes } : {}), }); }), ); const uniqueUrls = (urls: readonly string[]): readonly string[] => [ ...new Set(urls.flatMap((url) => normalizeGoogleDiscoveryUrl(url) ?? [])), ]; const googleBundleUrlsWithIdentity = ( urls: readonly string[], ): Effect.Effect<readonly string[], OpenApiParseError> => Effect.gen(function* () { const normalized: string[] = []; for (const url of urls) { const discoveryUrl = normalizeGoogleDiscoveryUrl(url); if (!discoveryUrl) { return yield* new OpenApiParseError({ message: "Google Discovery document URL must be a supported googleapis.com HTTPS Discovery endpoint", }); } normalized.push(discoveryUrl); } return uniqueUrls([...normalized, GOOGLE_OAUTH2_DISCOVERY_URL]); }); type GoogleServiceAddPlan = | { readonly kind: "preset"; readonly service: GooglePresetServiceConfig; } | { readonly kind: "custom"; readonly service: GoogleCustomServiceConfig; readonly url: string; readonly fallbackSlug: IntegrationSlug; } | { readonly kind: "invalid-custom"; readonly service: GoogleCustomServiceConfig; readonly slug: IntegrationSlug; readonly presetId: string; readonly error: string; }; const hasCustomGoogleServiceOverride = (service: GoogleCustomServiceConfig): boolean => Boolean( service.custom.slug?.trim() || service.custom.name?.trim() || service.custom.description?.trim(), ); const googleCustomFallbackSlug = (index: number): IntegrationSlug => IntegrationSlug.make( index === 0 ? DEFAULT_GOOGLE_CUSTOM_SLUG : `${DEFAULT_GOOGLE_CUSTOM_SLUG}_${index + 1}`, ); const googleCustomResultId = (slug: IntegrationSlug): string => String(slug); const googleDiscoveryNameSlugPart = (name: string): string => name .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, "_") .replace(/^_+|_+$/g, ""); const googleDiscoverySlug = (name: string): IntegrationSlug => IntegrationSlug.make( googleServiceSlug(`google-${googleDiscoveryNameSlugPart(name) || "custom"}`), ); const googleDiscoveryDisplayName = (identity: GoogleDiscoveryDocumentIdentity): string => { const title = identity.title ?.trim() .replace(/\s+API(?:\s+v[0-9][A-Za-z0-9._-]*)?$/i, "") .trim(); if (title && title.length > 0) { return /^google\b/i.test(title) ? title : `Google ${title}`; } return identity.name .trim() .replace(/[-_.]+/g, " ") .replace(/\b\w/g, (char) => char.toUpperCase()); }; const googleDiscoveryDescription = ( identity: GoogleDiscoveryDocumentIdentity, displayName: string, ): string => { const description = identity.description?.trim(); if (description && description.length > 0) { return description.length > MAX_GOOGLE_CUSTOM_DESCRIPTION_LENGTH ? `${description.slice(0, MAX_GOOGLE_CUSTOM_DESCRIPTION_LENGTH - 3).trimEnd()}...` : description; } return `${displayName}.`; }; const googleServiceAddPlans = ( services: readonly GoogleServiceConfig[], ): readonly GoogleServiceAddPlan[] => { const plans: GoogleServiceAddPlan[] = []; let customIndex = 0; for (const service of services) { if (isGooglePresetServiceConfig(service)) { plans.push({ kind: "preset", service }); continue; } if (service.custom.urls.length === 0) { const slug = googleCustomFallbackSlug(customIndex); customIndex += 1; plans.push({ kind: "invalid-custom", service, slug, presetId: googleCustomResultId(slug), error: "Custom Google service requires at least one Discovery URL", }); continue; } if (service.custom.urls.length > 1 && hasCustomGoogleServiceOverride(service)) { const slug = googleServiceEntrySlug(service); plans.push({ kind: "invalid-custom", service, slug, presetId: googleCustomResultId(slug), error: "Custom Google service identity overrides require exactly one Discovery URL", }); continue; } for (const url of service.custom.urls) { const overrideSlug = service.custom.slug?.trim(); const fallbackSlug = overrideSlug && overrideSlug.length > 0 ? IntegrationSlug.make(overrideSlug) : googleCustomFallbackSlug(customIndex); plans.push({ kind: "custom", service, url, fallbackSlug, }); customIndex += 1; } } return plans; }; const describeGoogleAuthMethods = (record: IntegrationRecord): readonly AuthMethodDescriptor[] => { const config = decodeGoogleIntegrationConfig(record.config); if (!config) return []; return (config.authenticationTemplate ?? []).map( (template: Authentication): AuthMethodDescriptor => { if (template.kind === "oauth2") { return { id: String(template.slug), label: "OAuth2", kind: "oauth", template: String(template.slug), oauth: { authorizationUrl: template.authorizationUrl, tokenUrl: template.tokenUrl, scopes: template.scopes, }, }; } return describeApiKeyAuthMethod(template); }, ); }; const describeGoogleIntegrationDisplay = (record: IntegrationRecord): { readonly url?: string } => { const config = decodeGoogleIntegrationConfig(record.config); return { url: config?.baseUrl ?? config?.googleDiscoveryUrls?.[0] }; }; const makeGooglePluginExtension = ( options: GooglePluginOptions | undefined, ctx: PluginCtx<OpenapiStore>, ) => { const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer; const addGoogleOpenApiIntegrationFromDocuments = (input: { readonly urls: readonly string[]; readonly documents: readonly GoogleFetchedDiscoveryDocument[]; readonly slug: IntegrationSlug; readonly name: string; readonly description: string; readonly baseUrl?: string; readonly consentScopes?: readonly string[]; }) => Effect.gen(function* () { const consentScopes = input.consentScopes ?? googlePhotosBundleConsentScopes(input.urls); const conversion = yield* convertGoogleDiscoveryBundleToOpenApi({ documents: input.documents, ...(consentScopes !== undefined ? { consentScopes } : {}), }); const compiled = yield* compileOpenApiSpec(conversion.specText); const slug = input.slug; const existing = yield* ctx.core.integrations.get(slug); if (existing) { return yield* new IntegrationAlreadyExistsError({ slug }); } const specHash = yield* sha256Hex(conversion.specText); const integrationConfig: GoogleIntegrationConfig = { specHash, googleDiscoveryUrls: input.urls, ...(input.baseUrl ? { baseUrl: input.baseUrl } : {}), ...(conversion.authenticationTemplate ? { authenticationTemplate: conversion.authenticationTemplate } : {}), }; yield* ctx.storage.putSpec(specHash, conversion.specText); yield* ctx.storage.putDefs(specHash, JSON.stringify(compiled.hoistedDefs)); yield* ctx.transaction( Effect.gen(function* () { yield* ctx.core.integrations.register({ slug, name: input.name, description: input.description, config: integrationConfig satisfies GoogleIntegrationConfig as IntegrationConfig, canRemove: true, canRefresh: true, }); yield* ctx.storage.putOperations( String(slug), openApiStoredOperationsFromCompiled(String(slug), compiled), ); }), ); // Default the health check to the light OAuth2 userinfo identity call // added to every new Google OpenAPI integration. Older integrations // without oauth2/v2 can still fall back to the People API identity // operation.