var isString = require('lodash/isString');

/**
 * Extracts parts of a string, beginning at the character at the specified position,
 * and returns the specified number of characters.
 * The substr() does not change the original string.
 *
 * @customNeeds Validate string type for preventing SyntaxError
 *
 * @since 1.0.0
 * @category String
 *
 * @param {String} string -  The string to extract
 * @param {Number} start -  The position where to start the extraction. First character is at index 0
 * @param {Number?} length -  Optional. The number of characters to extract. If omitted, it extracts the rest of the string
 *
 * @returns {String} Returns extract part of a string
 *
 * @example
 *
 * famulus.substr('Hello World!', 0, 5)
 * // => 'Hello'
 *
 * famulus.substr('Hello World!', 6)
 * // => 'World!'
 */
function substr(string, start, length) {
    if (!isString(string)) {
        return string;
    }

    return string.substr(start, length);
}

module.exports = substr;


import { initializer } from 'knifecycle';
import http from 'http';
import ms from 'ms';

function noop() {}

/* Architecture Note #2: HTTP Server
The `httpServer` service is responsible for instanciating
 the httpServer and handling its start/shutdown.
*/

export default initializer(
  {
    name: 'httpServer',
    inject: [
      '?ENV',
      '?HOST',
      '?PORT',
      '?MAX_HEADERS_COUNT',
      '?KEEP_ALIVE_TIMEOUT',
      '?MAX_CONNECTIONS',
      '?TIMEOUT',
      'httpRouter',
      '?log',
    ],
  },
  initHTTPServer,
);

/**
 * Initialize an HTTP server
 * @param  {Object}   services
 * The services the server depends on
 * @param  {Object}   services.ENV
 * The process environment variables
 * @param  {Object}   services.HOST
 * The server host
 * @param  {Object}   services.PORT
 * The server port
 * @param  {Object}   services.MAX_HEADERS_COUNT
 * The https://nodejs.org/api/http.html#http_server_maxheaderscount
 * @param  {Object}   services.KEEP_ALIVE_TIMEOUT
 * See https://nodejs.org/api/http.html#http_server_keepalivetimeout
 * @param  {Object}   services.MAX_CONNECTIONS
 * See https://nodejs.org/api/net.html#net_server_maxconnections
 * @param  {Object}   services.TIMEOUT
 * See https://nodejs.org/api/http.html#http_server_timeout
 * @param  {Function} services.httpRouter
 * The function to run with the req/res tuple
 * @param  {Function} [services.log=noop]
 * A logging function
 * @return {Promise}
 * A promise of an object with a NodeJS HTTP server
 *  in its `service` property.
 */
async function initHTTPServer({
  ENV = {},
  HOST = '127.0.0.1',
  PORT = 8080,
  MAX_HEADERS_COUNT = 800,
  KEEP_ALIVE_TIMEOUT = ms('5m'),
  TIMEOUT = ms('2m'),
  MAX_CONNECTIONS,
  httpRouter,
  log = noop,
}) {
  const sockets = ENV.DESTROY_SOCKETS ? new Set() : {}.undef;
  const httpServer = http.createServer(httpRouter);
  const listenPromise = new Promise(resolve => {
    httpServer.listen(PORT, HOST, () => {
      log('info', `HTTP Server listening at "http://${HOST}:${PORT}".`);
      resolve(httpServer);
    });
  });
  const errorPromise = new Promise((resolve, reject) => {
    httpServer.once('error', reject);
  });

  httpServer.timeout = TIMEOUT;
  httpServer.keepAliveTimeout = KEEP_ALIVE_TIMEOUT;
  httpServer.maxHeadersCount = MAX_HEADERS_COUNT;
  httpServer.maxConnections = MAX_CONNECTIONS;

  if ('undefined' !== typeof MAX_CONNECTIONS) {
    httpServer.maxConnections = MAX_CONNECTIONS;
  }

  if (ENV.DESTROY_SOCKETS) {
    httpServer.on('connection', socket => {
      sockets.add(socket);
      socket.on('close', () => {
        sockets.delete(socket);
      });
    });
  }

  return Promise.race([listenPromise, errorPromise]).then(() => ({
    service: httpServer,
    errorPromise,
    dispose: () =>
      new Promise((resolve, reject) => {
        log('debug', 'Closing HTTP server.');
        // Avoid to keepalive connections on shutdown
        httpServer.timeout = 1;
        httpServer.keepAliveTimeout = 1;
        httpServer.close(err => {
          if (err) {
            reject(err);
            return;
          }
          log('debug', 'HTTP server closed');
          resolve();
        });
        if (ENV.DESTROY_SOCKETS) {
          for (const socket of sockets.values()) {
            socket.destroy();
          }
        }
      }),
  }));
}


var _ = require('lodash');

/**
 * Sort an array by the name of existing property and add a first element into array
 *
 * @customNeeds Sort array by name and add first element.
 * For e.g. user names - [ {name: 'All'}, {name: 'Aron'}, {name: 'Bob'} ]
 *
 * @since 1.1.0
 * @category Array
 *
 * @param {Array} array - The array to sort and add
 * @param {String} sortBy - Name of the property from an array by which array will be sorted
 * @param {*} element - Element which is added into an array
  *
 * @returns {Array} Returns the new array
 *
 * @example
 *
 * famulus.sortAndAddFirstElement([{name:'Bob'}, {name:'Aron'}], 'name', {name:'All'})
 * // => [ {name:'All'}, {name:'Aron'}, {name:'Bob'} ]
 */
function sortAndAddFirstElement(array, sortBy, element) {
    return _(array)
        .sortBy(sortBy)
        .unshift(element)
        .value();
}

module.exports = sortAndAddFirstElement;


/**
 * Interface for building an object by configuration
 *
 * @customNeeds - have an interface for building an object based on configuration
 *
 * @since 2.1.0
 * @category Object
 *
 * @param {Array} config - Keys with configuration
 * [
 *   'key/value' - "OR" if no value, set value after "/",
 *   'key|this.firstName + " " + this.lastName' - set value from the expression after "|" which is bind to the passed object,
 *   'key:new Date()' - set value from the expression after ":"
 * ]
 *
 * @returns {Function}
 *
 * @example
 *
 * var email = objectInterface(['body', 'count/1', 'sender|this.firstName + " " + this.lastName', 'isRead: false', 'created: new Date()'])
 * // => function
 *
 * email({body: 'Hello world!', count: '', firstName: 'Vasyl', lastName: 'Stokolosa', another: ''})
 * // => {body: "Hello world!", count: 1, created: Mon Jul 09 2018 10:31:08, isRead: false, sender: "Vasyl Stokolosa"}
 */
function objectInterface(config) {
    return function(obj) {
        var result = {};

        for (var i = 0; i < config.length; i++) {
            var OR, NEXT, REAL;

            if ((OR = config[i].split('/')) && OR[1]) {
                result[OR[0]] = obj[OR[0]] || Function('return ' + OR[1])();
            }
            else if ((NEXT = config[i].split('|')) && NEXT[1]) {
                result[NEXT[0]] = Function('return ' + NEXT[1]).call(obj);
            }
            else if ((REAL = config[i].split(':')) && REAL[1]) {
                result[REAL[0]] = Function('return ' + REAL[1])();
            }
            else {
                result[config[i]] = obj[config[i]];
            }
        }

        return result;
    }
}

module.exports = objectInterface;


import { initializer } from 'knifecycle';
import HTTPError from 'yhttperror';
import statuses from 'statuses';

const DEFAULT_TIMEOUT = 30 * 1000;

function noop() {}
function createIncrementor(n = 0) {
  return function increment() {
    return n++ + '';
  };
}

/* Architecture Note #3: HTTP Transactions
The `httpTransaction` service create a new transaction
 for every single HTTP request incoming. It helps
 ensuring each request receives a response and avoid
 idle requests via a configurable timeout.

It is also a convenient abstraction of the actual
 request/response between the router and
 the NodeJS world. A common need is to fake the
 HTTP method for backward compatibility with old
 browsers/proxies by using the
 `X-HTTP-Method-Override` header.

You could simply do this by wrapping this service
 like so:
```js
import { initHTTPTransaction } from 'swagger-http-router';
import { wrapInitializer } from 'knifecycle/dist/util';

export const initHTTPTransactionWithMethodOverride =
  wrapInitializer(async (services, httpTransaction) => {
  return async (...args) => {
    const [request, transaction] = httpTransaction(...args);

    return [{
      ...request,
      method: request.headers['x-http-method-override'] ?
        request.headers['x-http-method-override'].toLowerCase() :
        request.method,
      headers: Object.keys(req.headers)
        .filter(
          headerName =>
          'x-http-method-override' === headerName
        )
        .reduce((newHeaders, headerName) => {}, {
          newHeaders[headerName] = req.headers[headerName];
          return newHeaders;
        })
    }, httpTransaction];
  };
}, initHTTPTransaction);
```
*/

export default initializer(
  {
    name: 'httpTransaction',
    type: 'service',
    inject: ['?TIMEOUT', '?TRANSACTIONS', 'log', 'time', 'delay', '?uniqueId'],
  },
  initHTTPTransaction,
);

/**
 * Instantiate the httpTransaction service
 * @param  {Object}     services
 * The services to inject
 * @param  {Number}     [services.TIMEOUT=30000]
 * A number indicating how many ms the transaction
 *  should take to complete before being cancelled.
 * @param  {Object}     [services.TRANSACTIONS={}]
 * A hash of every current transactions
 * @param  {Function}   services.time
 * A timing function
 * @param  {Object}     services.delay
 * A delaying service
 * @param  {Function}   [services.log]
 * A logging function
 * @param  {Function}   [services.uniqueId]
 * A function returning unique identifiers
 * @return {Promise<Function>}
 * A promise of the httpTransaction function
 * @example
 * import { initHTTPTransaction } from 'swagger-http-router';
 *
 * const httpTransaction = await initHTTPTransaction({
 *   log: console.log.bind(console),
 *   time: Date.now.bind(Date),
 * });
 */
function initHTTPTransaction({
  TIMEOUT = DEFAULT_TIMEOUT,
  TRANSACTIONS,
  log = noop,
  time,
  delay,
  uniqueId = createIncrementor(),
}) {
  // Not using default value to always
  // get an empty object here and avoid
  // conflicts between instances spawned
  // with defaults
  TRANSACTIONS = TRANSACTIONS || {};
  log('debug', 'HTTP Transaction initialized.');
  return Promise.resolve(httpTransaction);

  /**
   * Create a new HTTP transaction
   * @param  {HTTPRequest}  req
   * A raw NodeJS HTTP incoming message
   * @param  {HTTPResponse} res
   * A raw NodeJS HTTP response
   * @return {Array}
   * The normalized request and the HTTP
   * transaction created in an array.
   */
  function httpTransaction(req, res) {
    let initializationPromise;

    /* Architecture Note #3.1: New Transaction
    The idea is to maintain a hash of each pending
     transaction. To do so, we create a transaction
     object that contains useful informations about
     the transaction and we store it into the
     `TRANSACTIONS` hash.

    Each transaction has a unique id that is either
     generated or picked up in the `Transaction-Id`
     request header. This allows to trace
     transactions end to end with that unique id.
    */
    return Promise.resolve().then(() => {
      const request = {
        url: req.url,
        method: req.method.toLowerCase(),
        headers: req.headers,
        body: req,
      };
      const transaction = {
        protocol: req.connection.encrypted ? 'https' : 'http',
        ip:
          (req.headers['x-forwarded-for'] || '').split(',')[0] ||
          req.connection.remoteAddress,
        startInBytes: req.socket.bytesRead,
        startOutBytes: req.socket.bytesWritten,
        startTime: time(),
        url: req.url,
        method: req.method,
        reqHeaders: req.headers,
        errored: false,
      };
      const delayPromise = delay.create(TIMEOUT);
      let id = req.headers['transaction-id'] || uniqueId();

      // Handle bad client transaction ids
      if (TRANSACTIONS[id]) {
        initializationPromise = Promise.reject(
          new HTTPError(400, 'E_TRANSACTION_ID_NOT_UNIQUE', id),
        );
        id = uniqueId();
      } else {
        initializationPromise = Promise.resolve();
      }

      transaction.id = id;
      TRANSACTIONS[id] = transaction;

      return [
        request,
        {
          id,
          start: startTransaction.bind(
            null,
            { id, req, res, delayPromise },
            initializationPromise,
          ),
          catch: catchTransaction.bind(null, { id, req, res }),
          end: endTransaction.bind(null, { id, req, res, delayPromise }),
        },
      ];
    });
  }

  function startTransaction(
    { id, delayPromise },
    initializationPromise,
    buildResponse,
  ) {
    /* Architecture Note #3.2: Transaction start
    Once initiated, the transaction can be started. It
     basically spawns a promise that will be resolved
     to the actual response or rejected if the timeout
     is reached.
    */
    return Promise.race([
      initializationPromise.then(() => buildResponse()),
      delayPromise.then(() => {
        throw new HTTPError(504, 'E_TRANSACTION_TIMEOUT', TIMEOUT, id);
      }),
    ]);
  }

  function catchTransaction({ id, req }, err) {
    /* Architecture Note #3.3: Transaction errors
    Here we are simply casting and logging errors.
     It is important for debugging but also for
     ending the transaction properly if an error
     occurs.
    */
    err = HTTPError.cast(err, err.httpCode || 500);
    log('error', 'An error occured', {
      guruMeditation: id,
      request:
        TRANSACTIONS[id].protocol +
        '://' +
        (req.headers.host || 'localhost') +
        TRANSACTIONS[id].url,
      verb: req.method,
      status: err.httpCode,
      code: err.code,
      stack: err.stack,
      details: err.params,
    });

    TRANSACTIONS[id].errored = true;

    throw err;
  }

  function endTransaction({ id, req, res, delayPromise }, response) {
    /* Architecture Note #3.4: Transaction end
    We end the transaction by writing the final status
     and headers and piping the response body if any.

    The transaction can till error at that time but it
     is too late for changing the response status so
     we are just logging the event.
     This could be handled with
     [HTTP trailers](https://nodejs.org/api/http.html#http_response_addtrailers_headers)
     but the lack of client side support for now is
     preventing us to use them.

     Once terminated, the transaction is removed
      from the `TRANSACTIONS` hash.
    */
    return new Promise((resolve, reject) => {
      res.on('error', reject);
      res.on('finish', resolve);
      res.writeHead(
        response.status,
        statuses[response.status],
        Object.assign({}, response.headers, { 'Transaction-Id': id }),
      );
      if (response.body && response.body.pipe) {
        response.body.pipe(res);
      } else {
        res.end();
      }
    })
      .catch(err => {
        TRANSACTIONS[id].errored = true;
        log('error', 'An error occured', {
          guruMeditation: id,
          request:
            TRANSACTIONS[id].protocol +
            '://' +
            (req.headers.host || 'localhost') +
            TRANSACTIONS[id].url,
          method: req.method,
          stack: err.stack || err,
        });
      })
      .then(() => {
        TRANSACTIONS[id].endTime = time();
        TRANSACTIONS[id].endInBytes = req.socket.bytesRead;
        TRANSACTIONS[id].endOutBytes = req.socket.bytesWritten;
        TRANSACTIONS[id].statusCode = response.status;
        TRANSACTIONS[id].resHeaders = response.headers || {};

        log('info', TRANSACTIONS[id]);

        delete TRANSACTIONS[id];

        return delay.clear(delayPromise);
      });
  }
}


var dates = require('./.internal/dates');

/**
 * Difference between dates which are passed, in formats 'milliseconds', 'days', 'hours', 'minutes'
 *
 * @customNeeds -
 *
 * @since 2.0.0
 * @category Date
 *
 * @param {Date} date1 - The Date for compare
 * @param {Date} date2 - The Date for compare
 * @param {String} differenceType - [ 'days', 'hours', 'minutes', 'milliseconds', 'all' ]
 *
 * @returns {Number|Object} Returns the numeric value or object depends on passed differenceType param
 *
 * @example
 *
 * famulus.dateDifference(new Date('06-20-2018'), new Date('06-26-2018'), 'days')
 * // => 6
 *
 * famulus.dateDifference(new Date('06-20-2018'), new Date('06-26-2018'), 'hours')
 * // => 144
 *
 * famulus.dateDifference(new Date('06-20-2018'), new Date('06-26-2018'), 'minutes')
 * // => 8640
 *
 * famulus.dateDifference(new Date('06-26-2018'), new Date('06-20-2018'), 'milliseconds')
 * // => 518400000
 *
 * famulus.dateDifference(new Date('06-26-2018 10:10'), new Date('06-20-2018 08:00'), 'all')
 * // => {days: 6, hours: 2, minutes: 10, milliseconds: 526200000}
 */
function dateDifference(date1, date2, differenceType) {
    var diffMilliseconds = Math.abs(date1 - date2);

    switch(differenceType) {
        case 'days':
            return dates._getDaysDiff(diffMilliseconds);
        case 'hours':
            return dates._differenceInHours(diffMilliseconds);
        case 'minutes':
            return dates._differenceInMinutes(diffMilliseconds);
        case 'milliseconds':
            return diffMilliseconds;

        default:
            return {
                days: dates._getDaysDiff(diffMilliseconds),
                hours: dates._getHoursDiff(diffMilliseconds),
                minutes: dates._getMinutesDiff(diffMilliseconds),
                milliseconds: diffMilliseconds
            }
    }
}

module.exports = dateDifference;


import { initializer } from 'knifecycle';
import { DEFAULT_DEBUG_NODE_ENVS, DEFAULT_STRINGIFYERS } from './constants';

export default initializer(
  {
    name: 'errorHandler',
    type: 'service',
    inject: ['?ENV', '?DEBUG_NODE_ENVS', '?STRINGIFYERS'],
  },
  initErrorHandler,
);

/**
 * Initialize an error handler for the
 * HTTP router
 * @param  {Object}   services
 * The services the server depends on
 * @param  {Object}   [services.ENV]
 * The services the server depends on
 * @param  {Array}   [services.DEBUG_NODE_ENVS]
 * The environnement that activate debugging
 *  (prints stack trace in HTTP errors responses)
 * @param  {Object} [services.STRINGIFYERS]
 * The synchronous body stringifyers
 * @return {Promise}
 * A promise of a function to handle errors
 */
function initErrorHandler({
  ENV = {},
  DEBUG_NODE_ENVS = DEFAULT_DEBUG_NODE_ENVS,
  STRINGIFYERS = DEFAULT_STRINGIFYERS,
}) {
  return Promise.resolve(errorHandler);

  /**
   * Handle an HTTP transaction error and
   * map it to a serializable response
   * @param  {String}  transactionId
   * A raw NodeJS HTTP incoming message
   * @param  {Object} responseSpec
   * The response specification
   * @param  {HTTPError} err
   * The encountered error
   * @return {Promise}
   * A promise resolving when the operation
   *  completes
   */
  function errorHandler(transactionId, responseSpec, err) {
    return Promise.resolve().then(() => {
      const response = {};

      response.status = err.httpCode || 500;
      response.headers = Object.assign({}, err.headers || {}, {
        // Avoid caching errors
        'cache-control': 'private',
        // Fallback to the default stringifyer to always be
        // able to display errors
        'content-type':
          responseSpec &&
          responseSpec.contentTypes[0] &&
          STRINGIFYERS[responseSpec.contentTypes[0]]
            ? responseSpec.contentTypes[0]
            : Object.keys(STRINGIFYERS)[0],
      });

      response.body = {
        error: {
          code: err.code || 'E_UNEXPECTED',
          // Enjoy nerdy stuff:
          // https://en.wikipedia.org/wiki/Guru_Meditation
          guruMeditation: transactionId,
        },
      };

      if (ENV && DEBUG_NODE_ENVS.includes(ENV.NODE_ENV)) {
        response.body.error.stack = err.stack;
        response.body.error.params = err.params;
      }

      return response;
    });
  }
}


var dates = require('./.internal/dates');

/**
 * Difference between now and date which is passed, in formats 'milliseconds', 'days', 'hours', 'minutes'
 *
 * @customNeeds -
 *
 * @since 2.0.0
 * @category Date
 *
 * @param {Date} date - The Date to inspect
 * @param {String} differenceType - [ 'days', 'hours', 'minutes', 'milliseconds', 'all' ]
 *
 * @returns {Number|Object} Returns the numeric value or object depends on passed differenceType param
 *
 * @example
 * example result for now is new Date('12-26-2017')
 *
 * famulus.dateDifferenceFromNow(new Date('12-20-2017'), 'milliseconds')
 * // => 555261242
 *
 * famulus.dateDifferenceFromNow(new Date('12-20-2017'), 'days')
 * // => 6
 *
 * famulus.dateDifferenceFromNow(new Date('12-20-2017'), 'hours')
 * // => 156
 */
function dateDifferenceFromNow(date, differenceType) {
    var now = new Date(),
        diffMilliseconds = Math.abs(date - now);

    switch(differenceType) {
        case 'days':
            return dates._getDaysDiff(diffMilliseconds);
        case 'hours':
            return dates._differenceInHours(diffMilliseconds);
        case 'minutes':
            return dates._differenceInMinutes(diffMilliseconds);
        case 'milliseconds':
            return diffMilliseconds;

        default:
            return {
                days: dates._getDaysDiff(diffMilliseconds),
                hours: dates._getHoursDiff(diffMilliseconds),
                minutes: dates._getMinutesDiff(diffMilliseconds),
                milliseconds: diffMilliseconds
            }
    }
}

module.exports = dateDifferenceFromNow;


import bytes from 'bytes';
import Stream from 'stream';
import { initializer } from 'knifecycle';
import HTTPError from 'yhttperror';
import YError from 'yerror';
import Siso from 'siso';
import Ajv from 'ajv';
import strictQs from 'strict-qs';
import { flattenSwagger, getSwaggerOperations } from './utils';
import {
  prepareValidators,
  applyValidators,
  filterHeaders,
} from './validation';
import {
  extractBodySpec,
  extractResponseSpec,
  checkResponseCharset,
  checkResponseMediaType,
  executeHandler,
} from './lib';
import { getBody, sendBody } from './body';

const SEARCH_SEPARATOR = '?';

import {
  DEFAULT_DEBUG_NODE_ENVS,
  DEFAULT_BUFFER_LIMIT,
  DEFAULT_PARSERS,
  DEFAULT_STRINGIFYERS,
  DEFAULT_DECODERS,
  DEFAULT_ENCODERS,
} from './constants';

function noop() {}
function identity(x) {
  return x;
}

/* Architecture Note #2: HTTP Router
The `httpRouter` service is responsible for handling
 the request, validating it and wiring the handlers
 response to the actual HTTP response.

It is very opiniated and clearly diverges from the
 current standards based on a middlewares/plugins
 approach.

Here, the single source of truth is your API
 definition. No documentation, no route.
*/

export default initializer(
  {
    name: 'httpRouter',
    inject: [
      '?ENV',
      '?DEBUG_NODE_ENVS',
      '?BUFFER_LIMIT',
      'HANDLERS',
      'API',
      '?PARSERS',
      '?STRINGIFYERS',
      '?DECODERS',
      '?ENCODERS',
      '?QUERY_PARSER',
      '?log',
      'httpTransaction',
      'errorHandler',
    ],
    options: { singleton: true },
  },
  initHTTPRouter,
);

/**
 * Initialize an HTTP router
 * @param  {Object}   services
 * The services the server depends on
 * @param  {Object}   services.API
 * The Swagger definition of the API
 * @param  {Object}   services.HANDLERS
 * The handlers for the operations decribe
 *  by the Swagger API definition
 * @param  {Object}   [services.ENV]
 * The services the server depends on
 * @param  {Array}   [services.DEBUG_NODE_ENVS]
 * The environnement that activate debugging
 *  (prints stack trace in HTTP errors responses)
 * @param  {String}   [services.BUFFER_LIMIT]
 * The maximum bufferisation before parsing the
 *  request body
 * @param  {Object} [services.PARSERS]
 * The synchronous body parsers (for operations
 *  that defines a request body schema)
 * @param  {Object} [services.STRINGIFYERS]
 * The synchronous body stringifyers (for
 *  operations that defines a response body
 *  schema)
 * @param  {Object} [services.ENCODERS]
 * A map of encoder stream constructors
 * @param  {Object} [services.DECODERS]
 * A map of decoder stream constructors
 * @param  {Object} [services.QUERY_PARSER]
 * A query parser with the `strict-qs` signature
 * @param  {Function} [services.log=noop]
 * A logging function
 * @param  {Function} services.httpTransaction
 * A function to create a new HTTP transaction
 * @return {Promise}
 * A promise of a function to handle HTTP requests.
 */
function initHTTPRouter({
  ENV = {},
  DEBUG_NODE_ENVS = DEFAULT_DEBUG_NODE_ENVS,
  BUFFER_LIMIT = DEFAULT_BUFFER_LIMIT,
  HANDLERS,
  API,
  PARSERS = DEFAULT_PARSERS,
  STRINGIFYERS = DEFAULT_STRINGIFYERS,
  DECODERS = DEFAULT_DECODERS,
  ENCODERS = DEFAULT_ENCODERS,
  QUERY_PARSER = strictQs,
  log = noop,
  httpTransaction,
  errorHandler,
}) {
  const bufferLimit = bytes.parse(BUFFER_LIMIT);
  const ajv = new Ajv({
    verbose: ENV && DEBUG_NODE_ENVS.includes(ENV.NODE_ENV),
  });
  const consumableCharsets = Object.keys(DECODERS);
  const produceableCharsets = Object.keys(ENCODERS);
  const defaultResponseSpec = {
    contentTypes: Object.keys(STRINGIFYERS),
    charsets: produceableCharsets,
  };

  return flattenSwagger(API)
    .then(_createRouters.bind(null, { HANDLERS, ajv }))
    .then(routers => {
      let handleFatalError;
      log('debug', 'HTTP Router initialized.');

      return {
        service: httpRouter,
        fatalErrorPromise: {
          promise: new Promise((resolve, reject) => {
            handleFatalError = reject;
          }),
        },
      };

      /**
       * Handle an HTTP incoming message
       * @param  {HTTPRequest}  req
       * A raw NodeJS HTTP incoming message
       * @param  {HTTPResponse} res
       * A raw NodeJS HTTP response
       * @return {Promise}
       * A promise resolving when the operation
       *  completes
       */
      function httpRouter(req, res) {
        let operation;
        let responseSpec = defaultResponseSpec;

        return httpTransaction(req, res)
          .then(([request, transaction]) =>
            transaction
              .start(() =>
                Promise.resolve()
                  .then(() => {
                    const method = request.method;
                    const path = request.url.split(SEARCH_SEPARATOR)[0];
                    const search = request.url.substr(path.length);
                    const parts = path.split('/').filter(a => a);
                    let [result, pathParameters] = routers[method]
                      ? routers[method].find(parts)
                      : [];

                    // Second chance for HEAD calls
                    if (!result && 'head' === method) {
                      [result, pathParameters] = routers.get
                        ? routers.get.find(parts)
                        : [];
                    }

                    const { handler, operation: _operation_, validators } =
                      result || {};

                    if (!handler) {
                      log('debug', 'No handler found for: ', method, parts);
                      throw new HTTPError(404, 'E_NOT_FOUND', method, parts);
                    }

                    operation = _operation_;

                    return {
                      search,
                      pathParameters,
                      validators,
                      operation,
                      handler,
                    };
                  })
                  .then(
                    ({
                      search,
                      pathParameters,
                      validators,
                      operation,
                      handler,
                    }) => {
                      const consumableMediaTypes =
                        operation.consumes || API.consumes || [];
                      const produceableMediaTypes =
                        (operation && operation.produces) || API.produces || [];
                      const bodySpec = extractBodySpec(
                        request,
                        consumableMediaTypes,
                        consumableCharsets,
                      );
                      responseSpec = extractResponseSpec(
                        operation,
                        request,
                        produceableMediaTypes,
                        produceableCharsets,
                      );

                      return getBody(
                        {
                          DECODERS,
                          PARSERS,
                          bufferLimit,
                        },
                        operation,
                        request.body,
                        bodySpec,
                      )
                        .then(body =>
                          Object.assign(
                            body ? { body } : {},
                            pathParameters,
                            QUERY_PARSER(operation.parameters, search),
                            filterHeaders(
                              operation.parameters,
                              request.headers,
                            ),
                          ),
                        )
                        .then(parameters => {
                          applyValidators(operation, validators, parameters);
                          return parameters;
                        })
                        .catch(err => {
                          throw HTTPError.cast(err, 400);
                        })
                        .then(executeHandler.bind(null, operation, handler))
                        .then(response => {
                          if (response.body) {
                            response.headers['content-type'] =
                              response.headers['content-type'] ||
                              responseSpec.contentTypes[0];
                          }

                          // Check the stringifyer only when a schema is
                          // specified
                          const responseHasSchema =
                            operation.responses &&
                            operation.responses[response.status] &&
                            operation.responses[response.status].schema;

                          if (
                            responseHasSchema &&
                            !STRINGIFYERS[response.headers['content-type']]
                          ) {
                            return Promise.reject(
                              new HTTPError(
                                500,
                                'E_STRINGIFYER_LACK',
                                response.headers['content-type'],
                              ),
                            );
                          }
                          if (response.body) {
                            checkResponseCharset(
                              request,
                              responseSpec,
                              produceableCharsets,
                            );
                            checkResponseMediaType(
                              request,
                              responseSpec,
                              produceableMediaTypes,
                            );
                          }

                          return response;
                        });
                    },
                  ),
              )
              .catch(transaction.catch)
              .catch(errorHandler.bind(null, transaction.id, responseSpec))
              .then(response => {
                if (response.body && 'head' === request.method) {
                  log(
                    'warning',
                    'Body stripped:',
                    response.body instanceof Stream ? 'Stream' : response.body,
                  );
                  return Object.keys(response)
                    .filter(key => 'body' !== key)
                    .reduce((cleanedResponse, key) => {
                      cleanedResponse[key] = response[key];
                      return cleanedResponse;
                    }, {});
                }
                return response;
              })
              // Here sendBody is not binded since we need
              // the `operation` value at the exact moment
              // of the then stage execution
              .then(response =>
                sendBody(
                  {
                    DEBUG_NODE_ENVS,
                    ENV,
                    API,
                    ENCODERS,
                    STRINGIFYERS,
                    log,
                    ajv,
                  },
                  operation,
                  response,
                ),
              )
              .then(transaction.end),
          )
          .catch(handleFatalError);
      }
    });
}

function _explodePath(path, parameters) {
  return path
    .split('/')
    .filter(identity)
    .map(node => {
      const matches = /^{([a-z0-9]+)}$/i.exec(node);

      if (!matches) {
        return node;
      }

      const parameter = (parameters || []).find(
        aParameter => aParameter.name === matches[1],
      );

      if (!parameter) {
        throw new YError('E_UNDECLARED_PATH_PARAMETER', node);
      }
      return parameter;
    });
}

function _createRouters({ HANDLERS, ajv }, API) {
  const routers = {};

  return Promise.all(
    getSwaggerOperations(API).map(operation => {
      const { path, method, operationId, parameters } = operation;

      if (!path.startsWith('/')) {
        throw new YError('E_BAD_PATH', path);
      }
      if (!operationId) {
        throw new YError('E_NO_OPERATION_ID', path, method);
      }

      if (!HANDLERS[operationId]) {
        throw new YError('E_NO_HANDLER', operationId);
      }

      return Promise.resolve(HANDLERS[operationId]).then(handler => {
        routers[method] = routers[method] || new Siso();
        routers[method].register(
          _explodePath((API.basePath || '') + path, parameters),
          { handler, operation, validators: prepareValidators(ajv, operation) },
        );
      });
    }),
  ).then(() => routers);
}


'use strict';

import StreamReader from './lib/stream-reader';
import isAtHTMLTag from './lib/is-html';
import { isQuote } from './lib/quotes';

const code = ch => ch.charCodeAt(0);
const SQUARE_BRACE_L = code('[');
const SQUARE_BRACE_R = code(']');
const ROUND_BRACE_L  = code('(');
const ROUND_BRACE_R  = code(')');
const CURLY_BRACE_L  = code('{');
const CURLY_BRACE_R  = code('}');

const specialChars = new Set('#.*:$-_!@%^+>/'.split('').map(code));
const bracePairs = new Map()
	.set(SQUARE_BRACE_L, SQUARE_BRACE_R)
	.set(ROUND_BRACE_L,  ROUND_BRACE_R)
	.set(CURLY_BRACE_L,  CURLY_BRACE_R);

const defaultOptions = {
	syntax: 'markup',
	lookAhead: null,
	prefix: ''
};

/**
 * Extracts Emmet abbreviation from given string.
 * The goal of this module is to extract abbreviation from current editor’s line,
 * e.g. like this: `<span>.foo[title=bar|]</span>` -> `.foo[title=bar]`, where
 * `|` is a current caret position.
 * @param {String}  line A text line where abbreviation should be expanded
 * @param {Number}  [pos] Caret position in line. If not given, uses end-of-line
 * @param {Object}  [options]
 * @param {Boolean} [options.lookAhead] Allow parser to look ahead of `pos` index for
 * searching of missing abbreviation parts. Most editors automatically inserts
 * closing braces for `[`, `{` and `(`, which will most likely be right after
 * current caret position. So in order to properly expand abbreviation, user
 * must explicitly move caret right after auto-inserted braces. With this option
 * enabled, parser will search for closing braces right after `pos`. Default is `true`
 * @param {String} [options.syntax] Name of context syntax of expanded abbreviation.
 * Either 'markup' (default) or 'stylesheet'. In 'stylesheet' syntax, braces `[]`
 * and `{}` are not supported thus not extracted.
 * @param {String} [options.prefix] A string that should precede abbreviation in
 * order to make it successfully extracted. If given, the abbreviation will be
 * extracted from the nearest `prefix` occurrence.
 * @return {Object} Object with `abbreviation` and its `location` in given line
 * if abbreviation can be extracted, `null` otherwise
 */
export default function extractAbbreviation(line, pos, options) {
	// make sure `pos` is within line range
	pos = Math.min(line.length, Math.max(0, pos == null ? line.length : pos));

	if (typeof options === 'boolean') {
		options = Object.assign({}, defaultOptions, { lookAhead: options });
	} else {
		options = Object.assign({}, defaultOptions, options);
	}

	if (options.lookAhead == null || options.lookAhead === true) {
		pos = offsetPastAutoClosed(line, pos, options);
	}

	let c;
	const start = getStartOffset(line, pos, options.prefix);
	if (start === -1) {
		return null;
	}

	const stream = new StreamReader(line, start);
	stream.pos = pos;
	const stack = [];

	while (!stream.sol()) {
		c = stream.peek();

		if (isCloseBrace(c, options.syntax)) {
			stack.push(c);
		} else if (isOpenBrace(c, options.syntax)) {
			if (stack.pop() !== bracePairs.get(c)) {
				// unexpected brace
				break;
			}
		} else if (has(stack, SQUARE_BRACE_R) || has(stack, CURLY_BRACE_R)) {
			// respect all characters inside attribute sets or text nodes
			stream.pos--;
			continue;
		} else if (isAtHTMLTag(stream) || !isAbbreviation(c)) {
			break;
		}

		stream.pos--;
	}

	if (!stack.length && stream.pos !== pos) {
		// found something, remove some invalid symbols from the
		// beginning and return abbreviation
		const abbreviation = line.slice(stream.pos, pos).replace(/^[*+>^]+/, '');
		return {
			abbreviation,
			location: pos - abbreviation.length,
			start: options.prefix
				? start - options.prefix.length
				: pos - abbreviation.length,
			end: pos
		};
	}
}

/**
 * Returns new `line` index which is right after characters beyound `pos` that
 * editor will likely automatically close, e.g. }, ], and quotes
 * @param {String} line
 * @param {Number} pos
 * @return {Number}
 */
function offsetPastAutoClosed(line, pos, options) {
	// closing quote is allowed only as a next character
	if (isQuote(line.charCodeAt(pos))) {
		pos++;
	}

	// offset pointer until non-autoclosed character is found
	while (isCloseBrace(line.charCodeAt(pos), options.syntax)) {
		pos++;
	}

	return pos;
}

/**
 * Returns start offset (left limit) in `line` where we should stop looking for
 * abbreviation: it’s nearest to `pos` location of `prefix` token
 * @param {String} line
 * @param {Number} pos
 * @param {String} prefix
 * @return {Number}
 */
function getStartOffset(line, pos, prefix) {
	if (!prefix) {
		return 0;
	}

	const stream = new StreamReader(line);
	const compiledPrefix = String(prefix).split('').map(code);
	stream.pos = pos;
	let result;

	while (!stream.sol()) {
		if (consumePair(stream, SQUARE_BRACE_R, SQUARE_BRACE_L) || consumePair(stream, CURLY_BRACE_R, CURLY_BRACE_L)) {
			continue;
		}

		result = stream.pos;
		if (consumeArray(stream, compiledPrefix)) {
			return result;
		}

		stream.pos--;
	}

	return -1;
}

/**
 * Consumes full character pair, if possible
 * @param {StreamReader} stream
 * @param {Number} close
 * @param {Number} open
 * @return {Boolean}
 */
function consumePair(stream, close, open) {
	const start = stream.pos;
	if (stream.eat(close)) {
		while (!stream.sol()) {
			if (stream.eat(open)) {
				return true;
			}

			stream.pos--;
		}
	}

	stream.pos = start;
	return false;
}

/**
 * Consumes all character codes from given array, right-to-left, if possible
 * @param {StreamReader} stream
 * @param {Number[]} arr
 */
function consumeArray(stream, arr) {
	const start = stream.pos;
	let consumed = false;

	for (let i = arr.length - 1; i >= 0 && !stream.sol(); i--) {
		if (!stream.eat(arr[i])) {
			break;
		}

		consumed = i === 0;
	}

	if (!consumed) {
		stream.pos = start;
	}

	return consumed;
}

function has(arr, value) {
	return arr.indexOf(value) !== -1;
}

function isAbbreviation(c) {
	return (c > 64 && c < 91)   // uppercase letter
		|| (c > 96 && c < 123)  // lowercase letter
		|| (c > 47 && c < 58)   // number
		|| specialChars.has(c); // special character
}

function isOpenBrace(c, syntax) {
	return c === ROUND_BRACE_L || (syntax === 'markup' && (c === SQUARE_BRACE_L || c === CURLY_BRACE_L));
}

function isCloseBrace(c, syntax) {
	return c === ROUND_BRACE_R || (syntax === 'markup' && (c === SQUARE_BRACE_R || c === CURLY_BRACE_R));
}


'use strict';

import { isQuote, eatQuoted } from './quotes';

const TAB         = 9;
const SPACE       = 32;
const DASH        = 45; // -
const SLASH       = 47; // /
const COLON       = 58; // :
const EQUALS      = 61; // =
const ANGLE_LEFT  = 60; // <
const ANGLE_RIGHT = 62; // >

/**
 * Check if given reader’s current position points at the end of HTML tag
 * @param  {StreamReader} stream
 * @return {Boolean}
 */
export default function (stream) {
	const start = stream.pos;

	if (!stream.eat(ANGLE_RIGHT)) {
		return false;
	}

	let ok = false;
	stream.eat(SLASH); // possibly self-closed element

	while (!stream.sol()) {
		stream.eatWhile(isWhiteSpace);

		if (eatIdent(stream)) {
			// ate identifier: could be a tag name, boolean attribute or unquoted
			// attribute value
			if (stream.eat(SLASH)) {
				// either closing tag or invalid tag
				ok = stream.eat(ANGLE_LEFT);
				break;
			} else if (stream.eat(ANGLE_LEFT)) {
				// opening tag
				ok = true;
				break;
			} else if (stream.eat(isWhiteSpace)) {
				// boolean attribute
				continue;
			} else if (stream.eat(EQUALS)) {
				// simple unquoted value or invalid attribute
				if (eatIdent(stream)) {
					continue;
				}
				break;
			} else if (eatAttributeWithUnquotedValue(stream)) {
				// identifier was a part of unquoted value
				ok = true;
				break;
			}

			// invalid tag
			break;
		}

		if (eatAttribute(stream)) {
			continue;
		}

		break;
	}

	stream.pos = start;
	return ok;
}

/**
 * Eats HTML attribute from given string.
 * @param  {StreamReader} state
 * @return {Boolean}       `true` if attribute was consumed.
 */
function eatAttribute(stream) {
	return eatAttributeWithQuotedValue(stream) || eatAttributeWithUnquotedValue(stream);
}

/**
 * @param  {StreamReader} stream
 * @return {Boolean}
 */
function eatAttributeWithQuotedValue(stream) {
	const start = stream.pos;
	if (eatQuoted(stream) && stream.eat(EQUALS) && eatIdent(stream)) {
		return true;
	}

	stream.pos = start;
	return false;
}

/**
 * @param  {StreamReader} stream
 * @return {Boolean}
 */
function eatAttributeWithUnquotedValue(stream) {
	const start = stream.pos;
	if (stream.eatWhile(isUnquotedValue) && stream.eat(EQUALS) && eatIdent(stream)) {
		return true;
	}

	stream.pos = start;
	return false;
}

/**
 * Eats HTML identifier from stream
 * @param  {StreamReader} stream
 * @return {Boolean}
 */
function eatIdent(stream) {
	return stream.eatWhile(isIdent);
}

/**
 * Check if given character code belongs to HTML identifier
 * @param  {Number}  c
 * @return {Boolean}
 */
function isIdent(c) {
	return c === COLON || c === DASH || isAlpha(c) || isNumber(c);
}

/**
 * Check if given character code is alpha code (letter though A to Z)
 * @param  {Number}  c
 * @return {Boolean}
 */
function isAlpha(c) {
	c &= ~32; // quick hack to convert any char code to uppercase char code
	return c >= 65 && c <= 90; // A-Z
}

/**
 * Check if given code is a number
 * @param  {Number}  c
 * @return {Boolean}
 */
function isNumber(c) {
	return c > 47 && c < 58;
}

/**
 * Check if given code is a whitespace
 * @param  {Number}  c
 * @return {Boolean}
 */
function isWhiteSpace(c) {
	return c === SPACE || c === TAB;
}

/**
 * Check if given code may belong to unquoted attribute value
 * @param  {Number}  c
 * @return {Boolean}
 */
function isUnquotedValue(c) {
	return c && c !== EQUALS && !isWhiteSpace(c) && !isQuote(c);
}


/**
 * @copyright 2017-present, Charlike Mike Reagent <olsten.larck@gmail.com>
 * @license Apache-2.0
 */

const execa = require('execa-pro')
const arrayify = require('arrayify')
const resolvePlugins = require('resolve-plugins-sync')
const prettyConfig = require('@tunnckocore/pretty-config')

module.exports = { hela, exec, shell }

/**
 * > Controls, merges and resolves all tasks from config files
 * > and passed through `opts.tasks`.
 * >
 * > All `opts` are passed to [execa-pro][] and to [execa][],
 * > so you can pass `opts.stdio: 'inherit'` for example
 * > to output the result of each command in the console, useful for things like prompt inputs.
 * > Resolving works recursively and support ESLint style presets through
 * > the `opts.extends`. The `extends` property can be `string` (the name of the preset,
 * > prefixed with `hela-config-`),  a function (that is passed with `{ extends, tasks }` object)
 * > or an object containing another `extends` and/or `tasks` properties.
 * >
 * > Configuration is handled by [@tunnckocore/pretty-config](https://github.com/tunnckoCore/pretty-config)
 * > which is pretty similar to the [cosmiconfig][] package and so the config
 * > files lookup order is:
 * > - `.helarc.{json,yaml,yml,js}`
 * > - `hela.config.js`
 * > - `.hela.config.js`
 * > - `.helarc` - YAML or JSON syntax
 * > - `package.json` - one of `hela`, `helaConfig` or `config.hela` fields
 *
 * @name    .hela
 * @param   {Object} `opts` requires to have at least `tasks` ot `extends` properties
 * @returns {Promise} so you can use `async/await` mechanism
 * @api public
 */

async function hela (opts) {
  const options = Object.assign(
    { argv: {}, prefix: 'hela-config-', stdio: 'inherit' },
    opts
  )

  if (options.tasks || (options.presets || options.extends)) {
    return presetResolver(options)
  }

  return prettyConfig('hela', options).then((config) => {
    if (!config) {
      throw new Error('hela: no config')
    }

    return presetResolver(Object.assign({}, config, options))
  })
}

/**
 * > A mirror of [execa-pro][]'s `.exec` method, see its docs. This is also
 * included in the object that is passed to each task if the task is a function.
 *
 * @name    .exec
 * @param   {string|Array} `cmds` commands to call, if array of strings executes in series
 * @param   {Object} `options` optional, passed to [execa-pro][] and so to [execa][]
 * @returns {Promise} so you can use `async/await` mechanism
 * @api public
 */

async function exec (cmds, options) {
  return execa.exec(cmds, options)
}

/**
 * > A mirror of [execa-pro][]'s `.shell` method, see its docs. This is also
 * included in the object that is passed to each task if the task is a function.
 *
 * @name    .shell
 * @param   {string|Array} `cmds` commands to call, if array of strings executes in series
 * @param   {Object} `options` optional, passed to [execa-pro][] and so to [execa][]
 * @returns {Promise} so you can use `async/await` mechanism
 * @api public
 */

async function shell (cmds, options) {
  return execa.shell(cmds, options)
}

/**
 * Utils
 */

function factory (type, opts) {
  return (cmds, options) => {
    const cmd = { exec: execa.exec, shell: execa.shell }
    return cmd[type](cmds, Object.assign({}, opts, options))
  }
}

// todo: externalize as `hela-resolver`?
function presetResolver (opts) {
  const presets = arrayify(opts.presets || opts.extends)

  if (presets.length > 0) {
    const arg = Object.assign({}, opts)
    const options = Object.assign({ first: arg }, opts)
    const tasks = resolvePlugins(presets, options).reduce(
      (acc, preset) => presetReducer(acc, preset),
      {}
    )

    return transformTasks(opts, Object.assign({}, tasks, opts.tasks))
  }

  return transformTasks(opts, opts.tasks)
}

function presetReducer (acc, preset) {
  return preset.presets || preset.extends
    ? presetResolver(preset)
    : Object.assign({}, acc, preset.tasks || preset)
}

function transformTasks (opts, tasks) {
  return Object.keys(Object.assign({}, tasks))
    .filter(isValidTask)
    .reduce(taskReducer(opts, tasks), {})
}

function isValidTask (val) {
  return typeof val === 'string' || typeof val === 'function' || Array.isArray(val)
}

function taskReducer (opts, tasks) {
  return (acc, name) => {
    const task = tasks[name]

    if (typeof task === 'string' || Array.isArray(task)) {
      acc[name] = async () => shell(task, opts)
    }
    if (typeof task === 'function') {
      const helpers = {
        hela,
        exec: factory('exec', opts),
        shell: factory('shell', opts),
        helaExec: factory('exec', opts),
        helaShell: factory('shell', opts),
      }

      const options = Object.assign({}, opts, helpers)

      acc[name] = async () => task(options)
    }

    return acc
  }
}


#!/usr/bin/env node

/**
 * @copyright 2017-present, Charlike Mike Reagent <olsten.larck@gmail.com>
 * @license Apache-2.0
 */

/* eslint-disable no-console */

const fs = require('fs')
const path = require('path')
const util = require('util')
const parser = require('mri')
const { hela } = require('./index.js')

const options = { cwd: process.cwd() }

options.parse = (opts) => parser(process.argv.slice(2), opts)
options.argv = Object.assign({ silent: true }, options.parse())

const [taskName] = options.argv._
options.taskName = taskName

if (!options.argv._.length) {
  console.log('Usage: hela <taskName>')
  process.exit(1)
}

const readJson = async (fp) => {
  const pkgStr = await util.promisify(fs.readFile)(fp, 'utf8')

  return JSON.parse(pkgStr)
}

// const slientSpecials = (er, name) => {
//   // Don't show stack/message
//   // if it is `nyc check-coverage` command,
//   // because it is already show that threshold is not meet.
//   // if it is `eslint` because it show the report
//   const isEslint = er.message.includes('eslint')
//   const isNyc = er.message.includes('nyc')
//   const isCov = er.message.includes('check-coverage')

//   if (!isNyc && !isCov && !isEslint) {
//     console.error(`hela: task "${name}" failed`)
//     console.error(er.stack || er.message)
//   }
// }

async function run () {
  const pkg = await readJson(path.join(options.cwd, 'package.json'))
  const tasks = await hela({ pkg, ...options })
  const name = options.taskName

  if (Object.keys(tasks).length === 0) {
    throw new Error('hela: no tasks')
  }

  const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
  if (!hasOwn(tasks, name)) {
    throw new Error(`hela: no such task -> ${name}`)
  }

  return tasks[name]()
}

run().catch((er) => {
  console.error('hela: task failed')

  if (options.argv.silent === false) {
    console.error(er.stack || er.message)
  }

  process.exit(er.code)
})


import debug from 'debug'
import vow from 'vow'
import matter from 'gray-matter'
import readline from 'readline'
import google from 'googleapis'
import GoogleAuth from 'google-auth-library'
import config from 'config'
import hjson from 'hjson'
import {
  Readable,
  Writable
} from 'stream'
import {
  join
} from 'path'
import {
  FileCache,
  ValueCache,
  init as initCache,
  save as saveCache
} from 'metalsmith-cache'

let fileCache = false
let valueCache = false

initCache().then(() => {
  fileCache = new FileCache('google-drive')
  valueCache = new ValueCache('google-drive-params')
})

const dbg = debug('metalsmith-google-drive')
const drive = google.drive('v3')
let oauth

/**
 * ### default
 *
 * see README.md re: auth properties
 *
 * @param {Object} options
 * @param {String} options.src google drive parent id folder
 * @param {String} options.dest path under which to place files for metalsmith
 * @param {Object} options.auth
 * @param {String} options.auth.client_id
 * @param {String} options.auth.client_secret
 * @param {Array} options.auth.redirect_uris
 */
function plugin (options) {
  if (!options) throw new Error('no options passed')
  if (!options.src) throw new Error('required: options.src')
  if (!options.dest) throw new Error('required: options.dest')
  if (!options.auth) throw new Error('required: options.auth')
  // if (options.cache !== undefined) cache = options.cache
  if (options.invalidateCache) {
    initCache().then(() => {
      dbg('invalidating cache')
      fileCache.collection.clear()
      valueCache.collection.clear()
    })
  }
  const folder = new Folder(options)
  return folder.googleDrive.bind(folder)
}

class Folder {
  constructor (options) {
    Object.assign(this, options)
  }
  googleDrive (files, metalsmith) {
    // enable debug-ui
    debug.log = metalsmith.log || debug.log
    this.files = files
    this.metalsmith = metalsmith
    return vow.resolve()
    .then(() => initCache())
    .then(() => _doAuth(this.auth))
    .then(() => this.scrape())
    .then(() => this.mergeStore())
    .then(() => valueCache.store(this.src, new Date().toISOString()))
    .then(() => saveCache())
    .then(() => vow.resolve(this.files))
    .catch((err) => {
      if (err === 'skip') return dbg('skipped scrape')
      dbg(err)
    })
  }
  /**
   * ### _scrape
   *
   * scrape files from google drive
   *
   * nested highland stream...
   *
   *  * without backpressure a stream will just push everything into a slow
   *    consumer which is kind of the antithesis of a stream (a lake?)
   *  * the calls to drive api do not have any inherant backpressure so it needs
   *    to be created artificially. highlands `throttle` or `ratelimit` are based
   *    on ms, so that would just be an arbitrary limit uneffected by how fast the
   *    api is running. `parallel` allows to have x concurrent requests.
   *  * `parallel` only works on a stream of streams, that means if you want
   *    `generator().map( ... ).parallel(5)` then map *must* return a stream. In
   *    this case the stream returned contains only a single promise. Think of it
   *    more like a promise decorated with the highland api rather than an actual
   *    stream.
   *
   * @param {string} src google drive parent folder id
   */
  scrape () {
    dbg('scrape', this.src)
    const defer = vow.defer()
    const stream = this.streamFiles()
    stream.on('error', defer.reject.bind(defer))
    const worker = this.processDownloadedFiles()
    worker.on('finish', defer.resolve.bind(defer))
    worker.on('error', defer.reject.bind(defer))

    // now kiss!
    stream.pipe(worker)

    return defer.promise()
  }
  processDownloadedFiles () {
    const folder = this
    return Object.assign(
      new Writable({
        objectMode: true,
        highWaterMark: 60
      }),
      {
        concurrency: 5, // configurable here
        workers: 0, // don't change this
        /**
         * ## _write
         * this is where the parallel stream magic happens.. pay attention
         * each time `_write` is called, we either call `next` immediately (if
         * we haven't yet reached concurrency limit) or we call `next` once the
         * worker has finished.
         *
         * @param {Object} file pushed from streamFiles
         * @param {String} encoding ignored
         * @param {Function} next call when ready for subsequent worker to start
         */
        _write: function _write (file, encoding, next) {
          this.workers++
          vow.resolve(file)
          .then((file) => folder.downloadFile(file))
          .then((file) => folder.frontMatter(file))
          .then((file) => folder.storeFile(file))
          .catch(dbg)
          .then(() => {
            this.workers--
            next()
          })

          // if not at concurrency limit, call next immediately (don't wait for
          // worker to finish)
          if (this.workers < this.concurrency) {
            next()
            next = () => {}
          }
        }
      }
    )
  }
  /**
   * ### streamFiles
   *
   * some rad highland stream magic!
   *
   *  * the highland constructor is passed a function which will be called every
   *    time the stream is ready for more data
   *  * the two args passed to this fn are to `push` (queue?) data into the stream
   *    and to `next` when there's a good opportunity to do something else to
   *    consume the data
   *  * so basically every time the stream has capacity, we will request a page
   *    from the API and push each file onto the stream. if there's no
   *    `nextPageToken` then we notify the stream that there's no more.
   *
   * @param {String} parent id of parent folder
   */
  streamFiles () {
    const folder = this
    return Object.assign(
      new Readable({
        objectMode: true,
        highWaterMark: 60
      }),
      {
        // buffer docs ready to be pushed to outbound stream buffer
        toPush: [],
        // issued by google api for paged requests
        pageToken: false,
        // flag to ensure we don't request again if already awaiting request
        // and `_read` is called
        requested: false,
        // flag for whether EOF has been reached
        exhausted: false,
        // only request changes since `lastRun`
        lastRun: valueCache.retrieve(folder.src),
        count: 0,
        _read: function _read () {
          this._next()
        },
        _next: function _next () {
          // if our toPush is empty, request more
          if (!this.toPush.length) return this._request()
          // push oldest item
          const oldest = this.toPush.shift()
          if (this.push(oldest)) {
            // then go again if this.push returned truthy
            return process.nextTick(this._next.bind(this))
          }
        },
        _request: function _request () {
          // block additional requests
          if (this.requested) return
          if (this.exhausted) return
          this.requested = true
          const request = {}
          request.auth = oauth
          request.fields = 'files(id, name, mimeType, trashed)'
          request.q = [
            `mimeType != "application/vnd.google-apps.folder"`,
            ` and "${folder.src}" in parents`,
            this.lastRun ? ` and modifiedTime > "${this.lastRun}"` : ''
          ].join('')
          if (this.pageToken) request.pageToken = this.pageToken
          // async api call, no need to promisify because we bat for both teams ?
          drive.files.list(request, (err, result) => {
            if (err) throw new Error(err)
            this.count += result.files.length
            result.files.forEach((file) => {
              // deal with deleted files
              if (file.trashed) {
                dbg(`ignoring trashed ${file.name}`)
                this.count -= 1
                return fileCache.remove(join(folder.dest, file.id))
              }
              this.toPush.push(file)
            })
            if (!result.nextPageToken) {
              // end of stream
              dbg(`${this.count} files to be updated`)
              this.toPush.push(null)
              this.exhausted = true
            } else {
              // indicate that more values can be retrieved
              this.pageToken = result.nextPageToken
            }
            this._next()
            this.requested = false
          })
        }
      }
    )
  }
  /**
   * ### downloadFile
   *
   * @param {Object} file as streadmed from _streamFiles
   */
  downloadFile (file) {
    dbg(`downloading ${file.name}`)

    const defer = vow.defer()
    const chunks = []
    drive.files.get({
      auth: oauth,
      fileId: file.id,
      // `alt: 'media'` triggers file download rather than just meta
      alt: 'media'
    })
    .on('data', (chunk) => {
      chunks.push(chunk)
    })
    .on('end', () => {
      file.contents = Buffer.concat(chunks)
      defer.resolve(file)
    })
    .on('error', (err) => defer.reject(err))
    return defer.promise()
  }
  /**
   * ### frontMatter
   *
   * @param {Object} file
   */
  frontMatter (file) {
    if (!/^text/.exec(file.mimeType)) return file
    let meta = matter(file.contents.toString('utf8'))
    if (Object.keys(meta).length === 0) {
      return vow.reject(`no front matter? ${file.name}`)
    }
    Object.assign(
      file,
      { contents: meta.content },
      meta.data
    )
    return file
  }
  /**
   * ### storeFile
   *
   * store file in persist cache with drive id as key
   *
   * @param {Object} file
   */
  storeFile (file) {
    fileCache.store(join(this.dest, file.name), file)
    return file
  }
  /**
   * ### _getFiles
   *
   * this gets files from local persistent store, *not* from drive
   *
   * @param {String} dest path under which to store files
   */
  mergeStore () {
    const files = fileCache.match(`${this.dest}/**/*`)
    const count = Object.keys(files).length
    dbg(`merging ${count} tracked files from ${this.src}`)
    Object.assign(this.files, files)
  }
}

/**
 * ### _doAuth
 *
 * set up the API auth structure, retrieve cached token or request one
 *
 * __ tokens __
 *  * this token lasts until invalidated.
 *
 * @param {Object} auth see properties passed to googleAuth.OAuth2
 */
function _doAuth (auth) {
  if (oauth) return vow.resolve()
  dbg('doing oAuth2')
  const googleAuth = new GoogleAuth()
  oauth = new googleAuth.OAuth2(
    auth.client_id,
    auth.client_secret,
    [
      'urn:ietf:wg:oauth:2.0:oob',
      'http://localhost'
    ]
  )
  return vow.resolve()
  .then(() => {
    // see if token has been recorded in config
    let configToken = config.get('metalsmith-google-drive').token
    // no idea why google auth needs the token to be writable, but whatever
    if (configToken) configToken = config.util.cloneDeep(configToken)
    // fall back to cache (params), then do token flow
    return configToken || valueCache.retrieve('token') || _tokenFlow()
  })
  .then((token) => {
    oauth.credentials = token
  })
  .catch((err) => {
    // if some failure occurred invalidate oauth object so we don't simply
    // try to use it next time.
    oauth = false
    return vow.reject(err)
  })
}

/**
 * ### _tokenFlow
 *
 * usually this auth flow happens in your browser, but here we do it via CLI
 * this fn prints the auth url, which gives the user a code to paste back in
 */
function _tokenFlow () {
  const defer = vow.defer()
  let prompt
  const authUrl = oauth.generateAuthUrl({
    access_type: 'offline',
    scope: ['https://www.googleapis.com/auth/drive.readonly']
  })
  console.log('authorise metalsmith-google-drive by visiting: ', authUrl)
  prompt = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  })
  prompt.question(
    'Enter the code from that page here (or "ok" to skip scrape): ',
    (code) => {
      prompt.close()
      if (code === 'ok') return defer.reject('skip')
      oauth.getToken(code, (err, result) => {
        if (err) {
          dbg(err)
          defer.reject(err)
        } else {
          console.log('---------- snip ----------')
          console.log(hjson.stringify(
            { 'metalsmith-google-drive': { token: result } },
            { separator: true, spaces: 2, bracesSameLine: true, quotes: 'all' }
          ))
          console.log('---------- snip ----------')
          console.log('this token is cached automatically, but you can store')
          console.log('it in a config file like config/local.js if you want.')
          valueCache.store('token', result)
          defer.resolve(result)
        }
      })
    }
  )
  return defer.promise()
}

export default plugin
export {
  plugin
}


var extend = require('xtend')
var vdom = require('virtual-dom')
var createApp = require('../index')
var h = vdom.h

/*
* create the app passing in virtual-dom
*/
var app = createApp(vdom)

/*
* The only way to modify state is to trigger an action
* the modifer function is where you change state based on the type of an action
*/
function modifier (action, state) {
  if (action.type === 'example') {
    return extend(state, { example: true })
  } else if (action.type === 'title') {
    return extend(state, { title: action.title })
  }
}

/*
* Start the application with the modifier function and the initial state as args
* `app.start()` returns the `render()` function that's used to render your virtual tree
*/
var render = app.start(modifier, {
  example: false
})

/*
* return the tree of your app for rendering
* this returns a real dom tree that can be appended to your web page
*/
var domTree = render(function (state) {
  return h('.app', [
    h('h1', state.title),
    h('label', 'Write a new title: '),
    h('input', {
      type: 'text',
      placeholder: state.title,
      oninput: function (e) {
        app.store({ type: 'title', title: e.target.value })
      }
    })
  ])
})

/*
* get the new state every time an action updates the state
*/
app.on('*', function (action, state, oldState) {
  console.log('action happened so here is the new state:', state)
})

/*
* listen to only specific action types
*/
app.on('title', function (action, state, oldState) {
  console.log('state has a new title:', state.title)
})

/*
* trigger an action using `app.store()`
*/
app.store({
  type: 'example'
})

/*
* action objects must have a `type` property, and can have any other arbitrary properties
*/
app.store({
  type: 'title',
  title: 'awesome example'
})

/*
* append the dom tree to the page
*/

document.body.appendChild(domTree)


var data = require("gulp-data");
var optional = require("optional");
var path = require("path");

/**
 * Load JSON data from a companion file and store in file data.
 *
 * @param {Object} file Vinyl File object
 * @return JSON data
 */
function json(file) {
  var filename = path.basename(file.path, path.extname(file.path)) + ".json";
  return optional(path.join(path.dirname(file.path), filename)) || {};
}

module.exports = function() {
  return data(json);
};


/**
 * Our `Elevator` implementation implements
 * 3 states :
 *  - `open`: the elevator doors are opened
 *  - `closed`: the elevator doors are closed
 *  - `moving`: the elevator is moving towards a level
 *
 * It exposes an external `goToLevel` method used to
 * post a user request for the elevator to go to the
 * given level.
 */
const Elevator = function () {
  const self = this;
  const TIME_PER_LEVEL = 1 * 1000;
  this.level = 0;
  this.stack = [];
  this.fsm = new Fsm();

  /**
   * The elevator is in a stationary state
   * and its doors are opened.
   */
  const open = new Fsm.State({
    fsm: self.fsm,
    name: 'open',
    onEntry: function () {
      const state = this;

      console.log('Door opened at level', self.level);
      setTimeout(function () {
        state.transitionTo(closed);
      }, 2000);
    },

    /**
     * As the elevator's doors are currently opened
     * we push any user request to go to
     * a level on the level stack.
     */
    onEvent: function (event) {
      if (event.name === 'goToLevel'
        && event.level !== self.level) {
        self.pushLevel(event.level);
      }
    }
  });

  /**
   * The elevator is in a stationary state
   * and its doors are closed.
   */
  const closed = new Fsm.State({
    fsm: self.fsm,
    name: 'closed',
    onEntry: function () {
      console.log('Door closed');
      // If there is a channel in the stack,
      // we move to that channel.
      if (self.stack[0]) {
        this.transitionTo(moving);
      }
    },

    /**
     * When the elevator's doors are closed,
     * we wait for a request to move to another
     * level.
     */
    onEvent: function (event) {
      if (event.name === 'goToLevel') {
        if (event.level === self.level) {
          this.transitionTo(open);
        } else {
          self.pushLevel(event.level);
          this.transitionTo(moving);
        }
      }
    }
  });

  /**
   * The elevator is currently moving from a
   * level to another.
   */
  const moving = new Fsm.State({
    fsm: self.fsm,
    name: 'moving',

    onEntry: function () {
      const state = this;
      const next  = self.stack.shift();

      console.log('Moving to level', next);
      setTimeout(function () {
        console.log('Reached level', next);
        self.level = next;
        state.transitionTo(open);
      }, TIME_PER_LEVEL * Math.abs(next - self.level));
    },

    /**
     * As the elevator is currently moving and
     * cannot change direction nor open the
     * doors, we push any user request to go to
     * a level on the level stack.
     */
    onEvent: function (event) {
      if (event.name === 'goToLevel') {
        self.pushLevel(event.level);
      }
    }
  });

  // Starting the elevator in the `closed` state.
  this.fsm.start(closed);
};

/**
 * States whether the elevator is already scheduled
 * to go to the given level.
 */
Elevator.prototype.isduplicate = function (level) {
  for (let i = 0; i < this.stack.length; ++i) {
    if (this.stack[i] === level) {
      return (true);
    }
  }
  return (false);
};

/**
 * Schedules the elevator to go to the given level,
 * if it is not already.
 */
Elevator.prototype.pushLevel = function (level) {
  if (!this.isduplicate(level)) {
      this.stack.push(level);
  }
};

/**
 * Requests the elevator to go to the
 * given level.
 */
Elevator.prototype.goToLevel = function (level) {
   this.fsm.postEvent({ name: 'goToLevel', level: level });
};

// Creating a new `Elevator` instance.
const elevator = new Elevator();

// Sending level inputs to the `Elevator`.
elevator.goToLevel(1);
elevator.goToLevel(5);


const crypto = require("crypto");
const { constantTimeCompare } = require("./timing.js");

const ENC_ALGORITHM_CBC = "aes-256-cbc";
const ENC_ALGORITHM_GCM = "aes-256-gcm";
const HMAC_ALGORITHM = "sha256";
const HMAC_KEY_SIZE = 32;
const PASSWORD_KEY_SIZE = 32;

/**
 * Encrypted content components
 * @typedef {Object} EncryptedComponents
 * @property {String} content - The encrypted string
 * @property {String} iv - The IV in hex form
 * @property {String} salt - The salt
 * @property {String} auth - The HMAC in hex form for CBC, or tag in hex form for GCM
 * @property {Number} rounds - The PBKDF2 rounds
 * @property {String} method - The encryption method (gcm/cbc)
 */

/**
 * Decrypt text using AES-CBC
 * @param {EncryptedComponents} encryptedComponents Encrypted components
 * @param {DerivedKeyInfo} keyDerivationInfo Key derivation information
 * @returns {Promise.<String>} A promise that resolves with the decrypted string
 */
function decryptCBC(encryptedComponents, keyDerivationInfo) {
    // Extract the components
    const encryptedContent = encryptedComponents.content;
    const iv = new Buffer(encryptedComponents.iv, "hex");
    const salt = encryptedComponents.salt;
    const hmacData = encryptedComponents.auth;
    // Get HMAC tool
    const hmacTool = crypto.createHmac(HMAC_ALGORITHM, keyDerivationInfo.hmac);
    // Generate the HMAC
    hmacTool.update(encryptedContent);
    hmacTool.update(encryptedComponents.iv);
    hmacTool.update(salt);
    const newHmaxHex = hmacTool.digest("hex");
    // Check hmac for tampering
    if (constantTimeCompare(hmacData, newHmaxHex) !== true) {
        throw new Error("Authentication failed while decrypting content");
    }
    // Decrypt
    const decryptTool = crypto.createDecipheriv(ENC_ALGORITHM_CBC, keyDerivationInfo.key, iv);
    const decryptedText = decryptTool.update(encryptedContent, "base64", "utf8");
    return Promise.resolve(`${decryptedText}${decryptTool.final("utf8")}`);
}

/**
 * Decrypt text using AES-GCM
 * @param {EncryptedComponents} encryptedComponents Encrypted components
 * @param {DerivedKeyInfo} keyDerivationInfo Key derivation information
 * @returns {Promise.<String>} A promise that resolves with the decrypted string
 */
function decryptGCM(encryptedComponents, keyDerivationInfo) {
    // Extract the components
    const encryptedContent = encryptedComponents.content;
    const iv = new Buffer(encryptedComponents.iv, "hex");
    const { auth: tagHex, salt } = encryptedComponents;
    // Prepare tool
    const decryptTool = crypto.createDecipheriv(ENC_ALGORITHM_GCM, keyDerivationInfo.key, iv);
    // Add additional auth data
    decryptTool.setAAD(new Buffer(`${encryptedComponents.iv}${keyDerivationInfo.salt}`, "utf8"));
    // Set auth tag
    decryptTool.setAuthTag(new Buffer(tagHex, "hex"));
    // Perform decryption
    const decryptedText = decryptTool.update(encryptedContent, "base64", "utf8");
    return Promise.resolve(`${decryptedText}${decryptTool.final("utf8")}`);
}

/**
 * Encrypt text using AES-CBC
 * @param {String} text The text to encrypt
 * @param {DerivedKeyInfo} keyDerivationInfo Key derivation information
 * @param {Buffer} iv A buffer containing the IV
 * @returns {Promise.<EncryptedComponents>} A promise that resolves with encrypted components
 */
function encryptCBC(text, keyDerivationInfo, iv) {
    return Promise.resolve().then(() => {
        const ivHex = iv.toString("hex");
        const encryptTool = crypto.createCipheriv(ENC_ALGORITHM_CBC, keyDerivationInfo.key, iv);
        const hmacTool = crypto.createHmac(HMAC_ALGORITHM, keyDerivationInfo.hmac);
        const { rounds } = keyDerivationInfo;
        // Perform encryption
        let encryptedContent = encryptTool.update(text, "utf8", "base64");
        encryptedContent += encryptTool.final("base64");
        // Generate hmac
        hmacTool.update(encryptedContent);
        hmacTool.update(ivHex);
        hmacTool.update(keyDerivationInfo.salt);
        const hmacHex = hmacTool.digest("hex");
        // Output encrypted components
        return {
            mode: "cbc",
            auth: hmacHex,
            iv: ivHex,
            salt: keyDerivationInfo.salt,
            rounds,
            content: encryptedContent
        };
    });
}

/**
 * Encrypt text using AES-GCM
 * @param {String} text The text to encrypt
 * @param {DerivedKeyInfo} keyDerivationInfo Key derivation information
 * @param {Buffer} iv A buffer containing the IV
 * @returns {Promise.<EncryptedComponents>} A promise that resolves with encrypted components
 */
function encryptGCM(text, keyDerivationInfo, iv) {
    return Promise.resolve().then(() => {
        const ivHex = iv.toString("hex");
        const { rounds } = keyDerivationInfo;
        const encryptTool = crypto.createCipheriv(ENC_ALGORITHM_GCM, keyDerivationInfo.key, iv);
        // Add additional auth data
        encryptTool.setAAD(new Buffer(`${ivHex}${keyDerivationInfo.salt}`, "utf8"));
        // Perform encryption
        let encryptedContent = encryptTool.update(text, "utf8", "base64");
        encryptedContent += encryptTool.final("base64");
        // Handle authentication
        const tag = encryptTool.getAuthTag();
        // Output encrypted components
        return {
            mode: "gcm",
            iv: ivHex,
            salt: keyDerivationInfo.salt,
            rounds,
            content: encryptedContent,
            auth: tag.toString("hex")
        };
    });
}

/**
 * IV generator
 * @returns {Promise.<Buffer>} A promise that resolves with an IV
 */
function generateIV() {
    return Promise.resolve(new Buffer(crypto.randomBytes(16)));
}

/**
 * Salt generator
 * @param {Number} length The length of the string to generate
 * @returns {Promise.<String>} A promise that resolves with a salt (hex)
 * @throws {Error} Rejects if length is invalid
 */
function generateSalt(length) {
    if (length <= 0) {
        return Promise.reject(
            new Error(`Failed generating salt: Invalid length supplied: ${length}`)
        );
    }
    let output = "";
    while (output.length < length) {
        output += crypto.randomBytes(3).toString("base64");
        if (output.length > length) {
            output = output.substr(0, length);
        }
    }
    return Promise.resolve(output);
}

module.exports = {
    decryptCBC,
    decryptGCM,
    encryptCBC,
    encryptGCM,
    generateIV,
    generateSalt
};


const { ALGO_DEFAULT } = require("./shared.js");

const PBKDF2_ROUND_DEFAULT = 1000;

/**
 * Encrypted content components
 * @typedef {Object} EncryptedComponents
 * @property {String} content - The encrypted string
 * @property {String} iv - The IV in hex form
 * @property {String} salt - The salt
 * @property {String} hmac - The HMAC in hex form
 * @property {Number} rounds - The PBKDF2 rounds
 * @property {String} method - The encryption method (cbc/gcm)
 */

/**
 * Pack encrypted content components into the final encrypted form
 * @param {String} encryptedContent The encrypted text
 * @param {String} iv The IV in hex form
 * @param {String} salt The salt
 * @param {String} auth The HMAC for CBC mode, or the authentication tag for GCM mode
 * @param {Number} rounds The PBKDF2 round count
 * @param {String} method The encryption method (cbc/gcm)
 * @returns {String} The final encrypted form
 */
function packEncryptedContent(encryptedContent, iv, salt, auth, rounds, method) {
    return [encryptedContent, iv, salt, auth, rounds, method].join("$");
}

/**
 * Unpack encrypted content components from an encrypted string
 * @param {String} encryptedContent The encrypted string
 * @returns {EncryptedComponents} The extracted components
 * @throws {Error} Throws if the number of components is incorrect
 */
function unpackEncryptedContent(encryptedContent) {
    const [content, iv, salt, auth, roundsRaw, methodRaw] = encryptedContent.split("$");
    // iocane was originally part of Buttercup's core package and used defaults from that originally.
    // There will be 4 components for pre 0.15.0 archives, and 5 in newer archives. The 5th component
    // is the pbkdf2 round count, which is optional:
    const rounds = roundsRaw ? parseInt(roundsRaw, 10) : PBKDF2_ROUND_DEFAULT;
    // Originally only "cbc" was supported, but GCM was added in version 1
    const method = methodRaw || "cbc";
    return {
        content,
        iv,
        salt,
        auth,
        rounds,
        method
    };
}

module.exports = {
    packEncryptedContent,
    unpackEncryptedContent
};


const { pbkdf2: deriveKey } = require("pbkdf2");

const DERIVED_KEY_ALGORITHM = "sha256";
const HMAC_KEY_SIZE = 32;
const PASSWORD_KEY_SIZE = 32;

/**
 * Derived key info
 * @typedef DerivedKeyInfo
 * @property {String} salt - The salt used
 * @property {Buffer} key - The derived key
 * @property {Buffer} hmac - The HMAC
 * @property {Number} rounds - The number of rounds used
 */

/**
 * Derive a key from a password
 * @param {Function} pbkdf2Gen The generator method
 * @param {String} password The password to derive from
 * @param {String} salt The salt (Optional)
 * @param {Number} rounds The number of iterations
 * @param {Boolean=} generateHMAC Enable HMAC key generation
 * @throws {Error} Rejects if no password is provided
 * @throws {Error} Rejects if no salt is provided
 * @throws {Error} Rejects if no rounds are provided
 * @returns {Promise.<DerivedKeyInfo>} A promise that resolves with an object (DerivedKeyInfo)
 */
function deriveFromPassword(pbkdf2Gen, password, salt, rounds, generateHMAC = true) {
    if (!password) {
        return Promise.reject(new Error("Failed deriving key: Password must be provided"));
    }
    if (!salt) {
        return Promise.reject(new Error("Failed deriving key: Salt must be provided"));
    }
    if (!rounds || rounds <= 0) {
        return Promise.reject(new Error("Failed deriving key: Rounds must be greater than 0"));
    }
    const bits = generateHMAC ? (PASSWORD_KEY_SIZE + HMAC_KEY_SIZE) * 8 : PASSWORD_KEY_SIZE * 8;
    return pbkdf2Gen(password, salt, rounds, bits)
        .then(derivedKeyData => derivedKeyData.toString("hex"))
        .then(function(derivedKeyHex) {
            const dkhLength = derivedKeyHex.length;
            const keyBuffer = generateHMAC
                ? new Buffer(derivedKeyHex.substr(0, dkhLength / 2), "hex")
                : new Buffer(derivedKeyHex, "hex");
            const output = {
                salt: salt,
                key: keyBuffer,
                rounds: rounds
            };
            if (generateHMAC) {
                output.hmac = new Buffer(derivedKeyHex.substr(dkhLength / 2, dkhLength / 2), "hex");
            }
            return output;
        });
}

/**
 * The default PBKDF2 function
 * @param {String} password The password to use
 * @param {String} salt The salt to use
 * @param {Number} rounds The number of iterations
 * @param {Number} bits The size of the key to generate, in bits
 * @returns {Promise.<Buffer>} A Promise that resolves with the hash
 */
function pbkdf2(password, salt, rounds, bits) {
    return new Promise((resolve, reject) => {
        deriveKey(password, salt, rounds, bits / 8, DERIVED_KEY_ALGORITHM, (err, key) => {
            if (err) {
                return reject(err);
            }
            return resolve(key);
        });
    });
}

module.exports = {
    deriveFromPassword,
    pbkdf2
};


var util = require('util')
  , format = util.format.bind(util)
  , db

/**
 * Return a new Node instance with `node` and `opts`
 *
 * @param {String || Object} node
 * @param {Object} opts
 * @return {Object}
 * @api public
 */

module.exports = function (node, opts) {
  opts = opts || {}
  opts.inner = opts.inner || 'membership'
  opts.outer = opts.outer || 'members'
  return new Node(String(node), opts)
}

/**
 * Set the redis client
 *
 * @param {Object} client
 * @api public
 */

module.exports.setClient = function (client) {
  db = client
}

/**
 * Node Constructor
 */

function Node (id, opts) {
  var me = this
  this.id = id
  Object.defineProperty(me, 'inner', {
      value : opts.inner
    , enumerable : false
    , writable : true
  })
  Object.defineProperty(me, 'outer', {
      value : opts.outer
    , enumerable : false
    , writable : true
  })
  Object.defineProperty(me, this.inner, {
      value : new Edge({ inner : this.inner, outer : this.outer, id : id })
    , enumerable : false
    , writable : true
  })
  Object.defineProperty(me, this.outer, {
      value : new Edge({ inner : this.outer, outer : this.inner, id : id })
    , enumerable : false
    , writable : true
  })
}

/**
 * Return the string id of the node instance
 *
 * @return {String}
 * @api public
 */

Node.prototype.toString = function () {
  return this.id
}

/**
 * Delete the node and all of its edges
 *
 * @param {Function} cb
 * @api public
 */

Node.prototype.delete = function (cb) {
  var me = this
  db.multi()
    .smembers([ me[me.inner].innerkey ])
    .smembers([ me[me.outer].innerkey ])
    .exec(function (error, replies) {
      var multi = db.multi()
      replies.forEach(function (reply) {
        reply.forEach(function (gid) {
          multi
            .srem([ format(me.inner + '_%s', gid), me.id ])
            .srem([ format(me.outer + '_%s', gid), me.id ])
            .srem([ me[me.inner].innerkey, gid ])
            .srem([ me[me.outer].innerkey, gid ])
        })
      })
      multi.exec(cb)
    })
  ;
}

/**
 * Initialize a new `Edge` with inner/outer edge names
 *
 * @param {Object} opts
 */

function Edge (opts) {
  this.id = opts.id
  this.innerformat = opts.inner + '_%s'
  this.outerformat = opts.outer + '_%s'
  this.innerkey = format(this.innerformat, this.id)
  this.outerkey = format(this.outerformat, this.id)
  this[opts.inner] = function (cb) {
    this.all(function (error, array) {
      if (error) return cb(error)
      if (!array || !array.length) return cb(null, array || [])
      array = array.map(function (gid) { return format(this.innerformat, String(gid)) }, this)
      db.sunion(array, cb)
    }.bind(this))
  }
  this[opts.outer] = function (cb) {
    this.all(function (error, array) {
      if (error) return cb(error)
      if (!array || !array.length) return cb(null, array || [])
      array = array.map(function (gid) { return format(this.outerformat, gid) }, this)
      db.sunion(array, cb)
    }.bind(this))
  }
}

/**
 * Return all of the inner edges for the parent `node` instance
 *
 * @param {Function} cb
 */

Edge.prototype.all = function (cb) {
  db.smembers([ this.innerkey ], cb)
}

/**
 * Add edges to `arr` for the parent `node` instance
 *
 * @param {String || Array} arr
 * @param {Function} cb
 */

Edge.prototype.add = function (arr, cb) {
  arr = Array.isArray(arr) ? arr : [ arr ]
  arr = arr.map(String)
  var multi = db.multi()
  arr.forEach(function (gid) {
    multi.sadd([ this.innerkey, String(gid) ])
    multi.sadd([ format(this.outerformat, String(gid)), this.id ])
  }, this)
  multi.exec(cb)
}

/**
 * Checks if parent `node` instance has an edge to `member`
 *
 * @param {String} member
 * @param {Function} cb
 */

Edge.prototype.has = function (member, cb) {
  db.sismember(this.innerkey, member, cb)
}

/**
 * Delete edges to `arr` for the parent `node` instance
 *
 * @param {String || Array} arr
 * @param {Function} cb
 */

Edge.prototype.delete = function (arr, cb) {
  arr = Array.isArray(arr) ? arr : [ arr ]
  arr = arr.map(String)
  var multi = db.multi()
  arr.forEach(function (gid) {
    multi.srem([ this.innerkey, String(gid) ])
    multi.srem([ format(this.outerformat, String(gid)), this.id ])
  }, this)
  multi.exec(cb)
}

/**
 * Return the inner edges with the edges of `arr` removed
 *
 * @param {String || Array} arr
 * @param {Function} cb
 */

Edge.prototype.without = function (arr, cb) {
  arr = Array.isArray(arr) ? arr : [ arr ]
  arr = arr.map(function (gid) { return format(this.innerformat, String(gid)) }, this)
  arr.unshift(this.innerkey)
  db.sdiff(arr, cb)
}

/**
 * Return an intersection (logical AND) of inner edges with those of `arr`
 *
 * @param {String || Array} arr
 * @param {Function} cb
 */

Edge.prototype.intersect = function (arr, cb) {
  arr = Array.isArray(arr) ? arr : [ arr ]
  arr = arr.map(function (gid) { return format(this.innerformat, String(gid)) }, this)
  arr.unshift(this.innerkey)
  db.sinter(arr, cb)
}

/**
 * Return a union (logical XOR) of inner edges with those of `arr`
 *
 * @param {String || Array} arr
 * @param {Function} cb
 */

Edge.prototype.union = function (arr, cb) {
  arr = Array.isArray(arr) ? arr : [ arr ]
  arr = arr.map(function (gid) { return format(this.innerformat, String(gid)) }, this)
  arr.unshift(this.innerkey)
  db.sunion(arr, cb)
}


import Route from '@ember/routing/route';
import arraySwap from 'ember-redirect/utils/array-swap';
import { lookup, register } from 'ember-redirect/utils/container';
import window from 'ember-window-mock';

/**
 * - `type` and `value` are in Ember > v2.8
 * - `name` is in Ember === 2.8
 */
function getDynamicSegments(segments) {
  return segments
    .filter(item => item.type === 1 || !!item.name)
    .map(item => item.value || item.name);
}

/**
 * Loosely validate a URL `string`. Stolen from: https://github.com/segmentio/is-url
 *
 * @param {String} string
 * @return {Boolean}
 */
function resemblesURL(route) {
  // eslint-disable-next-line no-useless-escape
  var matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/;
  return matcher.test(route);
}

export default function(routeName, options, instance) {
  let routeContainerKey = `route:${routeName}`;
  let routeObject       = lookup(instance, routeContainerKey);

  if (!routeObject) {
    routeObject = Route.extend({});
    register(instance, routeContainerKey, routeObject, { singleton: false });
  }

  routeObject.reopen({
    beforeModel(transition) {
      let newDynObject       = {};
      let thisRouteName      = this.routeName;
      let routeNames         = this.router._routerMicrolib ? this.router._routerMicrolib.recognizer.names : this.router.router.recognizer.names;
      let dynSegsOfNextRoute = null;
      let dynSegsOfThisRoute = null;

      // If the redirect route is not a valid route name in this app AND then redirect
      // looks like a url, send um that-a-way via location.replace
      if (!routeNames[options.redirect] && resemblesURL(options.redirect)) {
        transition.abort();
        window.location.assign(options.redirect);
        return false;
      }

      dynSegsOfNextRoute = getDynamicSegments(routeNames[options.redirect].segments);
      dynSegsOfThisRoute = getDynamicSegments(routeNames[thisRouteName].segments);

      // Make sure we only try to make a redirect at the most nested
      // route and not a parent resource.
      if(this.routeName !== transition.targetName) {
        return false;
      }

      // Make sure that the lengths are the same else we are trying to transition to a route that needs more
      // segments then we can supply.
      if(dynSegsOfNextRoute.length <= dynSegsOfThisRoute.length) {

        dynSegsOfNextRoute.forEach(function(item, index) {
          // This means that we have the same dynamic segment on both this and the next route so we will pair them together.
          if(dynSegsOfThisRoute.indexOf(dynSegsOfNextRoute[index]) !== -1) {
            newDynObject[dynSegsOfNextRoute[index]] = transition.params[thisRouteName][dynSegsOfNextRoute[index]];
            dynSegsOfThisRoute = arraySwap(dynSegsOfThisRoute, index, dynSegsOfThisRoute.indexOf(dynSegsOfNextRoute[index]));
          }
          else {
            newDynObject[dynSegsOfNextRoute[index]] = transition.params[thisRouteName][dynSegsOfThisRoute[index]];
          }
        });

        // Pass along query params.
        newDynObject['queryParams'] = transition.queryParams;

        this.replaceWith(transition.router.recognizer.generate(options.redirect, newDynObject));
      }

      return this._super(...arguments);
    }
  });

  return routeObject;
}


'use strict';

const BinaryStream = require('lib/binary-stream');
const array = require('types/array');
const buffer = require('types/buffer');
const bool = require('types/bool');
const reserved = require('types/reserved');
const string = require('types/string');
const numbers = require('types/numbers');
const when = require('types/when');
const select = require('types/select');
const { encode } = require('lib/encode');
const { decode } = require('lib/decode');
const { encodingLength } = require('lib/encoding-length');
const Transaction = require('lib/transaction');
const NotEnoughDataError = require('lib/not-enough-data-error');

const types = {
  array,
  bool,
  buffer,
  reserved,
  string,
  when,
  select,
};

for (const type of Object.keys(numbers)) {
  types[type] = numbers[type]; // eslint-disable-line security/detect-object-injection
}

const kschema = Symbol('schema');

/**
 * Create transform stream to encode objects into Buffer.
 * @param {Object} [schema]
 * @returns {EncodeStream}
 */
function createEncodeStream(schema) {
  const stream = new BinaryStream({
    readableObjectMode: false,
    writableObjectMode: true,
    transform: transformEncode,
  });

  stream[kschema] = schema;
  return stream;
}

/**
 * Create transform stream to decode binary data into object.
 * @param {Buffer|Object} [bufOrSchema]
 * @returns {DecodeStream}
 */
function createDecodeStream(bufOrSchema) {
  let schema = null;
  const isBuffer = Buffer.isBuffer(bufOrSchema);

  if (!isBuffer) {
    schema = bufOrSchema;
  }

  const stream = new BinaryStream({
    transform: transformDecode,
    readableObjectMode: true,
    writableObjectMode: false,
  });

  stream[kschema] = schema;

  if (isBuffer) {
    stream.append(bufOrSchema);
  }

  return stream;
}

/**
 * The `transform` function for transform stream.
 * @param {*} chunk Any valid js data type.
 * @param {string} encoding
 * @param {Function} cb
 */
function transformEncode(chunk, encoding, cb) {
  try {
    encode(chunk, this[kschema], this);

    const buf = this.slice();
    this.consume(buf.length);

    cb(null, buf);
  } catch (error) {
    cb(error);
  }
}

/**
 * The `transform` function for transform stream.
 * @param {*} chunk Any valid js data type.
 * @param {string} encoding
 * @param {Function} cb
 */
function transformDecode(chunk, encoding, cb) {
  this.append(chunk);

  try {
    while (this.length > 0) {
      const transaction = new Transaction(this);
      const data = decode(transaction, this[kschema]);

      transaction.commit();
      this.push(data);
    }

    cb();
  } catch (error) {
    if (error instanceof NotEnoughDataError) {
      cb();
    } else {
      cb(error);
    }
  }
}

module.exports = {
  /* Main api */
  createEncodeStream,
  createDecodeStream,
  encode,
  decode,
  encodingLength,

  /* aliases */
  createEncode: createEncodeStream,
  createDecode: createDecodeStream,

  /* Data types */
  types,

  /* Re-export utils */
  BinaryStream,
  NotEnoughDataError,
};