text: var mach = require('../index');
  var Promise = require('../utils/Promise');
  var decodeBase64 = require('../utils/decodeBase64');
  var encodeBase64 = require('../utils/encodeBase64');
  var makeHash = require('../utils/makeHash');
  var CookieStore = require('./session/CookieStore');
  
  mach.extend(
    require('../extensions/server')
  );
  
  /**
   * The maximum size of an HTTP cookie.
   */
  var MAX_COOKIE_SIZE = 4096;
  
  /**
   * Stores the given session and returns a promise for a value that should be stored
   * in the session cookie to retrieve the session data again on the next request.
   */
  function encodeSession(session, store, secret) {
    return store.save(session).then(function (data) {
      var cookie = encodeBase64(data + '--' + makeHashWithSecret(data, secret));
  
      if (cookie.length > MAX_COOKIE_SIZE)
        throw new Error('Cookie data size exceeds 4kb; content dropped');
  
      return cookie;
    });
  }
  
  /**
   * Decodes the given cookie value and returns a promise for the corresponding session
   * data from the store. Also verifies the hash value to ensure the cookie has not been
   * tampered with. If it has, returns null.
   */
  function decodeCookie(cookie, store, secret) {
    var value = decodeBase64(cookie);
    var index = value.lastIndexOf('--');
    var data = value.substring(0, index);
    var hash = value.substring(index + 2);
  
    // Verify the cookie has not been tampered with.
    if (hash === makeHashWithSecret(data, secret))
      return store.load(data);
  
    return null;
  }
  
  function makeHashWithSecret(data, secret) {
    return makeHash(secret ? data + secret : data);
  }
  
  /**
   * A middleware that provides support for HTTP sessions using cookies.
   *
   * Options may be any of the following:
   *
   * - secret         A cryptographically secure secret key that will be used to verify
   *                  the integrity of session data that is received from the client
   * - name           The name of the cookie. Defaults to "_session"
   * - path           The path of the cookie. Defaults to "/"
   * - domain         The cookie's domain. Defaults to null
   * - secure         True to only send this cookie over HTTPS. Defaults to false
   * - expireAfter    The number of seconds after which sessions expire. Defaults
   *                  to 0 (no expiration)
   * - httpOnly       True to restrict access to this cookie to HTTP(S) APIs.
   *                  Defaults to true
   * - store          An instance of MemoryStore, CookieStore, or RedisStore that
   *                  is used to store session data. Defaults to a new CookieStore
   *
   * Example:
   *
   *   app.use(mach.session, {
   *     secret: 'the-secret',
   *     secure: true
   *   });
   *
   * Hint: A great way to generate a cryptographically secure session secret from
   * the command line:
   *
   *   $ node -p "require('crypto').randomBytes(64).toString('hex')"
   *
   * Note: Since cookies are only able to reliably store about 4k of data, if the
   * session cookie payload exceeds that the session will be dropped.
   */
  function session(app, options) {
    options = options || {};
  
    if (typeof options === 'string')
      options = { secret: options };
  
    var secret = options.secret;
    var name = options.name || '_session';
    var path = options.path || '/';
    var domain = options.domain;
    var expireAfter = options.expireAfter || 0;
    var httpOnly = ('httpOnly' in options) ? (options.httpOnly || false) : true;
    var secure = options.secure || false;
    var store = options.store || new CookieStore(options);
  
    if (!secret) {
      console.warn([
        'WARNING: There was no "secret" option provided to mach.session! This poses',
        'a security vulnerability because session data will be stored on clients without',
        'any server-side verification that it has not been tampered with. It is strongly',
        'recommended that you set a secret to prevent exploits that may be attempted using',
        'carefully crafted cookies.'
      ].join('\n'));
    }
  
    return function (conn) {
      if (conn.session)
        return conn.call(app); // Don't overwrite the existing session.
  
      var cookie = conn.request.cookies[name];
  
      return Promise.resolve(cookie && decodeCookie(cookie, store, secret)).then(function (object) {
        conn.session = object || {};
  
        return conn.call(app).then(function () {
          return Promise.resolve(conn.session && encodeSession(conn.session, store, secret)).then(function (newCookie) {
            var expires = expireAfter && new Date(Date.now() + (expireAfter * 1000));
  
            // Don't bother setting the cookie if its value
            // hasn't changed and there is no expires date.
            if (newCookie === cookie && !expires)
              return;
  
            conn.response.setCookie(name, {
              value: newCookie,
              path: path,
              domain: domain,
              expires: expires,
              httpOnly: httpOnly,
              secure: secure
            });
          }, conn.onError);
        });
      }, conn.onError);
    };
  }
  
  module.exports = session;
  