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
13// survive adapter serialization.13// survive adapter serialization.
14// ---------------------------------------------------------------------------14// ---------------------------------------------------------------------------
15 15
16import { Effect, Schema } from "effect";16import { Effect, Option, Schema } from "effect";
17 17
18import {18import { defineSchema, type StorageDeps, type StorageFailure } from "@executor-js/sdk/core";
19 defineSchema,
20 type StorageDeps,
21 type StorageFailure,
22} from "@executor-js/sdk/core";
23 19
24import {20import {
25 GoogleDiscoveryMethodBinding,21 GoogleDiscoveryMethodBinding,
@@ -135,15 +131,14 @@ const encodeBinding = Schema.encodeSync(GoogleDiscoveryMethodBinding); · 109 unmodified lines
135const decodeBinding = Schema.decodeUnknownSync(GoogleDiscoveryMethodBinding);131const decodeBinding = Schema.decodeUnknownSync(GoogleDiscoveryMethodBinding);
136 132
137const toJsonRecord = (value: unknown): Record<string, unknown> => value as Record<string, unknown>;133const toJsonRecord = (value: unknown): Record<string, unknown> => value as Record<string, unknown>;
134const decodeString = Schema.decodeUnknownSync(Schema.String);
135const decodeJsonObject = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown));
136const decodeJsonString = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
138 137
139const decodeJson = (value: unknown): unknown => {138const decodeJson = (value: unknown): unknown => {
140 if (value === null || value === undefined) return value;139 if (value === null || value === undefined) return value;
141 if (typeof value !== "string") return value;140 if (typeof value !== "string") return value;
142 try {141 return Option.getOrElse(decodeJsonString(value), () => value);
143 return JSON.parse(value);
144 } catch {
145 return value;
146 }
147};142};
148 143
149// --- auth column packing/unpacking ------------------------------------------144// --- auth column packing/unpacking ------------------------------------------
@@ -239,12 +234,10 @@ const rowsToValueMap = ( · 89 unmodified lines
239): Record<string, GoogleDiscoveryCredentialValue> => {234): Record<string, GoogleDiscoveryCredentialValue> => {
240 const out: Record<string, GoogleDiscoveryCredentialValue> = {};235 const out: Record<string, GoogleDiscoveryCredentialValue> = {};
241 for (const row of rows) {236 for (const row of rows) {
242 const name = row.name as string;237 const name = decodeString(row.name);
243 if (row.kind === "secret" && typeof row.secret_id === "string") {238 if (row.kind === "secret" && typeof row.secret_id === "string") {
244 const prefix = row.secret_prefix as string | undefined | null;239 const prefix = row.secret_prefix as string | undefined | null;
245 out[name] = prefix240 out[name] = prefix ? { secretId: row.secret_id, prefix } : { secretId: row.secret_id };
246 ? { secretId: row.secret_id, prefix }
247 : { secretId: row.secret_id };
248 } else if (row.kind === "text" && typeof row.text_value === "string") {241 } else if (row.kind === "text" && typeof row.text_value === "string") {
249 out[name] = row.text_value;242 out[name] = row.text_value;
250 }243 }
@@ -320,9 +313,7 @@ export interface GoogleDiscoveryStore { · 69 unmodified lines
320 313
321 /** Source rows whose oauth2 auth columns reference the given secret id.314 /** Source rows whose oauth2 auth columns reference the given secret id.
322 * `slot` distinguishes client_id vs client_secret. */315 * `slot` distinguishes client_id vs client_secret. */
323 readonly findSourcesBySecret: (316 readonly findSourcesBySecret: (secretId: string) => Effect.Effect<
324 secretId: string,
325 ) => Effect.Effect<
326 readonly {317 readonly {
327 readonly namespace: string;318 readonly namespace: string;
328 readonly scope_id: string;319 readonly scope_id: string;
@@ -333,9 +324,7 @@ export interface GoogleDiscoveryStore { · 4 unmodified lines
333 >;324 >;
334 325
335 /** Source rows whose oauth2 auth points at the given connection id. */326 /** Source rows whose oauth2 auth points at the given connection id. */
336 readonly findSourcesByConnection: (327 readonly findSourcesByConnection: (connectionId: string) => Effect.Effect<
337 connectionId: string,
338 ) => Effect.Effect<
339 readonly {328 readonly {
340 readonly namespace: string;329 readonly namespace: string;
341 readonly scope_id: string;330 readonly scope_id: string;
@@ -382,7 +371,7 @@ export const makeGoogleDiscoveryStore = ( · 40 unmodified lines
382 });371 });
383 if (!row) return null;372 if (!row) return null;
384 const decoded = decodeBinding(decodeJson(row.binding));373 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 };
386 }),375 }),
387 376
388 putBinding: (toolId, sourceId, scope, binding) =>377 putBinding: (toolId, sourceId, scope, binding) =>
@@ -420,7 +409,7 @@ export const makeGoogleDiscoveryStore = ( · 31 unmodified lines
420 { field: "scope_id", value: scope },409 { field: "scope_id", value: scope },
421 ],410 ],
422 });411 });
423 const ids = rows.map((r) => r.id as string);412 const ids = rows.map((r) => decodeString(r.id));
424 yield* db.deleteMany({413 yield* db.deleteMany({
425 model: "google_discovery_binding",414 model: "google_discovery_binding",
426 where: [415 where: [
@@ -442,7 +431,7 @@ export const makeGoogleDiscoveryStore = ( · 15 unmodified lines
442 });431 });
443 const out = new Map<string, GoogleDiscoveryMethodBinding>();432 const out = new Map<string, GoogleDiscoveryMethodBinding>();
444 for (const row of rows) {433 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)));
446 }435 }
447 return out;436 return out;
448 }),437 }),
@@ -462,7 +451,7 @@ export const makeGoogleDiscoveryStore = ( · 13 unmodified lines
462 yield* deleteSourceChildren(source.namespace, source.scope);451 yield* deleteSourceChildren(source.namespace, source.scope);
463 452
464 const encoded = stripExtractedFields(453 const encoded = stripExtractedFields(
465 encodeStoredSourceData(source.config) as Record<string, unknown>,454 decodeJsonObject(encodeStoredSourceData(source.config)),
466 );455 );
467 yield* db.create({456 yield* db.create({
468 model: "google_discovery_source",457 model: "google_discovery_source",
@@ -477,11 +466,7 @@ export const makeGoogleDiscoveryStore = ( · 8 unmodified lines
477 },466 },
478 forceAllowId: true,467 forceAllowId: true,
479 });468 });
480 yield* writeCredentialRows(469 yield* writeCredentialRows(source.namespace, source.scope, source.config.credentials);
481 source.namespace,
482 source.scope,
483 source.config.credentials,
484 );
485 }),470 }),
486 471
487 updateSourceMeta: (sourceId, scope, update) =>472 updateSourceMeta: (sourceId, scope, update) =>
@@ -502,7 +487,7 @@ export const makeGoogleDiscoveryStore = ( · 14 unmodified lines
502 { field: "scope_id", value: scope },487 { field: "scope_id", value: scope },
503 ],488 ],
504 update: {489 update: {
505 name: update.name ?? (row.name as string),490 name: update.name ?? decodeString(row.name),
506 updated_at: new Date(),491 updated_at: new Date(),
507 ...authToColumns(auth),492 ...authToColumns(auth),
508 },493 },
@@ -532,9 +517,9 @@ export const makeGoogleDiscoveryStore = ( · 23 unmodified lines
532 });517 });
533 if (!row) return null;518 if (!row) return null;
534 return {519 return {
535 namespace: row.id as string,520 namespace: decodeString(row.id),
536 scope: row.scope_id as string,521 scope: decodeString(row.scope_id),
537 name: row.name as string,522 name: decodeString(row.name),
538 config: yield* hydrateStoredSourceData(row, sourceId, scope),523 config: yield* hydrateStoredSourceData(row, sourceId, scope),
539 };524 };
540 }),525 }),
@@ -558,15 +543,11 @@ export const makeGoogleDiscoveryStore = ( · 17 unmodified lines
558 [543 [
559 db.findMany({544 db.findMany({
560 model: "google_discovery_source",545 model: "google_discovery_source",
561 where: [546 where: [{ field: "auth_client_id_secret_id", value: secretId }],
562 { field: "auth_client_id_secret_id", value: secretId },
563 ],
564 }),547 }),
565 db.findMany({548 db.findMany({
566 model: "google_discovery_source",549 model: "google_discovery_source",
567 where: [550 where: [{ field: "auth_client_secret_secret_id", value: secretId }],
568 { field: "auth_client_secret_secret_id", value: secretId },
569 ],
570 }),551 }),
571 ],552 ],
572 { concurrency: "unbounded" },553 { concurrency: "unbounded" },
@@ -579,17 +560,17 @@ export const makeGoogleDiscoveryStore = ( · 6 unmodified lines
579 }[] = [];560 }[] = [];
580 for (const r of byClientId) {561 for (const r of byClientId) {
581 out.push({562 out.push({
582 namespace: r.id as string,563 namespace: decodeString(r.id),
583 scope_id: r.scope_id as string,564 scope_id: decodeString(r.scope_id),
584 name: r.name as string,565 name: decodeString(r.name),
585 slot: "auth.oauth2.client_id",566 slot: "auth.oauth2.client_id",
586 });567 });
587 }568 }
588 for (const r of byClientSecret) {569 for (const r of byClientSecret) {
589 out.push({570 out.push({
590 namespace: r.id as string,571 namespace: decodeString(r.id),
591 scope_id: r.scope_id as string,572 scope_id: decodeString(r.scope_id),
592 name: r.name as string,573 name: decodeString(r.name),
593 slot: "auth.oauth2.client_secret",574 slot: "auth.oauth2.client_secret",
594 });575 });
595 }576 }
@@ -605,9 +586,9 @@ export const makeGoogleDiscoveryStore = ( · 9 unmodified lines
605 .pipe(586 .pipe(
606 Effect.map((rows) =>587 Effect.map((rows) =>
607 rows.map((r) => ({588 rows.map((r) => ({
608 namespace: r.id as string,589 namespace: decodeString(r.id),
609 scope_id: r.scope_id as string,590 scope_id: decodeString(r.scope_id),
610 name: r.name as string,591 name: decodeString(r.name),
611 slot: "auth.oauth2.connection",592 slot: "auth.oauth2.connection",
612 })),593 })),
613 ),594 ),
@@ -631,15 +612,15 @@ export const makeGoogleDiscoveryStore = ( · 17 unmodified lines
631 return [612 return [
632 ...headers.map((r) => ({613 ...headers.map((r) => ({
633 kind: "credential_header" as const,614 kind: "credential_header" as const,
634 source_id: r.source_id as string,615 source_id: decodeString(r.source_id),
635 scope_id: r.scope_id as string,616 scope_id: decodeString(r.scope_id),
636 name: r.name as string,617 name: decodeString(r.name),
637 })),618 })),
638 ...params.map((r) => ({619 ...params.map((r) => ({
639 kind: "credential_query_param" as const,620 kind: "credential_query_param" as const,
640 source_id: r.source_id as string,621 source_id: decodeString(r.source_id),
641 scope_id: r.scope_id as string,622 scope_id: decodeString(r.scope_id),
642 name: r.name as string,623 name: decodeString(r.name),
643 })),624 })),
644 ];625 ];
645 }),626 }),
@@ -651,8 +632,8 @@ export const makeGoogleDiscoveryStore = ( · 5 unmodified lines
651 const requested = new Set(keys);632 const requested = new Set(keys);
652 const out = new Map<string, string>();633 const out = new Map<string, string>();
653 for (const r of rows) {634 for (const r of rows) {
654 const key = `${r.scope_id as string}:${r.id as string}`;635 const key = `${decodeString(r.scope_id)}:${decodeString(r.id)}`;
655 if (requested.has(key)) out.set(key, r.name as string);636 if (requested.has(key)) out.set(key, decodeString(r.name));
656 }637 }
657 return out;638 return out;
658 }),639 }),
@@ -698,11 +679,7 @@ export const makeGoogleDiscoveryStore = ( · 39 unmodified lines
698 forceAllowId: true,679 forceAllowId: true,
699 });680 });
700 }681 }
701 const paramRows = valueMapToRows(682 const paramRows = valueMapToRows(sourceId, scope, credentials.queryParams);
702 sourceId,
703 scope,
704 credentials.queryParams,
705 );
706 if (paramRows.length > 0) {683 if (paramRows.length > 0) {
707 yield* db.createMany({684 yield* db.createMany({
708 model: "google_discovery_source_credential_query_param",685 model: "google_discovery_source_credential_query_param",
@@ -719,7 +696,7 @@ export const makeGoogleDiscoveryStore = ( · 10 unmodified lines
719 scope: string,696 scope: string,
720 ): Effect.Effect<GoogleDiscoveryStoredSourceData, StorageFailure> {697 ): Effect.Effect<GoogleDiscoveryStoredSourceData, StorageFailure> {
721 return Effect.gen(function* () {698 return Effect.gen(function* () {
722 const partial = decodeJson(row.config) as Record<string, unknown>;699 const partial = decodeJsonObject(decodeJson(row.config));
723 const headerRows = yield* db.findMany({700 const headerRows = yield* db.findMany({
724 model: "google_discovery_source_credential_header",701 model: "google_discovery_source_credential_header",
725 where: [702 where: [
@@ -737,8 +714,7 @@ export const makeGoogleDiscoveryStore = ( · 11 unmodified lines
737 const headers = rowsToValueMap(headerRows);714 const headers = rowsToValueMap(headerRows);
738 const queryParams = rowsToValueMap(paramRows);715 const queryParams = rowsToValueMap(paramRows);
739 const credentials =716 const credentials =
740 Object.keys(headers).length === 0 &&717 Object.keys(headers).length === 0 && Object.keys(queryParams).length === 0
741 Object.keys(queryParams).length === 0
742 ? undefined718 ? undefined
743 : {719 : {
744 ...(Object.keys(headers).length > 0 ? { headers } : {}),720 ...(Object.keys(headers).length > 0 ? { headers } : {}),
@@ -757,9 +733,7 @@ export const makeGoogleDiscoveryStore = ( · 12 unmodified lines
757// Strip auth/credentials from the encoded source-data shape. Those733// Strip auth/credentials from the encoded source-data shape. Those
758// moved to columns and child tables; the remaining structural fields734// moved to columns and child tables; the remaining structural fields
759// live in the `config` JSON.735// live in the `config` JSON.
760const stripExtractedFields = (736const stripExtractedFields = (encoded: Record<string, unknown>): Record<string, unknown> => {
761 encoded: Record<string, unknown>,
762): Record<string, unknown> => {
763 const { auth, credentials, ...rest } = encoded;737 const { auth, credentials, ...rest } = encoded;
764 void auth;738 void auth;
765 void credentials;739 void credentials;
packages/plugins/google-discovery/src/sdk/plugin.ts+137−165
@@ -1,4 +1,4 @@
1import { Effect, Option } from "effect";1import { Effect, Option, Predicate, Schema } from "effect";
2 2
3import {3import {
4 ScopeId,4 ScopeId,
@@ -12,16 +12,12 @@ import { · 7 unmodified lines
12} from "@executor-js/sdk/core";12} from "@executor-js/sdk/core";
13 13
14import { GoogleDiscoveryGroup } from "../api/group";14import { GoogleDiscoveryGroup } from "../api/group";
15import {15import { GoogleDiscoveryExtensionService, GoogleDiscoveryHandlers } from "../api/handlers";
16 GoogleDiscoveryExtensionService,
17 GoogleDiscoveryHandlers,
18} from "../api/handlers";
19 16
20import {17import {
21 googleDiscoverySchema,18 googleDiscoverySchema,
22 makeGoogleDiscoveryStore,19 makeGoogleDiscoveryStore,
23 type GoogleDiscoveryStore,20 type GoogleDiscoveryStore,
24 type GoogleDiscoveryStoredSource,
25} from "./binding-store";21} from "./binding-store";
26import { extractGoogleDiscoveryManifest } from "./document";22import { extractGoogleDiscoveryManifest } from "./document";
27import { annotationsForOperation, invokeGoogleDiscoveryTool } from "./invoke";23import { annotationsForOperation, invokeGoogleDiscoveryTool } from "./invoke";
@@ -86,46 +82,20 @@ export type GoogleDiscoveryExtensionFailure = · 58 unmodified lines
86 | GoogleDiscoverySourceError82 | GoogleDiscoverySourceError
87 | StorageFailure;83 | StorageFailure;
88 84
89export 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
114// ---------------------------------------------------------------------------85// ---------------------------------------------------------------------------
115// URL normalization + slug helpers (unchanged)86// URL normalization + slug helpers (unchanged)
116// ---------------------------------------------------------------------------87// ---------------------------------------------------------------------------
117 88
118const DISCOVERY_SERVICE_HOST = "https://www.googleapis.com/discovery/v1/apis";89const DISCOVERY_SERVICE_HOST = "https://www.googleapis.com/discovery/v1/apis";
90const decodeString = Schema.decodeUnknownSync(Schema.String);
91const isGoogleDiscoverySourceError = (error: unknown): error is GoogleDiscoverySourceError =>
92 Predicate.isTagged("GoogleDiscoverySourceError")(error);
119 93
120const normalizeDiscoveryUrl = (discoveryUrl: string): string => {94const normalizeDiscoveryUrl = (discoveryUrl: string): string => {
121 const trimmed = discoveryUrl.trim();95 const trimmed = discoveryUrl.trim();
122 if (trimmed.length === 0) return trimmed;96 if (trimmed.length === 0) return trimmed;
123 let parsed: URL;97 if (!URL.canParse(trimmed)) return trimmed;
124 try {98 const parsed = new URL(trimmed);
125 parsed = new URL(trimmed);
126 } catch {
127 return trimmed;
128 }
129 if (parsed.pathname !== "/$discovery/rest") return trimmed;99 if (parsed.pathname !== "/$discovery/rest") return trimmed;
130 const version = parsed.searchParams.get("version")?.trim();100 const version = parsed.searchParams.get("version")?.trim();
131 if (!version) return trimmed;101 if (!version) return trimmed;
@@ -164,7 +134,7 @@ const resolveGoogleDiscoveryCredentials = ( · 32 unmodified lines
164 }),134 }),
165 }).pipe(135 }).pipe(
166 Effect.mapError((err) =>136 Effect.mapError((err) =>
167 err instanceof GoogleDiscoverySourceError137 isGoogleDiscoverySourceError(err)
168 ? err138 ? err
169 : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }),139 : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }),
170 ),140 ),
@@ -182,7 +152,7 @@ const resolveGoogleDiscoveryCredentials = ( · 11 unmodified lines
182 }),152 }),
183 }).pipe(153 }).pipe(
184 Effect.mapError((err) =>154 Effect.mapError((err) =>
185 err instanceof GoogleDiscoverySourceError155 isGoogleDiscoverySourceError(err)
186 ? err156 ? err
187 : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }),157 : new GoogleDiscoverySourceError({ message: "Secret resolution failed" }),
188 ),158 ),
@@ -200,29 +170,35 @@ const fetchDiscoveryDocument = ( · 11 unmodified lines
200 readonly queryParams?: Record<string, string>;170 readonly queryParams?: Record<string, string>;
201 },171 },
202) =>172) =>
203 Effect.tryPromise({173 Effect.gen(function* () {
204 try: async () => {174 const response = yield* Effect.tryPromise({
205 const url = new URL(normalizeDiscoveryUrl(discoveryUrl));175 try: () => {
206 for (const [key, value] of Object.entries(credentials?.queryParams ?? {})) {176 const url = new URL(normalizeDiscoveryUrl(discoveryUrl));
207 url.searchParams.set(key, value);177 for (const [key, value] of Object.entries(credentials?.queryParams ?? {})) {
208 }178 url.searchParams.set(key, value);
209 const response = await fetch(url.toString(), {179 }
210 headers: credentials?.headers,180 return fetch(url.toString(), {
211 signal: AbortSignal.timeout(20_000),181 headers: credentials?.headers,
212 });182 signal: AbortSignal.timeout(20_000),
213 if (!response.ok) {
214 throw new GoogleDiscoverySourceError({
215 message: `Google Discovery fetch failed with status ${response.status}`,
216 });183 });
217 }184 },
218 return response.text();185 catch: () =>
219 },186 new GoogleDiscoverySourceError({
220 catch: (cause) =>187 message: "Google Discovery fetch failed",
221 cause instanceof GoogleDiscoverySourceError188 }),
222 ? cause189 });
223 : new GoogleDiscoverySourceError({190 if (!response.ok) {
224 message: cause instanceof Error ? cause.message : String(cause),191 return yield* new GoogleDiscoverySourceError({
225 }),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 });
226 });202 });
227 203
228const normalizeSlug = (value: string): string =>204const normalizeSlug = (value: string): string =>
@@ -302,6 +278,97 @@ const registerManifest = ( · 73 unmodified lines
302 return manifest.methods.length;278 return manifest.methods.length;
303 });279 });
304 280
281const 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
370export type GoogleDiscoveryPluginExtension = ReturnType<typeof makeGoogleDiscoveryPluginExtension>;
371
305// ---------------------------------------------------------------------------372// ---------------------------------------------------------------------------
306// Plugin373// Plugin
307// ---------------------------------------------------------------------------374// ---------------------------------------------------------------------------
@@ -312,101 +379,13 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 4 unmodified lines
312 schema: googleDiscoverySchema,379 schema: googleDiscoverySchema,
313 storage: (deps) => makeGoogleDiscoveryStore(deps),380 storage: (deps) => makeGoogleDiscoveryStore(deps),
314 381
315 extension: (ctx) =>382 extension: makeGoogleDiscoveryPluginExtension,
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,
404 383
405 invokeTool: ({ ctx, toolRow, args }) =>384 invokeTool: ({ ctx, toolRow, args }) =>
406 invokeGoogleDiscoveryTool({385 invokeGoogleDiscoveryTool({
407 ctx: ctx as PluginCtx<GoogleDiscoveryStore>,386 ctx: ctx as PluginCtx<GoogleDiscoveryStore>,
408 toolId: toolRow.id,387 toolId: toolRow.id,
409 toolScope: toolRow.scope_id as string,388 toolScope: decodeString(toolRow.scope_id),
410 args,389 args,
411 }),390 }),
412 391
@@ -414,7 +393,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 1 unmodified line
414 Effect.gen(function* () {393 Effect.gen(function* () {
415 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;394 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;
416 const scopes = new Set<string>();395 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));
418 const byScope = new Map<string, ReadonlyMap<string, GoogleDiscoveryMethodBinding>>();397 const byScope = new Map<string, ReadonlyMap<string, GoogleDiscoveryMethodBinding>>();
419 for (const scope of scopes) {398 for (const scope of scopes) {
420 const bindings = yield* typedCtx.storage.getBindingsForSource(sourceId, scope);399 const bindings = yield* typedCtx.storage.getBindingsForSource(sourceId, scope);
@@ -422,7 +401,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 1 unmodified line
422 }401 }
423 const out: Record<string, ToolAnnotations> = {};402 const out: Record<string, ToolAnnotations> = {};
424 for (const row of toolRows) {403 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);
426 if (binding) {405 if (binding) {
427 out[row.id] = annotationsForOperation(binding.method, binding.pathTemplate);406 out[row.id] = annotationsForOperation(binding.method, binding.pathTemplate);
428 }407 }
@@ -443,16 +422,11 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 14 unmodified lines
443 usagesForSecret: ({ ctx, args }) =>422 usagesForSecret: ({ ctx, args }) =>
444 Effect.gen(function* () {423 Effect.gen(function* () {
445 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;424 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;
446 const sources = yield* typedCtx.storage.findSourcesBySecret(425 const sources = yield* typedCtx.storage.findSourcesBySecret(args.secretId);
447 args.secretId,426 const childRows = yield* typedCtx.storage.findCredentialRowsBySecret(args.secretId);
448 );
449 const childRows = yield* typedCtx.storage.findCredentialRowsBySecret(
450 args.secretId,
451 );
452 const sourceKeys = new Set<string>();427 const sourceKeys = new Set<string>();
453 for (const s of sources) sourceKeys.add(`${s.scope_id}:${s.namespace}`);428 for (const s of sources) sourceKeys.add(`${s.scope_id}:${s.namespace}`);
454 for (const r of childRows)429 for (const r of childRows) sourceKeys.add(`${r.scope_id}:${r.source_id}`);
455 sourceKeys.add(`${r.scope_id}:${r.source_id}`);
456 const names = yield* typedCtx.storage.lookupSourceNames([...sourceKeys]);430 const names = yield* typedCtx.storage.lookupSourceNames([...sourceKeys]);
457 431
458 const out: Usage[] = [];432 const out: Usage[] = [];
@@ -486,9 +460,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 27 unmodified lines
486 usagesForConnection: ({ ctx, args }) =>460 usagesForConnection: ({ ctx, args }) =>
487 Effect.gen(function* () {461 Effect.gen(function* () {
488 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;462 const typedCtx = ctx as PluginCtx<GoogleDiscoveryStore>;
489 const sources = yield* typedCtx.storage.findSourcesByConnection(463 const sources = yield* typedCtx.storage.findSourcesByConnection(args.connectionId);
490 args.connectionId,
491 );
492 return sources.map(464 return sources.map(
493 (s) =>465 (s) =>
494 new Usage({466 new Usage({
@@ -563,7 +535,7 @@ export const googleDiscoveryPlugin = definePlugin(() => ({ · 68 unmodified lines
563 servicePath: manifest.servicePath,535 servicePath: manifest.servicePath,
564 });536 });
565 yield* registerManifest(typedCtx, sourceId, scope, manifest, next);537 yield* registerManifest(typedCtx, sourceId, scope, manifest, next);
566 }).pipe(Effect.mapError((err) => (err instanceof Error ? err : new Error(String(err))))),538 }),
567 539
568 // Connection refresh is owned by the canonical `"oauth2"`540 // Connection refresh is owned by the canonical `"oauth2"`
569 // ConnectionProvider registered by core — no plugin-specific handler541 // ConnectionProvider registered by core — no plugin-specific handler