New features include:
- Search support
- Change server support
- Windows 10 support
- Refreshed look and feel
While creating a website using Web Optimization to handle bundling, I became curious as to what the {version} pattern matched. I couldn't find documentation for this besides what it outlined here:
The bundling framework follows several common conventions such as: •Selecting “.min” file for release when “FileX.min.js” and “FileX.js” exist. •Selecting the non “.min” version for debug. •Ignoring “-vsdoc” files (such as jquery-1.7.1-vsdoc.js), which are used only by IntelliSense.Digging into the source code here, I was able to find exactly what {version} matches, the C# regex @"(\d+(\s*\.\s*\d+){1,3})(-[a-z][0-9a-z-]*)?". This means it matches:
1 or more digits followed by 0 or more whitespace followed by the '.' character followed by 0 or more whitespace followed by 1 or more digits followed by the preceding group at least 1 times but no more than 3 times optionally followed by the '-' character followed by any a-z character followed by 0 or more a-z characters or numbersExample matches would be:
test("Test Vectors", function () {
    // Base32Decode should correctly decode the test vectors from the RFC
    strictEqual(Base32Decode("").length, 0, "Base32Decode should return an empty array for the empty string");
    ok(compareUint8ArrayToString(Base32Decode("MY======"), "f"), "Base32Decode should return 'f' for 'MY======'");
    ok(compareUint8ArrayToString(Base32Decode("MZXQ===="), "fo"), "Base32Decode should return 'f' for 'MZXQ===='");
    ok(compareUint8ArrayToString(Base32Decode("MZXW6YQ="), "foob"), "Base32Decode should return 'foob' for 'MZXW6YQ='");
    ok(compareUint8ArrayToString(Base32Decode("MZXW6YTB"), "fooba"), "Base32Decode should return 'fooba' for 'MZXW6YTB'");
    ok(compareUint8ArrayToString(Base32Decode("MZXW6YTBOI======"), "foobar"), "Base32Decode should return 'foobar' for 'MZXW6YTBOI======'");
});
Obviously these tests won't pass until we have a working Base32 decoder. The decoder is fairly straight-forward for inputs that don't have padding (i.e., the number of bytes are multiples of 40). In that case, you simple map the bits per the RFC:
The RFC goes into detail about what cases are possible with padding, etc. but I'll leave that as an exercise to the reader. I could have made the code smaller, but I wanted to be clear and follow the RFC as closely as possible. Here is the implementation:
var Base32Decode = function (base32EncodedString) {
    /// Decodes a base32 encoded string into a Uin8Array, note padding is not supported 
    /// The base32 encoded string to be decoded
    /// The Unit8Array representation of the data that was encoded in base32EncodedString 
    if (!base32EncodedString && base32EncodedString !== "") {
        throw "base32EncodedString cannot be null or undefined";
    }
    if (base32EncodedString.length * 5 % 8 !== 0) {
        throw "base32EncodedString is not of the proper length. Please verify padding.";
    }
    base32EncodedString = base32EncodedString.toLowerCase();
    var alphabet = "abcdefghijklmnopqrstuvwxyz234567";
    var returnArray = new Array(base32EncodedString.length * 5 / 8);
    var currentByte = 0;
    var bitsRemaining = 8;
    var mask = 0;
    var arrayIndex = 0;
    for (var count = 0; count < base32EncodedString.length; count++) {
        var currentIndexValue = alphabet.indexOf(base32EncodedString[count]);
        if (-1 === currentIndexValue) {
            if ("=" === base32EncodedString[count]) {
                var paddingCount = 0;
                for (count = count; count < base32EncodedString.length; count++) {
                    if ("=" !== base32EncodedString[count]) {
                        throw "Invalid '=' in encoded string";
                    } else {
                        paddingCount++;
                    }
                }
                switch (paddingCount) {
                    case 6:
                        returnArray = returnArray.slice(0, returnArray.length - 4);
                        break;
                    case 4:
                        returnArray = returnArray.slice(0, returnArray.length - 3);
                        break;
                    case 3:
                        returnArray = returnArray.slice(0, returnArray.length - 2);
                        break;
                    case 1:
                        returnArray = returnArray.slice(0, returnArray.length - 1);
                        break;
                    default:
                        throw "Incorrect padding";
                }
            } else {
                throw "base32EncodedString contains invalid characters or invalid padding.";
            }
        } else {
            if (bitsRemaining > 5) {
                mask = currentIndexValue << (bitsRemaining - 5);
                currentByte = currentByte | mask;
                bitsRemaining -= 5;
            } else {
                mask = currentIndexValue >> (5 - bitsRemaining);
                currentByte = currentByte | mask;
                returnArray[arrayIndex++] = currentByte;
                currentByte = currentIndexValue << (3 + bitsRemaining);
                bitsRemaining += 3;
            }
        }
    }
    return new Uint8Array(returnArray);
};
I've added more tests around padding and other specifics you can find at the source below, but enjoy a live demo converting base32 encoded strings to hexadecimal:
You can find the source for both the tests and the actual decode on github here: Base32Decode in JavaScript.
 Also, if you'd like, you can run the tests directly from your browser via this link.
function GenerateToken(base32EncodedSecret: string, callback: (number) => void): void {
    if (!msCrypto) {
        throw "MsCrypto not found";
    }
    // Google by default puts spaces in the secret, so strip them out.
    base32EncodedSecret = base32EncodedSecret.replace(/\s/g, "");
    // This method decodes the secret to bytes, the code is excluded here.
    var keyData: Uint8Array = GoogleAuthenticator.Base32Decode(base32EncodedSecret);
    var time: number = Math.floor(Date.now() / 30000);
    var data: Uint8Array = GoogleAuthenticator.NumericToUint8Array(time);
    // We need to create a key that the subtle object can actualy do work with
    var importKeyOp: KeyOperation = msCrypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
    importKeyOp.onerror = function (e) {
        console.log("error event handler fired.");
        callback(-1);
    }
    importKeyOp.oncomplete = function (e) {
        var key: Key = e.target.result;
        // HMAC the secret with the time
        var signkey = msCrypto.subtle.sign({ name: "HMAC", hash: "SHA-1" }, key, data);
        signkey.onerror = function (evt) {
            console.error("onerror event handler fired.");
            callback(-1);
        };
        signkey.oncomplete = function (evt) {
            // Now that we have the hash, we need to perform the HOTP specific byte selection
            // (called dynamic truncation in the RFC)
            var signature: ArrayBuffer = evt.target.result;
            if (signature) {
                var signatureArray: Uint8Array = new Uint8Array(signature);
                var offset: number = signatureArray[signatureArray.length - 1] & 0xf;
                var binary: number = ((signatureArray[offset] & 0x7f) << 24) |
                    ((signatureArray[offset + 1] & 0xff) << 16) |
                    ((signatureArray[offset + 2] & 0xff) << 8) |
                    (signatureArray[offset + 3] & 0xff);
                callback(binary % 1000000);
            } else {
                console.error("Sign with HMAC - SHA-1: FAIL");
                callback(-1);
            }
        };
    };
}
// Module containing all logic for coalescable timers
module Coalescable {
    // Variable holding all coalesced timers that are aggregating the individual timers
    var timers: CoalescedTimer[] = new CoalescedTimer[];
    export function SetCoalescableTimeout(expression: any, msec: number, tolerance: number): void {
        // Search existing coalesced timers for timers that can accomodate this request
        for (var index: number = 0; index < timers.length; index++) {
            var coalescedTimer: CoalescedTimer = timers[index];
            if (msec - tolerance < coalescedTimer.msec &&
                msec + tolerance > coalescedTimer.msec) {
                coalescedTimer.Timers.push(expression);
                return;
            }
        }
        // Create a new coalesced timer since none can accomodate this request
        var coalescedTimer: CoalescedTimer = new CoalescedTimer(msec)
        coalescedTimer.Timers.push(expression);
        timers.push(coalescedTimer);
    }
    class CoalescedTimer {
        constructor(public msec: number) {
            setInterval(function () => {
                for (var index: number = 0; index < this.Timers.length; index++) {
                    new Function(this.Timers[index])();
                }
            }, msec);
        }
        public Timers: any[] = new any[];
    }
}
Coalescable.SetCoalescableTimeout("alert('1')", 5000, 10); // Will create a new coalesced timer since none exist.
Coalescable.SetCoalescableTimeout("alert('2')", 6000, 2000); // Will be coalesced into the existing timer to run every 5000ms.