History

Fix Google Discovery boundary lint

Rhys Sullivan committed 6 months ago

Commit ed17ac41 parent b039b6e2 files · +179 −233Browse files

Unified · Split

2 files
  1. packages/plugins/google-discovery/src/sdk/binding-store.ts +42 −68
  2. packages/plugins/google-discovery/src/sdk/plugin.ts +137 −165
packages/plugins/google-discovery/src/sdk/binding-store.ts+42−68
@@ -13,13 +13,9 @@ · 12 unmodified lines
1313 // survive adapter serialization.
1414 // ---------------------------------------------------------------------------
1515
16−import { Effect, Schema } from "effect";
16+import { Effect, Option, Schema } from "effect";
1717
18−import {
19− defineSchema,
20− type StorageDeps,
21− type StorageFailure,
22−} from "@executor-js/sdk/core";
18+import { defineSchema, type StorageDeps, type StorageFailure } from "@executor-js/sdk/core";
2319
2420 import {
2521 GoogleDiscoveryMethodBinding,
@@ -135,15 +131,14 @@ const encodeBinding = Schema.encodeSync(GoogleDiscoveryMethodBinding); · 109 unmodified lines
135131 const decodeBinding = Schema.decodeUnknownSync(GoogleDiscoveryMethodBinding);
136132
137133 const toJsonRecord = (value: unknown): Record<string, unknown> => value as Record<string, unknown>;
134+const decodeString = Schema.decodeUnknownSync(Schema.String);
135+const decodeJsonObject = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown));
136+const decodeJsonString = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
138137
139138 const decodeJson = (value: unknown): unknown => {
140139 if (value === null || value === undefined) return value;
141140 if (typeof value !== "string") return value;
142− try {
143− return JSON.parse(value);
144− } catch {
145− return value;
146− }
141+ return Option.getOrElse(decodeJsonString(value), () => value);
147142 };
148143
149144 // --- auth column packing/unpacking ------------------------------------------
@@ -239,12 +234,10 @@ const rowsToValueMap = ( · 89 unmodified lines
239234 ): Record<string, GoogleDiscoveryCredentialValue> => {
240235 const out: Record<string, GoogleDiscoveryCredentialValue> = {};
241236 for (const row of rows) {
242− const name = row.name as string;
237+ const name = decodeString(row.name);
243238 if (row.kind === "secret" && typeof row.secret_id === "string") {
244239 const prefix = row.secret_prefix as string | undefined | null;
245− out[name] = prefix
246− ? { secretId: row.secret_id, prefix }
247− : { secretId: row.secret_id };
240+ out[name] = prefix ? { secretId: row.secret_id, prefix } : { secretId: row.secret_id };
248241 } else if (row.kind === "text" && typeof row.text_value === "string") {
249242 out[name] = row.text_value;
250243 }
@@ -320,9 +313,7 @@ export interface GoogleDiscoveryStore { · 69 unmodified lines
320313
321314 /** Source rows whose oauth2 auth columns reference the given secret id.
322315 * `slot` distinguishes client_id vs client_secret. */
323− readonly findSourcesBySecret: (
324− secretId: string,
325− ) => Effect.Effect<
316+ readonly findSourcesBySecret: (secretId: string) => Effect.Effect<
326317 readonly {
327318 readonly namespace: string;
328319 readonly scope_id: string;
@@ -333,9 +324,7 @@ export interface GoogleDiscoveryStore { · 4 unmodified lines
333324 >;
334325
335326 /** Source rows whose oauth2 auth points at the given connection id. */
336− readonly findSourcesByConnection: (
337− connectionId: string,
338− ) => Effect.Effect<
327+ readonly findSourcesByConnection: (connectionId: string) => Effect.Effect<
339328 readonly {
340329 readonly namespace: string;
341330 readonly scope_id: string;
@@ -382,7 +371,7 @@ export const makeGoogleDiscoveryStore = ( · 40 unmodified lines
382371 });
383372 if (!row) return null;
384373 const decoded = decodeBinding(decodeJson(row.binding));
385− return { namespace: row.source_id as string, binding: decoded };
374+ return { namespace: decodeString(row.source_id), binding: decoded };
386375 }),
387376
388377 putBinding: (toolId, sourceId, scope, binding) =>
@@ -420,7 +409,7 @@ export const makeGoogleDiscoveryStore = ( · 31 unmodified lines
420409 { field: "scope_id", value: scope },
421410 ],
422411 });
423− const ids = rows.map((r) => r.id as string);
412+ const ids = rows.map((r) => decodeString(r.id));
424413 yield* db.deleteMany({
425414 model: "google_discovery_binding",
426415 where: [
@@ -442,7 +431,7 @@ export const makeGoogleDiscoveryStore = ( · 15 unmodified lines
442431 });
443432 const out = new Map<string, GoogleDiscoveryMethodBinding>();
444433 for (const row of rows) {
445− out.set(row.id as string, decodeBinding(decodeJson(row.binding)));
434+ out.set(decodeString(row.id), decodeBinding(decodeJson(row.binding)));
446435 }
447436 return out;
448437 }),
@@ -462,7 +451,7 @@ export const makeGoogleDiscoveryStore = ( · 13 unmodified lines
462451 yield* deleteSourceChildren(source.namespace, source.scope);
463452
464453 const encoded = stripExtractedFields(
465− encodeStoredSourceData(source.config) as Record<string, unknown>,
454+ decodeJsonObject(encodeStoredSourceData(source.config)),
466455 );
467456 yield* db.create({
468457 model: "google_discovery_source",
@@ -477,11 +466,7 @@ export const makeGoogleDiscoveryStore = ( · 8 unmodified lines
477466 },
478467 forceAllowId: true,
479468 });
480− yield* writeCredentialRows(
481− source.namespace,
482− source.scope,
483− source.config.credentials,
484− );
469+ yield* writeCredentialRows(source.namespace, source.scope, source.config.credentials);
485470 }),
486471
487472 updateSourceMeta: (sourceId, scope, update) =>
@@ -502,7 +487,7 @@ export const makeGoogleDiscoveryStore = ( · 14 unmodified lines
502487 { field: "scope_id", value: scope },
503488 ],
504489 update: {
505− name: update.name ?? (row.name as string),
490+ name: update.name ?? decodeString(row.name),
506491 updated_at: new Date(),
507492 ...authToColumns(auth),
508493 },
@@ -532,9 +517,9 @@ export const makeGoogleDiscoveryStore = ( · 23 unmodified lines
532517 });
533518 if (!row) return null;
534519 return {
535− namespace: row.id as string,
536− scope: row.scope_id as string,
537− name: row.name as string,
520+ namespace: decodeString(row.id),
521+ scope: decodeString(row.scope_id),
522+ name: decodeString(row.name),
538523 config: yield* hydrateStoredSourceData(row, sourceId, scope),
539524 };
540525 }),
@@ -558,15 +543,11 @@ export const makeGoogleDiscoveryStore = ( · 17 unmodified lines
558543 [
559544 db.findMany({
560545 model: "google_discovery_source",
561− where: [
562− { field: "auth_client_id_secret_id", value: secretId },
563− ],
546+ where: [{ field: "auth_client_id_secret_id", value: secretId }],
564547 }),
565548 db.findMany({
566549 model: "google_discovery_source",
567− where: [
568− { field: "auth_client_secret_secret_id", value: secretId },
569− ],
550+ where: [{ field: "auth_client_secret_secret_id", value: secretId }],
570551 }),
571552 ],
572553 { concurrency: "unbounded" },
@@ -579,17 +560,17 @@ export const makeGoogleDiscoveryStore = ( · 6 unmodified lines
579560 }[] = [];
580561 for (const r of byClientId) {
581562 out.push({
582− namespace: r.id as string,
583− scope_id: r.scope_id as string,
584− name: r.name as string,
563+ namespace: decodeString(r.id),
564+ scope_id: decodeString(r.scope_id),
565+ name: decodeString(r.name),
585566 slot: "auth.oauth2.client_id",
586567 });
587568 }
588569 for (const r of byClientSecret) {
589570 out.push({
590− namespace: r.id as string,
591− scope_id: r.scope_id as string,
592− name: r.name as string,
571+ namespace: decodeString(r.id),
572+ scope_id: decodeString(r.scope_id),
573+ name: decodeString(r.name),
593574 slot: "auth.oauth2.client_secret",
594575 });
595576 }
@@ -605,9 +586,9 @@ export const makeGoogleDiscoveryStore = ( · 9 unmodified lines
605586 .pipe(
606587 Effect.map((rows) =>
607588 rows.map((r) => ({
608− namespace: r.id as string,
609− scope_id: r.scope_id as string,
610− name: r.name as string,
589+ namespace: decodeString(r.id),
590+ scope_id: decodeString(r.scope_id),
591+ name: decodeString(r.name),
611592 slot: "auth.oauth2.connection",
612593 })),
613594 ),
@@ -631,15 +612,15 @@ export const makeGoogleDiscoveryStore = ( · 17 unmodified lines
631612 return [
632613 ...headers.map((r) => ({
633614 kind: "credential_header" as const,
634− source_id: r.source_id as string,
635− scope_id: r.scope_id as string,
636− name: r.name as string,
615+ source_id: decodeString(r.source_id),
616+ scope_id: decodeString(r.scope_id),
617+ name: decodeString(r.name),
637618 })),
638619 ...params.map((r) => ({
639620 kind: "credential_query_param" as const,
640− source_id: r.source_id as string,
641− scope_id: r.scope_id as string,
642− name: r.name as string,
621+ source_id: decodeString(r.source_id),
622+ scope_id: decodeString(r.scope_id),
623+ name: decodeString(r.name),
643624 })),
644625 ];
645626 }),
@@ -651,8 +632,8 @@ export const makeGoogleDiscoveryStore = ( · 5 unmodified lines
651632 const requested = new Set(keys);
652633 const out = new Map<string, string>();
653634 for (const r of rows) {
654− const key = `${r.scope_id as string}:${r.id as string}`;
655− if (requested.has(key)) out.set(key, r.name as string);
635+ const key = `${decodeString(r.scope_id)}:${decodeString(r.id)}`;
636+ if (requested.has(key)) out.set(key, decodeString(r.name));
656637 }
657638 return out;
658639 }),
@@ -698,11 +679,7 @@ export const makeGoogleDiscoveryStore = ( · 39 unmodified lines
698679 forceAllowId: true,
699680 });
700681 }
701− const paramRows = valueMapToRows(
702− sourceId,
703− scope,
704− credentials.queryParams,
705− );
682+ const paramRows = valueMapToRows(sourceId, scope, credentials.queryParams);
706683 if (paramRows.length > 0) {
707684 yield* db.createMany({
708685 model: "google_discovery_source_credential_query_param",
@@ -719,7 +696,7 @@ export const makeGoogleDiscoveryStore = ( · 10 unmodified lines
719696 scope: string,
720697 ): Effect.Effect<GoogleDiscoveryStoredSourceData, StorageFailure> {
721698 return Effect.gen(function* () {
722− const partial = decodeJson(row.config) as Record<string, unknown>;
699+ const partial = decodeJsonObject(decodeJson(row.config));
723700 const headerRows = yield* db.findMany({
724701 model: "google_discovery_source_credential_header",
725702 where: [
@@ -737,8 +714,7 @@ export const makeGoogleDiscoveryStore = ( · 11 unmodified lines
737714 const headers = rowsToValueMap(headerRows);
738715 const queryParams = rowsToValueMap(paramRows);
739716 const credentials =
740− Object.keys(headers).length === 0 &&
741− Object.keys(queryParams).length === 0
717+ Object.keys(headers).length === 0 && Object.keys(queryParams).length === 0
742718 ? undefined
743719 : {
744720 ...(Object.keys(headers).length > 0 ? { headers } : {}),
@@ -757,9 +733,7 @@ export const makeGoogleDiscoveryStore = ( · 12 unmodified lines
757733 // Strip auth/credentials from the encoded source-data shape. Those
758734 // moved to columns and child tables; the remaining structural fields
759735 // live in the `config` JSON.
760−const stripExtractedFields = (
761− encoded: Record<string, unknown>,
762−): Record<string, unknown> => {
736+const stripExtractedFields = (encoded: Record<string, unknown>): Record<string, unknown> => {
763737 const { auth, credentials, ...rest } = encoded;
764738 void auth;
765739 void credentials;
packages/plugins/google-discovery/src/sdk/plugin.ts+137−165
@@ -1,4 +1,4 @@
1−import { Effect, Option } from "effect";
1+import { Effect, Option, Predicate, Schema } from "effect";
22
33 import {
44 ScopeId,
@@ -12,16 +12,12 @@ import { · 7 unmodified lines
1212 } from "@executor-js/sdk/core";
1313
1414 import { GoogleDiscoveryGroup } from "../api/group";
15−import {
16− GoogleDiscoveryExtensionService,
17− GoogleDiscoveryHandlers,
18−} from "../api/handlers";
15+import { GoogleDiscoveryExtensionService, GoogleDiscoveryHandlers } from "../api/handlers";
1916
2017 import {
2118 googleDiscoverySchema,
2219 makeGoogleDiscoveryStore,
2320 type GoogleDiscoveryStore,
24− type GoogleDiscoveryStoredSource,
2521 } from "./binding-store";
2622 import { extractGoogleDiscoveryManifest } from "./document";
2723 import { annotationsForOperation, invokeGoogleDiscoveryTool } from "./invoke";
@@ -86,46 +82,20 @@ export type GoogleDiscoveryExtensionFailure = · 58 unmodified lines
8682 | GoogleDiscoverySourceError
8783 | StorageFailure;
8884
89−export interface GoogleDiscoveryPluginExtension {
90− readonly probeDiscovery: (
91− input: string | GoogleDiscoveryProbeInput,
92− ) => Effect.Effect<
93− GoogleDiscoveryProbeResult,
94− GoogleDiscoveryParseError | GoogleDiscoverySourceError
95− >;
96− readonly addSource: (
97− input: GoogleDiscoveryAddSourceInput,
98− ) => Effect.Effect<
99− { readonly toolCount: number; readonly namespace: string },
100− GoogleDiscoveryParseError | GoogleDiscoverySourceError | StorageFailure
101− >;
102− readonly removeSource: (namespace: string, scope: string) => Effect.Effect<void, StorageFailure>;
103− readonly getSource: (
104− namespace: string,
105− scope: string,
106− ) => Effect.Effect<GoogleDiscoveryStoredSource | null, StorageFailure>;
107− readonly updateSource: (
108− namespace: string,
109− scope: string,
110− input: GoogleDiscoveryUpdateSourceInput,
111− ) => Effect.Effect<void, StorageFailure>;
112−}
113−
11485 // ---------------------------------------------------------------------------
11586 // URL normalization + slug helpers (unchanged)
11687 // ---------------------------------------------------------------------------
11788
11889 const DISCOVERY_SERVICE_HOST = "https://www.googleapis.com/discovery/v1/apis";
90+const decodeString = Schema.decodeUnknownSync(Schema.String);
91+const isGoogleDiscoverySourceError = (error: unknown): error is GoogleDiscoverySourceError =>
92+ Predicate.isTagged("GoogleDiscoverySourceError")(error);
11993
12094 const normalizeDiscoveryUrl = (discoveryUrl: string): string => {
12195 const trimmed = discoveryUrl.trim();
12296 if (trimmed.length === 0) return trimmed;
123− let parsed: URL;
124− try {
125− parsed = new URL(trimmed);
126− } catch {
127− return trimmed;
128− }
97+ if (!URL.canParse(trimmed)) return trimmed;
98+ const parsed = new URL(trimmed);
12999 if (parsed.pathname !== "/$discovery/rest") return trimmed;
130100 const version = parsed.searchParams.get("version")?.trim();
131101 if (!version) return trimmed;
@@ -164,7 +134,7 @@ const resolveGoogleDiscoveryCredentials = ( · 32 unmodified lines
164134 }),
165135 }).pipe(
166136 Effect.mapError((err) =>
167− err instanceof GoogleDiscoverySourceError
137+ isGoogleDiscoverySourceError(err)
168138 ? err
169139 : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }),
170140 ),
@@ -182,7 +152,7 @@ const resolveGoogleDiscoveryCredentials = ( · 11 unmodified lines
182152 }),
183153 }).pipe(
184154 Effect.mapError((err) =>
185− err instanceof GoogleDiscoverySourceError
155+ isGoogleDiscoverySourceError(err)
186156 ? err
187157 : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }),
188158 ),
@@ -200,29 +170,35 @@ const fetchDiscoveryDocument = ( · 11 unmodified lines
200170 readonly queryParams?: Record<string, string>;
201171 },
202172 ) =>
203− Effect.tryPromise({
204− try: async () => {
205− const url = new URL(normalizeDiscoveryUrl(discoveryUrl));
206− for (const [key, value] of Object.entries(credentials?.queryParams ?? {})) {
207− url.searchParams.set(key, value);
208− }
209− const response = await fetch(url.toString(), {
210− headers: credentials?.headers,
211− signal: AbortSignal.timeout(20_000),
212− });
213− if (!response.ok) {
214− throw new GoogleDiscoverySourceError({
215− message: `Google Discovery fetch failed with status ${response.status}`,
173+ Effect.gen(function* () {
174+ const response = yield* Effect.tryPromise({
175+ try: () => {
176+ const url = new URL(normalizeDiscoveryUrl(discoveryUrl));
177+ for (const [key, value] of Object.entries(credentials?.queryParams ?? {})) {
178+ url.searchParams.set(key, value);
179+ }
180+ return fetch(url.toString(), {
181+ headers: credentials?.headers,
182+ signal: AbortSignal.timeout(20_000),
216183 });
217− }
218− return response.text();
219− },
220− catch: (cause) =>
221− cause instanceof GoogleDiscoverySourceError
222− ? cause
223− : new GoogleDiscoverySourceError({
224− message: cause instanceof Error ? cause.message : String(cause),
225− }),
184+ },
185+ catch: () =>
186+ new GoogleDiscoverySourceError({
187+ message: "Google Discovery fetch failed",
188+ }),
189+ });
190+ if (!response.ok) {
191+ return yield* new GoogleDiscoverySourceError({
192+ message: `Google Discovery fetch failed with status ${response.status}`,
193+ });
194+ }
195+ return yield* Effect.tryPromise({
196+ try: () => response.text(),
197+ catch: () =>
198+ new GoogleDiscoverySourceError({
199+ message: "Google Discovery response body read failed",
200+ }),
201+ });
226202 });
227203
228204 const normalizeSlug = (value: string): string =>
@@ -302,6 +278,97 @@ const registerManifest = ( · 73 unmodified lines
302278 return manifest.methods.length;
303279 });
304280
281+const makeGoogleDiscoveryPluginExtension = (ctx: PluginCtx<GoogleDiscoveryStore>) => ({
282+ probeDiscovery: (input: string | GoogleDiscoveryProbeInput) =>
283+ Effect.gen(function* () {
284+ const discoveryUrl = typeof input === "string" ? input : input.discoveryUrl;
285+ const credentials =
286+ typeof input === "string"
287+ ? undefined
288+ : yield* resolveGoogleDiscoveryCredentials(input.credentials, ctx);
289+ const text = yield* fetchDiscoveryDocument(discoveryUrl, credentials);
290+ const manifest = yield* extractGoogleDiscoveryManifest(text);
291+ const scopes = Object.keys(
292+ Option.isSome(manifest.oauthScopes) ? manifest.oauthScopes.value : {},
293+ ).sort();
294+ const operations = manifest.methods.map((method) => ({
295+ toolPath: method.toolPath,
296+ method: method.binding.method,
297+ pathTemplate: method.binding.pathTemplate,
298+ description: Option.isSome(method.description) ? method.description.value : null,
299+ }));
300+ return {
301+ name: Option.isSome(manifest.title)
302+ ? manifest.title.value
303+ : `${manifest.service} ${manifest.version}`,
304+ title: Option.isSome(manifest.title) ? manifest.title.value : null,
305+ service: manifest.service,
306+ version: manifest.version,
307+ toolCount: manifest.methods.length,
308+ scopes,
309+ operations,
310+ };
311+ }),
312+
313+ addSource: (input: GoogleDiscoveryAddSourceInput) =>
314+ ctx.transaction(
315+ Effect.gen(function* () {
316+ const credentials = yield* resolveGoogleDiscoveryCredentials(input.credentials, ctx);
317+ const text = yield* fetchDiscoveryDocument(input.discoveryUrl, credentials);
318+ const manifest = yield* extractGoogleDiscoveryManifest(text);
319+ const namespace =
320+ input.namespace ??
321+ deriveNamespace({
322+ name: input.name,
323+ service: manifest.service,
324+ version: manifest.version,
325+ });
326+ const sourceData = new GoogleDiscoveryStoredSourceDataSchema({
327+ name: input.name,
328+ discoveryUrl: normalizeDiscoveryUrl(input.discoveryUrl),
329+ credentials: input.credentials,
330+ service: manifest.service,
331+ version: manifest.version,
332+ rootUrl: manifest.rootUrl,
333+ servicePath: manifest.servicePath,
334+ auth: input.auth,
335+ });
336+ const toolCount = yield* registerManifest(
337+ ctx,
338+ namespace,
339+ input.scope,
340+ manifest,
341+ sourceData,
342+ );
343+ return { toolCount, namespace };
344+ }),
345+ ),
346+
347+ removeSource: (namespace: string, scope: string) =>
348+ ctx.transaction(
349+ Effect.gen(function* () {
350+ yield* ctx.storage.removeBindingsBySource(namespace, scope);
351+ yield* ctx.storage.removeSource(namespace, scope);
352+ yield* ctx.core.sources.unregister(namespace).pipe(Effect.ignore);
353+ }),
354+ ),
355+
356+ // OAuth start/complete live on `ctx.oauth` now — the UI calls
357+ // the shared `/scopes/:scopeId/oauth/*` endpoints directly with a
358+ // Google-specific `authorization-code` strategy and writes the
359+ // resulting connection back via `updateSource`.
360+
361+ getSource: (namespace: string, scope: string) => ctx.storage.getSource(namespace, scope),
362+
363+ updateSource: (namespace: string, scope: string, input: GoogleDiscoveryUpdateSourceInput) =>
364+ ctx.storage.updateSourceMeta(namespace, scope, {
365+ name: input.name?.trim() || undefined,
366+ auth: input.auth,
367+ }),
368+});
369+
370+export type GoogleDiscoveryPluginExtension = ReturnType<typeof makeGoogleDiscoveryPluginExtension>;
371+
305372 // ---------------------------------------------------------------------------
306373 // Plugin
307374 // ---------------------------------------------------------------------------
@@ -312,101 +379,13 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 4 unmodified lines
312379 schema: googleDiscoverySchema,
313380 storage: (deps) => makeGoogleDiscoveryStore(deps),
314381
315− extension: (ctx) =>
316− ({
317− probeDiscovery: (input) =>
318− Effect.gen(function* () {
319− const discoveryUrl = typeof input === "string" ? input : input.discoveryUrl;
320− const credentials =
321− typeof input === "string"
322− ? undefined
323− : yield* resolveGoogleDiscoveryCredentials(input.credentials, ctx);
324− const text = yield* fetchDiscoveryDocument(discoveryUrl, credentials);
325− const manifest = yield* extractGoogleDiscoveryManifest(text);
326− const scopes = Object.keys(
327− Option.isSome(manifest.oauthScopes) ? manifest.oauthScopes.value : {},
328− ).sort();
329− const operations = manifest.methods.map((method) => ({
330− toolPath: method.toolPath,
331− method: method.binding.method,
332− pathTemplate: method.binding.pathTemplate,
333− description: Option.isSome(method.description) ? method.description.value : null,
334− }));
335− return {
336− name: Option.isSome(manifest.title)
337− ? manifest.title.value
338− : `${manifest.service} ${manifest.version}`,
339− title: Option.isSome(manifest.title) ? manifest.title.value : null,
340− service: manifest.service,
341− version: manifest.version,
342− toolCount: manifest.methods.length,
343− scopes,
344− operations,
345− };
346− }),
347−
348− addSource: (input) =>
349− ctx.transaction(
350− Effect.gen(function* () {
351− const credentials = yield* resolveGoogleDiscoveryCredentials(input.credentials, ctx);
352− const text = yield* fetchDiscoveryDocument(input.discoveryUrl, credentials);
353− const manifest = yield* extractGoogleDiscoveryManifest(text);
354− const namespace =
355− input.namespace ??
356− deriveNamespace({
357− name: input.name,
358− service: manifest.service,
359− version: manifest.version,
360− });
361− const sourceData = new GoogleDiscoveryStoredSourceDataSchema({
362− name: input.name,
363− discoveryUrl: normalizeDiscoveryUrl(input.discoveryUrl),
364− credentials: input.credentials,
365− service: manifest.service,
366− version: manifest.version,
367− rootUrl: manifest.rootUrl,
368− servicePath: manifest.servicePath,
369− auth: input.auth,
370− });
371− const toolCount = yield* registerManifest(
372− ctx,
373− namespace,
374− input.scope,
375− manifest,
376− sourceData,
377− );
378− return { toolCount, namespace };
379− }),
380− ),
381−
382− removeSource: (namespace, scope) =>
383− ctx.transaction(
384− Effect.gen(function* () {
385− yield* ctx.storage.removeBindingsBySource(namespace, scope);
386− yield* ctx.storage.removeSource(namespace, scope);
387− yield* ctx.core.sources.unregister(namespace).pipe(Effect.ignore);
388− }),
389− ),
390−
391− // OAuth start/complete live on `ctx.oauth` now — the UI calls
392− // the shared `/scopes/:scopeId/oauth/*` endpoints directly with a
393− // Google-specific `authorization-code` strategy and writes the
394− // resulting connection back via `updateSource`.
395−
396− getSource: (namespace, scope) => ctx.storage.getSource(namespace, scope),
397−
398− updateSource: (namespace, scope, input) =>
399− ctx.storage.updateSourceMeta(namespace, scope, {
400− name: input.name?.trim() || undefined,
401− auth: input.auth,
402− }),
403− }) satisfies GoogleDiscoveryPluginExtension,
382+ extension: makeGoogleDiscoveryPluginExtension,
404383
405384 invokeTool: ({ ctx, toolRow, args }) =>
406385 invokeGoogleDiscoveryTool({
407386 ctx: ctx as PluginCtx<GoogleDiscoveryStore>,
408387 toolId: toolRow.id,
409− toolScope: toolRow.scope_id as string,
388+ toolScope: decodeString(toolRow.scope_id),
410389 args,
411390 }),
412391
@@ -414,7 +393,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 1 unmodified line
414393 Effect.gen(function* () {
415394 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;
416395 const scopes = new Set<string>();
417− for (const row of toolRows) scopes.add(row.scope_id as string);
396+ for (const row of toolRows) scopes.add(decodeString(row.scope_id));
418397 const byScope = new Map<string, ReadonlyMap<string, GoogleDiscoveryMethodBinding>>();
419398 for (const scope of scopes) {
420399 const bindings = yield* typedCtx.storage.getBindingsForSource(sourceId, scope);
@@ -422,7 +401,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 1 unmodified line
422401 }
423402 const out: Record<string, ToolAnnotations> = {};
424403 for (const row of toolRows) {
425− const binding = byScope.get(row.scope_id as string)?.get(row.id);
404+ const binding = byScope.get(decodeString(row.scope_id))?.get(row.id);
426405 if (binding) {
427406 out[row.id] = annotationsForOperation(binding.method, binding.pathTemplate);
428407 }
@@ -443,16 +422,11 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 14 unmodified lines
443422 usagesForSecret: ({ ctx, args }) =>
444423 Effect.gen(function* () {
445424 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;
446− const sources = yield* typedCtx.storage.findSourcesBySecret(
447− args.secretId,
448− );
449− const childRows = yield* typedCtx.storage.findCredentialRowsBySecret(
450− args.secretId,
451− );
425+ const sources = yield* typedCtx.storage.findSourcesBySecret(args.secretId);
426+ const childRows = yield* typedCtx.storage.findCredentialRowsBySecret(args.secretId);
452427 const sourceKeys = new Set<string>();
453428 for (const s of sources) sourceKeys.add(`${s.scope_id}:${s.namespace}`);
454− for (const r of childRows)
455− sourceKeys.add(`${r.scope_id}:${r.source_id}`);
429+ for (const r of childRows) sourceKeys.add(`${r.scope_id}:${r.source_id}`);
456430 const names = yield* typedCtx.storage.lookupSourceNames([...sourceKeys]);
457431
458432 const out: Usage[] = [];
@@ -486,9 +460,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 27 unmodified lines
486460 usagesForConnection: ({ ctx, args }) =>
487461 Effect.gen(function* () {
488462 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;
489− const sources = yield* typedCtx.storage.findSourcesByConnection(
490− args.connectionId,
491− );
463+ const sources = yield* typedCtx.storage.findSourcesByConnection(args.connectionId);
492464 return sources.map(
493465 (s) =>
494466 new Usage({
@@ -563,7 +535,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 68 unmodified lines
563535 servicePath: manifest.servicePath,
564536 });
565537 yield* registerManifest(typedCtx, sourceId, scope, manifest, next);
566− }).pipe(Effect.mapError((err) => (err instanceof Error ? err : new Error(String(err))))),
538+ }),
567539
568540 // Connection refresh is owned by the canonical `"oauth2"`
569541 // ConnectionProvider registered by core — no plugin-specific handler