docs: function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
    if (!is.string(url)) {
      throw new Error("WebAPI.initialize: Invalid server url");
    }
    if (!is.string(cdnUrl)) {
      throw new Error("WebAPI.initialize: Invalid cdnUrl");
    }
    if (!is.string(certificateAuthority)) {
      throw new Error("WebAPI.initialize: Invalid certificateAuthority");
    }
  
    // Thanks to function-hoisting, we can put this return statement before all of the
    //   below function definitions.
    return {
      connect
    };
  
    // Then we connect to the server with user-specific information. This is the only API
    //   exposed to the browser context, ensuring that it can't connect to arbitrary
    //   locations.
    function connect({ username: initialUsername, password: initialPassword }) {
      let username = initialUsername;
      let password = initialPassword;
  
      // Thanks, function hoisting!
      return {
        confirmCode,
        getAttachment,
        getAvatar,
        getDevices,
        getKeysForNumber,
        getMessageSocket,
        getMyKeys,
        getProfile,
        getProvisioningSocket,
        putAttachment,
        registerKeys,
        requestVerificationSMS,
        requestVerificationVoice,
        sendMessages,
        setSignedPreKey
      };
  
      function _ajax(param) {
        if (!param.urlParameters) {
          // eslint-disable-next-line no-param-reassign
          param.urlParameters = "";
        }
        return _outerAjax(null, {
          certificateAuthority,
          contentType: "application/json; charset=utf-8",
          data: param.jsonData && _jsonThing(param.jsonData),
          host: url,
          password,
          path: URL_CALLS[param.call] + param.urlParameters,
          proxyUrl,
          responseType: param.responseType,
          timeout: param.timeout,
          type: param.httpType,
          user: username,
          validateResponse: param.validateResponse
        }).catch(e => {
          const { code } = e;
          if (code === 200) {
            // happens sometimes when we get no response
            // (TODO: Fix server to return 204? instead)
            return null;
          }
          let message;
          switch (code) {
            case -1:
              message =
                "Failed to connect to the server, please check your network connection.";
              break;
            case 413:
              message = "Rate limit exceeded, please try again later.";
              break;
            case 403:
              message = "Invalid code, please try again.";
              break;
            case 417:
              // TODO: This shouldn't be a thing?, but its in the API doc?
              message = "Number already registered.";
              break;
            case 401:
              message =
                "Invalid authentication, most likely someone re-registered and invalidated our registration.";
              break;
            case 404:
              message = "Number is not registered.";
              break;
            default:
              message =
                "The server rejected our query, please file a bug report.";
          }
          e.message = message;
          throw e;
        });
      }
  
      function getProfile(number) {
        return _ajax({
          call: "profile",
          httpType: "GET",
          urlParameters: `/${number}`,
          responseType: "json"
        });
      }
  
      function getAvatar(path) {
        // Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
        //   attachment CDN, it uses our self-signed certificate, so we pass it in.
        return _outerAjax(`${cdnUrl}/${path}`, {
          certificateAuthority,
          contentType: "application/octet-stream",
          proxyUrl,
          responseType: "arraybuffer",
          timeout: 0,
          type: "GET"
        });
      }
  
      function requestVerificationSMS(number) {
        return _ajax({
          call: "accounts",
          httpType: "GET",
          urlParameters: `/sms/code/${number}`
        });
      }
  
      function requestVerificationVoice(number) {
        return _ajax({
          call: "accounts",
          httpType: "GET",
          urlParameters: `/voice/code/${number}`
        });
      }
  
      async function confirmCode(
        number,
        code,
        newPassword,
        signalingKey,
        registrationId,
        deviceName
      ) {
        const jsonData = {
          signalingKey: _btoa(_getString(signalingKey)),
          supportsSms: false,
          fetchesMessages: true,
          registrationId
        };
  
        let call;
        let urlPrefix;
        let schema;
        let responseType;
  
        if (deviceName) {
          jsonData.name = deviceName;
          call = "devices";
          urlPrefix = "/";
          schema = { deviceId: "number" };
          responseType = "json";
        } else {
          call = "accounts";
          urlPrefix = "/code/";
        }
  
        // We update our saved username and password, since we're creating a new account
        username = number;
        password = newPassword;
  
        const response = await _ajax({
          call,
          httpType: "PUT",
          urlParameters: urlPrefix + code,
          jsonData,
          responseType,
          validateResponse: schema
        });
  
        // From here on out, our username will be our phone number combined with device
        username = `${number}.${response.deviceId || 1}`;
  
        return response;
      }
  
      function getDevices() {
        return _ajax({
          call: "devices",
          httpType: "GET"
        });
      }
  
      function registerKeys(genKeys) {
        const keys = {};
        keys.identityKey = _btoa(_getString(genKeys.identityKey));
        keys.signedPreKey = {
          keyId: genKeys.signedPreKey.keyId,
          publicKey: _btoa(_getString(genKeys.signedPreKey.publicKey)),
          signature: _btoa(_getString(genKeys.signedPreKey.signature))
        };
  
        keys.preKeys = [];
        let j = 0;
        // eslint-disable-next-line guard-for-in, no-restricted-syntax
        for (const i in genKeys.preKeys) {
          keys.preKeys[j] = {
            keyId: genKeys.preKeys[i].keyId,
            publicKey: _btoa(_getString(genKeys.preKeys[i].publicKey))
          };
          j += 1;
        }
  
        // This is just to make the server happy
        // (v2 clients should choke on publicKey)
        keys.lastResortKey = { keyId: 0x7fffffff, publicKey: _btoa("42") };
  
        return _ajax({
          call: "keys",
          httpType: "PUT",
          jsonData: keys
        });
      }
  
      function setSignedPreKey(signedPreKey) {
        return _ajax({
          call: "signed",
          httpType: "PUT",
          jsonData: {
            keyId: signedPreKey.keyId,
            publicKey: _btoa(_getString(signedPreKey.publicKey)),
            signature: _btoa(_getString(signedPreKey.signature))
          }
        });
      }
  
      function getMyKeys() {
        return _ajax({
          call: "keys",
          httpType: "GET",
          responseType: "json",
          validateResponse: { count: "number" }
        }).then(res => res.count);
      }
  
      function getKeysForNumber(number, deviceId = "*") {
        return _ajax({
          call: "keys",
          httpType: "GET",
          urlParameters: `/${number}/${deviceId}`,
          responseType: "json",
          validateResponse: { identityKey: "string", devices: "object" }
        }).then(res => {
          if (res.devices.constructor !== Array) {
            throw new Error("Invalid response");
          }
          res.identityKey = _base64ToBytes(res.identityKey);
          res.devices.forEach(device => {
            if (
              !_validateResponse(device, { signedPreKey: "object" }) ||
              !_validateResponse(device.signedPreKey, {
                publicKey: "string",
                signature: "string"
              })
            ) {
              throw new Error("Invalid signedPreKey");
            }
            if (device.preKey) {
              if (
                !_validateResponse(device, { preKey: "object" }) ||
                !_validateResponse(device.preKey, { publicKey: "string" })
              ) {
                throw new Error("Invalid preKey");
              }
              // eslint-disable-next-line no-param-reassign
              device.preKey.publicKey = _base64ToBytes(device.preKey.publicKey);
            }
            // eslint-disable-next-line no-param-reassign
            device.signedPreKey.publicKey = _base64ToBytes(
              device.signedPreKey.publicKey
            );
            // eslint-disable-next-line no-param-reassign
            device.signedPreKey.signature = _base64ToBytes(
              device.signedPreKey.signature
            );
          });
          return res;
        });
      }
  
      function sendMessages(destination, messageArray, timestamp, silent) {
        const jsonData = { messages: messageArray, timestamp };
  
        if (silent) {
          jsonData.silent = true;
        }
  
        return _ajax({
          call: "messages",
          httpType: "PUT",
          urlParameters: `/${destination}`,
          jsonData,
          responseType: "json"
        });
      }
  
      function getAttachment(id) {
        return _ajax({
          call: "attachment",
          httpType: "GET",
          urlParameters: `/${id}`,
          responseType: "json",
          validateResponse: { location: "string" }
        }).then(response =>
          // Using _outerAJAX, since it's not hardcoded to the Signal Server
          _outerAjax(response.location, {
            contentType: "application/octet-stream",
            proxyUrl,
            responseType: "arraybuffer",
            timeout: 0,
            type: "GET"
          })
        );
      }
  
      function putAttachment(encryptedBin) {
        return _ajax({
          call: "attachment",
          httpType: "GET",
          responseType: "json"
        }).then(response =>
          // Using _outerAJAX, since it's not hardcoded to the Signal Server
          _outerAjax(response.location, {
            contentType: "application/octet-stream",
            data: encryptedBin,
            processData: false,
            proxyUrl,
            timeout: 0,
            type: "PUT"
          }).then(() => response.idString)
        );
      }
  
      function getMessageSocket() {
        console.info("opening message socket", url);
        const fixedScheme = url
          .replace("https://", "wss://")
          .replace("http://", "ws://");
        const login = encodeURIComponent(username);
        const pass = encodeURIComponent(password);
  
        return _createSocket(
          `${fixedScheme}/v1/websocket/?login=${login}&password=${pass}&agent=OWD`,
          { certificateAuthority, proxyUrl }
        );
      }
  
      function getProvisioningSocket() {
        console.info("opening provisioning socket", url);
        const fixedScheme = url
          .replace("https://", "wss://")
          .replace("http://", "ws://");
  
        return _createSocket(
          `${fixedScheme}/v1/websocket/provisioning/?agent=OWD`,
          { certificateAuthority, proxyUrl }
        );
      }
    }
  }