Skip to content

Commit 38a17ed

Browse files
committed
Fix Google Photos Picker discovery
1 parent c85d12c commit 38a17ed

3 files changed

Lines changed: 42 additions & 17 deletions

File tree

packages/plugins/google/src/sdk/discovery.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ it("accepts only supported HTTPS Google Discovery endpoints", () => {
5353
expect(
5454
normalizeGoogleDiscoveryUrl("https://chat.googleapis.com/$discovery/rest?version=v1"),
5555
).toBe("https://www.googleapis.com/discovery/v1/apis/chat/v1/rest");
56+
expect(
57+
normalizeGoogleDiscoveryUrl("https://photospicker.googleapis.com/$discovery/rest?version=v1"),
58+
).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1");
5659

5760
expect(isGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe(
5861
true,

packages/plugins/google/src/sdk/discovery.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,9 @@ export const normalizeGoogleDiscoveryUrl = (discoveryUrl: string): string | null
283283
) {
284284
return null;
285285
}
286-
return `${DISCOVERY_SERVICE_HOST}/${service}/${version}/rest`;
286+
return service === "photospicker"
287+
? `https://${host}/$discovery/rest?version=${version}`
288+
: `${DISCOVERY_SERVICE_HOST}/${service}/${version}/rest`;
287289
};
288290

289291
const normalizeDiscoveryUrl = (discoveryUrl: string): string => {
@@ -679,12 +681,39 @@ const GOOGLE_OAUTH_SECURITY_SCHEME = "googleOAuth2";
679681
const GOOGLE_PHOTOS_LIBRARY_SERVICE = "photoslibrary";
680682
const GOOGLE_PHOTOS_PICKER_SERVICE = "photospicker";
681683
const GOOGLE_PHOTOS_APPENDONLY_SCOPE = "https://www.googleapis.com/auth/photoslibrary.appendonly";
684+
const GOOGLE_PHOTOS_PICKER_SCOPE =
685+
"https://www.googleapis.com/auth/photospicker.mediaitems.readonly";
686+
const GOOGLE_PHOTOS_PICKER_SCOPE_DESCRIPTION = "Read selected Google Photos media";
682687
const GOOGLE_PHOTOS_UPLOAD_TOOL_PATH = "photoslibrary.mediaItems.upload";
683688
const GOOGLE_PHOTOS_UPLOAD_PATH = "/uploads";
684689

685690
const isGooglePhotosService = (service: string): boolean =>
686691
service === GOOGLE_PHOTOS_LIBRARY_SERVICE || service === GOOGLE_PHOTOS_PICKER_SERVICE;
687692

693+
const discoveryScopesForService = (
694+
service: string,
695+
document: DiscoveryDocument,
696+
): Record<string, string> => {
697+
const scopes = discoveryScopes(document);
698+
if (service !== GOOGLE_PHOTOS_PICKER_SERVICE || scopes[GOOGLE_PHOTOS_PICKER_SCOPE] !== undefined) {
699+
return scopes;
700+
}
701+
return {
702+
...scopes,
703+
[GOOGLE_PHOTOS_PICKER_SCOPE]: GOOGLE_PHOTOS_PICKER_SCOPE_DESCRIPTION,
704+
};
705+
};
706+
707+
const discoveryMethodScopesForService = (
708+
service: string,
709+
method: DiscoveryMethod,
710+
): readonly string[] => {
711+
const scopes = method.scopes ?? [];
712+
return service === GOOGLE_PHOTOS_PICKER_SERVICE && scopes.length === 0
713+
? [GOOGLE_PHOTOS_PICKER_SCOPE]
714+
: scopes;
715+
};
716+
688717
/** The v2 oauth auth template for a Google-discovery integration. The spec
689718
* itself carries the matching `securitySchemes.googleOAuth2` entry; this is the
690719
* catalog-level template a connection's access token renders through. */
@@ -811,6 +840,7 @@ export const convertGoogleDiscoveryToOpenApi = Effect.fn("OpenApi.convertGoogleD
811840
method,
812841
toolPath,
813842
pathTemplate: pathTemplate.startsWith("/") ? pathTemplate : `/${pathTemplate}`,
843+
oauthScopes: discoveryMethodScopesForService(service, method),
814844
});
815845
}
816846

@@ -826,7 +856,7 @@ export const convertGoogleDiscoveryToOpenApi = Effect.fn("OpenApi.convertGoogleD
826856
});
827857
}
828858

829-
const scopes = compactDiscoveryScopeMap(discoveryScopes(document));
859+
const scopes = compactDiscoveryScopeMap(discoveryScopesForService(service, document));
830860
const authenticationTemplate = googleOauthTemplate(scopes);
831861

832862
const spec: OpenApiDocument = {
@@ -923,7 +953,7 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn(
923953
for (const info of infos) {
924954
const schemaPrefix = schemaComponentPart(`${info.service}_${info.version}`);
925955
const schemaNameForRef = (name: string) => `${schemaPrefix}_${schemaComponentPart(name)}`;
926-
const scopeDescriptions = discoveryScopes(info.document);
956+
const scopeDescriptions = discoveryScopesForService(info.service, info.document);
927957
const filterPhotosScopes = consentScopeSet !== null && isGooglePhotosService(info.service);
928958

929959
for (const [scope, description] of Object.entries(scopeDescriptions)) {
@@ -939,7 +969,7 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn(
939969
const methodId = Option.getOrUndefined(method.id);
940970
const rawPathTemplate = Option.getOrUndefined(method.path);
941971
if (!methodId || !rawPathTemplate || !method.httpMethod) continue;
942-
const methodScopes = method.scopes ?? [];
972+
const methodScopes = discoveryMethodScopesForService(info.service, method);
943973
const oauthScopes = filterPhotosScopes
944974
? methodScopes.filter((scope) => consentScopeSet.has(scope))
945975
: methodScopes;

packages/plugins/google/src/sdk/plugin.test.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const CALENDAR_URL = "https://www.googleapis.com/discovery/v1/apis/calendar/v3/r
3535
const GMAIL_URL = "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest";
3636
const DRIVE_URL = "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest";
3737
const PHOTOS_LIBRARY_URL = "https://www.googleapis.com/discovery/v1/apis/photoslibrary/v1/rest";
38-
const PHOTOS_PICKER_URL = "https://www.googleapis.com/discovery/v1/apis/photospicker/v1/rest";
38+
const PHOTOS_PICKER_URL = "https://photospicker.googleapis.com/$discovery/rest?version=v1";
3939

4040
const calendarDoc = {
4141
name: "calendar",
@@ -197,23 +197,13 @@ const photosPickerDoc = {
197197
title: "Google Photos Picker API",
198198
rootUrl: "https://photospicker.googleapis.com/",
199199
servicePath: "v1/",
200-
auth: {
201-
oauth2: {
202-
scopes: {
203-
"https://www.googleapis.com/auth/photospicker.mediaitems.readonly": {
204-
description: "Read selected Google Photos media",
205-
},
206-
},
207-
},
208-
},
209200
resources: {
210201
mediaItems: {
211202
methods: {
212203
list: {
213204
id: "photospicker.mediaItems.list",
214205
httpMethod: "GET",
215206
path: "mediaItems",
216-
scopes: ["https://www.googleapis.com/auth/photospicker.mediaitems.readonly"],
217207
parameters: {},
218208
},
219209
},
@@ -233,12 +223,14 @@ const DISCOVERY_BODIES: Readonly<Record<string, string>> = {
233223
};
234224

235225
// A stub HTTP client that serves the canned Discovery document for whichever
236-
// URL the bundle converter fetches (query params are ignored when matching).
226+
// URL the bundle converter fetches. Service-hosted Discovery URLs carry their
227+
// version in the query string, so match the full URL before falling back to the
228+
// path-only key used by central Discovery URLs.
237229
const discoveryHttpClientLayer = Layer.succeed(HttpClient.HttpClient)(
238230
HttpClient.make((request: HttpClientRequest.HttpClientRequest) => {
239231
const url = new URL(request.url);
240232
const key = `${url.origin}${url.pathname}`;
241-
const body = DISCOVERY_BODIES[key];
233+
const body = DISCOVERY_BODIES[url.toString()] ?? DISCOVERY_BODIES[key];
242234
return Effect.succeed(
243235
HttpClientResponse.fromWeb(
244236
request,

0 commit comments

Comments
 (0)