namespace pxt . auth { const EMPTY_USERNAME = "??" ; const AUTH_CONTAINER = "auth" ; const CSRF_TOKEN_KEY = "csrf-token" ; const AUTH_LOGIN_STATE_KEY = "login-state" ; const AUTH_USER_STATE_KEY = "user-state" ; const X_PXT_TARGET = "x-pxt-target" ; export type ApiResult < T > = { resp : T ; statusCode : number ; success : boolean ; err : any ; } ; const DEV_BACKEND_DEFAULT = "" ; const DEV_BACKEND_PROD = "https://www.makecode.com" ; const DEV_BACKEND_STAGING = "https://staging.pxt.io" ; const DEV_BACKEND_LOCALHOST_5500 = "http://localhost:5500" ; const DEV_BACKEND_LOCALHOST_8080 = "http://localhost:8080" ; const DEV_BACKEND = DEV_BACKEND_STAGING ; let authDisabled = false ; export type UserProfile = { id ? : string ; idp ? : { provider ? : pxt . IdentityProviderId ; username ? : string ; displayName ? : string ; picture ? : { mimeType ? : string ; width ? : number ; height ? : number ; encoded ? : string ; dataUrl ? : string ; } ; pictureUrl ? : string ; } ; } ; export type UserSkillmapState = { mapProgress : any ; completedTags : any ; } ; export type UserBadgeState = { badges : Badge [ ] ; } ; export type SetPrefResult = { success : boolean ; res : UserPreferences ; } ; /**
     * User preference state that should be synced with the cloud.
     */ export type UserPreferences = { language ? : string ; highContrast ? : boolean ; reader ? : string ; skillmap ? : UserSkillmapState ; badges ? : UserBadgeState ; email ? : boolean ; } ; export const DEFAULT_USER_PREFERENCES = ( ) => ( { highContrast : false , language : pxt . appTarget . appTheme . defaultLocale , reader : "" , skillmap : { mapProgress : { } , completedTags : { } } , email : false } ) ; /**
     * Cloud-synced user state.
     */ export type State = { profile ? : UserProfile ; preferences ? : UserPreferences ; } ; let _client ; export function client ( ) { return _client ; } const PREFERENCES_DEBOUNCE_MS = 1 * 1000 ; const PREFERENCES_DEBOUNCE_MAX_MS = 10 * 1000 ; let debouncePreferencesChangedTimeout = 0 ; let debouncePreferencesChangedStarted = 0 ; export abstract class AuthClient { /**
         * In-memory cache of user profile state. This is persisted in local storage.
         * DO NOT ACCESS DIRECTLY UNLESS YOU KNOW IT IS OK. Functions allowed direct
         * access are marked as such below.
         */ private state$ : Readonly < State > ; constructor ( ) { pxt . Util . assert ( ! _client ) ; _client = this ; } public async initAsync ( ) { try { this . state$ = await pxt . storage . shared . getAsync < State > ( AUTH_CONTAINER , AUTH_USER_STATE_KEY ) ; } catch { } if ( ! this . state$ ) { this . state$ = { } ; } this . setUserProfileAsync ( this . state$ . profile ) ; this . setUserPreferencesAsync ( this . state$ . preferences ) ; } protected abstract onSignedIn ( ) ; protected abstract onSignedOut ( ) ; protected abstract onSignInFailed ( ) ; protected abstract onUserProfileChanged ( ) ; protected abstract onUserPreferencesChanged ( diff ) ; protected abstract onProfileDeleted ( userId ) ; protected abstract onApiError ( err ) ; protected abstract onStateCleared ( ) ; param idp param persistent param callbackState public async loginAsync ( idp , persistent , callbackState = undefined ) { if ( ! hasIdentity ( ) || ! idpEnabled ( idp ) ) { return ; } callbackState = callbackState ?? NilCallbackState ; const state = this . getState ( ) ; if ( ! state . profile ) { await this . authCheckAsync ( ) ; } const currIdp = state . profile ?. idp ; if ( currIdp === idp ) { pxt . debug ( `loginAsync: Already signed into ${ idp }.` ) ; return ; } this . clearState ( ) ; const genId = ( ) => ( Math . PI * Math . random ( ) ) . toString ( 36 ) . slice ( 2 ) ; const loginState = { key : genId ( ) , callbackState , callbackPathname : window . location . pathname , idp , } ; await pxt . storage . shared . setAsync ( AUTH_CONTAINER , AUTH_LOGIN_STATE_KEY , loginState ) ; const loginUrl = pxt . Util . stringifyQueryString ( '/api/auth/login' , { response_type : "token" , provider : idp , persistent , redirect_uri : `${ window . location . origin }${ window . location . pathname }?authcallback=1&state=${ loginState . key }` } ) ; const apiResult = await this . apiAsync < LoginResponse > ( loginUrl ) ; if ( apiResult . success ) { pxt . tickEvent ( 'auth.login.start' , { 'provider' : idp } ) ; window . location . href = apiResult . resp . loginUrl ; } else { await this . onSignInFailed ( ) ; } } /**
         * Sign out the user and clear the auth token cookie.
         */ public async logoutAsync ( continuationHash ? ) { if ( ! hasIdentity ( ) ) { return ; } pxt . tickEvent ( 'auth.logout' ) ; await this . apiAsync ( '/api/auth/logout' ) ; await pxt . storage . shared . delAsync ( AUTH_CONTAINER , CSRF_TOKEN_KEY ) ; this . clearState ( ) ; const hash = continuationHash ? continuationHash . startsWith ( '#' ) ? continuationHash : `#${ continuationHash }` : "" ; if ( pxt . BrowserUtils . hasWindow ( ) ) { window . location . href = `${ window . location . origin }${ window . location . pathname }${ hash }` ; location . reload ( ) ; } } public async deleteProfileAsync ( ) { if ( ! await this . loggedInAsync ( ) ) { return ; } const userId = this . getState ( ) . profile ?. id ; const res = await this . apiAsync ( '/api/user' , null , 'DELETE' ) ; if ( res . err ) { await this . onApiError ( ( res . err ) ) ; } else { try { await pxt . storage . shared . delAsync ( AUTH_CONTAINER , CSRF_TOKEN_KEY ) ; try { await this . onProfileDeleted ( userId ) ; } catch { pxt . tickEvent ( 'auth.profile.cloudToLocalFailed' ) ; } this . clearState ( ) ; } finally { pxt . tickEvent ( 'auth.profile.deleted' ) ; } } } private initialUserPreferences_ : Promise < UserPreferences | undefined > = undefined ; public async initialUserPreferencesAsync ( ) { if ( ! await this . loggedInAsync ( ) ) { return undefined ; } if ( ! this . initialUserPreferences_ ) { this . initialUserPreferences_ = this . fetchUserPreferencesAsync ( ) ; } return this . initialUserPreferences_ ; } public async userProfileAsync ( ) { if ( ! await this . loggedInAsync ( ) ) { return undefined ; } const state = this . getState ( ) ; return { ... state . profile } ; } public async userPreferencesAsync ( ) { const state = this . getState ( ) ; return { ... state . preferences } ; } private initialAuthCheck_ : Promise < UserProfile | undefined > = undefined ; /**
         * Checks to see if we're already logged in by trying to fetch user info from
         * the backend. If we have a valid auth token cookie, it will succeed.
         */ public async authCheckAsync ( ) { if ( ! hasIdentity ( ) ) { return undefined ; } if ( ! ( await pxt . storage . shared . getAsync ( AUTH_CONTAINER , CSRF_TOKEN_KEY ) ) ) { return undefined ; } const state = this . getState ( ) ; if ( state . profile ?. id ) { if ( ! this . initialAuthCheck_ ) { this . initialAuthCheck_ = Promise . resolve ( state . profile ) ; } } else if ( ! this . initialAuthCheck_ ) { this . initialAuthCheck_ = this . fetchUserAsync ( ) ; } return this . initialAuthCheck_ ; } public async loggedInAsync ( ) { await this . authCheckAsync ( ) ; return this . hasUserId ( ) ; } public async updateUserProfileAsync ( opts ) { if ( ! await this . loggedInAsync ( ) ) { return false ; } const state = this . getState ( ) ; const result = await this . apiAsync < UserProfile > ( '/api/user/profile' , { id : state . profile . id , username : opts . username , avatarUrl : opts . avatarUrl } as UserProfile ) ; if ( result . success ) { await this . setUserProfileAsync ( result . resp ) ; } return result . success ; } private patchQueue : { ops : ts . pxtc . jsonPatch . PatchOperation [ ] ; filter ? : ( op : ts . pxtc . jsonPatch . PatchOperation ) => boolean ; } [ ] = [ ] ; public async patchUserPreferencesAsync ( patchOps , opts = { } ) { const defaultSuccessAsync = async ( ) : Promise < SetPrefResult > => ( { success : true , res : await this . userPreferencesAsync ( ) } ) ; patchOps = Array . isArray ( patchOps ) ? patchOps : [ patchOps ] ; patchOps = patchOps . filter ( op => ! ! op ) ; if ( ! patchOps . length ) { return await defaultSuccessAsync ( ) ; } const patchDiff = ( pSrc , ops , filter ? ) => { const pDst = U . deepCopy ( pSrc ) ; ts . pxtc . jsonPatch . patchInPlace ( pDst , ops ) ; let diff = ts . pxtc . jsonPatch . diff ( pSrc , pDst ) ; if ( diff . length && filter ) { diff = diff . filter ( filter ) ; } return diff ; } ; const curPref = await this . userPreferencesAsync ( ) ; const diff = patchDiff ( curPref , patchOps , opts . filter ) ; if ( ! diff . length ) { return await defaultSuccessAsync ( ) ; } ts . pxtc . jsonPatch . patchInPlace ( curPref , diff ) ; await this . setUserPreferencesAsync ( curPref ) ; if ( ! await this . loggedInAsync ( ) ) { return await defaultSuccessAsync ( ) ; } this . patchQueue . push ( { ops : patchOps , filter : opts . filter } ) ; clearTimeout ( debouncePreferencesChangedTimeout ) ; const syncPrefs = async ( ) => { debouncePreferencesChangedStarted = 0 ; if ( ! this . patchQueue . length ) { return await defaultSuccessAsync ( ) ; } const getResult = await this . apiAsync < Partial < UserPreferences > > ( '/api/user/preferences' ) ; if ( ! getResult . success ) { pxt . reportError ( "identity" , "failed to fetch preferences for patch" , getResult as any ) ; return { success : false , res : undefined } ; } const remotePrefs = U . deepCopy ( getResult . resp ) || DEFAULT_USER_PREFERENCES ( ) ; const patchQueue = this . patchQueue ; this . patchQueue = [ ] ; patchQueue . forEach ( patch => { const diff = patchDiff ( remotePrefs , patch . ops , patch . filter ) ; ts . pxtc . jsonPatch . patchInPlace ( remotePrefs , diff ) ; } ) ; const finalOps = pxtc . jsonPatch . diff ( getResult . resp , remotePrefs ) ; const patchResult = await this . apiAsync < UserPreferences > ( '/api/user/preferences' , finalOps , 'PATCH' ) ; if ( patchResult . success ) { this . setUserPreferencesAsync ( patchResult . resp ) ; } else { pxt . reportError ( "identity" , "failed to patch preferences" , patchResult as any ) ; } return { success : patchResult . success , res : patchResult . resp } ; } ; if ( opts . immediate ) { return await syncPrefs ( ) ; } else { if ( ! debouncePreferencesChangedStarted ) { debouncePreferencesChangedStarted = U . now ( ) ; } if ( PREFERENCES_DEBOUNCE_MAX_MS < U . now ( ) - debouncePreferencesChangedStarted ) { return await syncPrefs ( ) ; } else { debouncePreferencesChangedTimeout = setTimeout ( syncPrefs , PREFERENCES_DEBOUNCE_MS ) ; return { success : false , res : undefined } ; } } } hasUserId ( ) { if ( ! hasIdentity ( ) ) { return false ; } const state = this . getState ( ) ; return ! ! state . profile ?. id ; } private async fetchUserAsync ( ) { if ( ! hasIdentity ( ) ) { return undefined ; } const state = this . getState ( ) ; if ( state . profile ?. id ) { return state . profile ; } const result = await this . apiAsync ( '/api/user/profile' ) ; if ( result . success ) { const profile = result . resp ; await this . setUserProfileAsync ( profile ) ; return profile ; } return undefined ; } private async setUserProfileAsync ( profile ) { const wasLoggedIn = this . hasUserId ( ) ; this . transformUserProfile ( profile ) ; const isLoggedIn = this . hasUserId ( ) ; this . onUserProfileChanged ( ) ; if ( isLoggedIn && ! wasLoggedIn ) { await this . onSignedIn ( ) ; } else if ( ! isLoggedIn && wasLoggedIn ) { await this . onSignedOut ( ) ; } } private async setUserPreferencesAsync ( newPref ) { const oldPref = this . getState ( ) . preferences ?? DEFAULT_USER_PREFERENCES ( ) ; const diff = ts . pxtc . jsonPatch . diff ( oldPref , newPref ) ; this . transformUserPreferences ( { ... oldPref , ... newPref } ) ; await this . onUserPreferencesChanged ( diff ) ; } private async fetchUserPreferencesAsync ( ) { if ( ! await this . loggedInAsync ( ) ) { return undefined ; } const state = this . getState ( ) ; const result = await this . apiAsync < Partial < UserPreferences > > ( '/api/user/preferences' ) ; if ( result . success ) { if ( result . resp ) { this . setUserPreferencesAsync ( result . resp ) ; return state . preferences ; } } return undefined ; } /**
         * Updates user profile state and writes it to local storage.
         * Direct access to state$ allowed.
         */ private transformUserProfile ( profile ) { this . state$ = { ... this . state$ , profile : { ... profile } } ; this . saveState ( ) ; } /**
         * Updates user preference state and writes it to local storage.
         * Direct access to state$ allowed.
         */ protected transformUserPreferences ( preferences ) { this . state$ = { ... this . state$ , preferences : { ... preferences } } ; this . saveState ( ) ; } /**
         * Read-only access to current state.
         * Direct access to state$ allowed.
         */ getState ( ) { return this . state$ ; } ; /**
         * Write auth state to local storage.
         * Direct access to state$ allowed.
         */ private saveState ( ) { pxt . storage . shared . setAsync ( AUTH_CONTAINER , AUTH_USER_STATE_KEY , this . state$ ) . then ( ( ) => { } ) ; } /**
         * Clear all auth state.
         * Direct access to state$ allowed.
         */ private clearState ( ) { this . state$ = { } ; pxt . storage . shared . delAsync ( AUTH_CONTAINER , AUTH_USER_STATE_KEY ) . then ( ( ) => this . onStateCleared ( ) ) ; } async apiAsync < T = any > ( url , data ? , method ? ) { const headers = { } ; const csrfToken = await pxt . storage . shared . getAsync < string > ( AUTH_CONTAINER , CSRF_TOKEN_KEY ) ; if ( csrfToken ) { headers [ "authorization" ] = `mkcd ${ csrfToken }` ; } headers [ X_PXT_TARGET ] = pxt . appTarget ?. id ; url = pxt . BrowserUtils . isLocalHostDev ( ) ? `${ DEV_BACKEND }${ url }` : url ; return pxt . Util . requestAsync ( { url , headers , data , method : method ? method : data ? "POST" : "GET" , withCredentials : true , } ) . then ( r => { return { statusCode : r . statusCode , resp : r . json , success : Math . floor ( r . statusCode / 100 ) === 2 , err : null } ; } ) . catch ( async ( e ) => { if ( ! /logout/ . test ( url ) && e . statusCode == 401 ) { await this . logoutAsync ( ) ; } return { statusCode : e . statusCode , err : e , resp : null , success : false } ; } ) ; } } export type CallbackState = { hash ? : string ; params ? : pxt . Map < string > ; } ; const NilCallbackState = { hash : '' , params : { } } ; /**
     * During login, we store some state in local storage so we know where to
     * redirect after login completes. This is the shape of that state.
     */ type LoginState = { key : string ; callbackState : CallbackState ; callbackPathname : string ; idp : pxt . IdentityProviderId ; } ; type LoginResponse = { loginUrl : string ; } ; export async function loginCallbackAsync ( qs ) { let loginState ; let callbackState = { ... NilCallbackState } ; do { loginState = await pxt . storage . shared . getAsync < LoginState > ( AUTH_CONTAINER , AUTH_LOGIN_STATE_KEY ) ; if ( ! loginState ) { pxt . debug ( "Auth state not found in storge." ) ; return ; } await pxt . storage . shared . delAsync ( AUTH_CONTAINER , AUTH_LOGIN_STATE_KEY ) ; const stateKey = qs [ 'state' ] ; if ( ! stateKey || loginState . key !== stateKey ) { pxt . debug ( "Failed to get auth state for key" ) ; return ; } callbackState = { ... NilCallbackState , ... loginState . callbackState } ; const error = qs [ 'error' ] ; if ( error ) { const error_description = qs [ 'error_description' ] ; pxt . tickEvent ( 'auth.login.error' , { 'error' : error , 'provider' : loginState . idp } ) ; pxt . log ( `Auth failed: ${ error }:${ error_description }` ) ; callbackState = { ... NilCallbackState } ; break ; } const authToken = qs [ 'token' ] ; if ( ! authToken ) { pxt . debug ( "Missing authToken in auth callback." ) ; break ; } await pxt . storage . shared . setAsync ( AUTH_CONTAINER , CSRF_TOKEN_KEY , authToken ) ; pxt . tickEvent ( 'auth.login.success' , { 'provider' : loginState . idp } ) ; } while ( false ) ; const hash = callbackState . hash . startsWith ( '#' ) ? callbackState . hash : `#${ callbackState . hash }` ; const params = pxt . Util . stringifyQueryString ( '' , callbackState . params ) ; const pathname = loginState . callbackPathname . startsWith ( '/' ) ? loginState . callbackPathname : `/${ loginState . callbackPathname }` ; const redirect = `${ pathname }${ hash }${ params }` ; window . location . href = redirect ; } export function identityProviders ( ) { return Object . keys ( pxt . appTarget ?. cloud ?. cloudProviders || { } ) . map ( id => pxt . appTarget . cloud . cloudProviders [ id ] ) . filter ( prov => prov . identity ) . sort ( ( a , b ) => a . order - b . order ) ; } export function identityProvider ( id ) { return identityProviders ( ) . filter ( prov => prov . id === id ) . shift ( ) ; } export function hasIdentity ( ) { return ! authDisabled && ! pxt . BrowserUtils . isPxtElectron ( ) && identityProviders ( ) . length > 0 ; } function idpEnabled ( idp ) { return identityProviders ( ) . filter ( prov => prov . id === idp ) . length > 0 ; } export function enableAuth ( enabled = true ) { authDisabled = ! enabled ; } export function userName ( user ) { return user ?. idp ?. displayName ?? user ?. idp ?. username ?? EMPTY_USERNAME ; } export function userInitials ( user ) { const username = pxt . auth . userName ( user ) ; return ts . pxtc . Util . initials ( username ) ; } export function generateUserProfilePicDataUrl ( profile ) { if ( profile ?. idp ?. picture ?. encoded ) { const url = window . URL || window . webkitURL ; try { const decoded = pxt . Util . stringToUint8Array ( atob ( profile . idp . picture . encoded ) ) ; const blob = new Blob ( [ decoded ] , { type : profile . idp . picture . mimeType } ) ; profile . idp . picture . dataUrl = url . createObjectURL ( blob ) ; } catch { } } } /**
     * Checks only the ID and sourceURL
     */ export function badgeEquals ( badgeA , badgeB ) { return badgeA . id === badgeB . id && badgeA . sourceURL === badgeB . sourceURL ; } export function hasBadge ( preferences , badge ) { return preferences . badges . some ( toCheck => badgeEquals ( toCheck , badge ) ) ; } }