@@ -18,6 +18,10 @@ const DISCOVERY_SERVICE_HOST = "https://www.googleapis.com/discovery/v1/apis";
1818const GOOGLE_BUNDLE_BASE_URL = "https://www.googleapis.com/" ;
1919const GOOGLE_OAUTH_AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth" ;
2020const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token" ;
21+ const GOOGLE_PHOTOS_PICKER_SERVICE = "photospicker" ;
22+ const GOOGLE_PHOTOS_PICKER_SCOPE =
23+ "https://www.googleapis.com/auth/photospicker.mediaitems.readonly" ;
24+ const GOOGLE_PHOTOS_PICKER_SCOPE_DESCRIPTION = "Read selected Google Photos media" ;
2125const OPENAPI_SCHEMA_TYPES = new Set ( [
2226 "array" ,
2327 "boolean" ,
@@ -28,6 +32,23 @@ const OPENAPI_SCHEMA_TYPES = new Set([
2832 "string" ,
2933] ) ;
3034
35+ type GoogleDiscoveryServiceOverride = {
36+ readonly preserveServiceHostedUrl ?: true ;
37+ readonly scopes ?: Record < string , string > ;
38+ readonly fallbackMethodScopes ?: readonly string [ ] ;
39+ } ;
40+
41+ // Photos Picker Discovery is only available service-hosted, and its live doc omits the required OAuth scope.
42+ const GOOGLE_DISCOVERY_SERVICE_OVERRIDES : Record < string , GoogleDiscoveryServiceOverride > = {
43+ [ GOOGLE_PHOTOS_PICKER_SERVICE ] : {
44+ preserveServiceHostedUrl : true ,
45+ scopes : {
46+ [ GOOGLE_PHOTOS_PICKER_SCOPE ] : GOOGLE_PHOTOS_PICKER_SCOPE_DESCRIPTION ,
47+ } ,
48+ fallbackMethodScopes : [ GOOGLE_PHOTOS_PICKER_SCOPE ] ,
49+ } ,
50+ } ;
51+
3152type JsonPrimitive = string | number | boolean | null ;
3253type JsonValue = JsonPrimitive | readonly JsonValue [ ] | { readonly [ key : string ] : JsonValue } ;
3354
@@ -283,7 +304,10 @@ export const normalizeGoogleDiscoveryUrl = (discoveryUrl: string): string | null
283304 ) {
284305 return null ;
285306 }
286- return `${ DISCOVERY_SERVICE_HOST } /${ service } /${ version } /rest` ;
307+ const override = GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ;
308+ return override ?. preserveServiceHostedUrl === true
309+ ? `https://${ host } /$discovery/rest?version=${ version } `
310+ : `${ DISCOVERY_SERVICE_HOST } /${ service } /${ version } /rest` ;
287311} ;
288312
289313const normalizeDiscoveryUrl = ( discoveryUrl : string ) : string => {
@@ -677,14 +701,38 @@ const buildDiscoveryOperation = (input: {
677701
678702const GOOGLE_OAUTH_SECURITY_SCHEME = "googleOAuth2" ;
679703const GOOGLE_PHOTOS_LIBRARY_SERVICE = "photoslibrary" ;
680- const GOOGLE_PHOTOS_PICKER_SERVICE = "photospicker" ;
681704const GOOGLE_PHOTOS_APPENDONLY_SCOPE = "https://www.googleapis.com/auth/photoslibrary.appendonly" ;
682705const GOOGLE_PHOTOS_UPLOAD_TOOL_PATH = "photoslibrary.mediaItems.upload" ;
683706const GOOGLE_PHOTOS_UPLOAD_PATH = "/uploads" ;
684707
685708const isGooglePhotosService = ( service : string ) : boolean =>
686709 service === GOOGLE_PHOTOS_LIBRARY_SERVICE || service === GOOGLE_PHOTOS_PICKER_SERVICE ;
687710
711+ const discoveryScopesForService = (
712+ service : string ,
713+ document : DiscoveryDocument ,
714+ ) : Record < string , string > => {
715+ const scopes = discoveryScopes ( document ) ;
716+ const overrideScopes = GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ?. scopes ;
717+ if ( ! overrideScopes ) {
718+ return scopes ;
719+ }
720+ const missingScopes = Object . fromEntries (
721+ Object . entries ( overrideScopes ) . filter ( ( [ scope ] ) => scopes [ scope ] === undefined ) ,
722+ ) ;
723+ return Object . keys ( missingScopes ) . length === 0 ? scopes : { ...scopes , ...missingScopes } ;
724+ } ;
725+
726+ const discoveryMethodScopesForService = (
727+ service : string ,
728+ method : DiscoveryMethod ,
729+ ) : readonly string [ ] => {
730+ const scopes = method . scopes ?? [ ] ;
731+ return scopes . length === 0
732+ ? ( GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ?. fallbackMethodScopes ?? scopes )
733+ : scopes ;
734+ } ;
735+
688736/** The v2 oauth auth template for a Google-discovery integration. The spec
689737 * itself carries the matching `securitySchemes.googleOAuth2` entry; this is the
690738 * catalog-level template a connection's access token renders through. */
@@ -811,6 +859,7 @@ export const convertGoogleDiscoveryToOpenApi = Effect.fn("OpenApi.convertGoogleD
811859 method,
812860 toolPath,
813861 pathTemplate : pathTemplate . startsWith ( "/" ) ? pathTemplate : `/${ pathTemplate } ` ,
862+ oauthScopes : discoveryMethodScopesForService ( service , method ) ,
814863 } ) ;
815864 }
816865
@@ -826,7 +875,7 @@ export const convertGoogleDiscoveryToOpenApi = Effect.fn("OpenApi.convertGoogleD
826875 } ) ;
827876 }
828877
829- const scopes = compactDiscoveryScopeMap ( discoveryScopes ( document ) ) ;
878+ const scopes = compactDiscoveryScopeMap ( discoveryScopesForService ( service , document ) ) ;
830879 const authenticationTemplate = googleOauthTemplate ( scopes ) ;
831880
832881 const spec : OpenApiDocument = {
@@ -923,7 +972,7 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn(
923972 for ( const info of infos ) {
924973 const schemaPrefix = schemaComponentPart ( `${ info . service } _${ info . version } ` ) ;
925974 const schemaNameForRef = ( name : string ) => `${ schemaPrefix } _${ schemaComponentPart ( name ) } ` ;
926- const scopeDescriptions = discoveryScopes ( info . document ) ;
975+ const scopeDescriptions = discoveryScopesForService ( info . service , info . document ) ;
927976 const filterPhotosScopes = consentScopeSet !== null && isGooglePhotosService ( info . service ) ;
928977
929978 for ( const [ scope , description ] of Object . entries ( scopeDescriptions ) ) {
@@ -939,7 +988,7 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn(
939988 const methodId = Option . getOrUndefined ( method . id ) ;
940989 const rawPathTemplate = Option . getOrUndefined ( method . path ) ;
941990 if ( ! methodId || ! rawPathTemplate || ! method . httpMethod ) continue ;
942- const methodScopes = method . scopes ?? [ ] ;
991+ const methodScopes = discoveryMethodScopesForService ( info . service , method ) ;
943992 const oauthScopes = filterPhotosScopes
944993 ? methodScopes . filter ( ( scope ) => consentScopeSet . has ( scope ) )
945994 : methodScopes ;
0 commit comments