@@ -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,35 @@ 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+
53+ const googleDiscoveryUrlForService = (
54+ service : string ,
55+ version : string ,
56+ host = `${ service } .googleapis.com` ,
57+ ) : string => {
58+ const override = GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ;
59+ return override ?. preserveServiceHostedUrl === true
60+ ? `https://${ host } /$discovery/rest?version=${ version } `
61+ : `${ DISCOVERY_SERVICE_HOST } /${ service } /${ version } /rest` ;
62+ } ;
63+
3164type JsonPrimitive = string | number | boolean | null ;
3265type JsonValue = JsonPrimitive | readonly JsonValue [ ] | { readonly [ key : string ] : JsonValue } ;
3366
@@ -266,7 +299,7 @@ export const normalizeGoogleDiscoveryUrl = (discoveryUrl: string): string | null
266299 const match = parsed . pathname . match ( DISCOVERY_SERVICE_PATH_RE ) ;
267300 const service = match ?. [ 1 ] ;
268301 const version = match ?. [ 2 ] ;
269- return service && version ? ` ${ DISCOVERY_SERVICE_HOST } / ${ service } / ${ version } /rest` : null ;
302+ return service && version ? googleDiscoveryUrlForService ( service , version ) : null ;
270303 }
271304
272305 const service = serviceFromGoogleApisHost ( host ) ;
@@ -283,7 +316,7 @@ export const normalizeGoogleDiscoveryUrl = (discoveryUrl: string): string | null
283316 ) {
284317 return null ;
285318 }
286- return ` ${ DISCOVERY_SERVICE_HOST } / ${ service } / ${ version } /rest` ;
319+ return googleDiscoveryUrlForService ( service , version , host ) ;
287320} ;
288321
289322const normalizeDiscoveryUrl = ( discoveryUrl : string ) : string => {
@@ -677,14 +710,38 @@ const buildDiscoveryOperation = (input: {
677710
678711const GOOGLE_OAUTH_SECURITY_SCHEME = "googleOAuth2" ;
679712const GOOGLE_PHOTOS_LIBRARY_SERVICE = "photoslibrary" ;
680- const GOOGLE_PHOTOS_PICKER_SERVICE = "photospicker" ;
681713const GOOGLE_PHOTOS_APPENDONLY_SCOPE = "https://www.googleapis.com/auth/photoslibrary.appendonly" ;
682714const GOOGLE_PHOTOS_UPLOAD_TOOL_PATH = "photoslibrary.mediaItems.upload" ;
683715const GOOGLE_PHOTOS_UPLOAD_PATH = "/uploads" ;
684716
685717const isGooglePhotosService = ( service : string ) : boolean =>
686718 service === GOOGLE_PHOTOS_LIBRARY_SERVICE || service === GOOGLE_PHOTOS_PICKER_SERVICE ;
687719
720+ const discoveryScopesForService = (
721+ service : string ,
722+ document : DiscoveryDocument ,
723+ ) : Record < string , string > => {
724+ const scopes = discoveryScopes ( document ) ;
725+ const overrideScopes = GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ?. scopes ;
726+ if ( ! overrideScopes ) {
727+ return scopes ;
728+ }
729+ const missingScopes = Object . fromEntries (
730+ Object . entries ( overrideScopes ) . filter ( ( [ scope ] ) => scopes [ scope ] === undefined ) ,
731+ ) ;
732+ return Object . keys ( missingScopes ) . length === 0 ? scopes : { ...scopes , ...missingScopes } ;
733+ } ;
734+
735+ const discoveryMethodScopesForService = (
736+ service : string ,
737+ method : DiscoveryMethod ,
738+ ) : readonly string [ ] => {
739+ const scopes = method . scopes ?? [ ] ;
740+ return scopes . length === 0
741+ ? ( GOOGLE_DISCOVERY_SERVICE_OVERRIDES [ service ] ?. fallbackMethodScopes ?? scopes )
742+ : scopes ;
743+ } ;
744+
688745/** The v2 oauth auth template for a Google-discovery integration. The spec
689746 * itself carries the matching `securitySchemes.googleOAuth2` entry; this is the
690747 * catalog-level template a connection's access token renders through. */
@@ -811,6 +868,7 @@ export const convertGoogleDiscoveryToOpenApi = Effect.fn("OpenApi.convertGoogleD
811868 method,
812869 toolPath,
813870 pathTemplate : pathTemplate . startsWith ( "/" ) ? pathTemplate : `/${ pathTemplate } ` ,
871+ oauthScopes : discoveryMethodScopesForService ( service , method ) ,
814872 } ) ;
815873 }
816874
@@ -826,7 +884,7 @@ export const convertGoogleDiscoveryToOpenApi = Effect.fn("OpenApi.convertGoogleD
826884 } ) ;
827885 }
828886
829- const scopes = compactDiscoveryScopeMap ( discoveryScopes ( document ) ) ;
887+ const scopes = compactDiscoveryScopeMap ( discoveryScopesForService ( service , document ) ) ;
830888 const authenticationTemplate = googleOauthTemplate ( scopes ) ;
831889
832890 const spec : OpenApiDocument = {
@@ -923,7 +981,7 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn(
923981 for ( const info of infos ) {
924982 const schemaPrefix = schemaComponentPart ( `${ info . service } _${ info . version } ` ) ;
925983 const schemaNameForRef = ( name : string ) => `${ schemaPrefix } _${ schemaComponentPart ( name ) } ` ;
926- const scopeDescriptions = discoveryScopes ( info . document ) ;
984+ const scopeDescriptions = discoveryScopesForService ( info . service , info . document ) ;
927985 const filterPhotosScopes = consentScopeSet !== null && isGooglePhotosService ( info . service ) ;
928986
929987 for ( const [ scope , description ] of Object . entries ( scopeDescriptions ) ) {
@@ -939,7 +997,7 @@ export const convertGoogleDiscoveryBundleToOpenApi = Effect.fn(
939997 const methodId = Option . getOrUndefined ( method . id ) ;
940998 const rawPathTemplate = Option . getOrUndefined ( method . path ) ;
941999 if ( ! methodId || ! rawPathTemplate || ! method . httpMethod ) continue ;
942- const methodScopes = method . scopes ?? [ ] ;
1000+ const methodScopes = discoveryMethodScopesForService ( info . service , method ) ;
9431001 const oauthScopes = filterPhotosScopes
9441002 ? methodScopes . filter ( ( scope ) => consentScopeSet . has ( scope ) )
9451003 : methodScopes ;
0 commit comments