949 lines · 30.4 KB · TypeScript
RawDownload
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.