text: /**
   * Returns a singleton
   *
   * @module signer
   */
  
  var url         = require('url')
    , crypto      = require('crypto')
    , querystring = require('querystring');
  
  module.exports = exports = {
    _algorithm: "sha256",
    _digest: "base64",
  
    _ttl: 3600, // seconds
  
    _privateKey: "",
  
    /**
     * Initialize the signer singleton with a privateKey and other options
     *
     * Must be called first with the privateKey before calling any other functions
     * 
     * @param {Object} options            The options object
     * @param {String} options.privateKey The private key to hash the URLs with
     * @param {String} [options.algorithm="sha256"]  The algorithm to pass to [crypto.createHash(algorithm)]{@link http://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm}
     * @param {String} [options.digest="base64"]     The digest to pass to [crypto.digest(encoding)]{@link http://nodejs.org/api/crypto.html#crypto_hash_digest_encoding}
     * @param {Number} [options.ttl=3600]        Time to live in seconds for the URL after which it should expire; 0 for never
     */
    init: function(options) {
      this._algorithm = options.algorithm || this._algorithm;
      this._digest = options.digest || this._digest;
  
      this._ttl = Number(options.ttl) >= 0 ? Number(options.ttl) : this._ttl;
      this._privateKey = options.privateKey;
    },
  
    /**
     * Sign a given URL and return the signature
     * 
     * @param  {String} urlToSign URL to sign
     * @throws {Error}            If init() is not called first with a privateKey
     * @return {String}           URL signature
     */
    getUrlSignature: function(urlToSign) {
      if (!this._privateKey.length) {
        throw new Error("Call init() with privateKey first");
      }
  
      return crypto
          .createHmac(this._algorithm, this._privateKey)
          .update(urlToSign, 'utf-8')
          .digest(this._digest);
    },
  
    /**
     * Sign a given URL and return a new URL having two additional query params: `expires`, `signature`
     *
     * `expires` is TTL seconds ahead of the time at whoch the URL was signed
     *
     * If the given URL already has `expires` or `signature` set as query params, they will be
     * overridden.
     * 
     * @param  {String} urlToSign The URL to sign
     * @throws {Error}            If init() is not called first with a privateKey
     * @return {String}           The new URL with `expires` and `signature` set as query params
     */
    getSignedUrl: function(urlToSign) {
      if (!this._privateKey.length) {
        throw new Error("Call init() with privateKey first");
      }
  
      var parsedUrl = url.parse(urlToSign, true)
        , urlQuery  = parsedUrl.query;
  
      delete(urlQuery.expires);
      delete(urlQuery.signature);
  
      if (this._ttl) {
        urlQuery.expires = Math.floor(new Date()/1000) + this._ttl;
        parsedUrl.search = querystring.stringify(urlQuery);
      }
  
      urlQuery.signature = this.getUrlSignature(parsedUrl.format());
      parsedUrl.search = querystring.stringify(urlQuery);
  
      return parsedUrl.format();
    },
  
    /**
     * Checks if the signature of a given URL matches the expected signature
     * 
     * @param  {String}  signature   The expected signature
     * @param  {Strign}  urlToVerify The URL to check
     * @throws {Error}            If init() is not called first with a privateKey
     * @return {Boolean}             Whether or not the signature is valid
     */
    verifyUrlSignature: function(signature, urlToVerify) {
      return signature === this.getUrlSignature(urlToVerify);
    },
  
    /**
     * Verifies a given URL by checking first if the signature is valid and then checking it has
     * not expired
     * 
     * @param  {String}         urlToVerify The URL to verify
     * @throws {Error}            If init() is not called first with a privateKey
     * @return {Boolean|String}             true, `expired`, or `invalid`
     */
    verifySignedUrl: function(urlToVerify) {
      var parsedUrl = url.parse(urlToVerify, true)
        , urlQuery = parsedUrl.query
        , signature = urlQuery.signature
        , expires = urlQuery.expires;
  
        delete(urlQuery.signature);
  
        parsedUrl.search = querystring.stringify(urlQuery);
        
        if (!this.verifyUrlSignature(signature, parsedUrl.format())) {
          return 'invalid';
        }
        
        if (expires && Math.floor(new Date()/1000) > expires) {
          return 'expired';
        }
  
        return true;
    },
  
    /**
     * Return the Express middleware
     *
     * @param  {Object}   options
     * @param  {Function} [options.invalid=sends 403] The callback with signature fn(req, res) to
     *                                                call when the request URL is invalid
     * @param  {Function} [options.expired=sends 410] The callback with signature fn(req, res) to
     *                                                call when the request URL is expired
     * @return {Function} Express middleware function
     */
    verifier: function(options) {
      var self = this;
      options = options || {};
  
      var callbacks = {
        invalid: options.invalid || function(req, res) {
          res.sendStatus(403); // Forbidden
        },
        expired: options.expired || function(req, res) {
          res.sendStatus(410); // Gone
        }
      };
  
      return function(req, res, next) {
        var url = req.originalUrl;
  
        var valid = self.verifySignedUrl(url);
  
        if (valid !== true && callbacks[valid]) {
          callbacks[valid](req, res);
        } else {
          next();
        }
      };
    }
  };