555 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			555 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* global Promise */
 | |
| ;(function (exports) {
 | |
|   'use strict';
 | |
| 
 | |
|   var OAUTH3 = exports.OAUTH3 = {
 | |
|     utils: {
 | |
|       clientUri: function (location) {
 | |
|         return OAUTH3.utils.uri.normalize(location.host + location.pathname);
 | |
|       }
 | |
|     , atob: function (base64) {
 | |
|         return (exports.atob || require('atob'))(base64);
 | |
|       }
 | |
|     , _urlSafeBase64ToBase64: function (b64) {
 | |
|         // URL-safe Base64 to Base64
 | |
|         // https://en.wikipedia.org/wiki/Base64
 | |
|         // https://gist.github.com/catwell/3046205
 | |
|         var mod = b64.length % 4;
 | |
|         if (2 === mod) { b64 += '=='; }
 | |
|         if (3 === mod) { b64 += '='; }
 | |
|         b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
 | |
|         return b64;
 | |
|       }
 | |
|     , uri: {
 | |
|         normalize: function (uri) {
 | |
|           // tested with
 | |
|           //   example.com
 | |
|           //   example.com/
 | |
|           //   http://example.com
 | |
|           //   https://example.com/
 | |
|           return uri
 | |
|             .replace(/^(https?:\/\/)?/i, '')
 | |
|             .replace(/\/?$/, '')
 | |
|             ;
 | |
|         }
 | |
|       }
 | |
|     , url: {
 | |
|         normalize: function (url) {
 | |
|           // tested with
 | |
|           //   example.com
 | |
|           //   example.com/
 | |
|           //   http://example.com
 | |
|           //   https://example.com/
 | |
|           return url
 | |
|             .replace(/^(https?:\/\/)?/i, 'https://')
 | |
|             .replace(/\/?$/, '')
 | |
|             ;
 | |
|         }
 | |
|       }
 | |
|     , query: {
 | |
|         stringify: function (params) {
 | |
|           var qs = [];
 | |
| 
 | |
|           Object.keys(params).forEach(function (key) {
 | |
|             // TODO nullify instead?
 | |
|             if ('undefined' === typeof params[key]) {
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             if ('scope' === key) {
 | |
|               params[key] = OAUTH3.utils.scope.stringify(params[key]);
 | |
|             }
 | |
| 
 | |
|             qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
 | |
|           });
 | |
| 
 | |
|           return qs.join('&');
 | |
|         }
 | |
|       }
 | |
|     , scope: {
 | |
|         stringify: function (scope) {
 | |
|           if (Array.isArray(scope)) {
 | |
|             scope = scope.join(' ');
 | |
|           }
 | |
|           return scope;
 | |
|         }
 | |
|       }
 | |
|     , randomState: function () {
 | |
|         // TODO put in different file for browser vs node
 | |
|         try {
 | |
|           return Array.prototype.slice.call(
 | |
|             window.crypto.getRandomValues(new Uint8Array(16))
 | |
|           ).map(function (ch) { return (ch).toString(16); }).join('');
 | |
|         } catch(e) {
 | |
|           return OAUTH3.utils._insecureRandomState();
 | |
|         }
 | |
|       }
 | |
|     , _insecureRandomState: function () {
 | |
|         var i;
 | |
|         var ch;
 | |
|         var str;
 | |
|         // TODO use fisher-yates on 0..255 and select [0] 16 times
 | |
|         // [security] https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.5qx0bf95a
 | |
|         // https://github.com/v8/v8/blob/b0e4dce6091a8777bda80d962df76525dc6c5ea9/src/js/math.js#L135-L144
 | |
|         // Note: newer versions of v8 do not have this bug, but other engines may still
 | |
|         console.warn('[security] crypto.getRandomValues() failed, falling back to Math.random()');
 | |
|         str = '';
 | |
|         for (i = 0; i < 32; i += 1) {
 | |
|           ch = Math.round(Math.random() * 255).toString(16);
 | |
|           if (ch.length < 2) { ch = '0' + ch; }
 | |
|           str += ch;
 | |
|         }
 | |
|         return str;
 | |
|       }
 | |
|     }
 | |
|   , urls: {
 | |
|       discover: function (providerUri, opts) {
 | |
|         if (!providerUri) {
 | |
|           throw new Error("cannot discover without providerUri");
 | |
|         }
 | |
|         if (!opts.client_id) {
 | |
|           throw new Error("cannot discover without options.client_id");
 | |
|         }
 | |
|         var clientId = OAUTH3.utils.url.normalize(opts.client_id || opts.client_uri);
 | |
|         providerUri = OAUTH3.utils.url.normalize(providerUri);
 | |
| 
 | |
|         var params = {
 | |
|           action: 'directives'
 | |
|         , state: OAUTH3.utils.randomState()
 | |
|         , redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/')
 | |
|         , response_type: 'rpc'
 | |
|         , _method: 'GET'
 | |
|         , _pathname: '.well-known/oauth3/directives.json'
 | |
|         , debug: opts.debug || undefined
 | |
|         };
 | |
| 
 | |
|         var result = {
 | |
|           url: providerUri + '/.well-known/oauth3/#/?' + OAUTH3.utils.query.stringify(params)
 | |
|         , state: params.state
 | |
|         , method: 'GET'
 | |
|         , query: params
 | |
|         };
 | |
| 
 | |
|         return result;
 | |
|       }
 | |
|     }
 | |
|   , hooks: {
 | |
|       directives: {
 | |
|         get: function (providerUri) {
 | |
|           providerUri = OAUTH3.utils.uri.normalize(providerUri);
 | |
|           console.warn('[Warn] You should implement: OAUTH3.hooks.directives.get = function (providerUri) { return directives; }');
 | |
|           if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
 | |
|           return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}');
 | |
|         }
 | |
|       , set: function (providerUri, directives) {
 | |
|           providerUri = OAUTH3.utils.uri.normalize(providerUri);
 | |
|           console.warn('[Warn] You should implement: OAUTH3.hooks.directives.set = function (providerUri, directives) { return directives; }');
 | |
|           console.warn(directives);
 | |
|           if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
 | |
|           window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
 | |
|           OAUTH3.hooks.directives._cache[providerUri] = directives;
 | |
|           return directives;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   , discover: function (providerUri, opts) {
 | |
|       if (!providerUri) {
 | |
|         throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri);
 | |
|       }
 | |
| 
 | |
|       return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives.get(providerUri)).then(function (directives) {
 | |
|         if (directives && directives.issuer) {
 | |
|           return directives;
 | |
|         }
 | |
|         return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) {
 | |
|           directives.issuer = directives.issuer || OAUTH3.utils.url.normalize(providerUri);
 | |
|           // OAUTH3.PromiseA.resolve() is taken care of because this is wrapped
 | |
|           return OAUTH3.hooks.directives.set(providerUri, directives);
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|     // this is the browser version
 | |
|   , _discoverHelper: function (providerUri, opts) {
 | |
|       return OAUTH3._browser.discover(providerUri, opts);
 | |
|     }
 | |
|   , request: function (preq) {
 | |
|       return OAUTH3._browser.request(preq);
 | |
|     }
 | |
|   , implicitGrant: function(providerUri, opts) {
 | |
|       var promise;
 | |
| 
 | |
|       if (opts.broker) {
 | |
|         promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts);
 | |
|       }
 | |
|       else {
 | |
|         promise = OAUTH3._implicitGrant(providerUri, opts);
 | |
|       }
 | |
| 
 | |
|       return promise.then(function (tokens) {
 | |
|         return OAUTH3.hooks.refreshSession(
 | |
|           opts.session || {
 | |
|             provider_uri: providerUri
 | |
|           , client_id: opts.client_id
 | |
|           , client_uri: opts.client_uri || opts.clientUri
 | |
|           }
 | |
|         , tokens
 | |
|         );
 | |
|       });
 | |
|     }
 | |
|   , _discoverThenImplicitGrant: function(providerUri, opts) {
 | |
|       opts.windowType = opts.windowType || 'popup';
 | |
|       return OAUTH3._discover(providerUri, opts).then(function (directives) {
 | |
|         return OAUTH3._implicitGrant(directives, opts).then(function (tokens) {
 | |
|           OAUTH3._browser.closeFrame(tokens.state || opts._state);
 | |
|           opts._state = undefined;
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|   , _discover: function(providerUri, opts) {
 | |
|       providerUri = OAUTH3.utils.url.normalize(providerUri);
 | |
| 
 | |
|       if (providerUri.match(OAUTH3._browser.window.location.hostname)) {
 | |
|         console.warn("It looks like you're a provider checking for your own directive,"
 | |
|           + " so we we're just gonna use"
 | |
|           + " OAUTH3.request({ method: 'GET', url: '.well-known/oauth3/directive.json' })");
 | |
|         return OAUTH3.request({
 | |
|           method: 'GET'
 | |
|         , url: OAUTH3.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json'
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       if (!(opts.client_id || opts.client_uri).match(OAUTH3._browser.window.location.hostname)) {
 | |
|         console.warn("It looks like your client_id doesn't match your current window..."
 | |
|           + " this probably won't end well");
 | |
|         console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname);
 | |
|       }
 | |
| 
 | |
|       var discReq = OAUTH3.urls.discover(
 | |
|         providerUri
 | |
|       , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
 | |
|         , windowType: opts.broker && opts.windowType || 'background'
 | |
|         , broker: opts.broker
 | |
|         , debug: opts.debug
 | |
|         }
 | |
|       );
 | |
|       opts._state = discReq.state;
 | |
|       //var discReq = OAUTH3.urls.discover(providerUri, opts);
 | |
| 
 | |
|       // hmm... we're gonna need a broker for this since switching windows is distracting,
 | |
|       // popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS
 | |
|       // eventually it should be the browser (and postMessage may be a viable option now), but whatever...
 | |
| 
 | |
|       // TODO allow postMessage from providerUri in addition to callback
 | |
|       // TODO allow node to open a desktop browser window
 | |
|       return OAUTH3._browser.frameRequest(
 | |
|         discReq.url
 | |
|       , discReq.state
 | |
|       , { windowType: opts.windowType
 | |
|         , reuseWindow: opts.broker && '-broker'
 | |
|         , debug: opts.debug
 | |
|         }
 | |
|       ).then(function (params) {
 | |
|         // discWin.child.close()
 | |
|         // TODO params should have response_type indicating json, binary, etc
 | |
|         var directives = JSON.parse(OAUTH3.utils.atob(OAUTH3.utils.urlSafeBase64ToBase64(params.result || params.directives)));
 | |
|         return OAUTH3.hooks.directives.set(providerUri, directives);
 | |
|       });
 | |
|     }
 | |
|   , _implicitGrant: function(providerUri, opts) {
 | |
|       // TODO this may need to be synchronous for browser security policy
 | |
|       return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives.get(providerUri)).then(function (directives) {
 | |
|         // Do some stuff
 | |
|         var authReq = OAUTH3.urls.implicitGrant(
 | |
|           directives
 | |
|         , { redirect_uri: opts.redirect_uri
 | |
|           , client_id: opts.client_id || opts.client_uri
 | |
|           , client_uri: opts.client_uri || opts.client_id
 | |
|           , state: opts._state
 | |
|           , debug: opts.debug
 | |
|           }
 | |
|         );
 | |
| 
 | |
|         if (opts.debug) {
 | |
|           window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!");
 | |
|         }
 | |
| 
 | |
|         return new OAUTH3.PromiseA(function (resolve, reject) {
 | |
|           return OAUTH3._browser.frameRequest(
 | |
|             authReq.url
 | |
|           , authReq.state // state should recycle params
 | |
|           , { windowType: opts.windowType
 | |
|             , reuseWindow: opts.broker && '-broker'
 | |
|             , debug: opts.debug
 | |
|             }
 | |
|           ).then(function (tokens) {
 | |
|             if (tokens.error) {
 | |
|               return reject(OAUTH3.utils._formatError(tokens.error));
 | |
|             }
 | |
| 
 | |
|             OAUTH3._browser.closeFrame(authReq.state, { debug: opts.debug || tokens.debug });
 | |
| 
 | |
|             return tokens;
 | |
|           });
 | |
|         });
 | |
|       });
 | |
|     }
 | |
| 
 | |
| 
 | |
|     //
 | |
|     // Let the Code Waste begin!!
 | |
|     //
 | |
|   , _browser: {
 | |
|       window: window
 | |
|       // TODO we don't need to include this if we're using jQuery or angular
 | |
|     , request: function (preq, _sys) {
 | |
|         return new OAUTH3.PromiseA(function (resolve, reject) {
 | |
|           var xhr;
 | |
|           try {
 | |
|             xhr = new XMLHttpRequest(_sys);
 | |
|           } catch(e) {
 | |
|             xhr = new XMLHttpRequest();
 | |
|           }
 | |
|           xhr.onreadystatechange = function () {
 | |
|             console.error('state change');
 | |
|             var data;
 | |
|             if (xhr.readyState !== XMLHttpRequest.DONE) {
 | |
|               // nothing to do here
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             if (xhr.status !== 200) {
 | |
|               reject(new Error('bad status code: ' + xhr.status));
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             try {
 | |
|               data = JSON.parse(xhr.responseText);
 | |
|             } catch(e) {
 | |
|               data = xhr.responseText;
 | |
|             }
 | |
| 
 | |
|             resolve({
 | |
|               request: xhr
 | |
|             , data: data
 | |
|             , status: xhr.status
 | |
|             });
 | |
|           };
 | |
|           xhr.open(preq.method, preq.url, true);
 | |
|           var headers = preq.headers || {};
 | |
|           Object.keys(headers).forEach(function (key) {
 | |
|             xhr.setRequestHeader(key, headers[key]);
 | |
|           });
 | |
|           xhr.send();
 | |
|         });
 | |
|       }
 | |
|     , discover: function (providerUri, opts) {
 | |
|         opts = opts || {};
 | |
|         //opts.debug = true;
 | |
|         providerUri = OAUTH3.utils.url.normalize(providerUri);
 | |
|         if (providerUri.match(OAUTH3._browser.window.location.hostname)) {
 | |
|           console.warn("It looks like you're a provider checking for your own directive,"
 | |
|             + " so we we're just gonna use OAUTH3.request({ method: 'GET', url: '.well-known/oauth3/directive.json' })");
 | |
|           return OAUTH3.request({
 | |
|             method: 'GET'
 | |
|           , url: OAUTH3.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json'
 | |
|           });
 | |
|         }
 | |
| 
 | |
|         if (!(opts.client_id || opts.client_uri).match(OAUTH3._browser.window.location.hostname)) {
 | |
|           console.warn("It looks like your client_id doesn't match your current window... this probably won't end well");
 | |
|           console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname);
 | |
|         }
 | |
|         var discObj = OAUTH3.urls.discover(
 | |
|           providerUri
 | |
|         , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)), debug: opts.debug }
 | |
|         );
 | |
| 
 | |
|         // TODO ability to reuse iframe instead of closing
 | |
|         return OAUTH3._browser._iframe.insert(discObj.url, discObj.state, opts).then(function (params) {
 | |
|           OAUTH3._browser.closeFrame(discObj.state, { debug: opts.debug || params.debug });
 | |
|           if (params.error) {
 | |
|             return OAUTH3.utils._formatError(providerUri, params.error);
 | |
|           }
 | |
|           var directives = JSON.parse(OAUTH3.utils.atob(OAUTH3.utils._urlSafeBase64ToBase64(params.result || params.directives)));
 | |
|           return directives;
 | |
|         }, function (err) {
 | |
|           OAUTH3._browser.closeFrame(discObj.state, { debug: opts.debug || err.debug });
 | |
|           return OAUTH3.PromiseA.reject(err);
 | |
|         });
 | |
|       }
 | |
|     , frameRequest: function (url, state, opts) {
 | |
|         var previousFrame = OAUTH3._browser._frames[state];
 | |
| 
 | |
|         if (!opts.windowType) {
 | |
|           opts.windowType = 'popup';
 | |
|         }
 | |
| 
 | |
|         opts = opts || {};
 | |
|         if (opts.debug) {
 | |
|           opts.timeout = opts.timeout || 15 * 60 * 1000;
 | |
|         }
 | |
| 
 | |
|         return new OAUTH3.PromiseA(function (resolve, reject) {
 | |
|           var tok;
 | |
| 
 | |
|           function cleanup() {
 | |
|             delete window['--oauth3-callback-' + state];
 | |
|             clearTimeout(tok);
 | |
|             tok = null;
 | |
|           }
 | |
| 
 | |
|           window['--oauth3-callback-' + state] = function (params) {
 | |
|             resolve(params);
 | |
|             cleanup();
 | |
|           };
 | |
| 
 | |
|           tok = setTimeout(function () {
 | |
|             var err = new Error("the iframe request did not complete within 15 seconds");
 | |
|             err.code = "E_TIMEOUT";
 | |
|             reject(err);
 | |
|             cleanup();
 | |
|           }, opts.timeout || 15 * 1000);
 | |
| 
 | |
|           if ('background' === opts.windowType) {
 | |
|             if (previousFrame) {
 | |
|               previousFrame.location = url;
 | |
|               //promise = previousFrame.promise;
 | |
|             }
 | |
|             else {
 | |
|               OAUTH3._browser._iframe.insert(url, state, opts);
 | |
|             }
 | |
|           } else if ('popup' === opts.windowType) {
 | |
|             if (previousFrame) {
 | |
|               previousFrame.location = url;
 | |
|               if (opts.debug) {
 | |
|                 previousFrame.focus();
 | |
|               }
 | |
|             }
 | |
|             else {
 | |
|               OAUTH3._browser.frame.open(url, state, opts);
 | |
|             }
 | |
|           } else if ('inline' === opts.windowType) {
 | |
|             // callback function will never execute and would need to redirect back to current page
 | |
|             // rather than the callback.html
 | |
|             url += '&original_url=' + OAUTH3._browser.window.location.href;
 | |
|             OAUTH3._browser.window.location = url;
 | |
|             //promise = OAUTH3.PromiseA.resolve({ url: url });
 | |
|             return;
 | |
|           } else {
 | |
|             throw new Error("login framing method options.windowType="
 | |
|               + opts.windowType + " not type yet implemented");
 | |
|           }
 | |
| 
 | |
|         }).then(function (params) {
 | |
|           var err;
 | |
| 
 | |
|           if (params.error || params.error_description) {
 | |
|             err = new Error(params.error_description || "Unknown response error");
 | |
|             err.code = params.error || "E_UKNOWN_ERROR";
 | |
|             err.params = params;
 | |
|             //_formatError
 | |
|             return OAUTH3.PromiseA.reject(err);
 | |
|           }
 | |
| 
 | |
|           return params;
 | |
|         });
 | |
|       }
 | |
|     , closeFrame: function (state, opts) {
 | |
|         function close() {
 | |
|           try {
 | |
|             OAUTH3._browser._frames[state].close();
 | |
|           } catch(e) {
 | |
|             try {
 | |
|               OAUTH3._browser._frames[state].remove();
 | |
|             } catch(e) {
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           delete OAUTH3._browser._frames[state];
 | |
|         }
 | |
| 
 | |
|         if (opts.debug) {
 | |
|           if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) {
 | |
|             close();
 | |
|           }
 | |
|         }
 | |
|         else {
 | |
|           close();
 | |
|         }
 | |
|       }
 | |
|     , _frames: {}
 | |
|     , iframe: {
 | |
|         insert: function (url, state, opts) {
 | |
|           // TODO hidden / non-hidden (via directive even)
 | |
|           var framesrc = '<iframe class="js-oauth3-iframe" src="' + url + '" ';
 | |
|           if (opts.debug) {
 | |
|             framesrc += ' width="800px" height="800px" style="opacity: 0.8;" frameborder="1"';
 | |
|           }
 | |
|           else {
 | |
|             framesrc += ' width="1px" height="1px" frameborder="0"';
 | |
|           }
 | |
|           framesrc += '></iframe>';
 | |
| 
 | |
|           var frame = OAUTH3._browser._frames[state] = OAUTH3._browser.window.document.createElement('div');
 | |
|           OAUTH3._browser._frames[state].innerHTML = framesrc;
 | |
|           OAUTH3._browser.window.document.body.appendChild(OAUTH3._browser._frames[state]);
 | |
| 
 | |
|           return frame;
 | |
|         }
 | |
|       }
 | |
|     , frame: {
 | |
|         open: function (url, state, opts) {
 | |
|           if (opts.debug) {
 | |
|             opts.timeout = opts.timeout || 15 * 60 * 1000;
 | |
|           }
 | |
| 
 | |
|           var promise = new OAUTH3.PromiseA(function (resolve, reject) {
 | |
|             var tok;
 | |
| 
 | |
|             function cleanup() {
 | |
|               clearTimeout(tok);
 | |
|               tok = null;
 | |
|               delete window['--oauth3-callback-' + state];
 | |
|               // close is done later in case the window is reused or self-closes synchronously itself / by parent
 | |
|               // (probably won't ever happen, but that's a negotiable implementation detail)
 | |
|             }
 | |
| 
 | |
|             window['--oauth3-callback-' + state] = function (params) {
 | |
|               console.log('YOLO!!');
 | |
|               resolve(params);
 | |
|               cleanup();
 | |
|             };
 | |
| 
 | |
|             tok = setTimeout(function () {
 | |
|               var err = new Error("the windowed request did not complete within 3 minutes");
 | |
|               err.code = "E_TIMEOUT";
 | |
|               reject(err);
 | |
|               cleanup();
 | |
|             }, opts.timeout || 3 * 60 * 1000);
 | |
| 
 | |
|             setTimeout(function () {
 | |
|               if (!promise.child) {
 | |
|                 reject("TODO: open the iframe first and discover oauth3 directives before popup");
 | |
|                 cleanup();
 | |
|               }
 | |
|             }, 0);
 | |
|           });
 | |
| 
 | |
|           // TODO allow size changes (via directive even)
 | |
|           OAUTH3._browser._frames[state] = window.open(
 | |
|             url
 | |
|           , 'oauth3-login-' + (opts.reuseWindow || state)
 | |
|           , 'height=' + (opts.height || 720) + ',width=' + (opts.width || 620)
 | |
|           );
 | |
|           // TODO periodically garbage collect expired handlers from window object
 | |
|           return promise;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   };
 | |
|   if ('undefined' !== typeof Promise) {
 | |
|     OAUTH3.PromiseA = Promise;
 | |
|   }
 | |
| 
 | |
| }('undefined' !== typeof exports ? exports : window));
 |