@@ -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,24 @@ 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+ const GOOGLE_DISCOVERY_SERVICE_OVERRIDES : Record < string , GoogleDiscoveryServiceOverride > = {
42+ forms : { preserveServiceHostedUrl : true } ,
43+ keep : { preserveServiceHostedUrl : true } ,
44+ [ GOOGLE_PHOTOS_PICKER_SERVICE ] : {
45+ preserveServiceHostedUrl : true ,
46+ scopes : {
47+ [ GOOGLE_PHOTOS_PICKER_SCOPE ] : GOOGLE_PHOTOS_PICKER_SCOPE_DESCRIPTION ,
48+ } ,
49+ fallbackMethodScopes : [ GOOGLE_PHOTOS_PICKER_SCOPE ] ,
50+ } ,
51+ } ;
52+
3153type JsonPrimitive = string | number | boolean | null ;
3254type JsonValue = JsonPrimitive | readonly JsonValue [ ] | { readonly [ key : string ] : JsonValue } ;
3355
@@ -283,7 +305,10 @@ export const normalizeGoogleDiscoveryUrl = (discoveryUrl: string): string | null
283305 ) {
284306 return null ;
285307 }
286- return `${ DISCOVERY_SERVICE_HOST } /${ service } /${ version } /rest` ;
308+ const override = GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ;
309+ return override ?. preserveServiceHostedUrl === true
310+ ? `https://${ host } /$discovery/rest?version=${ version } `
311+ : `${ DISCOVERY_SERVICE_HOST } /${ service } /${ version } /rest` ;
287312} ;
288313
289314const normalizeDiscoveryUrl = ( discoveryUrl : string ) : string => {
@@ -677,14 +702,38 @@ const buildDiscoveryOperation = (input: {
677702
678703const GOOGLE_OAUTH_SECURITY_SCHEME = "googleOAuth2" ;
679704const GOOGLE_PHOTOS_LIBRARY_SERVICE = "photoslibrary" ;
680- const GOOGLE_PHOTOS_PICKER_SERVICE = "photospicker" ;
681705const GOOGLE_PHOTOS_APPENDONLY_SCOPE = "https://www.googleapis.com/auth/photoslibrary.appendonly" ;
682706const GOOGLE_PHOTOS_UPLOAD_TOOL_PATH = "photoslibrary.mediaItems.upload" ;
683707const GOOGLE_PHOTOS_UPLOAD_PATH = "/uploads" ;
684708
685709const isGooglePhotosService = ( service : string ) : boolean =>
686710 service === GOOGLE_PHOTOS_LIBRARY_SERVICE || service === GOOGLE_PHOTOS_PICKER_SERVICE ;
687711
712+ const discoveryScopesForService = (
713+ service : string ,
714+ document : DiscoveryDocument ,
715+ ) : Record < string , string > => {
716+ const scopes = discoveryScopes ( document ) ;
717+ const overrideScopes = GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ?. scopes ;
718+ if ( ! overrideScopes ) {
719+ return scopes ;
720+ }
721+ const missingScopes = Object . fromEntries (
722+ Object . entries ( overrideScopes ) . filter ( ( [ scope ] ) => scopes [ scope ] === undefined ) ,
723+ ) ;
724+ return Object . keys ( missingScopes ) . length === 0 ? scopes : { ...scopes , ...missingScopes } ;
725+ } ;
726+
727+ const discoveryMethodScopesForService = (
728+ service : string ,
729+ method : DiscoveryMethod ,
730+ ) : readonly string [ ] => {
731+ const scopes = method . scopes ?? [ ] ;
732+ return scopes . length === 0
733+ ? ( GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ?. fallbackMethodScopes ?? scopes )
734+ : scopes ;
735+ } ;
736+
688737/** The v2 oauth auth template for a Google-discovery integration. The spec
689738 * itself carries the matching `securitySchemes.googleOAuth2` entry; this is the
690739 * catalog-level template a connection's access token renders through. */
@@ -811,6 +860,7 @@ export const convertGoogleDiscoveryToOpenApi = Effect.fn("OpenApi.convertGoogleD
811860 method,
812861 toolPath,
813862 pathTemplate : pathTemplate . startsWith ( "/" ) ? pathTemplate : `/${ pathTemplate } ` ,
863+ oauthScopes : discoveryMethodScopesForService ( service , method ) ,
814864 } ) ;
815865 }
816866
@@ -826,7 +876,7 @@ export const convertGoogleDiscoveryToOpenApi = Effect.fn("OpenApi.convertGoogleD
826876 } ) ;
827877 }
828878
829- const scopes = compactDiscoveryScopeMap ( discoveryScopes ( document ) ) ;
879+ const scopes = compactDiscoveryScopeMap ( discoveryScopesForService ( service , document ) ) ;
830880 const authenticationTemplate = googleOauthTemplate ( scopes ) ;
831881
832882 const spec : OpenApiDocument = {
@@ -923,7 +973,7 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn(
923973 for ( const info of infos ) {
924974 const schemaPrefix = schemaComponentPart ( `${ info . service } _${ info . version } ` ) ;
925975 const schemaNameForRef = ( name : string ) => `${ schemaPrefix } _${ schemaComponentPart ( name ) } ` ;
926- const scopeDescriptions = discoveryScopes ( info . document ) ;
976+ const scopeDescriptions = discoveryScopesForService ( info . service , info . document ) ;
927977 const filterPhotosScopes = consentScopeSet !== null && isGooglePhotosService ( info . service ) ;
928978
929979 for ( const [ scope , description ] of Object . entries ( scopeDescriptions ) ) {
@@ -939,7 +989,7 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn(
939989 const methodId = Option . getOrUndefined ( method . id ) ;
940990 const rawPathTemplate = Option . getOrUndefined ( method . path ) ;
941991 if ( ! methodId || ! rawPathTemplate || ! method . httpMethod ) continue ;
942- const methodScopes = method . scopes ?? [ ] ;
992+ const methodScopes = discoveryMethodScopesForService ( info . service , method ) ;
943993 const oauthScopes = filterPhotosScopes
944994 ? methodScopes . filter ( ( scope ) => consentScopeSet . has ( scope ) )
945995 : methodScopes ;
0 commit comments