inventory api, update item, redis for config, access key, admin config

preparations for more in depth studio supprot as well
This commit is contained in:
SushiDesigner 2023-09-11 22:16:22 -06:00
parent 8e3f903c25
commit 9df560f717
19 changed files with 311 additions and 44 deletions

View File

@ -4,4 +4,5 @@ logshook=
PROD=true
LOCALCERTIFICATEPATH=
LOCALREDISCONNECTION=
DB_PASSWORD=
DB_PASSWORD=
ACCESS_KEY=

View File

@ -22,3 +22,17 @@ easy enough
```
PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host pm2 start server.mjs
```
# Setting up Access keys
Open regedit go to Computer\HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node
Make a key called ROBLOX Corporation if it doesn't exist
Inside of that key make another key called Roblox if it doesn't exist
Finally inside that key made a string value called AccessKey for the value put the same value as the one from the env file thank you.
# Contribution
Anyone is welcome to contribute.

View File

@ -1,13 +0,0 @@
const mongoose = require('mongoose')
const ConfigSchema = new mongoose.Schema({
RegistrationEnabled: {type: Boolean, required: true},
MaintenanceEnabled: {type: Boolean, required: true},
KeysEnabled: {type: Boolean, required: true},
GamesEnabled: {type: Boolean, required: true}
},
{collection: 'config'}
)
const model = mongoose.model('ConfigSchema', ConfigSchema)
module.exports = model

11
model/configNew.mjs Normal file
View File

@ -0,0 +1,11 @@
import { Schema } from 'redis-om'
const configSchema = new Schema('config', {
RegistrationEnabled: { type: 'boolean' },
MaintenanceEnabled: { type: 'boolean' },
GamesEnabled: { type: 'boolean' },
KeysEnabled: { type: 'boolean' },
bannermessage: { type: 'string' },
})
export default configSchema

View File

@ -56,6 +56,13 @@ UserSchema.virtual('feed.userdata', {
justOne: true
})
UserSchema.virtual('inventory.itemdata', {
ref: 'CatalogSchema',
localField: 'inventory.ItemId',
foreignField: 'ItemId',
justOne: true
})
const model = mongoose.model('UserSchema', UserSchema)

6
mykey.pub Normal file
View File

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC04Nimx5hGYvQ54rZPWJ9qvvoP
SsBXt3PREKhramu1gXpv+W4Mh/vdzlTsNqmedZ2gaX0rd9smy3Kp2lgxKsuyX1gc
918k9L/PUzKvnfxy93RDwXdo6qJze+mdQlkDi9U5W4MAzcx6ann3YTHyiKHfs9Dq
F+kBJQiloPgcnk3HPQIDAQAB
-----END PUBLIC KEY-----

11
package-lock.json generated
View File

@ -33,6 +33,7 @@
"jsonwebtoken": "^9.0.0",
"mongo-sanitize": "^1.1.0",
"mongoose": "^6.5.2",
"mongoose-execution-time": "^1.0.2",
"mongoose-unique-validator": "^3.1.0",
"multer": "^1.4.5-lts.1",
"node-fetch": "^3.2.10",
@ -3524,6 +3525,11 @@
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mongoose-execution-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mongoose-execution-time/-/mongoose-execution-time-1.0.2.tgz",
"integrity": "sha512-ROCOxLNOQcXuNcEmpcYUj6oeLWuBWMZXOPW4duuCJwmm4b7hGuLEExwWiy/4TtMULkw/heCHHOvXjyPw2+g9iA=="
},
"node_modules/mongoose-unique-validator": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mongoose-unique-validator/-/mongoose-unique-validator-3.1.0.tgz",
@ -7683,6 +7689,11 @@
}
}
},
"mongoose-execution-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mongoose-execution-time/-/mongoose-execution-time-1.0.2.tgz",
"integrity": "sha512-ROCOxLNOQcXuNcEmpcYUj6oeLWuBWMZXOPW4duuCJwmm4b7hGuLEExwWiy/4TtMULkw/heCHHOvXjyPw2+g9iA=="
},
"mongoose-unique-validator": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mongoose-unique-validator/-/mongoose-unique-validator-3.1.0.tgz",

View File

@ -36,6 +36,7 @@
"jsonwebtoken": "^9.0.0",
"mongo-sanitize": "^1.1.0",
"mongoose": "^6.5.2",
"mongoose-execution-time": "^1.0.2",
"mongoose-unique-validator": "^3.1.0",
"multer": "^1.4.5-lts.1",
"node-fetch": "^3.2.10",

View File

@ -185,4 +185,28 @@ router.post("/queue", requireAuth,async (req, res) => {
return res.json({data: response, pages: Math.ceil(Math.max(responsecount/resultsPerPage, 1)), count: responsecount })
})
router.post("/config", requireAuth,async (req, res) => {
if (req.userdocument.admin == false) {
return res.redirect('/')
}
return res.json({data: {GamesEnabled: req.config.GamesEnabled, KeysEnabled: req.config.KeysEnabled, MaintenanceEnabled: req.config.MaintenanceEnabled, RegistrationEnabled: req.config.RegistrationEnabled, bannermessage: req.config.bannermessage} })
})
router.post("/config/update", requireAuth,async (req, res) => {
if (req.userdocument.admin == false) {
return res.redirect('/')
}
if (req.body.setting != "RegistrationEnabled" && req.body.setting != "MaintenanceEnabled" && req.body.setting != "GamesEnabled" && req.body.setting != "KeysEnabled"){
return res.json({data: {status: 'error', error: 'Malformed input!'}})
}
req.config[req.body.setting] = req.body.update
await req.configRepository.save(req.config)
return res.json({data: {GamesEnabled: req.config.GamesEnabled, KeysEnabled: req.config.KeysEnabled, MaintenanceEnabled: req.config.MaintenanceEnabled, RegistrationEnabled: req.config.RegistrationEnabled, bannermessage: req.config.bannermessage} })
})
module.exports = router

View File

@ -16,7 +16,7 @@ function selectKeys(obj, keysArray) {
}
router.get("/",requireAuth,async (req, res) => {
const filtered = selectKeys(req.userdocument,["username","coins","userid","admin","ugcpermission","moderation","colors","inventory","joindate","lastclaimofcurrency","membership","friendrequests","friends","badges","status","timesincelastrequest","avatartype","discordid","bio","recentlyplayed","css"])
const filtered = selectKeys(req.userdocument,["username","coins","userid","admin","ugcpermission","moderation","colors","joindate","lastclaimofcurrency","membership","friendrequests","friends","badges","status","timesincelastrequest","avatartype","discordid","bio","recentlyplayed","css"])
//console.log(filtered.recentlyplayedgames)
filtered._2faenabled = false
if (req.userdocument?.twofasecrets){

View File

@ -47,7 +47,7 @@ router.post("/", requireAuth,async (req, res) => {
coins: req.userdocument.coins - itemdoc.Price
},
$push: {
inventory: {Type: itemdoc.Type,ItemId: itemdoc.ItemId, ItemName: itemdoc.Name, Equipped: false}
inventory: {Type: itemdoc.Type,ItemId: itemdoc.ItemId, Equipped: false}
}
},
function(err, doc) {

View File

@ -4,13 +4,15 @@ const user = require('./../..//model/user.js')
const games = require("./../../model/games.js")
const RelativeTime = require("@yaireo/relative-time")
const relativeTime = new RelativeTime()
const bodyParser = require('body-parser')
router.use(bodyParser.json())
router.get("/:id",async (req, res) => {
var id = req.params.id;
if (isNaN(parseFloat(id)) === true){
return res.json({error: true})
}
const response = await user.findOne({userid: id}).lean()
const response = await user.findOne({userid: id}).lean().select("-inventory")
if (!response){
return res.json({error: true, message: "404"})
@ -33,7 +35,107 @@ router.get("/:id",async (req, res) => {
status = JSON.parse(response.status)
}
return res.json({error:false, userinfo: {joindate: response.joindate, joindateepoch:new Date(response._id.getTimestamp()).getTime(), lastonline: relativeTime.from(new Date(response.timesincelastrequest)), lastonlineepoch: response.timesincelastrequest, coins: response.coins, username: response.username,userid: response.userid,friends: response.friends, admin: response.admin, discordid: response.discordid, membership: response.membership, inventory: response.inventory, bio: response.bio, status,followers: response.followers?.length, css: response.css, aboutme: response.aboutme}})
return res.json({error:false, userinfo: {joindate: response.joindate, joindateepoch:new Date(response._id.getTimestamp()).getTime(), lastonline: relativeTime.from(new Date(response.timesincelastrequest)), lastonlineepoch: response.timesincelastrequest, coins: response.coins, username: response.username,userid: response.userid,friends: response.friends, admin: response.admin, discordid: response.discordid, membership: response.membership, bio: response.bio, status,followers: response.followers?.length, css: response.css, aboutme: response.aboutme}})
})
router.post("/:id/inventory",async (req, res) => {
var id = req.params.id;
const resultsPerPage = 5
let page = req.body.page ?? 0
if (page != 0){
page-=1
}
let filter = req.body.filter ?? "Shirts"
console.log(req.body)
filter = filter.charAt(0).toUpperCase() + filter.slice(1)
console.log(filter)
let onlywearing = req.body.onlywearing ?? false
if (isNaN(parseFloat(id)) === true){
return res.json({error: true})
}
console.log(onlywearing)
let response
if (onlywearing === true){
response = await user.aggregate([{
$match: {
userid: parseInt(id)
}
}, {
$project: {
inventory: 1
}
}, {
$unwind: {
path: '$inventory'
}
}, {
$match: {
'inventory.Equipped': true
}
}, {
$group: {
_id: '$_id',
inventory: {
$push: '$inventory'
}
}
}
])
}else{
response = await user.aggregate([{
$match: {
userid: parseInt(id)
}
}, {
$project: {
inventory: 1
}
}, {
$unwind: {
path: '$inventory'
}
}, {
$match: {
'inventory.Type': filter
}
}, {
$group: {
_id: '$_id',
inventory: {
$push: '$inventory'
}
}
},
{$project: {
inventory: {$slice: ['$inventory', parseFloat(page)*resultsPerPage, resultsPerPage]}
}}
])
}
await user.populate(response, {path: "inventory.itemdata", select: "Name"})
console.log(response?.[0]?.inventory?.length)
if (!response[0]?.inventory){
return res.json({"error": false, inventory: []})
}
if (onlywearing === true){
// we aren't gonna use pagination for equipped yet cause lazy and its only used on peoples profiles anyways
return res.json({error:false, inventory: response?.[0]?.inventory})
}
let responsecount
if (onlywearing === true){
responsecount = (await user.aggregate([{$match: {userid: parseInt(id)}}, {$project: {inventory: 1}}, {$unwind: {path: '$inventory'}}, {$match: {'inventory.Type': filter,'inventory.Equipped': onlywearing}}, {$count: 'inventory'}]))[0].inventory
}else{
responsecount = (await user.aggregate([{$match: {userid: parseInt(id)}}, {$project: {inventory: 1}}, {$unwind: {path: '$inventory'}}, {$match: {'inventory.Type': filter}}, {$count: 'inventory'}]))[0].inventory
}
console.log(responsecount)
//const responsecount = await user.aggregate([ { $match : { userid : id } },{$project: { count: { $size:"$inventory" }}}]) // alternative yea
if (!response){
return res.json({error: true, message: "404"})
}
return res.json({error:false, inventory: response?.[0]?.inventory, pages: Math.ceil(Math.max(responsecount/resultsPerPage, 1))})
})

View File

@ -6,6 +6,7 @@ var path = require('path');
const crypto = require('crypto');
require('dotenv').config()
const RCC_HOST = process.env.RCC_HOST
const ACCESS_KEY = process.env.ACCESS_KEY
const User = require('../model/user.js')
const catalog = require("../model/item")
const games = require('./../model/games.js')
@ -54,6 +55,8 @@ if (req.query.name){
console.log(ip)
var sanitizedid = req.query.id.match(rgx)
if (ip === RCC_HOST || ip === "::ffff:"+RCC_HOST){
console.log(req.headers["accesskey"])
if (req.headers?.["accesskey"] === ACCESS_KEY){
fs.access("./assets/ugc/gamefile-"+sanitizedid+".rbxl", fs.F_OK, (err) => {
if (err) {
@ -67,6 +70,8 @@ if (req.query.name){
return
})
}
}
return res.status(401).end()
}else{
if (!req.query.id){
req.query.id = req.query.assetversionid

View File

@ -4,6 +4,7 @@ const { requireAuth } = require('./../middleware/authmiddleware')
const User = require('./../model/item.js')
const bodyParser = require('body-parser')
router.use(bodyParser.json())
const xss = require("xss")
router.post("/fetch", async (req, res) => {
const resultsPerPage = 30
@ -12,6 +13,7 @@ router.post("/fetch", async (req, res) => {
page-=1
}
let {filter, sort} = req.body
let libraryassets = ["User Ad", "Gamepass", "Video"] // we don't want to include these in the catalog
//console.log(req.body)
try{
if (filter === "Best Selling"){
@ -20,8 +22,8 @@ router.post("/fetch", async (req, res) => {
responsecount = await User.countDocuments({Type: sort, Hidden: {$exists:false}})
}
if (sort === "All"){
response = await User.find({Hidden: {$exists:false}, Type: { $ne: "User Ad" } }).limit(resultsPerPage).skip(0+parseFloat(page)*resultsPerPage).sort({Sales: "descending"}).lean().select(['-_id'])
responsecount = await User.countDocuments({Hidden: {$exists:false}, Type: { $ne: "User Ad" }})
response = await User.find({Hidden: {$exists:false}, Type: { $nin: libraryassets } }).limit(resultsPerPage).skip(0+parseFloat(page)*resultsPerPage).sort({Sales: "descending"}).lean().select(['-_id'])
responsecount = await User.countDocuments({Hidden: {$exists:false}, Type: { $nin: libraryassets }})
}
}else{
if (sort != "All"){
@ -29,8 +31,8 @@ router.post("/fetch", async (req, res) => {
responsecount = await User.countDocuments({Type: sort, Hidden: {$exists:false}})
}
if (sort === "All"){
response = await User.find({Hidden: {$exists:false}, Type: { $ne: "User Ad" }}).limit(resultsPerPage).skip(0+parseFloat(page)*resultsPerPage).lean().select(['-_id'])
responsecount = await User.countDocuments({Hidden: {$exists:false}, Type: { $ne: "User Ad" }})
response = await User.find({Hidden: {$exists:false}, Type: { $nin: libraryassets }}).limit(resultsPerPage).skip(0+parseFloat(page)*resultsPerPage).lean().select(['-_id'])
responsecount = await User.countDocuments({Hidden: {$exists:false}, Type: { $nin: libraryassets }})
}
}
@ -46,7 +48,7 @@ router.post("/fetch", async (req, res) => {
router.get('/iteminfo/:id', async (req, res) => {
var id = req.params.id;
if (isNaN(parseFloat(id)) === true){
if (isNaN(parseInt(id)) === true){
return res.json({status: "error", error: "Must be number"})
}
const response = await User.findOne({ItemId: id}).lean()
@ -56,7 +58,69 @@ router.get('/iteminfo/:id', async (req, res) => {
}
return res.json({error: false, iteminfo: response})
});
})
router.post('/iteminfo/:id/configure',requireAuth, async (req, res) => {
var id = req.params.id;
let {name, description, price} = req.body
if (typeof name === "undefined" && typeof description === "undefined" && typeof price === "undefined"){
return res.json({status: "error", error: "Nothing to update"})
}
if (isNaN(parseInt(id)) === true){
return res.json({status: "error", error: "Must be number"})
}
const response = await User.findOne({ItemId: parseInt(id)})
if (!response){
return res.json({status: "error", error: "Not found"})
}
if (response.Creator !== req.userdocument.userid && req.userdocument.admin === false){
return res.status(401).json({status: "error", error: "Unauthorized!"})
}
let save = false
if (price && price != null){
if (isNaN(parseInt(price)) === true){
return res.json({status: "error", error: "Must be number"})
}
price = parseInt(price)
if (price < 5 && response.Type != "Gamepass"){
return res.json({status: 'error', error: 'Minimum price is 5 rocks.'})
}else if (price < 1 && response.Type === "Gamepass"){
return res.json({status: 'error', error: 'Minimum price is 1 rock.'})
}
response.Price = price
response.markModified('Price')
save = true
}
if (description && description != ""){
response.Description = xss(description)
response.markModified('Description')
save = true
}
if (name && name != ""){
response.Name = xss(name)
response.markModified('Name')
save = true
}
if (save === true){
await response.save()
}
console.log(name, description, price)
return res.json({status: "success", message: "Item updated!"})
})
router.post("/search", async (req, res) => {
const resultsPerPage = 30

View File

@ -166,6 +166,7 @@ router.get('/gameinfo/:id', async (req, res) => {
const response = await games.findOne({idofgame: id}).lean().select(['idofgame', 'version', 'nameofgame', 'numberofplayers', 'visits', 'useridofowner', 'players','descrption']).populate("owner", "username")
//console.log(response)
//console.log(response)
if (!response){
return res.json({status: "error", error: "Not found"})

14
routes/ide.js Normal file
View File

@ -0,0 +1,14 @@
const express = require("express")
const router = express.Router()
const { requireAuth } = require('./../middleware/authmiddleware')
const bodyParser = require('body-parser')
router.use(bodyParser.json())
router.get("/welcome", requireAuth,async (req, res) => {
res.send("test")
})
module.exports = router

View File

@ -68,7 +68,7 @@ router.post("/marketplace/purchase",requireAuth, async (req, res) => {
coins: req.userdocument.coins - itemdoc.Price
},
$push: {
inventory: {Type: itemdoc.Type,ItemId: itemdoc.ItemId, ItemName: itemdoc.Name, Equipped: false}
inventory: {Type: itemdoc.Type,ItemId: itemdoc.ItemId, Equipped: false}
}
},
function(err, doc) {
@ -211,7 +211,7 @@ router.post('/v1/purchases/products/:assetId',requireAuth,async (req, res) => {
coins: req.userdocument.coins - itemdoc.Price
},
$push: {
inventory: {Type: itemdoc.Type,ItemId: itemdoc.ItemId, ItemName: itemdoc.Name, Equipped: false}
inventory: {Type: itemdoc.Type,ItemId: itemdoc.ItemId, Equipped: false}
}
},
function(err, doc) {

View File

@ -16,7 +16,7 @@ var cookieParser = require('cookie-parser')
var session = require('express-session')
const helmet = require("helmet");
const mongoose = require('mongoose');
const config = require('./model/config.js')
import configNew from './model/configNew.mjs'
import ipWhitelist from './model/ipWhitelist.mjs'
const user = require('./model/user.js')
const model = require("./model/user.js")
@ -52,13 +52,15 @@ let redis
if (PROD === "true"){
redis = createClient()
}else{
redis = createClient({url: "redis://default:2BxaAV7Dcbt8d6QqNm58TdUfdIQtEY5q@redis-15195.c53.west-us.azure.cloud.redislabs.com:15195"})
const localRedisConnection = process.env.LOCALREDISCONNECTION
redis = createClient({url: localRedisConnection})
}
redis.on('error', (err) => console.log('Redis Client Error', err));
await redis.connect()
import { Repository } from 'redis-om'
const configRepository = new Repository(configNew, redis)
const ipWhiteListRepository = new Repository(ipWhitelist, redis)
@ -121,8 +123,11 @@ const JWT_SECRET = process.env.JWT_SECRET
const RCC_HOST = process.env.RCC_HOST
const DB_PASSWORD = process.env.DB_PASSWORD
console.log(RCC_HOST)
const { logExecutionTime } = require('mongoose-execution-time');
//mongoose.plugin(logExecutionTime);
if (PROD === "true"){
mongoose.connect('mongodb://localhost:27017/meteoritedb', {
mongoose.connect('mongodb://127.0.0.1:27017/meteoritedb', {
useNewUrlParser: true,
useUnifiedTopology: true,
authSource: "admin",
@ -130,26 +135,30 @@ if (PROD === "true"){
pass: DB_PASSWORD,
})
}else{
mongoose.connect('mongodb://localhost:27017/meteoritedb', {
mongoose.connect('mongodb://127.0.0.1:27017/meteoritedb', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
}
app.disable('x-powered-by') // we don't wanna tell potential attackers our exact framework yet lol
// automatically create a default document in mongodb for our config
// if the config document doesn't exist auto create one these are also the default settings your site will start with
// automatically create a default document in redisdb for our config
// if the redis document doesn't exist auto create one these are also the default settings your site will start with
async function createconfig(){
try {
var resp = await config.findOne()
if (!resp) {
const response = await config.create({
var resp = await redis.exists('config:ONE')
if (resp === 0){
// doesn't exist
await configRepository.save('ONE', {
RegistrationEnabled: true,
MaintenanceEnabled: false,
GamesEnabled: true,
KeysEnabled: false,
GamesEnabled: true
})
}
bannermessage: "",
})
}
} catch (err) {
throw(err)
}
@ -162,8 +171,9 @@ app.use(async function (req, res, next) {
return next()
}
res.header("Cache-Control", "no-store,no-cache,must-revalidate");
var resp = await config.findOne().lean()
var resp = await configRepository.fetch('ONE')
req.config = resp
req.configRepository = configRepository
//console.log(req.headers['x-forwarded-proto'])
if (!req.headers['x-forwarded-proto']){
@ -173,9 +183,9 @@ app.use(async function (req, res, next) {
req.headers['x-forwarded-proto'] = "http"
}
}
if (!req.headers['cf-connecting-ip']){ //localhost
/*if (!req.headers['cf-connecting-ip']){ //localhost
res.header("Access-Control-Allow-Origin", "*");
}
}*/
if (req.headers['x-forwarded-host'] === "www.mete0r.xyz" && req.headers['x-forwarded-host'] && req.headers?.["user-agent"] != "RobloxStudio/WinInet" && req.headers?.["user-agent"] != "Roblox/WinInet"){
if (req.method === "GET" && req.url.startsWith('/game/') === false && req.url.startsWith("/login/") === false){
return res.redirect(302, req.headers['x-forwarded-proto']+"://mete0r.xyz"+req.url)
@ -185,7 +195,7 @@ app.use(async function (req, res, next) {
//req.headers['x-forwarded-host'] = "mete0r.xyz"
//console.log(req.headers?.['cf-connecting-ip'])
//console.log(req.socket.remoteAddress)
//console.log(req.url)
console.log(req.url)
if (req.url === "/assets/2020.zip"){
return res.redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
}
@ -378,9 +388,9 @@ app.use('/api/requestad',requestAdRouter)
app.use('/api/bank',bankRouter)*/
/*const groupRouter = require('./routes/api/groups.js');
const groupRouter = require('./routes/api/groups.js');
app.use('/api/groups',groupRouter)*/
app.use('/api/groups',groupRouter)
const feedRouter = require('./routes/api/feed.js');
@ -390,6 +400,10 @@ const commentRouter = require('./routes/api/comment.js');
app.use('/api/comments',commentRouter)
const ideRouter = require('./routes/ide.js');
app.use(['/ide','//ide'],ideRouter)
/*
app.get("/My/Places", (req, res) => {

View File

@ -2109,6 +2109,11 @@
"@aws-sdk/credential-providers" "^3.186.0"
"saslprep" "^1.0.3"
"mongoose-execution-time@^1.0.2":
"integrity" "sha512-ROCOxLNOQcXuNcEmpcYUj6oeLWuBWMZXOPW4duuCJwmm4b7hGuLEExwWiy/4TtMULkw/heCHHOvXjyPw2+g9iA=="
"resolved" "https://registry.npmjs.org/mongoose-execution-time/-/mongoose-execution-time-1.0.2.tgz"
"version" "1.0.2"
"mongoose-unique-validator@^3.1.0":
"integrity" "sha512-UsBBlFapip8gc8x1h+nLWnkOy+GTy9Z+zmTyZ35icLV3EoLIVz180vJzepfMM9yBy2AJh+maeuoM8CWtqejGUg=="
"resolved" "https://registry.npmjs.org/mongoose-unique-validator/-/mongoose-unique-validator-3.1.0.tgz"