text: var bufferEqual = require('buffer-equal-constant-time');
  var Buffer = require('safe-buffer').Buffer;
  var crypto = require('crypto');
  var formatEcdsa = require('ecdsa-sig-formatter');
  var util = require('util');
  
  var MSG_INVALID_ALGORITHM = '"%s" is not a valid algorithm.\n  Supported algorithms are:\n  "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512" and "none".'
  var MSG_INVALID_SECRET = 'secret must be a string or buffer';
  var MSG_INVALID_VERIFIER_KEY = 'key must be a string or a buffer';
  var MSG_INVALID_SIGNER_KEY = 'key must be a string, a buffer or an object';
  
  var supportsKeyObjects = typeof crypto.createPublicKey === 'function';
  if (supportsKeyObjects) {
    MSG_INVALID_VERIFIER_KEY += ' or a KeyObject';
  }
  
  function checkIsPublicKey(key) {
    if (Buffer.isBuffer(key)) {
      return key;
    }
  
    if (typeof key === 'string') {
      return key;
    }
  
    if (!supportsKeyObjects) {
      throw typeError(MSG_INVALID_VERIFIER_KEY);
    }
  
    if (typeof key !== 'object') {
      throw typeError(MSG_INVALID_VERIFIER_KEY);
    }
  
    if (typeof key.type !== 'string') {
      throw typeError(MSG_INVALID_VERIFIER_KEY);
    }
  
    if (typeof key.asymmetricKeyType !== 'string') {
      throw typeError(MSG_INVALID_VERIFIER_KEY);
    }
  
    if (typeof key.export !== 'function') {
      throw typeError(MSG_INVALID_VERIFIER_KEY);
    }
  
    return key;
  };
  
  function checkIsPrivateKey(key) {
    if (Buffer.isBuffer(key)) {
      return key;
    }
  
    if (typeof key === 'string') {
      return key;
    }
  
    if (typeof key === 'object') {
      return key;
    }
  
    throw typeError(MSG_INVALID_SIGNER_KEY);
  };
  
  function fromBase64(base64) {
    return base64
      .replace(/=/g, '')
      .replace(/\+/g, '-')
      .replace(/\//g, '_');
  }
  
  function toBase64(base64url) {
    base64url = base64url.toString();
  
    var padding = 4 - base64url.length % 4;
    if (padding !== 4) {
      for (var i = 0; i < padding; ++i) {
        base64url += '=';
      }
    }
  
    return base64url
      .replace(/\-/g, '+')
      .replace(/_/g, '/');
  }
  
  function typeError(template) {
    var args = [].slice.call(arguments, 1);
    var errMsg = util.format.bind(util, template).apply(null, args);
    return new TypeError(errMsg);
  }
  
  function bufferOrString(obj) {
    return Buffer.isBuffer(obj) || typeof obj === 'string';
  }
  
  function normalizeInput(thing) {
    if (!bufferOrString(thing))
      thing = JSON.stringify(thing);
    return thing;
  }
  
  function createHmacSigner(bits) {
    return function sign(thing, secret) {
      if (!bufferOrString(secret))
        throw typeError(MSG_INVALID_SECRET);
      thing = normalizeInput(thing);
      var hmac = crypto.createHmac('sha' + bits, secret);
      var sig = (hmac.update(thing), hmac.digest('base64'))
      return fromBase64(sig);
    }
  }
  
  function createHmacVerifier(bits) {
    return function verify(thing, signature, secret) {
      var computedSig = createHmacSigner(bits)(thing, secret);
      return bufferEqual(Buffer.from(signature), Buffer.from(computedSig));
    }
  }
  
  function createKeySigner(bits) {
   return function sign(thing, privateKey) {
      checkIsPrivateKey(privateKey);
      thing = normalizeInput(thing);
      // Even though we are specifying "RSA" here, this works with ECDSA
      // keys as well.
      var signer = crypto.createSign('RSA-SHA' + bits);
      var sig = (signer.update(thing), signer.sign(privateKey, 'base64'));
      return fromBase64(sig);
    }
  }
  
  function createKeyVerifier(bits) {
    return function verify(thing, signature, publicKey) {
      checkIsPublicKey(publicKey);
      thing = normalizeInput(thing);
      signature = toBase64(signature);
      var verifier = crypto.createVerify('RSA-SHA' + bits);
      verifier.update(thing);
      return verifier.verify(publicKey, signature, 'base64');
    }
  }
  
  function createPSSKeySigner(bits) {
   return function sign(thing, privateKey) {
      if (!bufferOrString(privateKey) && !(typeof privateKey === 'object'))
        throw typeError(MSG_INVALID_SIGNER_KEY);
      thing = normalizeInput(thing);
      var signer = crypto.createSign('RSA-SHA' + bits);
      var sig = (signer.update(thing), signer.sign({key: privateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING}, 'base64'));
      return fromBase64(sig);
    }
  }
  
  function createPSSKeyVerifier(bits) {
    return function verify(thing, signature, publicKey) {
      if (!bufferOrString(publicKey))
        throw typeError(MSG_INVALID_VERIFIER_KEY);
      thing = normalizeInput(thing);
      signature = toBase64(signature);
      var verifier = crypto.createVerify('RSA-SHA' + bits);
      verifier.update(thing);
      return verifier.verify({key: publicKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING}, signature, 'base64');
    }
  }
  
  function createECDSASigner(bits) {
    var inner = createKeySigner(bits);
    return function sign() {
      var signature = inner.apply(null, arguments);
      signature = formatEcdsa.derToJose(signature, 'ES' + bits);
      return signature;
    };
  }
  
  function createECDSAVerifer(bits) {
    var inner = createKeyVerifier(bits);
    return function verify(thing, signature, publicKey) {
      signature = formatEcdsa.joseToDer(signature, 'ES' + bits).toString('base64');
      var result = inner(thing, signature, publicKey);
      return result;
    };
  }
  
  function createNoneSigner() {
    return function sign() {
      return '';
    }
  }
  
  function createNoneVerifier() {
    return function verify(thing, signature) {
      return signature === '';
    }
  }
  
  module.exports = function jwa(algorithm) {
    var signerFactories = {
      hs: createHmacSigner,
      rs: createKeySigner,
      ps: createPSSKeySigner,
      es: createECDSASigner,
      none: createNoneSigner,
    }
    var verifierFactories = {
      hs: createHmacVerifier,
      rs: createKeyVerifier,
      ps: createPSSKeyVerifier,
      es: createECDSAVerifer,
      none: createNoneVerifier,
    }
    var match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/i);
    if (!match)
      throw typeError(MSG_INVALID_ALGORITHM, algorithm);
    var algo = (match[1] || match[3]).toLowerCase();
    var bits = match[2];
  
    return {
      sign: signerFactories[algo](bits),
      verify: verifierFactories[algo](bits),
    }
  };
  