From 191d3d3ac3370db9b86c20a9cdb5e9cf6335f1b2 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 5 May 2026 21:21:22 -0700 Subject: [PATCH] Fix Google Discovery SDK typed boundaries --- .../google-discovery/src/sdk/binding-store.ts | 111 +++---- .../google-discovery/src/sdk/invoke.ts | 74 +++-- .../google-discovery/src/sdk/plugin.ts | 297 +++++++++--------- 3 files changed, 243 insertions(+), 239 deletions(-) diff --git a/packages/plugins/google-discovery/src/sdk/binding-store.ts b/packages/plugins/google-discovery/src/sdk/binding-store.ts index 3d1c35db9..45313f1ad 100644 --- a/packages/plugins/google-discovery/src/sdk/binding-store.ts +++ b/packages/plugins/google-discovery/src/sdk/binding-store.ts @@ -13,13 +13,9 @@ // survive adapter serialization. // --------------------------------------------------------------------------- -import { Effect, Schema } from "effect"; +import { Effect, Option, Schema } from "effect"; -import { - defineSchema, - type StorageDeps, - type StorageFailure, -} from "@executor-js/sdk/core"; +import { defineSchema, type StorageDeps, type StorageFailure } from "@executor-js/sdk/core"; import { GoogleDiscoveryMethodBinding, @@ -136,14 +132,12 @@ const decodeBinding = Schema.decodeUnknownSync(GoogleDiscoveryMethodBinding); const toJsonRecord = (value: unknown): Record => value as Record; +const decodeJsonString = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown)); + const decodeJson = (value: unknown): unknown => { if (value === null || value === undefined) return value; if (typeof value !== "string") return value; - try { - return JSON.parse(value); - } catch { - return value; - } + return Option.getOrElse(decodeJsonString(value), () => value); }; // --- auth column packing/unpacking ------------------------------------------ @@ -239,12 +233,11 @@ const rowsToValueMap = ( ): Record => { const out: Record = {}; for (const row of rows) { - const name = row.name as string; + if (typeof row.name !== "string") continue; + const name = row.name; if (row.kind === "secret" && typeof row.secret_id === "string") { const prefix = row.secret_prefix as string | undefined | null; - out[name] = prefix - ? { secretId: row.secret_id, prefix } - : { secretId: row.secret_id }; + out[name] = prefix ? { secretId: row.secret_id, prefix } : { secretId: row.secret_id }; } else if (row.kind === "text" && typeof row.text_value === "string") { out[name] = row.text_value; } @@ -277,7 +270,10 @@ export interface GoogleDiscoveryStore { toolId: string, scope: string, ) => Effect.Effect< - { readonly namespace: string; readonly binding: GoogleDiscoveryMethodBinding } | null, + { + readonly namespace: string; + readonly binding: GoogleDiscoveryMethodBinding; + } | null, StorageFailure >; readonly putBinding: ( @@ -320,9 +316,7 @@ export interface GoogleDiscoveryStore { /** Source rows whose oauth2 auth columns reference the given secret id. * `slot` distinguishes client_id vs client_secret. */ - readonly findSourcesBySecret: ( - secretId: string, - ) => Effect.Effect< + readonly findSourcesBySecret: (secretId: string) => Effect.Effect< readonly { readonly namespace: string; readonly scope_id: string; @@ -333,9 +327,7 @@ export interface GoogleDiscoveryStore { >; /** Source rows whose oauth2 auth points at the given connection id. */ - readonly findSourcesByConnection: ( - connectionId: string, - ) => Effect.Effect< + readonly findSourcesByConnection: (connectionId: string) => Effect.Effect< readonly { readonly namespace: string; readonly scope_id: string; @@ -382,7 +374,7 @@ export const makeGoogleDiscoveryStore = ( }); if (!row) return null; const decoded = decodeBinding(decodeJson(row.binding)); - return { namespace: row.source_id as string, binding: decoded }; + return { namespace: row.source_id, binding: decoded }; }), putBinding: (toolId, sourceId, scope, binding) => @@ -420,7 +412,7 @@ export const makeGoogleDiscoveryStore = ( { field: "scope_id", value: scope }, ], }); - const ids = rows.map((r) => r.id as string); + const ids = rows.map((r) => r.id); yield* db.deleteMany({ model: "google_discovery_binding", where: [ @@ -442,7 +434,7 @@ export const makeGoogleDiscoveryStore = ( }); const out = new Map(); for (const row of rows) { - out.set(row.id as string, decodeBinding(decodeJson(row.binding))); + out.set(row.id, decodeBinding(decodeJson(row.binding))); } return out; }), @@ -477,11 +469,7 @@ export const makeGoogleDiscoveryStore = ( }, forceAllowId: true, }); - yield* writeCredentialRows( - source.namespace, - source.scope, - source.config.credentials, - ); + yield* writeCredentialRows(source.namespace, source.scope, source.config.credentials); }), updateSourceMeta: (sourceId, scope, update) => @@ -502,7 +490,7 @@ export const makeGoogleDiscoveryStore = ( { field: "scope_id", value: scope }, ], update: { - name: update.name ?? (row.name as string), + name: update.name ?? row.name, updated_at: new Date(), ...authToColumns(auth), }, @@ -532,9 +520,9 @@ export const makeGoogleDiscoveryStore = ( }); if (!row) return null; return { - namespace: row.id as string, - scope: row.scope_id as string, - name: row.name as string, + namespace: row.id, + scope: row.scope_id, + name: row.name, config: yield* hydrateStoredSourceData(row, sourceId, scope), }; }), @@ -558,15 +546,11 @@ export const makeGoogleDiscoveryStore = ( [ db.findMany({ model: "google_discovery_source", - where: [ - { field: "auth_client_id_secret_id", value: secretId }, - ], + where: [{ field: "auth_client_id_secret_id", value: secretId }], }), db.findMany({ model: "google_discovery_source", - where: [ - { field: "auth_client_secret_secret_id", value: secretId }, - ], + where: [{ field: "auth_client_secret_secret_id", value: secretId }], }), ], { concurrency: "unbounded" }, @@ -579,17 +563,17 @@ export const makeGoogleDiscoveryStore = ( }[] = []; for (const r of byClientId) { out.push({ - namespace: r.id as string, - scope_id: r.scope_id as string, - name: r.name as string, + namespace: r.id, + scope_id: r.scope_id, + name: r.name, slot: "auth.oauth2.client_id", }); } for (const r of byClientSecret) { out.push({ - namespace: r.id as string, - scope_id: r.scope_id as string, - name: r.name as string, + namespace: r.id, + scope_id: r.scope_id, + name: r.name, slot: "auth.oauth2.client_secret", }); } @@ -605,9 +589,9 @@ export const makeGoogleDiscoveryStore = ( .pipe( Effect.map((rows) => rows.map((r) => ({ - namespace: r.id as string, - scope_id: r.scope_id as string, - name: r.name as string, + namespace: r.id, + scope_id: r.scope_id, + name: r.name, slot: "auth.oauth2.connection", })), ), @@ -631,15 +615,15 @@ export const makeGoogleDiscoveryStore = ( return [ ...headers.map((r) => ({ kind: "credential_header" as const, - source_id: r.source_id as string, - scope_id: r.scope_id as string, - name: r.name as string, + source_id: r.source_id, + scope_id: r.scope_id, + name: r.name, })), ...params.map((r) => ({ kind: "credential_query_param" as const, - source_id: r.source_id as string, - scope_id: r.scope_id as string, - name: r.name as string, + source_id: r.source_id, + scope_id: r.scope_id, + name: r.name, })), ]; }), @@ -651,8 +635,8 @@ export const makeGoogleDiscoveryStore = ( const requested = new Set(keys); const out = new Map(); for (const r of rows) { - const key = `${r.scope_id as string}:${r.id as string}`; - if (requested.has(key)) out.set(key, r.name as string); + const key = `${r.scope_id}:${r.id}`; + if (requested.has(key)) out.set(key, r.name); } return out; }), @@ -698,11 +682,7 @@ export const makeGoogleDiscoveryStore = ( forceAllowId: true, }); } - const paramRows = valueMapToRows( - sourceId, - scope, - credentials.queryParams, - ); + const paramRows = valueMapToRows(sourceId, scope, credentials.queryParams); if (paramRows.length > 0) { yield* db.createMany({ model: "google_discovery_source_credential_query_param", @@ -737,8 +717,7 @@ export const makeGoogleDiscoveryStore = ( const headers = rowsToValueMap(headerRows); const queryParams = rowsToValueMap(paramRows); const credentials = - Object.keys(headers).length === 0 && - Object.keys(queryParams).length === 0 + Object.keys(headers).length === 0 && Object.keys(queryParams).length === 0 ? undefined : { ...(Object.keys(headers).length > 0 ? { headers } : {}), @@ -757,9 +736,7 @@ export const makeGoogleDiscoveryStore = ( // Strip auth/credentials from the encoded source-data shape. Those // moved to columns and child tables; the remaining structural fields // live in the `config` JSON. -const stripExtractedFields = ( - encoded: Record, -): Record => { +const stripExtractedFields = (encoded: Record): Record => { const { auth, credentials, ...rest } = encoded; void auth; void credentials; diff --git a/packages/plugins/google-discovery/src/sdk/invoke.ts b/packages/plugins/google-discovery/src/sdk/invoke.ts index cac95144d..a3d1aa16c 100644 --- a/packages/plugins/google-discovery/src/sdk/invoke.ts +++ b/packages/plugins/google-discovery/src/sdk/invoke.ts @@ -1,4 +1,4 @@ -import { Effect, Layer, Option } from "effect"; +import { Effect, Layer, Option, Schema } from "effect"; import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; import type { PluginCtx, StorageFailure } from "@executor-js/sdk/core"; @@ -13,6 +13,16 @@ import { const SAFE_METHODS = new Set(["get", "head", "options"]); +const UnknownErrorMessage = Schema.Struct({ message: Schema.String }); +const decodeUnknownErrorMessage = Schema.decodeUnknownOption(UnknownErrorMessage); + +const errorMessageFromUnknown = (cause: unknown): string => { + const decoded = decodeUnknownErrorMessage(cause); + if (Option.isSome(decoded)) return decoded.value.message; + // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: preserves existing fallback text for HTTP client errors + return String(cause); +}; + export const annotationsForOperation = ( method: string, pathTemplate: string, @@ -46,19 +56,27 @@ const replacePathParameters = (input: { pathTemplate: string; args: Record; parameters: readonly GoogleDiscoveryParameter[]; -}): string => - input.pathTemplate.replaceAll(/\{([^}]+)\}/g, (_, name: string) => { - const parameter = input.parameters.find( - (entry) => entry.location === "path" && entry.name === name, - ); - const values = stringValuesFromParameter(input.args[name], false); - if (values.length === 0) { - if (parameter?.required) { - throw new Error(`Missing required path parameter: ${name}`); +}): Effect.Effect => + Effect.gen(function* () { + let failure: GoogleDiscoveryInvocationError | undefined; + const resolved = input.pathTemplate.replaceAll(/\{([^}]+)\}/g, (_, name: string) => { + const parameter = input.parameters.find( + (entry) => entry.location === "path" && entry.name === name, + ); + const values = stringValuesFromParameter(input.args[name], false); + if (values.length === 0) { + if (parameter?.required) { + failure = new GoogleDiscoveryInvocationError({ + message: `Missing required path parameter: ${name}`, + statusCode: Option.none(), + }); + } + return ""; } - return ""; - } - return encodeURIComponent(values[0]!); + return encodeURIComponent(values[0]!); + }); + if (failure) return yield* failure; + return resolved; }); const resolveBaseUrl = (source: GoogleDiscoveryStoredSourceData): string => @@ -87,7 +105,7 @@ const performRequest = Effect.fn("GoogleDiscovery.invoke")(function* (input: { }) { const client = yield* HttpClient.HttpClient; - const resolvedPath = replacePathParameters({ + const resolvedPath = yield* replacePathParameters({ pathTemplate: input.pathTemplate, args: input.args, parameters: input.parameters, @@ -138,7 +156,7 @@ const performRequest = Effect.fn("GoogleDiscovery.invoke")(function* (input: { Effect.mapError( (err) => new GoogleDiscoveryInvocationError({ - message: `HTTP request failed: ${err.message}`, + message: `HTTP request failed: ${errorMessageFromUnknown(err)}`, statusCode: Option.none(), cause: err, }), @@ -147,9 +165,9 @@ const performRequest = Effect.fn("GoogleDiscovery.invoke")(function* (input: { const contentType = response.headers["content-type"] ?? null; const mapBodyError = Effect.mapError( - (err: { readonly message?: string }) => + (err: unknown) => new GoogleDiscoveryInvocationError({ - message: `Failed to read response body: ${err.message ?? String(err)}`, + message: `Failed to read response body: ${errorMessageFromUnknown(err)}`, statusCode: Option.some(response.status), cause: err, }), @@ -191,21 +209,17 @@ export const invokeGoogleDiscoveryTool = (input: { Effect.gen(function* () { const entry = yield* input.ctx.storage.getBinding(input.toolId, input.toolScope); if (!entry) { - return yield* Effect.fail( - new GoogleDiscoveryInvocationError({ - message: `No Google Discovery operation found for tool "${input.toolId}"`, - statusCode: Option.none(), - }), - ); + return yield* new GoogleDiscoveryInvocationError({ + message: `No Google Discovery operation found for tool "${input.toolId}"`, + statusCode: Option.none(), + }); } const stored = yield* input.ctx.storage.getSource(entry.namespace, input.toolScope); if (!stored) { - return yield* Effect.fail( - new GoogleDiscoveryInvocationError({ - message: `No Google Discovery source found for "${entry.namespace}"`, - statusCode: Option.none(), - }), - ); + return yield* new GoogleDiscoveryInvocationError({ + message: `No Google Discovery source found for "${entry.namespace}"`, + statusCode: Option.none(), + }); } const source = stored.config; @@ -215,7 +229,7 @@ export const invokeGoogleDiscoveryTool = (input: { Effect.mapError( (err) => new GoogleDiscoveryOAuthError({ - message: "message" in err ? (err as { message: string }).message : String(err), + message: errorMessageFromUnknown(err), }), ), )}` diff --git a/packages/plugins/google-discovery/src/sdk/plugin.ts b/packages/plugins/google-discovery/src/sdk/plugin.ts index 268757289..9be01ed86 100644 --- a/packages/plugins/google-discovery/src/sdk/plugin.ts +++ b/packages/plugins/google-discovery/src/sdk/plugin.ts @@ -1,4 +1,4 @@ -import { Effect, Option } from "effect"; +import { Effect, Option, Predicate, Schema } from "effect"; import { ScopeId, @@ -12,10 +12,7 @@ import { } from "@executor-js/sdk/core"; import { GoogleDiscoveryGroup } from "../api/group"; -import { - GoogleDiscoveryExtensionService, - GoogleDiscoveryHandlers, -} from "../api/handlers"; +import { GoogleDiscoveryExtensionService, GoogleDiscoveryHandlers } from "../api/handlers"; import { googleDiscoverySchema, @@ -86,7 +83,9 @@ export type GoogleDiscoveryExtensionFailure = | GoogleDiscoverySourceError | StorageFailure; -export interface GoogleDiscoveryPluginExtension { +export type GoogleDiscoveryPluginExtension = ReturnType; + +interface GoogleDiscoveryPluginExtensionShape { readonly probeDiscovery: ( input: string | GoogleDiscoveryProbeInput, ) => Effect.Effect< @@ -117,15 +116,24 @@ export interface GoogleDiscoveryPluginExtension { const DISCOVERY_SERVICE_HOST = "https://www.googleapis.com/discovery/v1/apis"; +const UnknownErrorMessage = Schema.Struct({ message: Schema.String }); +const decodeUnknownErrorMessage = Schema.decodeUnknownOption(UnknownErrorMessage); + +const errorMessageFromUnknown = (cause: unknown): string => { + const decoded = decodeUnknownErrorMessage(cause); + if (Option.isSome(decoded)) return decoded.value.message; + // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: preserves existing fallback text for fetch/host errors + return String(cause); +}; + +const isGoogleDiscoverySourceError = (cause: unknown): cause is GoogleDiscoverySourceError => + Predicate.isTagged("GoogleDiscoverySourceError")(cause); + 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 (!URL.canParse(trimmed)) return trimmed; + const parsed = new URL(trimmed); if (parsed.pathname !== "/$discovery/rest") return trimmed; const version = parsed.searchParams.get("version")?.trim(); if (!version) return trimmed; @@ -164,9 +172,11 @@ const resolveGoogleDiscoveryCredentials = ( }), }).pipe( Effect.mapError((err) => - err instanceof GoogleDiscoverySourceError + isGoogleDiscoverySourceError(err) ? err - : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }), + : new GoogleDiscoverySourceError({ + message: "Secret resolution failed", + }), ), ); const queryParams = yield* resolveSecretBackedMap({ @@ -182,9 +192,11 @@ const resolveGoogleDiscoveryCredentials = ( }), }).pipe( Effect.mapError((err) => - err instanceof GoogleDiscoverySourceError + isGoogleDiscoverySourceError(err) ? err - : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }), + : new GoogleDiscoverySourceError({ + message: "Secret resolution failed", + }), ), ); return { @@ -200,29 +212,35 @@ const fetchDiscoveryDocument = ( readonly queryParams?: Record; }, ) => - 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}`, + Effect.gen(function* () { + const response = yield* Effect.tryPromise({ + try: async () => { + const url = new URL(normalizeDiscoveryUrl(discoveryUrl)); + for (const [key, value] of Object.entries(credentials?.queryParams ?? {})) { + url.searchParams.set(key, value); + } + return fetch(url.toString(), { + headers: credentials?.headers, + signal: AbortSignal.timeout(20_000), }); - } - return response.text(); - }, - catch: (cause) => - cause instanceof GoogleDiscoverySourceError - ? cause - : new GoogleDiscoverySourceError({ - message: cause instanceof Error ? cause.message : String(cause), - }), + }, + catch: (cause) => + new GoogleDiscoverySourceError({ + message: errorMessageFromUnknown(cause), + }), + }); + if (!response.ok) { + return yield* new GoogleDiscoverySourceError({ + message: `Google Discovery fetch failed with status ${response.status}`, + }); + } + return yield* Effect.tryPromise({ + try: () => response.text(), + catch: (cause) => + new GoogleDiscoverySourceError({ + message: errorMessageFromUnknown(cause), + }), + }); }); const normalizeSlug = (value: string): string => @@ -302,111 +320,113 @@ const registerManifest = ( 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) => +const makeGoogleDiscoveryPluginExtension = (ctx: PluginCtx) => + ({ + 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 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 credentials = yield* resolveGoogleDiscoveryCredentials(input.credentials, ctx); + const text = yield* fetchDiscoveryDocument(input.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({ + const namespace = + input.namespace ?? + deriveNamespace({ 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 }; - }), - ), + 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); - }), - ), + 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`. + // 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), + 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, + updateSource: (namespace, scope, input) => + ctx.storage.updateSourceMeta(namespace, scope, { + name: input.name?.trim() || undefined, + auth: input.auth, + }), + }) satisfies GoogleDiscoveryPluginExtensionShape; + +// --------------------------------------------------------------------------- +// Plugin +// --------------------------------------------------------------------------- + +export const googleDiscoveryPlugin = definePlugin(() => ({ + id: "googleDiscovery" as const, + packageName: "@executor-js/plugin-google-discovery", + schema: googleDiscoverySchema, + storage: (deps) => makeGoogleDiscoveryStore(deps), + + extension: makeGoogleDiscoveryPluginExtension, invokeTool: ({ ctx, toolRow, args }) => invokeGoogleDiscoveryTool({ ctx: ctx as PluginCtx, toolId: toolRow.id, - toolScope: toolRow.scope_id as string, + toolScope: toolRow.scope_id, args, }), @@ -414,7 +434,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ Effect.gen(function* () { const typedCtx = ctx as PluginCtx; const scopes = new Set(); - for (const row of toolRows) scopes.add(row.scope_id as string); + for (const row of toolRows) scopes.add(row.scope_id); const byScope = new Map>(); for (const scope of scopes) { const bindings = yield* typedCtx.storage.getBindingsForSource(sourceId, scope); @@ -422,7 +442,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ } const out: Record = {}; for (const row of toolRows) { - const binding = byScope.get(row.scope_id as string)?.get(row.id); + const binding = byScope.get(row.scope_id)?.get(row.id); if (binding) { out[row.id] = annotationsForOperation(binding.method, binding.pathTemplate); } @@ -443,16 +463,11 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ usagesForSecret: ({ ctx, args }) => Effect.gen(function* () { const typedCtx = ctx as PluginCtx; - const sources = yield* typedCtx.storage.findSourcesBySecret( - args.secretId, - ); - const childRows = yield* typedCtx.storage.findCredentialRowsBySecret( - args.secretId, - ); + const sources = yield* typedCtx.storage.findSourcesBySecret(args.secretId); + const childRows = yield* typedCtx.storage.findCredentialRowsBySecret(args.secretId); const sourceKeys = new Set(); for (const s of sources) sourceKeys.add(`${s.scope_id}:${s.namespace}`); - for (const r of childRows) - sourceKeys.add(`${r.scope_id}:${r.source_id}`); + for (const r of childRows) sourceKeys.add(`${r.scope_id}:${r.source_id}`); const names = yield* typedCtx.storage.lookupSourceNames([...sourceKeys]); const out: Usage[] = []; @@ -486,9 +501,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ usagesForConnection: ({ ctx, args }) => Effect.gen(function* () { const typedCtx = ctx as PluginCtx; - const sources = yield* typedCtx.storage.findSourcesByConnection( - args.connectionId, - ); + const sources = yield* typedCtx.storage.findSourcesByConnection(args.connectionId); return sources.map( (s) => new Usage({ @@ -563,7 +576,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ servicePath: manifest.servicePath, }); yield* registerManifest(typedCtx, sourceId, scope, manifest, next); - }).pipe(Effect.mapError((err) => (err instanceof Error ? err : new Error(String(err))))), + }), // Connection refresh is owned by the canonical `"oauth2"` // ConnectionProvider registered by core — no plugin-specific handler