@@ -99,6 +99,14 @@ const tokenResponse =
9999 ( ) =>
100100 json ( 200 , body ) ;
101101
102+ const tokenResponseFetch =
103+ ( body : unknown ) : typeof globalThis . fetch =>
104+ async ( ) =>
105+ new Response ( JSON . stringify ( body ) , {
106+ status : 200 ,
107+ headers : { "content-type" : "application/json" } ,
108+ } ) ;
109+
102110// ---------------------------------------------------------------------------
103111// PKCE
104112// ---------------------------------------------------------------------------
@@ -575,6 +583,46 @@ describe("exchangeAuthorizationCode", () => {
575583 ) ,
576584 ) ;
577585
586+ it . effect ( "normalizes Slack's comma-delimited top-level scopes" , ( ) =>
587+ Effect . gen ( function * ( ) {
588+ const result = yield * exchangeAuthorizationCode ( {
589+ tokenUrl : "https://slack.com/api/oauth.v2.user.access" ,
590+ clientId : "cid" ,
591+ clientSecret : "csecret" ,
592+ redirectUrl : "https://app.example.com/cb" ,
593+ codeVerifier : "verifier" ,
594+ code : "abc" ,
595+ fetch : tokenResponseFetch ( {
596+ access_token : "xoxp-user-token" ,
597+ token_type : "Bearer" ,
598+ scope : "channels:read,chat:write,reactions:read" ,
599+ } ) ,
600+ } ) ;
601+
602+ expect ( result . scope ) . toBe ( "channels:read chat:write reactions:read" ) ;
603+ } ) ,
604+ ) ;
605+
606+ it . effect ( "preserves commas in scope tokens from non-Slack providers" , ( ) =>
607+ Effect . gen ( function * ( ) {
608+ const result = yield * exchangeAuthorizationCode ( {
609+ tokenUrl : "https://oauth.example.com/token" ,
610+ clientId : "cid" ,
611+ clientSecret : "csecret" ,
612+ redirectUrl : "https://app.example.com/cb" ,
613+ codeVerifier : "verifier" ,
614+ code : "abc" ,
615+ fetch : tokenResponseFetch ( {
616+ access_token : "provider-token" ,
617+ token_type : "Bearer" ,
618+ scope : "scope,with-comma other.scope" ,
619+ } ) ,
620+ } ) ;
621+
622+ expect ( result . scope ) . toBe ( "scope,with-comma other.scope" ) ;
623+ } ) ,
624+ ) ;
625+
578626 it . effect ( "keeps a standard top-level scope ahead of nested provider metadata" , ( ) =>
579627 withTokenEndpoint (
580628 tokenResponse ( {
@@ -856,6 +904,24 @@ describe("exchangeClientCredentials", () => {
856904} ) ;
857905
858906describe ( "refreshAccessToken" , ( ) => {
907+ it . effect ( "normalizes Slack's comma-delimited scopes on refresh" , ( ) =>
908+ Effect . gen ( function * ( ) {
909+ const result = yield * refreshAccessToken ( {
910+ tokenUrl : "https://slack.com/api/oauth.v2.user.access" ,
911+ clientId : "cid" ,
912+ clientSecret : "csecret" ,
913+ refreshToken : "refresh-token" ,
914+ fetch : tokenResponseFetch ( {
915+ access_token : "xoxp-refreshed-token" ,
916+ token_type : "Bearer" ,
917+ scope : "channels:read,chat:write,reactions:read" ,
918+ } ) ,
919+ } ) ;
920+
921+ expect ( result . scope ) . toBe ( "channels:read chat:write reactions:read" ) ;
922+ } ) ,
923+ ) ;
924+
859925 it . effect ( "posts grant_type=refresh_token with the refresh token" , ( ) =>
860926 withTokenEndpoint ( tokenResponse ( validRefreshBody ) , ( { tokenUrl, calls } ) =>
861927 Effect . gen ( function * ( ) {
0 commit comments