<commit_before>﻿///<amd-module name="hr.accesstokens"/>

import * as http from 'hr.http';
import { Fetcher, RequestInfo, RequestInit, Response, Request } from 'hr.fetcher';
import * as events from 'hr.eventdispatcher';
import * as ep from 'hr.externalpromise';
import {IWhitelist} from 'hr.whitelist';

//From https://github.com/auth0/jwt-decode/blob/master/lib/base64_url_decode.js
function b64DecodeUnicode(str: string) {
    return decodeURIComponent(atob(str).replace(/(.)/g, function(m, p) {
        var code = p.charCodeAt(0).toString(16).toUpperCase();
        if (code.length < 2) {
            code = '0' + code;
        }
        return '%' + code;
    }));
}

function base64_url_decode(str: string) {
    var output = str.replace(/-/g, "+").replace(/_/g, "/");
    switch (output.length % 4) {
        case 0:
            break;
        case 2:
            output += "==";
            break;
        case 3:
            output += "=";
            break;
        default:
            throw "Illegal base64url string!";
    }

    try {
        return b64DecodeUnicode(output);
    } catch (err) {
        return atob(output);
    }
};

//From https://github.com/auth0/jwt-decode/blob/master/lib/index.js
function parseJwt(token: string, options?: any) {
    if (typeof token !== 'string') {
        throw new Error('Invalid token specified');
    }

    options = options || {};
    var pos = options.header === true ? 0 : 1;
    return JSON.parse(base64_url_decode(token.split('.')[pos]));
};

interface IServerTokenResult {
    headerName: string;
    accessToken: string;
}

class TokenManager {
    private currentToken: string;
    private _headerName: string;
    private startTime: number;
    private currentSub: string;
    private expirationTick: number;
    private needLoginEvent: events.PromiseEventDispatcher<boolean, TokenManager> = new events.PromiseEventDispatcher<boolean, TokenManager>();
    private queuePromise: ep.ExternalPromise<string> = null;

    constructor(private tokenPath: string, private fetcher: Fetcher) {

    }

    public getToken(): Promise<string> {
        //First check if we should queue the request
        if (this.queuePromise !== null) {
            return this.queuePromise.Promise;
        }

        //Do we need to refresh?
        if (this.startTime === undefined || Date.now() / 1000 - this.startTime > this.expirationTick) {
            //If we need to refresh, create the queue and fire the refresh
            this.queuePromise = new ep.ExternalPromise<string>();
            this.doRefreshToken(); //Do NOT await this, we want execution to continue.
            return this.queuePromise.Promise; //Here we return the queued promise that will resolve when doRefreshToken is done.
        }

        //Didn't need refresh, return current token.
        return Promise.resolve(this.currentToken);
    }

    private async doRefreshToken(): Promise<void> {
        try {
            await this.readServerToken();
            this.resolveQueue();
        }
        catch (err) {
            //This error happens only if we can't get the access token
            //If we did not yet have a token, try to get one.
            if (await this.fireNeedLogin()) {
                //After login read the server token again and resolve the queue
                await this.readServerToken();
                this.resolveQueue();
            }
            else { //Got false from fireNeedLogin, which means no login was performed, return an error
                this.startTime = undefined;
                this.rejectQueue("Could not refresh access token or log back in.");
            }
        }
    }

    private async readServerToken(): Promise<void> {
        var data = await http.post<IServerTokenResult>(this.tokenPath, undefined, this.fetcher);
        this.currentToken = data.accessToken;
        this._headerName = data.headerName;

        var tokenObj = parseJwt(this.currentToken);

        if (this.currentSub !== undefined) {
            if (this.currentSub !== tokenObj.sub) { //Do not combine ifs
                //Subjects do not match, clear tokens
                this.clearToken();
                throw new Error("Sub did not match on new token, likely a different user. Aborting refresh.");
            }
        }
        else {
            this.currentSub = tokenObj.sub;
        }

        this.startTime = tokenObj.nbf;<commit_msg>Bring back resolving queue if undefined.<commit_after>﻿///<amd-module name="hr.accesstokens"/>

import * as http from 'hr.http';
import { Fetcher, RequestInfo, RequestInit, Response, Request } from 'hr.fetcher';
import * as events from 'hr.eventdispatcher';
import * as ep from 'hr.externalpromise';
import {IWhitelist} from 'hr.whitelist';

//From https://github.com/auth0/jwt-decode/blob/master/lib/base64_url_decode.js
function b64DecodeUnicode(str: string) {
    return decodeURIComponent(atob(str).replace(/(.)/g, function(m, p) {
        var code = p.charCodeAt(0).toString(16).toUpperCase();
        if (code.length < 2) {
            code = '0' + code;
        }
        return '%' + code;
    }));
}

function base64_url_decode(str: string) {
    var output = str.replace(/-/g, "+").replace(/_/g, "/");
    switch (output.length % 4) {
        case 0:
            break;
        case 2:
            output += "==";
            break;
        case 3:
            output += "=";
            break;
        default:
            throw "Illegal base64url string!";
    }

    try {
        return b64DecodeUnicode(output);
    } catch (err) {
        return atob(output);
    }
};

//From https://github.com/auth0/jwt-decode/blob/master/lib/index.js
function parseJwt(token: string, options?: any) {
    if (typeof token !== 'string') {
        throw new Error('Invalid token specified');
    }

    options = options || {};
    var pos = options.header === true ? 0 : 1;
    return JSON.parse(base64_url_decode(token.split('.')[pos]));
};

interface IServerTokenResult {
    headerName: string;
    accessToken: string;
}

class TokenManager {
    private currentToken: string;
    private _headerName: string;
    private startTime: number;
    private currentSub: string;
    private expirationTick: number;
    private needLoginEvent: events.PromiseEventDispatcher<boolean, TokenManager> = new events.PromiseEventDispatcher<boolean, TokenManager>();
    private queuePromise: ep.ExternalPromise<string> = null;

    constructor(private tokenPath: string, private fetcher: Fetcher) {

    }

    public getToken(): Promise<string> {
        //First check if we should queue the request
        if (this.queuePromise !== null) {
            return this.queuePromise.Promise;
        }

        //Do we need to refresh?
        if (this.startTime === undefined || Date.now() / 1000 - this.startTime > this.expirationTick) {
            //If we need to refresh, create the queue and fire the refresh
            this.queuePromise = new ep.ExternalPromise<string>();
            this.doRefreshToken(); //Do NOT await this, we want execution to continue.
            return this.queuePromise.Promise; //Here we return the queued promise that will resolve when doRefreshToken is done.
        }

        //Didn't need refresh, return current token.
        return Promise.resolve(this.currentToken);
    }

    private async doRefreshToken(): Promise<void> {
        try {
            await this.readServerToken();
            this.resolveQueue();
        }
        catch (err) {
            //This error happens only if we can't get the access token
            //If we did not yet have a token, allow the request to finish, the user is not logged in
            //Otherwise try to get the login
            if (this.currentToken === undefined) {
                this.resolveQueue();
            }
            else if (await this.fireNeedLogin()) {
                //After login read the server token again and resolve the queue
                await this.readServerToken();
                this.resolveQueue();
            }
            else { //Got false from fireNeedLogin, which means no login was performed, return an error
                this.startTime = undefined;
                this.rejectQueue("Could not refresh access token or log back in.");
            }
        }
    }

    private async readServerToken(): Promise<void> {
        var data = await http.post<IServerTokenResult>(this.tokenPath, undefined, this.fetcher);
        this.currentToken = data.accessToken;
        this._headerName = data.headerName;

        var tokenObj = parseJwt(this.currentToken);

        if (this.currentSub !== undefined) {
            if (this.currentSub !== tokenObj.sub) { //Do not combine ifs
                //Subjects do not match, clear tokens
                this.clearToken();
                throw new Error("Sub did not match on new token, likely a different user. Aborting refresh.");
            }
        }
        else {
            this.currentSub = tokenObj.sub;
        }

        this.startTime = tokenObj.nbf;
{'fraction_non_alphanumeric': 0.11193370165745857, 'fraction_numerical': 0.0064088397790055245, 'mean_word_length': 2.212992545260916, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 23, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2054619', 'n_tokens_mistral': 2521, 'n_tokens_neox': 2433, 'n_words': 728}