Bug 1384652 - Sync shield-recipe-client from GitHub (v60, commit abcf3c) r?Gijs draft
authorMike Cooper <mcooper@mozilla.com>
Wed, 26 Jul 2017 10:13:26 -0700
changeset 616058 9f5ceb29a3feaf3cbd97034717c7673ca3bfd7cb
parent 616057 add7f83758561c8d534b47af5e98379b4f2f19dd
child 639348 66d8af59ff952be22e43f47de8181a1cd385e19a
push id70554
push userbmo:mcooper@mozilla.com
push dateWed, 26 Jul 2017 17:18:29 +0000
reviewersGijs
bugs1384652
milestone56.0a1
Bug 1384652 - Sync shield-recipe-client from GitHub (v60, commit abcf3c) r?Gijs MozReview-Commit-ID: 4gvePmECerZ
browser/extensions/shield-recipe-client/bootstrap.js
browser/extensions/shield-recipe-client/install.rdf.in
browser/extensions/shield-recipe-client/jar.mn
browser/extensions/shield-recipe-client/lib/Addons.jsm
browser/extensions/shield-recipe-client/lib/ClientEnvironment.jsm
browser/extensions/shield-recipe-client/lib/FilterExpressions.jsm
browser/extensions/shield-recipe-client/lib/NormandyApi.jsm
browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm
browser/extensions/shield-recipe-client/lib/RecipeRunner.jsm
browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm
browser/extensions/shield-recipe-client/lib/StudyStorage.jsm
browser/extensions/shield-recipe-client/lib/Uptake.jsm
browser/extensions/shield-recipe-client/node_modules/jexl/LICENSE.txt
browser/extensions/shield-recipe-client/node_modules/jexl/lib/Jexl.js
browser/extensions/shield-recipe-client/node_modules/jexl/lib/Lexer.js
browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/Evaluator.js
browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/handlers.js
browser/extensions/shield-recipe-client/node_modules/jexl/lib/grammar.js
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/Parser.js
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/handlers.js
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/states.js
browser/extensions/shield-recipe-client/test/browser/.eslintrc.js
browser/extensions/shield-recipe-client/test/browser/browser.ini
browser/extensions/shield-recipe-client/test/browser/browser_ActionSandboxManager.js
browser/extensions/shield-recipe-client/test/browser/browser_ClientEnvironment.js
browser/extensions/shield-recipe-client/test/browser/browser_FilterExpressions.js
browser/extensions/shield-recipe-client/test/browser/browser_NormandyDriver.js
browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js
browser/extensions/shield-recipe-client/test/browser/browser_RecipeRunner.js
browser/extensions/shield-recipe-client/test/browser/browser_StudyStorage.js
browser/extensions/shield-recipe-client/test/browser/fixtures/addon-fixture/manifest.json
browser/extensions/shield-recipe-client/test/browser/fixtures/normandy.xpi
browser/extensions/shield-recipe-client/test/browser/head.js
browser/extensions/shield-recipe-client/test/unit/head_xpc.js
browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/api/v1/index.json
browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/index.json
browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/normandy.content-signature.mozilla.org-20210705.dev.chain
browser/extensions/shield-recipe-client/test/unit/test_ActionSandboxManager.js
browser/extensions/shield-recipe-client/test/unit/test_NormandyApi.js
browser/extensions/shield-recipe-client/test/unit/test_SandboxManager.js
browser/extensions/shield-recipe-client/test/unit/xpcshell.ini
browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY
browser/extensions/shield-recipe-client/vendor/ajv.js
browser/extensions/shield-recipe-client/vendor/mozjexl.js
--- a/browser/extensions/shield-recipe-client/bootstrap.js
+++ b/browser/extensions/shield-recipe-client/bootstrap.js
@@ -34,17 +34,19 @@ this.shutdown = function(data, reason) {
     "lib/NormandyApi.jsm",
     "lib/NormandyDriver.jsm",
     "lib/PreferenceExperiments.jsm",
     "lib/RecipeRunner.jsm",
     "lib/Sampling.jsm",
     "lib/SandboxManager.jsm",
     "lib/ShieldRecipeClient.jsm",
     "lib/Storage.jsm",
+    "lib/Uptake.jsm",
     "lib/Utils.jsm",
+    "vendor/mozjexl.js",
   ];
   for (const module of modules) {
     log.debug(`Unloading ${module}`);
     Cu.unload(`resource://shield-recipe-client/${module}`);
   }
 };
 
 this.uninstall = function() {};
--- a/browser/extensions/shield-recipe-client/install.rdf.in
+++ b/browser/extensions/shield-recipe-client/install.rdf.in
@@ -3,17 +3,17 @@
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>shield-recipe-client@mozilla.org</em:id>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
-    <em:version>55</em:version>
+    <em:version>60</em:version>
     <em:name>Shield Recipe Client</em:name>
     <em:description>Client to download and run recipes for SHIELD, Heartbeat, etc.</em:description>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
--- a/browser/extensions/shield-recipe-client/jar.mn
+++ b/browser/extensions/shield-recipe-client/jar.mn
@@ -1,9 +1,10 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 [features/shield-recipe-client@mozilla.org] chrome.jar:
 % resource shield-recipe-client %content/
   content/lib/ (./lib/*)
-  content/node_modules/jexl/ (./node_modules/jexl/*)
+  content/data/ (./data/*)
   content/skin/  (skin/*)
+  content/vendor/ (./vendor/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/Addons.jsm
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+
+this.EXPORTED_SYMBOLS = ["Addons"];
+
+/**
+ * SafeAddons store info about an add-on. They are single-depth
+ * objects to simplify cloning, and have no methods so they are safe
+ * to pass to sandboxes and filter expressions.
+ *
+ * @typedef {Object} SafeAddon
+ * @property {string} id
+ *   Add-on id, such as "shield-recipe-client@mozilla.com" or "{4ea51ac2-adf2-4af8-a69d-17b48c558a12}"
+ * @property {Date} installDate
+ * @property {boolean} isActive
+ * @property {string} name
+ * @property {string} type
+ *   "extension", "theme", etc.
+ * @property {string} version
+ */
+
+this.Addons = {
+  /**
+   * Get information about an installed add-on by ID.
+   *
+   * @param {string} addonId
+   * @returns {SafeAddon?} Add-on with given ID, or null if not found.
+   * @throws If addonId is not specified or not a string.
+   */
+  async get(addonId) {
+    const addon = await AddonManager.getAddonByID(addonId);
+    if (!addon) {
+      return null;
+    }
+    return this.serializeForSandbox(addon);
+  },
+
+  /**
+   * Get information about all installed add-ons.
+   * @async
+   * @returns {Array<SafeAddon>}
+   */
+  async getAll(addonId) {
+    const addons = await AddonManager.getAllAddons();
+    return addons.map(this.serializeForSandbox.bind(this));
+  },
+
+  /**
+   * Installs an add-on
+   * @prop installUrl {string} Url to download the .xpi for the add-on from.
+   * @async
+   * @returns {string} Add-on ID that was installed
+   * @throws {string} If the add-on can not be installed.
+   */
+  async install(installUrl) {
+    const installObj = await AddonManager.getInstallForURL(installUrl, null, "application/x-xpinstall");
+    const result = new Promise((resolve, reject) => installObj.addListener({
+      onInstallEnded(addonInstall, addon) {
+        resolve(addon.id);
+      },
+      onInstallFailed(addonInstall) {
+        reject(`AddonInstall error code: [${addonInstall.error}]`);
+      },
+      onDownloadFailed() {
+        reject(`Download failed: [${installUrl}]`);
+      },
+    }));
+    installObj.install();
+    return result;
+  },
+
+  /**
+   * Uninstalls an add-on by ID.
+   * @prop addonId {string} Add-on ID to uninstall.
+   * @async
+   * @throws If no add-on with `addonId` is installed.
+   */
+  async uninstall(addonId) {
+    const addon = await AddonManager.getAddonByID(addonId);
+    if (addon === null) {
+      throw new Error(`No addon with ID [${addonId}] found.`);
+    }
+    addon.uninstall();
+    return null;
+  },
+
+  /**
+   * Make a safe serialization of an add-on
+   * @param addon {Object} An add-on object as returned from AddonManager.
+   */
+  serializeForSandbox(addon) {
+    return {
+      id: addon.id,
+      installDate: new Date(addon.installDate),
+      isActive: addon.isActive,
+      name: addon.name,
+      type: addon.type,
+      version: addon.version,
+    };
+  },
+};
--- a/browser/extensions/shield-recipe-client/lib/ClientEnvironment.jsm
+++ b/browser/extensions/shield-recipe-client/lib/ClientEnvironment.jsm
@@ -11,19 +11,20 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyModuleGetter(this, "ShellService", "resource:///modules/ShellService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryArchive", "resource://gre/modules/TelemetryArchive.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NormandyApi", "resource://shield-recipe-client/lib/NormandyApi.jsm");
 XPCOMUtils.defineLazyModuleGetter(
     this,
     "PreferenceExperiments",
-    "resource://shield-recipe-client/lib/PreferenceExperiments.jsm",
+    "resource://shield-recipe-client/lib/PreferenceExperiments.jsm"
 );
 XPCOMUtils.defineLazyModuleGetter(this, "Utils", "resource://shield-recipe-client/lib/Utils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Addons", "resource://shield-recipe-client/lib/Addons.jsm");
 
 const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 
 this.EXPORTED_SYMBOLS = ["ClientEnvironment"];
 
 // Cached API request for client attributes that are determined by the Normandy
 // service.
 let _classifyRequest = null;
@@ -31,17 +32,17 @@ let _classifyRequest = null;
 this.ClientEnvironment = {
   /**
    * Fetches information about the client that is calculated on the server,
    * like geolocation and the current time.
    *
    * The server request is made lazily and is cached for the entire browser
    * session.
    */
-  getClientClassification() {
+  async getClientClassification() {
     if (!_classifyRequest) {
       _classifyRequest = NormandyApi.classifyClient();
     }
     return _classifyRequest;
   },
 
   clearClassifyCache() {
     _classifyRequest = null;
@@ -192,11 +193,16 @@ this.ClientEnvironment = {
         } else {
           names.active.push(experiment.name);
         }
       }
 
       return names;
     });
 
+    XPCOMUtils.defineLazyGetter(environment, "addons", async () => {
+      const addons = await Addons.getAll();
+      return Utils.keyBy(addons, "id");
+    });
+
     return environment;
   },
 };
--- a/browser/extensions/shield-recipe-client/lib/FilterExpressions.jsm
+++ b/browser/extensions/shield-recipe-client/lib/FilterExpressions.jsm
@@ -4,40 +4,62 @@
 
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://shield-recipe-client/lib/Sampling.jsm");
 Cu.import("resource://shield-recipe-client/lib/PreferenceFilters.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "mozjexl", "resource://shield-recipe-client/vendor/mozjexl.js");
+
 this.EXPORTED_SYMBOLS = ["FilterExpressions"];
 
-XPCOMUtils.defineLazyGetter(this, "nodeRequire", () => {
-  const {Loader, Require} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
-  const loader = new Loader({
-    paths: {
-      "": "resource://shield-recipe-client/node_modules/",
-    },
-  });
-  return new Require(loader, {});
-});
-
 XPCOMUtils.defineLazyGetter(this, "jexl", () => {
-  const {Jexl} = nodeRequire("jexl/lib/Jexl.js");
-  const jexl = new Jexl();
+  const jexl = new mozjexl.Jexl();
   jexl.addTransforms({
     date: dateString => new Date(dateString),
     stableSample: Sampling.stableSample,
     bucketSample: Sampling.bucketSample,
     preferenceValue: PreferenceFilters.preferenceValue,
     preferenceIsUserSet: PreferenceFilters.preferenceIsUserSet,
     preferenceExists: PreferenceFilters.preferenceExists,
+    keys,
   });
+  jexl.addBinaryOp("intersect", 40, operatorIntersect);
   return jexl;
 });
 
 this.FilterExpressions = {
   eval(expr, context = {}) {
     const onelineExpr = expr.replace(/[\t\n\r]/g, " ");
     return jexl.eval(onelineExpr, context);
   },
 };
+
+/**
+ * Return an array of the given object's own keys (specifically, its enumerable
+ * properties), or undefined if the argument isn't an object.
+ * @param {Object} obj
+ * @return {Array[String]|undefined}
+ */
+function keys(obj) {
+  if (typeof obj !== "object" || obj === null) {
+    return undefined;
+  }
+
+  return Object.keys(obj);
+}
+
+/**
+ * Find all the values that are present in both lists. Returns undefined if
+ * the arguments are not both Arrays.
+ * @param {Array} listA
+ * @param {Array} listB
+ * @return {Array|undefined}
+ */
+function operatorIntersect(listA, listB) {
+  if (!Array.isArray(listA) || !Array.isArray(listB)) {
+    return undefined;
+  }
+
+  return listA.filter(item => listB.includes(item));
+}
--- a/browser/extensions/shield-recipe-client/lib/NormandyApi.jsm
+++ b/browser/extensions/shield-recipe-client/lib/NormandyApi.jsm
@@ -1,29 +1,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/CanonicalJSON.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
-Cu.import("resource://shield-recipe-client/lib/Utils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(
+  this, "CanonicalJSON", "resource://gre/modules/CanonicalJSON.jsm");
+
 Cu.importGlobalProperties(["fetch", "URL"]); /* globals fetch, URL */
 
 this.EXPORTED_SYMBOLS = ["NormandyApi"];
 
 const log = LogManager.getLogger("normandy-api");
 const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
 
 let indexPromise = null;
 
 this.NormandyApi = {
+  InvalidSignatureError: class InvalidSignatureError extends Error {},
+
   clearIndexCache() {
     indexPromise = null;
   },
 
   apiCall(method, endpoint, data = {}) {
     const url = new URL(endpoint);
     method = method.toLowerCase();
 
@@ -58,17 +63,17 @@ this.NormandyApi = {
     } else if (url.startsWith("/")) {
       return server + url;
     }
     throw new Error("Can't use relative urls");
   },
 
   async getApiUrl(name) {
     if (!indexPromise) {
-      let apiBase = new URL(prefs.getCharPref("api_url"));
+      const apiBase = new URL(prefs.getCharPref("api_url"));
       if (!apiBase.pathname.endsWith("/")) {
         apiBase.pathname += "/";
       }
       indexPromise = this.get(apiBase.toString()).then(res => res.json());
     }
     const index = await indexPromise;
     if (!(name in index)) {
       throw new Error(`API endpoint with name "${name}" not found.`);
@@ -84,35 +89,42 @@ this.NormandyApi = {
     const recipesWithSigs = JSON.parse(rawText);
 
     const verifiedRecipes = [];
 
     for (const {recipe, signature: {signature, x5u}} of recipesWithSigs) {
       const serialized = CanonicalJSON.stringify(recipe);
       if (!rawText.includes(serialized)) {
         log.debug(rawText, serialized);
-        throw new Error("Canonical recipe serialization does not match!");
+        throw new NormandyApi.InvalidSignatureError("Canonical recipe serialization does not match!");
       }
 
-      const certChainResponse = await fetch(this.absolutify(x5u));
+      const certChainResponse = await this.get(this.absolutify(x5u));
       const certChain = await certChainResponse.text();
       const builtSignature = `p384ecdsa=${signature}`;
 
       const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
         .createInstance(Ci.nsIContentSignatureVerifier);
 
-      const valid = verifier.verifyContentSignature(
-        serialized,
-        builtSignature,
-        certChain,
-        "normandy.content-signature.mozilla.org"
-      );
+      let valid;
+      try {
+        valid = verifier.verifyContentSignature(
+          serialized,
+          builtSignature,
+          certChain,
+          "normandy.content-signature.mozilla.org"
+        );
+      } catch (err) {
+        throw new NormandyApi.InvalidSignatureError(`Recipe signature validation failed: ${err}`);
+      }
+
       if (!valid) {
-        throw new Error("Recipe signature is not valid");
+        throw new NormandyApi.InvalidSignatureError("Recipe signature is not valid");
       }
+
       verifiedRecipes.push(recipe);
     }
 
     log.debug(
       `Fetched ${verifiedRecipes.length} recipes from the server:`,
       verifiedRecipes.map(r => r.name).join(", ")
     );
 
--- a/browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm
+++ b/browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm
@@ -1,29 +1,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource:///modules/ShellService.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://shield-recipe-client/lib/Addons.jsm");
 Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
 Cu.import("resource://shield-recipe-client/lib/Storage.jsm");
 Cu.import("resource://shield-recipe-client/lib/Heartbeat.jsm");
 Cu.import("resource://shield-recipe-client/lib/FilterExpressions.jsm");
 Cu.import("resource://shield-recipe-client/lib/ClientEnvironment.jsm");
 Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm");
 Cu.import("resource://shield-recipe-client/lib/Sampling.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(
+  this, "StudyStorage", "resource://shield-recipe-client/lib/StudyStorage.jsm");
+
 const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 
 this.EXPORTED_SYMBOLS = ["NormandyDriver"];
 
 const log = LogManager.getLogger("normandy-driver");
 const actionLog = LogManager.getLogger("normandy-driver.actions");
 
 this.NormandyDriver = function(sandboxManager) {
@@ -151,22 +156,36 @@ this.NormandyDriver = function(sandboxMa
       return Cu.cloneInto(token, sandbox);
     },
 
     clearTimeout(token) {
       clearTimeout(token);
       sandboxManager.removeHold(`setTimeout-${token}`);
     },
 
+    addons: {
+      get: sandboxManager.wrapAsync(Addons.get.bind(Addons), {cloneInto: true}),
+      install: sandboxManager.wrapAsync(Addons.install.bind(Addons)),
+      uninstall: sandboxManager.wrapAsync(Addons.uninstall.bind(Addons)),
+    },
+
     // Sampling
     ratioSample: sandboxManager.wrapAsync(Sampling.ratioSample),
 
     // Preference Experiment API
     preferenceExperiments: {
       start: sandboxManager.wrapAsync(PreferenceExperiments.start, {cloneArguments: true}),
       markLastSeen: sandboxManager.wrapAsync(PreferenceExperiments.markLastSeen),
       stop: sandboxManager.wrapAsync(PreferenceExperiments.stop),
       get: sandboxManager.wrapAsync(PreferenceExperiments.get, {cloneInto: true}),
       getAllActive: sandboxManager.wrapAsync(PreferenceExperiments.getAllActive, {cloneInto: true}),
       has: sandboxManager.wrapAsync(PreferenceExperiments.has),
     },
+
+    // Study storage API
+    studies: {
+      create: sandboxManager.wrapAsync(StudyStorage.create, {cloneArguments: true}),
+      update: sandboxManager.wrapAsync(StudyStorage.update, {cloneArguments: true}),
+      get: sandboxManager.wrapAsync(StudyStorage.get, {cloneInto: true}),
+      has: sandboxManager.wrapAsync(StudyStorage.has),
+    },
   };
 };
--- a/browser/extensions/shield-recipe-client/lib/RecipeRunner.jsm
+++ b/browser/extensions/shield-recipe-client/lib/RecipeRunner.jsm
@@ -24,16 +24,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "SandboxManager",
                                   "resource://shield-recipe-client/lib/SandboxManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ClientEnvironment",
                                   "resource://shield-recipe-client/lib/ClientEnvironment.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CleanupManager",
                                   "resource://shield-recipe-client/lib/CleanupManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ActionSandboxManager",
                                   "resource://shield-recipe-client/lib/ActionSandboxManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "StudyStorage",
+                                  "resource://shield-recipe-client/lib/StudyStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Uptake",
+                                  "resource://shield-recipe-client/lib/Uptake.jsm");
 
 Cu.importGlobalProperties(["fetch"]);
 
 this.EXPORTED_SYMBOLS = ["RecipeRunner"];
 
 const log = LogManager.getLogger("recipe-runner");
 const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
 const TIMER_NAME = "recipe-client-addon-run";
@@ -97,106 +101,140 @@ this.RecipeRunner = {
     const runInterval = prefs.getIntPref(RUN_INTERVAL_PREF);
     timerManager.registerTimer(TIMER_NAME, () => this.run(), runInterval);
   },
 
   async run() {
     this.clearCaches();
     // Unless lazy classification is enabled, prep the classify cache.
     if (!Preferences.get("extensions.shield-recipe-client.experiments.lazy_classify", false)) {
-      await ClientEnvironment.getClientClassification();
+      try {
+        await ClientEnvironment.getClientClassification();
+      } catch (err) {
+        // Try to go on without this data; the filter expressions will
+        // gracefully fail without this info if they need it.
+      }
+    }
+
+    // Fetch recipes before execution in case we fail and exit early.
+    let recipes;
+    try {
+      recipes = await NormandyApi.fetchRecipes({enabled: true});
+    } catch (e) {
+      const apiUrl = prefs.getCharPref("api_url");
+      log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`);
+
+      let status = Uptake.RUNNER_SERVER_ERROR;
+      if (/NetworkError/.test(e)) {
+        status = Uptake.RUNNER_NETWORK_ERROR;
+      } else if (e instanceof NormandyApi.InvalidSignatureError) {
+        status = Uptake.RUNNER_INVALID_SIGNATURE;
+      }
+      Uptake.reportRunner(status);
+      return;
     }
 
     const actionSandboxManagers = await this.loadActionSandboxManagers();
     Object.values(actionSandboxManagers).forEach(manager => manager.addHold("recipeRunner"));
 
     // Run pre-execution hooks. If a hook fails, we don't run recipes with that
     // action to avoid inconsistencies.
     for (const [actionName, manager] of Object.entries(actionSandboxManagers)) {
       try {
         await manager.runAsyncCallback("preExecution");
         manager.disabled = false;
       } catch (err) {
         log.error(`Could not run pre-execution hook for ${actionName}:`, err.message);
         manager.disabled = true;
+        Uptake.reportAction(actionName, Uptake.ACTION_PRE_EXECUTION_ERROR);
       }
     }
 
-    // Fetch recipes from the API
-    let recipes;
-    try {
-      recipes = await NormandyApi.fetchRecipes({enabled: true});
-    } catch (e) {
-      const apiUrl = prefs.getCharPref("api_url");
-      log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`);
-      return;
-    }
-
     // Evaluate recipe filters
     const recipesToRun = [];
     for (const recipe of recipes) {
       if (await this.checkFilter(recipe)) {
         recipesToRun.push(recipe);
       }
     }
 
     // Execute recipes, if we have any.
     if (recipesToRun.length === 0) {
       log.debug("No recipes to execute");
     } else {
       for (const recipe of recipesToRun) {
         const manager = actionSandboxManagers[recipe.action];
+        let status;
         if (!manager) {
           log.error(
             `Could not execute recipe ${recipe.name}:`,
             `Action ${recipe.action} is either missing or invalid.`
           );
+          status = Uptake.RECIPE_INVALID_ACTION;
         } else if (manager.disabled) {
           log.warn(
             `Skipping recipe ${recipe.name} because ${recipe.action} failed during pre-execution.`
           );
+          status = Uptake.RECIPE_ACTION_DISABLED;
         } else {
           try {
             log.info(`Executing recipe "${recipe.name}" (action=${recipe.action})`);
             await manager.runAsyncCallback("action", recipe);
+            status = Uptake.RECIPE_SUCCESS;
           } catch (e) {
             log.error(`Could not execute recipe ${recipe.name}:`, e);
+            status = Uptake.RECIPE_EXECUTION_ERROR;
           }
         }
+
+        Uptake.reportRecipe(recipe.id, status);
       }
     }
 
     // Run post-execution hooks
     for (const [actionName, manager] of Object.entries(actionSandboxManagers)) {
       // Skip if pre-execution failed.
       if (manager.disabled) {
         log.info(`Skipping post-execution hook for ${actionName} due to earlier failure.`);
         continue;
       }
 
       try {
         await manager.runAsyncCallback("postExecution");
+        Uptake.reportAction(actionName, Uptake.ACTION_SUCCESS);
       } catch (err) {
         log.info(`Could not run post-execution hook for ${actionName}:`, err.message);
+        Uptake.reportAction(actionName, Uptake.ACTION_POST_EXECUTION_ERROR);
       }
     }
 
     // Nuke sandboxes
     Object.values(actionSandboxManagers).forEach(manager => manager.removeHold("recipeRunner"));
+
+    // Close storage connections
+    await StudyStorage.close();
+
+    Uptake.reportRunner(Uptake.RUNNER_SUCCESS);
   },
 
   async loadActionSandboxManagers() {
     const actions = await NormandyApi.fetchActions();
     const actionSandboxManagers = {};
     for (const action of actions) {
       try {
         const implementation = await NormandyApi.fetchImplementation(action);
         actionSandboxManagers[action.name] = new ActionSandboxManager(implementation);
       } catch (err) {
         log.warn(`Could not fetch implementation for ${action.name}:`, err);
+
+        let status = Uptake.ACTION_SERVER_ERROR;
+        if (/NetworkError/.test(err)) {
+          status = Uptake.ACTION_NETWORK_ERROR;
+        }
+        Uptake.reportAction(action.name, status);
       }
     }
     return actionSandboxManagers;
   },
 
   getFilterContext(recipe) {
     return {
       normandy: Object.assign(ClientEnvironment.getEnvironment(), {
--- a/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm
+++ b/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm
@@ -36,17 +36,16 @@ const DEFAULT_PREFS = {
   dev_mode: false,
   enabled: true,
   startup_delay_seconds: 300,
   "logging.level": Log.Level.Warn,
   user_id: "",
   run_interval_seconds: 86400, // 24 hours
 };
 const PREF_DEV_MODE = "extensions.shield-recipe-client.dev_mode";
-const PREF_SELF_SUPPORT_ENABLED = "browser.selfsupport.enabled";
 const PREF_LOGGING_LEVEL = PREF_BRANCH + "logging.level";
 
 let log = null;
 
 /**
  * Handles startup and shutdown of the entire add-on. Bootsrap.js defers to this
  * module for most tasks so that we can more easily test startup and shutdown
  * (bootstrap.js is difficult to import in tests).
@@ -58,44 +57,29 @@ this.ShieldRecipeClient = {
     // Setup logging and listen for changes to logging prefs
     LogManager.configure(Services.prefs.getIntPref(PREF_LOGGING_LEVEL));
     Preferences.observe(PREF_LOGGING_LEVEL, LogManager.configure);
     CleanupManager.addCleanupHandler(
       () => Preferences.ignore(PREF_LOGGING_LEVEL, LogManager.configure),
     );
     log = LogManager.getLogger("bootstrap");
 
-    // Disable self-support, since we replace its behavior.
-    // Self-support only checks its pref on start, so if we disable it, wait
-    // until next startup to run, unless the dev_mode preference is set.
-    if (Preferences.get(PREF_SELF_SUPPORT_ENABLED, true)) {
-      Preferences.set(PREF_SELF_SUPPORT_ENABLED, false);
-      if (!Preferences.get(PREF_DEV_MODE, false)) {
-        return;
-      }
-    }
-
     // Initialize experiments first to avoid a race between initializing prefs
     // and recipes rolling back pref changes when experiments end.
     try {
       await PreferenceExperiments.init();
     } catch (err) {
       log.error("Failed to initialize preference experiments:", err);
     }
 
     await RecipeRunner.init();
   },
 
   shutdown(reason) {
     CleanupManager.cleanup();
-
-    // Re-enable self-support if we're being disabled.
-    if (reason === REASONS.ADDON_DISABLE || reason === REASONS.ADDON_UNINSTALL) {
-      Services.prefs.setBoolPref(PREF_SELF_SUPPORT_ENABLED, true);
-    }
   },
 
   setDefaultPrefs() {
     for (const [key, val] of Object.entries(DEFAULT_PREFS)) {
       const fullKey = PREF_BRANCH + key;
       // If someone beat us to setting a default, don't overwrite it.
       if (!Preferences.isSet(fullKey)) {
         Preferences.set(fullKey, val);
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/StudyStorage.jsm
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * @typedef {Object} Study
+ * @property {string} name
+ *   Unique name of the study
+ * @property {string} version
+ *   Study add-on version number
+ * @property {string} description
+ *   Description of the study and its intent.
+ * @property {string} studyStartDate
+ *   ISO-formatted date string of when the study was started.
+ * @property {string} studyEndDate
+ *   ISO-formatted date string of when the study was ended.
+ * @property {string} addonId
+ *   Add-on ID for this particular study.
+ * @property {boolean} active
+ *   Is the study still running?
+ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB", "resource://gre/modules/IndexedDB.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ajv", "resource://shield-recipe-client/vendor/ajv.js");
+
+this.EXPORTED_SYMBOLS = ["StudyStorage"];
+
+const DB_NAME = "shield-studies";
+const DB_OPTIONS = {
+  version: 1,
+  storage: "persistent",
+};
+const DB_SCHEMA = {
+  type: "object",
+  additionalProperties: false,
+  properties: {
+    name: {
+      type: "string",
+      minLength: 1,
+    },
+    addonId: {
+      type: "string",
+      minLength: 1,
+    },
+    addonVersion: {
+      type: "string",
+      minLength: 1,
+    },
+    description: {
+      type: "string",
+      minLength: 1,
+    },
+    studyStartDate: {
+      type: "string",
+      minLength: 1,
+      format: "date-time",
+    },
+    studyEndDate: {
+      type: ["string", "null"],
+      minLength: 1,
+      format: "date-time",
+      default: null,
+    },
+    active: {
+      type: "boolean",
+      default: true,
+    },
+  },
+};
+const CREATE_SCHEMA = Object.assign({}, DB_SCHEMA, {
+  required: [
+    "name",
+    "addonId",
+    "addonVersion",
+    "description",
+    "studyStartDate",
+  ],
+});
+const UPDATE_SCHEMA = DB_SCHEMA;
+
+const ajvInstance = new ajv({
+  allErrors: true,
+  useDefaults: true,
+});
+const validateCreate = ajvInstance.compile(CREATE_SCHEMA);
+const validateUpdate = ajvInstance.compile(UPDATE_SCHEMA);
+
+/**
+ * Cache the database connection so that it is shared among multiple operations.
+ */
+let databasePromise;
+async function getDatabase() {
+  if (!databasePromise) {
+    databasePromise = IndexedDB.open(DB_NAME, DB_OPTIONS, db => {
+      db.createObjectStore(DB_NAME, {
+        keyPath: "name",
+      });
+    });
+  }
+  return databasePromise;
+}
+
+/**
+ * Get a transaction for interacting with the study store.
+ *
+ * NOTE: Methods on the store returned by this function MUST be called
+ * synchronously, otherwise the transaction with the store will expire.
+ * This is why the helper takes a database as an argument; if we fetched the
+ * database in the helper directly, the helper would be async and the
+ * transaction would expire before methods on the store were called.
+ */
+function getStore(db) {
+  return db.objectStore(DB_NAME, "readwrite");
+}
+
+this.StudyStorage = {
+  async clear() {
+    const db = await getDatabase();
+    await getStore(db).clear();
+  },
+
+  async close() {
+    if (databasePromise) {
+      const promise = databasePromise;
+      databasePromise = null;
+      const db = await promise;
+      await db.close();
+    }
+  },
+
+  async has(studyName) {
+    const db = await getDatabase();
+    const study = await getStore(db).get(studyName);
+    return !!study;
+  },
+
+  async get(studyName) {
+    const db = await getDatabase();
+    const study = await getStore(db).get(studyName);
+    if (!study) {
+      throw new Error(`Could not find a study named ${studyName}.`);
+    }
+
+    return study;
+  },
+
+  async create(study) {
+    if (!validateCreate(study)) {
+      throw new Error(
+        `Cannot create study: validation failed: ${ajvInstance.errorsText(validateCreate.errors)}`,
+      );
+    }
+
+    const db = await getDatabase();
+    if (await getStore(db).get(study.name)) {
+      throw new Error(
+        `Cannot create study with name ${study.name}: a study exists with that name already.`,
+      );
+    }
+
+    return getStore(db).add(study);
+  },
+
+  async update(studyName, data) {
+    const db = await getDatabase();
+    const savedStudy = await getStore(db).get(studyName);
+    if (!savedStudy) {
+      throw new Error(`Cannot update study ${studyName}: could not find study.`);
+    }
+
+    if (!validateUpdate(data)) {
+      throw new Error(`
+        Cannot update study ${studyName}: validation failed:
+        ${ajvInstance.errorsText(validateUpdate.errors)}
+      `);
+    }
+
+    return getStore(db).put(Object.assign({}, savedStudy, data));
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/lib/Uptake.jsm
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(
+  this, "UptakeTelemetry", "resource://services-common/uptake-telemetry.js");
+
+this.EXPORTED_SYMBOLS = ["Uptake"];
+
+const SOURCE_PREFIX = "shield-recipe-client";
+
+this.Uptake = {
+  // Action uptake
+  ACTION_NETWORK_ERROR: UptakeTelemetry.STATUS.NETWORK_ERROR,
+  ACTION_PRE_EXECUTION_ERROR: UptakeTelemetry.STATUS.CUSTOM_1_ERROR,
+  ACTION_POST_EXECUTION_ERROR: UptakeTelemetry.STATUS.CUSTOM_2_ERROR,
+  ACTION_SERVER_ERROR: UptakeTelemetry.STATUS.SERVER_ERROR,
+  ACTION_SUCCESS: UptakeTelemetry.STATUS.SUCCESS,
+
+  // Per-recipe uptake
+  RECIPE_ACTION_DISABLED: UptakeTelemetry.STATUS.CUSTOM_1_ERROR,
+  RECIPE_EXECUTION_ERROR: UptakeTelemetry.STATUS.APPLY_ERROR,
+  RECIPE_INVALID_ACTION: UptakeTelemetry.STATUS.DOWNLOAD_ERROR,
+  RECIPE_SUCCESS: UptakeTelemetry.STATUS.SUCCESS,
+
+  // Uptake for the runner as a whole
+  RUNNER_INVALID_SIGNATURE: UptakeTelemetry.STATUS.SIGNATURE_ERROR,
+  RUNNER_NETWORK_ERROR: UptakeTelemetry.STATUS.NETWORK_ERROR,
+  RUNNER_SERVER_ERROR: UptakeTelemetry.STATUS.SERVER_ERROR,
+  RUNNER_SUCCESS: UptakeTelemetry.STATUS.SUCCESS,
+
+  reportRunner(status) {
+    UptakeTelemetry.report(`${SOURCE_PREFIX}/runner`, status);
+  },
+
+  reportRecipe(recipeId, status) {
+    UptakeTelemetry.report(`${SOURCE_PREFIX}/recipe/${recipeId}`, status);
+  },
+
+  reportAction(actionName, status) {
+    UptakeTelemetry.report(`${SOURCE_PREFIX}/action/${actionName}`, status);
+  },
+};
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/LICENSE.txt
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/LICENSE.txt
@@ -1,19 +0,0 @@
-Copyright (c) 2015 TechnologyAdvice
-
-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.
\ No newline at end of file
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/Jexl.js
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/Jexl.js
@@ -1,225 +0,0 @@
-/*
- * Jexl
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-var Evaluator = require('./evaluator/Evaluator'),
-	Lexer = require('./Lexer'),
-	Parser = require('./parser/Parser'),
-	defaultGrammar = require('./grammar').elements;
-
-/**
- * Jexl is the Javascript Expression Language, capable of parsing and
- * evaluating basic to complex expression strings, combined with advanced
- * xpath-like drilldown into native Javascript objects.
- * @constructor
- */
-function Jexl() {
-	this._customGrammar = null;
-	this._lexer = null;
-	this._transforms = {};
-}
-
-/**
- * Adds a binary operator to Jexl at the specified precedence. The higher the
- * precedence, the earlier the operator is applied in the order of operations.
- * For example, * has a higher precedence than +, because multiplication comes
- * before division.
- *
- * Please see grammar.js for a listing of all default operators and their
- * precedence values in order to choose the appropriate precedence for the
- * new operator.
- * @param {string} operator The operator string to be added
- * @param {number} precedence The operator's precedence
- * @param {function} fn A function to run to calculate the result. The function
- *      will be called with two arguments: left and right, denoting the values
- *      on either side of the operator. It should return either the resulting
- *      value, or a Promise that resolves with the resulting value.
- */
-Jexl.prototype.addBinaryOp = function(operator, precedence, fn) {
-	this._addGrammarElement(operator, {
-		type: 'binaryOp',
-		precedence: precedence,
-		eval: fn
-	});
-};
-
-/**
- * Adds a unary operator to Jexl. Unary operators are currently only supported
- * on the left side of the value on which it will operate.
- * @param {string} operator The operator string to be added
- * @param {function} fn A function to run to calculate the result. The function
- *      will be called with one argument: the literal value to the right of the
- *      operator. It should return either the resulting value, or a Promise
- *      that resolves with the resulting value.
- */
-Jexl.prototype.addUnaryOp = function(operator, fn) {
-	this._addGrammarElement(operator, {
-		type: 'unaryOp',
-		weight: Infinity,
-		eval: fn
-	});
-};
-
-/**
- * Adds or replaces a transform function in this Jexl instance.
- * @param {string} name The name of the transform function, as it will be used
- *      within Jexl expressions
- * @param {function} fn The function to be executed when this transform is
- *      invoked.  It will be provided with two arguments:
- *          - {*} value: The value to be transformed
- *          - {{}} args: The arguments for this transform
- *          - {function} cb: A callback function to be called with an error
- *            if the transform fails, or a null first argument and the
- *            transformed value as the second argument on success.
- */
-Jexl.prototype.addTransform = function(name, fn) {
-	this._transforms[name] = fn;
-};
-
-/**
- * Syntactic sugar for calling {@link #addTransform} repeatedly.  This function
- * accepts a map of one or more transform names to their transform function.
- * @param {{}} map A map of transform names to transform functions
- */
-Jexl.prototype.addTransforms = function(map) {
-	for (var key in map) {
-		if (map.hasOwnProperty(key))
-			this._transforms[key] = map[key];
-	}
-};
-
-/**
- * Retrieves a previously set transform function.
- * @param {string} name The name of the transform function
- * @returns {function} The transform function
- */
-Jexl.prototype.getTransform = function(name) {
-	return this._transforms[name];
-};
-
-/**
- * Evaluates a Jexl string within an optional context.
- * @param {string} expression The Jexl expression to be evaluated
- * @param {Object} [context] A mapping of variables to values, which will be
- *      made accessible to the Jexl expression when evaluating it
- * @param {function} [cb] An optional callback function to be executed when
- *      evaluation is complete.  It will be supplied with two arguments:
- *          - {Error|null} err: Present if an error occurred
- *          - {*} result: The result of the evaluation
- * @returns {Promise<*>} resolves with the result of the evaluation.  Note that
- *      if a callback is supplied, the returned promise will already have
- *      a '.catch' attached to it in order to pass the error to the callback.
- */
-Jexl.prototype.eval = function(expression, context, cb) {
-	if (typeof context === 'function') {
-		cb = context;
-		context = {};
-	}
-	else if (!context)
-		context = {};
-	var valPromise = this._eval(expression, context);
-	if (cb) {
-		// setTimeout is used for the callback to break out of the Promise's
-		// try/catch in case the callback throws.
-		var called = false;
-		return valPromise.then(function(val) {
-			called = true;
-			setTimeout(cb.bind(null, null, val), 0);
-		}).catch(function(err) {
-			if (!called)
-				setTimeout(cb.bind(null, err), 0);
-		});
-	}
-	return valPromise;
-};
-
-/**
- * Removes a binary or unary operator from the Jexl grammar.
- * @param {string} operator The operator string to be removed
- */
-Jexl.prototype.removeOp = function(operator) {
-	var grammar = this._getCustomGrammar();
-	if (grammar[operator] && (grammar[operator].type == 'binaryOp' ||
-			grammar[operator].type == 'unaryOp')) {
-		delete grammar[operator];
-		this._lexer = null;
-	}
-};
-
-/**
- * Adds an element to the grammar map used by this Jexl instance, cloning
- * the default grammar first if necessary.
- * @param {string} str The key string to be added
- * @param {{type: <string>}} obj A map of configuration options for this
- *      grammar element
- * @private
- */
-Jexl.prototype._addGrammarElement = function(str, obj) {
-	var grammar = this._getCustomGrammar();
-	grammar[str] = obj;
-	this._lexer = null;
-};
-
-/**
- * Evaluates a Jexl string in the given context.
- * @param {string} exp The Jexl expression to be evaluated
- * @param {Object} [context] A mapping of variables to values, which will be
- *      made accessible to the Jexl expression when evaluating it
- * @returns {Promise<*>} resolves with the result of the evaluation.
- * @private
- */
-Jexl.prototype._eval = function(exp, context) {
-	var self = this,
-		grammar = this._getGrammar(),
-		parser = new Parser(grammar),
-		evaluator = new Evaluator(grammar, this._transforms, context);
-	return Promise.resolve().then(function() {
-		parser.addTokens(self._getLexer().tokenize(exp));
-		return evaluator.eval(parser.complete());
-	});
-};
-
-/**
- * Gets the custom grammar object, creating it first if necessary. New custom
- * grammars are created by executing a shallow clone of the default grammar
- * map. The returned map is available to be changed.
- * @returns {{}} a customizable grammar map.
- * @private
- */
-Jexl.prototype._getCustomGrammar = function() {
-	if (!this._customGrammar) {
-		this._customGrammar = {};
-		for (var key in defaultGrammar) {
-			if (defaultGrammar.hasOwnProperty(key))
-				this._customGrammar[key] = defaultGrammar[key];
-		}
-	}
-	return this._customGrammar;
-};
-
-/**
- * Gets the grammar map currently being used by Jexl; either the default map,
- * or a locally customized version. The returned map should never be changed
- * in any way.
- * @returns {{}} the grammar map currently in use.
- * @private
- */
-Jexl.prototype._getGrammar = function() {
-	return this._customGrammar || defaultGrammar;
-};
-
-/**
- * Gets a Lexer instance as a singleton in reference to this Jexl instance.
- * @returns {Lexer} an instance of Lexer, initialized with a grammar
- *      appropriate to this Jexl instance.
- * @private
- */
-Jexl.prototype._getLexer = function() {
-	if (!this._lexer)
-		this._lexer = new Lexer(this._getGrammar());
-	return this._lexer;
-};
-
-module.exports = new Jexl();
-module.exports.Jexl = Jexl;
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/Lexer.js
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/Lexer.js
@@ -1,244 +0,0 @@
-/*
- * Jexl
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-var numericRegex = /^-?(?:(?:[0-9]*\.[0-9]+)|[0-9]+)$/,
-	identRegex = /^[a-zA-Z_\$][a-zA-Z0-9_\$]*$/,
-	escEscRegex = /\\\\/,
-	preOpRegexElems = [
-		// Strings
-		"'(?:(?:\\\\')?[^'])*'",
-		'"(?:(?:\\\\")?[^"])*"',
-		// Whitespace
-		'\\s+',
-		// Booleans
-		'\\btrue\\b',
-		'\\bfalse\\b'
-	],
-	postOpRegexElems = [
-		// Identifiers
-		'\\b[a-zA-Z_\\$][a-zA-Z0-9_\\$]*\\b',
-		// Numerics (without negative symbol)
-		'(?:(?:[0-9]*\\.[0-9]+)|[0-9]+)'
-	],
-	minusNegatesAfter = ['binaryOp', 'unaryOp', 'openParen', 'openBracket',
-		'question', 'colon'];
-
-/**
- * Lexer is a collection of stateless, statically-accessed functions for the
- * lexical parsing of a Jexl string.  Its responsibility is to identify the
- * "parts of speech" of a Jexl expression, and tokenize and label each, but
- * to do only the most minimal syntax checking; the only errors the Lexer
- * should be concerned with are if it's unable to identify the utility of
- * any of its tokens.  Errors stemming from these tokens not being in a
- * sensible configuration should be left for the Parser to handle.
- * @type {{}}
- */
-function Lexer(grammar) {
-	this._grammar = grammar;
-}
-
-/**
- * Splits a Jexl expression string into an array of expression elements.
- * @param {string} str A Jexl expression string
- * @returns {Array<string>} An array of substrings defining the functional
- *      elements of the expression.
- */
-Lexer.prototype.getElements = function(str) {
-	var regex = this._getSplitRegex();
-	return str.split(regex).filter(function(elem) {
-		// Remove empty strings
-		return elem;
-	});
-};
-
-/**
- * Converts an array of expression elements into an array of tokens.  Note that
- * the resulting array may not equal the element array in length, as any
- * elements that consist only of whitespace get appended to the previous
- * token's "raw" property.  For the structure of a token object, please see
- * {@link Lexer#tokenize}.
- * @param {Array<string>} elements An array of Jexl expression elements to be
- *      converted to tokens
- * @returns {Array<{type, value, raw}>} an array of token objects.
- */
-Lexer.prototype.getTokens = function(elements) {
-	var tokens = [],
-		negate = false;
-	for (var i = 0; i < elements.length; i++) {
-		if (this._isWhitespace(elements[i])) {
-			if (tokens.length)
-				tokens[tokens.length - 1].raw += elements[i];
-		}
-		else if (elements[i] === '-' && this._isNegative(tokens))
-			negate = true;
-		else {
-			if (negate) {
-				elements[i] = '-' + elements[i];
-				negate = false;
-			}
-			tokens.push(this._createToken(elements[i]));
-		}
-	}
-	// Catch a - at the end of the string. Let the parser handle that issue.
-	if (negate)
-		tokens.push(this._createToken('-'));
-	return tokens;
-};
-
-/**
- * Converts a Jexl string into an array of tokens.  Each token is an object
- * in the following format:
- *
- *     {
- *         type: <string>,
- *         [name]: <string>,
- *         value: <boolean|number|string>,
- *         raw: <string>
- *     }
- *
- * Type is one of the following:
- *
- *      literal, identifier, binaryOp, unaryOp
- *
- * OR, if the token is a control character its type is the name of the element
- * defined in the Grammar.
- *
- * Name appears only if the token is a control string found in
- * {@link grammar#elements}, and is set to the name of the element.
- *
- * Value is the value of the token in the correct type (boolean or numeric as
- * appropriate). Raw is the string representation of this value taken directly
- * from the expression string, including any trailing spaces.
- * @param {string} str The Jexl string to be tokenized
- * @returns {Array<{type, value, raw}>} an array of token objects.
- * @throws {Error} if the provided string contains an invalid token.
- */
-Lexer.prototype.tokenize = function(str) {
-	var elements = this.getElements(str);
-	return this.getTokens(elements);
-};
-
-/**
- * Creates a new token object from an element of a Jexl string. See
- * {@link Lexer#tokenize} for a description of the token object.
- * @param {string} element The element from which a token should be made
- * @returns {{value: number|boolean|string, [name]: string, type: string,
- *      raw: string}} a token object describing the provided element.
- * @throws {Error} if the provided string is not a valid expression element.
- * @private
- */
-Lexer.prototype._createToken = function(element) {
-	var token = {
-		type: 'literal',
-		value: element,
-		raw: element
-	};
-	if (element[0] == '"' || element[0] == "'")
-		token.value = this._unquote(element);
-	else if (element.match(numericRegex))
-		token.value = parseFloat(element);
-	else if (element === 'true' || element === 'false')
-		token.value = element === 'true';
-	else if (this._grammar[element])
-		token.type = this._grammar[element].type;
-	else if (element.match(identRegex))
-		token.type = 'identifier';
-	else
-		throw new Error("Invalid expression token: " + element);
-	return token;
-};
-
-/**
- * Escapes a string so that it can be treated as a string literal within a
- * regular expression.
- * @param {string} str The string to be escaped
- * @returns {string} the RegExp-escaped string.
- * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
- * @private
- */
-Lexer.prototype._escapeRegExp = function(str) {
-	str = str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-	if (str.match(identRegex))
-		str = '\\b' + str + '\\b';
-	return str;
-};
-
-/**
- * Gets a RegEx object appropriate for splitting a Jexl string into its core
- * elements.
- * @returns {RegExp} An element-splitting RegExp object
- * @private
- */
-Lexer.prototype._getSplitRegex = function() {
-	if (!this._splitRegex) {
-		var elemArray = Object.keys(this._grammar);
-		// Sort by most characters to least, then regex escape each
-		elemArray = elemArray.sort(function(a ,b) {
-			return b.length - a.length;
-		}).map(function(elem) {
-			return this._escapeRegExp(elem);
-		}, this);
-		this._splitRegex = new RegExp('(' + [
-			preOpRegexElems.join('|'),
-			elemArray.join('|'),
-			postOpRegexElems.join('|')
-		].join('|') + ')');
-	}
-	return this._splitRegex;
-};
-
-/**
- * Determines whether the addition of a '-' token should be interpreted as a
- * negative symbol for an upcoming number, given an array of tokens already
- * processed.
- * @param {Array<Object>} tokens An array of tokens already processed
- * @returns {boolean} true if adding a '-' should be considered a negative
- *      symbol; false otherwise
- * @private
- */
-Lexer.prototype._isNegative = function(tokens) {
-	if (!tokens.length)
-		return true;
-	return minusNegatesAfter.some(function(type) {
-		return type === tokens[tokens.length - 1].type;
-	});
-};
-
-/**
- * A utility function to determine if a string consists of only space
- * characters.
- * @param {string} str A string to be tested
- * @returns {boolean} true if the string is empty or consists of only spaces;
- *      false otherwise.
- * @private
- */
-Lexer.prototype._isWhitespace = function(str) {
-	for (var i = 0; i < str.length; i++) {
-		if (str[i] != ' ')
-			return false;
-	}
-	return true;
-};
-
-/**
- * Removes the beginning and trailing quotes from a string, unescapes any
- * escaped quotes on its interior, and unescapes any escaped escape characters.
- * Note that this function is not defensive; it assumes that the provided
- * string is not empty, and that its first and last characters are actually
- * quotes.
- * @param {string} str A string whose first and last characters are quotes
- * @returns {string} a string with the surrounding quotes stripped and escapes
- *      properly processed.
- * @private
- */
-Lexer.prototype._unquote = function(str) {
-	var quote = str[0],
-		escQuoteRegex = new RegExp('\\\\' + quote, 'g');
-	return str.substr(1, str.length - 2)
-		.replace(escQuoteRegex, quote)
-		.replace(escEscRegex, '\\');
-};
-
-module.exports = Lexer;
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/Evaluator.js
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/Evaluator.js
@@ -1,153 +0,0 @@
-/*
- * Jexl
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-var handlers = require('./handlers');
-
-/**
- * The Evaluator takes a Jexl expression tree as generated by the
- * {@link Parser} and calculates its value within a given context. The
- * collection of transforms, context, and a relative context to be used as the
- * root for relative identifiers, are all specific to an Evaluator instance.
- * When any of these things change, a new instance is required.  However, a
- * single instance can be used to simultaneously evaluate many different
- * expressions, and does not have to be reinstantiated for each.
- * @param {{}} grammar A grammar map against which to evaluate the expression
- *      tree
- * @param {{}} [transforms] A map of transform names to transform functions. A
- *      transform function takes two arguments:
- *          - {*} val: A value to be transformed
- *          - {{}} args: A map of argument keys to their evaluated values, as
- *              specified in the expression string
- *      The transform function should return either the transformed value, or
- *      a Promises/A+ Promise object that resolves with the value and rejects
- *      or throws only when an unrecoverable error occurs. Transforms should
- *      generally return undefined when they don't make sense to be used on the
- *      given value type, rather than throw/reject. An error is only
- *      appropriate when the transform would normally return a value, but
- *      cannot due to some other failure.
- * @param {{}} [context] A map of variable keys to their values. This will be
- *      accessed to resolve the value of each non-relative identifier. Any
- *      Promise values will be passed to the expression as their resolved
- *      value.
- * @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed
- *      to resolve the value of a relative identifier.
- * @constructor
- */
-var Evaluator = function(grammar, transforms, context, relativeContext) {
-	this._grammar = grammar;
-	this._transforms = transforms || {};
-	this._context = context || {};
-	this._relContext = relativeContext || this._context;
-};
-
-/**
- * Evaluates an expression tree within the configured context.
- * @param {{}} ast An expression tree object
- * @returns {Promise<*>} resolves with the resulting value of the expression.
- */
-Evaluator.prototype.eval = function(ast) {
-	var self = this;
-	return Promise.resolve().then(function() {
-		return handlers[ast.type].call(self, ast);
-	});
-};
-
-/**
- * Simultaneously evaluates each expression within an array, and delivers the
- * response as an array with the resulting values at the same indexes as their
- * originating expressions.
- * @param {Array<string>} arr An array of expression strings to be evaluated
- * @returns {Promise<Array<{}>>} resolves with the result array
- */
-Evaluator.prototype.evalArray = function(arr) {
-	return Promise.all(arr.map(function(elem) {
-		return this.eval(elem);
-	}, this));
-};
-
-/**
- * Simultaneously evaluates each expression within a map, and delivers the
- * response as a map with the same keys, but with the evaluated result for each
- * as their value.
- * @param {{}} map A map of expression names to expression trees to be
- *      evaluated
- * @returns {Promise<{}>} resolves with the result map.
- */
-Evaluator.prototype.evalMap = function(map) {
-	var keys = Object.keys(map),
-		result = {};
-	var asts = keys.map(function(key) {
-		return this.eval(map[key]);
-	}, this);
-	return Promise.all(asts).then(function(vals) {
-		vals.forEach(function(val, idx) {
-			result[keys[idx]] = val;
-		});
-		return result;
-	});
-};
-
-/**
- * Applies a filter expression with relative identifier elements to a subject.
- * The intent is for the subject to be an array of subjects that will be
- * individually used as the relative context against the provided expression
- * tree. Only the elements whose expressions result in a truthy value will be
- * included in the resulting array.
- *
- * If the subject is not an array of values, it will be converted to a single-
- * element array before running the filter.
- * @param {*} subject The value to be filtered; usually an array. If this value is
- *      not an array, it will be converted to an array with this value as the
- *      only element.
- * @param {{}} expr The expression tree to run against each subject. If the
- *      tree evaluates to a truthy result, then the value will be included in
- *      the returned array; otherwise, it will be eliminated.
- * @returns {Promise<Array>} resolves with an array of values that passed the
- *      expression filter.
- * @private
- */
-Evaluator.prototype._filterRelative = function(subject, expr) {
-	var promises = [];
-	if (!Array.isArray(subject))
-		subject = [subject];
-	subject.forEach(function(elem) {
-		var evalInst = new Evaluator(this._grammar, this._transforms,
-			this._context, elem);
-		promises.push(evalInst.eval(expr));
-	}, this);
-	return Promise.all(promises).then(function(values) {
-		var results = [];
-		values.forEach(function(value, idx) {
-			if (value)
-				results.push(subject[idx]);
-		});
-		return results;
-	});
-};
-
-/**
- * Applies a static filter expression to a subject value.  If the filter
- * expression evaluates to boolean true, the subject is returned; if false,
- * undefined.
- *
- * For any other resulting value of the expression, this function will attempt
- * to respond with the property at that name or index of the subject.
- * @param {*} subject The value to be filtered.  Usually an Array (for which
- *      the expression would generally resolve to a numeric index) or an
- *      Object (for which the expression would generally resolve to a string
- *      indicating a property name)
- * @param {{}} expr The expression tree to run against the subject
- * @returns {Promise<*>} resolves with the value of the drill-down.
- * @private
- */
-Evaluator.prototype._filterStatic = function(subject, expr) {
-	return this.eval(expr).then(function(res) {
-		if (typeof res === 'boolean')
-			return res ? subject : undefined;
-		return subject[res];
-	});
-};
-
-module.exports = Evaluator;
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/handlers.js
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/handlers.js
@@ -1,159 +0,0 @@
-/*
- * Jexl
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-/**
- * Evaluates an ArrayLiteral by returning its value, with each element
- * independently run through the evaluator.
- * @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an
- *      ObjectLiteral as the top node
- * @returns {Promise.<[]>} resolves to a map contained evaluated values.
- * @private
- */
-exports.ArrayLiteral = function(ast) {
-	return this.evalArray(ast.value);
-};
-
-/**
- * Evaluates a BinaryExpression node by running the Grammar's evaluator for
- * the given operator.
- * @param {{type: 'BinaryExpression', operator: <string>, left: {},
- *      right: {}}} ast An expression tree with a BinaryExpression as the top
- *      node
- * @returns {Promise<*>} resolves with the value of the BinaryExpression.
- * @private
- */
-exports.BinaryExpression = function(ast) {
-	var self = this;
-	return Promise.all([
-		this.eval(ast.left),
-		this.eval(ast.right)
-	]).then(function(arr) {
-		return self._grammar[ast.operator].eval(arr[0], arr[1]);
-	});
-};
-
-/**
- * Evaluates a ConditionalExpression node by first evaluating its test branch,
- * and resolving with the consequent branch if the test is truthy, or the
- * alternate branch if it is not. If there is no consequent branch, the test
- * result will be used instead.
- * @param {{type: 'ConditionalExpression', test: {}, consequent: {},
- *      alternate: {}}} ast An expression tree with a ConditionalExpression as
- *      the top node
- * @private
- */
-exports.ConditionalExpression = function(ast) {
-	var self = this;
-	return this.eval(ast.test).then(function(res) {
-		if (res) {
-			if (ast.consequent)
-				return self.eval(ast.consequent);
-			return res;
-		}
-		return self.eval(ast.alternate);
-	});
-};
-
-/**
- * Evaluates a FilterExpression by applying it to the subject value.
- * @param {{type: 'FilterExpression', relative: <boolean>, expr: {},
- *      subject: {}}} ast An expression tree with a FilterExpression as the top
- *      node
- * @returns {Promise<*>} resolves with the value of the FilterExpression.
- * @private
- */
-exports.FilterExpression = function(ast) {
-	var self = this;
-	return this.eval(ast.subject).then(function(subject) {
-		if (ast.relative)
-			return self._filterRelative(subject, ast.expr);
-		return self._filterStatic(subject, ast.expr);
-	});
-};
-
-/**
- * Evaluates an Identifier by either stemming from the evaluated 'from'
- * expression tree or accessing the context provided when this Evaluator was
- * constructed.
- * @param {{type: 'Identifier', value: <string>, [from]: {}}} ast An expression
- *      tree with an Identifier as the top node
- * @returns {Promise<*>|*} either the identifier's value, or a Promise that
- *      will resolve with the identifier's value.
- * @private
- */
-exports.Identifier = function(ast) {
-	if (ast.from) {
-		return this.eval(ast.from).then(function(context) {
-			if (context === undefined)
-				return undefined;
-			if (Array.isArray(context))
-				context = context[0];
-			return context[ast.value];
-		});
-	}
-	else {
-		return ast.relative ? this._relContext[ast.value] :
-			this._context[ast.value];
-	}
-};
-
-/**
- * Evaluates a Literal by returning its value property.
- * @param {{type: 'Literal', value: <string|number|boolean>}} ast An expression
- *      tree with a Literal as its only node
- * @returns {string|number|boolean} The value of the Literal node
- * @private
- */
-exports.Literal = function(ast) {
-	return ast.value;
-};
-
-/**
- * Evaluates an ObjectLiteral by returning its value, with each key
- * independently run through the evaluator.
- * @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an
- *      ObjectLiteral as the top node
- * @returns {Promise<{}>} resolves to a map contained evaluated values.
- * @private
- */
-exports.ObjectLiteral = function(ast) {
-	return this.evalMap(ast.value);
-};
-
-/**
- * Evaluates a Transform node by applying a function from the transforms map
- * to the subject value.
- * @param {{type: 'Transform', name: <string>, subject: {}}} ast An
- *      expression tree with a Transform as the top node
- * @returns {Promise<*>|*} the value of the transformation, or a Promise that
- *      will resolve with the transformed value.
- * @private
- */
-exports.Transform = function(ast) {
-	var transform = this._transforms[ast.name];
-	if (!transform)
-		throw new Error("Transform '" + ast.name + "' is not defined.");
-	return Promise.all([
-		this.eval(ast.subject),
-		this.evalArray(ast.args || [])
-	]).then(function(arr) {
-		return transform.apply(null, [arr[0]].concat(arr[1]));
-	});
-};
-
-/**
- * Evaluates a Unary expression by passing the right side through the
- * operator's eval function.
- * @param {{type: 'UnaryExpression', operator: <string>, right: {}}} ast An
- *      expression tree with a UnaryExpression as the top node
- * @returns {Promise<*>} resolves with the value of the UnaryExpression.
- * @constructor
- */
-exports.UnaryExpression = function(ast) {
-	var self = this;
-	return this.eval(ast.right).then(function(right) {
-		return self._grammar[ast.operator].eval(right);
-	});
-};
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/grammar.js
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/grammar.js
@@ -1,66 +0,0 @@
-/*
- * Jexl
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-/**
- * A map of all expression elements to their properties. Note that changes
- * here may require changes in the Lexer or Parser.
- * @type {{}}
- */
-exports.elements = {
-	'.': {type: 'dot'},
-	'[': {type: 'openBracket'},
-	']': {type: 'closeBracket'},
-	'|': {type: 'pipe'},
-	'{': {type: 'openCurl'},
-	'}': {type: 'closeCurl'},
-	':': {type: 'colon'},
-	',': {type: 'comma'},
-	'(': {type: 'openParen'},
-	')': {type: 'closeParen'},
-	'?': {type: 'question'},
-	'+': {type: 'binaryOp', precedence: 30,
-		eval: function(left, right) { return left + right; }},
-	'-': {type: 'binaryOp', precedence: 30,
-		eval: function(left, right) { return left - right; }},
-	'*': {type: 'binaryOp', precedence: 40,
-		eval: function(left, right) { return left * right; }},
-	'/': {type: 'binaryOp', precedence: 40,
-		eval: function(left, right) { return left / right; }},
-	'//': {type: 'binaryOp', precedence: 40,
-		eval: function(left, right) { return Math.floor(left / right); }},
-	'%': {type: 'binaryOp', precedence: 50,
-		eval: function(left, right) { return left % right; }},
-	'^': {type: 'binaryOp', precedence: 50,
-		eval: function(left, right) { return Math.pow(left, right); }},
-	'==': {type: 'binaryOp', precedence: 20,
-		eval: function(left, right) { return left == right; }},
-	'!=': {type: 'binaryOp', precedence: 20,
-		eval: function(left, right) { return left != right; }},
-	'>': {type: 'binaryOp', precedence: 20,
-		eval: function(left, right) { return left > right; }},
-	'>=': {type: 'binaryOp', precedence: 20,
-		eval: function(left, right) { return left >= right; }},
-	'<': {type: 'binaryOp', precedence: 20,
-		eval: function(left, right) { return left < right; }},
-	'<=': {type: 'binaryOp', precedence: 20,
-		eval: function(left, right) { return left <= right; }},
-	'&&': {type: 'binaryOp', precedence: 10,
-		eval: function(left, right) { return left && right; }},
-	'||': {type: 'binaryOp', precedence: 10,
-		eval: function(left, right) { return left || right; }},
-	'in': {type: 'binaryOp', precedence: 20,
-		eval: function(left, right) {
-			if (typeof right === 'string')
-				return right.indexOf(left) !== -1;
-			if (Array.isArray(right)) {
-				return right.some(function(elem) {
-					return elem == left;
-				});
-			}
-			return false;
-		}},
-	'!': {type: 'unaryOp', precedence: Infinity,
-		eval: function(right) { return !right; }}
-};
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/Parser.js
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/Parser.js
@@ -1,188 +0,0 @@
-/*
- * Jexl
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-var handlers = require('./handlers'),
-	states = require('./states').states;
-
-/**
- * The Parser is a state machine that converts tokens from the {@link Lexer}
- * into an Abstract Syntax Tree (AST), capable of being evaluated in any
- * context by the {@link Evaluator}.  The Parser expects that all tokens
- * provided to it are legal and typed properly according to the grammar, but
- * accepts that the tokens may still be in an invalid order or in some other
- * unparsable configuration that requires it to throw an Error.
- * @param {{}} grammar The grammar map to use to parse Jexl strings
- * @param {string} [prefix] A string prefix to prepend to the expression string
- *      for error messaging purposes.  This is useful for when a new Parser is
- *      instantiated to parse an subexpression, as the parent Parser's
- *      expression string thus far can be passed for a more user-friendly
- *      error message.
- * @param {{}} [stopMap] A mapping of token types to any truthy value. When the
- *      token type is encountered, the parser will return the mapped value
- *      instead of boolean false.
- * @constructor
- */
-function Parser(grammar, prefix, stopMap) {
-	this._grammar = grammar;
-	this._state = 'expectOperand';
-	this._tree = null;
-	this._exprStr = prefix || '';
-	this._relative = false;
-	this._stopMap = stopMap || {};
-}
-
-/**
- * Processes a new token into the AST and manages the transitions of the state
- * machine.
- * @param {{type: <string>}} token A token object, as provided by the
- *      {@link Lexer#tokenize} function.
- * @throws {Error} if a token is added when the Parser has been marked as
- *      complete by {@link #complete}, or if an unexpected token type is added.
- * @returns {boolean|*} the stopState value if this parser encountered a token
- *      in the stopState mapb; false if tokens can continue.
- */
-Parser.prototype.addToken = function(token) {
-	if (this._state == 'complete')
-		throw new Error('Cannot add a new token to a completed Parser');
-	var state = states[this._state],
-		startExpr = this._exprStr;
-	this._exprStr += token.raw;
-	if (state.subHandler) {
-		if (!this._subParser)
-			this._startSubExpression(startExpr);
-		var stopState = this._subParser.addToken(token);
-		if (stopState) {
-			this._endSubExpression();
-			if (this._parentStop)
-				return stopState;
-			this._state = stopState;
-		}
-	}
-	else if (state.tokenTypes[token.type]) {
-		var typeOpts = state.tokenTypes[token.type],
-			handleFunc = handlers[token.type];
-		if (typeOpts.handler)
-			handleFunc = typeOpts.handler;
-		if (handleFunc)
-			handleFunc.call(this, token);
-		if (typeOpts.toState)
-			this._state = typeOpts.toState;
-	}
-	else if (this._stopMap[token.type])
-		return this._stopMap[token.type];
-	else {
-		throw new Error('Token ' + token.raw + ' (' + token.type +
-			') unexpected in expression: ' + this._exprStr);
-	}
-	return false;
-};
-
-/**
- * Processes an array of tokens iteratively through the {@link #addToken}
- * function.
- * @param {Array<{type: <string>}>} tokens An array of tokens, as provided by
- *      the {@link Lexer#tokenize} function.
- */
-Parser.prototype.addTokens = function(tokens) {
-	tokens.forEach(this.addToken, this);
-};
-
-/**
- * Marks this Parser instance as completed and retrieves the full AST.
- * @returns {{}|null} a full expression tree, ready for evaluation by the
- *      {@link Evaluator#eval} function, or null if no tokens were passed to
- *      the parser before complete was called
- * @throws {Error} if the parser is not in a state where it's legal to end
- *      the expression, indicating that the expression is incomplete
- */
-Parser.prototype.complete = function() {
-	if (this._cursor && !states[this._state].completable)
-		throw new Error('Unexpected end of expression: ' + this._exprStr);
-	if (this._subParser)
-		this._endSubExpression();
-	this._state = 'complete';
-	return this._cursor ? this._tree : null;
-};
-
-/**
- * Indicates whether the expression tree contains a relative path identifier.
- * @returns {boolean} true if a relative identifier exists; false otherwise.
- */
-Parser.prototype.isRelative = function() {
-	return this._relative;
-};
-
-/**
- * Ends a subexpression by completing the subParser and passing its result
- * to the subHandler configured in the current state.
- * @private
- */
-Parser.prototype._endSubExpression = function() {
-	states[this._state].subHandler.call(this, this._subParser.complete());
-	this._subParser = null;
-};
-
-/**
- * Places a new tree node at the current position of the cursor (to the 'right'
- * property) and then advances the cursor to the new node. This function also
- * handles setting the parent of the new node.
- * @param {{type: <string>}} node A node to be added to the AST
- * @private
- */
-Parser.prototype._placeAtCursor = function(node) {
-	if (!this._cursor)
-		this._tree = node;
-	else {
-		this._cursor.right = node;
-		this._setParent(node, this._cursor);
-	}
-	this._cursor = node;
-};
-
-/**
- * Places a tree node before the current position of the cursor, replacing
- * the node that the cursor currently points to. This should only be called in
- * cases where the cursor is known to exist, and the provided node already
- * contains a pointer to what's at the cursor currently.
- * @param {{type: <string>}} node A node to be added to the AST
- * @private
- */
-Parser.prototype._placeBeforeCursor = function(node) {
-	this._cursor = this._cursor._parent;
-	this._placeAtCursor(node);
-};
-
-/**
- * Sets the parent of a node by creating a non-enumerable _parent property
- * that points to the supplied parent argument.
- * @param {{type: <string>}} node A node of the AST on which to set a new
- *      parent
- * @param {{type: <string>}} parent An existing node of the AST to serve as the
- *      parent of the new node
- * @private
- */
-Parser.prototype._setParent = function(node, parent) {
-	Object.defineProperty(node, '_parent', {
-		value: parent,
-		writable: true
-	});
-};
-
-/**
- * Prepares the Parser to accept a subexpression by (re)instantiating the
- * subParser.
- * @param {string} [exprStr] The expression string to prefix to the new Parser
- * @private
- */
-Parser.prototype._startSubExpression = function(exprStr) {
-	var endStates = states[this._state].endStates;
-	if (!endStates) {
-		this._parentStop = true;
-		endStates = this._stopMap;
-	}
-	this._subParser = new Parser(this._grammar, exprStr, endStates);
-};
-
-module.exports = Parser;
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/handlers.js
+++ b/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/handlers.js
@@ -1,210 +0,0 @@
-/*
- * Jexl
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-/**
- * Handles a subexpression that's used to define a transform argument's value.
- * @param {{type: <string>}} ast The subexpression tree
- */
-exports.argVal = function(ast) {
-	this._cursor.args.push(ast);
-};
-
-/**
- * Handles new array literals by adding them as a new node in the AST,
- * initialized with an empty array.
- */
-exports.arrayStart = function() {
-	this._placeAtCursor({
-		type: 'ArrayLiteral',
-		value: []
-	});
-};
-
-/**
- * Handles a subexpression representing an element of an array literal.
- * @param {{type: <string>}} ast The subexpression tree
- */
-exports.arrayVal = function(ast) {
-	if (ast)
-		this._cursor.value.push(ast);
-};
-
-/**
- * Handles tokens of type 'binaryOp', indicating an operation that has two
- * inputs: a left side and a right side.
- * @param {{type: <string>}} token A token object
- */
-exports.binaryOp = function(token) {
-	var precedence = this._grammar[token.value].precedence || 0,
-		parent = this._cursor._parent;
-	while (parent && parent.operator &&
-			this._grammar[parent.operator].precedence >= precedence) {
-		this._cursor = parent;
-		parent = parent._parent;
-	}
-	var node = {
-		type: 'BinaryExpression',
-		operator: token.value,
-		left: this._cursor
-	};
-	this._setParent(this._cursor, node);
-	this._cursor = parent;
-	this._placeAtCursor(node);
-};
-
-/**
- * Handles successive nodes in an identifier chain.  More specifically, it
- * sets values that determine how the following identifier gets placed in the
- * AST.
- */
-exports.dot = function() {
-	this._nextIdentEncapsulate = this._cursor &&
-		(this._cursor.type != 'BinaryExpression' ||
-		(this._cursor.type == 'BinaryExpression' && this._cursor.right)) &&
-		this._cursor.type != 'UnaryExpression';
-	this._nextIdentRelative = !this._cursor ||
-		(this._cursor && !this._nextIdentEncapsulate);
-	if (this._nextIdentRelative)
-		this._relative = true;
-};
-
-/**
- * Handles a subexpression used for filtering an array returned by an
- * identifier chain.
- * @param {{type: <string>}} ast The subexpression tree
- */
-exports.filter = function(ast) {
-	this._placeBeforeCursor({
-		type: 'FilterExpression',
-		expr: ast,
-		relative: this._subParser.isRelative(),
-		subject: this._cursor
-	});
-};
-
-/**
- * Handles identifier tokens by adding them as a new node in the AST.
- * @param {{type: <string>}} token A token object
- */
-exports.identifier = function(token) {
-	var node = {
-		type: 'Identifier',
-		value: token.value
-	};
-	if (this._nextIdentEncapsulate) {
-		node.from = this._cursor;
-		this._placeBeforeCursor(node);
-		this._nextIdentEncapsulate = false;
-	}
-	else {
-		if (this._nextIdentRelative)
-			node.relative = true;
-		this._placeAtCursor(node);
-	}
-};
-
-/**
- * Handles literal values, such as strings, booleans, and numerics, by adding
- * them as a new node in the AST.
- * @param {{type: <string>}} token A token object
- */
-exports.literal = function(token) {
-	this._placeAtCursor({
-		type: 'Literal',
-		value: token.value
-	});
-};
-
-/**
- * Queues a new object literal key to be written once a value is collected.
- * @param {{type: <string>}} token A token object
- */
-exports.objKey = function(token) {
-	this._curObjKey = token.value;
-};
-
-/**
- * Handles new object literals by adding them as a new node in the AST,
- * initialized with an empty object.
- */
-exports.objStart = function() {
-	this._placeAtCursor({
-		type: 'ObjectLiteral',
-		value: {}
-	});
-};
-
-/**
- * Handles an object value by adding its AST to the queued key on the object
- * literal node currently at the cursor.
- * @param {{type: <string>}} ast The subexpression tree
- */
-exports.objVal = function(ast) {
-	this._cursor.value[this._curObjKey] = ast;
-};
-
-/**
- * Handles traditional subexpressions, delineated with the groupStart and
- * groupEnd elements.
- * @param {{type: <string>}} ast The subexpression tree
- */
-exports.subExpression = function(ast) {
-	this._placeAtCursor(ast);
-};
-
-/**
- * Handles a completed alternate subexpression of a ternary operator.
- * @param {{type: <string>}} ast The subexpression tree
- */
-exports.ternaryEnd = function(ast) {
-	this._cursor.alternate = ast;
-};
-
-/**
- * Handles a completed consequent subexpression of a ternary operator.
- * @param {{type: <string>}} ast The subexpression tree
- */
-exports.ternaryMid = function(ast) {
-	this._cursor.consequent = ast;
-};
-
-/**
- * Handles the start of a new ternary expression by encapsulating the entire
- * AST in a ConditionalExpression node, and using the existing tree as the
- * test element.
- */
-exports.ternaryStart = function() {
-	this._tree = {
-		type: 'ConditionalExpression',
-		test: this._tree
-	};
-	this._cursor = this._tree;
-};
-
-/**
- * Handles identifier tokens when used to indicate the name of a transform to
- * be applied.
- * @param {{type: <string>}} token A token object
- */
-exports.transform = function(token) {
-	this._placeBeforeCursor({
-		type: 'Transform',
-		name: token.value,
-		args: [],
-		subject: this._cursor
-	});
-};
-
-/**
- * Handles token of type 'unaryOp', indicating that the operation has only
- * one input: a right side.
- * @param {{type: <string>}} token A token object
- */
-exports.unaryOp = function(token) {
-	this._placeAtCursor({
-		type: 'UnaryExpression',
-		operator: token.value
-	});
-};
deleted file mode 100644
--- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/states.js
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Jexl
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-var h = require('./handlers');
-
-/**
- * A mapping of all states in the finite state machine to a set of instructions
- * for handling or transitioning into other states. Each state can be handled
- * in one of two schemes: a tokenType map, or a subHandler.
- *
- * Standard expression elements are handled through the tokenType object. This
- * is an object map of all legal token types to encounter in this state (and
- * any unexpected token types will generate a thrown error) to an options
- * object that defines how they're handled.  The available options are:
- *
- *      {string} toState: The name of the state to which to transition
- *          immediately after handling this token
- *      {string} handler: The handler function to call when this token type is
- *          encountered in this state.  If omitted, the default handler
- *          matching the token's "type" property will be called. If the handler
- *          function does not exist, no call will be made and no error will be
- *          generated.  This is useful for tokens whose sole purpose is to
- *          transition to other states.
- *
- * States that consume a subexpression should define a subHandler, the
- * function to be called with an expression tree argument when the
- * subexpression is complete. Completeness is determined through the
- * endStates object, which maps tokens on which an expression should end to the
- * state to which to transition once the subHandler function has been called.
- *
- * Additionally, any state in which it is legal to mark the AST as completed
- * should have a 'completable' property set to boolean true.  Attempting to
- * call {@link Parser#complete} in any state without this property will result
- * in a thrown Error.
- *
- * @type {{}}
- */
-exports.states = {
-	expectOperand: {
-		tokenTypes: {
-			literal: {toState: 'expectBinOp'},
-			identifier: {toState: 'identifier'},
-			unaryOp: {},
-			openParen: {toState: 'subExpression'},
-			openCurl: {toState: 'expectObjKey', handler: h.objStart},
-			dot: {toState: 'traverse'},
-			openBracket: {toState: 'arrayVal', handler: h.arrayStart}
-		}
-	},
-	expectBinOp: {
-		tokenTypes: {
-			binaryOp: {toState: 'expectOperand'},
-			pipe: {toState: 'expectTransform'},
-			dot: {toState: 'traverse'},
-			question: {toState: 'ternaryMid', handler: h.ternaryStart}
-		},
-		completable: true
-	},
-	expectTransform: {
-		tokenTypes: {
-			identifier: {toState: 'postTransform', handler: h.transform}
-		}
-	},
-	expectObjKey: {
-		tokenTypes: {
-			identifier: {toState: 'expectKeyValSep', handler: h.objKey},
-			closeCurl: {toState: 'expectBinOp'}
-		}
-	},
-	expectKeyValSep: {
-		tokenTypes: {
-			colon: {toState: 'objVal'}
-		}
-	},
-	postTransform: {
-		tokenTypes: {
-			openParen: {toState: 'argVal'},
-			binaryOp: {toState: 'expectOperand'},
-			dot: {toState: 'traverse'},
-			openBracket: {toState: 'filter'},
-			pipe: {toState: 'expectTransform'}
-		},
-		completable: true
-	},
-	postTransformArgs: {
-		tokenTypes: {
-			binaryOp: {toState: 'expectOperand'},
-			dot: {toState: 'traverse'},
-			openBracket: {toState: 'filter'},
-			pipe: {toState: 'expectTransform'}
-		},
-		completable: true
-	},
-	identifier: {
-		tokenTypes: {
-			binaryOp: {toState: 'expectOperand'},
-			dot: {toState: 'traverse'},
-			openBracket: {toState: 'filter'},
-			pipe: {toState: 'expectTransform'},
-			question: {toState: 'ternaryMid', handler: h.ternaryStart}
-		},
-		completable: true
-	},
-	traverse: {
-		tokenTypes: {
-			'identifier': {toState: 'identifier'}
-		}
-	},
-	filter: {
-		subHandler: h.filter,
-		endStates: {
-			closeBracket: 'identifier'
-		}
-	},
-	subExpression: {
-		subHandler: h.subExpression,
-		endStates: {
-			closeParen: 'expectBinOp'
-		}
-	},
-	argVal: {
-		subHandler: h.argVal,
-		endStates: {
-			comma: 'argVal',
-			closeParen: 'postTransformArgs'
-		}
-	},
-	objVal: {
-		subHandler: h.objVal,
-		endStates: {
-			comma: 'expectObjKey',
-			closeCurl: 'expectBinOp'
-		}
-	},
-	arrayVal: {
-		subHandler: h.arrayVal,
-		endStates: {
-			comma: 'arrayVal',
-			closeBracket: 'expectBinOp'
-		}
-	},
-	ternaryMid: {
-		subHandler: h.ternaryMid,
-		endStates: {
-			colon: 'ternaryEnd'
-		}
-	},
-	ternaryEnd: {
-		subHandler: h.ternaryEnd,
-		completable: true
-	}
-};
--- a/browser/extensions/shield-recipe-client/test/browser/.eslintrc.js
+++ b/browser/extensions/shield-recipe-client/test/browser/.eslintrc.js
@@ -3,15 +3,9 @@
 module.exports = {
   extends: [
     "plugin:mozilla/browser-test"
   ],
 
   plugins: [
     "mozilla"
   ],
-
-  globals: {
-    // Bug 1366720 - SimpleTest isn't being exported correctly, so list
-    // it here for now.
-    "SimpleTest": false
-  }
 };
--- a/browser/extensions/shield-recipe-client/test/browser/browser.ini
+++ b/browser/extensions/shield-recipe-client/test/browser/browser.ini
@@ -1,14 +1,17 @@
 [DEFAULT]
+support-files =
+  action_server.sjs
+  fixtures/normandy.xpi
 head = head.js
+[browser_ActionSandboxManager.js]
 [browser_NormandyDriver.js]
 [browser_FilterExpressions.js]
 [browser_EventEmitter.js]
 [browser_Storage.js]
+[browser_StudyStorage.js]
 [browser_Heartbeat.js]
 [browser_RecipeRunner.js]
-support-files =
-  action_server.sjs
 [browser_LogManager.js]
 [browser_ClientEnvironment.js]
 [browser_ShieldRecipeClient.js]
 [browser_PreferenceExperiments.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_ActionSandboxManager.js
@@ -0,0 +1,167 @@
+"use strict";
+
+Cu.import("resource://shield-recipe-client/lib/ActionSandboxManager.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
+
+async function withManager(script, testFunction) {
+  const manager = new ActionSandboxManager(script);
+  manager.addHold("testing");
+  await testFunction(manager);
+  manager.removeHold("testing");
+}
+
+add_task(async function testMissingCallbackName() {
+  await withManager("1 + 1", async manager => {
+    is(
+      await manager.runAsyncCallback("missingCallback"),
+      undefined,
+      "runAsyncCallback returns undefined when given a missing callback name",
+    );
+  });
+});
+
+add_task(async function testCallback() {
+  const script = `
+    registerAsyncCallback("testCallback", async function(normandy) {
+      return 5;
+    });
+  `;
+
+  await withManager(script, async manager => {
+    const result = await manager.runAsyncCallback("testCallback");
+    is(result, 5, "runAsyncCallback executes the named callback inside the sandbox");
+  });
+});
+
+add_task(async function testArguments() {
+  const script = `
+    registerAsyncCallback("testCallback", async function(normandy, a, b) {
+      return a + b;
+    });
+  `;
+
+  await withManager(script, async manager => {
+    const result = await manager.runAsyncCallback("testCallback", 4, 6);
+    is(result, 10, "runAsyncCallback passes arguments to the callback");
+  });
+});
+
+add_task(async function testCloning() {
+  const script = `
+    registerAsyncCallback("testCallback", async function(normandy, obj) {
+      return {foo: "bar", baz: obj.baz};
+    });
+  `;
+
+  await withManager(script, async manager => {
+    const result = await manager.runAsyncCallback("testCallback", {baz: "biff"});
+
+    Assert.deepEqual(
+      result,
+      {foo: "bar", baz: "biff"},
+      (
+        "runAsyncCallback clones arguments into the sandbox and return values into the " +
+        "context it was called from"
+      ),
+    );
+  });
+});
+
+add_task(async function testError() {
+  const script = `
+    registerAsyncCallback("testCallback", async function(normandy) {
+      throw new Error("WHY")
+    });
+  `;
+
+  await withManager(script, async manager => {
+    try {
+      await manager.runAsyncCallback("testCallback");
+      ok(false, "runAsnycCallbackFromScript throws errors when raised by the sandbox");
+    } catch (err) {
+      is(err.message, "WHY", "runAsnycCallbackFromScript clones error messages");
+    }
+  });
+});
+
+add_task(async function testDriver() {
+  // The value returned by runAsyncCallback is cloned without the cloneFunctions
+  // option, so we can't inspect the driver itself since its methods will not be
+  // present. Instead, we inspect the properties on it available to the sandbox.
+  const script = `
+    registerAsyncCallback("testCallback", async function(normandy) {
+      return Object.keys(normandy);
+    });
+  `;
+
+  await withManager(script, async manager => {
+    const sandboxDriverKeys = await manager.runAsyncCallback("testCallback");
+    const referenceDriver = new NormandyDriver(manager);
+    for (const prop of Object.keys(referenceDriver)) {
+      ok(sandboxDriverKeys.includes(prop), `runAsyncCallback's driver has the "${prop}" property.`);
+    }
+  });
+});
+
+add_task(async function testGlobalObject() {
+  // Test that window is an alias for the global object, and that it
+  // has some expected functions available on it.
+  const script = `
+    window.setOnWindow = "set";
+    this.setOnGlobal = "set";
+
+    registerAsyncCallback("testCallback", async function(normandy) {
+      return {
+        setOnWindow: setOnWindow,
+        setOnGlobal: window.setOnGlobal,
+        setTimeoutExists: setTimeout !== undefined,
+        clearTimeoutExists: clearTimeout !== undefined,
+      };
+    });
+  `;
+
+  await withManager(script, async manager => {
+    const result = await manager.runAsyncCallback("testCallback");
+    Assert.deepEqual(result, {
+      setOnWindow: "set",
+      setOnGlobal: "set",
+      setTimeoutExists: true,
+      clearTimeoutExists: true,
+    }, "sandbox.window is the global object and has expected functions.");
+  });
+});
+
+add_task(async function testRegisterActionShim() {
+  const recipe = {
+    foo: "bar",
+  };
+  const script = `
+    class TestAction {
+      constructor(driver, recipe) {
+        this.driver = driver;
+        this.recipe = recipe;
+      }
+
+      execute() {
+        return new Promise(resolve => {
+          resolve({
+            foo: this.recipe.foo,
+            isDriver: "log" in this.driver,
+          });
+        });
+      }
+    }
+
+    registerAction('test-action', TestAction);
+  `;
+
+  await withManager(script, async manager => {
+    const result = await manager.runAsyncCallback("action", recipe);
+    is(result.foo, "bar", "registerAction registers an async callback for actions");
+    is(
+      result.isDriver,
+      true,
+      "registerAction passes the driver to the action class constructor",
+    );
+  });
+});
--- a/browser/extensions/shield-recipe-client/test/browser/browser_ClientEnvironment.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_ClientEnvironment.js
@@ -1,13 +1,15 @@
 "use strict";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/AddonManager.jsm", this);
+Cu.import("resource://testing-common/AddonTestUtils.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/ClientEnvironment.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm", this);
 
 
 add_task(async function testTelemetry() {
   // setup
   await TelemetryController.submitExternalPing("testfoo", {foo: 1});
   await TelemetryController.submitExternalPing("testbar", {bar: 2});
@@ -107,8 +109,28 @@ add_task(async function testExperiments(
   Assert.deepEqual(
     experiments.expired,
     ["expired"],
     "experiments.expired returns all expired experiment names",
   );
 
   getAll.restore();
 });
+
+add_task(withDriver(Assert, async function testAddonsInContext(driver) {
+  // Create before install so that the listener is added before startup completes.
+  const startupPromise = AddonTestUtils.promiseWebExtensionStartup("normandydriver@example.com");
+  const addonId = await driver.addons.install(TEST_XPI_URL);
+  await startupPromise;
+
+  const environment = ClientEnvironment.getEnvironment();
+  const addons = await environment.addons;
+  Assert.deepEqual(addons[addonId], {
+    id: [addonId],
+    name: "normandy_fixture",
+    version: "1.0",
+    installDate: addons[addonId].installDate,
+    isActive: true,
+    type: "extension",
+  }, "addons should be available in context");
+
+  await driver.addons.uninstall(addonId);
+}));
--- a/browser/extensions/shield-recipe-client/test/browser/browser_FilterExpressions.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_FilterExpressions.js
@@ -86,8 +86,103 @@ add_task(async function() {
   ok(val, "preferenceIsUserSet expression determines if user's preference has been set");
 
   // Compare if the preference has _any_ value, whether it's user-set or default,
   val = await FilterExpressions.eval('"normandy.test.nonexistant"|preferenceExists == true');
   ok(!val, "preferenceExists expression determines if preference exists at all");
   val = await FilterExpressions.eval('"normandy.test.value"|preferenceExists == true');
   ok(val, "preferenceExists expression fails existence check appropriately");
 });
+
+// keys tests
+add_task(async function testKeys() {
+  let val;
+
+  // Test an object defined in JEXL
+  val = await FilterExpressions.eval("{foo: 1, bar: 2}|keys");
+  Assert.deepEqual(
+    new Set(val),
+    new Set(["foo", "bar"]),
+    "keys returns the keys from an object in JEXL",
+  );
+
+  // Test an object in the context
+  let context = {ctxObject: {baz: "string", biff: NaN}};
+  val = await FilterExpressions.eval("ctxObject|keys", context);
+
+  Assert.deepEqual(
+    new Set(val),
+    new Set(["baz", "biff"]),
+    "keys returns the keys from an object in the context",
+  );
+
+  // Test that values from the prototype are not included
+  context = {ctxObject: Object.create({fooProto: 7})};
+  context.ctxObject.baz = 8;
+  context.ctxObject.biff = 5;
+  is(
+    await FilterExpressions.eval("ctxObject.fooProto", context),
+    7,
+    "Prototype properties are accessible via property access",
+  );
+  val = await FilterExpressions.eval("ctxObject|keys", context);
+  Assert.deepEqual(
+    new Set(val),
+    new Set(["baz", "biff"]),
+    "keys does not return properties from the object's prototype chain",
+  );
+
+  // Return undefined for non-objects
+  is(
+    await FilterExpressions.eval("ctxObject|keys", {ctxObject: 45}),
+    undefined,
+    "keys returns undefined for numbers",
+  );
+  is(
+    await FilterExpressions.eval("ctxObject|keys", {ctxObject: null}),
+    undefined,
+    "keys returns undefined for null",
+  );
+});
+
+// intersect tests
+add_task(async function testIntersect() {
+  let val;
+
+  val = await FilterExpressions.eval("[1, 2, 3] intersect [4, 2, 6, 7, 3]");
+  Assert.deepEqual(
+    new Set(val),
+    new Set([2, 3]),
+    "intersect finds the common elements between two lists in JEXL",
+  );
+
+  const context = {left: [5, 7], right: [4, 5, 3]};
+  val = await FilterExpressions.eval("left intersect right", context);
+  Assert.deepEqual(
+    new Set(val),
+    new Set([5]),
+    "intersect finds the common elements between two lists in the context",
+  );
+
+  val = await FilterExpressions.eval("['string', 2] intersect [4, 'string', 'other', 3]");
+  Assert.deepEqual(
+    new Set(val),
+    new Set(["string"]),
+    "intersect can compare strings",
+  );
+
+  // Return undefined when intersecting things that aren't lists.
+  is(
+    await FilterExpressions.eval("5 intersect 7"),
+    undefined,
+    "intersect returns undefined for numbers",
+  );
+  is(
+    await FilterExpressions.eval("val intersect other", {val: null, other: null}),
+    undefined,
+    "intersect returns undefined for null",
+  );
+  is(
+    await FilterExpressions.eval("5 intersect [1, 2, 5]"),
+    undefined,
+    "intersect returns undefined if only one operand is a list",
+  );
+});
--- a/browser/extensions/shield-recipe-client/test/browser/browser_NormandyDriver.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_NormandyDriver.js
@@ -1,22 +1,61 @@
 "use strict";
 
+Cu.import("resource://testing-common/AddonTestUtils.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/StudyStorage.jsm", this);
 
 add_task(withDriver(Assert, async function uuids(driver) {
   // Test that it is a UUID
   const uuid1 = driver.uuid();
   ok(UUID_REGEX.test(uuid1), "valid uuid format");
 
   // Test that UUIDs are different each time
   const uuid2 = driver.uuid();
   isnot(uuid1, uuid2, "uuids are unique");
 }));
 
+add_task(withDriver(Assert, async function installXpi(driver) {
+  // Test that we can install an XPI from any URL
+  // Create before install so that the listener is added before startup completes.
+  const startupPromise = AddonTestUtils.promiseWebExtensionStartup("normandydriver@example.com");
+
+  var addonId = await driver.addons.install(TEST_XPI_URL);
+  is(addonId, "normandydriver@example.com", "Expected test addon was installed");
+  isnot(addonId, null, "Addon install was successful");
+
+  // Wait until the add-on is fully started up to uninstall it.
+  await startupPromise;
+
+  const uninstallMsg = await driver.addons.uninstall(addonId);
+  is(uninstallMsg, null, `Uninstall returned an unexpected message [${uninstallMsg}]`);
+}));
+
+add_task(withDriver(Assert, async function uninstallInvalidAddonId(driver) {
+  const invalidAddonId = "not_a_valid_xpi_id@foo.bar";
+  try {
+    await driver.addons.uninstall(invalidAddonId);
+    ok(false, `Uninstalling an invalid XPI should fail. addons.uninstall resolved successfully though.`);
+  } catch (e) {
+    ok(true, `This is the expected failure`);
+  }
+}));
+
+
+add_task(withDriver(Assert, async function installXpiBadURL(driver) {
+  const xpiUrl = "file:///tmp/invalid_xpi.xpi";
+  try {
+    await driver.addons.install(xpiUrl);
+    ok(false, "Installation succeeded on an XPI that doesn't exist");
+  } catch (reason) {
+    ok(true, `Installation was rejected: [${reason}]`);
+  }
+}));
+
 add_task(withDriver(Assert, async function userId(driver) {
   // Test that userId is a UUID
   ok(UUID_REGEX.test(driver.userId), "userId is a uuid");
 }));
 
 add_task(withDriver(Assert, async function syncDeviceCounts(driver) {
   let client = await driver.client();
   is(client.syncMobileDevices, 0, "syncMobileDevices defaults to zero");
@@ -75,8 +114,98 @@ add_task(withSandboxManager(Assert, asyn
 
       await store.removeItem("willremove");
       is(await store.getItem("willremove"), null, "createStorage removes items");
 
       is('prefix' in store, false, "createStorage doesn't expose non-whitelist attributes");
     })();
   `);
 }));
+
+add_task(withDriver(Assert, async function getAddon(driver, sandboxManager) {
+  const ADDON_ID = "normandydriver@example.com";
+  let addon = await driver.addons.get(ADDON_ID);
+  Assert.equal(addon, null, "Add-on is not yet installed");
+
+  await driver.addons.install(TEST_XPI_URL);
+  addon = await driver.addons.get(ADDON_ID);
+
+  Assert.notEqual(addon, null, "Add-on object was returned");
+  ok(addon.installDate instanceof sandboxManager.sandbox.Date, "installDate should be a Date object");
+
+  Assert.deepEqual(addon, {
+    id: "normandydriver@example.com",
+    name: "normandy_fixture",
+    version: "1.0",
+    installDate: addon.installDate,
+    isActive: true,
+    type: "extension",
+  }, "Add-on is installed");
+
+  await driver.addons.uninstall(ADDON_ID);
+  addon = await driver.addons.get(ADDON_ID);
+
+  Assert.equal(addon, null, "Add-on has been uninstalled");
+}));
+
+add_task(withSandboxManager(Assert, async function testAddonsGetWorksInSandbox(sandboxManager) {
+  const driver = new NormandyDriver(sandboxManager);
+  sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true});
+
+  // Assertion helpers
+  sandboxManager.addGlobal("is", is);
+  sandboxManager.addGlobal("deepEqual", (...args) => Assert.deepEqual(...args));
+
+  const ADDON_ID = "normandydriver@example.com";
+
+  await driver.addons.install(TEST_XPI_URL);
+
+  await sandboxManager.evalInSandbox(`
+    (async function sandboxTest() {
+      const addon = await driver.addons.get("${ADDON_ID}");
+
+      deepEqual(addon, {
+        id: "${ADDON_ID}",
+        name: "normandy_fixture",
+        version: "1.0",
+        installDate: addon.installDate,
+        isActive: true,
+        type: "extension",
+      }, "Add-on is accesible in the driver");
+    })();
+  `);
+
+  await driver.addons.uninstall(ADDON_ID);
+}));
+
+add_task(withSandboxManager(Assert, async function testStudyStorage(sandboxManager) {
+  const driver = new NormandyDriver(sandboxManager);
+  sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true});
+
+  // Assertion helpers
+  sandboxManager.addGlobal("is", is);
+  sandboxManager.addGlobal("ok", ok);
+
+  await sandboxManager.evalInSandbox(`
+    (async function sandboxTest() {
+      await driver.studies.create({
+        name: "test-study",
+        addonId: "test@example.com",
+        addonVersion: "1.0",
+        description: "fake",
+        studyStartDate: new Date().toJSON(),
+      });
+
+      const hasStudy = await driver.studies.has("test-study");
+      ok(hasStudy, "studies.create creates studies from within a sandbox.");
+
+      let study = await driver.studies.get("test-study");
+      is(study.addonVersion, "1.0", "studies.get fetches studies from within a sandbox.");
+
+      await driver.studies.update("test-study", {addonVersion: "2.0"});
+      study = await driver.studies.get("test-study");
+      is(study.addonVersion, "2.0", "studies.update can update stored studies.");
+    })();
+  `);
+
+  await StudyStorage.clear();
+  await StudyStorage.close();
+}));
--- a/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js
@@ -362,38 +362,41 @@ add_task(withMockExperiments(withMockPre
 
   stopObserver.restore();
   PreferenceExperiments.stopAllObservers();
 })));
 
 // stop should also support user pref experiments
 add_task(withMockExperiments(withMockPreferences(async function(experiments, mockPreferences) {
   const stopObserver = sinon.stub(PreferenceExperiments, "stopObserver");
+  const hasObserver = sinon.stub(PreferenceExperiments, "hasObserver");
+  hasObserver.returns(true);
+
   mockPreferences.set("fake.preference", "experimentvalue", "user");
   experiments["test"] = experimentFactory({
     name: "test",
     expired: false,
     preferenceName: "fake.preference",
     preferenceValue: "experimentvalue",
     preferenceType: "string",
     previousPreferenceValue: "oldvalue",
     preferenceBranchType: "user",
   });
-  PreferenceExperiments.startObserver("test", "fake.preference", "experimentvalue");
 
   await PreferenceExperiments.stop("test");
   ok(stopObserver.calledWith("test"), "stop removed an observer");
   is(experiments["test"].expired, true, "stop marked the experiment as expired");
   is(
     Preferences.get("fake.preference"),
     "oldvalue",
     "stop reverted the preference to its previous value",
   );
 
   stopObserver.restore();
+  hasObserver.restore();
 })));
 
 // stop should not call stopObserver if there is no observer registered.
 add_task(withMockExperiments(withMockPreferences(async function(experiments) {
   const stopObserver = sinon.spy(PreferenceExperiments, "stopObserver");
   experiments["test"] = experimentFactory({name: "test", expired: false});
 
   await PreferenceExperiments.stop("test");
--- a/browser/extensions/shield-recipe-client/test/browser/browser_RecipeRunner.js
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_RecipeRunner.js
@@ -1,16 +1,18 @@
 "use strict";
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/ClientEnvironment.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/ActionSandboxManager.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/StudyStorage.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/Uptake.jsm", this);
 
 add_task(async function getFilterContext() {
   const recipe = {id: 17, arguments: {foo: "bar"}, unrelated: false};
   const context = RecipeRunner.getFilterContext(recipe);
 
   // Test for expected properties in the filter expression context.
   const expectedNormandyKeys = [
     "channel",
@@ -65,17 +67,17 @@ add_task(async function checkFilter() {
 });
 
 add_task(withMockNormandyApi(async function testClientClassificationCache() {
   const getStub = sinon.stub(ClientEnvironment, "getClientClassification")
     .returns(Promise.resolve(false));
 
   await SpecialPowers.pushPrefEnv({set: [
     ["extensions.shield-recipe-client.api_url",
-     "https://example.com/selfsupport-dummy"],
+      "https://example.com/selfsupport-dummy"],
   ]});
 
   // When the experiment pref is false, eagerly call getClientClassification.
   await SpecialPowers.pushPrefEnv({set: [
     ["extensions.shield-recipe-client.experiments.lazy_classify", false],
   ]});
   ok(!getStub.called, "getClientClassification hasn't been called");
   await RecipeRunner.run();
@@ -94,37 +96,46 @@ add_task(withMockNormandyApi(async funct
 }));
 
 /**
  * Mocks RecipeRunner.loadActionSandboxManagers for testing run.
  */
 async function withMockActionSandboxManagers(actions, testFunction) {
   const managers = {};
   for (const action of actions) {
-    managers[action.name] = new ActionSandboxManager("");
+    const manager = new ActionSandboxManager("");
+    manager.addHold("testing");
+    managers[action.name] = manager;
     sinon.stub(managers[action.name], "runAsyncCallback");
   }
 
-  const loadActionSandboxManagers = sinon.stub(
-    RecipeRunner,
-    "loadActionSandboxManagers",
-    async () => managers,
-  );
+  const loadActionSandboxManagers = sinon.stub(RecipeRunner, "loadActionSandboxManagers")
+    .resolves(managers);
   await testFunction(managers);
   loadActionSandboxManagers.restore();
+
+  for (const manager of Object.values(managers)) {
+    manager.removeHold("testing");
+    await manager.isNuked();
+  }
 }
 
 add_task(withMockNormandyApi(async function testRun(mockApi) {
+  const closeSpy = sinon.spy(StudyStorage, "close");
+  const reportRunner = sinon.stub(Uptake, "reportRunner");
+  const reportAction = sinon.stub(Uptake, "reportAction");
+  const reportRecipe = sinon.stub(Uptake, "reportRecipe");
+
   const matchAction = {name: "matchAction"};
   const noMatchAction = {name: "noMatchAction"};
   mockApi.actions = [matchAction, noMatchAction];
 
-  const matchRecipe = {action: "matchAction", filter_expression: "true"};
-  const noMatchRecipe = {action: "noMatchAction", filter_expression: "false"};
-  const missingRecipe = {action: "missingAction", filter_expression: "true"};
+  const matchRecipe = {id: "match", action: "matchAction", filter_expression: "true"};
+  const noMatchRecipe = {id: "noMatch", action: "noMatchAction", filter_expression: "false"};
+  const missingRecipe = {id: "missing", action: "missingAction", filter_expression: "true"};
   mockApi.recipes = [matchRecipe, noMatchRecipe, missingRecipe];
 
   await withMockActionSandboxManagers(mockApi.actions, async managers => {
     const matchManager = managers["matchAction"];
     const noMatchManager = managers["noMatchAction"];
 
     await RecipeRunner.run();
 
@@ -135,28 +146,109 @@ add_task(withMockNormandyApi(async funct
 
     // noMatch should be called for preExecution and postExecution, and skipped
     // for action since the filter expression does not match.
     sinon.assert.calledWith(noMatchManager.runAsyncCallback, "preExecution");
     sinon.assert.neverCalledWith(noMatchManager.runAsyncCallback, "action", noMatchRecipe);
     sinon.assert.calledWith(noMatchManager.runAsyncCallback, "postExecution");
 
     // missing is never called at all due to no matching action/manager.
-    await matchManager.isNuked();
-    await noMatchManager.isNuked();
+
+    // Test uptake reporting
+    sinon.assert.calledWith(reportRunner, Uptake.RUNNER_SUCCESS);
+    sinon.assert.calledWith(reportAction, "matchAction", Uptake.ACTION_SUCCESS);
+    sinon.assert.calledWith(reportAction, "noMatchAction", Uptake.ACTION_SUCCESS);
+    sinon.assert.calledWith(reportRecipe, "match", Uptake.RECIPE_SUCCESS);
+    sinon.assert.neverCalledWith(reportRecipe, "noMatch", Uptake.RECIPE_SUCCESS);
+    sinon.assert.calledWith(reportRecipe, "missing", Uptake.RECIPE_INVALID_ACTION);
   });
+
+  // Ensure storage is closed after the run.
+  sinon.assert.calledOnce(closeSpy);
+
+  closeSpy.restore();
+  reportRunner.restore();
+  reportAction.restore();
+  reportRecipe.restore();
+}));
+
+add_task(withMockNormandyApi(async function testRunRecipeError(mockApi) {
+  const reportRecipe = sinon.stub(Uptake, "reportRecipe");
+
+  const action = {name: "action"};
+  mockApi.actions = [action];
+
+  const recipe = {id: "recipe", action: "action", filter_expression: "true"};
+  mockApi.recipes = [recipe];
+
+  await withMockActionSandboxManagers(mockApi.actions, async managers => {
+    const manager = managers["action"];
+    manager.runAsyncCallback.callsFake(async callbackName => {
+      if (callbackName === "action") {
+        throw new Error("Action execution failure");
+      }
+    });
+
+    await RecipeRunner.run();
+
+    // Uptake should report that the recipe threw an exception
+    sinon.assert.calledWith(reportRecipe, "recipe", Uptake.RECIPE_EXECUTION_ERROR);
+  });
+
+  reportRecipe.restore();
+}));
+
+add_task(withMockNormandyApi(async function testRunFetchFail(mockApi) {
+  const closeSpy = sinon.spy(StudyStorage, "close");
+  const reportRunner = sinon.stub(Uptake, "reportRunner");
+
+  const action = {name: "action"};
+  mockApi.actions = [action];
+  mockApi.fetchRecipes.rejects(new Error("Signature not valid"));
+
+  await withMockActionSandboxManagers(mockApi.actions, async managers => {
+    const manager = managers["action"];
+    await RecipeRunner.run();
+
+    // If the recipe fetch failed, do not run anything.
+    sinon.assert.notCalled(manager.runAsyncCallback);
+    sinon.assert.calledWith(reportRunner, Uptake.RUNNER_SERVER_ERROR);
+
+    // Test that network errors report a specific uptake error
+    reportRunner.reset();
+    mockApi.fetchRecipes.rejects(new Error("NetworkError: The system was down"));
+    await RecipeRunner.run();
+    sinon.assert.calledWith(reportRunner, Uptake.RUNNER_NETWORK_ERROR);
+
+    // Test that signature issues report a specific uptake error
+    reportRunner.reset();
+    mockApi.fetchRecipes.rejects(new NormandyApi.InvalidSignatureError("Signature fail"));
+    await RecipeRunner.run();
+    sinon.assert.calledWith(reportRunner, Uptake.RUNNER_INVALID_SIGNATURE);
+  });
+
+  // If the recipe fetch failed, we don't need to call close since nothing
+  // opened a connection in the first place.
+  sinon.assert.notCalled(closeSpy);
+
+  closeSpy.restore();
+  reportRunner.restore();
 }));
 
 add_task(withMockNormandyApi(async function testRunPreExecutionFailure(mockApi) {
+  const closeSpy = sinon.spy(StudyStorage, "close");
+  const reportAction = sinon.stub(Uptake, "reportAction");
+  const reportRecipe = sinon.stub(Uptake, "reportRecipe");
+
   const passAction = {name: "passAction"};
   const failAction = {name: "failAction"};
   mockApi.actions = [passAction, failAction];
 
-  const passRecipe = {action: "passAction", filter_expression: "true"};
-  const failRecipe = {action: "failAction", filter_expression: "true"};
+  const passRecipe = {id: "pass", action: "passAction", filter_expression: "true"};
+  const failRecipe = {id: "fail", action: "failAction", filter_expression: "true"};
   mockApi.recipes = [passRecipe, failRecipe];
 
   await withMockActionSandboxManagers(mockApi.actions, async managers => {
     const passManager = managers["passAction"];
     const failManager = managers["failAction"];
     failManager.runAsyncCallback.returns(Promise.reject(new Error("oh no")));
 
     await RecipeRunner.run();
@@ -166,19 +258,57 @@ add_task(withMockNormandyApi(async funct
     sinon.assert.calledWith(passManager.runAsyncCallback, "action", passRecipe);
     sinon.assert.calledWith(passManager.runAsyncCallback, "postExecution");
 
     // fail should only be called for preExecution, since it fails during that
     sinon.assert.calledWith(failManager.runAsyncCallback, "preExecution");
     sinon.assert.neverCalledWith(failManager.runAsyncCallback, "action", failRecipe);
     sinon.assert.neverCalledWith(failManager.runAsyncCallback, "postExecution");
 
-    await passManager.isNuked();
-    await failManager.isNuked();
+    sinon.assert.calledWith(reportAction, "passAction", Uptake.ACTION_SUCCESS);
+    sinon.assert.calledWith(reportAction, "failAction", Uptake.ACTION_PRE_EXECUTION_ERROR);
+    sinon.assert.calledWith(reportRecipe, "fail", Uptake.RECIPE_ACTION_DISABLED);
   });
+
+  // Ensure storage is closed after the run, despite the failures.
+  sinon.assert.calledOnce(closeSpy);
+  closeSpy.restore();
+  reportAction.restore();
+  reportRecipe.restore();
+}));
+
+add_task(withMockNormandyApi(async function testRunPostExecutionFailure(mockApi) {
+  const reportAction = sinon.stub(Uptake, "reportAction");
+
+  const failAction = {name: "failAction"};
+  mockApi.actions = [failAction];
+
+  const failRecipe = {action: "failAction", filter_expression: "true"};
+  mockApi.recipes = [failRecipe];
+
+  await withMockActionSandboxManagers(mockApi.actions, async managers => {
+    const failManager = managers["failAction"];
+    failManager.runAsyncCallback.callsFake(async callbackName => {
+      if (callbackName === "postExecution") {
+        throw new Error("postExecution failure");
+      }
+    });
+
+    await RecipeRunner.run();
+
+    // fail should be called for every stage
+    sinon.assert.calledWith(failManager.runAsyncCallback, "preExecution");
+    sinon.assert.calledWith(failManager.runAsyncCallback, "action", failRecipe);
+    sinon.assert.calledWith(failManager.runAsyncCallback, "postExecution");
+
+    // Uptake should report a post-execution error
+    sinon.assert.calledWith(reportAction, "failAction", Uptake.ACTION_POST_EXECUTION_ERROR);
+  });
+
+  reportAction.restore();
 }));
 
 add_task(withMockNormandyApi(async function testLoadActionSandboxManagers(mockApi) {
   mockApi.actions = [
     {name: "normalAction"},
     {name: "missingImpl"},
   ];
   mockApi.implementations["normalAction"] = "window.scriptRan = true";
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser/browser_StudyStorage.js
@@ -0,0 +1,192 @@
+"use strict";
+
+Cu.import("resource://gre/modules/IndexedDB.jsm", this);
+Cu.import("resource://shield-recipe-client/lib/StudyStorage.jsm", this);
+
+const REQUIRED_FIELDS = ["name", "addonId", "addonVersion", "description", "studyStartDate"];
+
+function withStudyStorage(testFn) {
+  return async () => {
+    try {
+      await testFn(StudyStorage);
+    } finally {
+      await StudyStorage.clear();
+      await StudyStorage.close();
+    }
+  };
+}
+
+function studyFactory(attrs) {
+  return Object.assign({
+    name: "Test study",
+    addonId: "foo@example.com",
+    addonVersion: "2.0.0",
+    description: "fake",
+    studyStartDate: new Date().toJSON(),
+  }, attrs);
+}
+
+add_task(withStudyStorage(async function testGetMissing(storage) {
+  await Assert.rejects(
+    storage.get("does-not-exist"),
+    /Could not find/,
+    "get rejects when the requested study is not stored",
+  );
+}));
+
+add_task(withStudyStorage(async function testCreateGet(storage) {
+  const study = studyFactory({name: "test-study"});
+  await storage.create(study);
+
+  const storedStudy = await storage.get("test-study");
+  Assert.deepEqual(study, storedStudy, "Create saved a new study to the storage.");
+  is(storedStudy.studyEndDate, null, "Create defaults the study end date to null.");
+  ok(storedStudy.active, "Create defaults the study to active.");
+}));
+
+add_task(withStudyStorage(async function testCreateExists(storage) {
+  const study = studyFactory({name: "test-study"});
+  await storage.create(study);
+
+  const dupeStudy = studyFactory({name: "test-study"});
+  Assert.rejects(
+    storage.create(dupeStudy),
+    /test-study/,
+    "create throws when a study exists with the given name already",
+  );
+}));
+
+add_task(withStudyStorage(async function testCreateSchema(storage) {
+  for (const requiredField of REQUIRED_FIELDS) {
+    let study = studyFactory({ [requiredField]: undefined });
+    let msg = `create threw an error due to missing required field ${requiredField}`;
+    await Assert.rejects(storage.create(study), new RegExp(requiredField), msg);
+
+    // Blank values are not allowed for required fields either
+    study = studyFactory({ [requiredField]: "" });
+    msg = `create threw an error due to blank required field ${requiredField}`;
+    await Assert.rejects(storage.create(study), new RegExp(requiredField), msg);
+  }
+
+  let study = studyFactory({studyStartDate: "invalid-datetime"});
+  await Assert.rejects(
+    storage.create(study),
+    /studyStartDate/,
+    "create rejected due to an invalid start date",
+  );
+
+  study = studyFactory({studyEndDate: "invalid-datetime"});
+  await Assert.rejects(
+    storage.create(study),
+    /studyEndDate/,
+    "create rejected due to an invalid end date",
+  );
+
+  study = studyFactory({active: "not a boolean"});
+  await Assert.rejects(
+    storage.create(study),
+    /active/,
+    "create rejected due to an invalid active status",
+  );
+}));
+
+add_task(withStudyStorage(async function testCreateHas(storage) {
+  let hasStudy = await storage.has("test-study");
+  ok(!hasStudy, "has returns false before the study has been created.");
+
+  const study = studyFactory({name: "test-study"});
+  await storage.create(study);
+
+  hasStudy = await storage.has("test-study");
+  ok(hasStudy, "has returns true after the study has been created");
+}));
+
+add_task(withStudyStorage(async function testCreateUpdate(storage) {
+  const study = studyFactory({name: "test-study", addonVersion: "1.0", addonId: "foo@example.com"});
+  await storage.create(study);
+  await storage.update("test-study", {addonVersion: "2.0"});
+
+  const storedStudy = await storage.get("test-study");
+  is(storedStudy.addonVersion, "2.0", "update modified the stored study.");
+  is(storedStudy.addonId, "foo@example.com", "update did not modify unspecified fields");
+}));
+
+add_task(withStudyStorage(async function testUpdateSchema(storage) {
+  const study = studyFactory({name: "test-study"});
+  await storage.create(study);
+
+  // Required fields must not be blank if they're specified
+  for (const requiredField of REQUIRED_FIELDS) {
+    const msg = `update threw an error due to blank required field ${requiredField}`;
+    await Assert.rejects(
+      storage.update("test-study", { [requiredField]: "" }),
+      new RegExp(requiredField),
+      msg,
+    );
+  }
+
+  await Assert.rejects(
+    storage.update("test-study", {studyStartDate: "invalid-datetime"}),
+    /studyStartDate/,
+    "update rejected due to an invalid start date",
+  );
+
+  await Assert.rejects(
+    storage.update("test-study", {studyEndDate: "invalid-datetime"}),
+    /studyEndDate/,
+    "update rejected due to an invalid end date",
+  );
+
+  await Assert.rejects(
+    storage.update("test-study", {active: "not a boolean"}),
+    /active/,
+    "update rejected due to an invalid active status",
+  );
+}));
+
+add_task(withStudyStorage(async function testUpdateMissing(storage) {
+  await Assert.rejects(
+    storage.update("does-not-exist", {addonVersion: "2.0"}),
+    /could not find/,
+    "update rejects when the requested study is not stored",
+  );
+}));
+
+add_task(withStudyStorage(async function testUpdateMissing(storage) {
+  await Assert.rejects(
+    storage.update("does-not-exist", {addonVersion: "2.0"}),
+    /could not find/,
+    "update rejects when the requested study is not stored",
+  );
+}));
+
+add_task(withStudyStorage(async function testCloseDatabase(storage) {
+  const openSpy = sinon.spy(IndexedDB, "open");
+  sinon.assert.notCalled(openSpy);
+
+  // Using storage at all should open the database, but only once.
+  await storage.has("foo");
+  await storage.create(studyFactory({name: "test-study"}));
+  sinon.assert.calledOnce(openSpy);
+
+  // close can be called multiple times
+  await storage.close();
+  await storage.close();
+
+  // After being closed, new operations cause the database to be opened again
+  await storage.has("test-study");
+  sinon.assert.calledTwice(openSpy);
+
+  openSpy.restore();
+}));
+
+add_task(withStudyStorage(async function testClear(storage) {
+  await storage.create(studyFactory({name: "test-study1"}));
+  await storage.create(studyFactory({name: "test-study2"}));
+  const hasAll = (await storage.has("test-study1")) && (await storage.has("test-study2"));
+  ok(hasAll, "Before calling clear, both studies are in the storage.");
+
+  await storage.clear();
+  const hasAny = (await storage.has("test-study1")) || (await storage.has("test-study2"));
+  ok(!hasAny, "After calling clear, all studies are removed from storage.");
+}));
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/browser/fixtures/addon-fixture/manifest.json
@@ -0,0 +1,11 @@
+{
+    "manifest_version": 2,
+    "name": "normandy_fixture",
+    "version": "1.0",
+    "description": "Dummy test fixture that's a webextension",
+    "applications": {
+        "gecko": {
+            "id": "normandydriver@example.com"
+        }
+    }
+}
new file mode 100644
index 0000000000000000000000000000000000000000..71a6f8fe7cd90427ca710599e7125f986e8dd24d
GIT binary patch
literal 4230
zc$|$_Wl$7s*Ir-=rC})%M7qI6knWBpBwguNmXL-eq@`85q>)xax_c>!U0guAS)@x)
zKKy3h@15tF=b8D=J?F=D=00bxb6@wLOGg6-2m$~A1OVgwE_K=~oAsYW0Duu00Pv@(
zrKB&%rv_0LfVucNIyqVLzjC+yZsaSPw0}<|=5w4UhNvgEH_b&*6)ca!aA_2EeY3(t
z*CeZ1FLJR#&lDZxbkA7P2nvKAvwjLDSs{C|8nT&iJkvDhnSIlK<)3oy`(ZcW$7VAY
zcO-7C;{&aPFa39|0Gm2ub+;8fT448QfTo}yod|((Vh)ML<vI|N*9`@52e8P7C#}hL
zG6~Y+iOYeJT{sm%{;!R_Lb{&?vA#Bzw<P#fgbSf1o5vj?2qS0viu|4(HJR_@uh;rU
zx(Wm)A++$S<RvKWsECZ!)G1y7shCHa;wOcd3!#%Su{BJ-S54xHuA+bzdVn0}^m^`&
z`&kKP|7%Va50ApcafI*n2)oLmS3ei_QBdk2a3xvtN(gB})OTDlqJ8t>@LrITn2scX
zT-q=ToC{o9!<rAorJ|QM^z{W)AJ72hT?vAZ?e|7Hc7DyY9AnS71Y>o*P&{P2A~PNO
zXkEn`bpHx66?g*l+8Q_vQhLPP`7F}aK^8MW)r30)av^0#A7&Lals_1PhJs{I4m>!1
zaVFfQzG*vV@)IYZ8i6z%f1uOE4c$^Nr?!Q)Mm<#tZ9LoaZ7!5}L$VI%cX%(;&?jfe
zhbD=0#zvHEIN8Se_&BA4IpiC7*{Z?jL-*fL+nA~vqbp#vwF?<SwPmY>yFQN}ZmB<i
zS*fXLN^iU><+m}#TrfuNLwmd%oFKOlpp=s#!a2rvJhC6j5+X2x6sw6!e&Eu;y+S$t
zM9ukwD-}zcclt*mL5uWNNoH>K9=3ER=BZ*$E;Zg4TyOG<l+J&~S~FxHnZVjx@Ld;&
zI4P;K$zykagg3&K{6;r<*}!kqV~PC+rFpi*pD^Pyb`C4?#k2EE&-t&mY)Q0?#T_Af
z1EIs@ouO`nn_OtA+&9<0+IdecXs(_sv@a5x$OO?Bb%)97nZ_Wce<z*z&+$B+qs$aw
z_9zw^eNU^`yD#jsFrxT+fZFP*&uGAURM(w*2%*xFymLf-h{3d&N__;KzJO2=N*W7k
zYkeXYe~D^BQqpS2DH>3HV|s#?aViTA2+kPi@SDr*7NILps3m>?Z+MJBUb!yKO|3n3
zBrWKKQuJQI>7g{JN$E^Q8xfHy0Wa8oB|Pz^yt+fKqEn$Dl&}8Tmhd#+8c6w-C(a&Q
z%{Jh%C^^Bx{kH}R2|~89T4%$f{JeAJ+6Lf}orrLWpwp6>CtM8tI}8PMU)C!{qDiss
z1e9sP7pHsv%K=mS2HjLZ`A#x2hOGp27)F|?36}i|$LmHN^KiM&{UYx;a%l(;RrA3N
zKc@@7gWg7JvZR@F*5zoHY3%*LxmZ}KldS~!{OeS%9xUu7xRqR)-UqT+a^RpyLyk5Z
zW8bn{lq<_~%NmpHE<m_?6H<VWkrxWn&0+CjRH<&+NU{XI`4ZTG_;=@W5l3jMn|w|g
ztHQ=rXRmnZkI?s=Rc>mM*CBM!bZQ)3Q7dB=Ga7W72H|gtobgBr31f1GjGcEG<QWVN
zJL@+#+UFmut|y(!+D{HHcYX&-)d#aYQtAr(>Q_;|0Bd7mWitCh$!;RDzPm2_tzgx^
zHO5cCj|cSy#Pzmd9^@~hYX`{Rr`sR>MEhYZ@DqoZYoQ<_eq(iNxF<O#GyCCJsZXRO
zoW}TL-o@_TMkfA35MIi+UvxD?D72Rwpp!rQ=#^f@VbVd&eEcZ!)^ga0^VV+v2O`CW
z{&@R0QmHX9yQm~6_`A!^^<7ufE<E7_TmO`sI#phU3<VdK8=a^V%+XGv(AVXfe%pFI
zGx{cC=!1JmxuR_@N>GI+K_36i$%{SC%wIieFB_nliOxtKZB>u_gnbfh(ub11?s+4q
zV*YGTJ<`&xv;h2#)qD+{U(C0%U2sn(O(0Ew3z&mY1bycLQ9hN1u4bM3-D4vx3LWlm
zuPEr2;faqRW&|d_#xo(}9UXC0x!qp?+$Ve56p^l9Uk3%T9WdaLURF~yat2?kEwwIy
zqQ^;WtpizJ<>Rm8AFg2GX4SM}o0;hbruum2t;j6jjD}ol6D>@a9_+LW&H#m)e`f?M
zbj2!#Eur<d`-YE4$I|`G6(n|j=5{HwQ6P=PQ0sbcmnORJlCbCdGTD|MEovK=!9GNA
zR!ak+mp^$9GiIp-3AIcRxuww%Sj+;ck=nbIOS9{+kcE(V=7=R?UzOB8MGRVdgZN^g
zZxgt~$2<ol;uCtS4Q5Qv%A5b48*$em!93c}14o~c+uy%`CCdFSdf7N?m|^cHoqd$m
zmwk;(8w}159=Vbe;Cx3=ZW;}*MxNDLA<L!u`AMKjz}3d&v*C}@+n%9y%D>K~+bnB!
zOvcZ&eVQS3$*dd<NkGN6v|qHW=u3B8@dN(BjE9m3nQxLm7>|dS3SwMcv<3{yHw!=E
zSM?|?Dvmw5Lwb;@%*?CQ^Mvs}d|4=47;0bA8mzWLVwKc|uPy{XRL&(tB>kvT_9H!=
zlJr;Q!lQ$D)wn-YbCO&6?qcV>9oS5i5LnQRW_agy82oe_wp-@UbLyA+IWwZzECTU+
z6d!`$;YJ3}XN*x#EqH!VQ~#7#?^v?ff5nc4uwV@N8p|-rPCt{pa8j1LNSp(;AF8y=
zW3Kk#jAnm!yj-hSG1JJXqfDUG(b4EsNiZ~4fIVRN{h`E50|ArCfd%$?G)>lT6iIzG
zrbh2XcG+lWsAwS4T?~YsD&x#UB40nF)3XxW$U4#EUMWs12(&BBQo-HB|Hvc>SoBR&
z1tu{D2g}X@=J>4^FGJ+L`)TnGepn5e65*#=#)1XOq@-|wN7mDcPC90s4cea=yt{1V
zj^-p7Kdoq}vcaS$RGBJ#X5u3v)&Z93G>!Yyqt7mrAc;6<2FYeBF(8RSv(hnQ!eN;k
znPMO}0g$sZ4Kj5iTj!}o2IB7HSzcLPEsgH`ef8LiA<7uv#am4K-N1w1GwWZ;#L^^m
z_b#KJ5)r_(KT3!P`cAu6BX_AE4HHteV&2R-lk7_6+b2zIyvHG9z<#uvdjPlS_K#c9
zUV^nl<Qq}h(wEG6wwA4BfdIvp*5^gj*GlV5#hzIzq0D)F8**uBIcqwWECe17hKIuA
zh=t%I8H$9Zc{nnU-5t3$-?jsGnBN=gWt%lRU-}uK`j(h*m0jjxa_W6D_24A86s{VF
zgUZ8vPZ88!@uJUqG_3N4$A&B=DH^nT!(S^&n!>SaAwTM}`k*+cWw2t`xkiZ+p}kai
z&r?TQ>WALo{XE`QM9K?DrQcH>R=OKc!m-yf7G4)sIp0%Lz7Bx*EsZ!HuDq$6zp69L
z#!tT3g?}AQd&*ieX-T-<v<();(DMuwG{y?>&(`$ZseN`3LztT>ESwwtvfiI0p74&g
zHY$kv+Ss-A)W&|=G{xRY@*S5^a_i$g*roU=&-U|`&`()?SO-dDac(1+xRtt)3JG$1
zR=U(n9n$dXDn=gO!BncdJ3~Olg{^r%g+aaqtvYiNx*)6EiHYdaCSB%?5u1Q&xqA1C
z=Z<k)-lAc}$;9dCj&@->wMEa)eK>XVkWuz~!6T1fNVrI<(;$MVUvJHFcPM533<WU4
zI+{5B5mV4EkG2BrX;q3>5Fq0A2}^xr(}ypt@SDv|eVC+?jop)kB4Gvc@%s+6F$|w^
z!cMO{c;9#QeG@oMai>_iNhS!nG9AI{?%+KUJ(Ecy3I1&xzT(x0L;ge~V)hy5xQIF;
zXpOmJ4dioqlz_x73fYcbOtx=~gHC?t?iA=PpB4YE#<7@Q*XobCj;hHuRG*n=eC}Ql
zxhn{t2B2i5$+k=dWSQRaZt_|;JAZXENK*sNy4X01!ciRrTHb_#liz3(y2@y6r?aiR
zG*GM<;b@41+<5$H%aaGQ+<^<9T!tgI)_Mxc;V%;qa;M|s$21vR#wqjLj+uP#Mk8iC
zXYTAkR3KjW>^z+A?%#9=S=rXB=sMryXAR(zOcmNidv233!rMaC2H@is{oGNV9PbO)
zUk&k*po<qnOIK}cX?aAS^R_)2sxgH$01^A;eHZQJ1?+3dL(=}rwarCrQo0-4Y-C5z
zmZ#0!plu0d^d|OYBX;oujRGsSk6{w0ZiXcRDDJX+`5Chu$&W=bciW1VACDs*hW=oN
z7B^JeVPk$-tv}h|(N!yrvc85<J(|N?8Dpy~bu(ILt0O|rPq?Ld$gEZYbFzFkJ82Dh
z&%_O0FuCfEJ6}TUIq$VEfIVLfVIEgWrR5y<Jd51E1EE&wHgj~Q7oPAcs&QnJK#Oq=
z3U=44XwACmvbzaYgBvU+H8`v*1PgJ7owF@8KIjU!F^|SUW`f>QYX!M|?yWi-nPT~#
zvfmbOLUm|1(?J0}0RP-8M-_i-cdBl;z$3(2&e~}*)7)KpA;#l5MKm?RYc6k`_7W2%
z<m9ZM$*G2-{*ABme_qMKxKTafwMcyWExNu<VgM}wX6fu`XY204|H9qH*+3r`fK!*t
zX5#5X2*3iKVgUgE+iDQsLeecrq;wt_o*a&A&^g%K8KnQvtt;P`(@mqi>;|IRGaSmc
zxNE)G6r$g)s==r1j7Rh~i%Lb*$!J(T)DeUVuPZ7kE_!<voPS|1kJ<eWMba8DjrWP2
z_iJ`;<ACm8u=`TVb>j$#f@k4HOOsdeN$}-Ihwg^8?n<8CL3$}_H+Ivn^(rXiCof1|
zuE$<f2q0%uckbx?#RtmIucn6HqEx`&ec*qVgxMWyjSOpQ@bOO&Yw&T8jf~go2~G<w
ztsH5LXw~ZthO6(`Qf+R!HQ5x0g2XopH)(8BY}TluTo=u0+(3WPd#|wQn#p{5!fWKK
z!7>H+?L%XupZ`*|-u(LM@$iWU6w05d-dH9ZTBBZ~7_>L-x>^>zl$1|4#ndx0vX&^T
zU=HQy2mF<EeO!#oi(BM-o0R`#Uft~o$27+^c)9r0k$)k5<oO8cIi#t5yUgjIt#H5<
zAYP)o%-I1_M0LdVZSEmjdbQX*`O(-gSW}49ed0&SG{9~Aa4~z8WT0}R1bukPx<7qO
za!ZnrpP%CFD>tlLYFOAHoPT%L|A}BMz`ywHKNA>n{{6rGi9gMSuX%|9|G(e&cT#_s
p`M;zHNd8&$zfbu4oBwx08_7Q^pre6{_vZre_R!w)GA8|l{s)EOuulL0
--- a/browser/extensions/shield-recipe-client/test/browser/head.js
+++ b/browser/extensions/shield-recipe-client/test/browser/head.js
@@ -21,16 +21,23 @@ sinon.assert.fail = function(message) {
 registerCleanupFunction(async function() {
   // Cleanup window or the test runner will throw an error
   delete window.sinon;
 });
 
 
 this.UUID_REGEX = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/;
 
+this.TEST_XPI_URL = (function() {
+  const dir = getChromeDir(getResolvedURI(gTestPath));
+  dir.append("fixtures");
+  dir.append("normandy.xpi");
+  return Services.io.newFileURI(dir).spec;
+})();
+
 this.withSandboxManager = function(Assert, testFunction) {
   return async function inner() {
     const sandboxManager = new SandboxManager();
     sandboxManager.addHold("test running");
 
     await testFunction(sandboxManager);
 
     sandboxManager.removeHold("test running");
@@ -38,39 +45,44 @@ this.withSandboxManager = function(Asser
       .then(() => Assert.ok(true, "sandbox is nuked"))
       .catch(e => Assert.ok(false, "sandbox is nuked", e));
   };
 };
 
 this.withDriver = function(Assert, testFunction) {
   return withSandboxManager(Assert, async function inner(sandboxManager) {
     const driver = new NormandyDriver(sandboxManager);
-    await testFunction(driver);
+    await testFunction(driver, sandboxManager);
   });
 };
 
 this.withMockNormandyApi = function(testFunction) {
   return async function inner(...args) {
     const mockApi = {actions: [], recipes: [], implementations: {}};
 
-    sinon.stub(NormandyApi, "fetchActions", async () => mockApi.actions);
-    sinon.stub(NormandyApi, "fetchRecipes", async () => mockApi.recipes);
-    sinon.stub(NormandyApi, "fetchImplementation", async action => {
-      const impl = mockApi.implementations[action.name];
-      if (!impl) {
-        throw new Error("Missing");
+    // Use callsFake instead of resolves so that the current values in mockApi are used.
+    mockApi.fetchActions = sinon.stub(NormandyApi, "fetchActions").callsFake(async () => mockApi.actions);
+    mockApi.fetchRecipes = sinon.stub(NormandyApi, "fetchRecipes").callsFake(async () => mockApi.recipes);
+    mockApi.fetchImplementation = sinon.stub(NormandyApi, "fetchImplementation").callsFake(
+      async action => {
+        const impl = mockApi.implementations[action.name];
+        if (!impl) {
+          throw new Error("Missing");
+        }
+        return impl;
       }
-      return impl;
-    });
+    );
 
-    await testFunction(mockApi, ...args);
-
-    NormandyApi.fetchActions.restore();
-    NormandyApi.fetchRecipes.restore();
-    NormandyApi.fetchImplementation.restore();
+    try {
+      await testFunction(mockApi, ...args);
+    } finally {
+      mockApi.fetchActions.restore();
+      mockApi.fetchRecipes.restore();
+      mockApi.fetchImplementation.restore();
+    }
   };
 };
 
 const preferenceBranches = {
   user: Preferences,
   default: new Preferences({defaultBranch: true}),
 };
 
--- a/browser/extensions/shield-recipe-client/test/unit/head_xpc.js
+++ b/browser/extensions/shield-recipe-client/test/unit/head_xpc.js
@@ -18,24 +18,25 @@ if (!extensionDir.exists()) {
   extensionDir = extensionDir.parent;
   extensionDir.append(EXTENSION_ID + ".xpi");
 }
 Components.manager.addBootstrappedManifestLocation(extensionDir);
 
 // ================================================
 // Load mocking/stubbing library, sinon
 // docs: http://sinonjs.org/releases/v2.3.2/
+/* exported sinon */
 Cu.import("resource://gre/modules/Timer.jsm");
 const {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
 const loader = new Loader.Loader({
   paths: {
     "": "resource://testing-common/",
   },
   globals: {
     setTimeout,
     setInterval,
     clearTimeout,
     clearInterval,
   },
 });
 const require = Loader.Require(loader, {id: ""});
-const sinon = require("sinon-2.3.2");
+this.sinon = require("sinon-2.3.2");
 // ================================================
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/api/v1/index.json
@@ -0,0 +1,8 @@
+{
+  "action-list": "/api/v1/action/",
+  "classify-client": "/api/v1/classify_client/",
+  "filters": "/api/v1/filters/",
+  "recipe-list": "/api/v1/recipe/",
+  "recipe-signed": "/api/v1/recipe/signed/",
+  "reciperevision-list": "/api/v1/recipe_revision/"
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/index.json
@@ -0,0 +1,1 @@
+[{"recipe":{"action":"console-log","arguments":{"message":"this signature does not match this recipe"},"channels":[],"countries":[],"enabled":true,"extra_filter_expression":"true || true","filter_expression":"true || true","id":1,"last_updated":"2017-02-17T18:29:09.839239Z","locales":[],"name":"system-addon-test","revision_id":"b2cb8a26e132182d7d02cf50695d2c7f06cf3b954ff2ff63bca49d724ee91950"},"signature":{"public_key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p","signature":"p4g3eurmPsJK5UcGT97BRyKstpwZ_2mNJkDGpd6QXlkXfvgwprjeyb5yeIEkKUXqc6krWid4obB_OP9-CwOi9tvKY1pV8p98CT5BhF0IVgpF3b7KBW1a0BVdg5owoG5W","timestamp":"2017-02-17T18:29:09.847614Z","x5u":"/api/v1/recipe/signed/normandy.content-signature.mozilla.org-20210705.dev.chain"}}]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/normandy.content-signature.mozilla.org-20210705.dev.chain
@@ -0,0 +1,123 @@
+-----BEGIN CERTIFICATE-----
+MIIGRTCCBC2gAwIBAgIEAQAABTANBgkqhkiG9w0BAQwFADBrMQswCQYDVQQGEwJV
+UzEQMA4GA1UEChMHQWxsaXpvbTEXMBUGA1UECxMOQ2xvdWQgU2VydmljZXMxMTAv
+BgNVBAMTKERldnppbGxhIFNpZ25pbmcgU2VydmljZXMgSW50ZXJtZWRpYXRlIDEw
+HhcNMTYwNzA2MjE1NzE1WhcNMjEwNzA1MjE1NzE1WjCBrzELMAkGA1UEBhMCVVMx
+EzARBgNVBAgTCkNhbGlmb3JuaWExHDAaBgNVBAoTE01vemlsbGEgQ29ycG9yYXRp
+b24xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMS8wLQYDVQQDEyZub3JtYW5keS5j
+b250ZW50LXNpZ25hdHVyZS5tb3ppbGxhLm9yZzEjMCEGCSqGSIb3DQEJARYUc2Vj
+dXJpdHlAbW96aWxsYS5vcmcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARUQqIIAiTB
+GDVUWw/wk5h1IXpreq+BtE+gQr15O4tusHpCLGjOxwpHiJYnxk45fpE8JGAV19UO
+hmqMUEU0k31C1EGTSZW0ducSvHrh3a8wXShZ6dxLWHItbbCGA6A7PumjggJYMIIC
+VDAdBgNVHQ4EFgQUVfksSjlZ0i1TBiS1vcoObaMeXn0wge8GA1UdIwSB5zCB5IAU
+/YboUIXAovChEpudDBuodHKbjUuhgcWkgcIwgb8xCzAJBgNVBAYTAlVTMQswCQYD
+VQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVu
+dCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNp
+Z25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290
+Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIEAQAABDAMBgNVHRMBAf8EAjAA
+MA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzBEBgNVHR8E
+PTA7MDmgN6A1hjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3Mu
+bmV0L2NhL2NybC5wZW0wQgYJYIZIAYb4QgEEBDUWM2h0dHBzOi8vY29udGVudC1z
+aWduYXR1cmUuZGV2Lm1vemF3cy5uZXQvY2EvY3JsLnBlbTBOBggrBgEFBQcBAQRC
+MEAwPgYIKwYBBQUHMAKGMmh0dHBzOi8vY29udGVudC1zaWduYXR1cmUuZGV2Lm1v
+emF3cy5uZXQvY2EvY2EucGVtMDEGA1UdEQQqMCiCJm5vcm1hbmR5LmNvbnRlbnQt
+c2lnbmF0dXJlLm1vemlsbGEub3JnMA0GCSqGSIb3DQEBDAUAA4ICAQCwb+8JTAB7
+ZfQmFqPUIV2cQQv696AaDPQCtA9YS4zmUfcLMvfZVAbK397zFr0RMDdLiTUQDoeq
+rBEmPXhJRPiv6JAK4n7Jf6Y6XfXcNxx+q3garR09Vm/0CnEq/iV+ZAtPkoKIO9kr
+Nkzecd894yQCF4hIuPQ5qtMySeqJmH3Dp13eq4T0Oew1Bu32rNHuBJh2xYBkWdun
+aAw/YX0I5EqZBP/XA6gbiA160tTK+hnpnlMtw/ljkvfhHbWpICD4aSiTL8L3vABQ
+j7bqjMKR5xDkuGWshZfcmonpvQhGTye/RZ1vz5IzA3VOJt1mz5bdZlitpaOm/Yv0
+x6aODz8GP/PiRWFQ5CW8Uf/7pGc5rSyvnfZV2ix8EzFlo8cUtuN1fjrPFPOFOLvG
+iiB6S9nlXiKBGYIDdd8V8iC5xJpzjiAWJQigwSNzuc2K30+iPo3w0zazkwe5V8jW
+gj6gItYxh5xwVQTPHD0EOd9HvV1ou42+rH5Y+ISFUm25zz02UtUHEK0BKtL0lmdt
+DwVq5jcHn6bx2/iwUtlKvPXtfM/6JjTJlkLZLtS7U5/pwcS0owo9zAL0qg3bdm16
++v/olmPqQFLUHmamJTzv3rojj5X/uVdx1HMM3wBjV9tRYoYaZw9RIInRmM8Z1pHv
+JJ+CIZgCyd5vgp57BKiodRZcgHoCH+BkOQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIHijCCBXKgAwIBAgIEAQAABDANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQK
+Ex1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNv
+bnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2Vj
+K2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjIx
+NDkyNloXDTIxMDcwNTIxNDkyNlowazELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0Fs
+bGl6b20xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMTEwLwYDVQQDEyhEZXZ6aWxs
+YSBTaWduaW5nIFNlcnZpY2VzIEludGVybWVkaWF0ZSAxMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAypIfUURYgWzGw8G/1Pz9zW+Tsjirx2owThiv2gys
+wJiWL/9/2gzKOrYDEqlDUudfA/BjVRtT9+NbYgnhaCkNfADOAacWS83aMhedAqhP
+bVd5YhGQdpijI7f1AVTSb0ehrU2nhOZHvHX5Tk2fbRx3ryefIazNTLFGpiMBbsNv
+tSI/+fjW8s0MhKNqlLnk6a9mZKo0mEy7HjGYV8nzsgI17rKLx/s2HG4TFG0+JQzc
+UGlum3Tg58ritDzWdyKIkmKNZ48oLBX99Qc8j8B1UyiLv6TZmjVX0I+Ds7eSWHZk
+0axLEpTyf2r7fHvN4iDNCPajw+ZpuuBfbs80Ha8b8MHvnpf9fbwiirodNQOVpY4c
+t5E3Us3eYwBKdqDEbECWxCKGAS2/iVVUCNKHsg0sSxgqcwxrxyrddQRUQ0EM38DZ
+F/vHt+vTdHt07kezbjJe0Kklel59uSpghA0iL4vxzbTns1fuwYOgVrNGs3acTkiB
+GhFOxRXUPGtpdYmv+AaR9WlWJQY1GIEoVrinPVH7bcCwyh1CcUbHL+oAFTcmc6kZ
+7azNg21tWILIRL7R0IZYQm0tF5TTwCsjVC7FuHaBtkxtVrrZqeKjvVXQ8TK5VoI0
+BUQ6BKHGeTtm+0HBpheYBDy3wkOsEGbGHLEM6cMeiz6PyCXF8wXli8Qb/TjN3LHZ
+e30CAwEAAaOCAd8wggHbMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGG
+MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBT9huhQhcCi8KESm50M
+G6h0cpuNSzCB7AYDVR0jBIHkMIHhgBSDx8s0qJaMyQCehKcuzgzVNRA75qGBxaSB
+wjCBvzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFp
+biBWaWV3MSYwJAYDVQQKEx1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEm
+MCQGA1UEAxMdZGV2LmNvbnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG
+9w0BCQEWLGNsb3Vkc2VjK2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEu
+Y29tggEBMEIGCWCGSAGG+EIBBAQ1FjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJl
+LmRldi5tb3phd3MubmV0L2NhL2NybC5wZW0wTgYIKwYBBQUHAQEEQjBAMD4GCCsG
+AQUFBzAChjJodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3MubmV0
+L2NhL2NhLnBlbTANBgkqhkiG9w0BAQwFAAOCAgEAbum0z0ccqI1Wp49VtsGmUPHA
+gjPPy2Xa5NePmqY87WrGdhjN3xbLVb3hx8T2N6pqDjMY2rEynXKEOek3oJkQ3C6J
+8AFP6Y93gaAlNz6EA0J0mqdW5TMI8YEYsu2ma+dQQ8wm9f/5b+/Y8qwfhztP06W5
+H6IG04/RvgUwYMnSR4QvT309fu5UmCRUDzsO53ZmQCfmN94u3NxHc4S6n0Q6AKAM
+kh5Ld9SQnlqqqDykzn7hYDi8nTLWc7IYqkGfNMilDEKbAl4CjnSfyEvpdFAJ9sPR
+UL+kaWFSMvaqIPNpxS5OpoPZjmxEc9HHnCHxtfDHWdXTJILjijPrCdMaxOCHfIqV
+5roOJggI4RZ0YM68IL1MfN4IEVOrHhKjDHtd1gcmy2KU4jfj9LQq9YTnyvZ2z1yS
+lS310HG3or1K8Nnu5Utfe7T6ppX8bLRMkS1/w0p7DKxHaf4D/GJcCtM9lcSt9JpW
+6ACKFikjWR4ZxczYKgApc0wcjd7XBuO5777xtOeyEUDHdDft3jiXA91dYM5UAzc3
+69z/3zmaELzo0gWcrjLXh7fU9AvbU4EUF6rwzxbPGF78jJcGK+oBf8uWUCkBykDt
+VsAEZI1u4EDg8e/C1nFqaH9gNMArAgquYIB9rve+hdprIMnva0S147pflWopBWcb
+jwzgpfquuYnnxe0CNBA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIH3DCCBcSgAwIBAgIBATANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMCVVMx
+CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQKEx1D
+b250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNvbnRl
+bnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2VjK2Rl
+dnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjE4MTUy
+MloXDTI2MDcwNDE4MTUyMlowgb8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEW
+MBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVudCBTaWduYXR1
+cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNpZ25hdHVyZS5y
+b290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290Y29udGVudHNp
+Z25hdHVyZUBtb3ppbGxhLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
+ggIBAJcPcXhD8MzF6OTn5qZ0L7lX1+PEgLKhI9g1HxxDYDVup4Zm0kZhPGmFSlml
+6eVO99OvvHdAlHhQGCIG7h+w1cp66mWjfpcvtQH23uRoKZfiW3jy1jUWrvdXolxR
+t1taZosjzo+9OP8TvG6LpJj7AvqUiYD4wYnQJtt0jNRN4d6MUfQwiavSS5uTBuxd
+ZJ4TsPvEI+Iv4A4PSobSzxkg79LTMvsGtDLQv7nN5hMs9T18EL5GnIKoqnSQCU0d
+n2CN7S3QiQ+cbORWsSYqCTj1gUbFh6X3duEB/ypLcyWFbqeJmPHikjY8q8pLjZSB
+IYiTJYLyvYlKdM5QleC/xuBNnMPCftrwwLHjWE4Dd7C9t7k0R5xyOetuiHLCwOcQ
+tuckp7RgFKoviMNv3gdkzwVapOklcsaRkRscv6OMTKJNsdJVIDLrPF1wMABhbEQB
+64BL0uL4lkFtpXXbJzQ6mgUNQveJkfUWOoB+cA/6GtI4J0aQfvQgloCYI6jxNn/e
+Nvk5OV9KFOhXS2dnDft3wHU46sg5yXOuds1u6UrOoATBNFlkS95m4zIX1Svu091+
+CKTiLK85+ZiFtAlU2bPr3Bk3GhL3Z586ae6a4QUEx6SPQVXc18ezB4qxKqFc+avI
+ylccYMRhVP+ruADxtUM5Vy6x3U8BwBK5RLdecRx2FEBDImY1AgMBAAGjggHfMIIB
+2zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAWBgNVHSUBAf8EDDAK
+BggrBgEFBQcDAzAdBgNVHQ4EFgQUg8fLNKiWjMkAnoSnLs4M1TUQO+YwgewGA1Ud
+IwSB5DCB4YAUg8fLNKiWjMkAnoSnLs4M1TUQO+ahgcWkgcIwgb8xCzAJBgNVBAYT
+AlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UE
+ChMdQ29udGVudCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5j
+b250ZW50LXNpZ25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNl
+YytkZXZyb290Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIBATBCBglghkgB
+hvhCAQQENRYzaHR0cHM6Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5l
+dC9jYS9jcmwucGVtME4GCCsGAQUFBwEBBEIwQDA+BggrBgEFBQcwAoYyaHR0cHM6
+Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5ldC9jYS9jYS5wZW0wDQYJ
+KoZIhvcNAQEMBQADggIBAAAQ+fotZE79FfZ8Lz7eiTUzlwHXCdSE2XD3nMROu6n6
+uLTBPrf2C+k+U1FjKVvL5/WCUj6hIzP2X6Sb8+o0XHX9mKN0yoMORTEYJOnazYPK
+KSUUFnE4vGgQkr6k/31gGRMTICdnf3VOOAlUCQ5bOmGIuWi401E3sbd85U+TJe0A
+nHlU+XjtfzlqcBvQivdbA0s+GEG55uRPvn952aTBEMHfn+2JqKeLShl4AtUAfu+h
+6md3Z2HrEC7B3GK8ekWPu0G/ZuWTuFvOimZ+5C8IPRJXcIR/siPQl1x6dpTCew6t
+lPVcVuvg6SQwzvxetkNrGUe2Wb2s9+PK2PUvfOS8ee25SNmfG3XK9qJpqGUhzSBX
+T8QQgyxd0Su5G7Wze7aaHZd/fqIm/G8YFR0HiC2xni/lnDTXFDPCe+HCnSk0bH6U
+wpr6I1yK8oZ2IdnNVfuABGMmGOhvSQ8r7//ea9WKhCsGNQawpVWVioY7hpyNAJ0O
+Vn4xqG5f6allz8lgpwAQ+AeEEClHca6hh6mj9KhD1Of1CC2Vx52GHNh/jMYEc3/g
+zLKniencBqn3Y2XH2daITGJddcleN09+a1NaTkT3hgr7LumxM8EVssPkC+z9j4Vf
+Gbste+8S5QCMhh00g5vR9QF8EaFqdxCdSxrsA4GmpCa5UQl8jtCnpp2DLKXuOh72
+-----END CERTIFICATE-----
deleted file mode 100644
--- a/browser/extensions/shield-recipe-client/test/unit/test_ActionSandboxManager.js
+++ /dev/null
@@ -1,169 +0,0 @@
-"use strict";
-
-Cu.import("resource://shield-recipe-client/lib/ActionSandboxManager.jsm");
-Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm");
-
-async function withManager(script, testFunction) {
-  const manager = new ActionSandboxManager(script);
-  manager.addHold("testing");
-  await testFunction(manager);
-  manager.removeHold("testing");
-}
-
-add_task(async function testMissingCallbackName() {
-  await withManager("1 + 1", async manager => {
-    equal(
-      await manager.runAsyncCallback("missingCallback"),
-      undefined,
-      "runAsyncCallback returns undefined when given a missing callback name",
-    );
-  });
-});
-
-add_task(async function testCallback() {
-  const script = `
-    registerAsyncCallback("testCallback", async function(normandy) {
-      return 5;
-    });
-  `;
-
-  await withManager(script, async manager => {
-    const result = await manager.runAsyncCallback("testCallback");
-    equal(result, 5, "runAsyncCallback executes the named callback inside the sandbox");
-  });
-});
-
-add_task(async function testArguments() {
-  const script = `
-    registerAsyncCallback("testCallback", async function(normandy, a, b) {
-      return a + b;
-    });
-  `;
-
-  await withManager(script, async manager => {
-    const result = await manager.runAsyncCallback("testCallback", 4, 6);
-    equal(result, 10, "runAsyncCallback passes arguments to the callback");
-  });
-});
-
-add_task(async function testCloning() {
-  const script = `
-    registerAsyncCallback("testCallback", async function(normandy, obj) {
-      return {foo: "bar", baz: obj.baz};
-    });
-  `;
-
-  await withManager(script, async manager => {
-    const result = await manager.runAsyncCallback("testCallback", {baz: "biff"});
-
-    deepEqual(
-      result,
-      {foo: "bar", baz: "biff"},
-      (
-        "runAsyncCallback clones arguments into the sandbox and return values into the " +
-        "context it was called from"
-      ),
-    );
-  });
-});
-
-add_task(async function testError() {
-  const script = `
-    registerAsyncCallback("testCallback", async function(normandy) {
-      throw new Error("WHY")
-    });
-  `;
-
-  await withManager(script, async manager => {
-    try {
-      await manager.runAsyncCallback("testCallback");
-      ok(false, "runAsnycCallbackFromScript throws errors when raised by the sandbox");
-    } catch (err) {
-      equal(err.message, "WHY", "runAsnycCallbackFromScript clones error messages");
-    }
-  });
-});
-
-add_task(async function testDriver() {
-  const script = `
-    registerAsyncCallback("testCallback", async function(normandy) {
-      return normandy;
-    });
-  `;
-
-  await withManager(script, async manager => {
-    const sandboxDriver = await manager.runAsyncCallback("testCallback");
-    const referenceDriver = new NormandyDriver(manager);
-    equal(
-      sandboxDriver.constructor.name,
-      "NormandyDriver",
-      "runAsyncCallback passes a driver as the first parameter",
-    );
-    for (const prop in referenceDriver) {
-      ok(prop in sandboxDriver, "runAsyncCallback passes a driver as the first parameter");
-    }
-  });
-});
-
-add_task(async function testGlobalObject() {
-  // Test that window is an alias for the global object, and that it
-  // has some expected functions available on it.
-  const script = `
-    window.setOnWindow = "set";
-    this.setOnGlobal = "set";
-
-    registerAsyncCallback("testCallback", async function(normandy) {
-      return {
-        setOnWindow: setOnWindow,
-        setOnGlobal: window.setOnGlobal,
-        setTimeoutExists: setTimeout !== undefined,
-        clearTimeoutExists: clearTimeout !== undefined,
-      };
-    });
-  `;
-
-  await withManager(script, async manager => {
-    const result = await manager.runAsyncCallback("testCallback");
-    Assert.deepEqual(result, {
-      setOnWindow: "set",
-      setOnGlobal: "set",
-      setTimeoutExists: true,
-      clearTimeoutExists: true,
-    }, "sandbox.window is the global object and has expected functions.");
-  });
-});
-
-add_task(async function testRegisterActionShim() {
-  const recipe = {
-    foo: "bar",
-  };
-  const script = `
-    class TestAction {
-      constructor(driver, recipe) {
-        this.driver = driver;
-        this.recipe = recipe;
-      }
-
-      execute() {
-        return new Promise(resolve => {
-          resolve({
-            foo: this.recipe.foo,
-            isDriver: "log" in this.driver,
-          });
-        });
-      }
-    }
-
-    registerAction('test-action', TestAction);
-  `;
-
-  await withManager(script, async manager => {
-    const result = await manager.runAsyncCallback("action", recipe);
-    equal(result.foo, "bar", "registerAction registers an async callback for actions");
-    equal(
-      result.isDriver,
-      true,
-      "registerAction passes the driver to the action class constructor",
-    );
-  });
-});
--- a/browser/extensions/shield-recipe-client/test/unit/test_NormandyApi.js
+++ b/browser/extensions/shield-recipe-client/test/unit/test_NormandyApi.js
@@ -1,17 +1,33 @@
+/* globals sinon */
 "use strict";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/CanonicalJSON.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm", this);
 
 load("utils.js"); /* globals withMockPreferences */
 
+class MockResponse {
+  constructor(content) {
+    this.content = content;
+  }
+
+  async text() {
+    return this.content;
+  }
+
+  async json() {
+    return JSON.loads(this.content);
+  }
+}
+
 function withServer(server, task) {
   return withMockPreferences(async function inner(preferences) {
     const serverUrl = `http://localhost:${server.identity.primaryPort}`;
     preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1`);
     preferences.set(
       "security.content.signature.root_hash",
       // Hash of the key that signs the normandy dev certificates
       "4C:35:B1:C3:E3:12:D9:55:E7:78:ED:D0:A7:E7:8A:38:83:04:EF:01:BF:FA:03:29:B2:46:9F:3C:C5:EC:36:04"
@@ -32,19 +48,19 @@ function makeScriptServer(scriptPath) {
   server.start(-1);
   return server;
 }
 
 function withScriptServer(scriptPath, task) {
   return withServer(makeScriptServer(scriptPath), task);
 }
 
-function makeMockApiServer() {
+function makeMockApiServer(directory) {
   const server = new HttpServer();
-  server.registerDirectory("/", do_get_file("mock_api"));
+  server.registerDirectory("/", directory);
 
   server.setIndexHandler(async function(request, response) {
     response.processAsync();
     const dir = request.getProperty("directory");
     const index = dir.clone();
     index.append("index.json");
 
     if (!index.exists()) {
@@ -65,17 +81,17 @@ function makeMockApiServer() {
     }
   });
 
   server.start(-1);
   return server;
 }
 
 function withMockApiServer(task) {
-  return withServer(makeMockApiServer(), task);
+  return withServer(makeMockApiServer(do_get_file("mock_api")), task);
 }
 
 add_task(withMockApiServer(async function test_get(serverUrl) {
   // Test that NormandyApi can fetch from the test server.
   const response = await NormandyApi.get(`${serverUrl}/api/v1/`);
   const data = await response.json();
   equal(data["recipe-list"], "/api/v1/recipe/", "Expected data in response");
 }));
@@ -120,16 +136,87 @@ add_task(withMockApiServer(async functio
 }));
 
 add_task(withMockApiServer(async function test_fetchRecipes() {
   const recipes = await NormandyApi.fetchRecipes();
   equal(recipes.length, 1);
   equal(recipes[0].name, "system-addon-test");
 }));
 
+add_task(async function test_fetchRecipes_canonical_mismatch() {
+  const getApiUrl = sinon.stub(NormandyApi, "getApiUrl").resolves("http://localhost/recipes/");
+
+  // Recipe is non-canonical (it has whitespace, properties are out of order)
+  const response = new MockResponse(`[
+    {
+      "recipe": {"b": 1, "a": 2},
+      "signature": {"signature": "", "x5u": ""}
+    }
+  ]`);
+  const get = sinon.stub(NormandyApi, "get").resolves(response);
+
+  try {
+    await NormandyApi.fetchRecipes();
+    ok(false, "fetchRecipes did not throw for canonical JSON mismatch");
+  } catch (err) {
+    ok(err instanceof NormandyApi.InvalidSignatureError, "Error is an InvalidSignatureError");
+    ok(/Canonical/.test(err), "Error is due to canonical JSON mismatch");
+  }
+
+  getApiUrl.restore();
+  get.restore();
+});
+
+// Test validation errors due to validation throwing an exception (e.g. when
+// parameters passed to validation are malformed).
+add_task(async function test_fetchRecipes_validation_error() {
+  const getApiUrl = sinon.stub(NormandyApi, "getApiUrl").resolves("http://localhost/recipes/");
+
+  // Mock two URLs: recipes and the x5u
+  const get = sinon.stub(NormandyApi, "get").callsFake(async url => {
+    if (url.endsWith("recipes/")) {
+      return new MockResponse(CanonicalJSON.stringify([
+        {
+          recipe: {a: 1, b: 2},
+          signature: {signature: "invalidsignature", x5u: "http://localhost/x5u/"},
+        },
+      ]));
+    } else if (url.endsWith("x5u/")) {
+      return new MockResponse("certchain");
+    }
+
+    return null;
+  });
+
+  // Validation should fail due to a malformed x5u and signature.
+  try {
+    await NormandyApi.fetchRecipes();
+    ok(false, "fetchRecipes did not throw for a validation error");
+  } catch (err) {
+    ok(err instanceof NormandyApi.InvalidSignatureError, "Error is an InvalidSignatureError");
+    ok(/signature/.test(err), "Error is due to a validation error");
+  }
+
+  getApiUrl.restore();
+  get.restore();
+});
+
+// Test validation errors due to validation returning false (e.g. when parameters
+// passed to validation are correctly formed, but not valid for the data).
+const invalidSignatureServer = makeMockApiServer(do_get_file("invalid_recipe_signature_api"));
+add_task(withServer(invalidSignatureServer, async function test_fetchRecipes_invalid_signature() {
+  try {
+    await NormandyApi.fetchRecipes();
+    ok(false, "fetchRecipes did not throw for an invalid signature");
+  } catch (err) {
+    ok(err instanceof NormandyApi.InvalidSignatureError, "Error is an InvalidSignatureError");
+    ok(/signature/.test(err), "Error is due to an invalid signature");
+  }
+}));
+
 add_task(withMockApiServer(async function test_classifyClient() {
   const classification = await NormandyApi.classifyClient();
   Assert.deepEqual(classification, {
     country: "US",
     request_time: new Date("2017-02-22T17:43:24.657841Z"),
   });
 }));
 
--- a/browser/extensions/shield-recipe-client/test/unit/test_SandboxManager.js
+++ b/browser/extensions/shield-recipe-client/test/unit/test_SandboxManager.js
@@ -1,15 +1,15 @@
 "use strict";
 
 Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm");
 
 // wrapAsync should wrap privileged Promises with Promises that are usable by
 // the sandbox.
-add_task(async function() {
+add_task(function* () {
   const manager = new SandboxManager();
   manager.addHold("testing");
 
   manager.cloneIntoGlobal("driver", {
     async privileged() {
       return "privileged";
     },
     wrapped: manager.wrapAsync(async function() {
@@ -20,17 +20,17 @@ add_task(async function() {
       return this.aValue;
     }),
   }, {cloneFunctions: true});
 
   // Assertion helpers
   manager.addGlobal("ok", ok);
   manager.addGlobal("equal", equal);
 
-  const sandboxResult = await new Promise(resolve => {
+  const sandboxResult = yield new Promise(resolve => {
     manager.addGlobal("resolve", result => resolve(result));
     manager.evalInSandbox(`
       // Unwrapped privileged promises are not accessible in the sandbox
       try {
         const privilegedResult = driver.privileged().then(() => false);
         ok(false, "The sandbox could not use a privileged Promise");
       } catch (err) { }
 
@@ -40,31 +40,31 @@ add_task(async function() {
 
       // Resolve the Promise around the sandbox with the wrapped result to test
       // that the Promise in the sandbox works.
       wrappedResult.then(resolve);
     `);
   });
   equal(sandboxResult, "wrapped", "wrapAsync methods return Promises that work in the sandbox");
 
-  await manager.evalInSandbox(`
+  yield manager.evalInSandbox(`
     (async function sandboxTest() {
       equal(
         await driver.wrappedThis(),
         "aValue",
         "wrapAsync preserves the behavior of the this keyword",
       );
     })();
   `);
 
   manager.removeHold("testing");
 });
 
 // wrapAsync cloning options
-add_task(async function() {
+add_task(function* () {
   const manager = new SandboxManager();
   manager.addHold("testing");
 
   // clonedArgument stores the argument passed to cloned(), which we use to test
   // that arguments from within the sandbox are cloned outside.
   let clonedArgument = null;
   manager.cloneIntoGlobal("driver", {
     uncloned: manager.wrapAsync(async function() {
@@ -75,17 +75,17 @@ add_task(async function() {
       return {value: "cloned"};
     }, {cloneInto: true, cloneArguments: true}),
   }, {cloneFunctions: true});
 
   // Assertion helpers
   manager.addGlobal("ok", ok);
   manager.addGlobal("deepEqual", deepEqual);
 
-  await new Promise(resolve => {
+  yield new Promise(resolve => {
     manager.addGlobal("resolve", resolve);
     manager.evalInSandbox(`
       (async function() {
         // The uncloned return value should be privileged and inaccesible.
         const uncloned = await driver.uncloned();
         ok(!("value" in uncloned), "The sandbox could not use an uncloned return value");
 
         // The cloned return value should be usable.
--- a/browser/extensions/shield-recipe-client/test/unit/xpcshell.ini
+++ b/browser/extensions/shield-recipe-client/test/unit/xpcshell.ini
@@ -1,11 +1,13 @@
 [DEFAULT]
 head = head_xpc.js
 support-files =
   mock_api/**
+  invalid_recipe_signature_api/**
   query_server.sjs
   echo_server.sjs
   utils.js
 
 [test_NormandyApi.js]
 [test_Sampling.js]
 [test_SandboxManager.js]
+[test_Utils.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY
@@ -0,0 +1,190 @@
+ajv@5.2.1 MIT
+The MIT License (MIT)
+
+Copyright (c) 2015 Evgeny Poberezkin
+
+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.
+
+
+
+fast-deep-equal@1.0.0 MIT
+MIT License
+
+Copyright (c) 2017 Evgeny Poberezkin
+
+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.
+
+
+json-stable-stringify@1.0.1 MIT
+This software is released under the MIT license:
+
+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.
+
+
+co@4.6.0 MIT
+(The MIT License)
+
+Copyright (c) 2014 TJ Holowaychuk &lt;tj@vision-media.ca&gt;
+
+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.
+
+
+mozjexl@1.1.5 MIT
+Copyright for portions of mozJexl are held by TechnologyAdvice, 2015 as part of Jexl.
+All other copyright for mozJexl are held by the Mozilla Foundation, 2017.
+
+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.
+
+url@0.11.0 MIT
+The MIT License (MIT)
+
+Copyright Joyent, Inc. and other Node contributors.
+
+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.
+
+
+punycode@1.4.1 MIT
+
+webpack@3.1.0 MIT
+Copyright JS Foundation and other contributors
+
+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.
+
+
+querystring-es3@0.2.1 MIT
+
+json-schema-traverse@0.3.1 MIT
+MIT License
+
+Copyright (c) 2017 Evgeny Poberezkin
+
+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.
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/vendor/ajv.js
@@ -0,0 +1,1 @@
+/* eslint-disable */this.ajv=function(e){function r(t){if(a[t])return a[t].exports;var s=a[t]={i:t,l:!1,exports:{}};return e[t].call(s.exports,s,s.exports,r),s.l=!0,s.exports}var a={};return r.m=e,r.c=a,r.d=function(e,a,t){r.o(e,a)||Object.defineProperty(e,a,{configurable:!1,enumerable:!0,get:t})},r.n=function(e){var a=e&&e.__esModule?function(){return e['default']}:function(){return e};return r.d(a,'a',a),a},r.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r.p='',r(r.s=13)}([function(e,r,a){'use strict';function s(e,r,a){var t=a?' !== ':' === ',s=a?' || ':' && ',o=a?'!':'',i=a?'':'!';return'null'===e?r+t+'null':'array'===e?o+'Array.isArray('+r+')':'object'===e?'('+o+r+s+'typeof '+r+t+'"object"'+s+i+'Array.isArray('+r+'))':'integer'===e?'(typeof '+r+t+'"number"'+s+i+'('+r+' % 1)'+s+r+t+r+')':'typeof '+r+t+'"'+e+'"'}function o(e){for(var r={},a=0;a<e.length;a++)r[e[a]]=!0;return r}function t(e){return'number'==typeof e?'['+e+']':c.test(e)?'.'+e:'[\''+i(e)+'\']'}function i(e){return e.replace(m,'\\$&').replace(/\n/g,'\\n').replace(/\r/g,'\\r').replace(/\f/g,'\\f').replace(/\t/g,'\\t')}function l(e){return'\''+i(e)+'\''}function n(e,r){return'""'==e?r:(e+' + '+r).replace(/' \+ '/g,'')}function h(e){return e.replace(/~/g,'~0').replace(/\//g,'~1')}function d(e){return e.replace(/~1/g,'/').replace(/~0/g,'~')}e.exports={copy:function(e,r){for(var a in r=r||{},e)r[a]=e[a];return r},checkDataType:s,checkDataTypes:function(e,r){switch(e.length){case 1:return s(e[0],r,!0);default:var a='',i=o(e);for(var l in i.array&&i.object&&(a=i.null?'(':'(!'+r+' || ',a+='typeof '+r+' !== "object")',delete i.null,delete i.array,delete i.object),i.number&&delete i.integer,i)a+=(a?' && ':'')+s(l,r,!0);return a;}},coerceToTypes:function(e,r){if(Array.isArray(r)){for(var a,t=[],s=0;s<r.length;s++)a=r[s],p[a]?t[t.length]=a:'array'===e&&'array'===a&&(t[t.length]=a);if(t.length)return t}else{if(p[r])return[r];if('array'===e&&'array'===r)return['array']}},toHash:o,getProperty:t,escapeQuotes:i,equal:a(2),ucs2length:a(23),varOccurences:function(e,r){r+='[^0-9]';var a=e.match(new RegExp(r,'g'));return a?a.length:0},varReplace:function(e,r,a){return r+='([^0-9])',a=a.replace(/\$/g,'$$$$'),e.replace(new RegExp(r,'g'),a+'$1')},cleanUpCode:function(e){return e.replace(u,'').replace(f,'').replace(v,'if (!($1))')},finalCleanUpCode:function(e,r){var a=e.match(y);return a&&2==a.length&&(e=r?e.replace(P,'').replace(w,S):e.replace(g,'').replace(E,b)),a=e.match(x),a&&3===a.length?e.replace(k,''):e},schemaHasRules:function(e,r){if('boolean'==typeof e)return!e;for(var a in e)if(r[a])return!0},schemaHasRulesExcept:function(e,r,a){if('boolean'==typeof e)return!e&&'not'!=a;for(var t in e)if(t!=a&&r[t])return!0},toQuotedString:l,getPathExpr:function(e,r,a,t){var s=a?'\'/\' + '+r+(t?'':'.replace(/~/g, \'~0\').replace(/\\//g, \'~1\')'):t?'\'[\' + '+r+' + \']\'':'\'[\\\'\' + '+r+' + \'\\\']\'';return n(e,s)},getPath:function(e,r,a){var s=a?l('/'+h(r)):l(t(r));return n(e,s)},getData:function(e,r,a){var s,o,l,n;if(''===e)return'rootData';if('/'==e[0]){if(!$.test(e))throw new Error('Invalid JSON-pointer: '+e);o=e,l='rootData'}else{if(n=e.match(_),!n)throw new Error('Invalid JSON-pointer: '+e);if(s=+n[1],o=n[2],'#'==o){if(s>=r)throw new Error('Cannot access property/index '+s+' levels up, current level is '+r);return a[r-s]}if(s>r)throw new Error('Cannot access data '+s+' levels up, current level is '+r);if(l='data'+(r-s||''),!o)return l}for(var h,p=l,c=o.split('/'),m=0;m<c.length;m++)h=c[m],h&&(l+=t(d(h)),p+=' && '+l);return p},unescapeFragment:function(e){return d(decodeURIComponent(e))},unescapeJsonPointer:d,escapeFragment:function(e){return encodeURIComponent(h(e))},escapeJsonPointer:h};var p=o(['string','number','integer','boolean','null']),c=/^[a-z$_][a-z$_0-9]*$/i,m=/'|\\/g,u=/else\s*{\s*}/g,f=/if\s*\([^)]+\)\s*\{\s*\}(?!\s*else)/g,v=/if\s*\(([^)]+)\)\s*\{\s*\}\s*else(?!\s*if)/g,y=/[^v.]errors/g,g=/var errors = 0;|var vErrors = null;|validate.errors = vErrors;/g,P=/var errors = 0;|var vErrors = null;/g,E='return errors === 0;',b='validate.errors = null; return true;',w=/if \(errors === 0\) return data;\s*else throw new ValidationError\(vErrors\);/,S='return data;',x=/[^A-Za-z_$]rootData[^A-Za-z0-9_$]/g,k=/if \(rootData === undefined\) rootData = data;/,$=/^\/(?:[^~]|~0|~1)*$/,_=/^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/},function(e,r,a){'use strict';function t(e,r,a){var o=this._refs[a];if('string'==typeof o)if(this._refs[o])o=this._refs[o];else return t.call(this,e,r,o);if(o=o||this._schemas[a],o instanceof y)return l(o.schema,this._opts.inlineRefs)?o.schema:o.validate||this._compile(o);var i,n,h,d=s.call(this,r,a);return d&&(i=d.schema,r=d.root,h=d.baseId),i instanceof y?n=i.validate||e.call(this,i.schema,r,void 0,h):void 0!==i&&(n=l(i,this._opts.inlineRefs)?i:e.call(this,i,r,void 0,h)),n}function s(e,r){var a=u.parse(r,!1,!0),t=c(a),s=d(this._getId(e.schema));if(t!==s){var l=m(t),n=this._refs[l];if('string'==typeof n)return o.call(this,e,n,a);if(n instanceof y)n.validate||this._compile(n),e=n;else if(n=this._schemas[l],n instanceof y){if(n.validate||this._compile(n),l==m(r))return{schema:n,root:e,baseId:s};e=n}else return;if(!e.schema)return;s=d(this._getId(e.schema))}return i.call(this,a,s,e.schema,e)}function o(e,r,a){var t=s.call(this,e,r);if(t){var o=t.schema,l=t.baseId;e=t.root;var n=this._getId(o);return n&&(l=p(l,n)),i.call(this,a,l,o,e)}}function i(e,r,a,t){if(e.hash=e.hash||'','#/'==e.hash.slice(0,2)){for(var o,l=e.hash.split('/'),n=1;n<l.length;n++)if(o=l[n],o){if(o=v.unescapeFragment(o),a=a[o],void 0===a)break;var i;if(!P[o]&&(i=this._getId(a),i&&(r=p(r,i)),a.$ref)){var h=p(r,a.$ref),d=s.call(this,t,h);d&&(a=d.schema,t=d.root,r=d.baseId)}}if(void 0!==a&&a!==t.schema)return{schema:a,root:t,baseId:r}}}function l(e,r){if(!1===r)return!1;return void 0===r||!0===r?n(e):r?h(e)<=r:void 0}function n(e){var r;if(Array.isArray(e)){for(var a=0;a<e.length;a++)if(r=e[a],'object'==typeof r&&!n(r))return!1;}else for(var t in e){if('$ref'==t)return!1;if(r=e[t],'object'==typeof r&&!n(r))return!1}return!0}function h(e){var r,a=0;if(Array.isArray(e)){for(var t=0;t<e.length;t++)if(r=e[t],'object'==typeof r&&(a+=h(r)),a==Infinity)return Infinity;}else for(var s in e){if('$ref'==s)return Infinity;if(E[s])a++;else if(r=e[s],'object'==typeof r&&(a+=h(r)+1),a==Infinity)return Infinity}return a}function d(e,r){!1!==r&&(e=m(e));var a=u.parse(e,!1,!0);return c(a)}function c(e){var r=e.protocol||'//'==e.href.slice(0,2)?'//':'';return(e.protocol||'')+r+(e.host||'')+(e.path||'')+'#'}function m(e){return e?e.replace(b,''):''}function p(e,r){return r=m(r),u.resolve(e,r)}var u=a(15),f=a(2),v=a(0),y=a(4),g=a(24);e.exports=t,t.normalizeId=m,t.fullPath=d,t.url=p,t.ids=function(e){var r=m(this._getId(e)),a={"":r},t={"":d(r,!1)},s={},o=this;return g(e,{allKeys:!0},function(e,r,i,l,n,h,d){if(''!==r){var p=o._getId(e),c=a[l],y=t[l]+'/'+n;if(void 0!==d&&(y+='/'+('number'==typeof d?d:v.escapeFragment(d))),'string'==typeof p){p=c=m(c?u.resolve(c,p):p);var g=o._refs[p];if('string'==typeof g&&(g=o._refs[g]),g&&g.schema){if(!f(e,g.schema))throw new Error('id "'+p+'" resolves to more than one schema');}else if(p!=m(y))if('#'==p[0]){if(s[p]&&!f(e,s[p]))throw new Error('id "'+p+'" resolves to more than one schema');s[p]=e}else o._refs[p]=y}a[r]=c,t[r]=y}}),s},t.inlineRef=l,t.schema=s;var P=v.toHash(['properties','patternProperties','enum','dependencies','definitions']),E=v.toHash(['type','format','pattern','maxLength','minLength','maxProperties','minProperties','maxItems','minItems','maximum','minimum','uniqueItems','multipleOf','required','enum']),b=/#\/?$/},function(e){'use strict';e.exports=function e(r,t){if(r===t)return!0;var s,o=Array.isArray(r),i=Array.isArray(t);if(o&&i){if(r.length!=t.length)return!1;for(s=0;s<r.length;s++)if(!e(r[s],t[s]))return!1;return!0}if(o!=i)return!1;if(r&&t&&'object'==typeof r&&'object'==typeof t){var l=Object.keys(r);if(l.length!==Object.keys(t).length)return!1;var n=r instanceof Date,h=t instanceof Date;if(n&&h)return r.getTime()==t.getTime();if(n!=h)return!1;var d=r instanceof RegExp,p=t instanceof RegExp;if(d&&p)return r.toString()==t.toString();if(d!=p)return!1;for(s=0;s<l.length;s++)if(!Object.prototype.hasOwnProperty.call(t,l[s]))return!1;for(s=0;s<l.length;s++)if(!e(r[l[s]],t[l[s]]))return!1;return!0}return!1}},function(e,r,a){'use strict';function t(e,r,a){this.message=a||t.message(e,r),this.missingRef=o.url(e,r),this.missingSchema=o.normalizeId(o.fullPath(this.missingRef))}function s(e){return e.prototype=Object.create(Error.prototype),e.prototype.constructor=e,e}var o=a(1);e.exports={Validation:s(function(e){this.message='validation failed',this.errors=e,this.ajv=this.validation=!0}),MissingRef:s(t)},t.message=function(e,r){return'can\'t resolve reference '+r+' from id '+e}},function(e,r,a){'use strict';var t=a(0);e.exports=function(e){t.copy(e,this)}},function(e,r,a){var t='undefined'==typeof JSON?a(25):JSON;e.exports=function(e,r){r||(r={}),'function'==typeof r&&(r={cmp:r});var a=r.space||'';'number'==typeof a&&(a=Array(a+1).join(' '));var i=!('boolean'!=typeof r.cycles)&&r.cycles,l=r.replacer||function(e,r){return r},n=r.cmp&&function(e){return function(r){return function(t,s){var o={key:t,value:r[t]},i={key:s,value:r[s]};return e(o,i)}}}(r.cmp),h=[];return function e(r,d,p,c){var m=a?'\n'+Array(c+1).join(a):'',u=a?': ':':';if(p&&p.toJSON&&'function'==typeof p.toJSON&&(p=p.toJSON()),p=l.call(r,d,p),void 0!==p){if('object'!=typeof p||null===p)return t.stringify(p);if(s(p)){for(var f,v=[],y=0;y<p.length;y++)f=e(p,y,p[y],c+1)||t.stringify(null),v.push(m+a+f);return'['+v.join(',')+m+']'}if(-1!==h.indexOf(p)){if(i)return t.stringify('__cycle__');throw new TypeError('Converting circular structure to JSON')}else h.push(p);for(var g=o(p).sort(n&&n(p)),v=[],y=0;y<g.length;y++){var d=g[y],P=e(p,d,p[d],c+1);if(P){var E=t.stringify(d)+u+P;v.push(m+a+E)}}return h.splice(h.indexOf(p),1),'{'+v.join(',')+m+'}'}}({"":e},'',e,0)};var s=Array.isArray||function(e){return'[object Array]'==={}.toString.call(e)},o=Object.keys||function(e){var r=Object.prototype.hasOwnProperty||function(){return!0},a=[];for(var t in e)r.call(e,t)&&a.push(t);return a}},function(e){'use strict';e.exports=function(e,r){function a(e){for(var r=e.rules,a=0;a<r.length;a++)if(t(r[a]))return!0}function t(r){return void 0!==e.schema[r.keyword]||r.implements&&s(r)}function s(r){for(var a=r.implements,t=0;t<a.length;t++)if(void 0!==e.schema[a[t]])return!0}var o='',i=!0===e.schema.$async,l=e.util.schemaHasRulesExcept(e.schema,e.RULES.all,'$ref'),n=e.self._getId(e.schema);if(e.isTop){if(i){e.async=!0;var h='es7'==e.opts.async;e.yieldAwait=h?'await':'yield'}o+=' var validate = ',i?h?o+=' (async function ':('*'!=e.opts.async&&(o+='co.wrap'),o+='(function* '):o+=' (function ',o+=' (data, dataPath, parentData, parentDataProperty, rootData) { \'use strict\'; ',n&&(e.opts.sourceCode||e.opts.processCode)&&(o+=' /*# sourceURL='+n+' */ ')}if('boolean'==typeof e.schema||!(l||e.schema.$ref)){var d,r='false schema',p=e.level,c=e.dataLevel,m=e.schema[r],u=e.schemaPath+e.util.getProperty(r),f=e.errSchemaPath+'/'+r,v=!e.opts.allErrors,y='data'+(c||''),g='valid'+p;if(!1===e.schema){e.isTop?v=!0:o+=' var '+g+' = false; ';var P=P||[];P.push(o),o='',!1===e.createErrors?o+=' {} ':(o+=' { keyword: \''+(d||'false schema')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(f)+' , params: {} ',!1!==e.opts.messages&&(o+=' , message: \'boolean schema is false\' '),e.opts.verbose&&(o+=' , schema: false , parentSchema: validate.schema'+e.schemaPath+' , data: '+y+' '),o+=' } ');var E=o;o=P.pop(),o+=!e.compositeRule&&v?e.async?' throw new ValidationError(['+E+']); ':' validate.errors = ['+E+']; return false; ':' var err = '+E+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '}else o+=e.isTop?i?' return data; ':' validate.errors = null; return true; ':' var '+g+' = true; ';return e.isTop&&(o+=' }); return validate; '),o}if(e.isTop){var b=e.isTop,p=e.level=0,c=e.dataLevel=0,y='data';e.rootId=e.resolve.fullPath(e.self._getId(e.root.schema)),e.baseId=e.baseId||e.rootId,delete e.isTop,e.dataPathArr=[void 0],o+=' var vErrors = null; ',o+=' var errors = 0;     ',o+=' if (rootData === undefined) rootData = data; '}else{var p=e.level,c=e.dataLevel,y='data'+(c||'');if(n&&(e.baseId=e.resolve.url(e.baseId,n)),i&&!e.async)throw new Error('async schema in sync schema');o+=' var errs_'+p+' = errors;'}var d,g='valid'+p,v=!e.opts.allErrors,w='',S='',x=e.schema.type,k=Array.isArray(x);if(k&&1==x.length&&(x=x[0],k=!1),e.schema.$ref&&l)if('fail'==e.opts.extendRefs)throw new Error('$ref: validation keywords used in schema at path "'+e.errSchemaPath+'" (see option extendRefs)');else!0!==e.opts.extendRefs&&(l=!1,console.warn('$ref: keywords ignored in schema at path "'+e.errSchemaPath+'"'));if(x){if(e.opts.coerceTypes)var $=e.util.coerceToTypes(e.opts.coerceTypes,x);var _=e.RULES.types[x];if($||k||!0===_||_&&!a(_)){var u=e.schemaPath+'.type',f=e.errSchemaPath+'/type',u=e.schemaPath+'.type',f=e.errSchemaPath+'/type',R=k?'checkDataTypes':'checkDataType';if(o+=' if ('+e.util[R](x,y,!0)+') { ',$){var j='dataType'+p,I='coerced'+p;o+=' var '+j+' = typeof '+y+'; ','array'==e.opts.coerceTypes&&(o+=' if ('+j+' == \'object\' && Array.isArray('+y+')) '+j+' = \'array\'; '),o+=' var '+I+' = undefined; ';var O='',A=$;if(A)for(var L,Q=-1,q=A.length-1;Q<q;)L=A[Q+=1],Q&&(o+=' if ('+I+' === undefined) { ',O+='}'),'array'==e.opts.coerceTypes&&'array'!=L&&(o+=' if ('+j+' == \'array\' && '+y+'.length == 1) { '+I+' = '+y+' = '+y+'[0]; '+j+' = typeof '+y+';  } '),'string'==L?o+=' if ('+j+' == \'number\' || '+j+' == \'boolean\') '+I+' = \'\' + '+y+'; else if ('+y+' === null) '+I+' = \'\'; ':'number'==L||'integer'==L?(o+=' if ('+j+' == \'boolean\' || '+y+' === null || ('+j+' == \'string\' && '+y+' && '+y+' == +'+y+' ','integer'==L&&(o+=' && !('+y+' % 1)'),o+=')) '+I+' = +'+y+'; '):'boolean'==L?o+=' if ('+y+' === \'false\' || '+y+' === 0 || '+y+' === null) '+I+' = false; else if ('+y+' === \'true\' || '+y+' === 1) '+I+' = true; ':'null'==L?o+=' if ('+y+' === \'\' || '+y+' === 0 || '+y+' === false) '+I+' = null; ':'array'==e.opts.coerceTypes&&'array'==L&&(o+=' if ('+j+' == \'string\' || '+j+' == \'number\' || '+j+' == \'boolean\' || '+y+' == null) '+I+' = ['+y+']; ');o+=' '+O+' if ('+I+' === undefined) {   ';var P=P||[];P.push(o),o='',!1===e.createErrors?o+=' {} ':(o+=' { keyword: \''+(d||'type')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(f)+' , params: { type: \'',o+=k?''+x.join(','):''+x,o+='\' } ',!1!==e.opts.messages&&(o+=' , message: \'should be ',o+=k?''+x.join(','):''+x,o+='\' '),e.opts.verbose&&(o+=' , schema: validate.schema'+u+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+y+' '),o+=' } ');var E=o;o=P.pop(),o+=!e.compositeRule&&v?e.async?' throw new ValidationError(['+E+']); ':' validate.errors = ['+E+']; return false; ':' var err = '+E+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',o+=' } else {  ';var D=c?'data'+(c-1||''):'parentData',V=c?e.dataPathArr[c]:'parentDataProperty';o+=' '+y+' = '+I+'; ',c||(o+='if ('+D+' !== undefined)'),o+=' '+D+'['+V+'] = '+I+'; } '}else{var P=P||[];P.push(o),o='',!1===e.createErrors?o+=' {} ':(o+=' { keyword: \''+(d||'type')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(f)+' , params: { type: \'',o+=k?''+x.join(','):''+x,o+='\' } ',!1!==e.opts.messages&&(o+=' , message: \'should be ',o+=k?''+x.join(','):''+x,o+='\' '),e.opts.verbose&&(o+=' , schema: validate.schema'+u+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+y+' '),o+=' } ');var E=o;o=P.pop(),o+=!e.compositeRule&&v?e.async?' throw new ValidationError(['+E+']); ':' validate.errors = ['+E+']; return false; ':' var err = '+E+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '}o+=' } '}}if(e.schema.$ref&&!l)o+=' '+e.RULES.all.$ref.code(e,'$ref')+' ',v&&(o+=' } if (errors === ',o+=b?'0':'errs_'+p,o+=') { ',S+='}');else{e.opts.v5&&e.schema.patternGroups&&console.warn('keyword "patternGroups" is deprecated and disabled. Use option patternGroups: true to enable.');var N=e.RULES;if(N)for(var _,U=-1,C=N.length-1;U<C;)if(_=N[U+=1],a(_)){if(_.type&&(o+=' if ('+e.util.checkDataType(_.type,y)+') { '),e.opts.useDefaults&&!e.compositeRule)if('object'==_.type&&e.schema.properties){var m=e.schema.properties,T=Object.keys(m),M=T;if(M)for(var K,H=-1,F=M.length-1;H<F;){K=M[H+=1];var G=m[K];if(void 0!==G.default){var J=y+e.util.getProperty(K);o+='  if ('+J+' === undefined) '+J+' = ',o+='shared'==e.opts.useDefaults?' '+e.useDefault(G.default)+' ':' '+JSON.stringify(G.default)+' ',o+='; '}}}else if('array'==_.type&&Array.isArray(e.schema.items)){var z=e.schema.items;if(z)for(var G,Q=-1,B=z.length-1;Q<B;)if(G=z[Q+=1],void 0!==G.default){var J=y+'['+Q+']';o+='  if ('+J+' === undefined) '+J+' = ',o+='shared'==e.opts.useDefaults?' '+e.useDefault(G.default)+' ':' '+JSON.stringify(G.default)+' ',o+='; '}}var Y=_.rules;if(Y)for(var X,Z=-1,W=Y.length-1;Z<W;)if(X=Y[Z+=1],t(X)){var ee=X.code(e,X.keyword,_.type);ee&&(o+=' '+ee+' ',v&&(w+='}'))}if(v&&(o+=' '+w+' ',w=''),_.type&&(o+=' } ',x&&x===_.type&&!$)){o+=' else { ';var u=e.schemaPath+'.type',f=e.errSchemaPath+'/type',P=P||[];P.push(o),o='',!1===e.createErrors?o+=' {} ':(o+=' { keyword: \''+(d||'type')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(f)+' , params: { type: \'',o+=k?''+x.join(','):''+x,o+='\' } ',!1!==e.opts.messages&&(o+=' , message: \'should be ',o+=k?''+x.join(','):''+x,o+='\' '),e.opts.verbose&&(o+=' , schema: validate.schema'+u+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+y+' '),o+=' } ');var E=o;o=P.pop(),o+=!e.compositeRule&&v?e.async?' throw new ValidationError(['+E+']); ':' validate.errors = ['+E+']; return false; ':' var err = '+E+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',o+=' } '}v&&(o+=' if (errors === ',o+=b?'0':'errs_'+p,o+=') { ',S+='}')}}return v&&(o+=' '+S+' '),b?(i?(o+=' if (errors === 0) return data;           ',o+=' else throw new ValidationError(vErrors); '):(o+=' validate.errors = vErrors; ',o+=' return errors === 0;       '),o+=' }); return validate;'):o+=' var '+g+' = errors === errs_'+p+';',o=e.util.cleanUpCode(o),b&&(o=e.util.finalCleanUpCode(o,i)),o}},function(e){function r(e){var r=this,t=d.call(arguments,1);return new Promise(function(s,o){function i(r){var a;try{a=e.next(r)}catch(r){return o(r)}h(a)}function n(r){var a;try{a=e.throw(r)}catch(r){return o(r)}h(a)}function h(e){if(e.done)return s(e.value);var t=a.call(r,e.value);return t&&l(t)?t.then(i,n):n(new TypeError('You may only yield a function, promise, generator, array, or object, but the following object was passed: "'+(e.value+'"')))}return'function'==typeof e&&(e=e.apply(r,t)),e&&'function'==typeof e.next?void i():s(e)})}function a(e){return e?l(e)?e:n(e)||i(e)?r.call(this,e):'function'==typeof e?t.call(this,e):Array.isArray(e)?s.call(this,e):h(e)?o.call(this,e):e:e}function t(e){var r=this;return new Promise(function(a,t){e.call(r,function(e,r){return e?t(e):void(2<arguments.length&&(r=d.call(arguments,1)),a(r))})})}function s(e){return Promise.all(e.map(a,this))}function o(e){function r(e,r){t[r]=void 0,o.push(e.then(function(e){t[r]=e}))}for(var t=new e.constructor,s=Object.keys(e),o=[],n=0;n<s.length;n++){var i=s[n],h=a.call(this,e[i]);h&&l(h)?r(h,i):t[i]=e[i]}return Promise.all(o).then(function(){return t})}function l(e){return'function'==typeof e.then}function i(e){return'function'==typeof e.next&&'function'==typeof e.throw}function n(e){var r=e.constructor;return!!r&&('GeneratorFunction'===r.name||'GeneratorFunction'===r.displayName||i(r.prototype))}function h(e){return Object==e.constructor}var d=Array.prototype.slice;e.exports=r['default']=r.co=r,r.wrap=function(e){function a(){return r.call(this,e.apply(this,arguments))}return a.__generatorFunction__=e,a}},function(e){'use strict';e.exports=function(e,r){var a,t,s=' ',o=e.level,i=e.dataLevel,l=e.schema[r],n=e.schemaPath+e.util.getProperty(r),h=e.errSchemaPath+'/'+r,d=!e.opts.allErrors,p='data'+(i||''),c=e.opts.$data&&l&&l.$data;c?(s+=' var schema'+o+' = '+e.util.getData(l.$data,i,e.dataPathArr)+'; ',t='schema'+o):t=l;var m='maximum'==r,u=m?'exclusiveMaximum':'exclusiveMinimum',f=e.schema[u],v=e.opts.$data&&f&&f.$data,y=m?'<':'>',g=m?'>':'<',a=void 0;if(v){var P=e.util.getData(f.$data,i,e.dataPathArr),E='exclusive'+o,b='exclType'+o,w='exclIsNumber'+o,S='op'+o,x='\' + '+S+' + \'';s+=' var schemaExcl'+o+' = '+P+'; ',P='schemaExcl'+o,s+=' var '+E+'; var '+b+' = typeof '+P+'; if ('+b+' != \'boolean\' && '+b+' != \'undefined\' && '+b+' != \'number\') { ';var a=u,k=k||[];k.push(s),s='',!1===e.createErrors?s+=' {} ':(s+=' { keyword: \''+(a||'_exclusiveLimit')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(h)+' , params: {} ',!1!==e.opts.messages&&(s+=' , message: \''+u+' should be boolean\' '),e.opts.verbose&&(s+=' , schema: validate.schema'+n+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+p+' '),s+=' } ');var $=s;s=k.pop(),s+=!e.compositeRule&&d?e.async?' throw new ValidationError(['+$+']); ':' validate.errors = ['+$+']; return false; ':' var err = '+$+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',s+=' } else if ( ',c&&(s+=' ('+t+' !== undefined && typeof '+t+' != \'number\') || '),s+=' '+b+' == \'number\' ? ( ('+E+' = '+t+' === undefined || '+P+' '+y+'= '+t+') ? '+p+' '+g+'= '+P+' : '+p+' '+g+' '+t+' ) : ( ('+E+' = '+P+' === true) ? '+p+' '+g+'= '+t+' : '+p+' '+g+' '+t+' ) || '+p+' !== '+p+') { var op'+o+' = '+E+' ? \''+y+'\' : \''+y+'=\';'}else{var w='number'==typeof f,x=y;if(w&&c){var S='\''+x+'\'';s+=' if ( ',c&&(s+=' ('+t+' !== undefined && typeof '+t+' != \'number\') || '),s+=' ( '+t+' === undefined || '+f+' '+y+'= '+t+' ? '+p+' '+g+'= '+f+' : '+p+' '+g+' '+t+' ) || '+p+' !== '+p+') { '}else{w&&void 0===l?(E=!0,a=u,h=e.errSchemaPath+'/'+u,t=f,g+='='):(w&&(t=Math[m?'min':'max'](f,l)),f===(!w||t)?(E=!0,a=u,h=e.errSchemaPath+'/'+u,g+='='):(E=!1,x+='='));var S='\''+x+'\'';s+=' if ( ',c&&(s+=' ('+t+' !== undefined && typeof '+t+' != \'number\') || '),s+=' '+p+' '+g+' '+t+' || '+p+' !== '+p+') { '}}a=a||r;var k=k||[];k.push(s),s='',!1===e.createErrors?s+=' {} ':(s+=' { keyword: \''+(a||'_limit')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(h)+' , params: { comparison: '+S+', limit: '+t+', exclusive: '+E+' } ',!1!==e.opts.messages&&(s+=' , message: \'should be '+x+' ',s+=c?'\' + '+t:''+t+'\''),e.opts.verbose&&(s+=' , schema:  ',s+=c?'validate.schema'+n:''+l,s+='         , parentSchema: validate.schema'+e.schemaPath+' , data: '+p+' '),s+=' } ');var $=s;return s=k.pop(),s+=!e.compositeRule&&d?e.async?' throw new ValidationError(['+$+']); ':' validate.errors = ['+$+']; return false; ':' var err = '+$+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',s+=' } ',d&&(s+=' else { '),s}},function(e){'use strict';e.exports=function(e,r){var a,t,s=' ',o=e.level,i=e.dataLevel,l=e.schema[r],n=e.schemaPath+e.util.getProperty(r),h=e.errSchemaPath+'/'+r,d=!e.opts.allErrors,p='data'+(i||''),c=e.opts.$data&&l&&l.$data;c?(s+=' var schema'+o+' = '+e.util.getData(l.$data,i,e.dataPathArr)+'; ',t='schema'+o):t=l;var m='maxItems'==r?'>':'<';s+='if ( ',c&&(s+=' ('+t+' !== undefined && typeof '+t+' != \'number\') || '),s+=' '+p+'.length '+m+' '+t+') { ';var u=u||[];u.push(s),s='',!1===e.createErrors?s+=' {} ':(s+=' { keyword: \''+(r||'_limitItems')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(h)+' , params: { limit: '+t+' } ',!1!==e.opts.messages&&(s+=' , message: \'should NOT have ',s+='maxItems'==r?'more':'less',s+=' than ',s+=c?'\' + '+t+' + \'':''+l,s+=' items\' '),e.opts.verbose&&(s+=' , schema:  ',s+=c?'validate.schema'+n:''+l,s+='         , parentSchema: validate.schema'+e.schemaPath+' , data: '+p+' '),s+=' } ');var f=s;return s=u.pop(),s+=!e.compositeRule&&d?e.async?' throw new ValidationError(['+f+']); ':' validate.errors = ['+f+']; return false; ':' var err = '+f+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',s+='} ',d&&(s+=' else { '),s}},function(e){'use strict';e.exports=function(e,r){var a,t,s=' ',o=e.level,i=e.dataLevel,l=e.schema[r],n=e.schemaPath+e.util.getProperty(r),h=e.errSchemaPath+'/'+r,d=!e.opts.allErrors,p='data'+(i||''),c=e.opts.$data&&l&&l.$data;c?(s+=' var schema'+o+' = '+e.util.getData(l.$data,i,e.dataPathArr)+'; ',t='schema'+o):t=l;var m='maxLength'==r?'>':'<';s+='if ( ',c&&(s+=' ('+t+' !== undefined && typeof '+t+' != \'number\') || '),s+=!1===e.opts.unicode?' '+p+'.length ':' ucs2length('+p+') ',s+=' '+m+' '+t+') { ';var u=u||[];u.push(s),s='',!1===e.createErrors?s+=' {} ':(s+=' { keyword: \''+(r||'_limitLength')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(h)+' , params: { limit: '+t+' } ',!1!==e.opts.messages&&(s+=' , message: \'should NOT be ',s+='maxLength'==r?'longer':'shorter',s+=' than ',s+=c?'\' + '+t+' + \'':''+l,s+=' characters\' '),e.opts.verbose&&(s+=' , schema:  ',s+=c?'validate.schema'+n:''+l,s+='         , parentSchema: validate.schema'+e.schemaPath+' , data: '+p+' '),s+=' } ');var f=s;return s=u.pop(),s+=!e.compositeRule&&d?e.async?' throw new ValidationError(['+f+']); ':' validate.errors = ['+f+']; return false; ':' var err = '+f+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',s+='} ',d&&(s+=' else { '),s}},function(e){'use strict';e.exports=function(e,r){var a,t,s=' ',o=e.level,i=e.dataLevel,l=e.schema[r],n=e.schemaPath+e.util.getProperty(r),h=e.errSchemaPath+'/'+r,d=!e.opts.allErrors,p='data'+(i||''),c=e.opts.$data&&l&&l.$data;c?(s+=' var schema'+o+' = '+e.util.getData(l.$data,i,e.dataPathArr)+'; ',t='schema'+o):t=l;var m='maxProperties'==r?'>':'<';s+='if ( ',c&&(s+=' ('+t+' !== undefined && typeof '+t+' != \'number\') || '),s+=' Object.keys('+p+').length '+m+' '+t+') { ';var u=u||[];u.push(s),s='',!1===e.createErrors?s+=' {} ':(s+=' { keyword: \''+(r||'_limitProperties')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(h)+' , params: { limit: '+t+' } ',!1!==e.opts.messages&&(s+=' , message: \'should NOT have ',s+='maxProperties'==r?'more':'less',s+=' than ',s+=c?'\' + '+t+' + \'':''+l,s+=' properties\' '),e.opts.verbose&&(s+=' , schema:  ',s+=c?'validate.schema'+n:''+l,s+='         , parentSchema: validate.schema'+e.schemaPath+' , data: '+p+' '),s+=' } ');var f=s;return s=u.pop(),s+=!e.compositeRule&&d?e.async?' throw new ValidationError(['+f+']); ':' validate.errors = ['+f+']; return false; ':' var err = '+f+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',s+='} ',d&&(s+=' else { '),s}},,function(e,r,a){'use strict';function t(e){if(!(this instanceof t))return new t(e);e=this._opts=$.copy(e)||{},this._schemas={},this._refs={},this._fragments={},this._formats=w(e.format);var r=this._schemaUriFormat=this._formats['uri-reference'];this._schemaUriFormatFunc=function(e){return r.test(e)},this._cache=e.cache||new P,this._loadingSchemas={},this._compilations=[],this.RULES=S(),this._getId=n(e),e.loopRequired=e.loopRequired||Infinity,'property'==e.errorDataPath&&(e._errorDataPathProperty=!0),e.serialize===void 0&&(e.serialize=b),this._metaOpts=v(this),e.formats&&u(this),c(this),'object'==typeof e.meta&&this.addMetaSchema(e.meta),m(this),e.patternGroups&&k(this)}function s(e){var r=e._opts.meta;return e._opts.defaultMeta='object'==typeof r?e._getId(r)||r:e.getSchema(I)?I:void 0,e._opts.defaultMeta}function o(e,r){var a=g.schema.call(e,{schema:{}},r);if(a){var t=a.schema,s=a.root,o=a.baseId,i=y.call(e,t,s,void 0,o);return e._fragments[r]=new E({ref:r,fragment:!0,schema:t,root:s,baseId:o,validate:i}),i}}function i(e,r){return r=g.normalizeId(r),e._schemas[r]||e._refs[r]||e._fragments[r]}function l(e,r,a){for(var t in r){var s=r[t];s.meta||a&&!a.test(t)||(e._cache.del(s.cacheKey),delete r[t])}}function n(e){switch(e.schemaId){case'$id':return d;case'id':return h;default:return p;}}function h(e){return e.$id&&console.warn('schema $id ignored',e.$id),e.id}function d(e){return e.id&&console.warn('schema id ignored',e.id),e.$id}function p(e){if(e.$id&&e.id&&e.$id!=e.id)throw new Error('schema $id is different from id');return e.$id||e.id}function c(e){var r;if(e._opts.$data&&(r=a(54),e.addMetaSchema(r,r.$id,!0)),!1!==e._opts.meta){var t=a(55);e._opts.$data&&(t=x(t,A)),e.addMetaSchema(t,I,!0),e._refs['http://json-schema.org/schema']=I}}function m(e){var r=e._opts.schemas;if(r)if(Array.isArray(r))e.addSchema(r);else for(var a in r)e.addSchema(r[a],a)}function u(e){for(var r in e._opts.formats){var a=e._opts.formats[r];e.addFormat(r,a)}}function f(e,r){if(e._schemas[r]||e._refs[r])throw new Error('schema with key or id "'+r+'" already exists')}function v(e){for(var r=$.copy(e._opts),a=0;a<O.length;a++)delete r[O[a]];return r}var y=a(14),g=a(1),P=a(28),E=a(4),b=a(5),w=a(29),S=a(30),x=a(49),k=a(50),$=a(0),_=a(7);e.exports=t,t.prototype.validate=function(e,r){var a;if('string'!=typeof e){var t=this._addSchema(e);a=t.validate||this._compile(t)}else if(a=this.getSchema(e),!a)throw new Error('no schema with key or ref "'+e+'"');var s=a(r);return!0===a.$async?'*'==this._opts.async?_(s):s:(this.errors=a.errors,s)},t.prototype.compile=function(e,r){var a=this._addSchema(e,void 0,r);return a.validate||this._compile(a)},t.prototype.addSchema=function(e,r,a,t){if(Array.isArray(e)){for(var s=0;s<e.length;s++)this.addSchema(e[s],void 0,a,t);return}var o=this._getId(e);if(o!==void 0&&'string'!=typeof o)throw new Error('schema id must be string');r=g.normalizeId(r||o),f(this,r),this._schemas[r]=this._addSchema(e,a,t,!0)},t.prototype.addMetaSchema=function(e,r,a){this.addSchema(e,r,a,!0)},t.prototype.validateSchema=function(e,r){var a=e.$schema;if(void 0!==a&&'string'!=typeof a)throw new Error('$schema must be a string');if(a=a||this._opts.defaultMeta||s(this),!a)return console.warn('meta-schema not available'),this.errors=null,!0;var t=this._formats.uri;this._formats.uri='function'==typeof t?this._schemaUriFormatFunc:this._schemaUriFormat;var o;try{o=this.validate(a,e)}finally{this._formats.uri=t}if(!o&&r){var i='schema is invalid: '+this.errorsText();if('log'==this._opts.validateSchema)console.error(i);else throw new Error(i)}return o},t.prototype.getSchema=function(e){var r=i(this,e);switch(typeof r){case'object':return r.validate||this._compile(r);case'string':return this.getSchema(r);case'undefined':return o(this,e);}},t.prototype.removeSchema=function(e){if(e instanceof RegExp)return l(this,this._schemas,e),void l(this,this._refs,e);switch(typeof e){case'undefined':return l(this,this._schemas),l(this,this._refs),void this._cache.clear();case'string':var r=i(this,e);return r&&this._cache.del(r.cacheKey),delete this._schemas[e],void delete this._refs[e];case'object':var a=this._opts.serialize,t=a?a(e):e;this._cache.del(t);var s=this._getId(e);s&&(s=g.normalizeId(s),delete this._schemas[s],delete this._refs[s]);}},t.prototype.addFormat=function(e,r){'string'==typeof r&&(r=new RegExp(r)),this._formats[e]=r},t.prototype.errorsText=function(r,a){if(r=r||this.errors,!r)return'No errors';a=a||{};for(var t,e=void 0===a.separator?', ':a.separator,s=void 0===a.dataVar?'data':a.dataVar,o='',l=0;l<r.length;l++)t=r[l],t&&(o+=s+t.dataPath+' '+t.message+e);return o.slice(0,-e.length)},t.prototype._addSchema=function(e,r,a,t){if('object'!=typeof e&&'boolean'!=typeof e)throw new Error('schema should be object or boolean');var s=this._opts.serialize,o=s?s(e):e,i=this._cache.get(o);if(i)return i;t=t||!1!==this._opts.addUsedSchema;var l=g.normalizeId(this._getId(e));l&&t&&f(this,l);var n,h=!1!==this._opts.validateSchema&&!r;h&&!(n=l&&l==g.normalizeId(e.$schema))&&this.validateSchema(e,!0);var d=g.ids.call(this,e),p=new E({id:l,schema:e,localRefs:d,cacheKey:o,meta:a});return'#'!=l[0]&&t&&(this._refs[l]=p),this._cache.put(o,p),h&&n&&this.validateSchema(e,!0),p},t.prototype._compile=function(e,r){function a(){var r=e.validate,t=r.apply(null,arguments);return a.errors=r.errors,t}if(e.compiling)return e.validate=a,a.schema=e.schema,a.errors=null,a.root=r?r:a,!0===e.schema.$async&&(a.$async=!0),a;e.compiling=!0;var t;e.meta&&(t=this._opts,this._opts=this._metaOpts);var s;try{s=y.call(this,e.schema,r,e.localRefs)}finally{e.compiling=!1,e.meta&&(this._opts=t)}return e.validate=s,e.refs=s.refs,e.refVal=s.refVal,e.root=s.root,s},t.prototype.compileAsync=a(51);var R=a(52);t.prototype.addKeyword=R.add,t.prototype.getKeyword=R.get,t.prototype.removeKeyword=R.remove;var j=a(3);t.ValidationError=j.Validation,t.MissingRefError=j.MissingRef,t.$dataMetaSchema=x;var I='http://json-schema.org/draft-06/schema',O=['removeAdditional','useDefaults','coerceTypes'],A=['/properties']},function(e,r,a){'use strict';function t(e,r,a,l){function b(){var e=C.validate,r=e.apply(null,arguments);return b.errors=e.errors,r}function w(e,a,s,o){var l=!a||a&&a.schema==e;if(a.schema!=r.schema)return t.call(I,e,a,s,o);var f=!0===e.$async,b=v({isTop:!0,schema:e,isRoot:l,baseId:o,root:a,schemaPath:'',errSchemaPath:'#',errorPath:'""',MissingRefError:u.MissingRef,RULES:M,validate:v,util:m,resolve:c,resolveRef:S,usePattern:_,useDefault:R,useCustomRule:j,opts:O,formats:T,self:I});b=p(A,h)+p(Q,i)+p(D,n)+p(N,d)+b,O.processCode&&(b=O.processCode(b));var w;try{var x=new Function('self','RULES','formats','root','refVal','defaults','customRules','co','equal','ucs2length','ValidationError',b);w=x(I,M,T,r,A,D,N,y,P,g,E),A[0]=w}catch(r){throw console.error('Error compiling schema, function code:',b),r}return w.schema=e,w.errors=null,w.refs=L,w.refVal=A,w.root=l?w:a,f&&(w.$async=!0),!0===O.sourceCode&&(w.source={code:b,patterns:Q,defaults:D}),w}function S(e,s,o){s=c.url(e,s);var i,l,n=L[s];if(void 0!==n)return i=A[n],l='refVal['+n+']',$(i,l);if(!o&&r.refs){var h=r.refs[s];if(void 0!==h)return i=r.refVal[h],l=x(s,i),$(i,l)}l=x(s);var d=c.call(I,w,r,s);if(d===void 0){var p=a&&a[s];p&&(d=c.inlineRef(p,O.inlineRefs)?p:t.call(I,p,r,a,e))}return void 0===d?void 0:(k(s,d),$(d,l))}function x(e,r){var a=A.length;return A[a]=r,L[e]=a,'refVal'+a}function k(e,r){var a=L[e];A[a]=r}function $(e,r){return'object'==typeof e||'boolean'==typeof e?{code:r,schema:e,inline:!0}:{code:r,$async:e&&e.$async}}function _(e){var r=q[e];return void 0===r&&(r=q[e]=Q.length,Q[r]=e),'pattern'+r}function R(e){switch(typeof e){case'boolean':case'number':return''+e;case'string':return m.toQuotedString(e);case'object':if(null===e)return'null';var r=f(e),a=V[r];return void 0===a&&(a=V[r]=D.length,D[a]=e),'default'+a;}}function j(e,r,a,t){var s=e.definition.validateSchema;if(s&&!1!==I._opts.validateSchema){var o=s(r);if(!o){var i='keyword schema is invalid: '+I.errorsText(s.errors);if('log'==I._opts.validateSchema)console.error(i);else throw new Error(i)}}var l,n=e.definition.compile,h=e.definition.inline,d=e.definition.macro;if(n)l=n.call(I,r,a,t);else if(d)l=d.call(I,r,a,t),!1!==O.validateSchema&&I.validateSchema(l,!0);else if(h)l=h.call(I,t,e.keyword,r,a);else if(l=e.definition.validate,!l)return;if(void 0===l)throw new Error('custom keyword "'+e.keyword+'"failed to compile');var p=N.length;return N[p]=l,{code:'customRule'+p,validate:l}}var I=this,O=this._opts,A=[void 0],L={},Q=[],q={},D=[],V={},N=[];r=r||{schema:e,refVal:A,refs:L};var U=s.call(this,e,r,l),C=this._compilations[U.index];if(U.compiling)return C.callValidate=b;var T=this._formats,M=this.RULES;try{var K=w(e,r,a,l);C.validate=K;var H=C.callValidate;return H&&(H.schema=K.schema,H.errors=null,H.refs=K.refs,H.refVal=K.refVal,H.root=K.root,H.$async=K.$async,O.sourceCode&&(H.source=K.source)),K}finally{o.call(this,e,r,l)}}function s(e,r,a){var t=l.call(this,e,r,a);return 0<=t?{index:t,compiling:!0}:(t=this._compilations.length,this._compilations[t]={schema:e,root:r,baseId:a},{index:t,compiling:!1})}function o(e,r,a){var t=l.call(this,e,r,a);0<=t&&this._compilations.splice(t,1)}function l(e,r,a){for(var t,s=0;s<this._compilations.length;s++)if(t=this._compilations[s],t.schema==e&&t.root==r&&t.baseId==a)return s;return-1}function i(e,r){return'var pattern'+e+' = new RegExp('+m.toQuotedString(r[e])+');'}function n(e){return'var default'+e+' = defaults['+e+'];'}function h(e,r){return r[e]===void 0?'':'var refVal'+e+' = refVal['+e+'];'}function d(e){return'var customRule'+e+' = customRules['+e+'];'}function p(e,r){if(!e.length)return'';for(var a='',t=0;t<e.length;t++)a+=r(t,e);return a}var c=a(1),m=a(0),u=a(3),f=a(5),v=a(6),y=a(7),g=m.ucs2length,P=a(2),E=u.Validation;e.exports=t},function(e,r,a){'use strict';function t(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}function s(e,r,a){if(e&&n.isObject(e)&&e instanceof t)return e;var s=new t;return s.parse(e,r,a),s}var o=a(16),n=a(19);r.parse=s,r.resolve=function(e,r){return s(e,!1,!0).resolve(r)},r.resolveObject=function(e,r){return e?s(e,!1,!0).resolveObject(r):r},r.format=function(e){return n.isString(e)&&(e=s(e)),e instanceof t?e.format():t.prototype.format.call(e)},r.Url=t;var d=/^([a-z0-9.+-]+:)/i,i=/:[0-9]*$/,l=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,h=['{','}','|','\\','^','`'].concat(['<','>','"','`',' ','\r','\n','\t']),p=['\''].concat(h),c=['%','/','?',';','#'].concat(p),m=['/','?','#'],u=/^[+a-z0-9A-Z_-]{0,63}$/,f=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,v={javascript:!0,"javascript:":!0},y={javascript:!0,"javascript:":!0},g={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},P=a(20);t.prototype.parse=function(e,r,a){if(!n.isString(e))throw new TypeError('Parameter \'url\' must be a string, not '+typeof e);var t=e.indexOf('?'),E=-1!==t&&t<e.indexOf('#')?'?':'#',b=e.split(E),w=/\\/g;b[0]=b[0].replace(w,'/'),e=b.join(E);var S=e;if(S=S.trim(),!a&&1===e.split('#').length){var x=l.exec(S);if(x)return this.path=S,this.href=S,this.pathname=x[1],x[2]?(this.search=x[2],this.query=r?P.parse(this.search.substr(1)):this.search.substr(1)):r&&(this.search='',this.query={}),this}var $=d.exec(S);if($){$=$[0];var _=$.toLowerCase();this.protocol=_,S=S.substr($.length)}if(a||$||S.match(/^\/\/[^@\/]+@[^@\/]+/)){var R='//'===S.substr(0,2);R&&!($&&y[$])&&(S=S.substr(2),this.slashes=!0)}if(!y[$]&&(R||$&&!g[$])){for(var I,O=-1,A=0;A<m.length;A++)I=S.indexOf(m[A]),-1!==I&&(-1==O||I<O)&&(O=I);var i,L;L=-1===O?S.lastIndexOf('@'):S.lastIndexOf('@',O),-1!==L&&(i=S.slice(0,L),S=S.slice(L+1),this.auth=decodeURIComponent(i)),O=-1;for(var I,A=0;A<c.length;A++)I=S.indexOf(c[A]),-1!==I&&(-1===O||I<O)&&(O=I);-1===O&&(O=S.length),this.host=S.slice(0,O),S=S.slice(O),this.parseHost(),this.hostname=this.hostname||'';var Q='['===this.hostname[0]&&']'===this.hostname[this.hostname.length-1];if(!Q)for(var q,D=this.hostname.split(/\./),A=0,V=D.length;A<V;A++)if(q=D[A],q&&!q.match(u)){for(var N='',U=0,j=q.length;U<j;U++)N+=127<q.charCodeAt(U)?'x':q[U];if(!N.match(u)){var k=D.slice(0,A),C=D.slice(A+1),T=q.match(f);T&&(k.push(T[1]),C.unshift(T[2])),C.length&&(S='/'+C.join('.')+S),this.hostname=k.join('.');break}}this.hostname=this.hostname.length>255?'':this.hostname.toLowerCase(),Q||(this.hostname=o.toASCII(this.hostname));var M=this.port?':'+this.port:'',K=this.hostname||'';this.host=K+M,this.href+=this.host,Q&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),'/'!==S[0]&&(S='/'+S))}if(!v[_])for(var h,A=0,V=p.length;A<V;A++)if(h=p[A],-1!==S.indexOf(h)){var H=encodeURIComponent(h);H===h&&(H=escape(h)),S=S.split(h).join(H)}var F=S.indexOf('#');-1!==F&&(this.hash=S.substr(F),S=S.slice(0,F));var G=S.indexOf('?');if(-1===G?r&&(this.search='',this.query={}):(this.search=S.substr(G),this.query=S.substr(G+1),r&&(this.query=P.parse(this.query)),S=S.slice(0,G)),S&&(this.pathname=S),g[_]&&this.hostname&&!this.pathname&&(this.pathname='/'),this.pathname||this.search){var M=this.pathname||'',J=this.search||'';this.path=M+J}return this.href=this.format(),this},t.prototype.format=function(){var e=this.auth||'';e&&(e=encodeURIComponent(e),e=e.replace(/%3A/i,':'),e+='@');var r=this.protocol||'',a=this.pathname||'',t=this.hash||'',s=!1,o='';this.host?s=e+this.host:this.hostname&&(s=e+(-1===this.hostname.indexOf(':')?this.hostname:'['+this.hostname+']'),this.port&&(s+=':'+this.port)),this.query&&n.isObject(this.query)&&Object.keys(this.query).length&&(o=P.stringify(this.query));var i=this.search||o&&'?'+o||'';return r&&':'!==r.substr(-1)&&(r+=':'),this.slashes||(!r||g[r])&&!1!==s?(s='//'+(s||''),a&&'/'!==a.charAt(0)&&(a='/'+a)):!s&&(s=''),t&&'#'!==t.charAt(0)&&(t='#'+t),i&&'?'!==i.charAt(0)&&(i='?'+i),a=a.replace(/[?#]/g,function(e){return encodeURIComponent(e)}),i=i.replace('#','%23'),r+s+a+i+t},t.prototype.resolve=function(e){return this.resolveObject(s(e,!1,!0)).format()},t.prototype.resolveObject=function(e){if(n.isString(e)){var r=new t;r.parse(e,!1,!0),e=r}for(var a,o=new t,l=Object.keys(this),h=0;h<l.length;h++)a=l[h],o[a]=this[a];if(o.hash=e.hash,''===e.href)return o.href=o.format(),o;if(e.slashes&&!e.protocol){for(var d,c=Object.keys(e),m=0;m<c.length;m++)d=c[m],'protocol'!==d&&(o[d]=e[d]);return g[o.protocol]&&o.hostname&&!o.pathname&&(o.path=o.pathname='/'),o.href=o.format(),o}if(e.protocol&&e.protocol!==o.protocol){if(!g[e.protocol]){for(var u,f=Object.keys(e),P=0;P<f.length;P++)u=f[P],o[u]=e[u];return o.href=o.format(),o}if(o.protocol=e.protocol,!e.host&&!y[e.protocol]){for(var v=(e.pathname||'').split('/');v.length&&!(e.host=v.shift()););e.host||(e.host=''),e.hostname||(e.hostname=''),''!==v[0]&&v.unshift(''),2>v.length&&v.unshift(''),o.pathname=v.join('/')}else o.pathname=e.pathname;if(o.search=e.search,o.query=e.query,o.host=e.host||'',o.auth=e.auth,o.hostname=e.hostname||e.host,o.port=e.port,o.pathname||o.search){var E=o.pathname||'',p=o.search||'';o.path=E+p}return o.slashes=o.slashes||e.slashes,o.href=o.format(),o}var s=o.pathname&&'/'===o.pathname.charAt(0),b=e.host||e.pathname&&'/'===e.pathname.charAt(0),w=b||s||o.host&&e.pathname,S=w,x=o.pathname&&o.pathname.split('/')||[],v=e.pathname&&e.pathname.split('/')||[],k=o.protocol&&!g[o.protocol];if(k&&(o.hostname='',o.port=null,o.host&&(''===x[0]?x[0]=o.host:x.unshift(o.host)),o.host='',e.protocol&&(e.hostname=null,e.port=null,e.host&&(''===v[0]?v[0]=e.host:v.unshift(e.host)),e.host=null),w=w&&(''===v[0]||''===x[0])),b)o.host=e.host||''===e.host?e.host:o.host,o.hostname=e.hostname||''===e.hostname?e.hostname:o.hostname,o.search=e.search,o.query=e.query,x=v;else if(v.length)x||(x=[]),x.pop(),x=x.concat(v),o.search=e.search,o.query=e.query;else if(!n.isNullOrUndefined(e.search)){if(k){o.hostname=o.host=x.shift();var $=o.host&&0<o.host.indexOf('@')&&o.host.split('@');$&&(o.auth=$.shift(),o.host=o.hostname=$.shift())}return o.search=e.search,o.query=e.query,n.isNull(o.pathname)&&n.isNull(o.search)||(o.path=(o.pathname?o.pathname:'')+(o.search?o.search:'')),o.href=o.format(),o}if(!x.length)return o.pathname=null,o.path=o.search?'/'+o.search:null,o.href=o.format(),o;for(var _=x.slice(-1)[0],R=(o.host||e.host||1<x.length)&&('.'===_||'..'===_)||''===_,j=0,I=x.length;0<=I;I--)_=x[I],'.'===_?x.splice(I,1):'..'===_?(x.splice(I,1),j++):j&&(x.splice(I,1),j--);if(!w&&!S)for(;j--;j)x.unshift('..');w&&''!==x[0]&&(!x[0]||'/'!==x[0].charAt(0))&&x.unshift(''),R&&'/'!==x.join('/').substr(-1)&&x.push('');var i=''===x[0]||x[0]&&'/'===x[0].charAt(0);if(k){o.hostname=o.host=i?'':x.length?x.shift():'';var $=o.host&&0<o.host.indexOf('@')&&o.host.split('@');$&&(o.auth=$.shift(),o.host=o.hostname=$.shift())}return w=w||o.host&&x.length,w&&!i&&x.unshift(''),x.length?o.pathname=x.join('/'):(o.pathname=null,o.path=null),n.isNull(o.pathname)&&n.isNull(o.search)||(o.path=(o.pathname?o.pathname:'')+(o.search?o.search:'')),o.auth=e.auth||o.auth,o.slashes=o.slashes||e.slashes,o.href=o.format(),o},t.prototype.parseHost=function(){var e=this.host,r=i.exec(e);r&&(r=r[0],':'!==r&&(this.port=r.substr(1)),e=e.substr(0,e.length-r.length)),e&&(this.hostname=e)}},function(e,r,a){(function(e,t){var s;(function(o){function l(e){throw new RangeError(A[e])}function i(e,r){for(var a=e.length,t=[];a--;)t[a]=r(e[a]);return t}function n(e,r){var a=e.split('@'),t='';1<a.length&&(t=a[0]+'@',e=a[1]),e=e.replace(O,'.');var s=e.split('.'),o=i(s,r).join('.');return t+o}function h(e){for(var r,a,t=[],s=0,o=e.length;s<o;)r=e.charCodeAt(s++),55296<=r&&56319>=r&&s<o?(a=e.charCodeAt(s++),56320==(64512&a)?t.push(((1023&r)<<10)+(1023&a)+65536):(t.push(r),s--)):t.push(r);return t}function d(e){return i(e,function(e){var r='';return 65535<e&&(e-=65536,r+=D(55296|1023&e>>>10),e=56320|1023&e),r+=D(e),r}).join('')}function p(e){return 10>e-48?e-22:26>e-65?e-65:26>e-97?e-97:b}function c(e,r){return e+22+75*(26>e)-((0!=r)<<5)}function m(e,r,a){var t=0;for(e=a?Q(e/$):e>>1,e+=Q(e/r);e>L*x>>1;t+=b)e=Q(e/L);return Q(t+(L+1)*e/(e+w))}function u(e){var r,a,s,o,h,c,u,f,v,t,y=[],g=e.length,P=0,i=R,n=_;for(a=e.lastIndexOf(I),0>a&&(a=0),s=0;s<a;++s)128<=e.charCodeAt(s)&&l('not-basic'),y.push(e.charCodeAt(s));for(o=0<a?a+1:0;o<g;){for(h=P,c=1,u=b;;u+=b){if(o>=g&&l('invalid-input'),f=p(e.charCodeAt(o++)),(f>=b||f>Q((E-P)/c))&&l('overflow'),P+=f*c,v=u<=n?S:u>=n+x?x:u-n,f<v)break;t=b-v,c>Q(E/t)&&l('overflow'),c*=t}r=y.length+1,n=m(P-h,r,0==h),Q(P/r)>E-i&&l('overflow'),i+=Q(P/r),P%=r,y.splice(P++,0,i)}return d(y)}function f(e){var r,a,s,o,i,n,d,p,u,f,t,v,y,g,P,w=[];for(e=h(e),v=e.length,r=R,a=0,i=_,n=0;n<v;++n)t=e[n],128>t&&w.push(D(t));for(s=o=w.length,o&&w.push(I);s<v;){for(d=E,n=0;n<v;++n)t=e[n],t>=r&&t<d&&(d=t);for(y=s+1,d-r>Q((E-a)/y)&&l('overflow'),a+=(d-r)*y,r=d,n=0;n<v;++n)if(t=e[n],t<r&&++a>E&&l('overflow'),t==r){for(p=a,u=b;;u+=b){if(f=u<=i?S:u>=i+x?x:u-i,p<f)break;P=p-f,g=b-f,w.push(D(c(f+P%g,0))),p=Q(P/g)}w.push(D(c(p,0))),i=m(a,y,s==o),a=0,++s}++a,++r}return w.join('')}var v='object'==typeof r&&r&&!r.nodeType&&r,y='object'==typeof e&&e&&!e.nodeType&&e,g='object'==typeof t&&t;(g.global===g||g.window===g||g.self===g)&&(o=g);var P,E=2147483647,b=36,S=1,x=26,w=38,$=700,_=72,R=128,I='-',k=/^xn--/,j=/[^\x20-\x7E]/,O=/[\x2E\u3002\uFF0E\uFF61]/g,A={overflow:'Overflow: input needs wider integers to process',"not-basic":'Illegal input >= 0x80 (not a basic code point)',"invalid-input":'Invalid input'},L=b-S,Q=Math.floor,D=String.fromCharCode;P={version:'1.4.1',ucs2:{decode:h,encode:d},decode:u,encode:f,toASCII:function(e){return n(e,function(e){return j.test(e)?'xn--'+f(e):e})},toUnicode:function(e){return n(e,function(e){return k.test(e)?u(e.slice(4).toLowerCase()):e})}};s=function(){return P}.call(r,a,r,e),!(void 0!==s&&(e.exports=s))})(this)}).call(r,a(17)(e),a(18))},function(e){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],!e.children&&(e.children=[]),Object.defineProperty(e,'loaded',{enumerable:!0,get:function(){return e.l}}),Object.defineProperty(e,'id',{enumerable:!0,get:function(){return e.i}}),e.webpackPolyfill=1),e}},function(e){var r=function(){return this}();try{r=r||Function('return this')()||(1,eval)('this')}catch(a){'object'==typeof window&&(r=window)}e.exports=r},function(e){'use strict';e.exports={isString:function(e){return'string'==typeof e},isObject:function(e){return'object'==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},function(e,r,a){'use strict';r.decode=r.parse=a(21),r.encode=r.stringify=a(22)},function(e){'use strict';function r(e,r){return Object.prototype.hasOwnProperty.call(e,r)}e.exports=function(e,t,s,o){t=t||'&',s=s||'=';var l={};if('string'!=typeof e||0===e.length)return l;var n=/\+/g;e=e.split(t);var h=1e3;o&&'number'==typeof o.maxKeys&&(h=o.maxKeys);var d=e.length;0<h&&d>h&&(d=h);for(var p=0;p<d;++p){var i,c,m,u,f=e[p].replace(n,'%20'),v=f.indexOf(s);0<=v?(i=f.substr(0,v),c=f.substr(v+1)):(i=f,c=''),m=decodeURIComponent(i),u=decodeURIComponent(c),r(l,m)?a(l[m])?l[m].push(u):l[m]=[l[m],u]:l[m]=u}return l};var a=Array.isArray||function(e){return'[object Array]'===Object.prototype.toString.call(e)}},function(e){'use strict';function r(e,r){if(e.map)return e.map(r);for(var a=[],t=0;t<e.length;t++)a.push(r(e[t],t));return a}var a=function(e){switch(typeof e){case'string':return e;case'boolean':return e?'true':'false';case'number':return isFinite(e)?e:'';default:return'';}};e.exports=function(e,o,i,l){return o=o||'&',i=i||'=',null===e&&(e=void 0),'object'==typeof e?r(s(e),function(s){var l=encodeURIComponent(a(s))+i;return t(e[s])?r(e[s],function(e){return l+encodeURIComponent(a(e))}).join(o):l+encodeURIComponent(a(e[s]))}).join(o):l?encodeURIComponent(a(l))+i+encodeURIComponent(a(e)):''};var t=Array.isArray||function(e){return'[object Array]'===Object.prototype.toString.call(e)},s=Object.keys||function(e){var r=[];for(var a in e)Object.prototype.hasOwnProperty.call(e,a)&&r.push(a);return r}},function(e){'use strict';e.exports=function(e){for(var r,a=0,t=e.length,s=0;s<t;)a++,r=e.charCodeAt(s++),55296<=r&&56319>=r&&s<t&&(r=e.charCodeAt(s),56320==(64512&r)&&s++);return a}},function(e){'use strict';function r(e,s,o,l,n,h,d,p,c){if(o&&'object'==typeof o&&!Array.isArray(o))for(var m in s(o,l,n,h,d,p,c),o){var u=o[m];if(Array.isArray(u)){if(m in t.arrayKeywords)for(var f=0;f<u.length;f++)r(e,s,u[f],l+'/'+m+'/'+f,n,l,m,o,f);}else if(!(m in t.propsKeywords))(m in t.keywords||e.allKeys&&!(m in t.skipKeywords))&&r(e,s,u,l+'/'+m,n,l,m,o);else if(u&&'object'==typeof u)for(var i in u)r(e,s,u[i],l+'/'+m+'/'+a(i),n,l,m,o,i)}}function a(e){return e.replace(/~/g,'~0').replace(/\//g,'~1')}var t=e.exports=function(e,a,t){'function'==typeof a&&(t=a,a={}),r(a,t,e,'',e)};t.keywords={additionalItems:!0,items:!0,contains:!0,additionalProperties:!0,propertyNames:!0,not:!0},t.arrayKeywords={items:!0,allOf:!0,anyOf:!0,oneOf:!0},t.propsKeywords={definitions:!0,properties:!0,patternProperties:!0,dependencies:!0},t.skipKeywords={enum:!0,const:!0,required:!0,maximum:!0,minimum:!0,exclusiveMaximum:!0,exclusiveMinimum:!0,multipleOf:!0,maxLength:!0,minLength:!0,pattern:!0,format:!0,maxItems:!0,minItems:!0,uniqueItems:!0,maxProperties:!0,minProperties:!0}},function(e,r,a){r.parse=a(26),r.stringify=a(27)},function(e){var r,a,t,s,o={'"':'"',"\\":'\\',"/":'/',b:'\b',f:'\f',n:'\n',r:'\r',t:'\t'},l=function(e){throw{name:'SyntaxError',message:e,at:r,text:t}},n=function(e){return e&&e!==a&&l('Expected \''+e+'\' instead of \''+a+'\''),a=t.charAt(r),r+=1,a},i=function(){var e,r='';for('-'===a&&(r='-',n('-'));'0'<=a&&'9'>=a;)r+=a,n();if('.'===a)for(r+='.';n()&&'0'<=a&&'9'>=a;)r+=a;if('e'===a||'E'===a)for(r+=a,n(),('-'===a||'+'===a)&&(r+=a,n());'0'<=a&&'9'>=a;)r+=a,n();return e=+r,isFinite(e)?e:void l('Bad number')},h=function(){var e,r,t,s='';if('"'===a)for(;n();){if('"'===a)return n(),s;if('\\'!==a)s+=a;else if(n(),'u'===a){for(t=0,r=0;4>r&&(e=parseInt(n(),16),!!isFinite(e));r+=1)t=16*t+e;s+=String.fromCharCode(t)}else if('string'==typeof o[a])s+=o[a];else break}l('Bad string')},d=function(){for(;a&&' '>=a;)n()},p=function(){switch(a){case't':return n('t'),n('r'),n('u'),n('e'),!0;case'f':return n('f'),n('a'),n('l'),n('s'),n('e'),!1;case'n':return n('n'),n('u'),n('l'),n('l'),null;}l('Unexpected \''+a+'\'')},c=function(){var e=[];if('['===a){if(n('['),d(),']'===a)return n(']'),e;for(;a;){if(e.push(s()),d(),']'===a)return n(']'),e;n(','),d()}}l('Bad array')},m=function(){var e,r={};if('{'===a){if(n('{'),d(),'}'===a)return n('}'),r;for(;a;){if(e=h(),d(),n(':'),Object.hasOwnProperty.call(r,e)&&l('Duplicate key "'+e+'"'),r[e]=s(),d(),'}'===a)return n('}'),r;n(','),d()}}l('Bad object')};s=function(){return d(),'{'===a?m():'['===a?c():'"'===a?h():'-'===a?i():'0'<=a&&'9'>=a?i():p()},e.exports=function(e,o){var i;return t=e,r=0,a=' ',i=s(),d(),a&&l('Syntax error'),'function'==typeof o?function e(r,a){var t,s,i=r[a];if(i&&'object'==typeof i)for(t in i)Object.prototype.hasOwnProperty.call(i,t)&&(s=e(i,t),void 0===s?delete i[t]:i[t]=s);return o.call(r,a,i)}({"":i},''):i}},function(e){function r(e){return l.lastIndex=0,l.test(e)?'"'+e.replace(l,function(e){var r=n[e];return'string'==typeof r?r:'\\u'+('0000'+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function a(e,l){var n,i,h,d,p,c=t,m=l[e];switch(m&&'object'==typeof m&&'function'==typeof m.toJSON&&(m=m.toJSON(e)),'function'==typeof o&&(m=o.call(l,e,m)),typeof m){case'string':return r(m);case'number':return isFinite(m)?m+'':'null';case'boolean':case'null':return m+'';case'object':if(!m)return'null';if(t+=s,p=[],'[object Array]'===Object.prototype.toString.apply(m)){for(d=m.length,n=0;n<d;n+=1)p[n]=a(n,m)||'null';return h=0===p.length?'[]':t?'[\n'+t+p.join(',\n'+t)+'\n'+c+']':'['+p.join(',')+']',t=c,h}if(o&&'object'==typeof o)for(d=o.length,n=0;n<d;n+=1)i=o[n],'string'==typeof i&&(h=a(i,m),h&&p.push(r(i)+(t?': ':':')+h));else for(i in m)Object.prototype.hasOwnProperty.call(m,i)&&(h=a(i,m),h&&p.push(r(i)+(t?': ':':')+h));return h=0===p.length?'{}':t?'{\n'+t+p.join(',\n'+t)+'\n'+c+'}':'{'+p.join(',')+'}',t=c,h;}}var t,s,o,i=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,l=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,n={"":'\\b',"	":'\\t',"\n":'\\n',"":'\\f',"\r":'\\r','"':'\\"',"\\":'\\\\'};e.exports=function(e,r,l){var n;if(t='',s='','number'==typeof l)for(n=0;n<l;n+=1)s+=' ';else'string'==typeof l&&(s=l);if(o=r,r&&'function'!=typeof r&&('object'!=typeof r||'number'!=typeof r.length))throw new Error('JSON.stringify');return a('',{"":e})}},function(e){'use strict';var r=e.exports=function(){this._cache={}};r.prototype.put=function(e,r){this._cache[e]=r},r.prototype.get=function(e){return this._cache[e]},r.prototype.del=function(e){delete this._cache[e]},r.prototype.clear=function(){this._cache={}}},function(e,r,a){'use strict';function t(e){return e='full'==e?'full':'fast',l.copy(t[e])}function s(e){var r=e.match(n);if(!r)return!1;var a=+r[1],t=+r[2];return 1<=a&&12>=a&&1<=t&&t<=h[a]}function o(e,r){var a=e.match(d);if(!a)return!1;var t=a[1],s=a[2],o=a[3],i=a[5];return 23>=t&&59>=s&&59>=o&&(!r||i)}function i(e){if(b.test(e))return!1;try{return new RegExp(e),!0}catch(r){return!1}}var l=a(0),n=/^\d\d\d\d-(\d\d)-(\d\d)$/,h=[0,31,29,31,30,31,30,31,31,30,31,30,31],d=/^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d:\d\d)?$/i,p=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*$/i,c=/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i,m=/^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i,u=/^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i,f=/^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i,v=/^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i,y=/^(?:\/(?:[^~/]|~0|~1)*)*$|^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i,g=/^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/;e.exports=t,t.fast={date:/^\d\d\d\d-[0-1]\d-[0-3]\d$/,time:/^[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)?$/i,"date-time":/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s][0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)$/i,uri:/^(?:[a-z][a-z0-9+-.]*)(?::|\/)\/?[^\s]*$/i,"uri-reference":/^(?:(?:[a-z][a-z0-9+-.]*:)?\/\/)?[^\s]*$/i,"uri-template":u,url:f,email:/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i,hostname:p,ipv4:/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,ipv6:/^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i,regex:i,uuid:v,"json-pointer":y,"relative-json-pointer":g},t.full={date:s,time:o,"date-time":function(e){var r=e.split(P);return 2==r.length&&s(r[0])&&o(r[1],!0)},uri:function(e){return E.test(e)&&c.test(e)},"uri-reference":m,"uri-template":u,url:f,email:/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&''*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,hostname:function(e){return 255>=e.length&&p.test(e)},ipv4:/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,ipv6:/^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i,regex:i,uuid:v,"json-pointer":y,"relative-json-pointer":g};var P=/t|\s/i,E=/\/|:/,b=/[^\\]\\Z/},function(e,r,a){'use strict';var t=a(31),s=a(0).toHash;e.exports=function(){var e=[{type:'number',rules:[{maximum:['exclusiveMaximum']},{minimum:['exclusiveMinimum']},'multipleOf','format']},{type:'string',rules:['maxLength','minLength','pattern','format']},{type:'array',rules:['maxItems','minItems','uniqueItems','contains','items']},{type:'object',rules:['maxProperties','minProperties','required','dependencies','propertyNames',{properties:['additionalProperties','patternProperties']}]},{rules:['$ref','const','enum','not','anyOf','oneOf','allOf']}],r=['type'];return e.all=s(r),e.types=s(['number','integer','string','array','object','boolean','null']),e.forEach(function(a){a.rules=a.rules.map(function(a){var s;if('object'==typeof a){var o=Object.keys(a)[0];s=a[o],a=o,s.forEach(function(a){r.push(a),e.all[a]=!0})}r.push(a);var i=e.all[a]={keyword:a,code:t[a],implements:s};return i}),a.type&&(e.types[a.type]=a)}),e.keywords=s(r.concat(['additionalItems','$schema','id','title','description','default','definitions'])),e.custom={},e}},function(e,r,a){'use strict';e.exports={$ref:a(32),allOf:a(33),anyOf:a(34),const:a(35),contains:a(36),dependencies:a(37),enum:a(38),format:a(39),items:a(40),maximum:a(8),minimum:a(8),maxItems:a(9),minItems:a(9),maxLength:a(10),minLength:a(10),maxProperties:a(11),minProperties:a(11),multipleOf:a(41),not:a(42),oneOf:a(43),pattern:a(44),properties:a(45),propertyNames:a(46),required:a(47),uniqueItems:a(48),validate:a(6)}},function(e){'use strict';e.exports=function(e,r){var a,t,s=' ',o=e.level,i=e.dataLevel,l=e.schema[r],n=e.errSchemaPath+'/'+r,h=!e.opts.allErrors,d='data'+(i||''),p='valid'+o;if('#'==l||'#/'==l)e.isRoot?(a=e.async,t='validate'):(a=!0===e.root.schema.$async,t='root.refVal[0]');else{var c=e.resolveRef(e.baseId,l,e.isRoot);if(c===void 0){var m=e.MissingRefError.message(e.baseId,l);if('fail'==e.opts.missingRefs){console.error(m);var u=u||[];u.push(s),s='',!1===e.createErrors?s+=' {} ':(s+=' { keyword: \'$ref\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { ref: \''+e.util.escapeQuotes(l)+'\' } ',!1!==e.opts.messages&&(s+=' , message: \'can\\\'t resolve reference '+e.util.escapeQuotes(l)+'\' '),e.opts.verbose&&(s+=' , schema: '+e.util.toQuotedString(l)+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),s+=' } ');var f=s;s=u.pop(),s+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+f+']); ':' validate.errors = ['+f+']; return false; ':' var err = '+f+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',h&&(s+=' if (false) { ')}else if('ignore'==e.opts.missingRefs)console.warn(m),h&&(s+=' if (true) { ');else throw new e.MissingRefError(e.baseId,l,m)}else if(c.inline){var v=e.util.copy(e);v.level++;var y='valid'+v.level;v.schema=c.schema,v.schemaPath='',v.errSchemaPath=l;var g=e.validate(v).replace(/validate\.schema/g,c.code);s+=' '+g+' ',h&&(s+=' if ('+y+') { ')}else a=!0===c.$async,t=c.code}if(t){var u=u||[];u.push(s),s='',s+=e.opts.passContext?' '+t+'.call(this, ':' '+t+'( ',s+=' '+d+', (dataPath || \'\')','""'!=e.errorPath&&(s+=' + '+e.errorPath);var P=i?'data'+(i-1||''):'parentData',E=i?e.dataPathArr[i]:'parentDataProperty';s+=' , '+P+' , '+E+', rootData)  ';var b=s;if(s=u.pop(),a){if(!e.async)throw new Error('async schema referenced by sync schema');h&&(s+=' var '+p+'; '),s+=' try { '+e.yieldAwait+' '+b+'; ',h&&(s+=' '+p+' = true; '),s+=' } catch (e) { if (!(e instanceof ValidationError)) throw e; if (vErrors === null) vErrors = e.errors; else vErrors = vErrors.concat(e.errors); errors = vErrors.length; ',h&&(s+=' '+p+' = false; '),s+=' } ',h&&(s+=' if ('+p+') { ')}else s+=' if (!'+b+') { if (vErrors === null) vErrors = '+t+'.errors; else vErrors = vErrors.concat('+t+'.errors); errors = vErrors.length; } ',h&&(s+=' else { ')}return s}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.schema[r],s=e.schemaPath+e.util.getProperty(r),o=e.errSchemaPath+'/'+r,i=!e.opts.allErrors,l=e.util.copy(e),n='';l.level++;var h='valid'+l.level,d=l.baseId,p=!0,c=t;if(c)for(var m,u=-1,f=c.length-1;u<f;)m=c[u+=1],e.util.schemaHasRules(m,e.RULES.all)&&(p=!1,l.schema=m,l.schemaPath=s+'['+u+']',l.errSchemaPath=o+'/'+u,a+='  '+e.validate(l)+' ',l.baseId=d,i&&(a+=' if ('+h+') { ',n+='}'));return i&&(p?a+=' if (true) { ':a+=' '+n.slice(0,-1)+' '),a=e.util.cleanUpCode(a),a}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.level,s=e.dataLevel,o=e.schema[r],i=e.schemaPath+e.util.getProperty(r),l=e.errSchemaPath+'/'+r,n=!e.opts.allErrors,h='valid'+t,d='errs__'+t,p=e.util.copy(e),c='';p.level++;var m='valid'+p.level,u=o.every(function(r){return e.util.schemaHasRules(r,e.RULES.all)});if(u){var f=p.baseId;a+=' var '+d+' = errors; var '+h+' = false;  ';var v=e.compositeRule;e.compositeRule=p.compositeRule=!0;var y=o;if(y)for(var g,P=-1,E=y.length-1;P<E;)g=y[P+=1],p.schema=g,p.schemaPath=i+'['+P+']',p.errSchemaPath=l+'/'+P,a+='  '+e.validate(p)+' ',p.baseId=f,a+=' '+h+' = '+h+' || '+m+'; if (!'+h+') { ',c+='}';e.compositeRule=p.compositeRule=v,a+=' '+c+' if (!'+h+') {   var err =   ',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'anyOf\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: {} ',!1!==e.opts.messages&&(a+=' , message: \'should match some schema in anyOf\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+('data'+(s||''))+' '),a+=' } '),a+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',!e.compositeRule&&n&&(e.async?a+=' throw new ValidationError(vErrors); ':a+=' validate.errors = vErrors; return false; '),a+=' } else {  errors = '+d+'; if (vErrors !== null) { if ('+d+') vErrors.length = '+d+'; else vErrors = null; } ',e.opts.allErrors&&(a+=' } '),a=e.util.cleanUpCode(a)}else n&&(a+=' if (true) { ');return a}},function(e){'use strict';e.exports=function(e,r){var a,t=' ',s=e.level,o=e.dataLevel,i=e.schema[r],l=e.schemaPath+e.util.getProperty(r),n=e.errSchemaPath+'/'+r,h=!e.opts.allErrors,d='data'+(o||''),p='valid'+s,c=e.opts.$data&&i&&i.$data;c?(t+=' var schema'+s+' = '+e.util.getData(i.$data,o,e.dataPathArr)+'; ',a='schema'+s):a=i,c||(t+=' var schema'+s+' = validate.schema'+l+';'),t+='var '+p+' = equal('+d+', schema'+s+'); if (!'+p+') {   ';var m=m||[];m.push(t),t='',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'const\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: {} ',!1!==e.opts.messages&&(t+=' , message: \'should be equal to constant\' '),e.opts.verbose&&(t+=' , schema: validate.schema'+l+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } ');var u=t;return t=m.pop(),t+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+u+']); ':' validate.errors = ['+u+']; return false; ':' var err = '+u+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',t+=' }',h&&(t+=' else { '),t}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.level,s=e.dataLevel,o=e.schema[r],i=e.schemaPath+e.util.getProperty(r),l=e.errSchemaPath+'/'+r,n=!e.opts.allErrors,h='data'+(s||''),d='errs__'+t,p=e.util.copy(e);p.level++;var c='valid'+p.level,m='i'+t,u=p.dataLevel=e.dataLevel+1,f='data'+u,v=e.baseId,y=e.util.schemaHasRules(o,e.RULES.all);if(a+='var '+d+' = errors;var '+('valid'+t)+';',y){var g=e.compositeRule;e.compositeRule=p.compositeRule=!0,p.schema=o,p.schemaPath=i,p.errSchemaPath=l,a+=' var '+c+' = false; for (var '+m+' = 0; '+m+' < '+h+'.length; '+m+'++) { ',p.errorPath=e.util.getPathExpr(e.errorPath,m,e.opts.jsonPointers,!0);var P=h+'['+m+']';p.dataPathArr[u]=m;var E=e.validate(p);p.baseId=v,a+=2>e.util.varOccurences(E,f)?' '+e.util.varReplace(E,f,P)+' ':' var '+f+' = '+P+'; '+E+' ',a+=' if ('+c+') break; }  ',e.compositeRule=p.compositeRule=g,a+=' '+''+' if (!'+c+') {'}else a+=' if ('+h+'.length == 0) {';var b=b||[];b.push(a),a='',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'contains\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: {} ',!1!==e.opts.messages&&(a+=' , message: \'should contain a valid item\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } ');var w=a;return a=b.pop(),a+=!e.compositeRule&&n?e.async?' throw new ValidationError(['+w+']); ':' validate.errors = ['+w+']; return false; ':' var err = '+w+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',a+=' } else { ',y&&(a+='  errors = '+d+'; if (vErrors !== null) { if ('+d+') vErrors.length = '+d+'; else vErrors = null; } '),e.opts.allErrors&&(a+=' } '),a=e.util.cleanUpCode(a),a}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.level,s=e.dataLevel,o=e.schema[r],i=e.schemaPath+e.util.getProperty(r),l=e.errSchemaPath+'/'+r,n=!e.opts.allErrors,h='data'+(s||''),d='errs__'+t,p=e.util.copy(e),c='';p.level++;var m='valid'+p.level,u={},f={},v=e.opts.ownProperties;for(E in o){var y=o[E],g=Array.isArray(y)?f:u;g[E]=y}a+='var '+d+' = errors;';var P=e.errorPath;for(var E in a+='var missing'+t+';',f)if(g=f[E],g.length){if(a+=' if ( '+h+e.util.getProperty(E)+' !== undefined ',v&&(a+=' && Object.prototype.hasOwnProperty.call('+h+', \''+e.util.escapeQuotes(E)+'\') '),n){a+=' && ( ';var b=g;if(b)for(var w,S=-1,x=b.length-1;S<x;){w=b[S+=1],S&&(a+=' || ');var k=e.util.getProperty(w),$=h+k;a+=' ( ( '+$+' === undefined ',v&&(a+=' || ! Object.prototype.hasOwnProperty.call('+h+', \''+e.util.escapeQuotes(w)+'\') '),a+=') && (missing'+t+' = '+e.util.toQuotedString(e.opts.jsonPointers?w:k)+') ) '}a+=')) {  ';var _='missing'+t,R='\' + '+_+' + \'';e.opts._errorDataPathProperty&&(e.errorPath=e.opts.jsonPointers?e.util.getPathExpr(P,_,!0):P+' + '+_);var j=j||[];j.push(a),a='',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'dependencies\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: { property: \''+e.util.escapeQuotes(E)+'\', missingProperty: \''+R+'\', depsCount: '+g.length+', deps: \''+e.util.escapeQuotes(1==g.length?g[0]:g.join(', '))+'\' } ',!1!==e.opts.messages&&(a+=' , message: \'should have ',a+=1==g.length?'property '+e.util.escapeQuotes(g[0]):'properties '+e.util.escapeQuotes(g.join(', ')),a+=' when property '+e.util.escapeQuotes(E)+' is present\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } ');var I=a;a=j.pop(),a+=!e.compositeRule&&n?e.async?' throw new ValidationError(['+I+']); ':' validate.errors = ['+I+']; return false; ':' var err = '+I+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '}else{a+=' ) { ';var O=g;if(O)for(var w,A=-1,L=O.length-1;A<L;){w=O[A+=1];var k=e.util.getProperty(w),R=e.util.escapeQuotes(w),$=h+k;e.opts._errorDataPathProperty&&(e.errorPath=e.util.getPath(P,w,e.opts.jsonPointers)),a+=' if ( '+$+' === undefined ',v&&(a+=' || ! Object.prototype.hasOwnProperty.call('+h+', \''+e.util.escapeQuotes(w)+'\') '),a+=') {  var err =   ',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'dependencies\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: { property: \''+e.util.escapeQuotes(E)+'\', missingProperty: \''+R+'\', depsCount: '+g.length+', deps: \''+e.util.escapeQuotes(1==g.length?g[0]:g.join(', '))+'\' } ',!1!==e.opts.messages&&(a+=' , message: \'should have ',a+=1==g.length?'property '+e.util.escapeQuotes(g[0]):'properties '+e.util.escapeQuotes(g.join(', ')),a+=' when property '+e.util.escapeQuotes(E)+' is present\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } '),a+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } '}}a+=' }   ',n&&(c+='}',a+=' else { ')}e.errorPath=P;var Q=p.baseId;for(var E in u){var y=u[E];e.util.schemaHasRules(y,e.RULES.all)&&(a+=' '+m+' = true; if ( '+h+e.util.getProperty(E)+' !== undefined ',v&&(a+=' && Object.prototype.hasOwnProperty.call('+h+', \''+e.util.escapeQuotes(E)+'\') '),a+=') { ',p.schema=y,p.schemaPath=i+e.util.getProperty(E),p.errSchemaPath=l+'/'+e.util.escapeFragment(E),a+='  '+e.validate(p)+' ',p.baseId=Q,a+=' }  ',n&&(a+=' if ('+m+') { ',c+='}'))}return n&&(a+='   '+c+' if ('+d+' == errors) {'),a=e.util.cleanUpCode(a),a}},function(e){'use strict';e.exports=function(e,r){var a,t=' ',s=e.level,o=e.dataLevel,i=e.schema[r],l=e.schemaPath+e.util.getProperty(r),n=e.errSchemaPath+'/'+r,h=!e.opts.allErrors,d='data'+(o||''),p='valid'+s,c=e.opts.$data&&i&&i.$data;c?(t+=' var schema'+s+' = '+e.util.getData(i.$data,o,e.dataPathArr)+'; ',a='schema'+s):a=i;var m='i'+s,u='schema'+s;c||(t+=' var '+u+' = validate.schema'+l+';'),t+='var '+p+';',c&&(t+=' if (schema'+s+' === undefined) '+p+' = true; else if (!Array.isArray(schema'+s+')) '+p+' = false; else {'),t+=''+p+' = false;for (var '+m+'=0; '+m+'<'+u+'.length; '+m+'++) if (equal('+d+', '+u+'['+m+'])) { '+p+' = true; break; }',c&&(t+='  }  '),t+=' if (!'+p+') {   ';var f=f||[];f.push(t),t='',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'enum\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { allowedValues: schema'+s+' } ',!1!==e.opts.messages&&(t+=' , message: \'should be equal to one of the allowed values\' '),e.opts.verbose&&(t+=' , schema: validate.schema'+l+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } ');var v=t;return t=f.pop(),t+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+v+']); ':' validate.errors = ['+v+']; return false; ':' var err = '+v+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',t+=' }',h&&(t+=' else { '),t}},function(e){'use strict';e.exports=function(e,r,a){var t=' ',s=e.level,o=e.dataLevel,i=e.schema[r],l=e.schemaPath+e.util.getProperty(r),n=e.errSchemaPath+'/'+r,h=!e.opts.allErrors,d='data'+(o||'');if(!1===e.opts.format)return h&&(t+=' if (true) { '),t;var p,c=e.opts.$data&&i&&i.$data;c?(t+=' var schema'+s+' = '+e.util.getData(i.$data,o,e.dataPathArr)+'; ',p='schema'+s):p=i;var m=e.opts.unknownFormats,u=Array.isArray(m);if(c){var f='format'+s,v='isObject'+s,y='formatType'+s;t+=' var '+f+' = formats['+p+']; var '+v+' = typeof '+f+' == \'object\' && !('+f+' instanceof RegExp) && '+f+'.validate; var '+y+' = '+v+' && '+f+'.type || \'string\'; if ('+v+') { ',e.async&&(t+=' var async'+s+' = '+f+'.async; '),t+=' '+f+' = '+f+'.validate; } if (  ',c&&(t+=' ('+p+' !== undefined && typeof '+p+' != \'string\') || '),t+=' (','ignore'!=m&&(t+=' ('+p+' && !'+f+' ',u&&(t+=' && self._opts.unknownFormats.indexOf('+p+') == -1 '),t+=') || '),t+=' ('+f+' && '+y+' == \''+a+'\' && !(typeof '+f+' == \'function\' ? ',t+=e.async?' (async'+s+' ? '+e.yieldAwait+' '+f+'('+d+') : '+f+'('+d+')) ':' '+f+'('+d+') ',t+=' : '+f+'.test('+d+'))))) {'}else{var f=e.formats[i];if(!f){if('ignore'==m)return console.warn('unknown format "'+i+'" ignored in schema at path "'+e.errSchemaPath+'"'),h&&(t+=' if (true) { '),t;if(u&&0<=m.indexOf(i))return h&&(t+=' if (true) { '),t;throw new Error('unknown format "'+i+'" is used in schema at path "'+e.errSchemaPath+'"')}var v='object'==typeof f&&!(f instanceof RegExp)&&f.validate,y=v&&f.type||'string';if(v){var g=!0===f.async;f=f.validate}if(y!=a)return h&&(t+=' if (true) { '),t;if(g){if(!e.async)throw new Error('async format in sync schema');var P='formats'+e.util.getProperty(i)+'.validate';t+=' if (!('+e.yieldAwait+' '+P+'('+d+'))) { '}else{t+=' if (! ';var P='formats'+e.util.getProperty(i);v&&(P+='.validate'),t+='function'==typeof f?' '+P+'('+d+') ':' '+P+'.test('+d+') ',t+=') { '}}var E=E||[];E.push(t),t='',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'format\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { format:  ',t+=c?''+p:''+e.util.toQuotedString(i),t+='  } ',!1!==e.opts.messages&&(t+=' , message: \'should match format "',t+=c?'\' + '+p+' + \'':''+e.util.escapeQuotes(i),t+='"\' '),e.opts.verbose&&(t+=' , schema:  ',t+=c?'validate.schema'+l:''+e.util.toQuotedString(i),t+='         , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } ');var b=t;return t=E.pop(),t+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+b+']); ':' validate.errors = ['+b+']; return false; ':' var err = '+b+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',t+=' } ',h&&(t+=' else { '),t}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.level,s=e.dataLevel,o=e.schema[r],i=e.schemaPath+e.util.getProperty(r),l=e.errSchemaPath+'/'+r,n=!e.opts.allErrors,h='data'+(s||''),d='valid'+t,p='errs__'+t,c=e.util.copy(e),m='';c.level++;var u='valid'+c.level,f='i'+t,v=c.dataLevel=e.dataLevel+1,y='data'+v,g=e.baseId;if(a+='var '+p+' = errors;var '+d+';',Array.isArray(o)){var P=e.schema.additionalItems;if(!1===P){a+=' '+d+' = '+h+'.length <= '+o.length+'; ';var E=l;l=e.errSchemaPath+'/additionalItems',a+='  if (!'+d+') {   ';var b=b||[];b.push(a),a='',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'additionalItems\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: { limit: '+o.length+' } ',!1!==e.opts.messages&&(a+=' , message: \'should NOT have more than '+o.length+' items\' '),e.opts.verbose&&(a+=' , schema: false , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } ');var w=a;a=b.pop(),a+=!e.compositeRule&&n?e.async?' throw new ValidationError(['+w+']); ':' validate.errors = ['+w+']; return false; ':' var err = '+w+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',a+=' } ',l=E,n&&(m+='}',a+=' else { ')}var S=o;if(S)for(var x,k=-1,$=S.length-1;k<$;)if(x=S[k+=1],e.util.schemaHasRules(x,e.RULES.all)){a+=' '+u+' = true; if ('+h+'.length > '+k+') { ';var _=h+'['+k+']';c.schema=x,c.schemaPath=i+'['+k+']',c.errSchemaPath=l+'/'+k,c.errorPath=e.util.getPathExpr(e.errorPath,k,e.opts.jsonPointers,!0),c.dataPathArr[v]=k;var R=e.validate(c);c.baseId=g,a+=2>e.util.varOccurences(R,y)?' '+e.util.varReplace(R,y,_)+' ':' var '+y+' = '+_+'; '+R+' ',a+=' }  ',n&&(a+=' if ('+u+') { ',m+='}')}if('object'==typeof P&&e.util.schemaHasRules(P,e.RULES.all)){c.schema=P,c.schemaPath=e.schemaPath+'.additionalItems',c.errSchemaPath=e.errSchemaPath+'/additionalItems',a+=' '+u+' = true; if ('+h+'.length > '+o.length+') {  for (var '+f+' = '+o.length+'; '+f+' < '+h+'.length; '+f+'++) { ',c.errorPath=e.util.getPathExpr(e.errorPath,f,e.opts.jsonPointers,!0);var _=h+'['+f+']';c.dataPathArr[v]=f;var R=e.validate(c);c.baseId=g,a+=2>e.util.varOccurences(R,y)?' '+e.util.varReplace(R,y,_)+' ':' var '+y+' = '+_+'; '+R+' ',n&&(a+=' if (!'+u+') break; '),a+=' } }  ',n&&(a+=' if ('+u+') { ',m+='}')}}else if(e.util.schemaHasRules(o,e.RULES.all)){c.schema=o,c.schemaPath=i,c.errSchemaPath=l,a+='  for (var '+f+' = '+0+'; '+f+' < '+h+'.length; '+f+'++) { ',c.errorPath=e.util.getPathExpr(e.errorPath,f,e.opts.jsonPointers,!0);var _=h+'['+f+']';c.dataPathArr[v]=f;var R=e.validate(c);c.baseId=g,a+=2>e.util.varOccurences(R,y)?' '+e.util.varReplace(R,y,_)+' ':' var '+y+' = '+_+'; '+R+' ',n&&(a+=' if (!'+u+') break; '),a+=' }'}return n&&(a+=' '+m+' if ('+p+' == errors) {'),a=e.util.cleanUpCode(a),a}},function(e){'use strict';e.exports=function(e,r){var a,t=' ',s=e.level,o=e.dataLevel,i=e.schema[r],l=e.schemaPath+e.util.getProperty(r),n=e.errSchemaPath+'/'+r,h=!e.opts.allErrors,d='data'+(o||''),p=e.opts.$data&&i&&i.$data;p?(t+=' var schema'+s+' = '+e.util.getData(i.$data,o,e.dataPathArr)+'; ',a='schema'+s):a=i,t+='var division'+s+';if (',p&&(t+=' '+a+' !== undefined && ( typeof '+a+' != \'number\' || '),t+=' (division'+s+' = '+d+' / '+a+', ',t+=e.opts.multipleOfPrecision?' Math.abs(Math.round(division'+s+') - division'+s+') > 1e-'+e.opts.multipleOfPrecision+' ':' division'+s+' !== parseInt(division'+s+') ',t+=' ) ',p&&(t+='  )  '),t+=' ) {   ';var c=c||[];c.push(t),t='',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'multipleOf\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { multipleOf: '+a+' } ',!1!==e.opts.messages&&(t+=' , message: \'should be multiple of ',t+=p?'\' + '+a:''+a+'\''),e.opts.verbose&&(t+=' , schema:  ',t+=p?'validate.schema'+l:''+i,t+='         , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } ');var m=t;return t=c.pop(),t+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+m+']); ':' validate.errors = ['+m+']; return false; ':' var err = '+m+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',t+='} ',h&&(t+=' else { '),t}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.level,s=e.dataLevel,o=e.schema[r],i=e.schemaPath+e.util.getProperty(r),l=e.errSchemaPath+'/'+r,n=!e.opts.allErrors,h='data'+(s||''),d='errs__'+t,p=e.util.copy(e);p.level++;var c='valid'+p.level;if(e.util.schemaHasRules(o,e.RULES.all)){p.schema=o,p.schemaPath=i,p.errSchemaPath=l,a+=' var '+d+' = errors;  ';var m=e.compositeRule;e.compositeRule=p.compositeRule=!0,p.createErrors=!1;var u;p.opts.allErrors&&(u=p.opts.allErrors,p.opts.allErrors=!1),a+=' '+e.validate(p)+' ',p.createErrors=!0,u&&(p.opts.allErrors=u),e.compositeRule=p.compositeRule=m,a+=' if ('+c+') {   ';var f=f||[];f.push(a),a='',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'not\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: {} ',!1!==e.opts.messages&&(a+=' , message: \'should NOT be valid\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } ');var v=a;a=f.pop(),a+=!e.compositeRule&&n?e.async?' throw new ValidationError(['+v+']); ':' validate.errors = ['+v+']; return false; ':' var err = '+v+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',a+=' } else {  errors = '+d+'; if (vErrors !== null) { if ('+d+') vErrors.length = '+d+'; else vErrors = null; } ',e.opts.allErrors&&(a+=' } ')}else a+='  var err =   ',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'not\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: {} ',!1!==e.opts.messages&&(a+=' , message: \'should NOT be valid\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } '),a+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',n&&(a+=' if (false) { ');return a}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.level,s=e.dataLevel,o=e.schema[r],i=e.schemaPath+e.util.getProperty(r),l=e.errSchemaPath+'/'+r,n=!e.opts.allErrors,h='valid'+t,d='errs__'+t,p=e.util.copy(e),c='';p.level++;var m='valid'+p.level;a+='var '+d+' = errors;var prevValid'+t+' = false;var '+h+' = false;';var u=p.baseId,f=e.compositeRule;e.compositeRule=p.compositeRule=!0;var v=o;if(v)for(var y,g=-1,P=v.length-1;g<P;)y=v[g+=1],e.util.schemaHasRules(y,e.RULES.all)?(p.schema=y,p.schemaPath=i+'['+g+']',p.errSchemaPath=l+'/'+g,a+='  '+e.validate(p)+' ',p.baseId=u):a+=' var '+m+' = true; ',g&&(a+=' if ('+m+' && prevValid'+t+') '+h+' = false; else { ',c+='}'),a+=' if ('+m+') '+h+' = prevValid'+t+' = true;';return e.compositeRule=p.compositeRule=f,a+=''+c+'if (!'+h+') {   var err =   ',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'oneOf\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: {} ',!1!==e.opts.messages&&(a+=' , message: \'should match exactly one schema in oneOf\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+('data'+(s||''))+' '),a+=' } '),a+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',!e.compositeRule&&n&&(e.async?a+=' throw new ValidationError(vErrors); ':a+=' validate.errors = vErrors; return false; '),a+='} else {  errors = '+d+'; if (vErrors !== null) { if ('+d+') vErrors.length = '+d+'; else vErrors = null; }',e.opts.allErrors&&(a+=' } '),a}},function(e){'use strict';e.exports=function(e,r){var a,t=' ',s=e.level,o=e.dataLevel,i=e.schema[r],l=e.schemaPath+e.util.getProperty(r),n=e.errSchemaPath+'/'+r,h=!e.opts.allErrors,d='data'+(o||''),p=e.opts.$data&&i&&i.$data;p?(t+=' var schema'+s+' = '+e.util.getData(i.$data,o,e.dataPathArr)+'; ',a='schema'+s):a=i;var c=p?'(new RegExp('+a+'))':e.usePattern(i);t+='if ( ',p&&(t+=' ('+a+' !== undefined && typeof '+a+' != \'string\') || '),t+=' !'+c+'.test('+d+') ) {   ';var m=m||[];m.push(t),t='',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'pattern\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { pattern:  ',t+=p?''+a:''+e.util.toQuotedString(i),t+='  } ',!1!==e.opts.messages&&(t+=' , message: \'should match pattern "',t+=p?'\' + '+a+' + \'':''+e.util.escapeQuotes(i),t+='"\' '),e.opts.verbose&&(t+=' , schema:  ',t+=p?'validate.schema'+l:''+e.util.toQuotedString(i),t+='         , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } ');var u=t;return t=m.pop(),t+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+u+']); ':' validate.errors = ['+u+']; return false; ':' var err = '+u+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',t+='} ',h&&(t+=' else { '),t}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.level,s=e.dataLevel,o=e.schema[r],i=e.schemaPath+e.util.getProperty(r),l=e.errSchemaPath+'/'+r,n=!e.opts.allErrors,h='data'+(s||''),d='valid'+t,p='errs__'+t,c=e.util.copy(e),m='';c.level++;var u='valid'+c.level,f='key'+t,v='idx'+t,y=c.dataLevel=e.dataLevel+1,g='data'+y,P='dataProperties'+t,E=Object.keys(o||{}),b=e.schema.patternProperties||{},w=Object.keys(b),S=e.schema.additionalProperties,x=E.length||w.length,k=!1===S,$='object'==typeof S&&Object.keys(S).length,_=e.opts.removeAdditional,R=e.opts.ownProperties,j=e.baseId,I=e.schema.required;if(I&&!(e.opts.v5&&I.$data)&&I.length<e.opts.loopRequired)var O=e.util.toHash(I);if(e.opts.patternGroups)var A=e.schema.patternGroups||{},L=Object.keys(A);if(a+='var '+p+' = errors;var '+u+' = true;',R&&(a+=' var '+P+' = undefined;'),k||$||_){if(a+=R?' '+P+' = '+P+' || Object.keys('+h+'); for (var '+v+'=0; '+v+'<'+P+'.length; '+v+'++) { var '+f+' = '+P+'['+v+']; ':' for (var '+f+' in '+h+') { ',x){if(a+=' var isAdditional'+t+' = !(false ',E.length)if(5<E.length)a+=' || validate.schema'+i+'['+f+'] ';else{var Q=E;if(Q)for(var q,D=-1,V=Q.length-1;D<V;)q=Q[D+=1],a+=' || '+f+' == '+e.util.toQuotedString(q)+' '}if(w.length){var N=w;if(N)for(var U,C=-1,T=N.length-1;C<T;)U=N[C+=1],a+=' || '+e.usePattern(U)+'.test('+f+') '}if(e.opts.patternGroups&&L.length){var M=L;if(M)for(var K,C=-1,H=M.length-1;C<H;)K=M[C+=1],a+=' || '+e.usePattern(K)+'.test('+f+') '}a+=' ); if (isAdditional'+t+') { '}if('all'==_)a+=' delete '+h+'['+f+']; ';else{var F=e.errorPath;if(e.opts._errorDataPathProperty&&(e.errorPath=e.util.getPathExpr(e.errorPath,f,e.opts.jsonPointers)),k){if(_)a+=' delete '+h+'['+f+']; ';else{a+=' '+u+' = false; ';var G=l;l=e.errSchemaPath+'/additionalProperties';var J=J||[];J.push(a),a='',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'additionalProperties\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: { additionalProperty: \''+('\' + '+f+' + \'')+'\' } ',!1!==e.opts.messages&&(a+=' , message: \'should NOT have additional properties\' '),e.opts.verbose&&(a+=' , schema: false , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } ');var z=a;a=J.pop(),a+=!e.compositeRule&&n?e.async?' throw new ValidationError(['+z+']); ':' validate.errors = ['+z+']; return false; ':' var err = '+z+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',l=G,n&&(a+=' break; ')}}else if($)if('failing'==_){a+=' var '+p+' = errors;  ';var B=e.compositeRule;e.compositeRule=c.compositeRule=!0,c.schema=S,c.schemaPath=e.schemaPath+'.additionalProperties',c.errSchemaPath=e.errSchemaPath+'/additionalProperties',c.errorPath=e.opts._errorDataPathProperty?e.errorPath:e.util.getPathExpr(e.errorPath,f,e.opts.jsonPointers);var Y=h+'['+f+']';c.dataPathArr[y]=f;var X=e.validate(c);c.baseId=j,a+=2>e.util.varOccurences(X,g)?' '+e.util.varReplace(X,g,Y)+' ':' var '+g+' = '+Y+'; '+X+' ',a+=' if (!'+u+') { errors = '+p+'; if (validate.errors !== null) { if (errors) validate.errors.length = errors; else validate.errors = null; } delete '+h+'['+f+']; }  ',e.compositeRule=c.compositeRule=B}else{c.schema=S,c.schemaPath=e.schemaPath+'.additionalProperties',c.errSchemaPath=e.errSchemaPath+'/additionalProperties',c.errorPath=e.opts._errorDataPathProperty?e.errorPath:e.util.getPathExpr(e.errorPath,f,e.opts.jsonPointers);var Y=h+'['+f+']';c.dataPathArr[y]=f;var X=e.validate(c);c.baseId=j,a+=2>e.util.varOccurences(X,g)?' '+e.util.varReplace(X,g,Y)+' ':' var '+g+' = '+Y+'; '+X+' ',n&&(a+=' if (!'+u+') break; ')}e.errorPath=F}x&&(a+=' } '),a+=' }  ',n&&(a+=' if ('+u+') { ',m+='}')}var Z=e.opts.useDefaults&&!e.compositeRule;if(E.length){var W=E;if(W)for(var q,ee=-1,re=W.length-1;ee<re;){q=W[ee+=1];var ae=o[q];if(e.util.schemaHasRules(ae,e.RULES.all)){var te=e.util.getProperty(q),Y=h+te,se=Z&&void 0!==ae.default;c.schema=ae,c.schemaPath=i+te,c.errSchemaPath=l+'/'+e.util.escapeFragment(q),c.errorPath=e.util.getPath(e.errorPath,q,e.opts.jsonPointers),c.dataPathArr[y]=e.util.toQuotedString(q);var X=e.validate(c);if(c.baseId=j,2>e.util.varOccurences(X,g)){X=e.util.varReplace(X,g,Y);var oe=Y}else{var oe=g;a+=' var '+g+' = '+Y+'; '}if(se)a+=' '+X+' ';else{if(O&&O[q]){a+=' if ( '+oe+' === undefined ',R&&(a+=' || ! Object.prototype.hasOwnProperty.call('+h+', \''+e.util.escapeQuotes(q)+'\') '),a+=') { '+u+' = false; ';var F=e.errorPath,G=l,ie=e.util.escapeQuotes(q);e.opts._errorDataPathProperty&&(e.errorPath=e.util.getPath(F,q,e.opts.jsonPointers)),l=e.errSchemaPath+'/required';var J=J||[];J.push(a),a='',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'required\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: { missingProperty: \''+ie+'\' } ',!1!==e.opts.messages&&(a+=' , message: \'',a+=e.opts._errorDataPathProperty?'is a required property':'should have required property \\\''+ie+'\\\'',a+='\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } ');var z=a;a=J.pop(),a+=!e.compositeRule&&n?e.async?' throw new ValidationError(['+z+']); ':' validate.errors = ['+z+']; return false; ':' var err = '+z+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',l=G,e.errorPath=F,a+=' } else { '}else n?(a+=' if ( '+oe+' === undefined ',R&&(a+=' || ! Object.prototype.hasOwnProperty.call('+h+', \''+e.util.escapeQuotes(q)+'\') '),a+=') { '+u+' = true; } else { '):(a+=' if ('+oe+' !== undefined ',R&&(a+=' &&   Object.prototype.hasOwnProperty.call('+h+', \''+e.util.escapeQuotes(q)+'\') '),a+=' ) { ');a+=' '+X+' } '}}n&&(a+=' if ('+u+') { ',m+='}')}}if(w.length){var le=w;if(le)for(var U,ne=-1,he=le.length-1;ne<he;){U=le[ne+=1];var ae=b[U];if(e.util.schemaHasRules(ae,e.RULES.all)){c.schema=ae,c.schemaPath=e.schemaPath+'.patternProperties'+e.util.getProperty(U),c.errSchemaPath=e.errSchemaPath+'/patternProperties/'+e.util.escapeFragment(U),a+=R?' '+P+' = '+P+' || Object.keys('+h+'); for (var '+v+'=0; '+v+'<'+P+'.length; '+v+'++) { var '+f+' = '+P+'['+v+']; ':' for (var '+f+' in '+h+') { ',a+=' if ('+e.usePattern(U)+'.test('+f+')) { ',c.errorPath=e.util.getPathExpr(e.errorPath,f,e.opts.jsonPointers);var Y=h+'['+f+']';c.dataPathArr[y]=f;var X=e.validate(c);c.baseId=j,a+=2>e.util.varOccurences(X,g)?' '+e.util.varReplace(X,g,Y)+' ':' var '+g+' = '+Y+'; '+X+' ',n&&(a+=' if (!'+u+') break; '),a+=' } ',n&&(a+=' else '+u+' = true; '),a+=' }  ',n&&(a+=' if ('+u+') { ',m+='}')}}}if(e.opts.patternGroups&&L.length){var de=L;if(de)for(var K,pe=-1,ce=de.length-1;pe<ce;){K=de[pe+=1];var me=A[K],ae=me.schema;if(e.util.schemaHasRules(ae,e.RULES.all)){c.schema=ae,c.schemaPath=e.schemaPath+'.patternGroups'+e.util.getProperty(K)+'.schema',c.errSchemaPath=e.errSchemaPath+'/patternGroups/'+e.util.escapeFragment(K)+'/schema',a+=' var pgPropCount'+t+' = 0;  ',a+=R?' '+P+' = '+P+' || Object.keys('+h+'); for (var '+v+'=0; '+v+'<'+P+'.length; '+v+'++) { var '+f+' = '+P+'['+v+']; ':' for (var '+f+' in '+h+') { ',a+=' if ('+e.usePattern(K)+'.test('+f+')) { pgPropCount'+t+'++; ',c.errorPath=e.util.getPathExpr(e.errorPath,f,e.opts.jsonPointers);var Y=h+'['+f+']';c.dataPathArr[y]=f;var X=e.validate(c);c.baseId=j,a+=2>e.util.varOccurences(X,g)?' '+e.util.varReplace(X,g,Y)+' ':' var '+g+' = '+Y+'; '+X+' ',n&&(a+=' if (!'+u+') break; '),a+=' } ',n&&(a+=' else '+u+' = true; '),a+=' }  ',n&&(a+=' if ('+u+') { ',m+='}');var ue=me.minimum,fe=me.maximum;if(void 0!==ue||void 0!==fe){a+=' var '+d+' = true; ';var G=l;if(void 0!==ue){var ve=ue,ye='minimum',ge='less';a+=' '+d+' = pgPropCount'+t+' >= '+ue+'; ',l=e.errSchemaPath+'/patternGroups/minimum',a+='  if (!'+d+') {   ';var J=J||[];J.push(a),a='',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'patternGroups\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: { reason: \''+ye+'\', limit: '+ve+', pattern: \''+e.util.escapeQuotes(K)+'\' } ',!1!==e.opts.messages&&(a+=' , message: \'should NOT have '+ge+' than '+ve+' properties matching pattern "'+e.util.escapeQuotes(K)+'"\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } ');var z=a;a=J.pop(),a+=!e.compositeRule&&n?e.async?' throw new ValidationError(['+z+']); ':' validate.errors = ['+z+']; return false; ':' var err = '+z+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',a+=' } ',void 0!==fe&&(a+=' else ')}if(void 0!==fe){var ve=fe,ye='maximum',ge='more';a+=' '+d+' = pgPropCount'+t+' <= '+fe+'; ',l=e.errSchemaPath+'/patternGroups/maximum',a+='  if (!'+d+') {   ';var J=J||[];J.push(a),a='',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'patternGroups\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: { reason: \''+ye+'\', limit: '+ve+', pattern: \''+e.util.escapeQuotes(K)+'\' } ',!1!==e.opts.messages&&(a+=' , message: \'should NOT have '+ge+' than '+ve+' properties matching pattern "'+e.util.escapeQuotes(K)+'"\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } ');var z=a;a=J.pop(),a+=!e.compositeRule&&n?e.async?' throw new ValidationError(['+z+']); ':' validate.errors = ['+z+']; return false; ':' var err = '+z+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',a+=' } '}l=G,n&&(a+=' if ('+d+') { ',m+='}')}}}}return n&&(a+=' '+m+' if ('+p+' == errors) {'),a=e.util.cleanUpCode(a),a}},function(e){'use strict';e.exports=function(e,r){var a=' ',t=e.level,s=e.dataLevel,o=e.schema[r],i=e.schemaPath+e.util.getProperty(r),l=e.errSchemaPath+'/'+r,n=!e.opts.allErrors,h='data'+(s||''),d='errs__'+t,p=e.util.copy(e);p.level++;var c='valid'+p.level;if(e.util.schemaHasRules(o,e.RULES.all)){p.schema=o,p.schemaPath=i,p.errSchemaPath=l;var m='key'+t,u='idx'+t,f='i'+t,v='\' + '+m+' + \'',y=p.dataLevel=e.dataLevel+1,g='data'+y,P='dataProperties'+t,E=e.opts.ownProperties,b=e.baseId;a+=' var '+d+' = errors; ',E&&(a+=' var '+P+' = undefined; '),a+=E?' '+P+' = '+P+' || Object.keys('+h+'); for (var '+u+'=0; '+u+'<'+P+'.length; '+u+'++) { var '+m+' = '+P+'['+u+']; ':' for (var '+m+' in '+h+') { ',a+=' var startErrs'+t+' = errors; ';var w=m,S=e.compositeRule;e.compositeRule=p.compositeRule=!0;var x=e.validate(p);p.baseId=b,a+=2>e.util.varOccurences(x,g)?' '+e.util.varReplace(x,g,w)+' ':' var '+g+' = '+w+'; '+x+' ',e.compositeRule=p.compositeRule=S,a+=' if (!'+c+') { for (var '+f+'=startErrs'+t+'; '+f+'<errors; '+f+'++) { vErrors['+f+'].propertyName = '+m+'; }   var err =   ',!1===e.createErrors?a+=' {} ':(a+=' { keyword: \'propertyNames\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(l)+' , params: { propertyName: \''+v+'\' } ',!1!==e.opts.messages&&(a+=' , message: \'property name \\\''+v+'\\\' is invalid\' '),e.opts.verbose&&(a+=' , schema: validate.schema'+i+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+h+' '),a+=' } '),a+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',!e.compositeRule&&n&&(e.async?a+=' throw new ValidationError(vErrors); ':a+=' validate.errors = vErrors; return false; '),n&&(a+=' break; '),a+=' } }'}return n&&(a+=' '+''+' if ('+d+' == errors) {'),a=e.util.cleanUpCode(a),a}},function(e){'use strict';e.exports=function(e,r){var a,t=' ',s=e.level,o=e.dataLevel,i=e.schema[r],l=e.schemaPath+e.util.getProperty(r),n=e.errSchemaPath+'/'+r,h=!e.opts.allErrors,d='data'+(o||''),p='valid'+s,c=e.opts.$data&&i&&i.$data;c?(t+=' var schema'+s+' = '+e.util.getData(i.$data,o,e.dataPathArr)+'; ',a='schema'+s):a=i;var m='schema'+s;if(!c)if(i.length<e.opts.loopRequired&&e.schema.properties&&Object.keys(e.schema.properties).length){var u=[],f=i;if(f)for(var v,y=-1,g=f.length-1;y<g;){v=f[y+=1];var P=e.schema.properties[v];P&&e.util.schemaHasRules(P,e.RULES.all)||(u[u.length]=v)}}else var u=i;if(c||u.length){var E=e.errorPath,b=c||u.length>=e.opts.loopRequired,w=e.opts.ownProperties;if(h){if(t+=' var missing'+s+'; ',b){c||(t+=' var '+m+' = validate.schema'+l+'; ');var S='i'+s,x='schema'+s+'['+S+']',k='\' + '+x+' + \'';e.opts._errorDataPathProperty&&(e.errorPath=e.util.getPathExpr(E,x,e.opts.jsonPointers)),t+=' var '+p+' = true; ',c&&(t+=' if (schema'+s+' === undefined) '+p+' = true; else if (!Array.isArray(schema'+s+')) '+p+' = false; else {'),t+=' for (var '+S+' = 0; '+S+' < '+m+'.length; '+S+'++) { '+p+' = '+d+'['+m+'['+S+']] !== undefined ',w&&(t+=' &&   Object.prototype.hasOwnProperty.call('+d+', '+m+'['+S+']) '),t+='; if (!'+p+') break; } ',c&&(t+='  }  '),t+='  if (!'+p+') {   ';var $=$||[];$.push(t),t='',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'required\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { missingProperty: \''+k+'\' } ',!1!==e.opts.messages&&(t+=' , message: \'',t+=e.opts._errorDataPathProperty?'is a required property':'should have required property \\\''+k+'\\\'',t+='\' '),e.opts.verbose&&(t+=' , schema: validate.schema'+l+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } ');var _=t;t=$.pop(),t+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+_+']); ':' validate.errors = ['+_+']; return false; ':' var err = '+_+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',t+=' } else { '}else{t+=' if ( ';for(var R,j=u,S=-1,I=j.length-1;S<I;){R=j[S+=1],S&&(t+=' || ');var O=e.util.getProperty(R),A=d+O;t+=' ( ( '+A+' === undefined ',w&&(t+=' || ! Object.prototype.hasOwnProperty.call('+d+', \''+e.util.escapeQuotes(R)+'\') '),t+=') && (missing'+s+' = '+e.util.toQuotedString(e.opts.jsonPointers?R:O)+') ) '}t+=') {  ';var x='missing'+s,k='\' + '+x+' + \'';e.opts._errorDataPathProperty&&(e.errorPath=e.opts.jsonPointers?e.util.getPathExpr(E,x,!0):E+' + '+x);var $=$||[];$.push(t),t='',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'required\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { missingProperty: \''+k+'\' } ',!1!==e.opts.messages&&(t+=' , message: \'',t+=e.opts._errorDataPathProperty?'is a required property':'should have required property \\\''+k+'\\\'',t+='\' '),e.opts.verbose&&(t+=' , schema: validate.schema'+l+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } ');var _=t;t=$.pop(),t+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+_+']); ':' validate.errors = ['+_+']; return false; ':' var err = '+_+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',t+=' } else { '}}else if(b){c||(t+=' var '+m+' = validate.schema'+l+'; ');var S='i'+s,x='schema'+s+'['+S+']',k='\' + '+x+' + \'';e.opts._errorDataPathProperty&&(e.errorPath=e.util.getPathExpr(E,x,e.opts.jsonPointers)),c&&(t+=' if ('+m+' && !Array.isArray('+m+')) {  var err =   ',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'required\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { missingProperty: \''+k+'\' } ',!1!==e.opts.messages&&(t+=' , message: \'',t+=e.opts._errorDataPathProperty?'is a required property':'should have required property \\\''+k+'\\\'',t+='\' '),e.opts.verbose&&(t+=' , schema: validate.schema'+l+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } '),t+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } else if ('+m+' !== undefined) { '),t+=' for (var '+S+' = 0; '+S+' < '+m+'.length; '+S+'++) { if ('+d+'['+m+'['+S+']] === undefined ',w&&(t+=' || ! Object.prototype.hasOwnProperty.call('+d+', '+m+'['+S+']) '),t+=') {  var err =   ',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'required\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { missingProperty: \''+k+'\' } ',!1!==e.opts.messages&&(t+=' , message: \'',t+=e.opts._errorDataPathProperty?'is a required property':'should have required property \\\''+k+'\\\'',t+='\' '),e.opts.verbose&&(t+=' , schema: validate.schema'+l+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } '),t+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } } ',c&&(t+='  }  ')}else for(var R,L=u,Q=-1,q=L.length-1;Q<q;){R=L[Q+=1];var O=e.util.getProperty(R),k=e.util.escapeQuotes(R),A=d+O;e.opts._errorDataPathProperty&&(e.errorPath=e.util.getPath(E,R,e.opts.jsonPointers)),t+=' if ( '+A+' === undefined ',w&&(t+=' || ! Object.prototype.hasOwnProperty.call('+d+', \''+e.util.escapeQuotes(R)+'\') '),t+=') {  var err =   ',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'required\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { missingProperty: \''+k+'\' } ',!1!==e.opts.messages&&(t+=' , message: \'',t+=e.opts._errorDataPathProperty?'is a required property':'should have required property \\\''+k+'\\\'',t+='\' '),e.opts.verbose&&(t+=' , schema: validate.schema'+l+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } '),t+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } '}e.errorPath=E}else h&&(t+=' if (true) {');return t}},function(e){'use strict';e.exports=function(e,r){var a,t=' ',s=e.level,o=e.dataLevel,i=e.schema[r],l=e.schemaPath+e.util.getProperty(r),n=e.errSchemaPath+'/'+r,h=!e.opts.allErrors,d='data'+(o||''),p='valid'+s,c=e.opts.$data&&i&&i.$data;if(c?(t+=' var schema'+s+' = '+e.util.getData(i.$data,o,e.dataPathArr)+'; ',a='schema'+s):a=i,(i||c)&&!1!==e.opts.uniqueItems){c&&(t+=' var '+p+'; if ('+a+' === false || '+a+' === undefined) '+p+' = true; else if (typeof '+a+' != \'boolean\') '+p+' = false; else { '),t+=' var '+p+' = true; if ('+d+'.length > 1) { var i = '+d+'.length, j; outer: for (;i--;) { for (j = i; j--;) { if (equal('+d+'[i], '+d+'[j])) { '+p+' = false; break outer; } } } } ',c&&(t+='  }  '),t+=' if (!'+p+') {   ';var m=m||[];m.push(t),t='',!1===e.createErrors?t+=' {} ':(t+=' { keyword: \'uniqueItems\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(n)+' , params: { i: i, j: j } ',!1!==e.opts.messages&&(t+=' , message: \'should NOT have duplicate items (items ## \' + j + \' and \' + i + \' are identical)\' '),e.opts.verbose&&(t+=' , schema:  ',t+=c?'validate.schema'+l:''+i,t+='         , parentSchema: validate.schema'+e.schemaPath+' , data: '+d+' '),t+=' } ');var u=t;t=m.pop(),t+=!e.compositeRule&&h?e.async?' throw new ValidationError(['+u+']); ':' validate.errors = ['+u+']; return false; ':' var err = '+u+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',t+=' } ',h&&(t+=' else { ')}else h&&(t+=' if (true) { ');return t}},function(e){'use strict';var r=['multipleOf','maximum','exclusiveMaximum','minimum','exclusiveMinimum','maxLength','minLength','pattern','additionalItems','maxItems','minItems','uniqueItems','maxProperties','minProperties','required','additionalProperties','enum','format','const'];e.exports=function(e,a){for(var t=0;t<a.length;t++){e=JSON.parse(JSON.stringify(e));var s,o=a[t].split('/'),i=e;for(s=1;s<o.length;s++)i=i[o[s]];for(s=0;s<r.length;s++){var l=r[s],n=i[l];n&&(i[l]={anyOf:[n,{$ref:'https://raw.githubusercontent.com/epoberezkin/ajv/master/lib/refs/$data.json#'}]})}}return e}},function(e){'use strict';var r='http://json-schema.org/draft-06/schema';e.exports=function(e){var a=e._opts.defaultMeta,t='string'==typeof a?{$ref:a}:e.getSchema(r)?{$ref:r}:{};e.addKeyword('patternGroups',{metaSchema:{type:'object',additionalProperties:{type:'object',required:['schema'],properties:{maximum:{type:'integer',minimum:0},minimum:{type:'integer',minimum:0},schema:t},additionalProperties:!1}}}),e.RULES.all.properties.implements.push('patternGroups')}},function(e,r,a){'use strict';function t(e,r,a){function o(e){var r=e.$schema;return r&&!l.getSchema(r)?t.call(l,{$ref:r},!0):Promise.resolve()}function i(e){function a(a){function t(){delete l._loadingSchemas[n]}function s(e){return l._refs[e]||l._schemas[e]}var n=a.missingSchema;if(s(n))throw new Error('Schema '+n+' is loaded but '+a.missingRef+' cannot be resolved');var h=l._loadingSchemas[n];return h||(h=l._loadingSchemas[n]=l._opts.loadSchema(n),h.then(t,t)),h.then(function(e){if(!s(n))return o(e).then(function(){s(n)||l.addSchema(e,n,void 0,r)})}).then(function(){return i(e)})}try{return l._compile(e)}catch(r){if(r instanceof s)return a(r);throw r}}var l=this;if('function'!=typeof this._opts.loadSchema)throw new Error('options.loadSchema should be a function');'function'==typeof r&&(a=r,r=void 0);var n=o(e).then(function(){var a=l._addSchema(e,void 0,r);return a.validate||i(a)});return a&&n.then(function(e){a(null,e)},a),n}var s=a(3).MissingRef;e.exports=t},function(e,r,a){'use strict';var t=/^[a-z_$][a-z0-9_$-]*$/i,s=a(53);e.exports={add:function(e,r){function a(e,r,a){for(var t,o,n=0;n<l.length;n++)if(o=l[n],o.type==r){t=o;break}t||(t={type:r,rules:[]},l.push(t));var i={keyword:e,definition:a,custom:!0,code:s,implements:a.implements};t.rules.push(i),l.custom[e]=i}function o(e){if(!l.types[e])throw new Error('Unknown type '+e)}var l=this.RULES;if(l.keywords[e])throw new Error('Keyword '+e+' is already defined');if(!t.test(e))throw new Error('Keyword '+e+' is not a valid identifier');if(r){if(r.macro&&void 0!==r.valid)throw new Error('"valid" option cannot be used with macro keywords');var n=r.type;if(Array.isArray(n)){var h,i=n.length;for(h=0;h<i;h++)o(n[h]);for(h=0;h<i;h++)a(e,n[h],r)}else n&&o(n),a(e,n,r);var d=!0===r.$data&&this._opts.$data;if(d&&!r.validate)throw new Error('$data support: "validate" function is not defined');var p=r.metaSchema;p&&(d&&(p={anyOf:[p,{$ref:'https://raw.githubusercontent.com/epoberezkin/ajv/master/lib/refs/$data.json#'}]}),r.validateSchema=this.compile(p,!0))}l.keywords[e]=l.all[e]=!0},get:function(e){var r=this.RULES.custom[e];return r?r.definition:this.RULES.keywords[e]||!1},remove:function(e){var r=this.RULES;delete r.keywords[e],delete r.all[e],delete r.custom[e];for(var a,t=0;t<r.length;t++){a=r[t].rules;for(var s=0;s<a.length;s++)if(a[s].keyword==e){a.splice(s,1);break}}}}},function(e){'use strict';e.exports=function(e,r){var a,t,s=' ',o=e.level,i=e.dataLevel,l=e.schema[r],n=e.schemaPath+e.util.getProperty(r),h=e.errSchemaPath+'/'+r,d=!e.opts.allErrors,p='data'+(i||''),c='valid'+o,m='errs__'+o,u=e.opts.$data&&l&&l.$data;u?(s+=' var schema'+o+' = '+e.util.getData(l.$data,i,e.dataPathArr)+'; ',t='schema'+o):t=l;var f,v,y,g,P,E=this,b='definition'+o,w=E.definition,S='';if(u&&w.$data){P='keywordValidate'+o;var x=w.validateSchema;s+=' var '+b+' = RULES.custom[\''+r+'\'].definition; var '+P+' = '+b+'.validate;'}else{if(g=e.useCustomRule(E,l,e.schema,e),!g)return;t='validate.schema'+n,P=g.code,f=w.compile,v=w.inline,y=w.macro}var k=P+'.errors',$='i'+o,_='ruleErr'+o,R=w.async;if(R&&!e.async)throw new Error('async keyword in sync schema');if(v||y||(s+=''+k+' = null;'),s+='var '+m+' = errors;var '+c+';',u&&w.$data&&(S+='}',s+=' if ('+t+' === undefined) { '+c+' = true; } else { ',x&&(S+='}',s+=' '+c+' = '+b+'.validateSchema('+t+'); if ('+c+') { ')),v)s+=w.statements?' '+g.validate+' ':' '+c+' = '+g.validate+'; ';else if(y){var j=e.util.copy(e),S='';j.level++;var I='valid'+j.level;j.schema=g.validate,j.schemaPath='';var O=e.compositeRule;e.compositeRule=j.compositeRule=!0;var A=e.validate(j).replace(/validate\.schema/g,P);e.compositeRule=j.compositeRule=O,s+=' '+A}else{var L=L||[];L.push(s),s='',s+='  '+P+'.call( ',s+=e.opts.passContext?'this':'self',s+=f||!1===w.schema?' , '+p+' ':' , '+t+' , '+p+' , validate.schema'+e.schemaPath+' ',s+=' , (dataPath || \'\')','""'!=e.errorPath&&(s+=' + '+e.errorPath);var Q=i?'data'+(i-1||''):'parentData',q=i?e.dataPathArr[i]:'parentDataProperty';s+=' , '+Q+' , '+q+' , rootData )  ';var D=s;s=L.pop(),!1===w.errors?(s+=' '+c+' = ',R&&(s+=''+e.yieldAwait),s+=''+D+'; '):R?(k='customErrors'+o,s+=' var '+k+' = null; try { '+c+' = '+e.yieldAwait+D+'; } catch (e) { '+c+' = false; if (e instanceof ValidationError) '+k+' = e.errors; else throw e; } '):s+=' '+k+' = null; '+c+' = '+D+'; '}if(w.modifying&&(s+=' if ('+Q+') '+p+' = '+Q+'['+q+'];'),s+=''+S,w.valid)d&&(s+=' if (true) { ');else{s+=' if ( ',void 0===w.valid?(s+=' !',s+=y?''+I:''+c):s+=' '+!w.valid+' ',s+=') { ',a=E.keyword;var L=L||[];L.push(s),s='';var L=L||[];L.push(s),s='',!1===e.createErrors?s+=' {} ':(s+=' { keyword: \''+(a||'custom')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(h)+' , params: { keyword: \''+E.keyword+'\' } ',!1!==e.opts.messages&&(s+=' , message: \'should pass "'+E.keyword+'" keyword validation\' '),e.opts.verbose&&(s+=' , schema: validate.schema'+n+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+p+' '),s+=' } ');var V=s;s=L.pop(),s+=!e.compositeRule&&d?e.async?' throw new ValidationError(['+V+']); ':' validate.errors = ['+V+']; return false; ':' var err = '+V+';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ';var N=s;s=L.pop(),v?w.errors?'full'!=w.errors&&(s+='  for (var '+$+'='+m+'; '+$+'<errors; '+$+'++) { var '+_+' = vErrors['+$+']; if ('+_+'.dataPath === undefined) '+_+'.dataPath = (dataPath || \'\') + '+e.errorPath+'; if ('+_+'.schemaPath === undefined) { '+_+'.schemaPath = "'+h+'"; } ',e.opts.verbose&&(s+=' '+_+'.schema = '+t+'; '+_+'.data = '+p+'; '),s+=' } '):!1===w.errors?s+=' '+N+' ':(s+=' if ('+m+' == errors) { '+N+' } else {  for (var '+$+'='+m+'; '+$+'<errors; '+$+'++) { var '+_+' = vErrors['+$+']; if ('+_+'.dataPath === undefined) '+_+'.dataPath = (dataPath || \'\') + '+e.errorPath+'; if ('+_+'.schemaPath === undefined) { '+_+'.schemaPath = "'+h+'"; } ',e.opts.verbose&&(s+=' '+_+'.schema = '+t+'; '+_+'.data = '+p+'; '),s+=' } } '):y?(s+='   var err =   ',!1===e.createErrors?s+=' {} ':(s+=' { keyword: \''+(a||'custom')+'\' , dataPath: (dataPath || \'\') + '+e.errorPath+' , schemaPath: '+e.util.toQuotedString(h)+' , params: { keyword: \''+E.keyword+'\' } ',!1!==e.opts.messages&&(s+=' , message: \'should pass "'+E.keyword+'" keyword validation\' '),e.opts.verbose&&(s+=' , schema: validate.schema'+n+' , parentSchema: validate.schema'+e.schemaPath+' , data: '+p+' '),s+=' } '),s+=';  if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ',!e.compositeRule&&d&&(e.async?s+=' throw new ValidationError(vErrors); ':s+=' validate.errors = vErrors; return false; ')):!1===w.errors?s+=' '+N+' ':(s+=' if (Array.isArray('+k+')) { if (vErrors === null) vErrors = '+k+'; else vErrors = vErrors.concat('+k+'); errors = vErrors.length;  for (var '+$+'='+m+'; '+$+'<errors; '+$+'++) { var '+_+' = vErrors['+$+']; if ('+_+'.dataPath === undefined) '+_+'.dataPath = (dataPath || \'\') + '+e.errorPath+';  '+_+'.schemaPath = "'+h+'";  ',e.opts.verbose&&(s+=' '+_+'.schema = '+t+'; '+_+'.data = '+p+'; '),s+=' } } else { '+N+' } '),s+=' } ',d&&(s+=' else { ')}return s}},function(e){e.exports={$schema:'http://json-schema.org/draft-06/schema#',$id:'https://raw.githubusercontent.com/epoberezkin/ajv/master/lib/refs/$data.json#',description:'Meta-schema for $data reference (JSON-schema extension proposal)',type:'object',required:['$data'],properties:{$data:{type:'string',anyOf:[{format:'relative-json-pointer'},{format:'json-pointer'}]}},additionalProperties:!1}},function(e){e.exports={$schema:'http://json-schema.org/draft-06/schema#',$id:'http://json-schema.org/draft-06/schema#',title:'Core schema meta-schema',definitions:{schemaArray:{type:'array',minItems:1,items:{$ref:'#'}},nonNegativeInteger:{type:'integer',minimum:0},nonNegativeIntegerDefault0:{allOf:[{$ref:'#/definitions/nonNegativeInteger'},{default:0}]},simpleTypes:{enum:['array','boolean','integer','null','number','object','string']},stringArray:{type:'array',items:{type:'string'},uniqueItems:!0,default:[]}},type:['object','boolean'],properties:{$id:{type:'string',format:'uri-reference'},$schema:{type:'string',format:'uri'},$ref:{type:'string',format:'uri-reference'},title:{type:'string'},description:{type:'string'},default:{},multipleOf:{type:'number',exclusiveMinimum:0},maximum:{type:'number'},exclusiveMaximum:{type:'number'},minimum:{type:'number'},exclusiveMinimum:{type:'number'},maxLength:{$ref:'#/definitions/nonNegativeInteger'},minLength:{$ref:'#/definitions/nonNegativeIntegerDefault0'},pattern:{type:'string',format:'regex'},additionalItems:{$ref:'#'},items:{anyOf:[{$ref:'#'},{$ref:'#/definitions/schemaArray'}],default:{}},maxItems:{$ref:'#/definitions/nonNegativeInteger'},minItems:{$ref:'#/definitions/nonNegativeIntegerDefault0'},uniqueItems:{type:'boolean',default:!1},contains:{$ref:'#'},maxProperties:{$ref:'#/definitions/nonNegativeInteger'},minProperties:{$ref:'#/definitions/nonNegativeIntegerDefault0'},required:{$ref:'#/definitions/stringArray'},additionalProperties:{$ref:'#'},definitions:{type:'object',additionalProperties:{$ref:'#'},default:{}},properties:{type:'object',additionalProperties:{$ref:'#'},default:{}},patternProperties:{type:'object',additionalProperties:{$ref:'#'},default:{}},dependencies:{type:'object',additionalProperties:{anyOf:[{$ref:'#'},{$ref:'#/definitions/stringArray'}]}},propertyNames:{$ref:'#'},const:{},enum:{type:'array',minItems:1,uniqueItems:!0},type:{anyOf:[{$ref:'#/definitions/simpleTypes'},{type:'array',items:{$ref:'#/definitions/simpleTypes'},minItems:1,uniqueItems:!0}]},format:{type:'string'},allOf:{$ref:'#/definitions/schemaArray'},anyOf:{$ref:'#/definitions/schemaArray'},oneOf:{$ref:'#/definitions/schemaArray'},not:{$ref:'#'}},default:{}}}]);this.EXPORTED_SYMBOLS = ["ajv"];
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/vendor/mozjexl.js
@@ -0,0 +1,1 @@
+/* eslint-disable */this.mozjexl=function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={i:d,l:!1,exports:{}};return a[d].call(e.exports,e,e.exports,b),e.l=!0,e.exports}var c={};return b.m=a,b.c=c,b.d=function(a,c,d){b.o(a,c)||Object.defineProperty(a,c,{configurable:!1,enumerable:!0,get:d})},b.n=function(a){var c=a&&a.__esModule?function(){return a['default']}:function(){return a};return b.d(c,'a',c),c},b.o=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)},b.p='',b(b.s=56)}({12:function(a,b){b.argVal=function(a){this._cursor.args.push(a)},b.arrayStart=function(){this._placeAtCursor({type:'ArrayLiteral',value:[]})},b.arrayVal=function(a){a&&this._cursor.value.push(a)},b.binaryOp=function(a){for(var b=this._grammar[a.value].precedence||0,c=this._cursor._parent;c&&c.operator&&this._grammar[c.operator].precedence>=b;)this._cursor=c,c=c._parent;var d={type:'BinaryExpression',operator:a.value,left:this._cursor};this._setParent(this._cursor,d),this._cursor=c,this._placeAtCursor(d)},b.dot=function(){this._nextIdentEncapsulate=this._cursor&&('BinaryExpression'!=this._cursor.type||'BinaryExpression'==this._cursor.type&&this._cursor.right)&&'UnaryExpression'!=this._cursor.type,this._nextIdentRelative=!this._cursor||this._cursor&&!this._nextIdentEncapsulate,this._nextIdentRelative&&(this._relative=!0)},b.filter=function(a){this._placeBeforeCursor({type:'FilterExpression',expr:a,relative:this._subParser.isRelative(),subject:this._cursor})},b.identifier=function(a){var b={type:'Identifier',value:a.value};this._nextIdentEncapsulate?(b.from=this._cursor,this._placeBeforeCursor(b),this._nextIdentEncapsulate=!1):(this._nextIdentRelative&&(b.relative=!0),this._placeAtCursor(b))},b.literal=function(a){this._placeAtCursor({type:'Literal',value:a.value})},b.objKey=function(a){this._curObjKey=a.value},b.objStart=function(){this._placeAtCursor({type:'ObjectLiteral',value:{}})},b.objVal=function(a){this._cursor.value[this._curObjKey]=a},b.subExpression=function(a){this._placeAtCursor(a)},b.ternaryEnd=function(a){this._cursor.alternate=a},b.ternaryMid=function(a){this._cursor.consequent=a},b.ternaryStart=function(){this._tree={type:'ConditionalExpression',test:this._tree},this._cursor=this._tree},b.transform=function(a){this._placeBeforeCursor({type:'Transform',name:a.value,args:[],subject:this._cursor})},b.unaryOp=function(a){this._placeAtCursor({type:'UnaryExpression',operator:a.value})}},56:function(a,b,c){function d(){this._customGrammar=null,this._lexer=null,this._transforms={}}var e=c(57),f=c(59),g=c(60),h=c(62).elements;d.prototype.addBinaryOp=function(a,b,c){this._addGrammarElement(a,{type:'binaryOp',precedence:b,eval:c})},d.prototype.addUnaryOp=function(a,b){this._addGrammarElement(a,{type:'unaryOp',weight:Infinity,eval:b})},d.prototype.addTransform=function(a,b){this._transforms[a]=b},d.prototype.addTransforms=function(a){for(var b in a)a.hasOwnProperty(b)&&(this._transforms[b]=a[b])},d.prototype.getTransform=function(a){return this._transforms[a]},d.prototype.eval=function(a,b,c){'function'==typeof b?(c=b,b={}):!b&&(b={});var d=this._eval(a,b);if(c){var e=!1;return d.then(function(a){e=!0,setTimeout(c.bind(null,null,a),0)}).catch(function(a){e||setTimeout(c.bind(null,a),0)})}return d},d.prototype.removeOp=function(a){var b=this._getCustomGrammar();b[a]&&('binaryOp'==b[a].type||'unaryOp'==b[a].type)&&(delete b[a],this._lexer=null)},d.prototype._addGrammarElement=function(a,b){var c=this._getCustomGrammar();c[a]=b,this._lexer=null},d.prototype._eval=function(a,b){var c=this,d=this._getGrammar(),f=new g(d),h=new e(d,this._transforms,b);return Promise.resolve().then(function(){return f.addTokens(c._getLexer().tokenize(a)),h.eval(f.complete())})},d.prototype._getCustomGrammar=function(){if(!this._customGrammar)for(var a in this._customGrammar={},h)h.hasOwnProperty(a)&&(this._customGrammar[a]=h[a]);return this._customGrammar},d.prototype._getGrammar=function(){return this._customGrammar||h},d.prototype._getLexer=function(){return this._lexer||(this._lexer=new f(this._getGrammar())),this._lexer},a.exports=new d,a.exports.Jexl=d},57:function(a,b,c){var d=c(58),e=function(a,b,c,d){this._grammar=a,this._transforms=b||{},this._context=c||{},this._relContext=d||this._context};e.prototype.eval=function(a){var b=this;return Promise.resolve().then(function(){return d[a.type].call(b,a)})},e.prototype.evalArray=function(a){return Promise.all(a.map(function(a){return this.eval(a)},this))},e.prototype.evalMap=function(a){var b=Object.keys(a),c={},d=b.map(function(b){return this.eval(a[b])},this);return Promise.all(d).then(function(a){return a.forEach(function(a,d){c[b[d]]=a}),c})},e.prototype._filterRelative=function(a,b){if(void 0!==a){var c=[];return Array.isArray(a)||(a=[a]),a.forEach(function(a){var d=new e(this._grammar,this._transforms,this._context,a);c.push(d.eval(b))},this),Promise.all(c).then(function(b){var c=[];return b.forEach(function(b,d){b&&c.push(a[d])}),c})}},e.prototype._filterStatic=function(a,b){return this.eval(b).then(function(b){return'boolean'==typeof b?b?a:void 0:void 0===a?void 0:a[b]})},a.exports=e},58:function(a,b){b.ArrayLiteral=function(a){return this.evalArray(a.value)},b.BinaryExpression=function(a){var b=this;return Promise.all([this.eval(a.left),this.eval(a.right)]).then(function(c){return b._grammar[a.operator].eval(c[0],c[1])})},b.ConditionalExpression=function(a){var b=this;return this.eval(a.test).then(function(c){return c?a.consequent?b.eval(a.consequent):c:b.eval(a.alternate)})},b.FilterExpression=function(a){var b=this;return this.eval(a.subject).then(function(c){return a.relative?b._filterRelative(c,a.expr):b._filterStatic(c,a.expr)})},b.Identifier=function(a){return a.from?this.eval(a.from).then(function(b){if(void 0!==b)return Array.isArray(b)&&(b=b[0]),b[a.value]}):a.relative?this._relContext[a.value]:this._context[a.value]},b.Literal=function(a){return a.value},b.ObjectLiteral=function(a){return this.evalMap(a.value)},b.Transform=function(a){var b=this._transforms[a.name];if(!b)throw new Error('Transform \''+a.name+'\' is not defined.');return Promise.all([this.eval(a.subject),this.evalArray(a.args||[])]).then(function(a){return b.apply(null,[a[0]].concat(a[1]))})},b.UnaryExpression=function(a){var b=this;return this.eval(a.right).then(function(c){return b._grammar[a.operator].eval(c)})}},59:function(a){function b(a){this._grammar=a}var c=/^-?(?:(?:[0-9]*\.[0-9]+)|[0-9]+)$/,d=/^[a-zA-Z_\$][a-zA-Z0-9_\$]*$/,e=/\\\\/,f=['\'(?:(?:\\\\\')?[^\'])*\'','"(?:(?:\\\\")?[^"])*"','\\s+','\\btrue\\b','\\bfalse\\b'],g=['\\b[a-zA-Z_\\$][a-zA-Z0-9_\\$]*\\b','(?:(?:[0-9]*\\.[0-9]+)|[0-9]+)'],h=['binaryOp','unaryOp','openParen','openBracket','question','colon'];b.prototype.getElements=function(a){var b=this._getSplitRegex();return a.split(b).filter(function(a){return a})},b.prototype.getTokens=function(a){for(var b=[],c=!1,d=0;d<a.length;d++)this._isWhitespace(a[d])?b.length&&(b[b.length-1].raw+=a[d]):'-'===a[d]&&this._isNegative(b)?c=!0:(c&&(a[d]='-'+a[d],c=!1),b.push(this._createToken(a[d])));return c&&b.push(this._createToken('-')),b},b.prototype.tokenize=function(a){var b=this.getElements(a);return this.getTokens(b)},b.prototype._createToken=function(a){var b={type:'literal',value:a,raw:a};if('"'==a[0]||'\''==a[0])b.value=this._unquote(a);else if(a.match(c))b.value=parseFloat(a);else if('true'===a||'false'===a)b.value='true'==a;else if(this._grammar[a])b.type=this._grammar[a].type;else if(a.match(d))b.type='identifier';else throw new Error('Invalid expression token: '+a);return b},b.prototype._escapeRegExp=function(a){return a=a.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),a.match(d)&&(a='\\b'+a+'\\b'),a},b.prototype._getSplitRegex=function(){if(!this._splitRegex){var a=Object.keys(this._grammar);a=a.sort(function(c,a){return a.length-c.length}).map(function(a){return this._escapeRegExp(a)},this),this._splitRegex=new RegExp('('+[f.join('|'),a.join('|'),g.join('|')].join('|')+')')}return this._splitRegex},b.prototype._isNegative=function(a){return!a.length||h.some(function(b){return b===a[a.length-1].type})};var i=/^\s*$/;b.prototype._isWhitespace=function(a){return i.test(a)},b.prototype._unquote=function(a){var b=a[0],c=new RegExp('\\\\'+b,'g');return a.substr(1,a.length-2).replace(c,b).replace(e,'\\')},a.exports=b},60:function(a,b,c){function d(a,b,c){this._grammar=a,this._state='expectOperand',this._tree=null,this._exprStr=b||'',this._relative=!1,this._stopMap=c||{}}var e=c(12),f=c(61).states;d.prototype.addToken=function(a){if('complete'==this._state)throw new Error('Cannot add a new token to a completed Parser');var b=f[this._state],c=this._exprStr;if(this._exprStr+=a.raw,b.subHandler){this._subParser||this._startSubExpression(c);var d=this._subParser.addToken(a);if(d){if(this._endSubExpression(),this._parentStop)return d;this._state=d}}else if(b.tokenTypes[a.type]){var g=b.tokenTypes[a.type],h=e[a.type];g.handler&&(h=g.handler),h&&h.call(this,a),g.toState&&(this._state=g.toState)}else{if(this._stopMap[a.type])return this._stopMap[a.type];throw new Error('Token '+a.raw+' ('+a.type+') unexpected in expression: '+this._exprStr)}return!1},d.prototype.addTokens=function(a){a.forEach(this.addToken,this)},d.prototype.complete=function(){if(this._cursor&&!f[this._state].completable)throw new Error('Unexpected end of expression: '+this._exprStr);return this._subParser&&this._endSubExpression(),this._state='complete',this._cursor?this._tree:null},d.prototype.isRelative=function(){return this._relative},d.prototype._endSubExpression=function(){f[this._state].subHandler.call(this,this._subParser.complete()),this._subParser=null},d.prototype._placeAtCursor=function(a){this._cursor?(this._cursor.right=a,this._setParent(a,this._cursor)):this._tree=a,this._cursor=a},d.prototype._placeBeforeCursor=function(a){this._cursor=this._cursor._parent,this._placeAtCursor(a)},d.prototype._setParent=function(a,b){Object.defineProperty(a,'_parent',{value:b,writable:!0})},d.prototype._startSubExpression=function(a){var b=f[this._state].endStates;b||(this._parentStop=!0,b=this._stopMap),this._subParser=new d(this._grammar,a,b)},a.exports=d},61:function(a,b,c){var d=c(12);b.states={expectOperand:{tokenTypes:{literal:{toState:'expectBinOp'},identifier:{toState:'identifier'},unaryOp:{},openParen:{toState:'subExpression'},openCurl:{toState:'expectObjKey',handler:d.objStart},dot:{toState:'traverse'},openBracket:{toState:'arrayVal',handler:d.arrayStart}}},expectBinOp:{tokenTypes:{binaryOp:{toState:'expectOperand'},pipe:{toState:'expectTransform'},dot:{toState:'traverse'},question:{toState:'ternaryMid',handler:d.ternaryStart}},completable:!0},expectTransform:{tokenTypes:{identifier:{toState:'postTransform',handler:d.transform}}},expectObjKey:{tokenTypes:{identifier:{toState:'expectKeyValSep',handler:d.objKey},closeCurl:{toState:'expectBinOp'}}},expectKeyValSep:{tokenTypes:{colon:{toState:'objVal'}}},postTransform:{tokenTypes:{openParen:{toState:'argVal'},binaryOp:{toState:'expectOperand'},dot:{toState:'traverse'},openBracket:{toState:'filter'},pipe:{toState:'expectTransform'}},completable:!0},postTransformArgs:{tokenTypes:{binaryOp:{toState:'expectOperand'},dot:{toState:'traverse'},openBracket:{toState:'filter'},pipe:{toState:'expectTransform'}},completable:!0},identifier:{tokenTypes:{binaryOp:{toState:'expectOperand'},dot:{toState:'traverse'},openBracket:{toState:'filter'},pipe:{toState:'expectTransform'},question:{toState:'ternaryMid',handler:d.ternaryStart}},completable:!0},traverse:{tokenTypes:{identifier:{toState:'identifier'}}},filter:{subHandler:d.filter,endStates:{closeBracket:'identifier'}},subExpression:{subHandler:d.subExpression,endStates:{closeParen:'expectBinOp'}},argVal:{subHandler:d.argVal,endStates:{comma:'argVal',closeParen:'postTransformArgs'}},objVal:{subHandler:d.objVal,endStates:{comma:'expectObjKey',closeCurl:'expectBinOp'}},arrayVal:{subHandler:d.arrayVal,endStates:{comma:'arrayVal',closeBracket:'expectBinOp'}},ternaryMid:{subHandler:d.ternaryMid,endStates:{colon:'ternaryEnd'}},ternaryEnd:{subHandler:d.ternaryEnd,completable:!0}}},62:function(a,b){b.elements={".":{type:'dot'},"[":{type:'openBracket'},"]":{type:'closeBracket'},"|":{type:'pipe'},"{":{type:'openCurl'},"}":{type:'closeCurl'},":":{type:'colon'},",":{type:'comma'},"(":{type:'openParen'},")":{type:'closeParen'},"?":{type:'question'},"+":{type:'binaryOp',precedence:30,eval:function(a,b){return a+b}},"-":{type:'binaryOp',precedence:30,eval:function(a,b){return a-b}},"*":{type:'binaryOp',precedence:40,eval:function(a,b){return a*b}},"/":{type:'binaryOp',precedence:40,eval:function(a,b){return a/b}},"//":{type:'binaryOp',precedence:40,eval:function(a,b){return Math.floor(a/b)}},"%":{type:'binaryOp',precedence:50,eval:function(a,b){return a%b}},"^":{type:'binaryOp',precedence:50,eval:function(a,b){return Math.pow(a,b)}},"==":{type:'binaryOp',precedence:20,eval:function(a,b){return a==b}},"!=":{type:'binaryOp',precedence:20,eval:function(a,b){return a!=b}},">":{type:'binaryOp',precedence:20,eval:function(a,b){return a>b}},">=":{type:'binaryOp',precedence:20,eval:function(a,b){return a>=b}},"<":{type:'binaryOp',precedence:20,eval:function(a,b){return a<b}},"<=":{type:'binaryOp',precedence:20,eval:function(a,b){return a<=b}},"&&":{type:'binaryOp',precedence:10,eval:function(a,b){return a&&b}},"||":{type:'binaryOp',precedence:10,eval:function(a,b){return a||b}},in:{type:'binaryOp',precedence:20,eval:function(a,b){return'string'==typeof b?-1!==b.indexOf(a):!!Array.isArray(b)&&b.some(function(b){return b==a})}},"!":{type:'unaryOp',precedence:Infinity,eval:function(a){return!a}}}}});this.EXPORTED_SYMBOLS = ["mozjexl"];
\ No newline at end of file