diff --git a/.gitignore b/.gitignore
index 897ba1d..1782dc3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,6 @@ yarn-error.log*
# redis
dump.rdb
+
+# keys
+private.key
diff --git a/client/About.md b/client/About.md
index 39360a2..d610f5d 100644
--- a/client/About.md
+++ b/client/About.md
@@ -33,7 +33,7 @@ unpkg is not affiliated with or supported by npm, Inc. in any way. Please do not
### Abuse
-unpkg maintains [a blacklist](https://github.com/unpkg/unpkg.com/blob/master/server/PackageBlacklist.json) of packages that are known to be malicious. If you find such a package on npm, please take a moment to submit a PR that adds it to the list!
+unpkg maintains of packages that are known to be malicious. If you find such a package on npm, please let us know!
### Feedback
diff --git a/client/App.test.js b/client/App.test.js
deleted file mode 100644
index 76d121e..0000000
--- a/client/App.test.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom'
-import App from './App'
-
-it('renders without crashing', () => {
- const div = document.createElement('div')
- ReactDOM.render(, div)
-})
diff --git a/client/Layout.js b/client/Layout.js
index 455eb93..407ca45 100644
--- a/client/Layout.js
+++ b/client/Layout.js
@@ -47,7 +47,7 @@ class Layout extends React.Component {
componentDidMount() {
this.adjustUnderline()
- fetch('/_stats/last-month')
+ fetch('/_stats?period=last-month')
.then(res => res.json())
.then(stats => this.setState({ stats }))
diff --git a/package.json b/package.json
index 2ba6bfa..3024b96 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"algoliasearch": "^3.24.3",
"babel-plugin-unpkg-rewrite": "^3.1.0",
+ "body-parser": "^1.18.2",
"cors": "^2.8.1",
"countries-list": "^1.3.2",
"csso": "^3.1.1",
@@ -20,10 +21,12 @@
"gunzip-maybe": "^1.4.0",
"invariant": "^2.2.2",
"isomorphic-fetch": "^2.2.1",
+ "jsonwebtoken": "^8.1.0",
"mime": "^1.4.0",
"mkdirp": "^0.5.1",
"morgan": "^1.8.1",
"ndjson": "^1.5.0",
+ "node-forge": "^0.7.1",
"os-tmpdir": "^1.0.2",
"pretty-bytes": "^3",
"prop-types": "^15.5.8",
@@ -91,7 +94,8 @@
"/config/polyfills.js"
],
"testPathIgnorePatterns": [
- "[/\\\\](build|docs|node_modules|scripts)[/\\\\]"
+ "[/\\\\](build|docs|node_modules|scripts)[/\\\\]",
+ "__tests__/utils"
],
"testEnvironment": "node",
"testURL": "http://localhost",
diff --git a/public.key b/public.key
new file mode 100644
index 0000000..41d61cf
--- /dev/null
+++ b/public.key
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtWG6vJVKV8+hGDXtYS3i
+JN8DO4xsKAM7n72IMH3489J1UUwdFdP3CKAAQzl8kcet/9q5CrLeUnW5oQNezQiC
+FcSgF/KhJBITMWe5IIVWZOsFMvvNR+vISSL6We842gEAZWJbo2HZdFTdZjfino/4
+CL3Sr0Ue9PFVHcVkT9V7uS7f/7VbwKFbxdpesYeq8odNFPQy6rhmSBT9v0mGK36K
+f7kPuVqV7xlZ8nfiHdP+TAP2I4Iv2Ok7kMMy2qPjwizCShPcLIHzmyVdRuoUvxTf
+cvC/cI3NUC7Qconn9tEtyvFzegdhS0tQD+Mq9eWAEZYp0rV/TkkaAYkIOkVQoiwQ
+9QIDAQAB
+-----END PUBLIC KEY-----
diff --git a/scripts/add-blacklist.js b/scripts/add-blacklist.js
new file mode 100644
index 0000000..c775f3a
--- /dev/null
+++ b/scripts/add-blacklist.js
@@ -0,0 +1,120 @@
+const BlacklistAPI = require('../server/BlacklistAPI')
+
+const blacklist = [
+ 'goodjsproject',
+ 'thisoneisevil',
+ '03087dd164d4722425d74e095ff30bc2',
+ 'sf1b195d16f3f3c695888e7cde1b20978f',
+ 'sfd6e5f9f15adcc48d2fcb380e5aab44e5',
+ 'sfd1f03b91ff97ff303bb69254ec3a4fd3',
+ 'sf12fefe1d7b5bbe4d5661ff1bf6ea47bb',
+ 'sfabae91ef175b31df2d7e77ed948206f7',
+ 'sf149006f0b0c1c7e50e81181d9f5eba2d',
+ 'sfe6bd27516125ae460d5c2e63feb70c97',
+ 'sf09f01e9b87c212046c002a26f5117e87',
+ 'sf77de34c6c6f180be3a03226cee219442',
+ 'sf00e7081dec64c8557a40a79749e79d6c',
+ 'sfb2c291b5ce9ee8cbc0ed4f9e7ab7c3d1',
+ 'sf25d1f870f3355f4b02c34e65e451a8ef',
+ 'sf18d48571145efc20316195ae19cf7aeb',
+ 'sf694f2c2280ca2943d482059797ea1c97',
+ 'sf2f8a5346ecda7e03f803b398dd40b869',
+ 'sfaeebc97309de527e56215588f9c23dd3',
+ 'sf2976f560d7ec7f8b63d7adca6728aed2',
+ 'sf67de3bc862ca765e8cb9a72cf3453230',
+ 'sfe906b050b2f380096de1c090dafdbb29',
+ 'sfc98c62f745bac6d5f04c6e97e8294cec',
+ 'sf979c8da13b915e5eaa5d84373a7c4a9b',
+ 'sf6039ffb1c35d521773e4dcba7abf446c',
+ 'sfb2adb7312558f6dde15d99619ee7da27',
+ 'sfcba23f36564e58894e7aeebda67d3682',
+ 'sfe0786b97c862c7485d9cec1b912bc634',
+ 'sf024cf5fc99edfb929828b09084a9b6af',
+ 'sf7d6d35c4dc2d6be739993cf054b00d35',
+ 'sf78d0d2fbe897c94458a4822d624c969a',
+ 'sf2b1bbe2c61f6f90cda322a61338003af',
+ 'sff6a953f5f9960c2a1c907e186778d42f',
+ 'sf748f0c277591c02438e9c325a8cb4ff5',
+ 'sf606d995c90fd7c9fe17529a4697e4eb6',
+ 'sf695459fe0eb159619268fdf542d3bb25',
+ 'sf262b2b03ae6881a72365e05451b303ee',
+ 'sf8a8a260ab891ae90eae8155548683053',
+ 'sf9a307b3da7bfdcbb043c43857310d35b',
+ 'sf3e5834662f7c780cbb60ee5740622d12',
+ 'sff3a09cde265bde00a96ec01536a97029',
+ 'sf364acb7b43655b3496aa65c7d6bb561a',
+ 'sf89b5b735e7ba3bdfa52e7ae65026f9aa',
+ 'sfca1c0beaf0309c9c322adddaa1cbd40b',
+ 'sfef72db8254fe8714860d52da25b25fcc',
+ 'sff3114fad67cd8b012706478c4b9fed39',
+ 'sf1f60afa19f0a841aed6481bfcd91631c',
+ 'sf785d637bd20673dbf2213f77e3df1cba',
+ 'sf6142d76572f10a23113135dbee19dcf3',
+ 'sf37544977bf1874afa4d4e9e282f2bf4a',
+ 'sf0a49054aeba63d7f829eb3d02f0ad942',
+ 'sfcce0358fa72b83d85569c22e715a920e',
+ 'sf13890e52703bd39f71fb3815e555f0b7',
+ 'sfe00cc3e9fec6974a4c1131bdb0ce5ee6',
+ 'sf1343e7a08fa0ff25b6d215a3532bde13',
+ 'sff4f6781701d8edd0e7909201c356d7c9',
+ 'sf3d6dca96e72a14ecaed6ea97549fc088',
+ 'sfaec6e7e100bfadbc4cf244adf277da15',
+ 'sfa1b8f9052a194f3e9791769ce4cb352f',
+ 'sf8cfdc43795d38e3d6ba6a57aae334c29',
+ 'sf4606344a64d98d96120d5532b57b2a8b',
+ 'sf7bdb20e4d622f6569f3e8503138c859d',
+ 'sf0f4536523bf44a482a6bf466707c135e',
+ 'sfc4a5204967899f098e8c6486cc60af7d',
+ 'sf646dffa91307c680266d8e855b36f0be',
+ 'sfa68a6cbd527e17f7e55b574b6a5a53ae',
+ 'sff677d3897a4a7090aecfd5e13a6fc90b',
+ 'sfbd0ffe85dc79f626ccb492a78aeda94f',
+ 'sf37fe85de86dd3973db690594ae8b7bfc',
+ 'sf54e90e7565b48823679af97d05189af4',
+ 'sfb4ecfd026f1962076f3004cadc11931a',
+ 'sfdb26212339dc18d5dd794fa800d4e5a6',
+ 'sf4fdb84f60bad69846dd9fc9e2328a4f9',
+ 'sfcfc008ec1b2a6daf70571b7480ba6aa3',
+ 'sf1566a39461daff958cf2e4291ef13381',
+ 'sf8a2b7d68f1c7c7f34381dc1a198465b4',
+ 'sf3931b37c61d5b34186ca58f889d48047',
+ 'sf109ad06b86e6be8f0c3e94d5e4893f47',
+ 'sffefe6195a8b014a1cc7d9cf2449d1b50',
+ 'sf85bacaf85076693e911b948b2c02535a',
+ 'sf340a5f85afd785510da83f9cabf15726',
+ 'sffee5fae47344c13e9d7c6db0bb403b76',
+ 'sf4ec8bfe49e5a941b82bd07927f198b5d',
+ 'sf2b10045997d4f1f120a5393be267cd52',
+ 'sf14d2825be098ee2f80ead23cb181b8e4',
+ 'sfc1c052ab23baf866cc73b3c585c65503',
+ 'sfed9d0c920ecc6694c82ae859c1699758',
+ 'sf15c3851aa68992e9b80ec11211e401bc',
+ 'sf9c16721aff8f5ebb4fe7731a409eb622',
+ 'sfc65f86a6d8598a4171dec7f4c99fc856',
+ 'sf9b9ab3f53b6a705d772ca41a233be838',
+ 'sf0d200aa244146e0054f93c7f98c134c8',
+ 'sf3d9b13c2b94dea2a11a697d11f3312a8',
+ 'sfdf5195f21fffc06298b7c0b4f6bcb9ba',
+ 'sff25c5beafcd6f66bc2cc21e84f8aec85',
+ 'copyfish-npm-2-8-5',
+ '54e90e7565b48823679af97d05189af4',
+ '15c3851aa68992e9b80ec11211e401bc',
+ '4fdb84f60bad69846dd9fc9e2328a4f9',
+ '14d2825be098ee2f80ead23cb181b8e4',
+ '024cf5fc99edfb929828b09084a9b6af',
+ '8a2b7d68f1c7c7f34381dc1a198465b4',
+ '7bdb20e4d622f6569f3e8503138c859d',
+ '694f2c2280ca2943d482059797ea1c97',
+ 'b2c291b5ce9ee8cbc0ed4f9e7ab7c3d1',
+ '9b9ab3f53b6a705d772ca41a233be838',
+ 'df5195f21fffc06298b7c0b4f6bcb9ba',
+ 'fefe6195a8b014a1cc7d9cf2449d1b50',
+ 'fee5fae47344c13e9d7c6db0bb403b76',
+ '2976f560d7ec7f8b63d7adca6728aed2',
+ 'd6e5f9f15adcc48d2fcb380e5aab44e5',
+ 'c98c62f745bac6d5f04c6e97e8294cec',
+ 'abae91ef175b31df2d7e77ed948206f7',
+ '09f01e9b87c212046c002a26f5117e87'
+]
+
+blacklist.forEach(BlacklistAPI.addPackage)
diff --git a/scripts/create-token.js b/scripts/create-token.js
new file mode 100644
index 0000000..020c92b
--- /dev/null
+++ b/scripts/create-token.js
@@ -0,0 +1,19 @@
+const AuthAPI = require('../server/AuthAPI')
+
+const scopes = {}
+
+AuthAPI.createToken(scopes).then(
+ token => {
+ // Verify it, just to be sure.
+ AuthAPI.verifyToken(token).then(payload => {
+ console.log(token, '\n')
+ console.log(JSON.stringify(payload, null, 2), '\n')
+ console.log(AuthAPI.getPublicKey())
+ process.exit()
+ })
+ },
+ error => {
+ console.error(error)
+ process.exit(1)
+ }
+)
diff --git a/server/AuthAPI.js b/server/AuthAPI.js
new file mode 100644
index 0000000..e764c5c
--- /dev/null
+++ b/server/AuthAPI.js
@@ -0,0 +1,118 @@
+const fs = require('fs')
+const path = require('path')
+const crypto = require('crypto')
+const jwt = require('jsonwebtoken')
+const invariant = require('invariant')
+const forge = require('node-forge')
+const db = require('./RedisClient')
+
+let keys
+if (process.env.NODE_ENV === 'production') {
+ keys = {
+ public: fs.readFileSync(path.resolve(__dirname, '../public.key'), 'utf8'),
+ private: process.env.PRIVATE_KEY
+ }
+
+ invariant(keys.private, 'Missing $PRIVATE_KEY environment variable')
+} else {
+ // Generate a random keypair for dev/testing.
+ // See https://gist.github.com/sebadoom/2b70969e70db5da9a203bebd9cff099f
+ const keypair = forge.rsa.generateKeyPair({ bits: 2048 })
+ keys = {
+ public: forge.pki.publicKeyToPem(keypair.publicKey, 72),
+ private: forge.pki.privateKeyToPem(keypair.privateKey, 72)
+ }
+}
+
+function getCurrentSeconds() {
+ return Math.floor(Date.now() / 1000)
+}
+
+function createTokenId() {
+ return crypto.randomBytes(16).toString('hex')
+}
+
+function createToken(scopes = {}) {
+ return new Promise((resolve, reject) => {
+ const payload = {
+ jti: createTokenId(),
+ iss: 'https://unpkg.com',
+ iat: getCurrentSeconds(),
+ scopes
+ }
+
+ jwt.sign(payload, keys.private, { algorithm: 'RS256' }, (error, token) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(token)
+ }
+ })
+ })
+}
+
+const RevokedTokensSet = 'revoked-tokens'
+
+function verifyToken(token) {
+ return new Promise((resolve, reject) => {
+ const options = { algorithms: ['RS256'] }
+
+ jwt.verify(token, keys.public, options, (error, payload) => {
+ if (error) {
+ reject(error)
+ } else {
+ if (payload.jti) {
+ db.sismember(RevokedTokensSet, payload.jti, (error, value) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(value === 0 ? payload : null)
+ }
+ })
+ } else {
+ resolve(null)
+ }
+ }
+ })
+ })
+}
+
+function revokeToken(token) {
+ return verifyToken(token).then(payload => {
+ if (payload) {
+ return new Promise((resolve, reject) => {
+ db.sadd(RevokedTokensSet, payload.jti, error => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve()
+ }
+ })
+ })
+ }
+ })
+}
+
+function removeAllRevokedTokens() {
+ return new Promise((resolve, reject) => {
+ db.del(RevokedTokensSet, error => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve()
+ }
+ })
+ })
+}
+
+function getPublicKey() {
+ return keys.public
+}
+
+module.exports = {
+ createToken,
+ verifyToken,
+ revokeToken,
+ removeAllRevokedTokens,
+ getPublicKey
+}
diff --git a/server/BlacklistAPI.js b/server/BlacklistAPI.js
new file mode 100644
index 0000000..6b9d85c
--- /dev/null
+++ b/server/BlacklistAPI.js
@@ -0,0 +1,71 @@
+const db = require('./RedisClient')
+
+const BlacklistSet = 'blacklisted-packages'
+
+function addPackage(packageName) {
+ return new Promise((resolve, reject) => {
+ db.sadd(BlacklistSet, packageName, (error, value) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(value === 1)
+ }
+ })
+ })
+}
+
+function removePackage(packageName) {
+ return new Promise((resolve, reject) => {
+ db.srem(BlacklistSet, packageName, (error, value) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(value === 1)
+ }
+ })
+ })
+}
+
+function removeAllPackages() {
+ return new Promise((resolve, reject) => {
+ db.del(BlacklistSet, error => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve()
+ }
+ })
+ })
+}
+
+function getPackages() {
+ return new Promise((resolve, reject) => {
+ db.smembers(BlacklistSet, (error, value) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(value)
+ }
+ })
+ })
+}
+
+function containsPackage(packageName) {
+ return new Promise((resolve, reject) => {
+ db.sismember(BlacklistSet, packageName, (error, value) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(value === 1)
+ }
+ })
+ })
+}
+
+module.exports = {
+ addPackage,
+ removePackage,
+ removeAllPackages,
+ getPackages,
+ containsPackage
+}
diff --git a/server/PackageBlacklist.json b/server/PackageBlacklist.json
deleted file mode 100644
index 98abb87..0000000
--- a/server/PackageBlacklist.json
+++ /dev/null
@@ -1,118 +0,0 @@
-{
- "blacklist": [
- "goodjsproject",
- "thisoneisevil",
- "03087dd164d4722425d74e095ff30bc2",
- "sf1b195d16f3f3c695888e7cde1b20978f",
- "sfd6e5f9f15adcc48d2fcb380e5aab44e5",
- "sfd1f03b91ff97ff303bb69254ec3a4fd3",
- "sf12fefe1d7b5bbe4d5661ff1bf6ea47bb",
- "sfabae91ef175b31df2d7e77ed948206f7",
- "sf149006f0b0c1c7e50e81181d9f5eba2d",
- "sfe6bd27516125ae460d5c2e63feb70c97",
- "sf09f01e9b87c212046c002a26f5117e87",
- "sf77de34c6c6f180be3a03226cee219442",
- "sf00e7081dec64c8557a40a79749e79d6c",
- "sfb2c291b5ce9ee8cbc0ed4f9e7ab7c3d1",
- "sf25d1f870f3355f4b02c34e65e451a8ef",
- "sf18d48571145efc20316195ae19cf7aeb",
- "sf694f2c2280ca2943d482059797ea1c97",
- "sf2f8a5346ecda7e03f803b398dd40b869",
- "sfaeebc97309de527e56215588f9c23dd3",
- "sf2976f560d7ec7f8b63d7adca6728aed2",
- "sf67de3bc862ca765e8cb9a72cf3453230",
- "sfe906b050b2f380096de1c090dafdbb29",
- "sfc98c62f745bac6d5f04c6e97e8294cec",
- "sf979c8da13b915e5eaa5d84373a7c4a9b",
- "sf6039ffb1c35d521773e4dcba7abf446c",
- "sfb2adb7312558f6dde15d99619ee7da27",
- "sfcba23f36564e58894e7aeebda67d3682",
- "sfe0786b97c862c7485d9cec1b912bc634",
- "sf024cf5fc99edfb929828b09084a9b6af",
- "sf7d6d35c4dc2d6be739993cf054b00d35",
- "sf78d0d2fbe897c94458a4822d624c969a",
- "sf2b1bbe2c61f6f90cda322a61338003af",
- "sff6a953f5f9960c2a1c907e186778d42f",
- "sf748f0c277591c02438e9c325a8cb4ff5",
- "sf606d995c90fd7c9fe17529a4697e4eb6",
- "sf695459fe0eb159619268fdf542d3bb25",
- "sf262b2b03ae6881a72365e05451b303ee",
- "sf8a8a260ab891ae90eae8155548683053",
- "sf9a307b3da7bfdcbb043c43857310d35b",
- "sf3e5834662f7c780cbb60ee5740622d12",
- "sff3a09cde265bde00a96ec01536a97029",
- "sf364acb7b43655b3496aa65c7d6bb561a",
- "sf89b5b735e7ba3bdfa52e7ae65026f9aa",
- "sfca1c0beaf0309c9c322adddaa1cbd40b",
- "sfef72db8254fe8714860d52da25b25fcc",
- "sff3114fad67cd8b012706478c4b9fed39",
- "sf1f60afa19f0a841aed6481bfcd91631c",
- "sf785d637bd20673dbf2213f77e3df1cba",
- "sf6142d76572f10a23113135dbee19dcf3",
- "sf37544977bf1874afa4d4e9e282f2bf4a",
- "sf0a49054aeba63d7f829eb3d02f0ad942",
- "sfcce0358fa72b83d85569c22e715a920e",
- "sf13890e52703bd39f71fb3815e555f0b7",
- "sfe00cc3e9fec6974a4c1131bdb0ce5ee6",
- "sf1343e7a08fa0ff25b6d215a3532bde13",
- "sff4f6781701d8edd0e7909201c356d7c9",
- "sf3d6dca96e72a14ecaed6ea97549fc088",
- "sfaec6e7e100bfadbc4cf244adf277da15",
- "sfa1b8f9052a194f3e9791769ce4cb352f",
- "sf8cfdc43795d38e3d6ba6a57aae334c29",
- "sf4606344a64d98d96120d5532b57b2a8b",
- "sf7bdb20e4d622f6569f3e8503138c859d",
- "sf0f4536523bf44a482a6bf466707c135e",
- "sfc4a5204967899f098e8c6486cc60af7d",
- "sf646dffa91307c680266d8e855b36f0be",
- "sfa68a6cbd527e17f7e55b574b6a5a53ae",
- "sff677d3897a4a7090aecfd5e13a6fc90b",
- "sfbd0ffe85dc79f626ccb492a78aeda94f",
- "sf37fe85de86dd3973db690594ae8b7bfc",
- "sf54e90e7565b48823679af97d05189af4",
- "sfb4ecfd026f1962076f3004cadc11931a",
- "sfdb26212339dc18d5dd794fa800d4e5a6",
- "sf4fdb84f60bad69846dd9fc9e2328a4f9",
- "sfcfc008ec1b2a6daf70571b7480ba6aa3",
- "sf1566a39461daff958cf2e4291ef13381",
- "sf8a2b7d68f1c7c7f34381dc1a198465b4",
- "sf3931b37c61d5b34186ca58f889d48047",
- "sf109ad06b86e6be8f0c3e94d5e4893f47",
- "sffefe6195a8b014a1cc7d9cf2449d1b50",
- "sf85bacaf85076693e911b948b2c02535a",
- "sf340a5f85afd785510da83f9cabf15726",
- "sffee5fae47344c13e9d7c6db0bb403b76",
- "sf4ec8bfe49e5a941b82bd07927f198b5d",
- "sf2b10045997d4f1f120a5393be267cd52",
- "sf14d2825be098ee2f80ead23cb181b8e4",
- "sfc1c052ab23baf866cc73b3c585c65503",
- "sfed9d0c920ecc6694c82ae859c1699758",
- "sf15c3851aa68992e9b80ec11211e401bc",
- "sf9c16721aff8f5ebb4fe7731a409eb622",
- "sfc65f86a6d8598a4171dec7f4c99fc856",
- "sf9b9ab3f53b6a705d772ca41a233be838",
- "sf0d200aa244146e0054f93c7f98c134c8",
- "sf3d9b13c2b94dea2a11a697d11f3312a8",
- "sfdf5195f21fffc06298b7c0b4f6bcb9ba",
- "sff25c5beafcd6f66bc2cc21e84f8aec85",
- "copyfish-npm-2-8-5",
- "54e90e7565b48823679af97d05189af4",
- "15c3851aa68992e9b80ec11211e401bc",
- "4fdb84f60bad69846dd9fc9e2328a4f9",
- "14d2825be098ee2f80ead23cb181b8e4",
- "024cf5fc99edfb929828b09084a9b6af",
- "8a2b7d68f1c7c7f34381dc1a198465b4",
- "7bdb20e4d622f6569f3e8503138c859d",
- "694f2c2280ca2943d482059797ea1c97",
- "b2c291b5ce9ee8cbc0ed4f9e7ab7c3d1",
- "9b9ab3f53b6a705d772ca41a233be838",
- "df5195f21fffc06298b7c0b4f6bcb9ba",
- "fefe6195a8b014a1cc7d9cf2449d1b50",
- "fee5fae47344c13e9d7c6db0bb403b76",
- "2976f560d7ec7f8b63d7adca6728aed2",
- "d6e5f9f15adcc48d2fcb380e5aab44e5",
- "c98c62f745bac6d5f04c6e97e8294cec",
- "abae91ef175b31df2d7e77ed948206f7",
- "09f01e9b87c212046c002a26f5117e87"
- ]
-}
diff --git a/server/StatsAPI.js b/server/StatsAPI.js
index 6a0cc23..56523ac 100644
--- a/server/StatsAPI.js
+++ b/server/StatsAPI.js
@@ -1,14 +1,17 @@
-const cf = require('./CloudflareAPI')
const db = require('./RedisClient')
-
-const PackageBlacklist = require('./PackageBlacklist').blacklist
+const CloudflareAPI = require('./CloudflareAPI')
+const BlacklistAPI = require('./BlacklistAPI')
function prunePackages(packagesMap) {
- PackageBlacklist.forEach(packageName => {
- delete packagesMap[packageName]
- })
-
- return packagesMap
+ return Promise.all(
+ Object.keys(packagesMap).map(packageName =>
+ BlacklistAPI.isBlacklisted(packageName).then(blacklisted => {
+ if (blacklisted) {
+ delete packagesMap[packageName]
+ }
+ })
+ )
+ ).then(() => packagesMap)
}
function createDayKey(date) {
@@ -87,25 +90,25 @@ function sumMaps(maps) {
}
function addDailyMetrics(result) {
- return Promise.all(result.timeseries.map(addDailyMetricsToTimeseries)).then(
- () => {
- result.totals.requests.package = sumMaps(
- result.timeseries.map(timeseries => {
- return timeseries.requests.package
- })
- )
+ return Promise.all(
+ result.timeseries.map(addDailyMetricsToTimeseries)
+ ).then(() => {
+ result.totals.requests.package = sumMaps(
+ result.timeseries.map(timeseries => {
+ return timeseries.requests.package
+ })
+ )
- result.totals.bandwidth.package = sumMaps(
- result.timeseries.map(timeseries => timeseries.bandwidth.package)
- )
+ result.totals.bandwidth.package = sumMaps(
+ result.timeseries.map(timeseries => timeseries.bandwidth.package)
+ )
- result.totals.requests.protocol = sumMaps(
- result.timeseries.map(timeseries => timeseries.requests.protocol)
- )
+ result.totals.requests.protocol = sumMaps(
+ result.timeseries.map(timeseries => timeseries.requests.protocol)
+ )
- return result
- }
- )
+ return result
+ })
}
function extractPublicInfo(data) {
@@ -140,8 +143,12 @@ function extractPublicInfo(data) {
const DomainNames = ['unpkg.com', 'npmcdn.com']
function fetchStats(since, until) {
- return cf.getZones(DomainNames).then(zones => {
- return cf.getZoneAnalyticsDashboard(zones, since, until).then(dashboard => {
+ return CloudflareAPI.getZones(DomainNames).then(zones => {
+ return CloudflareAPI.getZoneAnalyticsDashboard(
+ zones,
+ since,
+ until
+ ).then(dashboard => {
return {
timeseries: dashboard.timeseries.map(extractPublicInfo),
totals: extractPublicInfo(dashboard.totals)
@@ -154,14 +161,9 @@ const oneMinute = 1000 * 60
const oneHour = oneMinute * 60
const oneDay = oneHour * 24
-function getStats(since, until, callback) {
- let promise = fetchStats(since, until)
-
- if (until - since > oneDay) promise = promise.then(addDailyMetrics)
-
- promise.then(value => {
- callback(null, value)
- }, callback)
+function getStats(since, until) {
+ const promise = fetchStats(since, until)
+ return until - since > oneDay ? promise.then(addDailyMetrics) : promise
}
module.exports = {
diff --git a/server/__tests__/AuthAPI-test.js b/server/__tests__/AuthAPI-test.js
new file mode 100644
index 0000000..af5c668
--- /dev/null
+++ b/server/__tests__/AuthAPI-test.js
@@ -0,0 +1,39 @@
+const AuthAPI = require('../AuthAPI')
+
+describe('Auth API', () => {
+ beforeEach(done => {
+ AuthAPI.removeAllRevokedTokens().then(() => done(), done)
+ })
+
+ it('creates tokens with the right scopes', done => {
+ const scopes = {
+ blacklist: {
+ add: true,
+ remove: true
+ }
+ }
+
+ AuthAPI.createToken(scopes).then(token => {
+ AuthAPI.verifyToken(token).then(payload => {
+ expect(payload.jti).toEqual(expect.any(String))
+ expect(payload.iss).toEqual(expect.any(String))
+ expect(payload.iat).toEqual(expect.any(Number))
+ expect(payload.scopes).toMatchObject(scopes)
+ done()
+ })
+ })
+ })
+
+ it('refuses to verify revoked tokens', done => {
+ const scopes = {}
+
+ AuthAPI.createToken(scopes).then(token => {
+ AuthAPI.revokeToken(token).then(() => {
+ AuthAPI.verifyToken(token).then(payload => {
+ expect(payload).toBe(null)
+ done()
+ })
+ })
+ })
+ })
+})
diff --git a/server/__tests__/BlacklistAPI-test.js b/server/__tests__/BlacklistAPI-test.js
new file mode 100644
index 0000000..b0a065c
--- /dev/null
+++ b/server/__tests__/BlacklistAPI-test.js
@@ -0,0 +1,24 @@
+const BlacklistAPI = require('../BlacklistAPI')
+
+describe('Blacklist API', () => {
+ beforeEach(done => {
+ BlacklistAPI.removeAllPackages().then(() => done(), done)
+ })
+
+ it('adds and removes packages to/from the blacklist', done => {
+ const packageName = 'bad-package'
+
+ BlacklistAPI.addPackage(packageName).then(() => {
+ BlacklistAPI.getPackages().then(packageNames => {
+ expect(packageNames).toEqual([packageName])
+
+ BlacklistAPI.removePackage(packageName).then(() => {
+ BlacklistAPI.getPackages().then(packageNames => {
+ expect(packageNames).toEqual([])
+ done()
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/server/__tests__/server-test.js b/server/__tests__/server-test.js
new file mode 100644
index 0000000..7720535
--- /dev/null
+++ b/server/__tests__/server-test.js
@@ -0,0 +1,201 @@
+const request = require('supertest')
+const createServer = require('../createServer')
+const withBlacklist = require('./utils/withBlacklist')
+const withRevokedToken = require('./utils/withRevokedToken')
+const withToken = require('./utils/withToken')
+
+describe('The server', () => {
+ let server
+ beforeEach(() => {
+ server = createServer()
+ })
+
+ it('rejects invalid package names', done => {
+ request(server)
+ .get('/_invalid/index.js')
+ .end((err, res) => {
+ expect(res.statusCode).toBe(403)
+ done()
+ })
+ })
+
+ it('redirects invalid query params', done => {
+ request(server)
+ .get('/react?main=index&invalid')
+ .end((err, res) => {
+ expect(res.statusCode).toBe(302)
+ expect(res.headers.location).toBe('/react?main=index')
+ done()
+ })
+ })
+
+ it('redirects /_meta to ?meta', done => {
+ request(server)
+ .get('/_meta/react?main=index')
+ .end((err, res) => {
+ expect(res.statusCode).toBe(302)
+ expect(res.headers.location).toBe('/react?main=index&meta')
+ done()
+ })
+ })
+
+ it('does not serve blacklisted packages', done => {
+ withBlacklist(['bad-package'], () => {
+ request(server)
+ .get('/bad-package/index.js')
+ .end((err, res) => {
+ expect(res.statusCode).toBe(403)
+ done()
+ })
+ })
+ })
+
+ describe('POST /_auth', () => {
+ it('creates a new auth token', done => {
+ request(server)
+ .post('/_auth')
+ .end((err, res) => {
+ expect(res.body).toHaveProperty('token')
+ done()
+ })
+ })
+ })
+
+ describe('GET /_auth', () => {
+ describe('with no auth', () => {
+ it('echoes back null', done => {
+ request(server)
+ .get('/_auth')
+ .end((err, res) => {
+ expect(res.body).toHaveProperty('auth')
+ expect(res.body.auth).toBe(null)
+ done()
+ })
+ })
+ })
+
+ describe('with a revoked auth token', () => {
+ it('echoes back null', done => {
+ withRevokedToken({ some: { scope: true } }, token => {
+ request(server)
+ .get('/_auth?token=' + token)
+ .end((err, res) => {
+ expect(res.body).toHaveProperty('auth')
+ expect(res.body.auth).toBe(null)
+ done()
+ })
+ })
+ })
+ })
+
+ describe('with a valid auth token', () => {
+ it('echoes back the auth payload', done => {
+ withToken({ some: { scope: true } }, token => {
+ request(server)
+ .get('/_auth?token=' + token)
+ .end((err, res) => {
+ expect(res.body).toHaveProperty('auth')
+ expect(typeof res.body.auth).toBe('object')
+ done()
+ })
+ })
+ })
+ })
+ })
+
+ describe('GET /_publicKey', () => {
+ it('echoes the public key', done => {
+ request(server)
+ .get('/_publicKey')
+ .end((err, res) => {
+ expect(res.text).toMatch(/PUBLIC KEY/)
+ done()
+ })
+ })
+ })
+
+ describe('POST /_blacklist', () => {
+ describe('with no auth', () => {
+ it('is forbidden', done => {
+ request(server)
+ .post('/_blacklist')
+ .end((err, res) => {
+ expect(res.statusCode).toBe(403)
+ done()
+ })
+ })
+ })
+
+ describe('with the "blacklist.add" scope', () => {
+ it('can add to the blacklist', done => {
+ withToken({ blacklist: { add: true } }, token => {
+ request(server)
+ .post('/_blacklist')
+ .send({ token, packageName: 'bad-package' })
+ .end((err, res) => {
+ expect(res.statusCode).toBe(200)
+ expect(res.headers['content-location']).toEqual(
+ '/_blacklist/bad-package'
+ )
+ expect(res.body.ok).toBe(true)
+ done()
+ })
+ })
+ })
+ })
+ })
+
+ describe('GET /_blacklist', () => {
+ describe('with no auth', () => {
+ it('is forbidden', done => {
+ request(server)
+ .get('/_blacklist')
+ .end((err, res) => {
+ expect(res.statusCode).toBe(403)
+ done()
+ })
+ })
+ })
+
+ describe('with the "blacklist.read" scope', () => {
+ it('can read the blacklist', done => {
+ withToken({ blacklist: { read: true } }, token => {
+ request(server)
+ .get('/_blacklist?token=' + token)
+ .end((err, res) => {
+ expect(res.statusCode).toBe(200)
+ done()
+ })
+ })
+ })
+ })
+ })
+
+ describe('DELETE /_blacklist/:packageName', () => {
+ describe('with no auth', () => {
+ it('is forbidden', done => {
+ request(server)
+ .delete('/_blacklist/bad-package')
+ .end((err, res) => {
+ expect(res.statusCode).toBe(403)
+ done()
+ })
+ })
+ })
+
+ describe('with the "blacklist.remove" scope', () => {
+ it('can remove a package from the blacklist', done => {
+ withToken({ blacklist: { remove: true } }, token => {
+ request(server)
+ .delete('/_blacklist/bad-package')
+ .send({ token })
+ .end((err, res) => {
+ expect(res.statusCode).toBe(200)
+ expect(res.body.ok).toBe(true)
+ done()
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/server/__tests__/utils/withBlacklist.js b/server/__tests__/utils/withBlacklist.js
new file mode 100644
index 0000000..6d45f71
--- /dev/null
+++ b/server/__tests__/utils/withBlacklist.js
@@ -0,0 +1,7 @@
+const BlacklistAPI = require('../../BlacklistAPI')
+
+function withBlacklist(blacklist, callback) {
+ return Promise.all(blacklist.map(BlacklistAPI.addPackage)).then(callback)
+}
+
+module.exports = withBlacklist
diff --git a/server/__tests__/utils/withRevokedToken.js b/server/__tests__/utils/withRevokedToken.js
new file mode 100644
index 0000000..fe3cf7e
--- /dev/null
+++ b/server/__tests__/utils/withRevokedToken.js
@@ -0,0 +1,12 @@
+const withToken = require('./withToken')
+const AuthAPI = require('../../AuthAPI')
+
+function withRevokedToken(scopes, callback) {
+ withToken(scopes, token => {
+ AuthAPI.revokeToken(token).then(() => {
+ callback(token)
+ })
+ })
+}
+
+module.exports = withRevokedToken
diff --git a/server/__tests__/utils/withToken.js b/server/__tests__/utils/withToken.js
new file mode 100644
index 0000000..dad4de4
--- /dev/null
+++ b/server/__tests__/utils/withToken.js
@@ -0,0 +1,7 @@
+const AuthAPI = require('../../AuthAPI')
+
+function withToken(scopes, callback) {
+ AuthAPI.createToken(scopes).then(callback)
+}
+
+module.exports = withToken
diff --git a/server/actions/addToBlacklist.js b/server/actions/addToBlacklist.js
new file mode 100644
index 0000000..bf65904
--- /dev/null
+++ b/server/actions/addToBlacklist.js
@@ -0,0 +1,48 @@
+const validateNpmPackageName = require('validate-npm-package-name')
+const BlacklistAPI = require('../BlacklistAPI')
+
+function addToBlacklist(req, res) {
+ const packageName = req.body.packageName
+
+ if (!packageName) {
+ return res
+ .status(403)
+ .send({ error: 'Missing "packageName" body parameter' })
+ }
+
+ const nameErrors = validateNpmPackageName(packageName).errors
+
+ // Disallow invalid package names.
+ if (nameErrors) {
+ const reason = nameErrors.join(', ')
+ return res.status(403).send({
+ error: `Invalid package name "${packageName}" (${reason})`
+ })
+ }
+
+ BlacklistAPI.addPackage(packageName).then(
+ added => {
+ if (added) {
+ const userId = req.user.jti
+ console.log(
+ `Package "${packageName}" was added to the blacklist by ${userId}`
+ )
+ }
+
+ res.set({ 'Content-Location': `/_blacklist/${packageName}` }).send({
+ ok: true,
+ message: `Package "${packageName}" was ${added
+ ? 'added to'
+ : 'already in'} the blacklist`
+ })
+ },
+ error => {
+ console.error(error)
+ res.status(500).send({
+ error: `Unable to add "${packageName}" to the blacklist`
+ })
+ }
+ )
+}
+
+module.exports = addToBlacklist
diff --git a/server/actions/createAuth.js b/server/actions/createAuth.js
new file mode 100644
index 0000000..3753177
--- /dev/null
+++ b/server/actions/createAuth.js
@@ -0,0 +1,24 @@
+const AuthAPI = require('../AuthAPI')
+
+const DefaultScopes = {
+ blacklist: {
+ read: true
+ }
+}
+
+function createAuth(req, res) {
+ AuthAPI.createToken(DefaultScopes).then(
+ token => {
+ res.send({ token })
+ },
+ error => {
+ console.error(error)
+
+ res.status(500).send({
+ error: 'Unable to generate auth token'
+ })
+ }
+ )
+}
+
+module.exports = createAuth
diff --git a/server/actions/removeFromBlacklist.js b/server/actions/removeFromBlacklist.js
new file mode 100644
index 0000000..1afa409
--- /dev/null
+++ b/server/actions/removeFromBlacklist.js
@@ -0,0 +1,42 @@
+const validateNpmPackageName = require('validate-npm-package-name')
+const BlacklistAPI = require('../BlacklistAPI')
+
+function removeFromBlacklist(req, res) {
+ const packageName = req.params.packageName
+
+ const nameErrors = validateNpmPackageName(packageName).errors
+
+ // Disallow invalid package names.
+ if (nameErrors) {
+ const reason = nameErrors.join(', ')
+ return res.status(403).send({
+ error: `Invalid package name "${packageName}" (${reason})`
+ })
+ }
+
+ BlacklistAPI.removePackage(packageName).then(
+ removed => {
+ if (removed) {
+ const userId = req.user.jti
+ console.log(
+ `Package "${packageName}" was removed from the blacklist by ${userId}`
+ )
+ }
+
+ res.send({
+ ok: true,
+ message: `Package "${packageName}" was ${removed
+ ? 'removed from'
+ : 'not in'} the blacklist`
+ })
+ },
+ error => {
+ console.error(error)
+ res.status(500).send({
+ error: `Unable to remove "${packageName}" from the blacklist`
+ })
+ }
+ )
+}
+
+module.exports = removeFromBlacklist
diff --git a/server/actions/showAuth.js b/server/actions/showAuth.js
new file mode 100644
index 0000000..282ae17
--- /dev/null
+++ b/server/actions/showAuth.js
@@ -0,0 +1,5 @@
+function showAuth(req, res) {
+ res.send({ auth: req.user })
+}
+
+module.exports = showAuth
diff --git a/server/actions/showBlacklist.js b/server/actions/showBlacklist.js
new file mode 100644
index 0000000..d03ddc0
--- /dev/null
+++ b/server/actions/showBlacklist.js
@@ -0,0 +1,17 @@
+const BlacklistAPI = require('../BlacklistAPI')
+
+function showBlacklist(req, res) {
+ BlacklistAPI.getPackages().then(
+ blacklist => {
+ res.send({ blacklist })
+ },
+ error => {
+ console.error(error)
+ res.status(500).send({
+ error: 'Unable to fetch blacklist'
+ })
+ }
+ )
+}
+
+module.exports = showBlacklist
diff --git a/server/actions/showPublicKey.js b/server/actions/showPublicKey.js
new file mode 100644
index 0000000..8fb46d1
--- /dev/null
+++ b/server/actions/showPublicKey.js
@@ -0,0 +1,7 @@
+const AuthAPI = require('../AuthAPI')
+
+function showPublicKey(req, res) {
+ res.type('text').send(AuthAPI.getPublicKey())
+}
+
+module.exports = showPublicKey
diff --git a/server/actions/showStats.js b/server/actions/showStats.js
new file mode 100644
index 0000000..1d1f9d4
--- /dev/null
+++ b/server/actions/showStats.js
@@ -0,0 +1,62 @@
+const subDays = require('date-fns/sub_days')
+const startOfDay = require('date-fns/start_of_day')
+const startOfSecond = require('date-fns/start_of_second')
+const StatsAPI = require('../StatsAPI')
+
+function showStats(req, res) {
+ let since, until
+ switch (req.query.period) {
+ case 'last-day':
+ until = startOfDay(new Date())
+ since = subDays(until, 1)
+ break
+ case 'last-week':
+ until = startOfDay(new Date())
+ since = subDays(until, 7)
+ break
+ case 'last-month':
+ until = startOfDay(new Date())
+ since = subDays(until, 30)
+ break
+ default:
+ until = req.query.until
+ ? new Date(req.query.until)
+ : startOfSecond(new Date())
+ since = new Date(req.query.since)
+ }
+
+ if (isNaN(since.getTime())) {
+ return res.status(403).send({ error: '?since is not a valid date' })
+ }
+
+ if (isNaN(until.getTime())) {
+ return res.status(403).send({ error: '?until is not a valid date' })
+ }
+
+ if (until <= since) {
+ return res
+ .status(403)
+ .send({ error: '?until date must come after ?since date' })
+ }
+
+ if (until >= new Date()) {
+ return res.status(403).send({ error: '?until must be a date in the past' })
+ }
+
+ StatsAPI.getStats(since, until).then(
+ stats => {
+ res
+ .set({
+ 'Cache-Control': 'public, max-age=60',
+ 'Cache-Tag': 'stats'
+ })
+ .send(stats)
+ },
+ error => {
+ console.error(error)
+ res.status(500).send({ error: 'Unable to fetch stats' })
+ }
+ )
+}
+
+module.exports = showStats
diff --git a/server/createServer.js b/server/createServer.js
index 0cfc671..adc3edb 100644
--- a/server/createServer.js
+++ b/server/createServer.js
@@ -1,23 +1,21 @@
const fs = require('fs')
const path = require('path')
const express = require('express')
+const bodyParser = require('body-parser')
const cors = require('cors')
const morgan = require('morgan')
const checkBlacklist = require('./middleware/checkBlacklist')
-const packageURL = require('./middleware/packageURL')
const fetchFile = require('./middleware/fetchFile')
+const parseURL = require('./middleware/parseURL')
+const requireAuth = require('./middleware/requireAuth')
const serveFile = require('./middleware/serveFile')
+const userToken = require('./middleware/userToken')
morgan.token('fwd', function(req) {
return req.get('x-forwarded-for').replace(/\s/g, '')
})
-/**
- * A list of packages we refuse to serve.
- */
-const PackageBlacklist = require('./PackageBlacklist').blacklist
-
function errorHandler(err, req, res, next) {
console.error(err.stack)
@@ -47,7 +45,6 @@ function createServer() {
}
app.use(errorHandler)
- app.use(cors())
app.use(
express.static('build', {
@@ -55,19 +52,36 @@ function createServer() {
})
)
- if (process.env.NODE_ENV !== 'test') {
+ app.use(cors())
+ app.use(bodyParser.json())
+ app.use(userToken)
- const createStatsServer = require('./createStatsServer')
- app.use('/_stats', createStatsServer())
+ app.get('/_publicKey', require('./actions/showPublicKey'))
+
+ app.post('/_auth', require('./actions/createAuth'))
+ app.get('/_auth', require('./actions/showAuth'))
+
+ app.post(
+ '/_blacklist',
+ requireAuth('blacklist.add'),
+ require('./actions/addToBlacklist')
+ )
+ app.get(
+ '/_blacklist',
+ requireAuth('blacklist.read'),
+ require('./actions/showBlacklist')
+ )
+ app.delete(
+ '/_blacklist/:packageName',
+ requireAuth('blacklist.remove'),
+ require('./actions/removeFromBlacklist')
+ )
+
+ if (process.env.NODE_ENV !== 'test') {
+ app.get('/_stats', require('./actions/showStats'))
}
- app.use(
- '/',
- packageURL,
- checkBlacklist(PackageBlacklist),
- fetchFile,
- serveFile
- )
+ app.use('/', parseURL, checkBlacklist, fetchFile, serveFile)
return app
}
diff --git a/server/createServer.test.js b/server/createServer.test.js
deleted file mode 100644
index 34a145b..0000000
--- a/server/createServer.test.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const request = require('supertest')
-const createServer = require('./createServer')
-
-describe('The server app', function() {
- let app
- beforeEach(() => {
- app = createServer()
- })
-
- it('rejects invalid package names', function(done) {
- request(app)
- .get('/_invalid/index.js')
- .then(res => {
- expect(res.statusCode).toBe(403)
- done()
- })
- })
-
- it('redirects invalid query params', function(done) {
- request(app)
- .get('/react?main=index&invalid')
- .then(res => {
- expect(res.statusCode).toBe(302)
- expect(res.headers.location).toBe('/react?main=index')
- done()
- })
- })
-
- it('redirects /_meta to ?meta', function(done) {
- request(app)
- .get('/_meta/react?main=index')
- .then(res => {
- expect(res.statusCode).toBe(302)
- expect(res.headers.location).toBe('/react?main=index&meta')
- done()
- })
- })
-})
diff --git a/server/createStatsServer.js b/server/createStatsServer.js
deleted file mode 100644
index 37e477c..0000000
--- a/server/createStatsServer.js
+++ /dev/null
@@ -1,87 +0,0 @@
-const express = require('express')
-const subDays = require('date-fns/sub_days')
-const startOfDay = require('date-fns/start_of_day')
-const startOfSecond = require('date-fns/start_of_second')
-const StatsAPI = require('./StatsAPI')
-
-function serveArbitraryStats(req, res) {
- const now = startOfSecond(new Date())
- const since = req.query.since ? new Date(req.query.since) : subDays(now, 30)
- const until = req.query.until ? new Date(req.query.until) : now
-
- if (isNaN(since.getTime())) {
- return res.status(403).send({ error: '?since is not a valid date' })
- }
-
- if (isNaN(until.getTime())) {
- return res.status(403).send({ error: '?until is not a valid date' })
- }
-
- if (until <= since) {
- return res
- .status(403)
- .send({ error: '?until date must come after ?since date' })
- }
-
- if (until > now) {
- return res.status(403).send({ error: '?until must be a date in the past' })
- }
-
- StatsAPI.getStats(since, until, (error, stats) => {
- if (error) {
- console.error(error)
- res.status(500).send({ error: 'Unable to fetch stats' })
- } else {
- res
- .set({
- 'Cache-Control': 'public, max-age=60',
- 'Cache-Tag': 'stats'
- })
- .send(stats)
- }
- })
-}
-
-function servePastDaysStats(days, req, res) {
- const until = startOfDay(new Date())
- const since = subDays(until, days)
-
- StatsAPI.getStats(since, until, (error, stats) => {
- if (error) {
- console.error(error)
- res.status(500).send({ error: 'Unable to fetch stats' })
- } else {
- res
- .set({
- 'Cache-Control': 'public, max-age=60',
- 'Cache-Tag': 'stats'
- })
- .send(stats)
- }
- })
-}
-
-function serveLastMonthStats(req, res) {
- servePastDaysStats(30, req, res)
-}
-
-function serveLastWeekStats(req, res) {
- servePastDaysStats(7, req, res)
-}
-
-function serveLastDayStats(req, res) {
- servePastDaysStats(1, req, res)
-}
-
-function createStatsServer() {
- const app = express.Router()
-
- app.get('/', serveArbitraryStats)
- app.get('/last-month', serveLastMonthStats)
- app.get('/last-week', serveLastWeekStats)
- app.get('/last-day', serveLastDayStats)
-
- return app
-}
-
-module.exports = createStatsServer
diff --git a/server/middleware/checkBlacklist.js b/server/middleware/checkBlacklist.js
index 0f652ec..13a85bc 100644
--- a/server/middleware/checkBlacklist.js
+++ b/server/middleware/checkBlacklist.js
@@ -1,15 +1,26 @@
-function checkBlacklist(blacklist) {
- return function(req, res, next) {
- // Do not allow packages that have been blacklisted.
- if (blacklist.includes(req.packageName)) {
- res
- .status(403)
- .type('text')
- .send(`Package "${req.packageName}" is blacklisted`)
- } else {
- next()
+const BlacklistAPI = require('../BlacklistAPI')
+
+function checkBlacklist(req, res, next) {
+ BlacklistAPI.containsPackage(req.packageName).then(
+ blacklisted => {
+ // Disallow packages that have been blacklisted.
+ if (blacklisted) {
+ res
+ .status(403)
+ .type('text')
+ .send(`Package "${req.packageName}" is blacklisted`)
+ } else {
+ next()
+ }
+ },
+ error => {
+ console.error(error)
+
+ res.status(500).send({
+ error: 'Unable to fetch the blacklist'
+ })
}
- }
+ )
}
module.exports = checkBlacklist
diff --git a/server/middleware/packageURL.js b/server/middleware/parseURL.js
similarity index 79%
rename from server/middleware/packageURL.js
rename to server/middleware/parseURL.js
index 7bb6a03..784f711 100644
--- a/server/middleware/packageURL.js
+++ b/server/middleware/parseURL.js
@@ -1,4 +1,4 @@
-const validateNPMPackageName = require('validate-npm-package-name')
+const validateNpmPackageName = require('validate-npm-package-name')
const parsePackageURL = require('../utils/parsePackageURL')
const createSearch = require('./utils/createSearch')
@@ -29,7 +29,7 @@ function sanitizeQuery(query) {
/**
* Parse and validate the URL.
*/
-function packageURL(req, res, next) {
+function parseURL(req, res, next) {
// Redirect /_meta/path to /path?meta.
if (req.path.match(/^\/_meta\//)) {
req.query.meta = ''
@@ -46,28 +46,30 @@ function packageURL(req, res, next) {
// Redirect requests with unknown query params to their equivalents
// with only known params so they can be served from the cache. This
// prevents people using random query params designed to bust the cache.
- if (!queryIsKnown(req.query))
+ if (!queryIsKnown(req.query)) {
return res.redirect(302, req.path + createSearch(sanitizeQuery(req.query)))
+ }
const url = parsePackageURL(req.url)
- // Do not allow invalid URLs.
- if (url == null)
+ // Disallow invalid URLs.
+ if (url == null) {
return res
.status(403)
.type('text')
.send(`Invalid URL: ${req.url}`)
+ }
- const nameErrors = validateNPMPackageName(url.packageName).errors
+ const nameErrors = validateNpmPackageName(url.packageName).errors
- // Do not allow invalid package names.
- if (nameErrors)
+ // Disallow invalid package names.
+ if (nameErrors) {
+ const reason = nameErrors.join(', ')
return res
.status(403)
.type('text')
- .send(
- `Invalid package name: ${url.packageName} (${nameErrors.join(', ')})`
- )
+ .send(`Invalid package name "${url.packageName}" (${reason})`)
+ }
req.packageName = url.packageName
req.packageVersion = url.packageVersion
@@ -80,4 +82,4 @@ function packageURL(req, res, next) {
next()
}
-module.exports = packageURL
+module.exports = parseURL
diff --git a/server/middleware/requireAuth.js b/server/middleware/requireAuth.js
new file mode 100644
index 0000000..0a9fe5d
--- /dev/null
+++ b/server/middleware/requireAuth.js
@@ -0,0 +1,40 @@
+/**
+ * Adds the given scope to the array in req.auth if the user has sufficient
+ * permissions. Otherwise rejects the request.
+ */
+function requireAuth(scope) {
+ let checkScopes
+ if (scope.includes('.')) {
+ const parts = scope.split('.')
+ checkScopes = scopes =>
+ parts.reduce((memo, part) => memo && memo[part], scopes) != null
+ } else {
+ checkScopes = scopes => scopes[scope] != null
+ }
+
+ return function(req, res, next) {
+ if (req.auth && req.auth.includes(scope)) {
+ return next() // Already auth'd
+ }
+
+ const user = req.user
+
+ if (!user) {
+ return res.status(403).send({ error: 'Missing auth token' })
+ }
+
+ if (!user.scopes || !checkScopes(user.scopes)) {
+ return res.status(403).send({ error: 'Insufficient scopes' })
+ }
+
+ if (req.auth) {
+ req.auth.push(scope)
+ } else {
+ req.auth = [scope]
+ }
+
+ next()
+ }
+}
+
+module.exports = requireAuth
diff --git a/server/middleware/userToken.js b/server/middleware/userToken.js
new file mode 100644
index 0000000..b577f0b
--- /dev/null
+++ b/server/middleware/userToken.js
@@ -0,0 +1,41 @@
+const AuthAPI = require('../AuthAPI')
+
+const ReadMethods = { GET: true, HEAD: true }
+
+/**
+ * Sets req.user from the payload in the auth token in the request.
+ */
+function userToken(req, res, next) {
+ if (req.user) {
+ return next()
+ }
+
+ const token = (ReadMethods[req.method] ? req.query : req.body).token
+
+ if (!token) {
+ req.user = null
+ return next()
+ }
+
+ AuthAPI.verifyToken(token).then(
+ payload => {
+ req.user = payload
+ next()
+ },
+ error => {
+ if (error.name === 'JsonWebTokenError') {
+ res.status(403).send({
+ error: `Bad auth token: ${error.message}`
+ })
+ } else {
+ console.error(error)
+
+ res.status(500).send({
+ error: 'Unable to verify auth'
+ })
+ }
+ }
+ )
+}
+
+module.exports = userToken
diff --git a/server/middleware/utils/getFileContentType.test.js b/server/middleware/utils/__tests__/getFileContentType-test.js
similarity index 95%
rename from server/middleware/utils/getFileContentType.test.js
rename to server/middleware/utils/__tests__/getFileContentType-test.js
index 5c0b0b4..c738663 100644
--- a/server/middleware/utils/getFileContentType.test.js
+++ b/server/middleware/utils/__tests__/getFileContentType-test.js
@@ -1,4 +1,4 @@
-const getFileContentType = require('./getFileContentType')
+const getFileContentType = require('../getFileContentType')
it('gets a content type of text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile', () => {
expect(getFileContentType('AUTHORS')).toBe('text/plain')
diff --git a/server/utils/parsePackageURL.test.js b/server/utils/__tests__/parsePackageURL-test.js
similarity index 92%
rename from server/utils/parsePackageURL.test.js
rename to server/utils/__tests__/parsePackageURL-test.js
index b3b7751..ab44c5a 100644
--- a/server/utils/parsePackageURL.test.js
+++ b/server/utils/__tests__/parsePackageURL-test.js
@@ -1,10 +1,10 @@
-const parsePackageURL = require('./parsePackageURL')
+const parsePackageURL = require('../parsePackageURL')
describe('parsePackageURL', () => {
it('parses plain packages', () => {
expect(parsePackageURL('/history@1.0.0/umd/history.min.js')).toEqual({
pathname: '/history@1.0.0/umd/history.min.js',
- search: null,
+ search: '',
query: {},
packageName: 'history',
packageVersion: '1.0.0',
@@ -15,7 +15,7 @@ describe('parsePackageURL', () => {
it('parses plain packages with a hyphen in the name', () => {
expect(parsePackageURL('/query-string@5.0.0/index.js')).toEqual({
pathname: '/query-string@5.0.0/index.js',
- search: null,
+ search: '',
query: {},
packageName: 'query-string',
packageVersion: '5.0.0',
@@ -26,7 +26,7 @@ describe('parsePackageURL', () => {
it('parses plain packages with no version specified', () => {
expect(parsePackageURL('/query-string/index.js')).toEqual({
pathname: '/query-string/index.js',
- search: null,
+ search: '',
query: {},
packageName: 'query-string',
packageVersion: 'latest',
@@ -37,7 +37,7 @@ describe('parsePackageURL', () => {
it('parses plain packages with version spec', () => {
expect(parsePackageURL('/query-string@>=4.0.0/index.js')).toEqual({
pathname: '/query-string@>=4.0.0/index.js',
- search: null,
+ search: '',
query: {},
packageName: 'query-string',
packageVersion: '>=4.0.0',
@@ -48,7 +48,7 @@ describe('parsePackageURL', () => {
it('parses scoped packages', () => {
expect(parsePackageURL('/@angular/router@4.3.3/src/index.d.ts')).toEqual({
pathname: '/@angular/router@4.3.3/src/index.d.ts',
- search: null,
+ search: '',
query: {},
packageName: '@angular/router',
packageVersion: '4.3.3',
@@ -59,7 +59,7 @@ describe('parsePackageURL', () => {
it('parses package names with a period in them', () => {
expect(parsePackageURL('/index.js')).toEqual({
pathname: '/index.js',
- search: null,
+ search: '',
query: {},
packageName: 'index.js',
packageVersion: 'latest',
diff --git a/yarn.lock b/yarn.lock
index e3bad96..6abb441 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -919,6 +919,10 @@ base64-js@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
+base64url@2.0.0, base64url@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb"
+
basic-auth@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884"
@@ -957,6 +961,21 @@ bluebird@^3.4.6:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
+body-parser@^1.18.2:
+ version "1.18.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
+ dependencies:
+ bytes "3.0.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.1"
+ http-errors "~1.6.2"
+ iconv-lite "0.4.19"
+ on-finished "~2.3.0"
+ qs "6.5.1"
+ raw-body "2.3.2"
+ type-is "~1.6.15"
+
boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -1013,6 +1032,10 @@ bser@1.0.2:
dependencies:
node-int64 "^0.4.0"
+buffer-equal-constant-time@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+
buffer@^4.9.0:
version "4.9.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
@@ -1037,6 +1060,10 @@ bytes@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a"
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+
caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -1337,6 +1364,10 @@ content-type@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+
convert-source-map@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
@@ -1566,6 +1597,12 @@ debug@2.6.8, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.6.0, debug@^2.6.
dependencies:
ms "2.0.0"
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1721,6 +1758,13 @@ ecc-jsbn@~0.1.1:
dependencies:
jsbn "~0.1.0"
+ecdsa-sig-formatter@1.0.9:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1"
+ dependencies:
+ base64url "^2.0.0"
+ safe-buffer "^5.0.1"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2589,7 +2633,7 @@ htmlparser2@~3.3.0:
domutils "1.1"
readable-stream "1.0"
-http-errors@~1.6.1, http-errors@~1.6.2:
+http-errors@1.6.2, http-errors@~1.6.1, http-errors@~1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
dependencies:
@@ -2630,6 +2674,10 @@ iconv-lite@0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
+iconv-lite@0.4.19:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+
iconv-lite@~0.4.13:
version "0.4.18"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
@@ -3262,6 +3310,21 @@ jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
+jsonwebtoken@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz#c6397cd2e5fd583d65c007a83dc7bb78e6982b83"
+ dependencies:
+ jws "^3.1.4"
+ lodash.includes "^4.3.0"
+ lodash.isboolean "^3.0.3"
+ lodash.isinteger "^4.0.4"
+ lodash.isnumber "^3.0.3"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.once "^4.0.0"
+ ms "^2.0.0"
+ xtend "^4.0.1"
+
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -3275,6 +3338,23 @@ jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
+jwa@^1.1.4:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5"
+ dependencies:
+ base64url "2.0.0"
+ buffer-equal-constant-time "1.0.1"
+ ecdsa-sig-formatter "1.0.9"
+ safe-buffer "^5.0.1"
+
+jws@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2"
+ dependencies:
+ base64url "^2.0.0"
+ jwa "^1.1.4"
+ safe-buffer "^5.0.1"
+
kind-of@^3.0.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -3406,6 +3486,10 @@ lodash.defaults@^4.0.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+lodash.includes@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
+
lodash.isarguments@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@@ -3414,6 +3498,26 @@ lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+lodash.isboolean@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+
+lodash.isinteger@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
+
+lodash.isnumber@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
+
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+
+lodash.isstring@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+
lodash.keys@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -3426,6 +3530,10 @@ lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+lodash.once@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+
lodash.pickby@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff"
@@ -3572,6 +3680,10 @@ mime@^1.3.4, mime@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0"
+mime@^1.4.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
+
min-document@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
@@ -3618,7 +3730,7 @@ ms@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
-ms@2.0.0:
+ms@2.0.0, ms@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -3672,6 +3784,10 @@ node-fetch@^1.0.1:
encoding "^0.1.11"
is-stream "^1.0.1"
+node-forge@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
+
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -4401,6 +4517,10 @@ qs@6.5.0, qs@^6.4.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49"
+qs@6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
@@ -4445,6 +4565,15 @@ range-parser@^1.0.3, range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+raw-body@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
+ dependencies:
+ bytes "3.0.0"
+ http-errors "1.6.2"
+ iconv-lite "0.4.19"
+ unpipe "1.0.0"
+
rc@^1.1.7:
version "1.2.1"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
@@ -5389,7 +5518,7 @@ uniqs@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
-unpipe@~1.0.0:
+unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"