var format = require('./format/format');

function type(v) {
  return typeof v;
}

function inequality(exp, act, path) {
  var strExp = format(exp, { clip: 128 });
  var strAct = format(act, { clip: 128 });
  return {
    diff: 'value',
    path: path,
    expected: strExp,
    actual: strAct,
  };
}

function propertyKeys(obj) {
  var keys = [];
  for (var k in obj) {
    keys.push(k);
  }
  return keys;
}

function protoName(obj) {
  if (obj.constructor && obj.constructor.name) {
    return obj.constructor.name;
  }
  return obj + '';
}

/** Given two arrays or strings, return the first element where they differ (shallow). */
function firstDiff(exp, act) {
  var i;
  for (i = 0; i < exp.length && i < act.length; ++i) {
    if (exp[i] != act[i]) {
      break;
    }
  }
  return i;
}

/** Function to compute the difference between two values. Note that unlike many other object
    diff implementations, this one tries to 'humanize' the results, summarizing them for
    improved readability.
    @param {string} exp The expected value.
    @param {string} act The actual value.
    @param {string} opt_path The expression used to access the value, such as 'a.b.c'.
    @param {bool} opt_deep Whether to do a deep comparison.
    @param {Array} opt_resukts If non-null, will be used to report the differences between the
        expected and actual values.
 */
function compare(exp, act, opt_path, opt_deep, opt_results) {
  var path = opt_path || '';
  var i, j, strExp, strAct;
  if (sameValue(exp, act)) {
    return true;
  }
  var et = type(exp);
  var at = type(act);

  if (et !== at || et === 'number' || at === 'boolean' || et === 'undefined'
    || exp === null || act === null) {
    if (opt_results) {
      opt_results.push(inequality(exp, act, path));
    }
    return false;
  } else if (et === 'object') {
    // Shallow comparison
    if (!opt_deep) {
      if (opt_results) {
        opt_results.push(inequality(exp, act, path));
      }
      return false;
    }

    // Compare prototypes
    var eProto = Object.getPrototypeOf(exp);
    var aProto = Object.getPrototypeOf(act);
    if (eProto != aProto) {
      if (opt_results) {
        var eName = protoName(exp);
        var aName = protoName(act);
        opt_results.push({
          diff: 'type',
          path: path,
          expected: eName,
          actual: aName,
        });
      }
      return false;
    }

    if (Array.isArray(exp) && Array.isArray(act)) {
      if (!opt_results && exp.length !== act.length) {
        return false;
      }
      for (i = 0; i < exp.length && i < act.length; i += 1) {
        if (!compare(exp[i], act[i], path + '[' + i + ']', opt_deep, opt_results, true)) {
          if (!opt_results) {
            return false;
          }
        }
      }
      var strEl;

      if (i < exp.length) {
        for (j = i; j < exp.length && j < i + 3; j += 1) {
          strEl = format(exp[j], { clip: 128 });
          opt_results.push({
            diff: 'element',
            path: path + '[' + j + ']',
            // expected: eName,
            actual: strEl,
          });
        }
        if (j < exp.length) {
          opt_results.push({ diff: 'more', more: (exp.length - j) });
        }
      }
      if (i < act.length) {
        strEl = format(act[i], { clip: 128 });
        for (j = i; j < act.length && j < i + 3; j += 1) {
          strEl = format(act[j], { clip: 128 });
          opt_results.push({
            diff: 'element',
            path: path + '[' + j + ']',
            expected: strEl,
          });
        }
        if (j < act.length) {
          opt_results.push({ diff: 'more', more: (act.length - j) });
        }
      }
      return false;
    }

    // Handle regular expression objects.
    if (exp instanceof RegExp) {
      if (opt_deep && (exp + '') === (act + '')) {
        return true;
      }
      if (opt_results) {
        opt_results.push(inequality(exp, act, path));
      }
      return false;
    }

    // Handle 'date' objects.
    if (exp instanceof Date) {
      if (opt_deep && exp.getTime() === act.getTime()) {
        return true;
      }
      if (opt_results) {
        opt_results.push(inequality(exp, act, path));
      }
      return false;
    }

    // Compare individual properties.
    var same = true;
    var eKeys = propertyKeys(exp);
    var aKeys = propertyKeys(act);
    eKeys.sort();
    aKeys.sort();

    // Check all keys in exp
    for (i = 0; i < eKeys.length; ++i) {
      var k = eKeys[i];
      if (act.hasOwnProperty(k)) {
        if (!compare(exp[k], act[k], path + '.' + k, opt_deep, opt_results, true)) {
          if (!opt_results) {
            return false;
          }
          same = false;
        }
      } else {
        same = false;
        if (opt_results) {
          // opt_results.push(propertyAbsent(exp[k], null, path + '.' + k));
          strExp = format(exp[k], { clip: 128 });
          var keyExp = path + '.' + k;
          opt_results.push({
            diff: 'property',
            path: keyExp,
            expected: strExp,
            // actual: aName,
          });
        }
      }
    }

    // Check all keys in act
    for (i = 0; i < aKeys.length; ++i) {
      var k2 = aKeys[i];
      if (!exp.hasOwnProperty(k2)) {
        if (!opt_results) {
          return false;
        }
        same = false;
        strAct = format(act[k2], { clip: 128 });
        var keyAct = path + '.' + k2;
        opt_results.push({
          diff: 'property',
          path: keyAct,
          // expected: eName,
          actual: strAct,
        });
      }
    }

    return same;
  } else if (et === 'string') {
    if (opt_results) {
      // See if the values have line breaks
      var eLines = exp.split('\n');
      var aLines = act.split('\n');
      var col = 0;
      var start;
      if (eLines.length > 2 || aLines.length > 2) {
        // find the first line where they differ
        var line = firstDiff(eLines, aLines);
        // find the first character where the lines differ
        var expLine = line < eLines.length ? eLines[line] : '';
        var actLine = line < aLines.length ? aLines[line] : '';
        col = firstDiff(expLine, actLine);

        start = Math.max(col - 16, 0);
        strExp = sliceWithEllipsis(expLine, start, Math.min(col + 40, expLine.length));
        strAct = sliceWithEllipsis(actLine, start, Math.min(col + 40, actLine.length));
        opt_results.push({
          diff: 'string',
          path: path,
          line: line,
          col: col,
          expected: strExp,
          actual: strAct
        });
      } else {
        // find the first character where they differ
        col = firstDiff(exp, act);

        // if that index < 16 or strings are short, then show the whole thing:
        if (col < 16 || (exp.length < 30 && act.length < 30)) {
          opt_results.push(inequality(exp, act, path));
          return false;
        }

        // Only show the part of the string that differs.
        start = Math.max(col - 16, 0);
        strExp = sliceWithEllipsis(exp, start, Math.min(col + 40, exp.length));
        strAct = sliceWithEllipsis(act, start, Math.min(col + 40, act.length));
        opt_results.push({
          diff: 'string',
          path: path,
          index: col,
          expected: strExp,
          actual: strAct
        });
      }
      // opt_results.push(msg);
      return false;
    }
    return false;
  } else {
    // buffer
    // arguments
    throw new Error('Type not handled: ' + et);
  }
}

function sliceWithEllipsis(str, start, end) {
  var result = '"' + str.slice(start, end) + '"';
  if (start > 0) {
    result = '...' + result;
  }
  if (end < str.length) {
    result += '...';
  }
  return result;
}

function diff(exp, act, opt_path, opt_deep, opt_max) {
  var diffs = [];
  if (compare(exp, act, opt_path, opt_deep, diffs)) {
    return [];
  }
  var result = [];
  if (diffs.length > 1) {
    if (opt_path) {
      result.push('Expected value of ' + opt_path + ' does not equal actual value:');
    } else {
      result.push('Expected value does not equal actual value:');
    }
  }
  var max = typeof opt_max === 'number' ? opt_max : diffs.length;
  for (var i = 0; i < diffs.length && i < max; ++i) {
    var d = diffs[i];
    var msg = [];
    var indent = '';
    if (diffs.length > 1) {
      indent = '  ';
      if (d.path && d.diff !== 'more') {
        msg.push(indent);
        msg.push(d.path);
        msg.push(': ');
      }
    }
    switch (d.diff) {
      case 'type':
        msg.push('Expected type');
        if (diffs.length === 1) {
          msg.push(' of ' + d.path);
        }
        msg.push(' to be \'' + d.expected + '\', actual type was \'' + d.actual + '\'.');
        break;
      case 'value':
        if (diffs.length === 1 && !d.path) {
          msg.push('Expected ' + d.actual + ' to be ' + d.expected + '.');
        } else if (d.expected.length <= 16 && d.actual.length <= 16) {
          msg.push('Expected');
          if (d.path && diffs.length === 1) {
            msg.push(' ' + d.path);
          } else {
            msg.push(' value');
          }
          msg.push(' to be ' + d.expected + ', actual value was ' + d.actual + '.');
        } else {
          msg.push('Expected value');
          if (d.path && diffs.length === 1) {
            msg.push(' of ' + d.path);
          }
          msg.push(' differs from actual value:');
          msg.push('\n' + indent + '  expected: ' + d.expected);
          msg.push('\n' + indent + '    actual: ' + d.actual);
        }
        break;
      case 'string':
        msg.push('Expected value');
        if (diffs.length === 1 && d.path) {
          msg.push(' of ' + d.path);
        }
        msg.push(' differs from actual value starting at');
        if (typeof d.line === 'number') {
          msg.push(' line ' + (d.line + 1) + ', column ' + (d.col + 1) + ':');
        } else {
          msg.push(' character ' + d.index + ':');
        }
        msg.push('\n' + indent + '  expected: ' + d.expected);
        msg.push('\n' + indent + '    actual: ' + d.actual);
        break;
      case 'element':
        if (!('expected' in d)) {
          msg.push('Unexpected element');
          if (diffs.length === 1) {
            msg.push(' ' + d.path);
          }
          msg.push(' with value ' + d.actual + '.');
        } else {
          msg.push('Missing expected element');
          if (diffs.length === 1) {
            msg.push(' ' + d.path);
          }
          msg.push(' with value ' + d.expected + '.');
        }
        break;
      case 'property':
        if (!('expected' in d)) {
          msg.push('Unexpected property');
          if (diffs.length === 1) {
            msg.push(' ' + d.path);
          }
          msg.push(' with value ' + d.actual + '.');
        } else {
          msg.push('Missing expected property');
          if (diffs.length === 1) {
            msg.push(' ' + d.path);
          }
          msg.push(' with value ' + d.expected + '.');
        }
        break;
      case 'more':
        msg.push(indent + '(...and ' + d.more + ' more.)');
        break;
    }
    result.push(msg.join(''));
  }
  if (max < diffs.length) {
    result.push('  ...' + (diff.length - max) + ' additional differences not shown.');
  }
  return result;
}

function sameValue(lhs, rhs) {
  // TODO: Handle obscure JS behaviors like +0, NaN, etc.
  return lhs === rhs;
}

module.exports = {
  compare: compare,
  diff: diff,
  format_diffs: diff,
}


'use strict';

const isPlainObject = require('is-plain-obj');
const property = require('@yr/property');

module.exports = get;

/**
 * Retrieve value stored at 'key'
 * Empty/null/undefined 'key' returns all data
 * @param {DataStore} store
 * @param {String} [key]
 * @param {Object} [options]
 *  - {Boolean} resolveReferences
 * @returns {*}
 */
function get(store, key, options) {
  if (key === '' || key == null) {
    return store._data;
  }
  return doGet(store, key, options);
}

/**
 * Retrieve value stored at 'key'
 * @param {DataStore} store
 * @param {String} key
 * @param {Object} [options]
 *  - {Number} referenceDepth
 * @returns {*}
 */
function doGet(store, key, options = {}) {
  // Resolve back to original key if referenced
  key = store._resolveRefKey(key);

  const { referenceDepth = 1 } = options;
  const cacheKey = `${key}:${referenceDepth}`;
  const shouldCache = !store._isWritable;

  if (shouldCache) {
    if (store._getCache[cacheKey]) {
      return store._getCache[cacheKey];
    }
  }

  let value = property.get(store._data, key);

  if (referenceDepth > 0) {
    value = resolveReferences(store, value, referenceDepth);
  }

  if (shouldCache) {
    store._getCache[cacheKey] = value;
  }

  return value;
}

/**
 * Resolve all references in 'value' up to max 'depth'
 * @param {DataStore} store
 * @param {Object} value
 * @param {Number} depth
 * @returns {Object}
 */
function resolveReferences(store, value, depth) {
  if (--depth < 0) {
    return value;
  }

  if (Array.isArray(value)) {
    const n = value.length;
    const v = new Array(n);
    let item;

    for (let i = n - 1; i >= 0; i--) {
      item = value[i];
      v[i] = resolveReferences(
        store,
        store._isRefValue(item) ? property.get(store._data, store._parseRefKey(item)) : item,
        depth
      );
    }

    value = v;
  } else if (isPlainObject(value)) {
    const v = {};
    let item;

    for (const prop in value) {
      item = value[prop];
      v[prop] = resolveReferences(
        store,
        store._isRefValue(item) ? property.get(store._data, store._parseRefKey(item)) : item,
        depth
      );
    }

    value = v;
  }

  return value;
}


/** Convert a string value to a printable string representation.
    @param {String} value The value to format.
    @param {Object} options Formatting options.
    @protected
*/
function formatString(value, options) {
  var opts = options || {};
  var result = value.replace(/[\0-\37]/g, function (ch) {
    switch (ch) {
      case '\n': return '\\n';
      case '\r': return '\\r';
      case '\t': return '\\t';
      case '\b': return '\\b';
      case '\v': return '\\v';
      case '\f': return '\\f';
      default: return '\\' + ch.charCodeAt(0).toString(8);
    }
  });
  if (opts.clip && result.length > opts.clip) {
    result = result.slice(0, Math.max(0, opts.clip - 3)) + '...';
  }
  return '"' + result + '"';
}

module.exports = formatString;


/* eslint key-spacing:0 spaced-comment:0 */
const path = require('path');
const debug = require('debug')('app:globalConfig:project');
const argv = require('yargs').argv;
const ip = require('ip');

debug('Creating default configuration.');
// ========================================================
// Default Configuration
// ========================================================
const config = {
  env : process.env.NODE_ENV || 'development',

  // ----------------------------------
  // Project Structure
  // ----------------------------------
  path_base  : path.resolve(__dirname, '../../../'),
  dir_examples: 'src/examples',
  dir_client : 'src',
  dir_dist   : 'examples',
  dir_public : 'public',
  dir_server : 'server',
  dir_test   : 'tests',

  // ----------------------------------
  // Server Configuration
  // ----------------------------------
  server_host : ip.address(), // use string 'localhost' to prevent exposure on local network
  server_port : process.env.PORT || 3000,

  // ----------------------------------
  // Compiler Configuration
  // ----------------------------------

  // 用了'transform-runtime'就不能出现export *,如果非要用,可以采用下面的方式
  // https://github.com/babel/babel/issues/2877
  // https://github.com/babel/babel/issues/5501#issuecomment-287589989
  compiler_babel: {
    plugins: [
      'babel-plugin-transform-decorators-legacy',
      'babel-plugin-transform-object-rest-spread',
      'babel-plugin-transform-export-extensions'
    ],
    presets: ['flow', 'es2015', 'react', 'stage-0']
  },
  // cheap-module-source-map works in FF, but doesn't work in Chrome
  // cheap-module-inline-source-map doesn't work in FF, but does work in Chrome
  // For development, I set devtool to 'eval-source-map'
  // For production, I set devtool to 'source-map'
  compiler_devtool         : 'source-map',
  compiler_hash_type       : 'hash',
  compiler_fail_on_warning : false,
  compiler_quiet           : false,
  compiler_public_path     : '/',
  compiler_stats           : {
    chunks : false,
    chunkModules : false,
    colors : true
  },
  compiler_vendors : [
    'react',
    'react-dom',
    'react-redux',
    'react-router',
    'redux',
    'redux-thunk'
  ],

  // ----------------------------------
  // Test Configuration
  // ----------------------------------
  coverage_reporters : [
    { type : 'text-summary' },
    { type : 'lcov', dir : 'coverage' }
  ]
};

/************************************************
-------------------------------------------------

All Internal Configuration Below
Edit at Your Own Risk

-------------------------------------------------
************************************************/

// ------------------------------------
// Environment
// ------------------------------------
// N.B.: globals added here must _also_ be added to .eslintrc
config.globals = {
  'process.env'  : {
    'NODE_ENV' : JSON.stringify(config.env)
  },
  'NODE_ENV'     : config.env,
  '__DEV__'      : config.env === 'development',
  '__PROD__'     : config.env === 'production',
  '__TEST__'     : config.env === 'test',
  '__COVERAGE__' : !argv.watch && config.env === 'test',
  '__BASENAME__' : JSON.stringify(process.env.BASENAME || '')
};

// ------------------------------------
// Validate Vendor Dependencies
// ------------------------------------
const pkg = require('../package.json');

config.compiler_vendors = config.compiler_vendors
  .filter((dep) => {
    if (pkg.dependencies[dep]) return true;

    debug(
      `Package "${dep}" was not found as an npm dependency in package.json; ` +
      `it won't be included in the webpack vendor bundle.
       Consider removing it from \`compiler_vendors\` in ~/config/index.js`
    );
  });

// ------------------------------------
// Utilities
// ------------------------------------
function base () {
  const args = [config.path_base].concat([].slice.call(arguments));
  return path.resolve.apply(path, args);
}

config.paths = {
  base   : base,
  examples: base.bind(null, config.dir_examples),
  client : base.bind(null, config.dir_client),
  public : base.bind(null, config.dir_public),
  dist   : base.bind(null, config.dir_dist)
};

// ========================================================
// Environment Configuration
// ========================================================
debug(`Looking for environment overrides for NODE_ENV "${config.env}".`);
const environments = require('./environments.config.js');
const overrides = environments[config.env];
if (overrides) {
  debug('Found overrides, applying to default configuration.');
  Object.assign(config, overrides(config));
} else {
  debug('No environment overrides found, defaults will be used.');
}

module.exports = config;


/** @module certainty */
var Subject = require('./subject');
var ProxyBase = require('./proxy');

/** A fluent context object containing the value of the field that was just tested. Used for
    additional assertions about a field.
    @constructor
  */
function KeyValue(subject, key) {
  this.subject = subject;
  this.key = key;
  this.format = subject.format;
  this.value = subject.value.get(key);
}

/** Ensure that the field has the expected value.
    @param {*} value The expected value of the field.
*/
KeyValue.prototype.withValue = function (expected) {
  if (this.value != expected) {
    this.subject.fail('Expected ' + this.subject.describe() + ' to contain key ' +
      this.format(this.key) + ' with value ' + this.format(expected) + ', actual value was ' +
      this.value + '.');
  }
}
ProxyBase.addMethods(KeyValue);

// Used when the key is missing, so that .withValue() doesn't produce a second error message.
function MissingKeyValue() {}
MissingKeyValue.prototype.withValue = function (_expected) {
  return this;
}

/** Subclass of Subject which provides assertions methods for Map types.
    @param {FailureStrategy} failureStrategy The failure strategy to use when an assertion fails.
    @param {object} value The value being checked.
    @constructor
    @extends Subject
*/
function MapSubject(failureStrategy, value) {
  Subject.call(this, failureStrategy, value);
}
MapSubject.prototype = Object.create(Subject.prototype);
MapSubject.prototype.constructor = MapSubject;

/** Ensure that the map is empty.
    @return {MapSubject} `this` for chaining.
*/
MapSubject.prototype.isEmpty = function () {
  if (this.value.size !== 0) {
    this.fail('Expected ' + this.describe() + ' to be an empty map.');
  }
  return this;
};

/** Ensure that the map is non-empty.
    @return {MapSubject} `this` for chaining.
*/
MapSubject.prototype.isNotEmpty = function () {
  if (this.value.size === 0) {
    this.fail('Expected ' + this.describe() + ' to be a non-empty map.');
  }
  return this;
};

/** Ensure that the map is the expected size.
    @param {number} size The expected size of the map.
    @return {MapSubject} `this` for chaining.
*/
MapSubject.prototype.hasSize = function (size) {
  if (this.value.size !== size) {
    this.fail('Expected ' + this.describe() + ' to be of size ' + size +
      ', actual size was ' + this.value.size + '.');
  }
  return this;
};

/** Ensure that the map contains a given key.
    @return {MapSubject} `this` for chaining.
    @param {*} key The key expected to be in the map.
*/
MapSubject.prototype.containsKey = function (key) {
  if (!this.value.has(key)) {
    this.fail('Expected ' + this.describe() + ' to contain key ' + this.format(key) + '.');
    return new MissingKeyValue();
  }
  return new KeyValue(this, key);
};

/** Ensure that the map does not contain a given key.
    @param {*} key The key not expected to be in the map.
    @return {MapSubject} `this` for chaining.
*/
MapSubject.prototype.doesNotContainKey = function (key) {
  if (this.value.has(key)) {
    this.fail('Expected ' + this.describe() + ' to not contain key ' +
      this.format(key) + '.');
  }
  return this;
};

/** Ensure that the map contains a given key / value pair.
    @param {*} key The key expected to be in the map.
    @param {*} value The expected value for that key.
    @return {MapSubject} `this` for chaining.
*/
MapSubject.prototype.containsEntry = function (key, value) {
  if (!this.value.has(key)) {
    this.fail('Expected ' + this.describe() + ' to contain key ' + this.format(key) + '.');
  } else if (this.value.get(key) != value) {
    this.fail('Expected ' + this.describe() + ' to contain key ' + this.format(key) +
      ' with value ' + this.format(value) + ', actual value was ' +
      this.format(this.value.get(key)) + '.');
  }
  return this;
};

module.exports = MapSubject;


var registry = require('./registry');

/** Convert an object value to a printable string representation.
    @param {Object} value The value to format.
    @param {Object} options Formatting options.
    @protected
*/
function formatObject(value, options) {
  if (value === undefined) {
    return 'undefined';
  }
  if (value === null) {
    return 'null';
  }

  if (typeof(value) == 'object') {
    if (value instanceof RegExp || value instanceof Date) {
      return value + '';
    }

    var opts = options || {};
    var innerOpts = Object.assign({}, opts, { clip: false });
    // If it's a plain object
    var proto = Object.getPrototypeOf(value);
    if (proto === Object.prototype || proto === null) {
      var keys = [];
      for (var k in value) {
        keys.push(k);
      }
      keys.sort();
      var parts = ['{'];
      var length = 4; // Include both open and close brace.
      for (var i = 0; i < keys.length; ++i) {
        var key = keys[i];
        var sep = i > 0 ? ', ' : ' ';
        var s = registry.format(value[key], innerOpts);
        length += sep.length + key.length + 2 + s.length;
        if (opts.clip && opts.clip < length) {
          parts.push(', ...');
          break;
        }
        parts.push(sep);
        parts.push(key);
        parts.push(': ');
        parts.push(s);
      }
      if (keys.length > 0) {
        parts.push(' ');
      }
      parts.push('}');
      return parts.join('');
    }
    if (typeof value.toString == 'function') {
      return value.toString();
    }
    return String(value);
  }
  throw new Error('Implement');
}

module.exports = formatObject;


/*
Copyright 2014 Lectorius, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Define keyczar as a module that can be loaded both by node require and a browser
/** @suppress {duplicate} */
var forge = forge || require('node-forge');
/** @suppress {duplicate} */
var keyczar_util = keyczar_util || require('./keyczar_util');

var keyczar = {};
(function() {
'use strict';

keyczar.TYPE_AES = 'AES';
keyczar.TYPE_RSA_PRIVATE = 'RSA_PRIV';
keyczar.TYPE_RSA_PUBLIC = 'RSA_PUB';
keyczar.PURPOSE_DECRYPT_ENCRYPT = 'DECRYPT_AND_ENCRYPT';
keyczar.PURPOSE_ENCRYPT = 'ENCRYPT';
keyczar.PURPOSE_VERIFY = 'VERIFY';
keyczar.PURPOSE_SIGN_VERIFY = 'SIGN_AND_VERIFY';
var STATUS_PRIMARY = 'PRIMARY';

// Java uses 4096, but C++ and Python use 2048.
var RSA_DEFAULT_BITS = 2048;
var AES_DEFAULT_BITS = 128;
var HMAC_DEFAULT_BITS = 256;

/**
@param {number=} size key length in bits
*/
function _generateAes(size) {
    if (!size) size = AES_DEFAULT_BITS;

    // generate random bytes for both AES and HMAC
    var keyBytes = forge.random.getBytes(size/8);
    var hmacBytes = forge.random.getBytes(HMAC_DEFAULT_BITS/8);
    return keyczar_util._aesFromBytes(keyBytes, hmacBytes);
}

/** Returns a new Keyczar key. Note: this is slow for RSA keys.
TODO: Support different types. Right now it generates asymmetric RSA keys.
TODO: Possibly generate the key in steps to avoid hanging a browser?
@param {string} type
@param {string=} purpose
@param {Object=} options
*/
keyczar.create = function(type, purpose, options) {
    if (!purpose) {
        purpose = keyczar.PURPOSE_DECRYPT_ENCRYPT;
    }
    if (!options) {
        options = {};
    }
    // TODO: Enforce a list of acceptable sizes
    if (!options.size) {
        options.size = null;
    }
    if (!options.name) {
        options.name = '';
    }

    var keyString = null;
    var size = options.size;
    if (type == keyczar.TYPE_RSA_PRIVATE) {
        if (!size) size = RSA_DEFAULT_BITS;

        var generator = forge.pki.rsa.createKeyPairGenerationState(size);
        // run until done
        forge.pki.rsa.stepKeyPairGenerationState(generator, 0);
        keyString = keyczar_util._rsaPrivateKeyToKeyczarJson(generator.keys.privateKey);
    } else if (type == keyczar.TYPE_AES) {
        keyString = _generateAes(size).toJson();
    } else {
        throw new Error('Unsupported key type: ' + type);
    }

    if (!(purpose == keyczar.PURPOSE_DECRYPT_ENCRYPT || purpose == keyczar.PURPOSE_SIGN_VERIFY)) {
        throw new Error('Unsupported purpose: ' + JSON.stringify(purpose, null, 2));
    }

    // Create the initial metadata
    var metadata = {
        name: options.name,
        purpose: purpose,
        type: type,
        encrypted: false,
        versions: [{
            exportable: false,
            status: STATUS_PRIMARY,
            versionNumber: 1
        }]
    };

    // TODO: This serializes/deserializes the keys; change _makeKeyczar to not parse strings?
    var data = {
        meta: JSON.stringify(metadata),
        "1": keyString
    };

    return _makeKeyczar(data);
};

// Return a new keyczar containing the public part of key, which must be an asymmetric key.
function _exportPublicKey(key) {
    var t = key.metadata.type;
    var p = key.metadata.purpose;
    if (!(t == keyczar.TYPE_RSA_PRIVATE && (p == keyczar.PURPOSE_DECRYPT_ENCRYPT || p == keyczar.PURPOSE_SIGN_VERIFY))) {
        throw new Error('Unsupported key type/purpose:' + t + '/' + p);
    }

    var publicPurpose = keyczar.PURPOSE_ENCRYPT;
    if (p == keyczar.PURPOSE_SIGN_VERIFY) {
        publicPurpose = keyczar.PURPOSE_VERIFY;
    }

    var metadata = {
        name: key.metadata.name,
        purpose: publicPurpose,
        type: keyczar.TYPE_RSA_PUBLIC,
        encrypted: false,
        // TODO: Probably should do a deep copy
        versions: key.metadata.versions
    };

    if (key.metadata.versions.length != 1) {
        throw new Error('TODO: Support key sets with multiple keys');
    }

    var primaryVersion = _getPrimaryVersion(key.metadata);

    var data = {
        meta: JSON.stringify(metadata)
    };
    data[String(primaryVersion)] = key.primary.exportPublicKeyJson();
    return _makeKeyczar(data);
}

/** Returns the key set contained in the JSON string serialized. If password is provided,
expects an encrypted key using a key derived from password.
@param {string} serialized key set in a JSON string.
@param {string=} password optional password used to encrypt the key.
*/
keyczar.fromJson = function(serialized, password) {
    var data = JSON.parse(serialized);
    return _makeKeyczar(data, password);
};

// find the primary version; ensure we don't have more than one
function _getPrimaryVersion(metadata) {
    var primaryVersion = null;
    for (var i = 0; i < metadata.versions.length; i++) {
        if (metadata.versions[i].status == STATUS_PRIMARY) {
            if (primaryVersion !== null) {
                throw new Error('Invalid key: multiple primary keys');
            }
            primaryVersion = metadata.versions[i].versionNumber;
        }
    }

    if (primaryVersion === null) {
        throw new Error('No primary key');
    }

    return primaryVersion;
}

var _PBE_CIPHER = 'AES128';
var _PBE_HMAC = 'HMAC_SHA1';
var _PBE_AES_KEY_BYTES = 16;

// PBKDF2 RFC 2898 recommends at least 8 bytes (64 bits) of salt
// http://tools.ietf.org/html/rfc2898#section-4
// but NIST recommends at least 16 bytes (128 bits; see Section 5.1)
// http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
var _SALT_BYTES = 16;

// NIST suggests count to be 1000 as a minimum, but that seems poor.
// 4 GPUs can do 3M attempts/second with 1000 iterations. See:
// http://blog.agilebits.com/2013/04/16/1password-hashcat-strong-master-passwords/
// 10000 iterations; 7 mixed case random letters = 9 days to crack
// 10000 iterations; 9 mixed case random letters = 193 years days to crack
// C++ Keyczar uses 4096 iterations by default (crypto_factory.cc)
var MIN_ITERATION_COUNT = 10000;

// We use 50000 iterations by default to increase brute force difficulty
// 7 mixed case random letters = 41 days to crack
// 9 mixed case random letters = 867 years to crack

// Timings from Chrome 28.0.1500.71 on a 2GHz i7 Mac:
//   10000 iterations ~= 83 ms/derivation
//   20000 iterations ~= 167 ms/derivation
//   50000 iterations ~= 407 ms/derivation
var _DEFAULT_ITERATIONS = 50000;

function _deriveKey(password, salt, iterationCount) {
    // check ! > 0 so that it fails for undefined
    if (!(iterationCount > 0)) {
        throw new Error('Invalid iterationCount: ' + iterationCount);
    }
    return forge.pkcs5.pbkdf2(
        password, salt, iterationCount, _PBE_AES_KEY_BYTES, forge.md.sha1.create());
}

var PBE_DECRYPT_FAILED_MESSAGE = 'AES decryption failed (password incorrect or data is corrupt?)';
function _decryptKey(keyString, password) {
    var data = JSON.parse(keyString);

    // derive the password key
    if (data.cipher != _PBE_CIPHER) {
        throw new Error('Unsupported encryption cipher: ' + data.cipher);
    }
    if (data.hmac != _PBE_HMAC) {
        throw new Error('Unsupported key derivation function: ' + data.hmac);
    }
    var iv = keyczar_util.decodeBase64Url(data.iv);
    var salt = keyczar_util.decodeBase64Url(data.salt);
    var key = keyczar_util.decodeBase64Url(data.key);

    var derivedKey = _deriveKey(password, salt, data.iterationCount);

    // decrypt the key with the derived key
    var cipher = forge.aes.startDecrypting(derivedKey, iv, null);
    cipher.update(new forge.util.ByteBuffer(key));
    var success = cipher.finish();
    if (!success) {
        throw new Error(PBE_DECRYPT_FAILED_MESSAGE);
    }

    var decryptedKeyBytes = cipher.output.getBytes();
    // unfortunately, this PBE format has no HMAC, so this could be garbage.
    // However, it is VERY unlikely we'll get garbage that happens to be valid
    // UTF-8-encoded JSON. This sanity check is very likely to catch errors
    try {
        var decryptedKeyString = forge.util.decodeUtf8(decryptedKeyBytes);
        var decryptedJSON = JSON.parse(decryptedKeyString);
        return decryptedKeyString;
    } catch (e) {
        // almost certainly any error is a password error
        throw new Error(PBE_DECRYPT_FAILED_MESSAGE);
    }
}

function _encryptKey(keyString, password) {
    // derive the key
    var iterationCount = _DEFAULT_ITERATIONS;
    var salt = forge.random.getBytes(_SALT_BYTES);
    var derivedKey = _deriveKey(password, salt, iterationCount);

    var iv = forge.random.getBytes(_PBE_AES_KEY_BYTES);
    var cipher = forge.aes.startEncrypting(derivedKey, iv, null);
    cipher.update(new forge.util.ByteBuffer(keyString));
    var success = cipher.finish();
    if (!success) {
        throw new Error('AES encryption failed');
    }

    var output = {
        salt: keyczar_util.encodeBase64Url(salt),
        iterationCount: iterationCount,
        hmac: _PBE_HMAC,

        cipher: _PBE_CIPHER,
        iv: keyczar_util.encodeBase64Url(iv),
        key: keyczar_util.encodeBase64Url(cipher.output.getBytes())
    };
    return JSON.stringify(output);
}

// Returns a Keyczar object from data.
/**
@param {*} data Deserialized JSON object containing the key
@param {string=} password Used to encrypt the key
*/
function _makeKeyczar(data, password) {
    var instance = {};

    instance.metadata = JSON.parse(data.meta);
    if (instance.metadata.encrypted !== false) {
        if (!password) {
            throw new Error('Key is encrypted; you must provide the password');
        }
        if (password.length === 0) {
            throw new Error('Must supply a password length > 0');
        }
    } else if (password) {
        throw new Error('Key is not encrypted but password provided');
    }

    var primaryVersion = _getPrimaryVersion(instance.metadata);
    var primaryKeyString = data[String(primaryVersion)];
    if (instance.metadata.encrypted) {
        primaryKeyString = _decryptKey(primaryKeyString, password);
    }

    var t = instance.metadata.type;
    var p = instance.metadata.purpose;
    if (t == keyczar.TYPE_RSA_PRIVATE) {
        instance.primary = keyczar_util.privateKeyFromKeyczar(primaryKeyString);
        instance.exportPublicKey = function() { return _exportPublicKey(instance); };
    } else if (t == keyczar.TYPE_RSA_PUBLIC) {
        instance.primary = keyczar_util.publicKeyFromKeyczar(primaryKeyString);
    } else if (t == keyczar.TYPE_AES && p == keyczar.PURPOSE_DECRYPT_ENCRYPT) {
        instance.primary = keyczar_util.aesFromKeyczar(primaryKeyString);
    } else {
        throw new Error('Unsupported key type: ' + t);
    }

    if (p == keyczar.PURPOSE_ENCRYPT || p == keyczar.PURPOSE_DECRYPT_ENCRYPT) {
        // Takes a raw byte string, returns a raw byte string
        instance.encryptBinary = function(plaintext) {
            // TODO: assert that plaintext does not contain special characters
            return instance.primary.encrypt(plaintext);
        };

        instance.encrypt = function(plaintext) {
            // encode as UTF-8 in case plaintext contains non-ASCII characters
            plaintext = forge.util.encodeUtf8(plaintext);
            var message = instance.encryptBinary(plaintext);
            message = keyczar_util.encodeBase64Url(message);
            return message;
        };

        // only include decryption if supported by this key type
        if (p == keyczar.PURPOSE_DECRYPT_ENCRYPT) {
            instance.decryptBinary = function(message) {
                return instance.primary.decrypt(message);
            };

            instance.decrypt = function(message) {
                message = keyczar_util.decodeBase64Url(message);
                var plaintext = instance.primary.decrypt(message);
                plaintext = forge.util.decodeUtf8(plaintext);
                return plaintext;
            };
        }
    } else if (p == keyczar.PURPOSE_VERIFY || p == keyczar.PURPOSE_SIGN_VERIFY) {
        instance.verify = function(message, signature) {
            message = forge.util.encodeUtf8(message);
            signature = keyczar_util.decodeBase64Url(signature);
            return instance.primary.verify(message, signature);
        };

        if (p == keyczar.PURPOSE_SIGN_VERIFY) {
            instance.sign = function(message) {
                message = forge.util.encodeUtf8(message);
                var signature = instance.primary.sign(message);
                return keyczar_util.encodeBase64Url(signature);
            };
        }
    }

    var _toJsonObject = function() {
        var out = {};
        out.meta = JSON.stringify(instance.metadata);

        // TODO: Store and serialize ALL keys. For now this works
        if (instance.metadata.versions.length != 1) {
            throw new Error('TODO: Support keyczars with multiple keys');
        }
        var primaryVersion = _getPrimaryVersion(instance.metadata);
        out[String(primaryVersion)] = instance.primary.toJson();
        return out;
    };

    // Returns the JSON serialization of this keyczar instance.
    instance.toJson = function() {
        if (instance.metadata.encrypted) {
            throw new Error('Key is encrypted; use toJsonEncrypted() instead');
        }
        var out = _toJsonObject();
        return JSON.stringify(out);
    };

    // Returns the decrypted version of this password-protected key.
    // WARNING: This is dangerous as it can be used to leak a password-protected key
    instance.exportDecryptedJson = function() {
        if (!instance.metadata.encrypted) {
            throw new Error('Key is not encrypted; use toJson() instead');
        }

        var unencrypted = _toJsonObject();

        // hack the metadata to mark it as unencrypted
        var meta = JSON.parse(unencrypted.meta);
        meta.encrypted = false;
        unencrypted.meta = JSON.stringify(meta);
        return JSON.stringify(unencrypted);
    };

    instance.toJsonEncrypted = function(password) {
        // TODO: Enforce some sort of minimum length?
        if (password.length === 0) {
            throw new Error('Password length must be > 0');
        }

        // get the unencrypted JSON object
        var unencrypted = _toJsonObject();

        // set metadata.encrypted = true
        var meta = JSON.parse(unencrypted.meta);
        meta.encrypted = true;
        unencrypted.meta = JSON.stringify(meta);

        // encrypt each key
        for (var property in unencrypted) {
            if (property == 'meta') continue;
            unencrypted[property] = _encryptKey(unencrypted[property], password);
        }

        return JSON.stringify(unencrypted);
    };

    return instance;
}

/**
@param {*} key key used to encrypt the session key
@param {string=} sessionMaterial existing session material for decryption
*/
keyczar.createSessionCrypter = function(key, sessionMaterial) {
    if (key.metadata.type != keyczar.TYPE_RSA_PRIVATE && key.metadata.type != keyczar.TYPE_RSA_PUBLIC) {
        throw new Error('Invalid key type for SessionCrypter: ' + key.metadata.type);
    }

    var sessionKey = null;
    var rawSessionMaterial = null;
    if (sessionMaterial) {
        // decrypt session key: not base64 encoded if leading byte is VERSION_BYTE
        if (sessionMaterial.charAt(0) == keyczar_util.VERSION_BYTE) {
            rawSessionMaterial = sessionMaterial;
        } else {
            rawSessionMaterial = keyczar_util.decodeBase64Url(sessionMaterial);
        }
        var decrypted = key.decryptBinary(rawSessionMaterial);
        var keyBytes = keyczar_util._unpackByteStrings(decrypted);

        sessionKey = keyczar_util._aesFromBytes(keyBytes[0], keyBytes[1]);
    } else {
        // generate the session key
        sessionKey = _generateAes();

        // encrypt the key
        var packed = sessionKey.pack();
        rawSessionMaterial = key.encryptBinary(packed);
    }

    var crypter = {
        rawSessionMaterial: rawSessionMaterial,
        sessionMaterial: keyczar_util.encodeBase64Url(rawSessionMaterial)
    };

    crypter.encryptBinary = function(plaintext) {
        // TODO: assert that plaintext has no special chars
        return sessionKey.encrypt(plaintext);
    };

    crypter.decryptBinary = function(message) {
        return sessionKey.decrypt(message);
    };

    sessionKey.sessionMaterial = sessionMaterial;
    return crypter;
};

// Returns a byte string containing (session material, session encryption).
// Convenience wrapper around a SessionCrypter.
keyczar.encryptWithSession = function(key, message) {
    var crypter = keyczar.createSessionCrypter(key);
    message = forge.util.encodeUtf8(message);
    var rawEncrypted = crypter.encryptBinary(message);
    var packed = keyczar_util._packByteStrings([crypter.rawSessionMaterial, rawEncrypted]);
    return keyczar_util.encodeBase64Url(packed);
};

keyczar.decryptWithSession = function(key, message) {
    message = keyczar_util.decodeBase64Url(message);
    var unpacked = keyczar_util._unpackByteStrings(message);
    var crypter = keyczar.createSessionCrypter(key, unpacked[0]);
    var plaintext = crypter.decryptBinary(unpacked[1]);
    return forge.util.decodeUtf8(plaintext);
};

if (typeof module !== 'undefined' && module.exports) {
    module.exports = keyczar;
}
})();


'use strict';

const property = require('@yr/property');
const runtime = require('@yr/runtime');

const DEFAULT_OPTIONS = {
  // Browser immutable by default
  immutable: runtime.isBrowser,
  merge: true
};

module.exports = set;

/**
 * Store 'value' at 'key'
 * @param {DataStore} store
 * @param {String} key
 * @param {*} value
 * @param {Object} [options]
 *  - {Boolean} immutable
 *  - {Boolean} merge
 * @returns {Boolean}
 */
function set(store, key, value, options) {
  return doSet(store, key, value, Object.assign({}, DEFAULT_OPTIONS, options));
}

/**
 * Store 'value' at 'key'
 * @param {DataStore} store
 * @param {String} key
 * @param {*} value
 * @param {Object} [options]
 *  - {Boolean} immutable
 *  - {Boolean} merge
 * @returns {Boolean}
 */
function doSet(store, key, value, options) {
  if (!key || typeof key !== 'string') {
    return false;
  }

  // Returns same if no change
  const newData = property.set(store._data, key, value, options);

  if (options.immutable) {
    if (newData !== store._data) {
      store._data = newData;
    } else {
      store.debug('WARNING no change after set "%s', key);
      return false;
    }
  }

  return true;
}


var registry = require('./registry');

/** Convert a map value to a printable string representation.
    @param {Map} value The value to format.
    @param {Object} options Formatting options.
    @protected
*/
function formatMap(value, options) {
  return 'Map(' + registry.format(Array.from(value.entries()), options) + ')';
}

module.exports = formatMap;


'use strict';

const agent = require('@yr/agent');
const get = require('./get');

const DEFAULT_LOAD_OPTIONS = {
  cacheControl: 'public, max-age=120, stale-if-error=180',
  rejectOnError: true,
  retry: 2,
  timeout: 5 * 1000
};
const RE_CACHE_CONTROL = /max-age=(\d+)|stale-if-error=(\d+)/g;

module.exports = fetch;

/**
 * Fetch data. If expired, load from 'url' and store at 'key'
 * @param {DataStore} store
 * @param {String} key
 * @param {String} url
 * @param {Object} options
 *  - {Boolean} abort
 *  - {String} cacheControl
 *  - {Boolean} ignoreQuery
 *  - {Number} retry
 *  - {Boolean} rejectOnError
 *  - {Number} timeout
 * @returns {Promise<Response>}
 */
function fetch(store, key, url, options) {
  options = Object.assign({}, DEFAULT_LOAD_OPTIONS, options);
  options.cacheControl = parseCacheControl(options.cacheControl);

  if (!key) {
    return Promise.resolve({
      body: undefined,
      duration: 0,
      headers: {},
      key,
      status: 400
    });
  }

  return doFetch(store, key, url, options);
}

/**
 * Fetch data. If expired, load from 'url' and store at 'key'
 * @param {DataStore} store
 * @param {String} key
 * @param {String} url
 * @param {Object} options
 *  - {Boolean} abort
 *  - {Object} cacheControl
 *  - {Boolean} ignoreQuery
 *  - {Number} retry
 *  - {Boolean} rejectOnError
 *  - {Number} timeout
 * @returns {Promise}
 */
function doFetch(store, key, url, options) {
  const { cacheControl, rejectOnError } = options;
  const value = get(store, key);
  const isMissing = !value;
  let isExpired = hasExpired(value && value[store.EXPIRY_KEY], false);

  store.debug('fetch %s from %s', key, url);

  // Load if missing or expired
  if (isMissing || isExpired) {
    if (!url) {
      return Promise.resolve({
        body: value,
        duration: 0,
        headers: {},
        key,
        status: 400
      });
    }

    return new Promise((resolve, reject) => {
      load(store, key, url, options)
        .then(res => {
          store.debug('fetched %s from %s', key, url);

          const body = get(store, key);

          resolve({
            body,
            duration: res.duration,
            headers: generateResponseHeaders(body[store.EXPIRY_KEY], cacheControl),
            key,
            status: res.status
          });
        })
        .catch(err => {
          // Abort if already destroyed
          if (store.destroyed) {
            return null;
          }

          isExpired = hasExpired(value && value[store.EXPIRY_KEY], true);

          if (rejectOnError && isExpired) {
            return reject(err);
          }

          store.debug('fetched stale %s after load error', key);
          return resolve({
            body: isExpired ? undefined : value,
            duration: 0,
            error: err,
            headers: generateResponseHeaders(value && value[store.EXPIRY_KEY], cacheControl, true),
            key,
            status: isExpired ? err.status : 200
          });
        });
    });
  }

  store.debug('fetched %s', key);
  return Promise.resolve({
    body: value,
    duration: 0,
    headers: generateResponseHeaders(value && value[store.EXPIRY_KEY], cacheControl),
    key,
    status: 200
  });
}

/**
 * Load data from 'url' and store at 'key'
 * @param {DataStore} store
 * @param {String} key
 * @param {String} url
 * @param {Object} options
 *  - {Boolean} abort
 *  - {Boolean} ignoreQuery
 *  - {Number} minExpiry
 *  - {Number} retry
 *  - {Boolean} staleIfError
 *  - {Number} timeout
 * @returns {Promise}
 */
function load(store, key, url, options) {
  const { cacheControl, rejectOnError, retry, timeout } = options;

  options.id = key;

  store.debug('load %s from %s', key, url);

  return agent
    .get(url, options)
    .timeout(timeout)
    .retry(retry)
    .then(res => {
      // Abort if already destroyed
      if (store.destroyed) {
        throw Error('store destroyed');
      }

      store.debug('loaded "%s" in %dms', key, res.duration);

      // Guard against empty data
      if (res.body) {
        // Parse cache-control headers
        if (res.headers && 'expires' in res.headers) {
          res.body[store.EXPIRY_KEY] = generateExpiry(res.headers, cacheControl);
        }

        // Enable handling by not calling inner set()
        store.set(key, res.body, options);
      }

      return res;
    })
    .catch(err => {
      // Abort if already destroyed
      if (store.destroyed) {
        throw err;
      }

      store.debug('unable to load "%s" from %s', key, url);

      if (rejectOnError) {
        store.set(key, undefined, options);
      }

      throw err;
    });
}

/**
 * Parse 'cacheControlString'
 * @param {String} cacheControlString
 * @returns {Object}
 */
function parseCacheControl(cacheControlString) {
  let maxAge = 0;
  let staleIfError = 0;

  if (cacheControlString && typeof cacheControlString === 'string') {
    let match;

    while ((match = RE_CACHE_CONTROL.exec(cacheControlString))) {
      if (match[1]) {
        maxAge = parseInt(match[1], 10) * 1000;
      } else if (match[2]) {
        staleIfError = parseInt(match[2], 10) * 1000;
      }
    }
  }

  return {
    maxAge,
    staleIfError
  };
}

/**
 * Merge 'cacheControl' with defaults
 * @param {Object} cacheControl
 * @param {Object} defaultCacheControl
 * @returns {Object}
 */
function mergeCacheControl(cacheControl, defaultCacheControl) {
  if (cacheControl == null) {
    return Object.assign({}, defaultCacheControl);
  }

  return {
    maxAge: 'maxAge' in cacheControl ? cacheControl.maxAge : defaultCacheControl.maxAge,
    staleIfError: 'staleIfError' in cacheControl ? cacheControl.staleIfError : defaultCacheControl.staleIfError
  };
}

/**
 * Generate expiry object from 'headers'
 * @param {Object} [headers]
 * @param {Object} defaultCacheControl
 * @returns {Number}
 */
function generateExpiry(headers = {}, defaultCacheControl) {
  const cacheControl = mergeCacheControl(parseCacheControl(headers['cache-control']), defaultCacheControl);

  const now = Date.now();
  let expires = now;

  if (headers.expires) {
    expires = typeof headers.expires === 'string' ? Number(new Date(headers.expires)) : headers.expires;
  }
  if (now >= expires) {
    expires = now + cacheControl.maxAge;
  }

  return {
    expires,
    expiresIfError: expires + cacheControl.staleIfError
  };
}

/**
 * Generate serialized headers object for response
 * @param {Object} [expiry]
 * @param {Object} defaultCacheControl
 * @param {Boolean} isError
 * @returns {Object}
 */
function generateResponseHeaders(expiry = {}, defaultCacheControl, isError) {
  const now = Date.now();
  let maxAge;

  if (isError) {
    maxAge =
      expiry && expiry.expiresIfError > now && expiry.expiresIfError - now < defaultCacheControl.maxAge
        ? Math.ceil((expiry.expiresIfError - now) / 1000)
        : defaultCacheControl.maxAge / 1000;
  } else {
    // Round up to nearest second
    maxAge =
      expiry && expiry.expires > now ? Math.ceil((expiry.expires - now) / 1000) : defaultCacheControl.maxAge / 1000;
  }

  return {
    // TODO: add stale-if-error
    'cache-control': `public, max-age=${maxAge}`,
    expires: new Date(now + maxAge * 1000).toUTCString()
  };
}

/**
 * Check if expired
 * @param {Object} expiry
 * @param {Boolean} isError
 * @returns {Boolean}
 */
function hasExpired(expiry, isError) {
  if (!expiry) {
    return true;
  }

  // Round up to nearest second
  return Math.ceil(Date.now() / 1000) * 1000 > (isError ? expiry.expiresIfError : expiry.expires);
}


var registry = require('./registry');

/** Convert a set value to a printable string representation.
    @param {Set} value The value to format.
    @param {Object} options Formatting options.
    @protected
*/
function formatSet(value, options) {
  return 'Set(' + registry.format(Array.from(value.values()), options) + ')';
}

module.exports = formatSet;


var registry = require('./registry');

/** Convert an array value to a printable string representation.
    @param {Array} value The value to format.
    @param {Object} options Formatting options.
    @protected
*/
function formatArray(value, options) {
  var opts = options || {};
  var innerOpts = Object.assign({}, opts, { clip: false });
  var parts = ['['];
  var length = 2; // Include both open and close bracket.
  for (var i = 0; i < value.length; ++i) {
    var sep = i > 0 ? ', ' : '';
    var s = registry.format(value[i], innerOpts);
    length += sep.length + s.length;
    if (opts.clip && opts.clip < length) {
      parts.push(', ...');
      break;
    }
    parts.push(sep);
    parts.push(s);
  }
  parts.push(']');
  return parts.join('');
}

module.exports = formatArray;


/// http.js --- Deals with HTTP requests in the browser
//
// Copyright (c) 2012 Quildreen Motta
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

/// Module iris.http

//// -- Dependencies ----------------------------------------------------------
var utils  = require('./utils')
var cassie = require('cassie')

//// -- Aliases ---------------------------------------------------------------
var keys               = Object.keys
var call               = Function.prototype.call
var to_array           = call.bind([].slice)
var class_of           = call.bind({}.toString)
var serialise          = utils.serialise
var build_query_string = utils.build_query_string
var register           = cassie.register
var Promise            = cassie.Promise

//// -- Helpers ---------------------------------------------------------------

///// Function make_xhr
// Creates a new XMLHttpRequest object that can be used for the current
// engine.
//
// make-xhr :: () -> XMLHttpRequest
var make_xhr = function() {
                 return 'XMLHttpRequest' in this?
                        /* W3C? */ function() {
                                     return new XMLHttpRequest() }
                        :
                        /* IE? */  function() {
                                     return new ActiveXObject('Microsoft.XMLHTTP') }}()

///// Function object_p
// Is the given `subject' an `Object'?
//
// object? :: a -> Bool
function object_p(subject) {
  return class_of(subject) == '[object Object]' }

///// Function status_type
// Returns the firendly HTTP status name for the class of HTTP statuses
// the given `status' belongs to.
//
// status-type :: Number -> String
function status_type(status) {
  var type = status.toString().charAt(0) - 1
  return statuses[type] }

///// Function serialise_for_type
// Serialises the given data according to the specified MIME type.
//
// serialise-for-type :: String, { String -> String } -> String
function serialise_for_type(mime, data) {
  return mime == 'application/json'?  JSON.stringify(data)
  :      /* otherwise */              serialise(data) }

///// Function normalise_status
// Normalises HTTP response statuses for IE.
//
// normalise-status :: Number -> Number
function normalise_status(status) {
  return status == 1223?   204
  :      /* otherwise */   status }

// Whether the engine supports XHR2's `timeout' attribute
//
// support-timeout? :: Bool
var support_timeout_p = 'timeout' in make_xhr()

// A regular expression matching successful HTTP response codes
//
// success :: RegExp
var success = /2\d{2}/

// A regular expression matching client and server error HTTP response
// codes.
//
// error :: RegExp
var error = /[45]\d{2}/

// A list of friendly name for the classes of HTTP status codes.
//
// statuses :: [String]
var statuses = [ 'information'
               , 'success'
               , 'redirected'
               , 'client-error'
               , 'server-error' ]

// A list of friendly name for a request's lifecycle's state.
//
// state-map :: [String]
var state_map = [ 'unsent'
                , 'opened'
                , 'headers-received'
                , 'loading'
                , 'completed' ]

//// -- Public interface ------------------------------------------------------

///// Data active
// A list of all active promises for HTTP requests.
//
// active :: [PromiseP]
var active = []

///// Object PromiseP <| Promise
// A promise for an HTTP request.
//
// PromiseP :: Promise <| { "client"  -> XMLHttpRequest
//                        , "uri"     -> String
//                        , "options" -> { String -> String }}
var PromiseP = Promise.derive({

  ////// Function init
  // Initialises an instance of a PromiseP.
  //
  // init! :: @this:Object* -> this
  init:
  function _init(client, uri, options) {
    Promise.init.call(this)
    this.client  = client
    this.uri     = uri
    this.options = options

    return this }

  ////// Function fire
  // Immediately invokes all functions registered for the given `event',
  // even if the promise hasn't been resolved yet.
  //
  // Different from flushing, the invoked callbacks are not removed from
  // the event's list of callbacks. So a callback may `fire' multiple
  // times.
  //
  // fire :: @this:PromiseP, String, Any... -> this
, fire:
  function _fire(event) {
    var args, callbacks, i, len
    args      = to_array(arguments, 1)
    callbacks = this.callbacks[event] || []

    for (i = 0, len = callbacks.length; i < len; ++i)
      callbacks[i].apply(this, args)

    return this }

  ////// Function forget
  // Aborts a request and resolves the promise with a `forgotten`
  // failure.
  //
  // forget :: @this:PromiseP* -> this
, forget:
  function _forget() {
    this.client.abort()
    return this.flush('forgotten', 'failed').fail('forgotten') }

  ////// Function timeout
  // Specifies the maximum amount of time (in seconds) the promise can
  // take to be fulfilled. If it takes more time than time, the promise
  // fails with a `timeout' error.
  //
  // timeout :: @this:PromiseP*, Number -> this
, timeout: support_timeout_p?  function _timeout(delay) {
                                 this.timeout = delay * 1000
                                 return this }

         : /* otherwise */     function _timeout(delay) {
                                 this.clearTimer()
                                 this.timer = setTimeout( function() {
                                                            this.flush('timeouted', 'failed')
                                                                .fail('timeouted')
                                                            this.forget() }.bind(this)
                                                        , delay * 1000 )
                                 return this }

  ////// Function clear_timer
  // Stops the timer for the promise. If one was previously set by
  // invoking `timeout'.
  //
  // clear-timer :: @this:Promise* -> this
, clearTimer: support_timeout_p?  function _clear_timer() {
                                     this.timeout = 0
                                     return this }

             : /* otherwise */     Promise.clearTimer

// Generalised HTTP statuses
, information : register('status:information')
, success     : register('status:success')
, redirected  : register('status:redirected')
, clientError : register('status:client-error')
, serverError : register('status:server-error')

// Ready states
, unsent          : register('state:unsent')
, opened          : register('state:opened')
, headersReceived : register('state:headers-received')
, loading         : register('state:loading')
, completed       : register('state:completed')

// General failure statuses
, errored : register('errored')
})
PromiseP.clear_timer = PromiseP.clearTimer

///// Function request
// Makes an HTTP request to the given URI, and returns a `PromiseP' that
// such request will be fulfilled.
//
// Any actual work is carried over after the promise is returned from
// this method. As such, the user can freely manipulate the promise
// object synchronously before the connection with the endpoint is even
// opened.
//
// Aside from the event queues flushed after the promise has been
// fulfilled (or failed), the promise will also fire events from time to
// time, or depending on certain occurrences — as soon as they
// happen. Callbacks registered for those events may be invoked more
// than once, and may be invoked before the promise is fulfilled.
//
// request :: String, { String -> String } -> PromiseP
function request(uri, options) {
  var client, promise, method, serialise_body_p, mime
  options          = options         || {}
  options.headers  = options.headers || {}
  method           = (options.method || 'GET').toUpperCase()
  uri              = build_uri(uri, options.query, options.body)

  options.headers['X-Requested-With'] = 'XMLHttpRequest'

  serialise_body_p = object_p(options.body)
  if (serialise_body_p) {
    mime = options.headers['Content-Type'] || 'application/x-www-form-urlencoded'
    options.body = serialise_for_type(mime, options.body)
    options.headers['Content-Type'] = mime }

  client  = make_xhr()
  promise = PromiseP.make(client, uri, options)

  setup_listeners()

  setTimeout(function() {
    client.open(method, uri, true, options.username, options.password)
    setup_headers(options.headers || {})
    client.send(options.body) })

  active.push(promise)

  return promise

  // Sticks a serialised query and body object at the end of an URI.
  // build-uri :: String, { String -> String }, { String -> String }? -> String
  function build_uri(uri, query, body) {
    uri = build_query_string(uri, query)
    return method == 'GET'?  build_query_string(uri, body)
    :      /* otherwise */   uri }

  // Setups the headers for the HTTP request
  // setup-headers :: { String -> String | [String] } -> Undefined
  function setup_headers(headers) {
    keys(headers).forEach(function(key) {
      client.setRequestHeader(key, headers[key]) })}

  // Generates a handler for the given type of error
  // make-error-handler :: String -> Event -> Undefined
  function make_error_handler(type) { return function(ev) {
    promise.flush(type, 'failed').fail(type, ev) }}

  // Invokes an error handler for the given type
  // raise :: String -> Undefined
  function raise(type) {
    make_error_handler(type)() }

  // Setups the event listeners for the HTTP request client
  // setup-listeners :: () -> Undefined
  function setup_listeners() {
    client.onerror            = make_error_handler('errored')
    client.onabort            = make_error_handler('forgotten')
    client.ontimeout          = make_error_handler('timeouted')
    client.onloadstart        = function(ev){ promise.fire('load:start', ev)    }
    client.onprogress         = function(ev){ promise.fire('load:progress', ev) }
    client.onloadend          = function(ev){ promise.fire('load:end', ev)      }
    client.onload             = function(ev){ promise.fire('load:success', ev)  }
    client.onreadystatechange = function(  ){
                                  var response, status, state
                                  state = client.readyState

                                  promise.fire('state:' + state_map[state])

                                  if (state == 4) {
                                    var binding_state = success.test(status)? 'ok'
                                                      : error.test(status)?   'failed'
                                                      : /* otherwise */       'any'

                                    response = client.responseText
                                    status   = normalise_status(client.status)
                                    active.splice(active.indexOf(promise), 1)
                                    promise.flush('status:' + status)
                                           .flush('status:' + status_type(status))

                                      status == 0?           raise('errored')
                                    : success.test(status)?  promise.bind(response, status)
                                    : error.test(status)?    promise.fail(response, status)
                                    : /* otherwise */        promise.done([response, status]) }}}
}

////// Function request_with_method
// Generates a specialised request function for the given method.
//
// request-with-method :: String -> String, { String -> String } -> PromiseP
function request_with_method(method) { return function(uri, options) {
  options = options || { }
  options.method = method.toUpperCase()
  return request(uri, options) }}

//// -- Exports ---------------------------------------------------------------
module.exports = { PromiseP: PromiseP
                 , request:  request
                 , active:   active
                 , get:      request_with_method('GET')
                 , post:     request_with_method('POST')
                 , put:      request_with_method('PUT')
                 , head:     request_with_method('HEAD')
                 , delete_:  request_with_method('DELETE')
                 , options:  request_with_method('OPTIONS')

                 , internal: { make_xhr: make_xhr }}


'use strict';

const WritableStream = require('stream').Writable;
const got = require('got');
const isOnline = require('is-online');
const pkg = require('./package.json');

let isOnlineCache;

function isOnlineAsPromise() {
    // Cache the isOnline() result up to `exports.connectivityCacheDuration` ms
    if (isOnlineCache && isOnlineCache.cachedAt > Date.now() - module.exports.connectivityCacheDuration) {
        return isOnlineCache;
    }

    isOnlineCache = new Promise((resolve, reject) => {
        isOnline((err, online) => {
            if (err) {
                delete isOnlineCache.promise;  // Do not cache errors
                /* istanbul ignore next */
                reject(err);
            } else {
                resolve(online);
            }
        });
    });

    isOnlineCache.cachedAt = Date.now();

    return isOnlineCache;
}

function checkConnectivity(requestErr) {
    return isOnlineAsPromise()
    .catch(() => { throw requestErr; })
    .then((online) => {
        if (!online) {
            throw requestErr;
        }

        return false;
    });
}

class DevNull extends WritableStream {
    _write(chunk, encoding, callback) {
        callback();
    }
}

function tryHead(link, gotOptions) {
    return got.head(link, gotOptions)
    .then(() => true);
}

function tryGet(link, options, gotOptions) {
    return new Promise((resolve, reject) => {
        let stream;
        let req;

        try {
            stream = got.stream(link, gotOptions);
        } catch (err) {
            return resolve(false);
        }

        stream
        .on('request', (req_) => { req = req_; })
        .on('response', (res) => {
            res.on('error', () => {});  // Swallow any response errors, because we are going to abort the request
            setImmediate(() => req.abort());
            resolve(true);
        })
        .on('error', (err, body, res) => {
            res && res.on('error', () => {});  // Swallow any response errors, because we are going to abort the request
            setImmediate(() => req.abort());

            if (err instanceof got.MaxRedirectsError || err instanceof got.HTTPError || err instanceof got.UnsupportedProtocolError) {
                return resolve(false);
            }

            /* istanbul ignore else */
            if (err instanceof got.RequestError) {
                return resolve(options.checkConnectivity ? checkConnectivity(err) : false);
            }

            /* istanbul ignore next */
            reject(err);
        })
        .pipe(new DevNull());
    });
}

// -------------------------------------------------------------------------

function isLinkWorking(link, options) {
    options = Object.assign({
        checkConnectivity: false,
        followRedirect: true,
        timeout: 15000,
        retries: 3,
        agent: null,
    }, options);

    const gotOptions = {
        timeout: options.timeout,
        followRedirect: options.followRedirect,
        retries: options.retries,
        agent: options.agent,
        headers: {
            'user-agent': `is-link-working/${pkg.version} (https://github.com/IndigoUnited/is-link-working)`,
        },
    };

    return tryHead(link, gotOptions)
    .catch(() => tryGet(link, options, gotOptions));
}

module.exports = isLinkWorking;
module.exports.connectivityCacheDuration = 5000;


/** @module certainty */
var Subject = require('./subject');
var ProxyBase = require('./proxy');

function isEmpty(obj) {
  /* eslint no-unused-vars: ["off"] */
  for (var x in obj) { return false; }
  return true;
}

/** A fluent context object containing the value of the field that was just tested. Used for
    additional assertions about a field.
    @constructor
  */
function FieldValue(subject, name, value) {
  this.subject = subject;
  this.name = name;
  this.value = subject.value[name];
  this.format = subject.format;
}

/** Ensure that the field has the expected value.
    @param {*} value The expected value of the field.
*/
FieldValue.prototype.withValue = function (expected) {
  if (this.value != expected) {
    this.subject.fail('Expected ' + this.subject.describe() + ' to have a field \'' + this.name +
      '\' with value ' + this.format(expected) + ', actual value was ' +
      this.format(this.value) + '.');
  }
  return this;
}
ProxyBase.addMethods(FieldValue);

// Used when the key is missing, so that .withValue() doesn't produce a second error message.
function MissingKeyValue() {}
MissingKeyValue.prototype.withValue = function (expected) {
  return this;
}

/** Subclass of Subject which provides assertions methods for object types.
    @param {FailureStrategy} failureStrategy The failure strategy to use when an assertion fails.
    @param {object} value The value being checked.
    @constructor
    @extends Subject
*/
function ObjectSubject(failureStrategy, value) {
  Subject.call(this, failureStrategy, value);
}
ObjectSubject.prototype = Object.create(Subject.prototype);
ObjectSubject.prototype.constructor = ObjectSubject;

/** Ensure that the object being tested is empty. This only checks for fields on the object, not
    its prototype.
    @return {ObjectSubject} `this` for chaining.
*/
ObjectSubject.prototype.isEmpty = function () {
  if (!isEmpty(this.value)) {
    this.fail('Expected ' + this.describe() + ' to be empty.');
  }
  return this;
};

/** Ensure that the object being tested is non-empty. This only checks for fields on the object,
    not its prototype.
    @return {ObjectSubject} `this` for chaining.
*/
ObjectSubject.prototype.isNotEmpty = function () {
  if (isEmpty(this.value)) {
    this.fail('Expected ' + this.describe() + ' to be non-empty.');
  }
  return this;
};

/** Ensure that the object being tested has a property (either its own or inherited) with the
    specified field name.
    @param {string} fieldName The name of the property.
    @return {FieldValue} FieldValue for additional assertions on the field value.
*/
ObjectSubject.prototype.hasField = function (fieldName) {
  if (!(fieldName in this.value)) {
    this.fail('Expected ' + this.describe() + ' to have a field named \'' +
      fieldName + '\'.');
    return new MissingKeyValue();
  }
  return new FieldValue(this, fieldName);
};

/** Ensure that the object being tested dows not have a property (either its own or inherited)
    with the specified field name.
    @param {string} fieldName The name of the property.
    @return {ObjectSubject} `this` for chaining.
*/
ObjectSubject.prototype.doesNotHaveField = function (fieldName) {
  if (fieldName in this.value) {
    this.fail('Expected ' + this.describe() + ' to not have a field named \'' +
      fieldName + '\'.');
  }
  return this;
};

/** Ensure that the object has a property with the given field name. The property must be one
    of the object's own properties, not one inherited from a prototype.
    @param {string} fieldName The name of the property.
    @return {FieldValue} FieldValue for additional assertions on the field value.
*/
ObjectSubject.prototype.hasOwnField = function (fieldName) {
  if (!Object.prototype.hasOwnProperty.call(this.value, fieldName)) {
    this.fail('Expected ' + this.describe() + ' to have own field named \'' +
      fieldName + '\'.');
  }
  return new FieldValue(this, fieldName);
};

/** Ensure that the object does not have it's own property with the given field name.
    @param {string} fieldName The name of the property.
    @return {ObjectSubject} `this` for chaining.
*/
ObjectSubject.prototype.doesNotHaveOwnField = function (fieldName) {
  if (Object.prototype.hasOwnProperty.call(this.value, fieldName)) {
    this.fail('Expected ' + this.describe() + ' to not have own field named \'' +
      fieldName + '\'.');
  }
  return this;
};

module.exports = ObjectSubject;


var _ = require('lodash'),
  bag = require('bagofrequest'),
  search = require('./endpoint/search'),
  getByListingId = require('./endpoint/getbylistingid'),
  report = require('./endpoint/report'),
  metadata = require('./endpoint/metadata'),
  endpoints = [search, getByListingId, report, metadata];

/**
 * class Sapi
 *
 * @param {String} key: Sensis API key, apply for the key here http://developers.sensis.com.au/docs/getting_started/Apply_for_an_API_key
 * @param {Object} opts: optional
 * - url: optional Sensis API base URL, defaults to http://developers.sensis.com.au/docs/using_endpoints
 */
function Sapi(key, opts) {
  opts = opts || {};

  function _authRequire(result, cb) {
    // SAPI does not differentiate required authentication and failed authentication
    // since both return status code 403, need to check key param existence to differentiate the two
    if (key) {
      cb(new Error('Authentication failed -  invalid key ' + key));
    } else {
      cb(new Error('Authentication required - set API key in Sapi constructor'));
    }
  }

  this.params = { key: key };
  this.url = (opts.url || 'http://api.sensis.com.au/ob-20110511/test').replace(/\/$/, '');
  this.opts = {
    handlers: {
      403: _authRequire
    }
  };
}

endpoints.forEach(function (endpoint) {

  var params = endpoint.params.required.concat(endpoint.params.optional);

  params.forEach(function (param) {
    /**
     * Chainable function for each parameter.
     * Usage pattern: sapi.param1().param2().param3().end();
     * The idea here is to allow the construction of 0...* parameters before calling the endpoint.
     *
     * @param {String} value: parameter value
     */
    Sapi.prototype[param] = function (value) {
      this.params[param] = value;
      return this;
    };
  });

  /**
   * End function for each endpoint that makes http request to SAPI.
   *
   * @param {Function} cb: standard cb(err, result) callback
   */
  Sapi.prototype[endpoint.name] = function (cb) {

    this.opts.queryStrings = this.params;
    this.opts.handlers = _.extend(this.opts.handlers, endpoint.handlers);

    bag.request('get', this.url + '/' + endpoint.path(this.params), this.opts, cb);
  };
});

module.exports = Sapi;


var esprima = require('esprima');
var syntaxError = require('syntax-error');

// LeakError copied from https://github.com/substack/node-syntax-error
function LeakError (opts, src, file) {
  Error.call(this);

  this.message = 'global leak detected: ' + opts.variable;

  this.line = opts.line - 1;
  this.column = opts.column;

  this.annotated = '\n'
    + (file || '(anonymous file)')
    + ':' + this.line
    + '\n'
    + src.split('\n')[this.line]
    + '\n'
    + Array(this.column + 1).join(' ') + '^'
    + '\n'
    + 'LeakError: ' + this.message
  ;
}

LeakError.prototype = new Error;

LeakError.prototype.toString = function () {
  return this.annotated;
};

LeakError.prototype.inspect = function () {
  return this.annotated;
};

function throwError(start, name, src, file) {
  var opts = {
    line: start.line
    , column: start.column
    , variable: name
  };
  throw new LeakError(opts, src, file);
}

function check(src, file) {
  src = src.replace(/^#![^\n]*/, '');
  src = '(function(){\n' + src + '\n})();';

  var err = syntaxError(src, file);
  if (err) {
    return err;
  }

  var obj = esprima.parse(src, {loc: true});

  var declared = [];
  var globals = Object.keys(global).concat(['errno', 'exports', 'module']);

  function getIds(node) {
    return node.declarations.map(function(child) {
      return child.id.name;
    });
  }

  function collectDeclarations(node) {
    var added = 0;

    if (node.id && node.id.name) {
      added++;
      declared.push(node.id.name);
      // deal with 'var a = function b() {}' where only 'a' should be defined as a variable
      if (node.init && node.init.type === 'FunctionExpression') {
        return added + collectDeclarations(node.init.body);
      }
    }

    if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
      return 0;
    }

    Object.keys(node).forEach(function(key) {
      var child = node[key];
      if (Array.isArray(child) || child && child.type) {
        added += collectDeclarations(child);
      }
    });
    return added;
  }

  function collectParams(node) {
    var params = node.params.map(function(param) {
      return param.name;
    })
    declared = declared.concat(params);
    return params.length;
  }

  function walk(node) {
    var name;

    if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'Program') {
      var added = collectDeclarations(node.body);
      if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
        added += collectParams(node);
      }
      walk(node.body);
      declared.length = declared.length - added;
      return;
    }

    if (node.type === 'AssignmentExpression' && node.left && node.left.name) {
      name = node.left.name;
      if (declared.indexOf(name) === -1 && globals.indexOf(name) === -1) {
        throwError(node.loc.start, name, src, file);
      }
    }

    Object.keys(node).forEach(function(key) {
      var child = node[key];
      if (Array.isArray(child)) {
        child.forEach(walk);
      } else if (child && typeof(child) === 'object' && child.type) {
        walk(child);
      }
    });
  }

  try {
    walk(obj);
  } catch(e) {

    if (e instanceof LeakError) {
      return e;
    }

    throw e;
  }
}

module.exports = check;
module.exports.LeakError = LeakError;


/**
 * Module dependencies.
 */
var util = require('util')
  , OAuth2Strategy = require('passport-oauth').OAuth2Strategy
  , InternalOAuthError = require('passport-oauth').InternalOAuthError
  , request = require('request');

/**
 * `Strategy` constructor.
 *
 * The FIWARE authentication strategy authenticates requests by delegating to
 * FIWARE using the OAuth 2.0 protocol.
 *
 * Applications must supply a `verify` callback which accepts an `accessToken`,
 * `refreshToken` and service-specific `profile`, and then calls the `done`
 * callback supplying a `user`, which should be set to `false` if the
 * credentials are not valid.  If an exception occured, `err` should be set.
 *
 * Options:
 *   - `clientID`      your FIWARE application's client id
 *   - `clientSecret`  your FIWARE application's client secret
 *   - `callbackURL`   URL to which FIWARE will redirect the user after granting authorization
 *   - `serverURL` URL of the FIWARE IdM to be used. If not provided the FIWARE Lab IdM is used
 *   - `isLegacy`      Whether the IDM version is lower than 7.0.0
 *
 * Examples:
 *
 *     passport.use(new FIWAREStrategy({
 *         clientID: '123-456-789',
 *         clientSecret: 'shhh-its-a-secret',
 *         callbackURL: 'https://www.example.net/auth/fiware/callback',
 *         isLegacy: false
 *       },
 *       function(accessToken, refreshToken, profile, done) {
 *         User.findOrCreate(..., function (err, user) {
 *           done(err, user);
 *         });
 *       }
 *     ));
 *
 * @param {Object} options
 * @param {Function} verify
 * @api public
 */
function Strategy(options, verify) {
  options = options || {};

  this.serverURL = options.serverURL || 'https://account.lab.fiware.org';
  this.isLegacy = options.isLegacy === true ? options.isLegacy : false;

  if (this.serverURL.endsWith('/')) {
    this.serverURL = this.serverURL.slice(0, -1);
  }

  options.authorizationURL = this.serverURL + '/oauth2/authorize';
  options.tokenURL = this.serverURL + '/oauth2/token';

  // Authorization: Basic BASE64(CLIENT_ID:CLIENT_SECRET)
  var authorizationHeader = 'Basic ' + new Buffer(options.clientID + ':' + options.clientSecret).toString('base64')

  options.customHeaders = {
    'Authorization': authorizationHeader
  }

  OAuth2Strategy.call(this, options, verify);
  this.name = 'fiware';
}

/**
 * Inherit from `OAuth2Strategy`.
 */
util.inherits(Strategy, OAuth2Strategy);

/**
 * Retrieve user profile from FIWARE.
 *
 * This function constructs a normalized profile, with the following properties:
 *
 *   - `provider`         always set to `fiware`
 *   - `id`
 *   - `username`
 *   - `displayName`
 *
 * @param {String} accessToken
 * @param {Function} done
 * @api protected
 */
Strategy.prototype.userProfile = function(accessToken, done) {
  let url = this.serverURL + '/user?access_token=' + accessToken
  let reqHandler = (err, body, res) => {
    if (err) {
      return done(new InternalOAuthError('failed to fetch user profile', err));
    }

    try {
      var json = JSON.parse(body)
        , i, len;

      var profile = { provider: 'fiware' };
      profile.id = json.id;
      profile.displayName = json.displayName;
      profile.emails = [
        {
          value: json.email
        }
      ];
      profile.email = json.email;
      profile.roles = json.roles;
      profile.organizations = json.organizations;
      profile.appId = json.app_id;

      // The entire profile
      profile._raw = body;
      profile._json = json;

      done(null, profile);
    } catch(e) {
      done(e);
    }
  }

  if (this.isLegacy) {
    this._oauth2.get(url, accessToken, reqHandler);
  } else {
    request({url: url, method: 'GET'}, (err, res, body) => {
      reqHandler(err, body, res)
    });
  }
}

/**
 * Expose `Strategy`.
 */
module.exports = Strategy;


'use strict';

const agent = require('@yr/agent');
const debugFactory = require('debug');
const fetch = require('./methods/fetch');
const get = require('./methods/get');
const HandlerContext = require('./HandlerContext');
const isPlainObject = require('is-plain-obj');
const runtime = require('@yr/runtime');
const set = require('./methods/set');

const HANDLED_METHODS = {
  reset: [reset, ['data']],
  set: [set, ['key', 'value', 'options']],
  fetch: [fetch, ['key', 'url', 'options']]
};
const EXPIRY_KEY = '__expiry';
const REF_KEY = '__ref:';

let uid = 0;

module.exports = class DataStore {
  /**
   * Constructor
   * @param {String} [id]
   * @param {Object} [data]
   * @param {Object} [options]
   *  - {Object} handlers
   *  - {Boolean} isWritable
   *  - {Object} serialisableKeys
   */
  constructor(id, data, options = {}) {
    this.EXPIRY_KEY = EXPIRY_KEY;
    this.REF_KEY = REF_KEY;

    this.changed = false;
    this.debug = debugFactory('yr:data' + (id ? ':' + id : ''));
    this.destroyed = false;
    this.id = id || `store${++uid}`;

    this._data = {};
    this._getCache = {};
    // Allow sub classes to send in methods for registration
    this._handledMethods = Object.assign({}, HANDLED_METHODS, options.handledMethods || {});
    this._handlers = [];
    this._isWritable = 'isWritable' in options ? options.isWritable : true;
    this._serialisableKeys = options.serialisableKeys || {};

    this.debug('created');

    if (options.handlers) {
      this.useHandler(options.handlers);
    }

    this.reset(data || {});
  }

  /**
   * Register 'handler' with optional key 'match'
   * Will handle every transaction if no 'match'
   * @param {RegExp|String|Array} [match]
   * @param {Function} handler
   */
  useHandler(match, handler) {
    const handlers = !Array.isArray(match) ? [[match, handler]] : match;
    let count = 0;

    handlers.forEach(([match, handler]) => {
      if (typeof match === 'function') {
        handler = match;
        match = '';
      }
      if (typeof handler === 'function') {
        this._handlers.push({ handler, match });
        count++;
      }
    });

    if (count) {
      this.debug(`using ${count} new handler${count > 1 ? 's' : ''}`);
    }
  }

  /**
   * Unregister 'handler'
   * @param {RegExp|String|Array} [match]
   * @param {Function} handler
   */
  unuseHandler(match, handler) {
    const matches = !Array.isArray(match) ? [[match, handler]] : match;

    for (let i = 0, n = matches.length; i < n; i++) {
      let [match, handler] = matches[i];
      let j = this._handlers.length;

      if (typeof match === 'function') {
        handler = match;
      }

      while (--j >= 0) {
        if (this._handlers[j].handler === handler) {
          this._handlers.splice(j, 1);
        }
      }
    }
  }

  /**
   * Set writeable state
   * @param {Boolean} value
   */
  setWriteable(value) {
    if (value !== this._isWritable) {
      this._isWritable = value;
      // Clear cache when toggling.
      // It would be more efficient to selectively invalidate keys,
      // but dangerous due to immutability and refs.
      this._getCache = {};
    }
  }

  /**
   * Retrieve value stored at 'key'
   * Empty/null/undefined 'key' returns all data
   * @param {String} [key]
   * @param {Object} [options]
   *  - {Boolean} resolveReferences
   * @returns {*}
   */
  get(key, options) {
    return get(this, key, options);
  }

  /**
   * Batch version of 'get()'
   * Accepts array of 'keys'
   * @param {Array} keys
   * @param {Object} [options]
   *  - {Boolean} resolveReferences
   * @returns {Array}
   */
  getAll(keys, options) {
    return keys.map(key => get(this, key, options));
  }

  /**
   * Store 'value' at 'key'
   * @param {String} key
   * @param {*} value
   * @param {Object} [options]
   *  - {Boolean} immutable
   *  - {Boolean} merge
   * @returns {void}
   */
  set(key, value, options) {
    if (!this._isWritable) {
      throw Error(`DataStore ${this.id} is not writeable`);
    }

    this.changed = this._routeHandledMethod('set', key, value, options);
  }

  /**
   * Batch version of 'set()'
   * Accepts hash of key/value pairs
   * @param {Object} keys
   * @param {Object} [options]
   *  - {Boolean} immutable
   *  - {Boolean} merge
   * @returns {void}
   */
  setAll(keys, options) {
    if (!this._isWritable) {
      throw Error(`DataStore ${this.id} is not writeable`);
    }

    let changed = false;

    if (isPlainObject(keys)) {
      for (const key in keys) {
        if (this._routeHandledMethod('set', key, keys[key], options)) {
          changed = true;
        }
      }
    }

    this.changed = changed;
  }

  /**
   * Retrieve reference to value stored at 'key'
   * @param {String} [key]
   * @returns {String}
   */
  reference(key) {
    if (!key) {
      return this.REF_KEY;
    }
    // Resolve back to original key if referenced
    key = this._resolveRefKey(key);
    return `${this.REF_KEY}${key}`;
  }

  /**
   * Batch version of 'reference()'
   * Accepts array of 'keys'
   * @param {Array<String>} keys
   * @returns {Array<String>}
   */
  referenceAll(keys) {
    return keys.map(key => this.reference(key));
  }

  /**
   * Retrieve unreferenced 'key'
   * @param {String} [key]
   * @returns {String}
   */
  unreference(key) {
    if (!key) {
      return '';
    }
    return this._isRefValue(key) ? this._parseRefKey(key) : key;
  }

  /**
   * Batch version of 'unreference()'
   * Accepts array of 'keys'
   * @param {Array<String>} keys
   * @returns {Array<String>}
   */
  unreferenceAll(keys) {
    return keys.map(key => this.unreference(key));
  }

  /**
   * Fetch data. If expired, load from 'url' and store at 'key'
   * @param {String|Object} key
   * @param {String} url
   * @param {Object} [options]
   *  - {Boolean} abort
   *  - {Boolean} ignoreQuery
   *  - {Number} minExpiry
   *  - {Number} retries
   *  - {Boolean} staleWhileRevalidate
   *  - {Boolean} staleIfError
   *  - {Number} timeout
   * @returns {Promise}
   */
  fetch(key, url, options) {
    // Handle passing existing response
    if (key != null && typeof key !== 'string' && 'body' in key && 'status' in key) {
      return Promise.resolve(key);
    }
    return this._routeHandledMethod('fetch', key, url, options);
  }

  /**
   * Batch version of 'fetch()'
   * Accepts an array of tuples [[key: String, url: String, options: Object]]
   * @param {Array<Array>} keys
   * @param {Object} [options]
   *  - {Boolean} abort
   *  - {Boolean} ignoreQuery
   *  - {Number} minExpiry
   *  - {Number} retry
   *  - {Boolean} staleWhileRevalidate
   *  - {Boolean} staleIfError
   *  - {Number} timeout
   * @returns {Promise<Array>}
   */
  fetchAll(keys, options) {
    if (Array.isArray(keys)) {
      return Promise.all(
        keys.map(args => {
          // Handle passing existing response
          if (args != null && !Array.isArray(args) && 'body' in args && 'status' in args) {
            return args;
          }

          const [key, url, opts = {}] = args;

          return this._routeHandledMethod('fetch', key, url, Object.assign({}, options, opts));
        })
      );
    }

    return Promise.resolve([]);
  }

  /**
   * Abort all outstanding load requests
   * @param {String} [key]
   */
  abort(key) {
    // Too dangerous to abort on server in case more than one outstanding request
    if (runtime.isBrowser) {
      agent.abortAll(key);
    }
  }

  /**
   * Reset underlying 'data'
   * @param {Object} data
   */
  reset(data) {
    this._routeHandledMethod('reset', data);
  }

  /**
   * Destroy instance
   */
  destroy() {
    if (!this.destroyed) {
      this.abort();
      this.destroyed = true;
      this._data = {};
      this._getCache = {};
      this._handledMethods = {};
      this._handlers = [];
      this._serialisableKeys = {};
      this.debug('destroyed');
      this.debug.destroy();
    }
  }

  /**
   * Store serialisability of 'key'
   * @param {String} key
   * @param {Boolean} value
   */
  setSerialisabilityOfKey(key, value) {
    if (key.charAt(0) === '/') {
      key = key.slice(1);
    }
    this._serialisableKeys[key] = value;
  }

  /**
   * Batch version of 'setSerialisabilityOfKey()'
   * Accepts hash of key/value pairs
   * @param {Object} keys
   */
  setSerialisabilityOfKeys(keys) {
    if (isPlainObject(keys)) {
      for (const key in keys) {
        this.setSerialisabilityOfKey(key, keys[key]);
      }
    }
  }

  /**
   * Dump all data, optionally stringified
   * @param {Boolean} stringify
   * @returns {Object|String}
   */
  dump(stringify) {
    const data = explode(this, this._data);

    if (stringify) {
      try {
        // Pretty print
        return JSON.stringify(data, null, 2);
      } catch (err) {
        return '';
      }
    }

    return data;
  }

  /**
   * Prepare for serialisation
   * @param {String} [key]
   * @returns {Object}
   */
  toJSON(key) {
    if (key) {
      return serialise(key, get(this, key), this._serialisableKeys);
    }
    return serialise(null, this._data, this._serialisableKeys);
  }

  /**
   * Determine if 'key' matches 'match'
   * @param {String} key
   * @param {RegExp|String} match
   * @returns {Boolean}
   */
  _isMatchKey(key, match) {
    // Treat no match as match all
    if (!match) {
      return true;
    }
    if (match instanceof RegExp) {
      return match.test(key);
    }
    if (typeof match === 'string') {
      return key.indexOf(match) === 0;
    }
    return false;
  }

  /**
   * Determine if 'value' is reference
   * @param {String} value
   * @returns {Boolean}
   */
  _isRefValue(value) {
    if (!value) {
      return false;
    }
    return typeof value === 'string' && value.indexOf(REF_KEY) === 0;
  }

  /**
   * Parse key from 'ref'
   * @param {String} ref
   * @returns {String}
   */
  _parseRefKey(ref) {
    if (typeof ref !== 'string') {
      return ref;
    }
    return ref.slice(REF_KEY.length);
  }

  /**
   * Resolve 'key' to nearest __ref key
   * @param {String} key
   * @returns {String}
   */
  _resolveRefKey(key) {
    // Handle passing of __ref key
    if (this._isRefValue(key)) {
      return this._parseRefKey(key);
    }

    const segs = key.split('/');
    const n = segs.length;
    let value = this._data;
    let idx = 0;
    let ref = key;

    // Walk data tree from root looking for nearest __ref
    while (idx < n) {
      if (value[segs[idx]] == null) {
        break;
      }
      value = value[segs[idx]];
      if (this._isRefValue(value)) {
        ref = this._parseRefKey(value);
        break;
      }
      idx++;
    }

    // Append relative keys
    if (ref !== key && idx < n - 1) {
      ref += `/${segs.slice(idx + 1).join('/')}`;
    }

    return ref;
  }

  /**
   * Route 'fn' through handlers
   * @param {String} methodName
   * @param {*} args
   * @returns {Object|null}
   */
  _routeHandledMethod(methodName, ...args) {
    const [fn, signature] = this._handledMethods[methodName];
    const isKeyed = signature[0] === 'key';
    let [key = '', ...rest] = args;

    if (isKeyed && key && key.charAt(0) === '/') {
      key = key.slice(1);
    }

    // Defer to handlers
    if (this._handlers.length) {
      const context = new HandlerContext(this, methodName, signature, args);

      for (let i = 0, n = this._handlers.length; i < n; i++) {
        if (this._isMatchKey(key, this._handlers[i].match)) {
          this._handlers[i].handler(context);
        }
      }

      const value = fn(this, ...context.toArguments());

      context.destroy();
      return value;
    }

    return fn(this, key, ...rest);
  }
};

/**
 * Reset underlying 'data'
 * @param {DataStore} store
 * @param {Object} data
 */
function reset(store, data) {
  store.debug('reset');
  store._data = data;
  store.changed = true;
}

/**
 * Retrieve serialisable 'data'
 * @param {String} key
 * @param {Object} data
 * @param {Object} config
 * @returns {Object}
 */
function serialise(key, data, config) {
  if (isPlainObject(data)) {
    const obj = {};

    for (const prop in data) {
      const keyChain = key ? `${key}/${prop}` : prop;
      const value = data[prop];

      if (config[keyChain] !== false) {
        if (isPlainObject(value)) {
          obj[prop] = serialise(keyChain, value, config);
        } else if (value != null && typeof value === 'object' && 'toJSON' in value) {
          obj[prop] = value.toJSON();
        } else {
          obj[prop] = value;
        }
      }
    }

    return obj;
  }

  return config[key] !== false ? data : null;
}

/**
 * Resolve all nested references for 'data'
 * @param {DataStore} store
 * @param {Object} data
 * @returns {Object}
 */
function explode(store, data) {
  if (isPlainObject(data)) {
    const obj = {};

    for (const prop in data) {
      obj[prop] = explode(store, data[prop]);
    }
    return obj;
  } else if (Array.isArray(data)) {
    return data.map(value => explode(store, value));
  } else if (store._isRefValue(data)) {
    return explode(store, store.get(store._parseRefKey(data)));
  }

  return data;
}


/// utils.js --- Utilities shared by all iris modules
//
// Copyright (c) 2012 Quildreen Motta
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

/// Module iris.utils

//// -- Aliases ---------------------------------------------------------------
var keys   = Object.keys
var encode = encodeURIComponent

//// --- Utilities ------------------------------------------------------------
function serialise(data) {
  return keys(data || {}).map(encode_pair).filter(Boolean).join('&')

  function encode_pair(key) {
    return data[key] != null?  encode(key) + '=' + encode(data[key])
    :      /* otherwise */     null }}

function build_query_string(uri, parameters) {
  var query = serialise(parameters || {})
  var sep   = /\?/.test(uri)?  '&' : '?'
  return query?           uri + sep + query
  :      /* otherwise */  uri }

//// -- Exports ---------------------------------------------------------------
module.exports = { serialise:          serialise
                 , build_query_string: build_query_string
                 , buildQueryString:   build_query_string
                 }