@@ -120,6 +120,39 @@ export const auth = betterAuth({
120120 } )
121121
122122 if ( existing ) {
123+ let scopeToStore = account . scope
124+
125+ // For Salesforce, fetch instance URL and add it to the scope
126+ if ( account . providerId === 'salesforce' && account . accessToken ) {
127+ try {
128+ const response = await fetch (
129+ 'https://login.salesforce.com/services/oauth2/userinfo' ,
130+ {
131+ headers : {
132+ Authorization : `Bearer ${ account . accessToken } ` ,
133+ } ,
134+ }
135+ )
136+
137+ if ( response . ok ) {
138+ const data = await response . json ( )
139+
140+ // Extract instance URL from profile field (format: https://na1.salesforce.com/id/...)
141+ if ( data . profile ) {
142+ const match = data . profile . match ( / ^ ( h t t p s : \/ \/ [ ^ / ] + ) / )
143+ if ( match && match [ 1 ] !== 'https://login.salesforce.com' ) {
144+ const instanceUrl = match [ 1 ]
145+ const existingScope =
146+ account . scope || 'api refresh_token openid offline_access'
147+ scopeToStore = `__sf_instance__:${ instanceUrl } ${ existingScope } `
148+ }
149+ }
150+ }
151+ } catch ( error ) {
152+ logger . error ( 'Failed to fetch Salesforce instance URL' , { error } )
153+ }
154+ }
155+
123156 await db
124157 . update ( schema . account )
125158 . set ( {
@@ -129,7 +162,7 @@ export const auth = betterAuth({
129162 idToken : account . idToken ,
130163 accessTokenExpiresAt : account . accessTokenExpiresAt ,
131164 refreshTokenExpiresAt : account . refreshTokenExpiresAt ,
132- scope : account . scope ,
165+ scope : scopeToStore ,
133166 updatedAt : new Date ( ) ,
134167 } )
135168 . where ( eq ( schema . account . id , existing . id ) )
@@ -140,24 +173,52 @@ export const auth = betterAuth({
140173 return { data : account }
141174 } ,
142175 after : async ( account ) => {
143- // Salesforce doesn't return expires_in in its token response (unlike other OAuth providers).
144- // We set a default 2-hour expiration so token refresh logic works correctly.
145- if ( account . providerId === 'salesforce' && ! account . accessTokenExpiresAt ) {
146- const twoHoursFromNow = new Date ( Date . now ( ) + 2 * 60 * 60 * 1000 )
147- try {
148- await db
149- . update ( schema . account )
150- . set ( { accessTokenExpiresAt : twoHoursFromNow } )
151- . where ( eq ( schema . account . id , account . id ) )
152- logger . info (
153- '[databaseHooks.account.create.after] Set default expiration for Salesforce token' ,
154- { accountId : account . id , expiresAt : twoHoursFromNow }
155- )
156- } catch ( error ) {
157- logger . error (
158- '[databaseHooks.account.create.after] Failed to set Salesforce token expiration' ,
159- { accountId : account . id , error }
160- )
176+ // Salesforce-specific handling: set default token expiration and fetch instance URL
177+ if ( account . providerId === 'salesforce' ) {
178+ const updates : {
179+ accessTokenExpiresAt ?: Date
180+ scope ?: string
181+ } = { }
182+
183+ // Salesforce doesn't return expires_in, set default 2-hour expiration
184+ if ( ! account . accessTokenExpiresAt ) {
185+ updates . accessTokenExpiresAt = new Date ( Date . now ( ) + 2 * 60 * 60 * 1000 )
186+ }
187+
188+ // Fetch instance URL from Salesforce userinfo endpoint and store it in scope
189+ if ( account . accessToken ) {
190+ try {
191+ const response = await fetch (
192+ 'https://login.salesforce.com/services/oauth2/userinfo' ,
193+ {
194+ headers : {
195+ Authorization : `Bearer ${ account . accessToken } ` ,
196+ } ,
197+ }
198+ )
199+
200+ if ( response . ok ) {
201+ const data = await response . json ( )
202+
203+ // Extract instance URL from profile field (format: https://na1.salesforce.com/id/...)
204+ if ( data . profile ) {
205+ const match = data . profile . match ( / ^ ( h t t p s : \/ \/ [ ^ / ] + ) / )
206+ if ( match && match [ 1 ] !== 'https://login.salesforce.com' ) {
207+ const instanceUrl = match [ 1 ]
208+ const existingScope =
209+ account . scope || 'api refresh_token openid offline_access'
210+ updates . scope = `__sf_instance__:${ instanceUrl } ${ existingScope } `
211+ }
212+ }
213+ }
214+ } catch ( error ) {
215+ logger . error ( 'Failed to fetch Salesforce instance URL' , { error } )
216+ }
217+ }
218+
219+ // Apply updates if any
220+ if ( Object . keys ( updates ) . length > 0 ) {
221+ await db . update ( schema . account ) . set ( updates ) . where ( eq ( schema . account . id , account . id ) )
161222 }
162223 }
163224 } ,
@@ -913,8 +974,6 @@ export const auth = betterAuth({
913974 redirectURI : `${ getBaseUrl ( ) } /api/auth/oauth2/callback/salesforce` ,
914975 getUserInfo : async ( tokens ) => {
915976 try {
916- logger . info ( 'Fetching Salesforce user profile' )
917-
918977 const response = await fetch (
919978 'https://login.salesforce.com/services/oauth2/userinfo' ,
920979 {
0 commit comments