511 lines · 17.6 KB · TypeScript
RawDownload
import { Effect, Option } from "effect"; import {  SourceDetectionResult,  definePlugin,  resolveSecretBackedMap,  type PluginCtx,  type StorageFailure,  type ToolAnnotations,} from "@executor-js/sdk/core"; import { GoogleDiscoveryGroup } from "../api/group";import {  GoogleDiscoveryExtensionService,  GoogleDiscoveryHandlers,} from "../api/handlers"; import {  googleDiscoverySchema,  makeGoogleDiscoveryStore,  type GoogleDiscoveryStore,  type GoogleDiscoveryStoredSource,} from "./binding-store";import { extractGoogleDiscoveryManifest } from "./document";import { annotationsForOperation, invokeGoogleDiscoveryTool } from "./invoke";import { GoogleDiscoveryParseError, GoogleDiscoverySourceError } from "./errors";import type {  GoogleDiscoveryAuth,  GoogleDiscoveryFetchCredentials,  GoogleDiscoveryManifest,  GoogleDiscoveryManifestMethod,  GoogleDiscoveryMethodBinding,  GoogleDiscoveryStoredSourceData,} from "./types";import { GoogleDiscoveryStoredSourceData as GoogleDiscoveryStoredSourceDataSchema } from "./types"; // ---------------------------------------------------------------------------// Public input / output shapes// --------------------------------------------------------------------------- export interface GoogleDiscoveryProbeOperation {  readonly toolPath: string;  readonly method: string;  readonly pathTemplate: string;  readonly description: string | null;} export interface GoogleDiscoveryProbeResult {  readonly name: string;  readonly title: string | null;  readonly service: string;  readonly version: string;  readonly toolCount: number;  readonly scopes: readonly string[];  readonly operations: readonly GoogleDiscoveryProbeOperation[];} export interface GoogleDiscoveryProbeInput {  readonly discoveryUrl: string;  readonly credentials?: GoogleDiscoveryFetchCredentials;} export interface GoogleDiscoveryAddSourceInput {  readonly name: string;  readonly scope: string;  readonly discoveryUrl: string;  readonly credentials?: GoogleDiscoveryFetchCredentials;  readonly namespace?: string;  readonly auth: GoogleDiscoveryAuth;} export interface GoogleDiscoveryUpdateSourceInput {  readonly name?: string;  /** Rewrite the source's auth — typically after a successful   *  re-authenticate, to point at a freshly minted Connection. */  readonly auth?: GoogleDiscoveryAuth;} /** * Errors any Google Discovery extension method may surface. */export type GoogleDiscoveryExtensionFailure =  | GoogleDiscoveryParseError  | GoogleDiscoverySourceError  | StorageFailure; export interface GoogleDiscoveryPluginExtension {  readonly probeDiscovery: (    input: string | GoogleDiscoveryProbeInput,  ) => Effect.Effect<    GoogleDiscoveryProbeResult,    GoogleDiscoveryParseError | GoogleDiscoverySourceError  >;  readonly addSource: (    input: GoogleDiscoveryAddSourceInput,  ) => Effect.Effect<    { readonly toolCount: number; readonly namespace: string },    GoogleDiscoveryParseError | GoogleDiscoverySourceError | StorageFailure  >;  readonly removeSource: (namespace: string, scope: string) => Effect.Effect<void, StorageFailure>;  readonly getSource: (    namespace: string,    scope: string,  ) => Effect.Effect<GoogleDiscoveryStoredSource | null, StorageFailure>;  readonly updateSource: (    namespace: string,    scope: string,    input: GoogleDiscoveryUpdateSourceInput,  ) => Effect.Effect<void, StorageFailure>;} // ---------------------------------------------------------------------------// URL normalization + slug helpers (unchanged)// --------------------------------------------------------------------------- const DISCOVERY_SERVICE_HOST = "https://www.googleapis.com/discovery/v1/apis"; const normalizeDiscoveryUrl = (discoveryUrl: string): string => {  const trimmed = discoveryUrl.trim();  if (trimmed.length === 0) return trimmed;  let parsed: URL;  try {    parsed = new URL(trimmed);  } catch {    return trimmed;  }  if (parsed.pathname !== "/$discovery/rest") return trimmed;  const version = parsed.searchParams.get("version")?.trim();  if (!version) return trimmed;  const host = parsed.hostname.toLowerCase();  if (!host.endsWith(".googleapis.com")) return trimmed;  const rawService = host.slice(0, -".googleapis.com".length);  const service =    rawService === "calendar-json"      ? "calendar"      : rawService.endsWith("-json")        ? rawService.slice(0, -5)        : rawService;  if (!service) return trimmed;  return `${DISCOVERY_SERVICE_HOST}/${service}/${version}/rest`;}; const resolveGoogleDiscoveryCredentials = (  credentials: GoogleDiscoveryFetchCredentials | undefined,  ctx: PluginCtx<GoogleDiscoveryStore>,): Effect.Effect<  { headers?: Record<string, string>; queryParams?: Record<string, string> } | undefined,  GoogleDiscoverySourceError> =>  Effect.gen(function* () {    if (!credentials) return undefined;    const headers = yield* resolveSecretBackedMap({      values: credentials.headers,      getSecret: ctx.secrets.get,      onMissing: (name) =>        new GoogleDiscoverySourceError({          message: `Secret not found for header "${name}"`,        }),      onError: (_error, name) =>        new GoogleDiscoverySourceError({          message: `Secret not found for header "${name}"`,        }),    }).pipe(      Effect.mapError((err) =>        err instanceof GoogleDiscoverySourceError          ? err          : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }),      ),    );    const queryParams = yield* resolveSecretBackedMap({      values: credentials.queryParams,      getSecret: ctx.secrets.get,      onMissing: (name) =>        new GoogleDiscoverySourceError({          message: `Secret not found for query parameter "${name}"`,        }),      onError: (_error, name) =>        new GoogleDiscoverySourceError({          message: `Secret not found for query parameter "${name}"`,        }),    }).pipe(      Effect.mapError((err) =>        err instanceof GoogleDiscoverySourceError          ? err          : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }),      ),    );    return {      ...(headers ? { headers } : {}),      ...(queryParams ? { queryParams } : {}),    };  }); const fetchDiscoveryDocument = (  discoveryUrl: string,  credentials?: {    readonly headers?: Record<string, string>;    readonly queryParams?: Record<string, string>;  },) =>  Effect.tryPromise({    try: async () => {      const url = new URL(normalizeDiscoveryUrl(discoveryUrl));      for (const [key, value] of Object.entries(credentials?.queryParams ?? {})) {        url.searchParams.set(key, value);      }      const response = await fetch(url.toString(), {        headers: credentials?.headers,        signal: AbortSignal.timeout(20_000),      });      if (!response.ok) {        throw new GoogleDiscoverySourceError({          message: `Google Discovery fetch failed with status ${response.status}`,        });      }      return response.text();    },    catch: (cause) =>      cause instanceof GoogleDiscoverySourceError        ? cause        : new GoogleDiscoverySourceError({            message: cause instanceof Error ? cause.message : String(cause),          }),  }); const normalizeSlug = (value: string): string =>  value    .toLowerCase()    .replace(/[^a-z0-9]+/g, "_")    .replace(/^_+|_+$/g, ""); const deriveNamespace = (input: { name: string; service: string; version: string }): string =>  normalizeSlug(    input.name || `google_${input.service}_${input.version.replace(/[^a-zA-Z0-9]+/g, "_")}`,  ) || `google_${input.service}`; // Connection refresh state is owned by the canonical `"oauth2"`// ConnectionProvider registered by core. `ctx.oauth.start` stamps the// Google-specific token endpoint + scopes onto the connection's// providerState at mint time — no plugin-owned schema needed. // ---------------------------------------------------------------------------// Register a parsed manifest against the executor core + plugin storage.// Runs inside a transaction.// --------------------------------------------------------------------------- const registerManifest = (  ctx: PluginCtx<GoogleDiscoveryStore>,  namespace: string,  scope: string,  manifest: GoogleDiscoveryManifest,  sourceData: GoogleDiscoveryStoredSourceData,) =>  Effect.gen(function* () {    yield* ctx.storage.removeBindingsBySource(namespace, scope);    yield* ctx.core.sources.unregister(namespace).pipe(Effect.ignore);     yield* ctx.core.sources.register({      id: namespace,      scope,      kind: "googleDiscovery",      name: sourceData.name,      url: sourceData.rootUrl,      canRemove: true,      canRefresh: true,      canEdit: true,      tools: manifest.methods.map((method: GoogleDiscoveryManifestMethod) => ({        name: method.toolPath,        description: Option.getOrElse(          method.description,          () => `${method.binding.method.toUpperCase()} ${method.binding.pathTemplate}`,        ),        inputSchema: Option.getOrUndefined(method.inputSchema),        outputSchema: Option.getOrUndefined(method.outputSchema),      })),    });     if (Object.keys(manifest.schemaDefinitions).length > 0) {      yield* ctx.core.definitions.register({        sourceId: namespace,        scope,        definitions: manifest.schemaDefinitions,      });    }     yield* Effect.forEach(      manifest.methods,      (method) =>        ctx.storage.putBinding(`${namespace}.${method.toolPath}`, namespace, scope, method.binding),      { discard: true },    );     yield* ctx.storage.putSource({      namespace,      scope,      name: sourceData.name,      config: sourceData,    });     return manifest.methods.length;  }); // ---------------------------------------------------------------------------// Plugin// --------------------------------------------------------------------------- export const googleDiscoveryPlugin = definePlugin(() => ({  id: "googleDiscovery" as const,  packageName: "@executor-js/plugin-google-discovery",  schema: googleDiscoverySchema,  storage: (deps) => makeGoogleDiscoveryStore(deps),   extension: (ctx) =>    ({      probeDiscovery: (input) =>        Effect.gen(function* () {          const discoveryUrl = typeof input === "string" ? input : input.discoveryUrl;          const credentials =            typeof input === "string"              ? undefined              : yield* resolveGoogleDiscoveryCredentials(input.credentials, ctx);          const text = yield* fetchDiscoveryDocument(discoveryUrl, credentials);          const manifest = yield* extractGoogleDiscoveryManifest(text);          const scopes = Object.keys(            Option.isSome(manifest.oauthScopes) ? manifest.oauthScopes.value : {},          ).sort();          const operations = manifest.methods.map((method) => ({            toolPath: method.toolPath,            method: method.binding.method,            pathTemplate: method.binding.pathTemplate,            description: Option.isSome(method.description) ? method.description.value : null,          }));          return {            name: Option.isSome(manifest.title)              ? manifest.title.value              : `${manifest.service} ${manifest.version}`,            title: Option.isSome(manifest.title) ? manifest.title.value : null,            service: manifest.service,            version: manifest.version,            toolCount: manifest.methods.length,            scopes,            operations,          };        }),       addSource: (input) =>        ctx.transaction(          Effect.gen(function* () {            const credentials = yield* resolveGoogleDiscoveryCredentials(input.credentials, ctx);            const text = yield* fetchDiscoveryDocument(input.discoveryUrl, credentials);            const manifest = yield* extractGoogleDiscoveryManifest(text);            const namespace =              input.namespace ??              deriveNamespace({                name: input.name,                service: manifest.service,                version: manifest.version,              });            const sourceData = new GoogleDiscoveryStoredSourceDataSchema({              name: input.name,              discoveryUrl: normalizeDiscoveryUrl(input.discoveryUrl),              credentials: input.credentials,              service: manifest.service,              version: manifest.version,              rootUrl: manifest.rootUrl,              servicePath: manifest.servicePath,              auth: input.auth,            });            const toolCount = yield* registerManifest(              ctx,              namespace,              input.scope,              manifest,              sourceData,            );            return { toolCount, namespace };          }),        ),       removeSource: (namespace, scope) =>        ctx.transaction(          Effect.gen(function* () {            yield* ctx.storage.removeBindingsBySource(namespace, scope);            yield* ctx.storage.removeSource(namespace, scope);            yield* ctx.core.sources.unregister(namespace).pipe(Effect.ignore);          }),        ),       // OAuth start/complete live on `ctx.oauth` now — the UI calls      // the shared `/scopes/:scopeId/oauth/*` endpoints directly with a      // Google-specific `authorization-code` strategy and writes the      // resulting connection back via `updateSource`.       getSource: (namespace, scope) => ctx.storage.getSource(namespace, scope),       updateSource: (namespace, scope, input) =>        ctx.storage.updateSourceMeta(namespace, scope, {          name: input.name?.trim() || undefined,          auth: input.auth,        }),    }) satisfies GoogleDiscoveryPluginExtension,   invokeTool: ({ ctx, toolRow, args }) =>    invokeGoogleDiscoveryTool({      ctx: ctx as PluginCtx<GoogleDiscoveryStore>,      toolId: toolRow.id,      toolScope: toolRow.scope_id as string,      args,    }),   resolveAnnotations: ({ ctx, sourceId, toolRows }) =>    Effect.gen(function* () {      const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;      const scopes = new Set<string>();      for (const row of toolRows) scopes.add(row.scope_id as string);      const byScope = new Map<string, ReadonlyMap<string, GoogleDiscoveryMethodBinding>>();      for (const scope of scopes) {        const bindings = yield* typedCtx.storage.getBindingsForSource(sourceId, scope);        byScope.set(scope, bindings);      }      const out: Record<string, ToolAnnotations> = {};      for (const row of toolRows) {        const binding = byScope.get(row.scope_id as string)?.get(row.id);        if (binding) {          out[row.id] = annotationsForOperation(binding.method, binding.pathTemplate);        }      }      return out;    }),   removeSource: ({ ctx, sourceId, scope }) =>    Effect.gen(function* () {      const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;      yield* typedCtx.storage.removeBindingsBySource(sourceId, scope);      yield* typedCtx.storage.removeSource(sourceId, scope);    }),   detect: ({ url }) =>    Effect.gen(function* () {      const trimmed = url.trim();      if (!trimmed) return null;      const parsed = yield* Effect.try({        try: () => new URL(trimmed),        catch: (error) => error,      }).pipe(Effect.option);      if (Option.isNone(parsed)) return null;       const isGoogleUrl = trimmed.includes("googleapis.com");      const isDiscoveryPath = trimmed.includes("/discovery/") || trimmed.includes("$discovery");      if (!isGoogleUrl && !isDiscoveryPath) return null;       const discoveryText = yield* fetchDiscoveryDocument(trimmed).pipe(        Effect.catch(() => Effect.succeed(null)),      );      if (!discoveryText) return null;       const manifest = yield* extractGoogleDiscoveryManifest(discoveryText).pipe(        Effect.catch(() => Effect.succeed(null)),      );      if (!manifest) return null;       const name = Option.getOrElse(        manifest.title,        () => `${manifest.service} ${manifest.version}`,      );       return new SourceDetectionResult({        kind: "googleDiscovery",        confidence: "high",        endpoint: trimmed,        name,        namespace: deriveNamespace({          name,          service: manifest.service,          version: manifest.version,        }),      });    }),   refreshSource: ({ ctx, sourceId, scope }) =>    Effect.gen(function* () {      const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;      const existing = yield* typedCtx.storage.getSource(sourceId, scope);      if (!existing) return;      const credentials = yield* resolveGoogleDiscoveryCredentials(        existing.config.credentials,        typedCtx,      );      const text = yield* fetchDiscoveryDocument(existing.config.discoveryUrl, credentials);      const manifest = yield* extractGoogleDiscoveryManifest(text);      const next = new GoogleDiscoveryStoredSourceDataSchema({        ...existing.config,        service: manifest.service,        version: manifest.version,        rootUrl: manifest.rootUrl,        servicePath: manifest.servicePath,      });      yield* registerManifest(typedCtx, sourceId, scope, manifest, next);    }).pipe(Effect.mapError((err) => (err instanceof Error ? err : new Error(String(err))))), 

Lines 1–500 of 511.