@@ -33,6 +33,13 @@ function mockDiscovery(fetchMock: ReturnType<typeof vi.fn>) {
3333 )
3434}
3535
36+ function jwt ( payload : Record < string , unknown > ) : string {
37+ const encoded = Buffer . from ( JSON . stringify ( payload ) )
38+ . toString ( "base64url" )
39+ . replace ( / = + $ / , "" )
40+ return `header.${ encoded } .signature`
41+ }
42+
3643let fetchMock : ReturnType < typeof vi . fn >
3744
3845beforeEach ( ( ) => {
@@ -184,6 +191,107 @@ describe("OpenSeaOAuth", () => {
184191 ) . rejects . toThrow ( / T o k e n r e q u e s t f a i l e d / )
185192 } )
186193
194+ test ( "reads OpenSea scopes from the access token when scope is omitted" , async ( ) => {
195+ mockDiscovery ( fetchMock )
196+ fetchMock . mockImplementationOnce ( ( ) =>
197+ Promise . resolve (
198+ jsonResponse ( {
199+ access_token : jwt ( {
200+ opensea_scopes :
201+ "write:orders read:eligibility read:rewards unknown:scope" ,
202+ } ) ,
203+ refresh_token : "rt" ,
204+ token_type : "Bearer" ,
205+ expires_in : 3600 ,
206+ } ) ,
207+ ) ,
208+ )
209+
210+ const oauth = new OpenSeaOAuth ( { clientId : CLIENT_ID , issuer : ISSUER } )
211+ const token = await oauth . exchangeCode ( {
212+ code : "the-code" ,
213+ codeVerifier : "the-verifier" ,
214+ redirectUri : "http://127.0.0.1:8151/callback" ,
215+ } )
216+
217+ expect ( token . scopes ) . toEqual ( [ "read:eligibility" , "write:orders" ] )
218+ } )
219+
220+ test ( "reads array OpenSea scope claims in canonical API order" , async ( ) => {
221+ mockDiscovery ( fetchMock )
222+ fetchMock . mockImplementationOnce ( ( ) =>
223+ Promise . resolve (
224+ jsonResponse ( {
225+ access_token : jwt ( {
226+ opensea_scopes : [ "write:favorites" , "read:favorites" ] ,
227+ } ) ,
228+ token_type : "Bearer" ,
229+ expires_in : 3600 ,
230+ } ) ,
231+ ) ,
232+ )
233+
234+ const oauth = new OpenSeaOAuth ( { clientId : CLIENT_ID , issuer : ISSUER } )
235+ const token = await oauth . exchangeCode ( {
236+ code : "the-code" ,
237+ codeVerifier : "the-verifier" ,
238+ redirectUri : "http://127.0.0.1:8151/callback" ,
239+ } )
240+
241+ expect ( token . scopes ) . toEqual ( [ "read:favorites" , "write:favorites" ] )
242+ } )
243+
244+ test ( "filters token response scopes through the OpenAPI catalog" , async ( ) => {
245+ mockDiscovery ( fetchMock )
246+ fetchMock . mockImplementationOnce ( ( ) =>
247+ Promise . resolve (
248+ jsonResponse ( {
249+ access_token : "opaque-token" ,
250+ token_type : "Bearer" ,
251+ expires_in : 3600 ,
252+ scope : "read:eligibility read:rewards unknown:scope" ,
253+ } ) ,
254+ ) ,
255+ )
256+
257+ const oauth = new OpenSeaOAuth ( { clientId : CLIENT_ID , issuer : ISSUER } )
258+ const token = await oauth . exchangeCode ( {
259+ code : "the-code" ,
260+ codeVerifier : "the-verifier" ,
261+ redirectUri : "http://127.0.0.1:8151/callback" ,
262+ } )
263+
264+ expect ( token . scopes ) . toEqual ( [ "read:eligibility" ] )
265+ } )
266+
267+ test . each ( [
268+ [ "a JWT without the claim" , jwt ( { wallet : "0xabc" } ) ] ,
269+ [
270+ "a JWT with only unknown claims" ,
271+ jwt ( { opensea_scopes : "unknown:scope" } ) ,
272+ ] ,
273+ ] ) ( "returns no scopes for %s" , async ( _label , accessToken ) => {
274+ mockDiscovery ( fetchMock )
275+ fetchMock . mockImplementationOnce ( ( ) =>
276+ Promise . resolve (
277+ jsonResponse ( {
278+ access_token : accessToken ,
279+ token_type : "Bearer" ,
280+ expires_in : 3600 ,
281+ } ) ,
282+ ) ,
283+ )
284+
285+ const oauth = new OpenSeaOAuth ( { clientId : CLIENT_ID , issuer : ISSUER } )
286+ const token = await oauth . exchangeCode ( {
287+ code : "the-code" ,
288+ codeVerifier : "the-verifier" ,
289+ redirectUri : "http://127.0.0.1:8151/callback" ,
290+ } )
291+
292+ expect ( token . scopes ) . toEqual ( [ ] )
293+ } )
294+
187295 test ( "refresh uses the refresh_token grant" , async ( ) => {
188296 mockDiscovery ( fetchMock )
189297 fetchMock . mockImplementationOnce ( ( _url : string , init : RequestInit ) => {
0 commit comments