Compare commits

...
This repository has been archived on 2024-03-30. You can view files and clone it, but cannot push or open issues or pull requests.

6 Commits
main ... main

Author SHA1 Message Date
beerpiss 7d162724db Add a whitelist for creating keychips 2024-03-12 10:37:39 +07:00
beerpiss 42693bb454 I'm going insane 2024-03-04 13:13:53 +07:00
beerpsi f53d4f780c Update app.js 2024-03-04 04:10:48 +00:00
beerpsi e96d8e891c The cors middleware is shite 2024-03-04 04:10:10 +00:00
beerpsi 4bbb166071 CORS is Shite 2024-03-04 04:02:23 +00:00
beerpiss ed98ebfebc sessions sessions sessions 2024-02-24 15:18:11 +07:00
5 changed files with 224 additions and 142 deletions

90
app.js
View File

@ -1,17 +1,73 @@
const express = require("express");
const app = express();
const cors = require("cors");
const sunGetRoutes = require("./chunithm/13/getRoutes");
const sunPostRoutes = require("./chunithm/13/postRoutes");
app.use(express.json());
app.use(cors({ credentials: true, origin: "*" }));
app.use("/SDHD", sunGetRoutes);
app.use("/SDHD", sunPostRoutes);
// Starting the server
const PORT = 4000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const expressSession = require("express-session");
const MySQLStore = require("express-mysql-session")(expressSession);
const app = express();
const nodeEnv = process.env.NODE_ENV;
if (!nodeEnv) {
console.error("No NODE_ENV specified in environment. Terminating.");
process.exit(1);
}
// Let NGINX do its magic.
app.set("trust proxy", "loopback");
app.set("query parser", "simple");
const store = new MySQLStore({
host: process.env.host,
user: process.env.user,
password: process.env.password,
database: process.env.database,
port: process.env.port,
schema: {
tableName: "cozynet_artemisapi_sessions",
columnNames: {
session_id: "session_id",
expires: "expires",
data: "data",
},
},
});
const userSessionMiddleware = expressSession({
name: "ArtemisAPI_SESSION",
secret: process.env.JWT_SECRET,
store,
resave: true,
saveUninitialized: false,
cookie: {
secure: nodeEnv === "production",
sameSite: nodeEnv === "production" ? "strict" : "none",
},
});
if (nodeEnv !== "production" && process.env.CLIENT_DEV_SERVER && process.env.CLIENT_DEV_SERVER.length > 0) {
const clientDevServer = process.env.CLIENT_DEV_SERVER;
app.use(cors({ credentials: true, origin: clientDevServer }));
} else {
app.use(cors({ credentials: false, origin: "*" }));
app.use((req, res, next) => {
res.header("Access-Control-Allow-Credentials", "false");
next();
});
app.use(helmet());
}
app.use(userSessionMiddleware);
const sunGetRoutes = require("./chunithm/13/getRoutes");
const sunPostRoutes = require("./chunithm/13/postRoutes");
app.use(express.json());
// app.use(cors({ credentials: true, origin: "*" }));
app.use("/SDHD", sunGetRoutes);
app.use("/SDHD", sunPostRoutes);
// Starting the server
const PORT = 4000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

View File

@ -1,28 +1,40 @@
const express = require("express");
const router = express.Router();
const pool1 = require("../../db");
const { zonedTimeToUtc, format } = require("date-fns-tz");
const { RequireNotGuest } = require("../../middleware/auth");
router.get("/access-code", getAccessCodeHandler);
router.get("/items", getItemsHandler);
router.get("/chuni-avatar-items", getChuniAvatarItems);
router.get("/profile-data", getProfileDataHandler);
router.get("/chuni-score-playlog", getChuniScorePlaylogHandler);
router.get("/chuni-best-and-top", getChuniBestAndTopHandler);
router.get("/chuni-player-team", getChuniPlayerTeamHandler);
router.get("/chuni-duel-items", getChuniDuelItems);
router.get("/chuni-nameplate-items", getChuniNamePlateItems);
router.get("/chuni-map-items", getChuniMapItems);
router.get("/listFriends", listFriendsHandler);
router.get("/listRivals", listRivalsHandler);
router.get("/chuni-static-music", getChuniStaticMusic);
router.get("/get-trophies", getTrophiesHandler);
router.get("/list-favorite-songs", listFavoriteSongsHandler);
router.get("/chuni-recent", getChuniRecentHandler);
const router = express.Router();
router.get("/user", RequireNotGuest, getUserHandler);
router.get("/access-code", RequireNotGuest, getAccessCodeHandler);
router.get("/items", RequireNotGuest, getItemsHandler);
router.get("/chuni-avatar-items", RequireNotGuest, getChuniAvatarItems);
router.get("/profile-data", RequireNotGuest, getProfileDataHandler);
router.get("/chuni-score-playlog", RequireNotGuest, getChuniScorePlaylogHandler);
router.get("/chuni-best-and-top", RequireNotGuest, getChuniBestAndTopHandler);
router.get("/chuni-player-team", RequireNotGuest, getChuniPlayerTeamHandler);
router.get("/chuni-duel-items", RequireNotGuest, getChuniDuelItems);
router.get("/chuni-nameplate-items", RequireNotGuest, getChuniNamePlateItems);
router.get("/chuni-map-items", RequireNotGuest, getChuniMapItems);
router.get("/listFriends", RequireNotGuest, listFriendsHandler);
router.get("/listRivals", RequireNotGuest, listRivalsHandler);
router.get("/chuni-static-music", RequireNotGuest, getChuniStaticMusic);
router.get("/get-trophies", RequireNotGuest, getTrophiesHandler);
router.get("/list-favorite-songs", RequireNotGuest, listFavoriteSongsHandler);
router.get("/chuni-recent", RequireNotGuest, getChuniRecentHandler);
function getUserHandler(req, res) {
return res
.status(200)
.json({
success: true,
message: "Found user.",
body: req.session.cozynet,
});
}
function getAccessCodeHandler(req, res) {
const userId = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
if (!userId || !version) {
return res
@ -61,8 +73,7 @@ function getAccessCodeHandler(req, res) {
}
function getItemsHandler(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -78,7 +89,7 @@ function getItemsHandler(req, res) {
JOIN chuni_profile_data pd ON uai.aimeUserId = pd.user
WHERE pd.version = ?
`,
[user, version],
[userId, version],
(error, results, fields) => {
if (error) {
console.error("Error retrieving items:", error);
@ -91,8 +102,7 @@ function getItemsHandler(req, res) {
}
function getProfileDataHandler(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -114,7 +124,7 @@ function getProfileDataHandler(req, res) {
FROM UserProfile
WHERE version = ?
`,
[user, version],
[userId, version],
(error, results, fields) => {
if (error) {
console.error("Error retrieving profile data:", error);
@ -132,8 +142,7 @@ function getProfileDataHandler(req, res) {
}
function getChuniScorePlaylogHandler(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -210,7 +219,7 @@ function getChuniScorePlaylogHandler(req, res) {
ORDER BY
userPlayDate DESC;
`,
[user, version], // Pass version as a parameter
[userId, version], // Pass version as a parameter
(error, results, fields) => {
if (error) throw error;
results.forEach((row) => {
@ -224,8 +233,7 @@ function getChuniScorePlaylogHandler(req, res) {
}
function getChuniRecentHandler(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -265,7 +273,7 @@ function getChuniRecentHandler(req, res) {
ORDER BY
rating DESC;
`,
[user, version], // Pass version as a parameter
[userId, version], // Pass version as a parameter
(error, results, fields) => {
if (error) throw error;
res.json(results);
@ -274,8 +282,7 @@ function getChuniRecentHandler(req, res) {
}
function getProfileDataHandler(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -297,7 +304,7 @@ function getProfileDataHandler(req, res) {
FROM UserProfile
WHERE version = ?
`,
[user, version],
[userId, version],
(error, results, fields) => {
if (error) {
console.error("Error retrieving profile data:", error);
@ -315,7 +322,7 @@ function getProfileDataHandler(req, res) {
}
function getChuniStaticMusic(req, res) {
const version = req.query.version; // Get version from URL parameter
const version = req.session.cozynet.version; // Get version from URL parameter
pool1.query(
`
@ -337,8 +344,7 @@ function getChuniStaticMusic(req, res) {
}
function getChuniBestAndTopHandler(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -399,7 +405,7 @@ function getChuniBestAndTopHandler(req, res) {
csp.userPlayDate DESC
`,
[user, version],
[userId, version],
(error, results, fields) => {
if (error) {
console.error("Error retrieving best and top data:", error);
@ -414,7 +420,7 @@ function getChuniBestAndTopHandler(req, res) {
}
function getChuniPlayerTeamHandler(req, res) {
const user = req.query.user;
const user = req.session.cozynet.userId;
pool1.query(
`SELECT *
@ -430,7 +436,7 @@ function getChuniPlayerTeamHandler(req, res) {
}
function getChuniAvatarItems(req, res) {
const user = req.query.user;
const user = req.session.cozynet.userId;
pool1.query(
`
@ -450,8 +456,7 @@ function getChuniAvatarItems(req, res) {
}
function getChuniNamePlateItems(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -477,7 +482,7 @@ ORDER BY
`,
[user, version],
[userId, version],
(error, results, fields) => {
if (error) throw error;
@ -487,8 +492,7 @@ ORDER BY
}
function getChuniMapItems(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -507,7 +511,7 @@ WHERE
AND chuni_item_item.itemKind = 8
AND chuni_profile_data.version = ?
`,
[user, version],
[userId, version],
(error, results, fields) => {
if (error) throw error;
@ -517,8 +521,7 @@ WHERE
}
function getChuniDuelItems(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
` SELECT
@ -535,7 +538,7 @@ WHERE
AND chuni_item_item.itemKind = 9
AND chuni_profile_data.version = ?
`,
[user, version],
[userId, version],
(error, results, fields) => {
if (error) throw error;
@ -544,10 +547,9 @@ WHERE
);
}
function listRivalsHandler(req, res) {
const user1 = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
if (!user1 || !version) {
if (!userId || !version) {
return res
.status(400)
.json({ error: "User and version parameters are required" });
@ -561,7 +563,7 @@ function listRivalsHandler(req, res) {
JOIN chuni_profile_data pd ON au.id = pd.user
WHERE cf.user = ? AND pd.version = ?
`,
[user1, version],
[userId, version],
(error, results) => {
if (error) {
console.error("Error retrieving rivals:", error);
@ -573,10 +575,9 @@ function listRivalsHandler(req, res) {
);
}
function listFavoriteSongsHandler(req, res) {
const user1 = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
if (!user1 || !version) {
if (!userId || !version) {
return res
.status(400)
.json({ error: "User and version parameters are required" });
@ -600,7 +601,7 @@ WHERE
`,
[user1, version],
[userId, version],
(error, results) => {
if (error) {
console.error("Error retrieving rivals:", error);
@ -613,10 +614,9 @@ WHERE
}
function listFriendsHandler(req, res) {
const user1 = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
if (!user1 || !version) {
if (!userId || !version) {
return res
.status(400)
.json({ error: "User and version parameters are required" });
@ -630,7 +630,7 @@ function listFriendsHandler(req, res) {
JOIN chuni_profile_data pd ON au.id = pd.user
WHERE cf.user = ? AND pd.version = ?
`,
[user1, version],
[userId, version],
(error, results) => {
if (error) {
console.error("Error retrieving rivals:", error);
@ -643,8 +643,7 @@ function listFriendsHandler(req, res) {
}
function getTrophiesHandler(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet;
pool1.query(
`
@ -654,7 +653,7 @@ FROM
cozynet_chuni_static_trophies cst
`,
[user, version],
[userId, version],
(error, results, fields) => {
if (error) {
console.error("Error retrieving items:", error);

View File

@ -1,36 +1,31 @@
// postRoutes.js
const express = require("express");
const router = express.Router();
const pool1 = require("../../db");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { RequireNotGuest } = require("../../middleware/auth");
function generateToken(userId, version, rivalCode) {
const payload = { userId, version, rivalCode }; // Include rivalCode in the payload
const secret = process.env.JWT_SECRET;
const options = { expiresIn: "12h" };
const token = jwt.sign(payload, secret, options);
return token;
}
const router = express.Router({ mergeParams: true });
router.post("/registerUser", registerUserHandler);
router.post("/verifyUser", verifyUserHandler);
router.post("/updateAccessCode", updateAccessCodeHandler);
router.post("/update_chuni_player_team", updateChuniPlayerTeamHandler);
router.post("/create_keychip", createKeychipHandler);
router.post("/logoutUser", logoutUserHandler);
router.post("/updateAccessCode", RequireNotGuest, updateAccessCodeHandler);
router.post("/update_chuni_player_team", RequireNotGuest, updateChuniPlayerTeamHandler);
router.post("/create_keychip", RequireNotGuest, createKeychipHandler);
router.post(
"/update_chuni_profile_player_team",
RequireNotGuest,
updateChuniProfilePlayerTeamHandler,
);
router.post("/update_chuni_item_duel", updateChuniItemDuelHandler);
router.post("/update_chuni_item_mapchar", updateChuniItemMapcharHandler);
router.post("/update_trophyid", updateTrophyIdHandler);
router.post("/update_nameplateId", updateNameplateIdHandler);
router.post("/updateAvatarCustomization", updateAvatarCustomizationHandler);
router.post("/update_chuni_player_rivals", updateChuniPlayerRivalsHandler);
router.post("/update_friendlist", updateChuniFriendList);
router.post("/update_chuni_favorite_music", updateChuniFavoriteMusic);
router.post("/update_chuni_item_duel", RequireNotGuest, updateChuniItemDuelHandler);
router.post("/update_chuni_item_mapchar", RequireNotGuest, updateChuniItemMapcharHandler);
router.post("/update_trophyid", RequireNotGuest, updateTrophyIdHandler);
router.post("/update_nameplateId", RequireNotGuest, updateNameplateIdHandler);
router.post("/updateAvatarCustomization", RequireNotGuest, updateAvatarCustomizationHandler);
router.post("/update_chuni_player_rivals", RequireNotGuest, updateChuniPlayerRivalsHandler);
router.post("/update_friendlist", RequireNotGuest, updateChuniFriendList);
router.post("/update_chuni_favorite_music", RequireNotGuest, updateChuniFavoriteMusic);
function registerUserHandler(req, res) {
const { accessCode, username, password, email } = req.body;
@ -50,14 +45,14 @@ function registerUserHandler(req, res) {
if (rows.length === 0) {
return res.status(404).json({
error:
"Please play at least one credit before signing in to the web ui",
"Please play at least one credit before registering.",
});
}
const existingUser = rows[0];
if (existingUser.username || existingUser.email) {
return res.status(409).json({
error: "Cannot change username, email, or password during sign-up.",
error: "Cannot change username, email, or password by signing up.",
});
}
@ -70,12 +65,12 @@ function registerUserHandler(req, res) {
});
}
registerUser(existingUser.id, username, hashedPassword, email, res);
registerUser(req, existingUser.id, username, hashedPassword, email, res);
});
},
);
}
function registerUser(userId, username, password, email, res) {
function registerUser(req, userId, username, password, email, res) {
const query =
"UPDATE aime_user SET username = ?, password = ?, email = ? WHERE id = ?";
@ -83,7 +78,7 @@ function registerUser(userId, username, password, email, res) {
if (err) {
console.error("Error registering user:", err);
return res.status(500).json({
error: "Username or email is already in use by another player",
error: "Username or email is already in use by another player.",
});
}
@ -149,14 +144,8 @@ function registerUser(userId, username, password, email, res) {
});
}
// Generate a token with the rival code included
const token = generateToken(userId, version, rivalCode);
res.json({
message: "User registered successfully",
userId,
token,
rivalCode,
});
MountAuthCookie(req, userId, version, rivalCode);
res.json({ message: "User registered successfully" });
},
);
}
@ -183,17 +172,17 @@ function verifyUserHandler(req, res) {
(err, rows) => {
if (err) {
console.error("Error verifying user:", err);
return res.status(500).json({ error: "Failed to verify user" });
return res.status(500).json({ error: "Internal server error." });
}
if (rows.length === 0) {
return res.status(404).json({ error: "Invalid username" });
return res.status(404).json({ error: "This username does not exist." });
}
bcrypt.compare(password, rows[0].password, function(err, result) {
if (err) {
console.error("Error comparing password:", err);
return res.status(500).json({ error: "Failed to compare password" });
return res.status(500).json({ error: "Internal server error." });
}
if (result) {
@ -260,27 +249,37 @@ function verifyUserHandler(req, res) {
);
}
const token = generateToken(rows[0].id, version, rivalCode);
MountAuthCookie(req, rows[0].id, version, rivalCode);
res.json({
message: "User verified successfully",
userId: rows[0].id,
accessCode: rows[0].access_code,
token,
rivalCode,
});
res.json({ message: "User verified successfully" });
},
);
},
);
} else {
return res.status(404).json({ error: "Invalid password" });
return res.status(403).json({ error: "Invalid password provided." });
}
});
},
);
}
function logoutUserHandler(req, res) {
if (req.session.cozynet?.userId === undefined) {
return res.status(409).json({
success: false,
message: "You are not logged in.",
});
}
req.session.destroy(() => 0);
return res.status(200).json({
success: true,
message: "Logged out.",
body: {},
})
}
// Assume you are using Express
function updateChuniPlayerTeamHandler(req, res) {
const { teamName } = req.body;
@ -318,7 +317,14 @@ function updateChuniPlayerTeamHandler(req, res) {
}
function createKeychipHandler(req, res) {
const { arcade_nickname, name, game, namcopcbid, serial, user } = req.body;
const user = req.session.cozynet.userId;
const allowedUsers = process.env.SYSOP_USER_IDS.split(",").map((e) => Number(e));
if (!allowedUsers.includes(user)) {
return res.status(401).send("You are not allowed to create a keychip.");
}
const { arcade_nickname, name, game, namcopcbid, serial } = req.body;
const sqlValidateAccessCode = `
SELECT aime_user.id
@ -413,7 +419,7 @@ function createKeychipHandler(req, res) {
}
function updateChuniProfilePlayerTeamHandler(req, res) {
const user = req.query.user;
const user = req.session.cozynet.userId;
const { teamId } = req.body;
@ -441,7 +447,7 @@ function updateChuniProfilePlayerTeamHandler(req, res) {
});
}
function updateChuniItemDuelHandler(req, res) {
const user = req.query.user;
const user = req.session.cozynet.userId;
const { voiceId } = req.body;
@ -470,7 +476,7 @@ function updateChuniItemDuelHandler(req, res) {
}
function updateChuniItemMapcharHandler(req, res) {
const user = req.query.user;
const user = req.session.cozynet.userId;
const { mapIconId } = req.body;
@ -498,7 +504,7 @@ function updateChuniItemMapcharHandler(req, res) {
});
}
function updateTrophyIdHandler(req, res) {
const user = req.query.user;
const user = req.session.cozynet.userId;
const { trophyId } = req.body;
const updateQuery = `
@ -528,7 +534,7 @@ function updateTrophyIdHandler(req, res) {
});
}
function updateNameplateIdHandler(req, res) {
const user = req.query.user;
const user = req.session.cozynet.userId;
const { nameplateId } = req.body;
@ -557,7 +563,7 @@ function updateNameplateIdHandler(req, res) {
}
function updateAvatarCustomizationHandler(req, res) {
const user = req.query.user;
const user = req.session.cozynet.userId;
const { avatarItem, avatarBack, avatarFace, avatarWear, avatarHead } =
req.body;
@ -614,7 +620,7 @@ function updateAvatarCustomizationHandler(req, res) {
}
function updateChuniFavoriteMusic(req, res) {
// Assuming you have middleware to verify and decode the JWT and store user ID in req.user
const { user: user1, version } = req.query;
const { userId: user1, version } = req.session.cozynet;
const songId = req.body.songId;
// Check if the user has already added the specified musicId as a favorite
@ -709,7 +715,8 @@ function updateChuniFavoriteMusic(req, res) {
function updateChuniPlayerRivalsHandler(req, res) {
// Assuming you have middleware to verify and decode the JWT and store user ID in req.user
const { user1, version, rivalCode } = req.body;
const { userId: user1, version } = req.session.cozynet;
const rivalCode = req.body.rivalCode;
// Check if user1 already has four rivals
const countQuery1 = `
@ -895,14 +902,13 @@ function updateChuniFriendList(req, res) {
});
}
function updateAccessCodeHandler(req, res) {
const user = req.query.user;
const version = req.query.version; // Get version from URL parameter
const { userId, version } = req.session.cozynet; // Get version from URL parameter
const { access_code } = req.body;
if (!user || !version) {
if (!version) {
return res
.status(400)
.json({ error: "User and version parameters are required" });
.json({ error: "Version parameter is required" });
}
const updateQuery = `
@ -912,7 +918,7 @@ function updateAccessCodeHandler(req, res) {
WHERE ac.user = ? AND pd.version = ?;
`;
pool1.query(updateQuery, [access_code, user, version], (error, results) => {
pool1.query(updateQuery, [access_code, userId, version], (error, results) => {
if (error) {
console.error("Error updating access code:", error);
return res.status(500).json({
@ -936,4 +942,10 @@ function updateAccessCodeHandler(req, res) {
});
});
}
module.exports = router;
function MountAuthCookie(req, userId, version, rivalCode) {
req.session.cozynet = { userId, version, rivalCode };
req.session.cookie.maxAge = 3.154e10;
}

12
middleware/auth.js Normal file
View File

@ -0,0 +1,12 @@
const RequireNotGuest = (req, res, next) => {
if (req.session.cozynet?.userId === undefined) {
return res.status(401).json({
success: false,
message: "This endpoint requires authentication.",
})
}
next();
}
module.exports = { RequireNotGuest };

View File

@ -5,8 +5,8 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start:dev": "nodemon --env-file=.env.development app.js",
"start:prod": "nodemon --env-file=.env.production app.js"
"start:dev": "nodemon -r dotenv/config app.js dotenv_config_path=.env.development",
"start:prod": "nodemon -r dotenv/config app.js dotenv_config_path=.env.production"
},
"author": "",
"license": "ISC",
@ -19,11 +19,14 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-jwt": "^8.4.1",
"express-mysql-session": "^3.0.0",
"express-session": "^1.17.3",
"find-config": "^1.0.0",
"fs": "^0.0.1-security",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.6.3",
"nodemon": "^3.1.0",
"promisify": "^0.0.3",
"util": "^0.12.5"
}