1- import { BaseCommand , openWorkspace } from '@yarnpkg/cli' ;
2- import { Configuration , MessageName , Report , miscUtils , formatUtils } from '@yarnpkg/core' ;
3- import { StreamReport } from '@yarnpkg/core' ;
4- import { PortablePath } from '@yarnpkg/fslib' ;
5- import { npmConfigUtils , npmHttpUtils } from '@yarnpkg/plugin-npm' ;
6- import { Command , Option , Usage } from 'clipanion' ;
7- import { prompt } from 'enquirer' ;
1+ import { BaseCommand , openWorkspace } from '@yarnpkg/cli' ;
2+ import { Configuration , MessageName , Report , miscUtils , formatUtils , nodeUtils , httpUtils } from '@yarnpkg/core' ;
3+ import { StreamReport } from '@yarnpkg/core' ;
4+ import { PortablePath } from '@yarnpkg/fslib' ;
5+ import { npmConfigUtils , npmHttpUtils } from '@yarnpkg/plugin-npm' ;
6+ import { Command , Option , Usage } from 'clipanion' ;
7+ import { prompt } from 'enquirer' ;
88
99// eslint-disable-next-line arca/no-default-export
1010export default class NpmLoginCommand extends BaseCommand {
@@ -46,6 +46,10 @@ export default class NpmLoginCommand extends BaseCommand {
4646 description : `Set the npmAlwaysAuth configuration` ,
4747 } ) ;
4848
49+ webLogin = Option . Boolean ( `--web-login` , {
50+ description : `Enable web login` ,
51+ } ) ;
52+
4953 async execute ( ) {
5054 const configuration = await Configuration . find ( this . context . cwd , this . context . plugins ) ;
5155
@@ -61,16 +65,15 @@ export default class NpmLoginCommand extends BaseCommand {
6165 stdout : this . context . stdout ,
6266 includeFooter : false ,
6367 } , async report => {
64- const credentials = await getCredentials ( {
65- configuration,
68+ const token = await performAuthentication ( {
6669 registry,
70+ configuration,
6771 report,
72+ webLogin : this . webLogin ,
6873 stdin : this . context . stdin as NodeJS . ReadStream ,
6974 stdout : this . context . stdout as NodeJS . WriteStream ,
7075 } ) ;
7176
72- const token = await registerOrLogin ( registry , credentials , configuration ) ;
73-
7477 await setAuthToken ( registry , token , { alwaysAuth : this . alwaysAuth , scope : this . scope } ) ;
7578 return report . reportInfo ( MessageName . UNNAMED , `Successfully logged in` ) ;
7679 } ) ;
@@ -92,10 +95,119 @@ export async function getRegistry({scope, publish, configuration, cwd}: {scope?:
9295 return npmConfigUtils . getDefaultRegistry ( { configuration} ) ;
9396}
9497
98+ type NpmWebLoginInitResponse = {
99+ loginUrl : string ;
100+ doneUrl : string ;
101+ } ;
102+
103+ async function webLoginInit ( registry : string , configuration : Configuration ) : Promise < NpmWebLoginInitResponse | null > {
104+ let response : any ;
105+ try {
106+ response = await npmHttpUtils . post ( `/-/v1/login` , null , {
107+ configuration,
108+ registry,
109+ authType : npmHttpUtils . AuthType . NO_AUTH ,
110+ jsonResponse : true ,
111+ headers : {
112+ [ `npm-auth-type` ] : `web` ,
113+ } ,
114+ } ) ;
115+ } catch {
116+ return null ;
117+ }
118+
119+ return response ;
120+ }
121+
122+ type NpmWebLoginCheckResponse =
123+ | { type : `success`, token : string }
124+ | { type : `waiting`, sleep : number } ;
125+
126+ async function webLoginCheck ( doneUrl : string , configuration : Configuration ) : Promise < NpmWebLoginCheckResponse | null > {
127+ const response = await httpUtils . request ( doneUrl , null , {
128+ configuration,
129+ jsonResponse : true ,
130+ } ) ;
131+
132+ if ( response . statusCode === 202 ) {
133+ const retryAfter = response . headers [ `retry-after` ] ?? `1` ;
134+ return { type : `waiting` , sleep : parseInt ( retryAfter , 10 ) } ;
135+ }
136+
137+ if ( response . statusCode === 200 )
138+ return { type : `success` , token : response . body . token } ;
139+
140+ return null ;
141+ }
142+
143+ async function loginViaWeb ( { registry, configuration, report} : CredentialOptions ) : Promise < string | null > {
144+ const loginResponse = await webLoginInit ( registry , configuration ) ;
145+ if ( ! loginResponse )
146+ return null ;
147+
148+ if ( nodeUtils . openUrl ) {
149+ report . reportInfo ( MessageName . UNNAMED , `Starting the web login process...` ) ;
150+ report . reportSeparator ( ) ;
151+
152+ const { openNow} = await prompt < { openNow : boolean } > ( {
153+ type : `confirm` ,
154+ name : `openNow` ,
155+ message : `Do you want to try to open your browser now?` ,
156+ required : true ,
157+ initial : true ,
158+ onCancel : ( ) => process . exit ( 130 ) ,
159+ } ) ;
160+
161+ report . reportSeparator ( ) ;
162+
163+ if ( ! openNow || ! await nodeUtils . openUrl ( loginResponse . loginUrl ) ) {
164+ report . reportWarning ( MessageName . UNNAMED , `We failed to automatically open the url; you'll have to open it yourself in your browser of choice:` ) ;
165+ report . reportWarning ( MessageName . UNNAMED , formatUtils . pretty ( configuration , loginResponse . loginUrl , formatUtils . Type . URL ) ) ;
166+ report . reportSeparator ( ) ;
167+ }
168+ }
169+
170+ while ( true ) {
171+ const sleepDuration = await webLoginCheck ( loginResponse . doneUrl , configuration ) ;
172+ if ( sleepDuration === null )
173+ return null ;
174+
175+ if ( sleepDuration . type === `waiting` ) {
176+ await new Promise ( resolve => setTimeout ( resolve , sleepDuration . sleep * 1000 ) ) ;
177+ } else {
178+ return sleepDuration . token ;
179+ }
180+ }
181+ }
182+
183+ const WEB_LOGIN_REGISTRIES = [
184+ `https://registry.yarnpkg.com` ,
185+ `https://registry.npmjs.org` ,
186+ ] ;
187+
188+ async function performAuthentication ( opts : CredentialOptions & { webLogin ?: boolean } ) : Promise < string > {
189+ if ( opts . webLogin ?? WEB_LOGIN_REGISTRIES . includes ( opts . registry ) ) {
190+ const webToken = await loginViaWeb ( opts ) ;
191+ if ( webToken !== null ) {
192+ return webToken ;
193+ }
194+ }
195+
196+ return await loginOrRegisterViaPassword ( opts ) ;
197+ }
198+
95199/**
96200 * Register a new user, or login if the user already exists
97201 */
98- async function registerOrLogin ( registry : string , credentials : Credentials , configuration : Configuration ) : Promise < string > {
202+ async function loginOrRegisterViaPassword ( { registry, configuration, report, stdin, stdout} : CredentialOptions ) : Promise < string > {
203+ const credentials = await getCredentials ( {
204+ configuration,
205+ registry,
206+ report,
207+ stdin,
208+ stdout,
209+ } ) ;
210+
99211 // Registration and login are both handled as a `put` by npm. Npm uses a lax
100212 // endpoint as of 2023-11 where there are no conflicts if the user already
101213 // exists, but some registries such as Verdaccio are stricter and return a
@@ -193,7 +305,15 @@ interface Credentials {
193305 password : string ;
194306}
195307
196- async function getCredentials ( { configuration, registry, report, stdin, stdout} : { configuration : Configuration , registry : string , report : Report , stdin : NodeJS . ReadStream , stdout : NodeJS . WriteStream } ) : Promise < Credentials > {
308+ interface CredentialOptions {
309+ configuration : Configuration ;
310+ registry : string ;
311+ report : Report ;
312+ stdin : NodeJS . ReadStream ;
313+ stdout : NodeJS . WriteStream ;
314+ }
315+
316+ async function getCredentials ( { configuration, registry, report, stdin, stdout} : CredentialOptions ) : Promise < Credentials > {
197317 report . reportInfo ( MessageName . UNNAMED , `Logging in to ${ formatUtils . pretty ( configuration , registry , formatUtils . Type . URL ) } ` ) ;
198318
199319 let isToken = false ;
0 commit comments