Repository: auth0/auth0-spa-js
Pull request #661 (2020-12-03 13:32:09)

[SDK-2025] Internal module refactor
### Description

This PR ensures types have been applied in a number of places, particularly around the web worker interface.

Also:

* Upgrades to TypeScript 4.1.2 (allows us to use `MessageEvent<T>`, useful for the web worker)
* The fetch methods and `oauthToken` method have been split from the `utils` module into `http` and `api` respectively
* Web worker, types and utils have been moved into a subfolder
* Dependencies updated to latest semver where possible
* Tests fixed
* VSCode setting so that VSCode uses the version of TypeScript we use in the library, rather than its own

Probably also closes SDK-2027.

Most of the changes in the diff are around the splitting of the utils module.

### Testing

No new tests as such, but affected tests have been refactored or fixed.

- [ ] This change adds test coverage for new/changed/fixed functionality

### Checklist

- [X] I have added documentation for new/changed functionality in this PR or in auth0.com/docs
- [X] All active GitHub checks for tests, formatting, and security are passing
- [X] The correct base branch is being used, if not `master`


stevehobbsdev: > Also, should we drop the docs ?

Yeah, let me see if I can easily revert them.

Patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e25ec36..e8e399a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,5 +6,6 @@
     "**/dist": true,
     "**/.rpt2_cache": true,
     "**/docs": true
-  }
+  },
+  "typescript.tsdk": "node_modules/typescript/lib"
 }
diff --git a/jest.config.js b/jest.config.js
index 6219a2f..86de452 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -14,5 +14,10 @@ module.exports = {
   ],
   coverageReporters: ['lcov', 'text', 'text-summary'],
   preset: 'ts-jest',
-  setupFiles: ['jest-localstorage-mock']
+  setupFiles: ['jest-localstorage-mock'],
+  globals: {
+    'ts-jest': {
+      tsconfig: './tsconfig.test.json'
+    }
+  }
 };
diff --git a/package-lock.json b/package-lock.json
index 9910f30..67e8f64 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@auth0/auth0-spa-js",
-  "version": "1.13.3",
+  "version": "1.13.4",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -1515,9 +1515,9 @@
       }
     },
     "@types/jest": {
-      "version": "26.0.15",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz",
-      "integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==",
+      "version": "26.0.16",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.16.tgz",
+      "integrity": "sha512-Gp12+7tmKCgv9JjtltxUXokohCAEZfpJaEW5tn871SGRp8I+bRWBonQO7vW5NHwnAHe5dd50+Q4zyKuN35i09g==",
       "dev": true,
       "requires": {
         "jest-diff": "^26.0.0",
@@ -1603,65 +1603,65 @@
       "dev": true
     },
     "@typescript-eslint/eslint-plugin-tslint": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-4.7.0.tgz",
-      "integrity": "sha512-YCL7TzcOQ4mWrRac5UdcM8Cq0xOvVFlyg5+g6f5dl+inrDdQ8OOHvjSkbBDFSK1P+w5qKMztWZgRHmUTPJ7iGg==",
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-4.9.0.tgz",
+      "integrity": "sha512-WI5126gGVknI/vem97oi2kCBjK/uf27tvZTMi9A1YmI0dHHXSSoaVaIA/nNDJhhywQWP7r9rhmYbfPV3nCkacw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/experimental-utils": "4.7.0",
+        "@typescript-eslint/experimental-utils": "4.9.0",
         "lodash": "^4.17.15"
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.7.0.tgz",
-      "integrity": "sha512-cymzovXAiD4EF+YoHAB5Oh02MpnXjvyaOb+v+BdpY7lsJXZQN34oIETeUwVT2XfV9rSNpXaIcknDLfupO/tUoA==",
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.9.0.tgz",
+      "integrity": "sha512-0p8GnDWB3R2oGhmRXlEnCvYOtaBCijtA5uBfH5GxQKsukdSQyI4opC4NGTUb88CagsoNQ4rb/hId2JuMbzWKFQ==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.3",
-        "@typescript-eslint/scope-manager": "4.7.0",
-        "@typescript-eslint/types": "4.7.0",
-        "@typescript-eslint/typescript-estree": "4.7.0",
+        "@typescript-eslint/scope-manager": "4.9.0",
+        "@typescript-eslint/types": "4.9.0",
+        "@typescript-eslint/typescript-estree": "4.9.0",
         "eslint-scope": "^5.0.0",
         "eslint-utils": "^2.0.0"
       }
     },
     "@typescript-eslint/parser": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.7.0.tgz",
-      "integrity": "sha512-+meGV8bMP1sJHBI2AFq1GeTwofcGiur8LoIr6v+rEmD9knyCqDlrQcFHR0KDDfldHIFDU/enZ53fla6ReF4wRw==",
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.9.0.tgz",
+      "integrity": "sha512-QRSDAV8tGZoQye/ogp28ypb8qpsZPV6FOLD+tbN4ohKUWHD2n/u0Q2tIBnCsGwQCiD94RdtLkcqpdK4vKcLCCw==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "4.7.0",
-        "@typescript-eslint/types": "4.7.0",
-        "@typescript-eslint/typescript-estree": "4.7.0",
+        "@typescript-eslint/scope-manager": "4.9.0",
+        "@typescript-eslint/types": "4.9.0",
+        "@typescript-eslint/typescript-estree": "4.9.0",
         "debug": "^4.1.1"
       }
     },
     "@typescript-eslint/scope-manager": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.7.0.tgz",
-      "integrity": "sha512-ILITvqwDJYbcDCROj6+Ob0oCKNg3SH46iWcNcTIT9B5aiVssoTYkhKjxOMNzR1F7WSJkik4zmuqve5MdnA0DyA==",
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.9.0.tgz",
+      "integrity": "sha512-q/81jtmcDtMRE+nfFt5pWqO0R41k46gpVLnuefqVOXl4QV1GdQoBWfk5REcipoJNQH9+F5l+dwa9Li5fbALjzg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.7.0",
-        "@typescript-eslint/visitor-keys": "4.7.0"
+        "@typescript-eslint/types": "4.9.0",
+        "@typescript-eslint/visitor-keys": "4.9.0"
       }
     },
     "@typescript-eslint/types": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.7.0.tgz",
-      "integrity": "sha512-uLszFe0wExJc+I7q0Z/+BnP7wao/kzX0hB5vJn4LIgrfrMLgnB2UXoReV19lkJQS1a1mHWGGODSxnBx6JQC3Sg==",
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.9.0.tgz",
+      "integrity": "sha512-luzLKmowfiM/IoJL/rus1K9iZpSJK6GlOS/1ezKplb7MkORt2dDcfi8g9B0bsF6JoRGhqn0D3Va55b+vredFHA==",
       "dev": true
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.7.0.tgz",
-      "integrity": "sha512-5XZRQznD1MfUmxu1t8/j2Af4OxbA7EFU2rbo0No7meb46eHgGkSieFdfV6omiC/DGIBhH9H9gXn7okBbVOm8jw==",
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.9.0.tgz",
+      "integrity": "sha512-rmDR++PGrIyQzAtt3pPcmKWLr7MA+u/Cmq9b/rON3//t5WofNR4m/Ybft2vOLj0WtUzjn018ekHjTsnIyBsQug==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.7.0",
-        "@typescript-eslint/visitor-keys": "4.7.0",
+        "@typescript-eslint/types": "4.9.0",
+        "@typescript-eslint/visitor-keys": "4.9.0",
         "debug": "^4.1.1",
         "globby": "^11.0.1",
         "is-glob": "^4.0.1",
@@ -1670,21 +1670,39 @@
         "tsutils": "^3.17.1"
       },
       "dependencies": {
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
         "semver": {
-          "version": "7.3.2",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
-          "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
+          "version": "7.3.4",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
+          "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
           "dev": true
         }
       }
     },
     "@typescript-eslint/visitor-keys": {
-      "version": "4.7.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.7.0.tgz",
-      "integrity": "sha512-aDJDWuCRsf1lXOtignlfiPODkzSxxop7D0rZ91L6ZuMlcMCSh0YyK+gAfo5zN/ih6WxMwhoXgJWC3cWQdaKC+A==",
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.9.0.tgz",
+      "integrity": "sha512-sV45zfdRqQo1A97pOSx3fsjR+3blmwtdCt8LDrXgCX36v4Vmz4KHrhpV6Fo2cRdXmyumxx11AHw0pNJqCNpDyg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "4.7.0",
+        "@typescript-eslint/types": "4.9.0",
         "eslint-visitor-keys": "^2.0.0"
       }
     },
@@ -1716,9 +1734,9 @@
       }
     },
     "acorn": {
-      "version": "6.4.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
-      "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
+      "version": "6.4.2",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
+      "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==",
       "dev": true
     },
     "acorn-globals": {
@@ -2003,9 +2021,9 @@
       "dev": true
     },
     "aws-sdk": {
-      "version": "2.788.0",
-      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.788.0.tgz",
-      "integrity": "sha512-UKc+4MFRezIkgk668mylj7MpVD2dCtiDBppgx05SwYSDGT60bixrKv3nE/YJK+gAv53CWVf7WL6hNv76+TWkTA==",
+      "version": "2.802.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.802.0.tgz",
+      "integrity": "sha512-PfjBr5Ag4PdcEYPrfMclVWk85kFSJNe7qllZBE8RhYNu+K+Z2pveKfYkC5mqYoKEYIQyI9by9N47F+Tqm1GXtg==",
       "dev": true,
       "requires": {
         "buffer": "4.9.2",
@@ -2480,9 +2498,9 @@
       }
     },
     "caporal": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/caporal/-/caporal-1.3.0.tgz",
-      "integrity": "sha512-4bj21UXbEu5cF+1gVjhwwRqMhY3lR7CUTlBr6YX3uzftL4l/sbu8EoHfOLZWHr+HeiosW9fTWkQCS2UZMAk5lw==",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/caporal/-/caporal-1.4.0.tgz",
+      "integrity": "sha512-3pWfIwKVdIbB/gWmpLloO6iGAXTRi9mcTinPOwvHfzH3BYjOhLgq2XRG3hKtp+F6vBcBXxMgCobUzBAx1d8T4A==",
       "dev": true,
       "requires": {
         "bluebird": "^3.4.7",
@@ -3175,9 +3193,9 @@
       "dev": true
     },
     "core-js": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.7.0.tgz",
-      "integrity": "sha512-NwS7fI5M5B85EwpWuIwJN4i/fbisQUwLwiSNUWeXlkAZ0sbBjLEvLvFLf1uzAUV66PcEPt4xCGCmOZSxVf3xzA=="
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.0.tgz",
+      "integrity": "sha512-W2VYNB0nwQQE7tKS7HzXd7r2y/y2SVJl4ga6oH/dnaLFzM0o2lB2P3zCkWj5Wc/zyMYjtgd5Hmhk0ObkQFZOIA=="
     },
     "core-util-is": {
       "version": "1.0.2",
@@ -3650,13 +3668,13 @@
       }
     },
     "es-check": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/es-check/-/es-check-5.1.2.tgz",
-      "integrity": "sha512-uecftCFHR0ggaI3cKwqOu5b5j21z78NgoLlQitE0fDh+xATDZKrFdMoMM9L2ihiGq/d2L2sFPsgzWEsb4Vnr6Q==",
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/es-check/-/es-check-5.1.4.tgz",
+      "integrity": "sha512-mPfnJ4af0P0QQ8qODF05eumLJBCUldYwIpMkvu6QMVh6uA0l4C0EwE6eDL/EDrrK2hODzoBprE9zPwilLZiD7A==",
       "dev": true,
       "requires": {
         "acorn": "^6.4.1",
-        "caporal": "1.3.0",
+        "caporal": "1.4.0",
         "glob": "^7.1.2"
       }
     },
@@ -3703,9 +3721,9 @@
       }
     },
     "eslint": {
-      "version": "7.13.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.13.0.tgz",
-      "integrity": "sha512-uCORMuOO8tUzJmsdRtrvcGq5qposf7Rw0LwkTJkoDbOycVQtQjmnhZSuLQnozLE4TmAzlMVV45eCHmQ1OpDKUQ==",
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.14.0.tgz",
+      "integrity": "sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
@@ -3829,6 +3847,15 @@
             "type-check": "~0.4.0"
           }
         },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
         "optionator": {
           "version": "0.9.1",
           "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -3856,10 +3883,13 @@
           "dev": true
         },
         "semver": {
-          "version": "7.3.2",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
-          "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
-          "dev": true
+          "version": "7.3.4",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
+          "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
         },
         "shebang-command": {
           "version": "2.0.0",
@@ -3911,6 +3941,12 @@
           "requires": {
             "isexe": "^2.0.0"
           }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true
         }
       }
     },
@@ -4431,51 +4467,6 @@
         "merge2": "^1.3.0",
         "micromatch": "^4.0.2",
         "picomatch": "^2.2.1"
-      },
-      "dependencies": {
-        "braces": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-          "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-          "dev": true,
-          "requires": {
-            "fill-range": "^7.0.1"
-          }
-        },
-        "fill-range": {
-          "version": "7.0.1",
-          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-          "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-          "dev": true,
-          "requires": {
-            "to-regex-range": "^5.0.1"
-          }
-        },
-        "is-number": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-          "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-          "dev": true
-        },
-        "micromatch": {
-          "version": "4.0.2",
-          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
-          "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
-          "dev": true,
-          "requires": {
-            "braces": "^3.0.1",
-            "picomatch": "^2.0.5"
-          }
-        },
-        "to-regex-range": {
-          "version": "5.0.1",
-          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-          "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-          "dev": true,
-          "requires": {
-            "is-number": "^7.0.0"
-          }
-        }
       }
     },
     "fast-json-stable-stringify": {
@@ -8867,9 +8858,9 @@
       "dev": true
     },
     "prettier": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
-      "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
+      "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
       "dev": true
     },
     "pretty-bytes": {
@@ -9294,9 +9285,9 @@
       }
     },
     "request": {
-      "version": "2.88.0",
-      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
-      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+      "version": "2.88.2",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
+      "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
       "dev": true,
       "requires": {
         "aws-sign2": "~0.7.0",
@@ -9306,7 +9297,7 @@
         "extend": "~3.0.2",
         "forever-agent": "~0.6.1",
         "form-data": "~2.3.2",
-        "har-validator": "~5.1.0",
+        "har-validator": "~5.1.3",
         "http-signature": "~1.2.0",
         "is-typedarray": "~1.0.0",
         "isstream": "~0.1.2",
@@ -9316,16 +9307,26 @@
         "performance-now": "^2.1.0",
         "qs": "~6.5.2",
         "safe-buffer": "^5.1.2",
-        "tough-cookie": "~2.4.3",
+        "tough-cookie": "~2.5.0",
         "tunnel-agent": "^0.6.0",
         "uuid": "^3.3.2"
       },
       "dependencies": {
-        "qs": {
-          "version": "6.5.2",
-          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+        "punycode": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+          "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
           "dev": true
+        },
+        "tough-cookie": {
+          "version": "2.5.0",
+          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+          "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+          "dev": true,
+          "requires": {
+            "psl": "^1.1.28",
+            "punycode": "^2.1.1"
+          }
         }
       }
     },
@@ -9440,9 +9441,9 @@
       }
     },
     "rollup": {
-      "version": "2.33.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.33.1.tgz",
-      "integrity": "sha512-uY4O/IoL9oNW8MMcbA5hcOaz6tZTMIh7qJHx/tzIJm+n1wLoY38BLn6fuy7DhR57oNFLMbDQtDeJoFURt5933w==",
+      "version": "2.34.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.34.1.tgz",
+      "integrity": "sha512-tGveB6NU5x4MS/iXaIsjfUkEv4hxzJJ4o0FRy5LO62Ndx3R2cmE1qsLYlSfRkvHUUPqWiFoxEm8pRftzh1a5HA==",
       "dev": true,
       "requires": {
         "fsevents": "~2.1.2"
@@ -9855,9 +9856,9 @@
       }
     },
     "rollup-plugin-web-worker-loader": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/rollup-plugin-web-worker-loader/-/rollup-plugin-web-worker-loader-1.4.0.tgz",
-      "integrity": "sha512-jhlOwSkrXkAVFY2Fded0M1AS9iqZDNjUJxNz4BFjBlaKuUzTx8Ikey+b3LSegUJE4mzAAw2DqJD8pX12R0ZdDw==",
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/rollup-plugin-web-worker-loader/-/rollup-plugin-web-worker-loader-1.5.0.tgz",
+      "integrity": "sha512-Zx5l370yGDje35rFkM/wbT4dMIq2+kSBdLWQpqLkBuxmrQJcx0umA05kSbNRzccFsudQH4FKYCK8GtSBQRQBBg==",
       "dev": true
     },
     "rollup-pluginutils": {
@@ -11433,9 +11434,9 @@
       }
     },
     "typescript": {
-      "version": "3.9.7",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
-      "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz",
+      "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
       "dev": true
     },
     "uglify-js": {
diff --git a/package.json b/package.json
index b3186dc..635b975 100644
--- a/package.json
+++ b/package.json
@@ -37,15 +37,15 @@
     "@auth0/component-cdn-uploader": "github:auth0/component-cdn-uploader#v2.2.2",
     "@rollup/plugin-replace": "^2.3.4",
     "@types/cypress": "^1.1.3",
-    "@types/jest": "^26.0.15",
-    "@typescript-eslint/eslint-plugin-tslint": "^4.7.0",
-    "@typescript-eslint/parser": "^4.7.0",
+    "@types/jest": "^26.0.16",
+    "@typescript-eslint/eslint-plugin-tslint": "^4.9.0",
+    "@typescript-eslint/parser": "^4.9.0",
     "cli-table": "^0.3.1",
     "codecov": "^3.8.1",
     "concurrently": "^5.3.0",
     "cypress": "^5.6.0",
-    "es-check": "^5.1.2",
-    "eslint": "^7.13.0",
+    "es-check": "^5.1.4",
+    "eslint": "^7.14.0",
     "gzip-size": "^5.1.1",
     "husky": "^4.3.0",
     "idtoken-verifier": "^2.1.0",
@@ -54,11 +54,11 @@
     "jest-localstorage-mock": "^2.4.3",
     "jsonwebtoken": "^8.5.1",
     "pem": "^1.14.4",
-    "prettier": "^2.1.2",
+    "prettier": "^2.2.1",
     "pretty-quick": "^3.1.0",
     "qss": "^2.0.3",
     "rimraf": "^3.0.2",
-    "rollup": "^2.33.1",
+    "rollup": "^2.34.1",
     "rollup-plugin-analyzer": "^3.3.0",
     "rollup-plugin-commonjs": "^10.1.0",
     "rollup-plugin-livereload": "^2.0.0",
@@ -68,19 +68,19 @@
     "rollup-plugin-terser": "^7.0.2",
     "rollup-plugin-typescript2": "^0.27.3",
     "rollup-plugin-visualizer": "^4.2.0",
-    "rollup-plugin-web-worker-loader": "^1.4.0",
+    "rollup-plugin-web-worker-loader": "^1.5.0",
     "serve": "^11.3.2",
     "ts-jest": "^26.4.4",
     "tslint": "^6.1.3",
     "tslint-config-security": "^1.16.0",
     "typedoc": "^0.18.0",
-    "typescript": "^3.9.7",
+    "typescript": "^4.1.2",
     "wait-on": "^5.2.0"
   },
   "dependencies": {
     "abortcontroller-polyfill": "^1.5.0",
     "browser-tabs-lock": "1.2.9",
-    "core-js": "^3.7.0",
+    "core-js": "^3.8.0",
     "es-cookie": "^1.3.2",
     "fast-text-encoding": "^1.0.3",
     "promise-polyfill": "^8.2.0",
diff --git a/src/Auth0Client.ts b/src/Auth0Client.ts
index 8126071..396f2ec 100644
--- a/src/Auth0Client.ts
+++ b/src/Auth0Client.ts
@@ -9,15 +9,17 @@ import {
   runIframe,
   sha256,
   bufferToBase64UrlEncoded,
-  oauthToken,
   validateCrypto
 } from './utils';
 
+import { oauthToken, TokenEndpointResponse } from './api';
+
 import { getUniqueScopes } from './scope';
 import { InMemoryCache, ICache, LocalStorageCache } from './cache';
 import TransactionManager from './transaction-manager';
 import { verify as verifyIdToken } from './jwt';
 import { AuthenticationError } from './errors';
+
 import {
   ClientStorage,
   CookieStorage,
@@ -57,7 +59,7 @@ import {
 } from './global';
 
 // @ts-ignore
-import TokenWorker from './token.worker.ts';
+import TokenWorker from './worker/token.worker.ts';
 import { isIE11, isSafari10, isSafari11, isSafari12_0 } from './user-agent';
 
 /**
@@ -73,7 +75,7 @@ const GET_TOKEN_SILENTLY_LOCK_KEY = 'auth0.lock.getTokenSilently';
 /**
  * @ignore
  */
-const cacheLocationBuilders = {
+const cacheLocationBuilders: Record<string, () => ICache> = {
   memory: () => new InMemoryCache().enclosedCache,
   localstorage: () => new LocalStorageCache()
 };
@@ -94,10 +96,11 @@ const supportWebWorker = () =>
 /**
  * @ignore
  */
-const getTokenIssuer = (issuer, domainUrl) => {
+const getTokenIssuer = (issuer: string, domainUrl: string) => {
   if (issuer) {
     return issuer.startsWith('https://') ? issuer : `https://${issuer}/`;
   }
+
   return `${domainUrl}/`;
 };
 
@@ -197,7 +200,7 @@ export default class Auth0Client {
     this.customOptions = getCustomInitialOptions(options);
   }
 
-  private _url(path) {
+  private _url(path: string) {
     const auth0Client = encodeURIComponent(
       btoa(JSON.stringify(this.options.auth0Client || DEFAULT_AUTH0_CLIENT))
     );
@@ -415,7 +418,7 @@ export default class Auth0Client {
    */
   public async getUser<TUser extends User = User>(
     options: GetUserOptions = {}
-  ): Promise<TUser> {
+  ): Promise<TUser | undefined> {
     const audience = options.audience || this.options.audience || 'default';
     const scope = getUniqueScopes(this.defaultScope, this.scope, options.scope);
 
@@ -513,7 +516,6 @@ export default class Auth0Client {
       code,
       auth0Client: this.options.auth0Client
     } as OAuthTokenOptions;
-
     // some old versions of the SDK might not have added redirect_uri to the
     // transaction, we dont want the key to be set to undefined.
     if (undefined !== transaction.redirect_uri) {
@@ -867,7 +869,7 @@ export default class Auth0Client {
       this.options.redirect_uri ||
       window.location.origin;
 
-    let tokenResult;
+    let tokenResult: TokenEndpointResponse;
 
     const {
       scope,
diff --git a/src/api.ts b/src/api.ts
new file mode 100644
index 0000000..7f5f468
--- /dev/null
+++ b/src/api.ts
@@ -0,0 +1,41 @@
+import { TokenEndpointOptions } from './global';
+import { DEFAULT_AUTH0_CLIENT } from './constants';
+import { getJSON } from './http';
+
+export type TokenEndpointResponse = {
+  id_token: string;
+  access_token: string;
+  refresh_token?: string;
+  expires_in: number;
+  scope?: string;
+};
+
+export async function oauthToken(
+  {
+    baseUrl,
+    timeout,
+    audience,
+    scope,
+    auth0Client,
+    ...options
+  }: TokenEndpointOptions,
+  worker?: Worker
+) {
+  return await getJSON<TokenEndpointResponse>(
+    `${baseUrl}/oauth/token`,
+    timeout,
+    audience || 'default',
+    scope,
+    {
+      method: 'POST',
+      body: JSON.stringify(options),
+      headers: {
+        'Content-type': 'application/json',
+        'Auth0-Client': btoa(
+          JSON.stringify(auth0Client || DEFAULT_AUTH0_CLIENT)
+        )
+      }
+    },
+    worker
+  );
+}
diff --git a/src/cache.ts b/src/cache.ts
index 77104da..0b9480b 100644
--- a/src/cache.ts
+++ b/src/cache.ts
@@ -24,7 +24,10 @@ interface CacheEntry {
 
 export interface ICache {
   save(entry: CacheEntry): void;
-  get(key: CacheKeyData, expiryAdjustmentSeconds?: number): Partial<CacheEntry>;
+  get(
+    key: CacheKeyData,
+    expiryAdjustmentSeconds?: number
+  ): Partial<CacheEntry> | undefined;
   clear(): void;
 }
 
@@ -64,7 +67,7 @@ export class LocalStorageCache implements ICache {
   public get(
     key: CacheKeyData,
     expiryAdjustmentSeconds = DEFAULT_EXPIRY_ADJUSTMENT_SECONDS
-  ): Partial<CacheEntry> {
+  ): Partial<CacheEntry> | undefined {
     const cacheKey = createKey(key);
     const payload = this.readJson(cacheKey);
     const nowSeconds = Math.floor(Date.now() / 1000);
@@ -142,10 +145,7 @@ export class LocalStorageCache implements ICache {
 
 export class InMemoryCache {
   public enclosedCache: ICache = (function () {
-    let cache: CachePayload = {
-      body: {},
-      expiresAt: 0
-    };
+    let cache: Record<string, CachePayload> = {};
 
     return {
       save(entry: CacheEntry) {
@@ -158,7 +158,7 @@ export class InMemoryCache {
       get(
         key: CacheKeyData,
         expiryAdjustmentSeconds = DEFAULT_EXPIRY_ADJUSTMENT_SECONDS
-      ) {
+      ): Partial<CacheEntry> | undefined {
         const cacheKey = createKey(key);
         const wrappedEntry: CachePayload = cache[cacheKey];
         const nowSeconds = Math.floor(Date.now() / 1000);
@@ -185,10 +185,7 @@ export class InMemoryCache {
       },
 
       clear() {
-        cache = {
-          body: {},
-          expiresAt: 0
-        };
+        cache = {};
       }
     };
   })();
diff --git a/src/global.ts b/src/global.ts
index b05ff51..ce02e70 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -493,3 +493,14 @@ export class User {
   sub?: string;
   [key: string]: any;
 }
+
+/**
+ * @ignore
+ */
+export type FetchOptions = {
+  method?: string;
+  headers?: Record<string, string>;
+  credentials?: 'include' | 'omit';
+  body?: string;
+  signal?: AbortSignal;
+};
diff --git a/src/http.ts b/src/http.ts
new file mode 100644
index 0000000..8497487
--- /dev/null
+++ b/src/http.ts
@@ -0,0 +1,131 @@
+import fetch from 'unfetch';
+
+import {
+  DEFAULT_FETCH_TIMEOUT_MS,
+  DEFAULT_SILENT_TOKEN_RETRY_COUNT
+} from './constants';
+
+import { sendMessage } from './worker/worker.utils';
+import { FetchOptions } from './global';
+import { GenericError } from './errors';
+
+export const createAbortController = () => new AbortController();
+
+const switchFetch = async (
+  fetchUrl: string,
+  audience: string,
+  scope: string,
+  fetchOptions: FetchOptions,
+  timeout: number,
+  worker?: Worker
+): Promise<any> => {
+  if (worker) {
+    // AbortSignal is not serializable, need to implement in the Web Worker
+    delete fetchOptions.signal;
+
+    return sendMessage(
+      {
+        auth: {
+          audience,
+          scope
+        },
+        timeout,
+        fetchUrl,
+        fetchOptions
+      },
+      worker
+    );
+  } else {
+    const response = await fetch(fetchUrl, fetchOptions);
+    return {
+      ok: response.ok,
+      json: await response.json()
+    };
+  }
+};
+
+export const fetchWithTimeout = (
+  url: string,
+  audience: string,
+  scope: string,
+  options: FetchOptions,
+  worker?: Worker,
+  timeout = DEFAULT_FETCH_TIMEOUT_MS
+): Promise<any> => {
+  const controller = createAbortController();
+  const signal = controller.signal;
+
+  const fetchOptions = {
+    ...options,
+    signal
+  };
+
+  let timeoutId: NodeJS.Timeout;
+
+  // The promise will resolve with one of these two promises (the fetch or the timeout), whichever completes first.
+  return Promise.race([
+    switchFetch(url, audience, scope, fetchOptions, timeout, worker),
+    new Promise((_, reject) => {
+      timeoutId = setTimeout(() => {
+        controller.abort();
+        reject(new Error("Timeout when executing 'fetch'"));
+      }, timeout);
+    })
+  ]).finally(() => {
+    clearTimeout(timeoutId);
+  });
+};
+
+export async function getJSON<T>(
+  url: string,
+  timeout: number,
+  audience: string,
+  scope: string,
+  options: FetchOptions,
+  worker?: Worker
+): Promise<T> {
+  let fetchError: null | Error = null;
+  let response: any;
+
+  for (let i = 0; i < DEFAULT_SILENT_TOKEN_RETRY_COUNT; i++) {
+    try {
+      response = await fetchWithTimeout(
+        url,
+        audience,
+        scope,
+        options,
+        worker,
+        timeout
+      );
+      fetchError = null;
+      break;
+    } catch (e) {
+      // Fetch only fails in the case of a network issue, so should be
+      // retried here. Failure status (4xx, 5xx, etc) return a resolved Promise
+      // with the failure in the body.
+      // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
+      fetchError = e;
+    }
+  }
+
+  if (fetchError) {
+    // unfetch uses XMLHttpRequest under the hood which throws
+    // ProgressEvents on error, which don't have message properties
+    fetchError.message = fetchError.message || 'Failed to fetch';
+    throw fetchError;
+  }
+
+  const {
+    json: { error, error_description, ...success },
+    ok
+  } = response;
+
+  if (!ok) {
+    const errorMessage =
+      error_description || `HTTP error. Unable to fetch ${url}`;
+
+    throw new GenericError(error || 'request_error', errorMessage);
+  }
+
+  return success;
+}
diff --git a/src/jwt.ts b/src/jwt.ts
index 9ef9832..40dcb54 100644
--- a/src/jwt.ts
+++ b/src/jwt.ts
@@ -1,7 +1,7 @@
 import { urlDecodeB64 } from './utils';
 import { IdToken, JWTVerifyOptions } from './global';
 
-const isNumber = n => typeof n === 'number';
+const isNumber = (n: any) => typeof n === 'number';
 
 const idTokendecoded = [
   'iss',
diff --git a/src/utils.ts b/src/utils.ts
index dc0e193..49307d5 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,23 +1,12 @@
-import fetch from 'unfetch';
-
-import {
-  AuthenticationResult,
-  PopupConfigOptions,
-  TokenEndpointOptions
-} from './global';
+import { AuthenticationResult, PopupConfigOptions } from './global';
 
 import {
   DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS,
-  DEFAULT_SILENT_TOKEN_RETRY_COUNT,
-  DEFAULT_FETCH_TIMEOUT_MS,
-  CLEANUP_IFRAME_TIMEOUT_IN_SECONDS,
-  DEFAULT_AUTH0_CLIENT
+  CLEANUP_IFRAME_TIMEOUT_IN_SECONDS
 } from './constants';
 
 import { PopupTimeoutError, TimeoutError, GenericError } from './errors';
 
-export const createAbortController = () => new AbortController();
-
 export const parseQueryResult = (queryString: string) => {
   if (queryString.indexOf('#') > -1) {
     queryString = queryString.substr(0, queryString.indexOf('#'));
@@ -118,7 +107,7 @@ export const runPopup = (authorizeUrl: string, config: PopupConfigOptions) => {
   }
 
   return new Promise<AuthenticationResult>((resolve, reject) => {
-    let popupEventListener;
+    let popupEventListener: EventListenerOrEventListenerObject;
 
     const timeoutId = setTimeout(() => {
       reject(new PopupTimeoutError(popup));
@@ -141,7 +130,7 @@ export const runPopup = (authorizeUrl: string, config: PopupConfigOptions) => {
       resolve(e.data.response);
     };
 
-    window.addEventListener('message', e => popupEventListener(e));
+    window.addEventListener('message', popupEventListener);
   });
 };
 
@@ -234,162 +223,6 @@ export const bufferToBase64UrlEncoded = (input: number[] | Uint8Array) => {
   );
 };
 
-const sendMessage = (message: any, to: Worker) =>
-  new Promise(function (resolve, reject) {
-    const messageChannel = new MessageChannel();
-    messageChannel.port1.onmessage = function (event) {
-      // Only for fetch errors, as these get retried
-      if (event.data.error) {
-        reject(new Error(event.data.error));
-      } else {
-        resolve(event.data);
-      }
-    };
-    to.postMessage(message, [messageChannel.port2]);
-  });
-
-const switchFetch = async (
-  url: string,
-  audience: string,
-  scope: string,
-  opts: { [index: string]: any },
-  timeout: number,
-  worker: Worker
-) => {
-  if (worker) {
-    // AbortSignal is not serializable, need to implement in the Web Worker
-    delete opts.signal;
-    return sendMessage({ url, audience, scope, timeout, ...opts }, worker);
-  } else {
-    const response = await fetch(url, opts);
-    return {
-      ok: response.ok,
-      json: await response.json()
-    };
-  }
-};
-
-export const fetchWithTimeout = (
-  url: string,
-  audience: string,
-  scope: string,
-  options: { [index: string]: any },
-  worker: Worker,
-  timeout = DEFAULT_FETCH_TIMEOUT_MS
-) => {
-  const controller = createAbortController();
-  const signal = controller.signal;
-
-  const fetchOptions = {
-    ...options,
-    signal
-  };
-
-  let timeoutId: ReturnType<typeof setTimeout>;
-  // The promise will resolve with one of these two promises (the fetch or the timeout), whichever completes first.
-  return Promise.race([
-    switchFetch(url, audience, scope, fetchOptions, timeout, worker),
-    new Promise((_, reject) => {
-      timeoutId = setTimeout(() => {
-        controller.abort();
-        reject(new Error("Timeout when executing 'fetch'"));
-      }, timeout);
-    })
-  ]).finally(() => {
-    clearTimeout(timeoutId);
-  });
-};
-
-const getJSON = async (
-  url: string,
-  timeout: number,
-  audience: string,
-  scope: string,
-  options: { [index: string]: any },
-  worker: Worker
-) => {
-  let fetchError: null | Error = null;
-  let response: any;
-
-  for (let i = 0; i < DEFAULT_SILENT_TOKEN_RETRY_COUNT; i++) {
-    try {
-      response = await fetchWithTimeout(
-        url,
-        audience,
-        scope,
-        options,
-        worker,
-        timeout
-      );
-      fetchError = null;
-      break;
-    } catch (e) {
-      // Fetch only fails in the case of a network issue, so should be
-      // retried here. Failure status (4xx, 5xx, etc) return a resolved Promise
-      // with the failure in the body.
-      // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
-      fetchError = e;
-    }
-  }
-
-  if (fetchError) {
-    // unfetch uses XMLHttpRequest under the hood which throws
-    // ProgressEvents on error, which don't have message properties
-    fetchError.message = fetchError.message || 'Failed to fetch';
-    throw fetchError;
-  }
-
-  const {
-    json: { error, error_description, ...success },
-    ok
-  } = response;
-
-  if (!ok) {
-    const errorMessage =
-      error_description || `HTTP error. Unable to fetch ${url}`;
-    const e: any = new Error(errorMessage);
-
-    e.error = error || 'request_error';
-    e.error_description = errorMessage;
-
-    throw e;
-  }
-
-  return success;
-};
-
-export const oauthToken = async (
-  {
-    baseUrl,
-    timeout,
-    audience,
-    scope,
-    auth0Client,
-    ...options
-  }: TokenEndpointOptions,
-  worker: Worker
-) =>
-  await getJSON(
-    `${baseUrl}/oauth/token`,
-    timeout,
-    audience || 'default',
-    scope,
-    {
-      method: 'POST',
-      body: JSON.stringify({
-        redirect_uri: window.location.origin,
-        ...options
-      }),
-      headers: {
-        'Content-type': 'application/json',
-        'Auth0-Client': btoa(
-          JSON.stringify(auth0Client || DEFAULT_AUTH0_CLIENT)
-        )
-      }
-    },
-    worker
-  );
-
 export const validateCrypto = () => {
   if (!getCrypto()) {
     throw new Error(
diff --git a/src/__mocks__/token.worker.ts b/src/worker/__mocks__/token.worker.ts
similarity index 100%
rename from src/__mocks__/token.worker.ts
rename to src/worker/__mocks__/token.worker.ts
diff --git a/src/token.worker.ts b/src/worker/token.worker.ts
similarity index 60%
rename from src/token.worker.ts
rename to src/worker/token.worker.ts
index b030e70..d29d30f 100644
--- a/src/token.worker.ts
+++ b/src/worker/token.worker.ts
@@ -1,49 +1,67 @@
-import { MISSING_REFRESH_TOKEN_ERROR_MESSAGE } from './constants';
+import { MISSING_REFRESH_TOKEN_ERROR_MESSAGE } from '../constants';
+import { WorkerRefreshTokenMessage } from './worker.types';
 
-let refreshTokens = {};
+let refreshTokens: Record<string, string> = {};
 
-const cacheKey = (audience, scope) => `${audience}|${scope}`;
+const cacheKey = (audience: string, scope: string) => `${audience}|${scope}`;
 
-const getRefreshToken = (audience, scope) =>
+const getRefreshToken = (audience: string, scope: string) =>
   refreshTokens[cacheKey(audience, scope)];
 
-const setRefreshToken = (refreshToken, audience, scope) =>
-  (refreshTokens[cacheKey(audience, scope)] = refreshToken);
+const setRefreshToken = (
+  refreshToken: string,
+  audience: string,
+  scope: string
+) => (refreshTokens[cacheKey(audience, scope)] = refreshToken);
 
-const deleteRefreshToken = (audience, scope) =>
+const deleteRefreshToken = (audience: string, scope: string) =>
   delete refreshTokens[cacheKey(audience, scope)];
 
-const wait: any = time => new Promise(resolve => setTimeout(resolve, time));
+const wait = (time: number) =>
+  new Promise(resolve => setTimeout(resolve, time));
 
 const messageHandler = async ({
-  data: { url, timeout, audience, scope, ...opts },
+  data: { timeout, auth, fetchUrl, fetchOptions },
   ports: [port]
-}) => {
-  let json;
+}: MessageEvent<WorkerRefreshTokenMessage>) => {
+  let json: {
+    refresh_token?: string;
+  };
+
+  const { audience, scope } = auth || {};
+
   try {
-    const body = JSON.parse(opts.body);
+    const body = JSON.parse(fetchOptions.body);
+
     if (!body.refresh_token && body.grant_type === 'refresh_token') {
       const refreshToken = getRefreshToken(audience, scope);
+
       if (!refreshToken) {
         throw new Error(MISSING_REFRESH_TOKEN_ERROR_MESSAGE);
       }
-      opts.body = JSON.stringify({ ...body, refresh_token: refreshToken });
+
+      fetchOptions.body = JSON.stringify({
+        ...body,
+        refresh_token: refreshToken
+      });
     }
 
     const abortController = new AbortController();
     const { signal } = abortController;
 
-    let response;
+    let response: any;
+
     try {
       response = await Promise.race([
         wait(timeout),
-        fetch(url, { ...opts, signal })
+        fetch(fetchUrl, { ...fetchOptions, signal })
       ]);
     } catch (error) {
       // fetch error, reject `sendMessage` using `error` key so that we retry.
       port.postMessage({
         error: error.message
       });
+
       return;
     }
 
diff --git a/src/worker/worker.types.ts b/src/worker/worker.types.ts
new file mode 100644
index 0000000..d3b3da5
--- /dev/null
+++ b/src/worker/worker.types.ts
@@ -0,0 +1,14 @@
+import { FetchOptions } from '../global';
+
+/**
+ * @ts-ignore
+ */
+export type WorkerRefreshTokenMessage = {
+  timeout: number;
+  fetchUrl: string;
+  fetchOptions: FetchOptions;
+  auth?: {
+    audience?: string;
+    scope?: string;
+  };
+};
diff --git a/src/worker/worker.utils.ts b/src/worker/worker.utils.ts
new file mode 100644
index 0000000..ec8f63e
--- /dev/null
+++ b/src/worker/worker.utils.ts
@@ -0,0 +1,22 @@
+import { WorkerRefreshTokenMessage } from './worker.types';
+
+/**
+ * Sends the specified message to the web worker
+ * @param message The message to send
+ * @param to The worker to send the message to
+ */
+export const sendMessage = (message: WorkerRefreshTokenMessage, to: Worker) =>
+  new Promise(function (resolve, reject) {
+    const messageChannel = new MessageChannel();
+
+    messageChannel.port1.onmessage = function (event) {
+      // Only for fetch errors, as these get retried
+      if (event.data.error) {
+        reject(new Error(event.data.error));
+      } else {
+        resolve(event.data);
+      }
+    };
+
+    to.postMessage(message, [messageChannel.port2]);
+  });
diff --git a/tsconfig.json b/tsconfig.json
index ad200b5..41ea346 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,8 @@
     "sourceMap": true,
     "declaration": true,
     "declarationDir": "./dist/typings",
-    "moduleResolution": "node"
+    "moduleResolution": "node",
+    "noImplicitAny": true
   },
   "exclude": ["./__tests__", "./dist/typings", "**/__mocks__/**"]
 }
diff --git a/typedoc.js b/typedoc.js
index 307d1f3..93cdcac 100644
--- a/typedoc.js
+++ b/typedoc.js
@@ -4,8 +4,9 @@ const excludeFiles = [
   'storage',
   'transaction-manager',
   'utils',
-  'token.worker',
-  'user-agent'
+  'user-agent',
+  'api',
+  'http'
 ];
 
 module.exports = {
@@ -17,6 +18,7 @@ module.exports = {
     '**/cypress/**/*',
     '**/node_modules/**/*',
     '**/__mocks__/**/*',
+    'src/worker/**/*',
     ...excludeFiles.map(f => `./src/${f}.ts`)
   ],
   mode: 'file',


Test patch:
diff --git a/__tests__/Auth0Client/buildLogoutUrl.test.ts b/__tests__/Auth0Client/buildLogoutUrl.test.ts
index 8e824c0..e9c26e7 100644
--- a/__tests__/Auth0Client/buildLogoutUrl.test.ts
+++ b/__tests__/Auth0Client/buildLogoutUrl.test.ts
@@ -14,7 +14,7 @@ import { TEST_CLIENT_ID, TEST_CODE_CHALLENGE, TEST_DOMAIN } from '../constants';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/checkSession.test.ts b/__tests__/Auth0Client/checkSession.test.ts
index 6c0fc46..f959f2c 100644
--- a/__tests__/Auth0Client/checkSession.test.ts
+++ b/__tests__/Auth0Client/checkSession.test.ts
@@ -21,7 +21,7 @@ import {
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/constructor.test.ts b/__tests__/Auth0Client/constructor.test.ts
index 0788653..7786bd4 100644
--- a/__tests__/Auth0Client/constructor.test.ts
+++ b/__tests__/Auth0Client/constructor.test.ts
@@ -14,7 +14,7 @@ import { TEST_CLIENT_ID, TEST_CODE_CHALLENGE, TEST_DOMAIN } from '../constants';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/getIdTokenClaims.test.ts b/__tests__/Auth0Client/getIdTokenClaims.test.ts
index 146761b..847a50d 100644
--- a/__tests__/Auth0Client/getIdTokenClaims.test.ts
+++ b/__tests__/Auth0Client/getIdTokenClaims.test.ts
@@ -14,7 +14,7 @@ import { TEST_CODE_CHALLENGE } from '../constants';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/getTokenSilently.test.ts b/__tests__/Auth0Client/getTokenSilently.test.ts
index 5eae71e..a54b5be 100644
--- a/__tests__/Auth0Client/getTokenSilently.test.ts
+++ b/__tests__/Auth0Client/getTokenSilently.test.ts
@@ -5,6 +5,7 @@ import { verify } from '../../src/jwt';
 import { MessageChannel } from 'worker_threads';
 import * as utils from '../../src/utils';
 import * as scope from '../../src/scope';
+import * as api from '../../src/api';
 
 import { expectToHaveBeenCalledWithAuth0ClientParam } from '../helpers';
 
@@ -43,7 +44,7 @@ import { DEFAULT_AUTH0_CLIENT } from '../../src/constants';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
@@ -285,7 +286,7 @@ describe('Auth0Client', () => {
         useRefreshTokens: true
       });
 
-      jest.spyOn(<any>utils, 'oauthToken');
+      jest.spyOn(<any>api, 'oauthToken');
       jest.spyOn(<any>utils, 'runIframe').mockResolvedValue({
         access_token: TEST_ACCESS_TOKEN,
         refresh_token: TEST_REFRESH_TOKEN,
@@ -297,7 +298,7 @@ describe('Auth0Client', () => {
         timeoutInSeconds: 10
       });
 
-      expect(utils.oauthToken).toHaveBeenCalledWith(
+      expect(api.oauthToken).toHaveBeenCalledWith(
         expect.objectContaining({
           timeout: 10000
         }),
diff --git a/__tests__/Auth0Client/getTokenWithPopup.test.ts b/__tests__/Auth0Client/getTokenWithPopup.test.ts
index 5ffaad8..4b6cd70 100644
--- a/__tests__/Auth0Client/getTokenWithPopup.test.ts
+++ b/__tests__/Auth0Client/getTokenWithPopup.test.ts
@@ -22,7 +22,7 @@ import { Auth0ClientOptions } from '../../src';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/getUser.test.ts b/__tests__/Auth0Client/getUser.test.ts
index 748d700..f86dcbf 100644
--- a/__tests__/Auth0Client/getUser.test.ts
+++ b/__tests__/Auth0Client/getUser.test.ts
@@ -14,7 +14,7 @@ import { TEST_CODE_CHALLENGE } from '../constants';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/handleRedirectCallback.test.ts b/__tests__/Auth0Client/handleRedirectCallback.test.ts
index e8de8a6..e4ef4e3 100644
--- a/__tests__/Auth0Client/handleRedirectCallback.test.ts
+++ b/__tests__/Auth0Client/handleRedirectCallback.test.ts
@@ -11,15 +11,24 @@ import { fetchResponse, loginWithRedirectFn, setupFn } from './helpers';
 
 import {
   TEST_ACCESS_TOKEN,
+  TEST_APP_STATE,
+  TEST_CLIENT_ID,
+  TEST_CODE,
   TEST_CODE_CHALLENGE,
+  TEST_DOMAIN,
+  TEST_ENCODED_STATE,
   TEST_ID_TOKEN,
-  TEST_REFRESH_TOKEN
+  TEST_RANDOM_STRING,
+  TEST_REFRESH_TOKEN,
+  TEST_SCOPES,
+  TEST_USER_ID
 } from '../constants';
+import { Auth0ClientOptions } from '../../src/global';
 
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
@@ -200,111 +209,139 @@ describe('Auth0Client', () => {
       expect(result).toBeDefined();
       expect(result.appState).toBe(appState);
     });
+  });
 
-    describe('when there is a valid query string in a hash', () => {
-      it('should throw an error if the /authorize call redirects with an error param', async () => {
-        const auth0 = setup();
-        let error;
-        const appState = {
-          key: 'property'
-        };
-        try {
-          await loginWithRedirect(
-            auth0,
-            { appState },
-            {
-              authorize: {
-                state: 'error-state',
-                error: 'some-error',
-                errorDescription: 'some-error-description'
-              },
-              useHash: true
-            }
-          );
-        } catch (e) {
-          error = e;
+  it('calls oauth/token without redirect uri if not set in transaction', async () => {
+    window.history.pushState(
+      {},
+      'Test',
+      `#/callback/?code=${TEST_CODE}&state=${TEST_ENCODED_STATE}`
+    );
+
+    mockFetch.mockResolvedValueOnce(
+      fetchResponse(true, {
+        id_token: TEST_ID_TOKEN,
+        refresh_token: TEST_REFRESH_TOKEN,
+        access_token: TEST_ACCESS_TOKEN,
+        expires_in: 86400
+      })
+    );
+
+    const auth0 = setup();
+    delete auth0['options']['redirect_uri'];
+
+    await auth0.loginWithRedirect();
+    await auth0.handleRedirectCallback();
+
+    expect(mockFetch.mock.calls[0][0]).toBe('https://auth0_domain/oauth/token');
+
+    const fetchBody = JSON.parse(mockFetch.mock.calls[0][1].body);
+    expect(fetchBody.redirect_uri).toBeUndefined();
+  });
+
+  describe('when there is a valid query string in a hash', () => {
+    it('should throw an error if the /authorize call redirects with an error param', async () => {
+      const auth0 = setup();
+      let error;
+      const appState = {
+        key: 'property'
+      };
+      try {
+        await loginWithRedirect(
+          auth0,
+          { appState },
+          {
+            authorize: {
+              state: 'error-state',
+              error: 'some-error',
+              errorDescription: 'some-error-description'
+            },
+            useHash: true
+          }
+        );
+      } catch (e) {
+        error = e;
+      }
+      expect(error).toBeDefined();
+      expect(error.error).toBe('some-error');
+      expect(error.error_description).toBe('some-error-description');
+      expect(error.state).toBe('error-state');
+      expect(error.appState).toBe(appState);
+    });
+
+    it('should clear the transaction data when the /authorize call redirects with a code param', async () => {
+      const auth0 = setup();
+
+      jest.spyOn(auth0['transactionManager'], 'remove');
+      await loginWithRedirect(
+        auth0,
+        {},
+        {
+          useHash: true
         }
-        expect(error).toBeDefined();
-        expect(error.error).toBe('some-error');
-        expect(error.error_description).toBe('some-error-description');
-        expect(error.state).toBe('error-state');
-        expect(error.appState).toBe(appState);
-      });
+      );
 
-      it('should clear the transaction data when the /authorize call redirects with a code param', async () => {
-        const auth0 = setup();
+      expect(auth0['transactionManager'].remove).toHaveBeenCalledWith();
+    });
 
-        jest.spyOn(auth0['transactionManager'], 'remove');
+    it('should clear the transaction data when the /authorize call redirects with an error param', async () => {
+      const auth0 = setup();
+      let error;
+      jest.spyOn(auth0['transactionManager'], 'remove');
+
+      try {
         await loginWithRedirect(
           auth0,
           {},
           {
+            authorize: {
+              error: 'some-error'
+            },
             useHash: true
           }
         );
+      } catch (e) {
+        error = e;
+      }
 
-        expect(auth0['transactionManager'].remove).toHaveBeenCalledWith();
-      });
-
-      it('should clear the transaction data when the /authorize call redirects with an error param', async () => {
-        const auth0 = setup();
-        let error;
-        jest.spyOn(auth0['transactionManager'], 'remove');
-
-        try {
-          await loginWithRedirect(
-            auth0,
-            {},
-            {
-              authorize: {
-                error: 'some-error'
-              },
-              useHash: true
-            }
-          );
-        } catch (e) {
-          error = e;
-        }
+      expect(error).toBeDefined();
+      expect(auth0['transactionManager'].remove).toHaveBeenCalledWith();
+    });
 
-        expect(error).toBeDefined();
-        expect(auth0['transactionManager'].remove).toHaveBeenCalledWith();
-      });
-
-      it('should throw an error if the /authorize call redirects with no params', async () => {
-        const auth0 = setup();
-        let error;
-        try {
-          await loginWithRedirect(
-            auth0,
-            {},
-            {
-              authorize: {
-                state: null,
-                code: null
-              },
-              useHash: true
-            }
-          );
-        } catch (e) {
-          error = e;
-        }
-        expect(error).toBeDefined();
-        expect(error.message).toBe(
-          'There are no query params available for parsing.'
+    it('should throw an error if the /authorize call redirects with no params', async () => {
+      const auth0 = setup();
+      let error;
+      try {
+        await loginWithRedirect(
+          auth0,
+          {},
+          {
+            authorize: {
+              state: null,
+              code: null
+            },
+            useHash: true
+          }
         );
-      });
-
-      it('should throw an error if there is no transaction', async () => {
-        const auth0 = setup();
-        let error;
-        try {
-          await auth0.handleRedirectCallback('#test?foo=bar');
-        } catch (e) {
-          error = e;
-        }
-        expect(error).toBeDefined();
-        expect(error.message).toBe('Invalid state');
-      });
+      } catch (e) {
+        error = e;
+      }
+      expect(error).toBeDefined();
+      expect(error.message).toBe(
+        'There are no query params available for parsing.'
+      );
+    });
+
+    it('should throw an error if there is no transaction', async () => {
+      const auth0 = setup();
+      let error;
+      try {
+        await auth0.handleRedirectCallback('#test?foo=bar');
+      } catch (e) {
+        error = e;
+      }
+      expect(error).toBeDefined();
+      expect(error.message).toBe('Invalid state');
     });
   });
 });
diff --git a/__tests__/Auth0Client/isAuthenticated.test.ts b/__tests__/Auth0Client/isAuthenticated.test.ts
index 0acd8c9..81abfe8 100644
--- a/__tests__/Auth0Client/isAuthenticated.test.ts
+++ b/__tests__/Auth0Client/isAuthenticated.test.ts
@@ -14,7 +14,7 @@ import { TEST_CODE_CHALLENGE } from '../constants';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/loginWithPopup.test.ts b/__tests__/Auth0Client/loginWithPopup.test.ts
index 55a548a..b1cddee 100644
--- a/__tests__/Auth0Client/loginWithPopup.test.ts
+++ b/__tests__/Auth0Client/loginWithPopup.test.ts
@@ -38,7 +38,7 @@ import version from '../../src/version';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/loginWithRedirect.test.ts b/__tests__/Auth0Client/loginWithRedirect.test.ts
index 43792ea..2f91db8 100644
--- a/__tests__/Auth0Client/loginWithRedirect.test.ts
+++ b/__tests__/Auth0Client/loginWithRedirect.test.ts
@@ -38,7 +38,7 @@ import version from '../../src/version';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/Auth0Client/logout.test.ts b/__tests__/Auth0Client/logout.test.ts
index f03d0cd..e2d5f7f 100644
--- a/__tests__/Auth0Client/logout.test.ts
+++ b/__tests__/Auth0Client/logout.test.ts
@@ -19,7 +19,7 @@ import { TEST_CLIENT_ID, TEST_CODE_CHALLENGE, TEST_DOMAIN } from '../constants';
 jest.mock('unfetch');
 jest.mock('es-cookie');
 jest.mock('../../src/jwt');
-jest.mock('../../src/token.worker');
+jest.mock('../../src/worker/token.worker');
 
 const mockWindow = <any>global;
 const mockFetch = (mockWindow.fetch = <jest.Mock>unfetch);
diff --git a/__tests__/api.test.ts b/__tests__/api.test.ts
new file mode 100644
index 0000000..a7f0736
--- /dev/null
+++ b/__tests__/api.test.ts
@@ -0,0 +1,334 @@
+import fetch from 'unfetch';
+
+import {
+  DEFAULT_AUTH0_CLIENT,
+  DEFAULT_SILENT_TOKEN_RETRY_COUNT
+} from '../src/constants';
+
+import version from '../src/version';
+import { oauthToken } from '../src/api';
+
+// @ts-ignore
+import Worker from '../src/worker/token.worker';
+import { MessageChannel } from 'worker_threads';
+import { TEST_REDIRECT_URI } from './constants';
+(<any>global).MessageChannel = MessageChannel;
+
+jest.mock('../src/worker/token.worker');
+jest.mock('unfetch');
+
+const mockFetch = <jest.Mock>fetch;
+(<any>global).MessageChannel = MessageChannel;
+(<any>global).fetch = mockFetch;
+
+describe('oauthToken', () => {
+  let abortController;
+
+  beforeEach(() => {
+    const http = require('../src/http');
+
+    // Set up an AbortController that we can test has been called in the event of a timeout
+    abortController = new AbortController();
+    jest.spyOn(abortController, 'abort');
+    http.createAbortController = jest.fn(() => abortController);
+  });
+
+  afterEach(() => {
+    jest.resetAllMocks();
+  });
+
+  it('calls oauth/token with the correct url', async () => {
+    mockFetch.mockReturnValue(
+      new Promise(res =>
+        res({ ok: true, json: () => new Promise(ress => ress(true)) })
+      )
+    );
+    const auth0Client = {
+      name: 'auth0-spa-js',
+      version: version
+    };
+
+    await oauthToken({
+      redirect_uri: 'http://localhost',
+      grant_type: 'authorization_code',
+      baseUrl: 'https://test.com',
+      client_id: 'client_idIn',
+      code: 'codeIn',
+      code_verifier: 'code_verifierIn',
+      auth0Client
+    });
+
+    expect(mockFetch).toBeCalledWith('https://test.com/oauth/token', {
+      body:
+        '{"redirect_uri":"http://localhost","grant_type":"authorization_code","client_id":"client_idIn","code":"codeIn","code_verifier":"code_verifierIn"}',
+      headers: {
+        'Content-type': 'application/json',
+        'Auth0-Client': btoa(JSON.stringify(auth0Client))
+      },
+      method: 'POST',
+      signal: abortController.signal
+    });
+
+    expect(mockFetch.mock.calls[0][1].signal).not.toBeUndefined();
+  });
+
+  it('calls oauth/token with a worker with the correct url', async () => {
+    mockFetch.mockReturnValue(
+      new Promise(res =>
+        res({ ok: true, json: () => new Promise(ress => ress(true)) })
+      )
+    );
+
+    const worker = new Worker();
+    const spy = jest.spyOn(worker, 'postMessage');
+
+    const body = {
+      redirect_uri: 'http://localhost',
+      grant_type: 'authorization_code',
+      client_id: 'client_idIn',
+      code: 'codeIn',
+      code_verifier: 'code_verifierIn'
+    };
+
+    const auth0Client = {
+      name: 'auth0-spa-js',
+      version: version
+    };
+
+    await oauthToken(
+      {
+        redirect_uri: 'http://localhost',
+        grant_type: 'authorization_code',
+        baseUrl: 'https://test.com',
+        client_id: 'client_idIn',
+        code: 'codeIn',
+        code_verifier: 'code_verifierIn',
+        audience: '__test_audience__',
+        scope: '__test_scope__',
+        auth0Client
+      },
+      worker
+    );
+
+    expect(mockFetch).toBeCalledWith('https://test.com/oauth/token', {
+      body: JSON.stringify(body),
+      headers: {
+        'Content-type': 'application/json',
+        'Auth0-Client': btoa(JSON.stringify(auth0Client))
+      },
+      method: 'POST',
+      signal: abortController.signal
+    });
+
+    expect(mockFetch.mock.calls[0][1].signal).not.toBeUndefined();
+
+    expect(spy).toHaveBeenCalledWith(
+      {
+        fetchUrl: 'https://test.com/oauth/token',
+        fetchOptions: {
+          body: JSON.stringify(body),
+          headers: {
+            'Content-type': 'application/json',
+            'Auth0-Client': btoa(JSON.stringify(auth0Client))
+          },
+          method: 'POST'
+        },
+        auth: {
+          audience: '__test_audience__',
+          scope: '__test_scope__'
+        },
+        timeout: 10000
+      },
+      expect.arrayContaining([expect.anything()])
+    );
+  });
+
+  it('handles error with error response', async () => {
+    const theError = {
+      error: 'the-error',
+      error_description: 'the-error-description'
+    };
+
+    mockFetch.mockReturnValue(
+      new Promise(res =>
+        res({
+          ok: false,
+          json: () => new Promise(ress => ress(theError))
+        })
+      )
+    );
+
+    try {
+      await oauthToken({
+        baseUrl: 'https://test.com',
+        client_id: 'client_idIn',
+        code: 'codeIn',
+        code_verifier: 'code_verifierIn',
+        grant_type: 'authorization_code',
+        auth0Client: DEFAULT_AUTH0_CLIENT
+      });
+    } catch (error) {
+      expect(error.message).toBe(theError.error_description);
+      expect(error.error).toBe(theError.error);
+      expect(error.error_description).toBe(theError.error_description);
+    }
+  });
+
+  it('handles error without error response', async () => {
+    mockFetch.mockReturnValue(
+      new Promise(res =>
+        res({
+          ok: false,
+          json: () => new Promise(ress => ress(false))
+        })
+      )
+    );
+
+    try {
+      await oauthToken({
+        baseUrl: 'https://test.com',
+        client_id: 'client_idIn',
+        code: 'codeIn',
+        code_verifier: 'code_verifierIn',
+        grant_type: 'authorization_code',
+        auth0Client: DEFAULT_AUTH0_CLIENT
+      });
+    } catch (error) {
+      expect(error.message).toBe(
+        `HTTP error. Unable to fetch https://test.com/oauth/token`
+      );
+      expect(error.error).toBe('request_error');
+      expect(error.error_description).toBe(
+        `HTTP error. Unable to fetch https://test.com/oauth/token`
+      );
+    }
+  });
+
+  it('retries the request in the event of a network failure', async () => {
+    // Fetch only fails in the case of a network issue, so should be
+    // retried here. Failure status (4xx, 5xx, etc) return a resolved Promise
+    // with the failure in the body.
+    // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
+    mockFetch.mockReturnValue(Promise.reject(new Error('Network failure')));
+
+    try {
+      await oauthToken({
+        baseUrl: 'https://test.com',
+        client_id: 'client_idIn',
+        code: 'codeIn',
+        code_verifier: 'code_verifierIn',
+        grant_type: 'authorization_code',
+        auth0Client: DEFAULT_AUTH0_CLIENT
+      });
+    } catch (error) {
+      expect(error.message).toBe('Network failure');
+
+      expect(mockFetch).toHaveBeenCalledTimes(DEFAULT_SILENT_TOKEN_RETRY_COUNT);
+    }
+  });
+
+  it('continues the program after failing a couple of times then succeeding', async () => {
+    // Fetch only fails in the case of a network issue, so should be
+    // retried here. Failure status (4xx, 5xx, etc) return a resolved Promise
+    // with the failure in the body.
+    // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
+    mockFetch
+      .mockReturnValueOnce(Promise.reject(new Error('Network failure')))
+      .mockReturnValueOnce(Promise.reject(new Error('Network failure')))
+      .mockReturnValue(
+        Promise.resolve({
+          ok: true,
+          json: () => Promise.resolve({ access_token: 'access-token' })
+        })
+      );
+
+    const result = await oauthToken({
+      baseUrl: 'https://test.com',
+      client_id: 'client_idIn',
+      code: 'codeIn',
+      code_verifier: 'code_verifierIn',
+      grant_type: 'authorization_code',
+      auth0Client: DEFAULT_AUTH0_CLIENT
+    });
+
+    expect(result.access_token).toBe('access-token');
+    expect(mockFetch).toHaveBeenCalledTimes(3);
+    expect(abortController.abort).not.toHaveBeenCalled();
+  });
+
+  it('throws a fetch error when the network is down', async () => {
+    mockFetch.mockReturnValue(Promise.reject(new ProgressEvent('error')));
+
+    await expect(
+      oauthToken({
+        baseUrl: 'https://test.com',
+        client_id: 'client_idIn',
+        code: 'codeIn',
+        code_verifier: 'code_verifierIn',
+        grant_type: 'authorization_code',
+        auth0Client: DEFAULT_AUTH0_CLIENT
+      })
+    ).rejects.toMatchObject({ message: 'Failed to fetch' });
+  });
+
+  it('surfaces a timeout error when the fetch continuously times out', async () => {
+    const createPromise = () =>
+      new Promise((resolve, _) => {
+        setTimeout(
+          () =>
+            resolve({
+              ok: true,
+              json: () => Promise.resolve({ access_token: 'access-token' })
+            }),
+          500
+        );
+      });
+
+    mockFetch.mockReturnValue(createPromise());
+
+    try {
+      await oauthToken({
+        baseUrl: 'https://test.com',
+        client_id: 'client_idIn',
+        code: 'codeIn',
+        code_verifier: 'code_verifierIn',
+        timeout: 100,
+        grant_type: 'authorization_code',
+        auth0Client: DEFAULT_AUTH0_CLIENT
+      });
+    } catch (e) {
+      expect(e.message).toBe("Timeout when executing 'fetch'");
+      expect(mockFetch).toHaveBeenCalledTimes(3);
+      expect(abortController.abort).toHaveBeenCalledTimes(3);
+    }
+  });
+
+  it('retries the request in the event of a timeout', async () => {
+    const fetchResult = {
+      ok: true,
+      json: () => Promise.resolve({ access_token: 'access-token' })
+    };
+
+    mockFetch.mockReturnValueOnce(
+      new Promise((resolve, _) => {
+        setTimeout(() => resolve(fetchResult), 1000);
+      })
+    );
+
+    mockFetch.mockReturnValue(Promise.resolve(fetchResult));
+
+    const result = await oauthToken({
+      baseUrl: 'https://test.com',
+      client_id: 'client_idIn',
+      code: 'codeIn',
+      code_verifier: 'code_verifierIn',
+      timeout: 500,
+      grant_type: 'authorization_code',
+      auth0Client: DEFAULT_AUTH0_CLIENT
+    });
+
+    expect(result.access_token).toBe('access-token');
+    expect(mockFetch).toHaveBeenCalledTimes(2);
+    expect(abortController.abort).toHaveBeenCalled();
+  });
+});
diff --git a/__tests__/http.test.ts b/__tests__/http.test.ts
new file mode 100644
index 0000000..9bf7281
--- /dev/null
+++ b/__tests__/http.test.ts
@@ -0,0 +1,21 @@
+import fetch from 'unfetch';
+import { fetchWithTimeout } from '../src/http';
+
+jest.mock('../src/worker/token.worker');
+jest.mock('unfetch');
+
+const mockUnfetch = <jest.Mock>fetch;
+
+describe('fetchWithTimeout', () => {
+  it('clears timeout when successful', async () => {
+    mockUnfetch.mockImplementation(() =>
+      Promise.resolve({
+        ok: true,
+        json: () => Promise.resolve()
+      })
+    );
+    jest.spyOn(window, 'clearTimeout');
+    await fetchWithTimeout('https://test.com/', null, null, {}, undefined);
+    expect(clearTimeout).toBeCalledTimes(1);
+  });
+});
diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts
index ba0a54e..550c9f4 100644
--- a/__tests__/index.test.ts
+++ b/__tests__/index.test.ts
@@ -1,4 +1,3 @@
-import { expectToHaveBeenCalledWithAuth0ClientParam } from './helpers';
 import { CacheLocation, Auth0ClientOptions } from '../src/global';
 import * as scope from '../src/scope';
 
@@ -26,18 +25,14 @@ jest.mock('../src/storage', () => ({
 
 jest.mock('../src/transaction-manager');
 jest.mock('../src/utils');
+jest.mock('../src/api');
 
-import createAuth0Client, {
-  Auth0Client,
-  GetTokenSilentlyOptions
-} from '../src/index';
+import createAuth0Client, { Auth0Client } from '../src/index';
 
 import {
-  GET_TOKEN_SILENTLY_LOCK_KEY,
   TEST_ACCESS_TOKEN,
   TEST_APP_STATE,
   TEST_ARRAY_BUFFER,
-  TEST_AUTH0_CLIENT_QUERY_STRING,
   TEST_BASE64_ENCODED_STRING,
   TEST_CLIENT_ID,
   TEST_CODE,
@@ -46,7 +41,6 @@ import {
   TEST_ID_TOKEN,
   TEST_QUERY_PARAMS,
   TEST_RANDOM_STRING,
-  TEST_REFRESH_TOKEN,
   TEST_SCOPES,
   TEST_USER_ID
 } from './constants';
@@ -64,7 +58,7 @@ jest.mock('../src/cache', () => ({
   LocalStorageCache: () => mockEnclosedCache
 }));
 
-jest.mock('../src/token.worker');
+jest.mock('../src/worker/token.worker');
 
 const webWorkerMatcher = expect.objectContaining({
   postMessage: expect.any(Function)
@@ -75,6 +69,7 @@ const setup = async (clientOptions: Partial<Auth0ClientOptions> = {}) => {
   const cache = mockEnclosedCache;
   const tokenVerifier = require('../src/jwt').verify;
   const utils = require('../src/utils');
+  const api = require('../src/api');
 
   utils.createQueryParams.mockReturnValue(TEST_QUERY_PARAMS);
   utils.encode.mockReturnValue(TEST_ENCODED_STATE);
@@ -95,7 +90,7 @@ const setup = async (clientOptions: Partial<Auth0ClientOptions> = {}) => {
     Promise.resolve({ state: TEST_ENCODED_STATE, code: TEST_CODE })
   );
 
-  utils.oauthToken.mockReturnValue(
+  api.oauthToken.mockReturnValue(
     Promise.resolve({
       id_token: TEST_ID_TOKEN,
       access_token: TEST_ACCESS_TOKEN
@@ -131,7 +126,8 @@ const setup = async (clientOptions: Partial<Auth0ClientOptions> = {}) => {
     tokenVerifier,
     transactionManager,
     utils,
-    popup
+    popup,
+    api
   };
 };
 
@@ -264,135 +260,6 @@ describe('Auth0', () => {
       });
     });
   });
-
-  describe('handleRedirectCallback()', () => {
-    describe('when there is a valid query string in the url', () => {
-      const localSetup = async (
-        clientOptions?: Partial<Auth0ClientOptions>
-      ) => {
-        window.history.pushState(
-          {},
-          'Test',
-          `?code=${TEST_CODE}&state=${TEST_ENCODED_STATE}`
-        );
-        const result = await setup(clientOptions);
-        result.transactionManager.get.mockReturnValue({
-          code_verifier: TEST_RANDOM_STRING,
-          nonce: TEST_ENCODED_STATE,
-          audience: 'default',
-          scope: TEST_SCOPES,
-          appState: TEST_APP_STATE
-        });
-        result.cache.get.mockReturnValue({ access_token: TEST_ACCESS_TOKEN });
-        return result;
-      };
-
-      xit('calls oauth/token without redirect uri if not set in transaction', async () => {
-        const { auth0, utils, transactionManager } = await localSetup();
-        const txn = transactionManager.get.mockReturnValue({
-          code_verifier: TEST_RANDOM_STRING,
-          nonce: TEST_RANDOM_STRING,
-          audience: 'default',
-          scope: TEST_SCOPES,
-          appState: TEST_APP_STATE
-        });
-        await auth0.handleRedirectCallback();
-        const arg = utils.oauthToken.mock.calls[0][0];
-        expect(arg.hasOwnProperty('redirect_uri')).toBeFalsy();
-      });
-    });
-    describe('when there is a valid query string in a hash', () => {
-      const localSetup = async (
-        clientOptions?: Partial<Auth0ClientOptions>
-      ) => {
-        window.history.pushState({}, 'Test', `/`);
-        window.history.pushState(
-          {},
-          'Test',
-          `#/callback/?code=${TEST_CODE}&state=${TEST_ENCODED_STATE}`
-        );
-        const result = await setup(clientOptions);
-        result.transactionManager.get.mockReturnValue({
-          code_verifier: TEST_RANDOM_STRING,
-          nonce: TEST_ENCODED_STATE,
-          audience: 'default',
-          scope: TEST_SCOPES,
-          appState: TEST_APP_STATE
-        });
-        result.cache.get.mockReturnValue({ access_token: TEST_ACCESS_TOKEN });
-        return result;
-      };
-
-      it('calls `tokenVerifier.verify` with the `id_token` from in the oauth/token response', async () => {
-        const { auth0, tokenVerifier } = await localSetup();
-
-        await auth0.handleRedirectCallback();
-
-        expect(tokenVerifier).toHaveBeenCalledWith({
-          id_token: TEST_ID_TOKEN,
-          nonce: TEST_ENCODED_STATE,
-          aud: TEST_CLIENT_ID,
-          iss: `https://${TEST_DOMAIN}/`,
-          leeway: undefined,
-          max_age: undefined
-        });
-      });
-      it('saves cache', async () => {
-        const { auth0, cache } = await localSetup();
-
-        await auth0.handleRedirectCallback();
-
-        expect(cache.save).toHaveBeenCalledWith({
-          client_id: TEST_CLIENT_ID,
-          access_token: TEST_ACCESS_TOKEN,
-          audience: 'default',
-          id_token: TEST_ID_TOKEN,
-          scope: TEST_SCOPES,
-          decodedToken: {
-            claims: { sub: TEST_USER_ID, aud: TEST_CLIENT_ID },
-            user: { sub: TEST_USER_ID }
-          }
-        });
-      });
-      it('saves `auth0.is.authenticated` key in storage', async () => {
-        const { auth0, cookieStorage } = await localSetup();
-
-        await auth0.handleRedirectCallback();
-
-        expect(cookieStorage.save).toHaveBeenCalledWith(
-          'auth0.is.authenticated',
-          true,
-          {
-            daysUntilExpire: 1
-          }
-        );
-      });
-      it('saves `auth0.is.authenticated` key in storage for an extended period', async () => {
-        const { auth0, cookieStorage } = await localSetup({
-          sessionCheckExpiryDays: 2
-        });
-
-        await auth0.handleRedirectCallback();
-
-        expect(cookieStorage.save).toHaveBeenCalledWith(
-          'auth0.is.authenticated',
-          true,
-          {
-            daysUntilExpire: 2
-          }
-        );
-      });
-      it('returns the transactions appState', async () => {
-        const { auth0 } = await localSetup();
-
-        const response = await auth0.handleRedirectCallback();
-
-        expect(response).toEqual({
-          appState: TEST_APP_STATE
-        });
-      });
-    });
-  });
 });
 
 describe('default creation function', () => {
diff --git a/__tests__/token.worker.test.ts b/__tests__/token.worker.test.ts
index a1200cc..6eba15c 100644
--- a/__tests__/token.worker.test.ts
+++ b/__tests__/token.worker.test.ts
@@ -14,7 +14,8 @@ describe('token worker', () => {
     // The web worker uses native fetch.
     window.fetch = mockFetch;
 
-    const { messageHandler } = require('../src/token.worker');
+    const { messageHandler } = require('../src/worker/token.worker');
+
     messageHandlerAsync = opts =>
       new Promise(resolve =>
         messageHandler({ data: opts, ports: [{ postMessage: resolve }] })
@@ -36,10 +37,13 @@ describe('token worker', () => {
       })
     );
     const response = await messageHandlerAsync({
-      url: '/foo',
-      method: 'POST',
-      body: JSON.stringify({})
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({})
+      }
     });
+
     expect(response.json).toEqual({
       foo: 'bar'
     });
@@ -53,19 +57,25 @@ describe('token worker', () => {
       })
     );
     await messageHandlerAsync({
-      url: '/foo',
-      method: 'POST',
-      body: JSON.stringify({
-        grant_type: 'authorization_code'
-      })
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({
+          grant_type: 'authorization_code'
+        })
+      }
     });
+
     await messageHandlerAsync({
-      url: '/foo',
-      method: 'POST',
-      body: JSON.stringify({
-        grant_type: 'refresh_token'
-      })
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({
+          grant_type: 'refresh_token'
+        })
+      }
     });
+
     expect(JSON.parse(mockFetch.mock.calls[1][1].body)).toEqual({
       grant_type: 'refresh_token',
       refresh_token: 'foo'
@@ -74,12 +84,15 @@ describe('token worker', () => {
 
   it(`errors with grant_type='refresh_token' and no token is stored`, async () => {
     const response = await messageHandlerAsync({
-      url: '/foo',
-      method: 'POST',
-      body: JSON.stringify({
-        grant_type: 'refresh_token'
-      })
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({
+          grant_type: 'refresh_token'
+        })
+      }
     });
+
     expect(response.json.error_description).toEqual(
       MISSING_REFRESH_TOKEN_ERROR_MESSAGE
     );
@@ -88,9 +101,11 @@ describe('token worker', () => {
   it(`errors when fetch rejects`, async () => {
     mockFetch.mockReturnValue(Promise.reject(new Error('fail')));
     const response = await messageHandlerAsync({
-      url: '/foo',
-      method: 'POST',
-      body: JSON.stringify({})
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({})
+      }
     });
     expect(response.error).toEqual('fail');
   });
@@ -111,27 +126,33 @@ describe('token worker', () => {
       );
 
     await messageHandlerAsync({
-      url: '/foo',
-      method: 'POST',
-      body: JSON.stringify({
-        grant_type: 'authorization_code'
-      })
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({
+          grant_type: 'authorization_code'
+        })
+      }
     });
 
     await messageHandlerAsync({
-      url: '/foo',
-      method: 'POST',
-      body: JSON.stringify({
-        grant_type: 'refresh_token'
-      })
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({
+          grant_type: 'refresh_token'
+        })
+      }
     });
 
     const result = await messageHandlerAsync({
-      url: '/foo',
-      method: 'POST',
-      body: JSON.stringify({
-        grant_type: 'refresh_token'
-      })
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({
+          grant_type: 'refresh_token'
+        })
+      }
     });
 
     expect(result.ok).toBe(false);
diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts
index 0ad8a5e..c19726a 100644
--- a/__tests__/utils.test.ts
+++ b/__tests__/utils.test.ts
@@ -1,4 +1,5 @@
 import 'fast-text-encoding';
+
 import {
   parseQueryResult,
   createQueryParams,
@@ -6,7 +7,6 @@ import {
   createRandomString,
   encode,
   decode,
-  fetchWithTimeout,
   sha256,
   runPopup,
   runIframe,
@@ -16,22 +16,9 @@ import {
   validateCrypto
 } from '../src/utils';
 
-import {
-  DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS,
-  DEFAULT_SILENT_TOKEN_RETRY_COUNT
-} from '../src/constants';
-import { MessageChannel } from 'worker_threads';
-import unfetch from 'unfetch';
-// @ts-ignore
-import Worker from '../src/token.worker';
-import version from '../src/version';
-
-jest.mock('../src/token.worker');
-jest.mock('unfetch');
-const mockUnfetch = <jest.Mock>unfetch;
+import { DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS } from '../src/constants';
+
 (<any>global).TextEncoder = TextEncoder;
-(<any>global).MessageChannel = MessageChannel;
-(<any>global).fetch = mockUnfetch;
 
 afterEach(() => {
   jest.resetAllMocks();
@@ -150,19 +137,7 @@ describe('utils', () => {
       expect(decode('dGVzdA==')).toBe('test');
     });
   });
-  describe('fetchWithTimeout', () => {
-    it('clears timeout when successful', async () => {
-      mockUnfetch.mockImplementation(() =>
-        Promise.resolve({
-          ok: true,
-          json: () => Promise.resolve()
-        })
-      );
-      jest.spyOn(window, 'clearTimeout');
-      await fetchWithTimeout('https://test.com/', null, null, {}, undefined);
-      expect(clearTimeout).toBeCalledTimes(1);
-    });
-  });
+
   describe('sha256', () => {
     it('generates a digest of the given data', async () => {
       (<any>global).crypto = {
@@ -255,294 +230,6 @@ describe('utils', () => {
     });
   });
 
-  describe('oauthToken', () => {
-    let oauthToken;
-    let abortController;
-
-    beforeEach(() => {
-      const utils = require('../src/utils');
-      oauthToken = utils.oauthToken;
-
-      // Set up an AbortController that we can test has been called in the event of a timeout
-      abortController = new AbortController();
-      jest.spyOn(abortController, 'abort');
-      utils.createAbortController = jest.fn(() => abortController);
-    });
-
-    it('calls oauth/token with the correct url', async () => {
-      mockUnfetch.mockReturnValue(
-        new Promise(res =>
-          res({ ok: true, json: () => new Promise(ress => ress(true)) })
-        )
-      );
-      const auth0Client = {
-        name: 'auth0-spa-js',
-        version: version
-      };
-
-      await oauthToken({
-        grant_type: 'authorization_code',
-        baseUrl: 'https://test.com',
-        client_id: 'client_idIn',
-        code: 'codeIn',
-        code_verifier: 'code_verifierIn',
-        auth0Client
-      });
-
-      expect(mockUnfetch).toBeCalledWith('https://test.com/oauth/token', {
-        body:
-          '{"redirect_uri":"http://localhost","grant_type":"authorization_code","client_id":"client_idIn","code":"codeIn","code_verifier":"code_verifierIn"}',
-        headers: {
-          'Content-type': 'application/json',
-          'Auth0-Client': btoa(JSON.stringify(auth0Client))
-        },
-        method: 'POST',
-        signal: abortController.signal
-      });
-
-      expect(mockUnfetch.mock.calls[0][1].signal).not.toBeUndefined();
-    });
-
-    it('calls oauth/token with a worker with the correct url', async () => {
-      mockUnfetch.mockReturnValue(
-        new Promise(res =>
-          res({ ok: true, json: () => new Promise(ress => ress(true)) })
-        )
-      );
-      const worker = new Worker();
-      const spy = jest.spyOn(worker, 'postMessage');
-      const body = {
-        redirect_uri: 'http://localhost',
-        grant_type: 'authorization_code',
-        client_id: 'client_idIn',
-        code: 'codeIn',
-        code_verifier: 'code_verifierIn'
-      };
-      const auth0Client = {
-        name: 'auth0-spa-js',
-        version: version
-      };
-
-      await oauthToken(
-        {
-          grant_type: 'authorization_code',
-          baseUrl: 'https://test.com',
-          client_id: 'client_idIn',
-          code: 'codeIn',
-          code_verifier: 'code_verifierIn',
-          audience: '__test_audience__',
-          scope: '__test_scope__',
-          auth0Client
-        },
-        worker
-      );
-
-      expect(mockUnfetch).toBeCalledWith('https://test.com/oauth/token', {
-        body: JSON.stringify(body),
-        headers: {
-          'Content-type': 'application/json',
-          'Auth0-Client': btoa(JSON.stringify(auth0Client))
-        },
-        method: 'POST',
-        signal: abortController.signal
-      });
-
-      expect(mockUnfetch.mock.calls[0][1].signal).not.toBeUndefined();
-      expect(spy).toHaveBeenCalledWith(
-        {
-          body: JSON.stringify(body),
-          audience: '__test_audience__',
-          scope: '__test_scope__',
-          headers: {
-            'Content-type': 'application/json',
-            'Auth0-Client': btoa(JSON.stringify(auth0Client))
-          },
-          method: 'POST',
-          timeout: 10000,
-          url: 'https://test.com/oauth/token'
-        },
-        expect.arrayContaining([expect.anything()])
-      );
-    });
-
-    it('handles error with error response', async () => {
-      const theError = {
-        error: 'the-error',
-        error_description: 'the-error-description'
-      };
-
-      mockUnfetch.mockReturnValue(
-        new Promise(res =>
-          res({
-            ok: false,
-            json: () => new Promise(ress => ress(theError))
-          })
-        )
-      );
-
-      try {
-        await oauthToken({
-          baseUrl: 'https://test.com',
-          client_id: 'client_idIn',
-          code: 'codeIn',
-          code_verifier: 'code_verifierIn'
-        });
-      } catch (error) {
-        expect(error.message).toBe(theError.error_description);
-        expect(error.error).toBe(theError.error);
-        expect(error.error_description).toBe(theError.error_description);
-      }
-    });
-
-    it('handles error without error response', async () => {
-      mockUnfetch.mockReturnValue(
-        new Promise(res =>
-          res({
-            ok: false,
-            json: () => new Promise(ress => ress(false))
-          })
-        )
-      );
-
-      try {
-        await oauthToken({
-          baseUrl: 'https://test.com',
-          client_id: 'client_idIn',
-          code: 'codeIn',
-          code_verifier: 'code_verifierIn'
-        });
-      } catch (error) {
-        expect(error.message).toBe(
-          `HTTP error. Unable to fetch https://test.com/oauth/token`
-        );
-        expect(error.error).toBe('request_error');
-        expect(error.error_description).toBe(
-          `HTTP error. Unable to fetch https://test.com/oauth/token`
-        );
-      }
-    });
-
-    it('retries the request in the event of a network failure', async () => {
-      // Fetch only fails in the case of a network issue, so should be
-      // retried here. Failure status (4xx, 5xx, etc) return a resolved Promise
-      // with the failure in the body.
-      // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
-      mockUnfetch.mockReturnValue(Promise.reject(new Error('Network failure')));
-
-      try {
-        await oauthToken({
-          baseUrl: 'https://test.com',
-          client_id: 'client_idIn',
-          code: 'codeIn',
-          code_verifier: 'code_verifierIn'
-        });
-      } catch (error) {
-        expect(error.message).toBe('Network failure');
-
-        expect(mockUnfetch).toHaveBeenCalledTimes(
-          DEFAULT_SILENT_TOKEN_RETRY_COUNT
-        );
-      }
-    });
-
-    it('continues the program after failing a couple of times then succeeding', async () => {
-      // Fetch only fails in the case of a network issue, so should be
-      // retried here. Failure status (4xx, 5xx, etc) return a resolved Promise
-      // with the failure in the body.
-      // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
-      mockUnfetch
-        .mockReturnValueOnce(Promise.reject(new Error('Network failure')))
-        .mockReturnValueOnce(Promise.reject(new Error('Network failure')))
-        .mockReturnValue(
-          Promise.resolve({
-            ok: true,
-            json: () => Promise.resolve({ access_token: 'access-token' })
-          })
-        );
-
-      const result = await oauthToken({
-        baseUrl: 'https://test.com',
-        client_id: 'client_idIn',
-        code: 'codeIn',
-        code_verifier: 'code_verifierIn'
-      });
-
-      expect(result.access_token).toBe('access-token');
-      expect(mockUnfetch).toHaveBeenCalledTimes(3);
-      expect(abortController.abort).not.toHaveBeenCalled();
-    });
-
-    it('throws a fetch error when the network is down', async () => {
-      mockUnfetch.mockReturnValue(Promise.reject(new ProgressEvent('error')));
-
-      await expect(
-        oauthToken({
-          baseUrl: 'https://test.com',
-          client_id: 'client_idIn',
-          code: 'codeIn',
-          code_verifier: 'code_verifierIn'
-        })
-      ).rejects.toMatchObject({ message: 'Failed to fetch' });
-    });
-
-    it('surfaces a timeout error when the fetch continuously times out', async () => {
-      const createPromise = () =>
-        new Promise((resolve, _) => {
-          setTimeout(
-            () =>
-              resolve({
-                ok: true,
-                json: () => Promise.resolve({ access_token: 'access-token' })
-              }),
-            500
-          );
-        });
-
-      mockUnfetch.mockReturnValue(createPromise());
-
-      try {
-        await oauthToken({
-          baseUrl: 'https://test.com',
-          client_id: 'client_idIn',
-          code: 'codeIn',
-          code_verifier: 'code_verifierIn',
-          timeout: 100
-        });
-      } catch (e) {
-        expect(e.message).toBe("Timeout when executing 'fetch'");
-        expect(mockUnfetch).toHaveBeenCalledTimes(3);
-        expect(abortController.abort).toHaveBeenCalledTimes(3);
-      }
-    });
-
-    it('retries the request in the event of a timeout', async () => {
-      const fetchResult = {
-        ok: true,
-        json: () => Promise.resolve({ access_token: 'access-token' })
-      };
-
-      mockUnfetch.mockReturnValueOnce(
-        new Promise((resolve, _) => {
-          setTimeout(() => resolve(fetchResult), 1000);
-        })
-      );
-
-      mockUnfetch.mockReturnValue(Promise.resolve(fetchResult));
-
-      const result = await oauthToken({
-        baseUrl: 'https://test.com',
-        client_id: 'client_idIn',
-        code: 'codeIn',
-        code_verifier: 'code_verifierIn',
-        timeout: 500
-      });
-
-      expect(result.access_token).toBe('access-token');
-      expect(mockUnfetch).toHaveBeenCalledTimes(2);
-      expect(abortController.abort).toHaveBeenCalled();
-    });
-  });
-
   describe('runPopup', () => {
     const TIMEOUT_ERROR = {
       error: 'timeout',
diff --git a/tsconfig.test.json b/tsconfig.test.json
new file mode 100644
index 0000000..483e75c
--- /dev/null
+++ b/tsconfig.test.json
@@ -0,0 +1,6 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "noImplicitAny": false
+  }
+}