MyCrypto/common/vendor/safe-t-connect.js

1013 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-ignore */
/* prettier-ignore */
/**
* (C) 2017 SatoshiLabs
* (C) 2018 Archos S.A.
*
* GPLv3
*/
var SAFE_T_CONNECT_VERSION = 4;
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
var HD_HARDENED = 0x80000000;
// react sometimes adds some other parameters that should not be there
function _fwStrFix(obj, fw) {
if (typeof fw === 'string') {
obj.requiredFirmware = fw;
}
return obj;
}
'use strict';
var chrome = window.chrome;
var IS_CHROME_APP = chrome && chrome.app && chrome.app.window;
var ERR_TIMED_OUT = 'Loading timed out';
var ERR_WINDOW_CLOSED = 'Window closed';
var ERR_WINDOW_BLOCKED = 'Window blocked';
var ERR_ALREADY_WAITING = 'Already waiting for a response';
var ERR_CHROME_NOT_CONNECTED = 'Internal Chrome popup is not responding.';
var DISABLE_LOGIN_BUTTONS = window.SAFE_T_DISABLE_LOGIN_BUTTONS || false;
var CHROME_URL = window.SAFE_T_CHROME_URL || './chrome/wrapper.html';
var POPUP_ORIGIN = window.SAFE_T_POPUP_ORIGIN || 'https://connect.safe-t.io';
var POPUP_PATH = window.SAFE_T_POPUP_PATH || POPUP_ORIGIN + '/' + SAFE_T_CONNECT_VERSION;
var POPUP_URL = window.SAFE_T_POPUP_URL || POPUP_PATH + '/popup/popup.html?v=' + new Date().getTime();
var POPUP_INIT_TIMEOUT = 15000;
/**
* Public API.
*/
function SafeTConnect() {
var manager = new PopupManager();
/**
* Popup errors.
*/
this.ERR_TIMED_OUT = ERR_TIMED_OUT;
this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED;
this.ERR_WINDOW_BLOCKED = ERR_WINDOW_BLOCKED;
this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING;
this.ERR_CHROME_NOT_CONNECTED = ERR_CHROME_NOT_CONNECTED;
/**
* Open the popup for further communication. All API functions open the
* popup automatically, but if you need to generate some parameters
* asynchronously, use `open` first to avoid popup blockers.
* @param {function(?Error)} callback
*/
this.open = function (callback) {
var onchannel = function (result) {
if (result instanceof Error) {
callback(result);
} else {
callback();
}
};
manager.waitForChannel(onchannel);
};
/**
* Close the opened popup, if any.
*/
this.close = function () { manager.close(); };
/**
* Enable or disable closing the opened popup after a successful call.
* @param {boolean} value
*/
this.closeAfterSuccess = function (value) { manager.closeAfterSuccess = value; };
/**
* Enable or disable closing the opened popup after a failed call.
* @param {boolean} value
*/
this.closeAfterFailure = function (value) { manager.closeAfterFailure = value; };
/**
* Set bitcore server
* @param {string|Array<string>} value
*/
this.setBitcoreURLS = function(value) {
if (typeof value === 'string') {
manager.bitcoreURLS = [ value ];
}else if (value instanceof Array) {
manager.bitcoreURLS = value;
}
}
/**
* Set currency. Human readable coin name
* @param {string|Array<string>} value
*/
this.setCurrency = function(value) {
if (typeof value === 'string') {
manager.currency = value;
}
}
/**
* Set currency units (mBTC, BTC)
* @param {string|Array<string>} value
*/
this.setCurrencyUnits = function(value) {
if (typeof value === 'string') {
manager.currencyUnits = value;
}
}
/**
* Set coin info json url
* @param {string|Array<string>} value
*/
this.setCoinInfoURL = function(value) {
if (typeof value === 'string') {
manager.coinInfoURL = value;
}
}
/**
* Set max. limit for account discovery
* @param {number} value
*/
this.setAccountDiscoveryLimit = function(value) {
if(!isNaN(value))
manager.accountDiscoveryLimit = value;
}
/**
* Set max. gap for account discovery
* @param {number} value
*/
this.setAccountDiscoveryGapLength = function(value) {
if(!isNaN(value))
manager.accountDiscoveryGapLength = value;
}
/**
* Set discovery BIP44 coin type
* @param {number} value
*/
this.setAccountDiscoveryBip44CoinType = function(value) {
if(!isNaN(value))
manager.accountDiscoveryBip44CoinType = value;
}
/**
* @typedef XPubKeyResult
* @param {boolean} success
* @param {?string} error
* @param {?string} xpubkey serialized extended public key
* @param {?string} path BIP32 serializd path of the key
*/
/**
* Load BIP32 extended public key by path.
*
* Path can be specified either in the string form ("m/44'/1/0") or as
* raw integer array. In case you omit the path, user is asked to select
* a BIP32 account to export, and the result contains m/44'/0'/x' node
* of the account.
*
* @param {?(string|array<number>)} path
* @param {function(XPubKeyResult)} callback
* @param {?(string|array<number>)} requiredFirmware
*/
this.getXPubKey = function (path, callback, requiredFirmware) {
if (typeof path === 'string') {
path = parseHDPath(path);
}
manager.sendWithChannel(_fwStrFix({
type: 'xpubkey',
path: path
}, requiredFirmware), callback);
};
this.getFreshAddress = function (callback, requiredFirmware) {
var wrapperCallback = function (result) {
if (result.success) {
callback({success: true, address: result.freshAddress});
} else {
callback(result);
}
}
manager.sendWithChannel(_fwStrFix({
type: 'accountinfo'
}, requiredFirmware), wrapperCallback);
}
this.getAccountInfo = function (input, callback, requiredFirmware) {
try {
manager.sendWithChannel(_fwStrFix({
type: 'accountinfo',
description: input
}, requiredFirmware), callback);
} catch(e) {
callback({success: false, error: e});
}
}
this.getAllAccountsInfo = function(callback, requiredFirmware){
try {
manager.sendWithChannel(_fwStrFix({
type: 'allaccountsinfo',
description: 'all'
}, requiredFirmware), callback);
} catch(e) {
callback({success: false, error: e});
}
}
this.getBalance = function (callback, requiredFirmware) {
manager.sendWithChannel(_fwStrFix({
type: 'accountinfo'
}, requiredFirmware), callback)
}
/**
* @typedef SignTxResult
* @param {boolean} success
* @param {?string} error
* @param {?string} serialized_tx serialized tx, in hex, including signatures
* @param {?array<string>} signatures array of input signatures, in hex
*/
/**
* Sign a transaction in the device and return both serialized
* transaction and the signatures.
*
* @param {array<TxInputType>} inputs
* @param {array<TxOutputType>} outputs
* @param {function(SignTxResult)} callback
* @param {?(string|array<number>)} requiredFirmware
*
* @see https://github.com/archos-safe-t/safe-t-common/blob/master/protob/types.proto
*/
this.signTx = function (inputs, outputs, callback, requiredFirmware, coin) {
manager.sendWithChannel(_fwStrFix({
type: 'signtx',
inputs: inputs,
outputs: outputs,
coin: coin
}, requiredFirmware), callback);
};
// new implementation with ethereum at beginnig
this.ethereumSignTx = function() {
this.signEthereumTx.apply(this, arguments);
}
// old fallback
this.signEthereumTx = function (
address_n,
nonce,
gas_price,
gas_limit,
to,
value,
data,
chain_id,
callback,
requiredFirmware
) {
if (requiredFirmware == null) {
requiredFirmware = '1.0.0';
}
if (typeof address_n === 'string') {
address_n = parseHDPath(address_n);
}
manager.sendWithChannel(_fwStrFix({
type: 'signethtx',
address_n: address_n,
nonce: nonce,
gas_price: gas_price,
gas_limit: gas_limit,
to: to,
value: value,
data: data,
chain_id: chain_id,
}, requiredFirmware), callback);
};
/**
* @typedef TxRecipient
* @param {number} amount the amount to send, in satoshis
* @param {string} address the address of the recipient
*/
/**
* Compose a transaction by doing BIP-0044 discovery, letting the user
* select an account, and picking UTXO by internal preferences.
* Transaction is then signed and returned in the same format as
* `signTx`. Only supports BIP-0044 accounts (single-signature).
*
* @param {array<TxRecipient>} recipients
* @param {function(SignTxResult)} callback
* @param {?(string|array<number>)} requiredFirmware
*/
this.composeAndSignTx = function (recipients, callback, requiredFirmware) {
manager.sendWithChannel(_fwStrFix({
type: 'composetx',
recipients: recipients
}, requiredFirmware), callback);
};
/**
* @typedef RequestLoginResult
* @param {boolean} success
* @param {?string} error
* @param {?string} public_key public key used for signing, in hex
* @param {?string} signature signature, in hex
*/
/**
* Sign a login challenge for active origin.
*
* @param {?string} hosticon
* @param {string} challenge_hidden
* @param {string} challenge_visual
* @param {string|function(RequestLoginResult)} callback
* @param {?(string|array<number>)} requiredFirmware
*
* @see https://github.com/archos-safe-t/safe-t-common/blob/master/protob/messages.proto
*/
this.requestLogin = function (
hosticon,
challenge_hidden,
challenge_visual,
callback,
requiredFirmware
) {
if (typeof callback === 'string') {
// special case for a login through <safe-t:login> button.
// `callback` is name of global var
callback = window[callback];
}
if (!callback) {
throw new TypeError('SafeTConnect: login callback not found');
}
manager.sendWithChannel(_fwStrFix({
type: 'login',
icon: hosticon,
challenge_hidden: challenge_hidden,
challenge_visual: challenge_visual
}, requiredFirmware), callback);
};
/**
* @typedef SignMessageResult
* @param {boolean} success
* @param {?string} error
* @param {?string} address address (in base58check)
* @param {?string} signature signature, in base64
*/
/**
* Sign a message
*
* @param {string|array} path
* @param {string} message to sign (ascii)
* @param {string|function(SignMessageResult)} callback
* @param {?string} opt_coin - (optional) name of coin (default Bitcoin)
* @param {?(string|array<number>)} requiredFirmware
*
*/
this.signMessage = function (
path,
message,
callback,
opt_coin,
requiredFirmware
) {
if (typeof path === 'string') {
path = parseHDPath(path);
}
if (!opt_coin) {
opt_coin = 'Bitcoin';
}
if (!callback) {
throw new TypeError('SafeTConnect: callback not found');
}
manager.sendWithChannel(_fwStrFix({
type: 'signmsg',
path: path,
message: message,
coin: opt_coin,
}, requiredFirmware), callback);
};
/**
* Sign an Ethereum message
*
* @param {string|array} path
* @param {string} message to sign (ascii)
* @param {string|function(SignMessageResult)} callback
* @param {?(string|array<number>)} requiredFirmware
*
*/
this.ethereumSignMessage = function (
path,
message,
callback,
requiredFirmware
) {
if (typeof path === 'string') {
path = parseHDPath(path);
}
if (!callback) {
throw new TypeError('SafeTConnect: callback not found');
}
manager.sendWithChannel(_fwStrFix({
type: 'signethmsg',
path: path,
message: message,
}, requiredFirmware), callback);
};
/**
* Verify message
*
* @param {string} address
* @param {string} signature (base64)
* @param {string} message (string)
* @param {string|function()} callback
* @param {?string} opt_coin - (optional) name of coin (default Bitcoin)
* @param {?(string|array<number>)} requiredFirmware
*
*/
this.verifyMessage = function (
address,
signature,
message,
callback,
opt_coin,
requiredFirmware
) {
if (!opt_coin) {
opt_coin = 'Bitcoin';
}
if (!callback) {
throw new TypeError('SafeTConnect: callback not found');
}
manager.sendWithChannel(_fwStrFix({
type: 'verifymsg',
address: address,
signature: signature,
message: message,
coin: {coin_name: opt_coin},
}, requiredFirmware), callback);
};
/**
* Verify ethereum message
*
* @param {string} address
* @param {string} signature (base64)
* @param {string} message (string)
* @param {string|function()} callback
* @param {?(string|array<number>)} requiredFirmware
*
*/
this.ethereumVerifyMessage = function (
address,
signature,
message,
callback,
requiredFirmware
) {
if (!callback) {
throw new TypeError('SafeTConnect: callback not found');
}
manager.sendWithChannel(_fwStrFix({
type: 'verifyethmsg',
address: address,
signature: signature,
message: message,
}, requiredFirmware), callback);
};
/**
* Symmetric key-value encryption
*
* @param {string|array} path
* @param {string} key to show on device display
* @param {string} value hexadecimal value, length a multiple of 16 bytes
* @param {boolean} encrypt / decrypt direction
* @param {boolean} ask_on_encrypt (should user confirm on encrypt?)
* @param {boolean} ask_on_decrypt (should user confirm on decrypt?)
* @param {string|function()} callback
* @param {?(string|array<number>)} requiredFirmware
*
*/
this.cipherKeyValue = function (
path,
key,
value,
encrypt,
ask_on_encrypt,
ask_on_decrypt,
callback,
requiredFirmware
) {
if (typeof path === 'string') {
path = parseHDPath(path);
}
if (typeof value !== 'string') {
throw new TypeError('SafeTConnect: Value must be a string');
}
if (!(/^[0-9A-Fa-f]*$/.test(value))) {
throw new TypeError('SafeTConnect: Value must be hexadecimal');
}
if (value.length % 32 !== 0) {
// 1 byte == 2 hex strings
throw new TypeError('SafeTConnect: Value length must be multiple of 16 bytes');
}
if (!callback) {
throw new TypeError('SafeTConnect: callback not found');
}
manager.sendWithChannel(_fwStrFix({
type: 'cipherkeyvalue',
path: path,
key: key,
value: value,
encrypt: !!encrypt,
ask_on_encrypt: !!ask_on_encrypt,
ask_on_decrypt: !!ask_on_decrypt
}, requiredFirmware), callback);
};
this.nemGetAddress = function (
address_n,
network,
callback,
requiredFirmware
) {
if (requiredFirmware == null) {
requiredFirmware = '1.0.0';
}
if (typeof address_n === 'string') {
address_n = parseHDPath(address_n);
}
manager.sendWithChannel(_fwStrFix({
type: 'nemGetAddress',
address_n: address_n,
network: network,
}, requiredFirmware), callback);
}
this.nemSignTx = function (
address_n,
transaction,
callback,
requiredFirmware
) {
if (requiredFirmware == null) {
requiredFirmware = '1.0.0';
}
if (typeof address_n === 'string') {
address_n = parseHDPath(address_n);
}
manager.sendWithChannel(_fwStrFix({
type: 'nemSignTx',
address_n: address_n,
transaction: transaction
}, requiredFirmware), callback);
}
this.pushTransaction = function (
rawTx,
callback
) {
if (!(/^[0-9A-Fa-f]*$/.test(rawTx))) {
throw new TypeError('SafeTConnect: Transaction must be hexadecimal');
}
if (!callback) {
throw new TypeError('SafeTConnect: callback not found');
}
manager.sendWithChannel({
type: 'pushtx',
rawTx: rawTx,
}, callback);
}
/**
* Display address on device
*
* @param {array} address
* @param {string} coin
* @param {boolean} segwit
* @param {?(string|array<number>)} requiredFirmware
*
*/
this.getAddress = function (address, coin, segwit, callback, requiredFirmware) {
if (typeof address === 'string') {
address = parseHDPath(address);
}
manager.sendWithChannel(_fwStrFix({
type: 'getaddress',
address_n: address,
coin: coin,
segwit: segwit
}, requiredFirmware), callback);
}
/**
* Display ethereum address on device
*
* @param {array} address
* @param {?(string|array<number>)} requiredFirmware
*
*/
this.ethereumGetAddress = function (address, callback, requiredFirmware) {
if (typeof address === 'string') {
address = parseHDPath(address);
}
manager.sendWithChannel(_fwStrFix({
type: 'ethgetaddress',
address_n: address,
}, requiredFirmware), callback);
}
var LOGIN_CSS =
'<style>@import url("@connect_path@/login_buttons.css")</style>';
var LOGIN_ONCLICK =
'SafeTConnect.requestLogin('
+ "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'"
+ ')';
var LOGIN_HTML =
'<div id="safe-tconnect-wrapper">'
+ ' <a id="safe-tconnect-button" onclick="' + LOGIN_ONCLICK + '">'
+ ' <span id="safe-tconnect-icon"></span>'
+ ' <span id="safe-tconnect-text">@text@</span>'
+ ' </a>'
+ ' <span id="safe-tconnect-info">'
+ ' <a id="safe-tconnect-infolink" href="https://www.archos.com/fr/products/crypto/archos_safetmini/index.html"'
+ ' target="_blank">What is a Safe-T mini?</a>'
+ ' </span>'
+ '</div>';
/**
* Find <safe-t:login> elements and replace them with login buttons.
* It's not required to use these special elements, feel free to call
* `SafeTConnect.requestLogin` directly.
*/
this.renderLoginButtons = function () {
var elements = document.getElementsByTagName('safe-t:login');
for (var i = 0; i < elements.length; i++) {
var e = elements[i];
var text = e.getAttribute('text') || 'Sign in with your SAFE_T';
var callback = e.getAttribute('callback') || '';
var hosticon = e.getAttribute('icon') || '';
var challenge_hidden = e.getAttribute('challenge_hidden') || '';
var challenge_visual = e.getAttribute('challenge_visual') || '';
// it's not valid to put markup into attributes, so let users
// supply a raw text and make Safe-T mini bold
text = text.replace('SAFE_T', '<strong>Safe-T mini</strong>');
e.outerHTML =
(LOGIN_CSS + LOGIN_HTML)
.replace('@text@', text)
.replace('@callback@', callback)
.replace('@hosticon@', hosticon)
.replace('@challenge_hidden@', challenge_hidden)
.replace('@challenge_visual@', challenge_visual)
.replace('@connect_path@', POPUP_PATH);
}
};
}
/*
* `getXPubKey()`
*/
function parseHDPath(string) {
return string
.toLowerCase()
.split('/')
.filter(function (p) { return p !== 'm'; })
.map(function (p) {
var hardened = false;
if (p[p.length - 1] === "'") {
hardened = true;
p = p.substr(0, p.length - 1);
}
if (isNaN(p)) {
throw new Error('Not a valid path.');
}
var n = parseInt(p);
if (hardened) { // hardened index
n = (n | 0x80000000) >>> 0;
}
return n;
});
}
/*
* Popup management
*/
function ChromePopup(url, name, width, height) {
var left = (screen.width - width) / 2;
var top = (screen.height - height) / 2;
var opts = {
id: name,
innerBounds: {
width: width,
height: height,
left: left,
top: top
}
};
var closed = function () {
if (this.onclose) {
this.onclose(false); // never report as blocked
}
}.bind(this);
var opened = function (w) {
this.window = w;
this.window.onClosed.addListener(closed);
}.bind(this);
chrome.app.window.create(url, opts, opened);
this.name = name;
this.window = null;
this.onclose = null;
}
function ChromeChannel(popup, waiting) {
var port = null;
var respond = function (data) {
if (waiting) {
var w = waiting;
waiting = null;
w(data);
}
};
var setup = function (p) {
if (p.name === popup.name) {
port = p;
port.onMessage.addListener(respond);
chrome.runtime.onConnect.removeListener(setup);
}
};
chrome.runtime.onConnect.addListener(setup);
this.respond = respond;
this.close = function () {
chrome.runtime.onConnect.removeListener(setup);
port.onMessage.removeListener(respond);
port.disconnect();
port = null;
};
this.send = function (value, callback) {
if (waiting === null) {
waiting = callback;
if (port) {
port.postMessage(value);
} else {
throw new Error(ERR_CHROME_NOT_CONNECTED);
}
} else {
throw new Error(ERR_ALREADY_WAITING);
}
};
}
function Popup(url, origin, name, width, height) {
var left = (screen.width - width) / 2;
var top = (screen.height - height) / 2;
var opts =
'width=' + width +
',height=' + height +
',left=' + left +
',top=' + top +
',menubar=no' +
',toolbar=no' +
',location=no' +
',personalbar=no' +
',status=no';
var w = window.open(url, name, opts);
var interval;
var blocked = w.closed;
var iterate = function () {
if (w.closed) {
clearInterval(interval);
if (this.onclose) {
this.onclose(blocked);
}
}
}.bind(this);
interval = setInterval(iterate, 100);
this.window = w;
this.origin = origin;
this.onclose = null;
}
function Channel(popup, waiting) {
var respond = function (data) {
if (waiting) {
var w = waiting;
waiting = null;
w(data);
}
};
var receive = function (event) {
var org1 = event.origin.match(/^.+\:\/\/[^\/]+/)[0];
var org2 = popup.origin.match(/^.+\:\/\/[^\/]+/)[0];
//if (event.source === popup.window && event.origin === popup.origin) {
if (event.source === popup.window && org1 === org2) {
respond(event.data);
}
};
window.addEventListener('message', receive);
this.respond = respond;
this.close = function () {
window.removeEventListener('message', receive);
};
this.send = function (value, callback) {
if (waiting === null) {
waiting = callback;
popup.window.postMessage(value, popup.origin);
} else {
throw new Error(ERR_ALREADY_WAITING);
}
};
}
function ConnectedChannel(p) {
var ready = function () {
clearTimeout(this.timeout);
this.popup.onclose = null;
this.ready = true;
this.onready();
}.bind(this);
var closed = function (blocked) {
clearTimeout(this.timeout);
this.channel.close();
if (blocked) {
this.onerror(new Error(ERR_WINDOW_BLOCKED));
} else {
this.onerror(new Error(ERR_WINDOW_CLOSED));
}
}.bind(this);
var timedout = function () {
this.popup.onclose = null;
if (this.popup.window) {
this.popup.window.close();
}
this.channel.close();
this.onerror(new Error(ERR_TIMED_OUT));
}.bind(this);
if (IS_CHROME_APP) {
this.popup = new ChromePopup(p.chromeUrl, p.name, p.width, p.height);
this.channel = new ChromeChannel(this.popup, ready);
} else {
this.popup = new Popup(p.url, p.origin, p.name, p.width, p.height);
this.channel = new Channel(this.popup, ready);
}
this.timeout = setTimeout(timedout, POPUP_INIT_TIMEOUT);
this.popup.onclose = closed;
this.ready = false;
this.onready = null;
this.onerror = null;
}
function PopupManager() {
var cc = null;
var closed = function () {
cc.channel.respond(new Error(ERR_WINDOW_CLOSED));
cc.channel.close();
cc = null;
};
var open = function (callback) {
cc = new ConnectedChannel({
name: 'safe-t-connect',
width: 600,
height: 500,
origin: POPUP_ORIGIN,
path: POPUP_PATH,
url: POPUP_URL,
chromeUrl: CHROME_URL
});
cc.onready = function () {
cc.popup.onclose = closed;
callback(cc.channel);
};
cc.onerror = function (error) {
cc = null;
callback(error);
};
}.bind(this);
this.closeAfterSuccess = true;
this.closeAfterFailure = true;
this.close = function () {
if (cc && cc.popup.window) {
cc.popup.window.close();
}
};
this.waitForChannel = function (callback) {
if (cc) {
if (cc.ready) {
callback(cc.channel);
} else {
callback(new Error(ERR_ALREADY_WAITING));
}
} else {
try {
open(callback);
} catch (e) {
callback(new Error(ERR_WINDOW_BLOCKED));
}
}
};
this.sendWithChannel = function (message, callback) {
message.bitcoreURLS = this.bitcoreURLS || null;
message.accountDiscoveryLimit = this.accountDiscoveryLimit || null;
message.accountDiscoveryGapLength = this.accountDiscoveryGapLength || null;
message.accountDiscoveryBip44CoinType = this.accountDiscoveryBip44CoinType || null;
var respond = function (response) {
var succ = response.success && this.closeAfterSuccess;
var fail = !response.success && this.closeAfterFailure;
if (succ || fail) {
this.close();
}
callback(response);
}.bind(this);
var onresponse = function (response) {
if (response instanceof Error) {
var error = response;
respond({ success: false, error: error.message });
} else {
respond(response);
}
};
var onchannel = function (channel) {
if (channel instanceof Error) {
var error = channel;
respond({ success: false, error: error.message });
} else {
channel.send(message, onresponse);
}
};
this.waitForChannel(onchannel);
};
}
var connect = new SafeTConnect();
module.exports = connect;