@@ -7,11 +7,17 @@ import {
77 setLastSeenAuthMethod ,
88 setLastSeenUserIdAfterAuth ,
99} from './session.js'
10- import { exchangeCustomPartnerToken , refreshAccessToken , InvalidGrantError } from './session/exchange.js'
10+ import {
11+ exchangeAccessForApplicationTokens ,
12+ exchangeCustomPartnerToken ,
13+ refreshAccessToken ,
14+ InvalidGrantError ,
15+ } from './session/exchange.js'
1116import { allDefaultScopes } from './session/scopes.js'
1217import { store as storeSessions , fetch as fetchSessions , remove as secureRemove } from './session/store.js'
13- import { IdentityToken , Sessions } from './session/schema.js'
18+ import { ApplicationToken , IdentityToken , Sessions } from './session/schema.js'
1419import { validateSession } from './session/validate.js'
20+ import { applicationId } from './session/identity.js'
1521import { pollForDeviceAuthorization , requestDeviceAuthorization } from './session/device-authorization.js'
1622import { getCurrentSessionId } from './conf-store.js'
1723import * as fqdnModule from '../../public/node/context/fqdn.js'
@@ -44,15 +50,54 @@ const validIdentityToken: IdentityToken = {
4450}
4551
4652const validTokens : OAuthSession = {
47- admin : { token : 'access_token ' , storeFqdn : 'mystore.myshopify.com' } ,
48- storefront : 'access_token ' ,
49- partners : 'access_token ' ,
53+ admin : { token : 'admin_token ' , storeFqdn : 'mystore.myshopify.com' } ,
54+ storefront : 'storefront_token ' ,
55+ partners : 'partners_token ' ,
5056 userId,
5157}
5258
59+ const appTokens : Record < string , ApplicationToken > = {
60+ // Admin APIs includes domain in the key
61+ 'mystore.myshopify.com-admin' : {
62+ accessToken : 'admin_token' ,
63+ expiresAt : futureDate ,
64+ scopes : [ 'scope' , 'scope2' ] ,
65+ } ,
66+ 'storefront-renderer' : {
67+ accessToken : 'storefront_token' ,
68+ expiresAt : futureDate ,
69+ scopes : [ 'scope1' ] ,
70+ } ,
71+ partners : {
72+ accessToken : 'partners_token' ,
73+ expiresAt : futureDate ,
74+ scopes : [ 'scope2' ] ,
75+ } ,
76+ 'business-platform' : {
77+ accessToken : 'business_platform_token' ,
78+ expiresAt : futureDate ,
79+ scopes : [ 'scope3' ] ,
80+ } ,
81+ }
82+
83+ const partnersToken : ApplicationToken = {
84+ accessToken : 'custom_partners_token' ,
85+ expiresAt : futureDate ,
86+ scopes : [ 'scope2' ] ,
87+ }
88+
5389const fqdn = 'fqdn.com'
5490
5591const validSessions : Sessions = {
92+ [ fqdn ] : {
93+ [ userId ] : {
94+ identity : validIdentityToken ,
95+ applications : appTokens ,
96+ } ,
97+ } ,
98+ }
99+
100+ const invalidSessions : Sessions = {
56101 [ fqdn ] : {
57102 [ userId ] : {
58103 identity : validIdentityToken ,
@@ -62,6 +107,7 @@ const validSessions: Sessions = {
62107}
63108
64109vi . mock ( '../../public/node/context/local.js' )
110+ vi . mock ( './session/identity' )
65111vi . mock ( './session/authorize' )
66112vi . mock ( './session/exchange' )
67113vi . mock ( './session/scopes' )
@@ -77,9 +123,11 @@ vi.mock('../../public/node/system.js')
77123
78124beforeEach ( ( ) => {
79125 vi . spyOn ( fqdnModule , 'identityFqdn' ) . mockResolvedValue ( fqdn )
126+ vi . mocked ( exchangeAccessForApplicationTokens ) . mockResolvedValue ( appTokens )
80127 vi . mocked ( refreshAccessToken ) . mockResolvedValue ( validIdentityToken )
128+ vi . mocked ( applicationId ) . mockImplementation ( ( app ) => app )
81129 vi . mocked ( exchangeCustomPartnerToken ) . mockResolvedValue ( {
82- accessToken : 'custom_partners_token' ,
130+ accessToken : partnersToken . accessToken ,
83131 userId : validIdentityToken . userId ,
84132 } )
85133 vi . mocked ( partnersRequest ) . mockResolvedValue ( undefined )
@@ -114,6 +162,7 @@ describe('ensureAuthenticated when previous session is invalid', () => {
114162 const got = await ensureAuthenticated ( defaultApplications )
115163
116164 // Then
165+ expect ( exchangeAccessForApplicationTokens ) . toBeCalled ( )
117166 expect ( refreshAccessToken ) . not . toBeCalled ( )
118167 expect ( businessPlatformRequest ) . toHaveBeenCalled ( )
119168 expect ( storeSessions ) . toHaveBeenCalledOnce ( )
@@ -153,16 +202,16 @@ The CLI is currently unable to prompt for reauthentication.`,
153202 test ( 'executes complete auth flow if session is for a different fqdn' , async ( ) => {
154203 // Given
155204 vi . mocked ( validateSession ) . mockResolvedValueOnce ( 'needs_full_auth' )
156- vi . mocked ( fetchSessions ) . mockResolvedValue ( validSessions )
205+ vi . mocked ( fetchSessions ) . mockResolvedValue ( invalidSessions )
157206 const expectedSessions = {
158- ...validSessions ,
207+ ...invalidSessions ,
159208 [ fqdn ] : {
160209 [ userId ] : {
161210 identity : {
162211 ...validIdentityToken ,
163212 alias : 'user@example.com' ,
164213 } ,
165- applications : { } ,
214+ applications : appTokens ,
166215 } ,
167216 } ,
168217 }
@@ -171,7 +220,7 @@ The CLI is currently unable to prompt for reauthentication.`,
171220 const got = await ensureAuthenticated ( defaultApplications )
172221
173222 // Then
174-
223+ expect ( exchangeAccessForApplicationTokens ) . toBeCalled ( )
175224 expect ( refreshAccessToken ) . not . toBeCalled ( )
176225 expect ( storeSessions ) . toBeCalledWith ( expectedSessions )
177226 expect ( got ) . toEqual ( validTokens )
@@ -190,7 +239,7 @@ The CLI is currently unable to prompt for reauthentication.`,
190239 const got = await ensureAuthenticated ( defaultApplications )
191240
192241 // Then
193-
242+ expect ( exchangeAccessForApplicationTokens ) . toBeCalled ( )
194243 expect ( businessPlatformRequest ) . toHaveBeenCalled ( )
195244 expect ( storeSessions ) . toHaveBeenCalledOnce ( )
196245
@@ -201,20 +250,26 @@ The CLI is currently unable to prompt for reauthentication.`,
201250 expect ( got ) . toEqual ( validTokens )
202251 } )
203252
204- test ( 'uses identity token to fetch email during full auth flow ' , async ( ) => {
253+ test ( 'falls back to userId when no business platform token available ' , async ( ) => {
205254 // Given
206255 vi . mocked ( validateSession ) . mockResolvedValueOnce ( 'needs_full_auth' )
207256 vi . mocked ( fetchSessions ) . mockResolvedValue ( undefined )
257+ const appTokensWithoutBusinessPlatform = {
258+ 'mystore.myshopify.com-admin' : appTokens [ 'mystore.myshopify.com-admin' ] ! ,
259+ 'storefront-renderer' : appTokens [ 'storefront-renderer' ] ! ,
260+ partners : appTokens . partners ! ,
261+ }
262+ vi . mocked ( exchangeAccessForApplicationTokens ) . mockResolvedValueOnce ( appTokensWithoutBusinessPlatform )
208263
209264 // When
210265 const got = await ensureAuthenticated ( defaultApplications )
211266
212267 // Then
213- expect ( businessPlatformRequest ) . toHaveBeenCalledWith ( expect . any ( String ) , 'access_token' )
268+ expect ( businessPlatformRequest ) . not . toHaveBeenCalled ( )
214269
215- // Verify the session was stored with email as alias
270+ // Verify the session was stored with userId as alias (fallback)
216271 const storedSession = vi . mocked ( storeSessions ) . mock . calls [ 0 ] ! [ 0 ]
217- expect ( storedSession [ fqdn ] ! [ userId ] ! . identity . alias ) . toBe ( 'user@example.com' )
272+ expect ( storedSession [ fqdn ] ! [ userId ] ! . identity . alias ) . toBe ( userId )
218273 } )
219274
220275 test ( 'executes complete auth flow if requesting additional scopes' , async ( ) => {
@@ -226,7 +281,7 @@ The CLI is currently unable to prompt for reauthentication.`,
226281 const got = await ensureAuthenticated ( defaultApplications )
227282
228283 // Then
229-
284+ expect ( exchangeAccessForApplicationTokens ) . toBeCalled ( )
230285 expect ( refreshAccessToken ) . not . toBeCalled ( )
231286 expect ( businessPlatformRequest ) . toHaveBeenCalled ( )
232287 expect ( storeSessions ) . toHaveBeenCalledOnce ( )
@@ -252,7 +307,7 @@ describe('when existing session is valid', () => {
252307 const got = await ensureAuthenticated ( defaultApplications )
253308
254309 // Then
255-
310+ expect ( exchangeAccessForApplicationTokens ) . not . toBeCalled ( )
256311 expect ( refreshAccessToken ) . not . toBeCalled ( )
257312 expect ( got ) . toEqual ( validTokens )
258313 await expect ( getLastSeenUserIdAfterAuth ( ) ) . resolves . toBe ( '1234-5678' )
@@ -265,18 +320,13 @@ describe('when existing session is valid', () => {
265320 vi . mocked ( validateSession ) . mockResolvedValueOnce ( 'ok' )
266321 vi . mocked ( fetchSessions ) . mockResolvedValue ( validSessions )
267322 vi . mocked ( getPartnersToken ) . mockReturnValue ( 'custom_cli_token' )
268- const expected = {
269- admin : { token : 'access_token' , storeFqdn : 'mystore.myshopify.com' } ,
270- storefront : 'access_token' ,
271- partners : 'custom_partners_token' ,
272- userId,
273- }
323+ const expected = { ...validTokens , partners : 'custom_partners_token' }
274324
275325 // When
276326 const got = await ensureAuthenticated ( defaultApplications )
277327
278328 // Then
279-
329+ expect ( exchangeAccessForApplicationTokens ) . not . toBeCalled ( )
280330 expect ( refreshAccessToken ) . not . toBeCalled ( )
281331 expect ( got ) . toEqual ( expected )
282332 await expect ( getLastSeenUserIdAfterAuth ( ) ) . resolves . toBe ( '1234-5678' )
@@ -294,16 +344,8 @@ describe('when existing session is valid', () => {
294344
295345 // Then
296346 expect ( refreshAccessToken ) . toBeCalled ( )
297-
298- const expectedSessions = {
299- [ fqdn ] : {
300- [ userId ] : {
301- identity : validIdentityToken ,
302- applications : { } ,
303- } ,
304- } ,
305- }
306- expect ( storeSessions ) . toBeCalledWith ( expectedSessions )
347+ expect ( exchangeAccessForApplicationTokens ) . toBeCalled ( )
348+ expect ( storeSessions ) . toBeCalledWith ( validSessions )
307349 expect ( got ) . toEqual ( validTokens )
308350 await expect ( getLastSeenUserIdAfterAuth ( ) ) . resolves . toBe ( '1234-5678' )
309351 await expect ( getLastSeenAuthMethod ( ) ) . resolves . toEqual ( 'device_auth' )
@@ -322,16 +364,8 @@ describe('when existing session is expired', () => {
322364
323365 // Then
324366 expect ( refreshAccessToken ) . toBeCalled ( )
325-
326- const expectedSessions = {
327- [ fqdn ] : {
328- [ userId ] : {
329- identity : validIdentityToken ,
330- applications : { } ,
331- } ,
332- } ,
333- }
334- expect ( storeSessions ) . toBeCalledWith ( expectedSessions )
367+ expect ( exchangeAccessForApplicationTokens ) . toBeCalled ( )
368+ expect ( storeSessions ) . toBeCalledWith ( validSessions )
335369 expect ( got ) . toEqual ( validTokens )
336370 await expect ( getLastSeenUserIdAfterAuth ( ) ) . resolves . toBe ( '1234-5678' )
337371 await expect ( getLastSeenAuthMethod ( ) ) . resolves . toEqual ( 'device_auth' )
@@ -351,7 +385,7 @@ describe('when existing session is expired', () => {
351385
352386 // Then
353387 expect ( refreshAccessToken ) . toBeCalled ( )
354-
388+ expect ( exchangeAccessForApplicationTokens ) . toBeCalled ( )
355389 expect ( businessPlatformRequest ) . toHaveBeenCalled ( )
356390 expect ( storeSessions ) . toHaveBeenCalledOnce ( )
357391
@@ -610,15 +644,7 @@ describe('ensureAuthenticated email fetch functionality', () => {
610644 const got = await ensureAuthenticated ( defaultApplications )
611645
612646 // Then
613- const expectedSessions = {
614- [ fqdn ] : {
615- [ userId ] : {
616- identity : validIdentityToken ,
617- applications : { } ,
618- } ,
619- } ,
620- }
621- expect ( storeSessions ) . toBeCalledWith ( expectedSessions )
647+ expect ( storeSessions ) . toBeCalledWith ( validSessions )
622648 expect ( got ) . toEqual ( validTokens )
623649 } )
624650
@@ -633,15 +659,7 @@ describe('ensureAuthenticated email fetch functionality', () => {
633659 // Then
634660 // The email fetch is not called during refresh - the session keeps its existing alias
635661 expect ( businessPlatformRequest ) . not . toHaveBeenCalled ( )
636- const expectedSessions = {
637- [ fqdn ] : {
638- [ userId ] : {
639- identity : validIdentityToken ,
640- applications : { } ,
641- } ,
642- } ,
643- }
644- expect ( storeSessions ) . toBeCalledWith ( expectedSessions )
662+ expect ( storeSessions ) . toBeCalledWith ( validSessions )
645663 expect ( got ) . toEqual ( validTokens )
646664 } )
647665
0 commit comments