Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
kindred-api
Advanced tools
Kindred is a thin Node.js wrapper on top of Riot Games API for League of Legends
Kindred is a Node.js wrapper with built-in rate-limiting (enforced per region), caching (in-memory and Redis), and parameter checking on top of Riot's League of Legends API.
My goal is to make a wrapper that is simple, sensible, and consistent. This project is heavily inspired by psuedonym117's Python wrapper. Look at the Quickstart Section to see what I mean.
yarn add kindred-api
// or npm install kindred-api
All examples here should be able to be run alone individually.
Make sure to check the official Riot Documentation to see what query parameters you can pass in to each endpoint (through the options parameter)!
Note: All region
parameters are OPTIONAL. All options
parameters are OPTIONAL unless stated otherwise.
k.ChampionMastery.all({ accId: 47776491 }, KindredAPI.print)
k.ChampionMastery.all({ id: 20026563 }, KindredAPI.print)
k.ChampionMastery.all({ id: 20026563 }).then(data => console.log(data))
k.ChampionMastery.get({ playerId: 20026563, championId: 203 }, KindredAPI.print)
k.ChampionMastery.get({ playerId: 20026563, championId: 203 }).then(data => console.log(data))
k.ChampionMastery.score({ id: 20026563 }, KindredAPI.print)
k.Champion.all({ region: REGIONS.KOREA }, KindredAPI.print)
k.Champion.get({ championId: 67 }, KindredAPI.print)
k.Champion.get({ championId: 67 }).then(data => console.log(data))
k.Champion.get({ championId: 67, region: 'kr' }, KindredAPI.print)
k.Game.get({ summonerId: 20026563 }, KindredAPI.print)
k.Game.recent({ id: 20026563 }, KindredAPI.print)
k.League.challengers(KindredAPI.print)
k.League.challengers({ region: 'na' }, KindredAPI.print)
k.League.challengers({ queue:'RANKED_FLEX_5x5' }, KindredAPI.print)
k.League.getLeagues({ summonerId: 20026563 }, KindredAPI.print)
k.League.get({ summonerId: 20026563 }, KindredAPI.print)
k.League.masters(KindredAPI.print)
k.League.masters({ region: 'na' }, KindredAPI.print)
k.League.masters({ queue:'RANKED_FLEX_5x5' }, KindredAPI.print)
k.League.positions({ summonerId: 20026563 }, KindredAPI.print)
k.Status.get().then(data => console.log(data))
k.Masteries.get({ id: 20026563 }, KindredAPI.print)
k.Masteries.by.account(47776491, REGIONS.NORTH_AMERICA, KindredAPI.print)
k.Masteries.by.id(32932398).then(data => console.log(data))
k.Masteries.by.name('Contractz', KindredAPI.print)
Note that this section has two different namespaces (Match and Matchlist).
id
parameter still refers to summonerId (not accountId).
k.Match.get({ id: 2482174957 }, KindredAPI.print)
k.Matchlist.get({ accId: 47776491 }, KindredAPI.print)
k.Matchlist.get({ id: 32932398 }, KindredAPI.print)
k.Matchlist.get({ name: 'Contractz' }, KindredAPI.print)
k.Matchlist.recent({ accId: 47776491 }, KindredAPI.print)
k.Matchlist.recent({ id: 32932398 }, KindredAPI.print)
// by summonerIdk.Matchlist.recent({ name: 'Contractz' }, KindredAPI.print)
k.Match.timeline({ id: 2478544123 }, KindredAPI.print)
k.Runes.get({ id: 20026563 }, KindredAPI.print)
k.Runes.get({ name: 'Contractz' }, KindredAPI.print)
k.Runes.get({ accId: 47776491 }, KindredAPI.print)
k.CurrentGame.get({ name: 'Contractz' }, KindredAPI.print)
k.CurrentGame.get({ id: 32932398 }, KindredAPI.print)
k.FeaturedGames.get().then(data => console.log(data))
k.FeaturedGames.get({ region: 'na' }, KindredAPI.print)
k.Static.champions(KindredAPI.print)
k.Static.champions({ options: { champData: 'all' } }).then(data => console.log(data))
k.Static.champion({ id: 131 }, KindredAPI.print)
k.Static.champion({ id: 131, options: { champData: 'enemytips', version: '7.7.1' } }, KindredAPI.print)
k.Static.items({ options: { itemListData: all } }, KindredAPI.print)
k.Static.item({ id: 3901, options: { itemData: ['image', 'gold'] } }, KindredAPI.print)
k.Static.items(KindredAPI.print)
k.Static.languageStrings(KindredAPI.print)
k.Static.languages().then(data => console.log(data)).catch(err => console.error(err))
k.Static.mapData().then(data => console.log(data))
k.Static.masteries({ options: { masteryListData: 'image' } }, KindredAPI.print)
k.Static.masteries(KindredAPI.print)
k.Static.mastery({ id: 6361 }, KindredAPI.print)
k.Static.mastery({ id: 6361, options: { masteryData: ['image', 'masteryTree'] } }, KindredAPI.print)
k.Static.mastery({ id: 6361, options: { masteryData: 'image' } }, KindredAPI.print)
k.Static.profileIcons(KindredAPI.print)
k.Static.realmData().then(data => console.log(data))
k.Static.runes().then(data => console.log(data))
k.Static.runes({ options: { runeListData: 'basic' } }, KindredAPI.print)
k.Static.rune({ id: 10002 }, KindredAPI.print)
k.Static.rune({ id: 10001, options: { runeData: 'image' } }, KindredAPI.print)
k.Static.spells(KindredAPI.print)
k.Static.spells({ options: { spellData: 'cost', dataById: true } }, KindredAPI.print)
k.Static.spell({ id: 31 }, KindredAPI.print)
k.Static.spell({ id: 31, options: { spellData: 'cooldown' } }, KindredAPI.print)
k.Static.versions(rprint)
k.Stats.ranked({ id: 20026563 }, KindredAPI.print)
k.Stats.ranked({ id: 20026563, options: { season: 'SEASON2016' } }, KindredAPI.print)
k.Stats.summary({ id: 20026563 }, KindredAPI.print)
k.Summoner.get({ accountId: 123123 }, KindredAPI.print)
k.Summoner.by.account(47776491, KindredAPI.print)
k.Summoner.by.account(47776491, REGIONS.NORTH_AMERICA, KindredAPI.print)
k.Summoner.get({ name: 'Contractz' }, KindredAPI.print)
k.Summoner.by.name('Contractz', KindredAPI.print)
k.Summoner.by.name('Contractz', REGIONS.NORTH_AMERICA, KindredAPI.print)
k.Summoner.by.name('Contractz').then(data => console.log(data))
k.Summoner.get({ id: 20026563 }, KindredAPI.print)
k.Summoner.by.id(32932398, REGIONS.NORTH_AMERICA, KindredAPI.print)
k.Tournament.DTO.by.code('123123')
k.Tournament.LobbyListEvents.by.code('123123')
Debug on, dev key rate limiting per region, in-memory cache with default settings on for quick scripts
var KindredAPI = require('kindred-api')
var REGIONS = KindredAPI.REGIONS
var QUEUES = KindredAPI.QUEUE_TYPES
var debug = true
var k = KindredAPI.QuickStart('YOUR_KEY', REGIONS.NORTH_AMERICA, debug)
/* Summoners! */
k.Summoner.get({ id: 32932398 }, KindredAPI.print)
k.Summoner.get({ name: 'Contractz' }, KindredAPI.print)
k.Summoner.by.id(32932398, KindredAPI.print)
k.Summoner.by.name('Contractz', REGIONS.NORTH_AMERICA, KindredAPI.print)
/* How to pass in options 101. */
var name = 'caaaaaaaaaria'
var region = REGIONS.NORTH_AMERICA
var options = {
queue: [QUEUES.TEAM_BUILDER_RANKED_SOLO, QUEUES.RANKED_FLEX_SR], // no need for joins or messy strings
// you can pass in arrays into any options params; array values will always be joined into a string
champion: 79
// option params should be spelled and capitalized the same as it is in Riot's docs!
// for example, Matchlist query params in Riot's docs include `champion`, `beginIndex`, `beginTime`, `season`
}
k.Summoner
.get({ name, region })
.then(data => k.Matchlist.get(
{ accId: data.accountId, options }
)
)
.then(data => console.log(data))
.catch(err => console.error(err))
/*
Instead of chaining requests like in the above, you can simply call
k.Matchlist.get with the `name` param or the `id` (summonerId) param.
Any function that targets just Ids or accountIds can use all three
different type of params (summonerId, accountId, name).
*/
k.Matchlist
.get({ name, options })
.then(data => console.log(data))
.catch(err => console.error(err))
var accId = 47776491
var id = 32932398 // summonerId
k.Matchlist.get({ name }, KindredAPI.print)
k.Matchlist.get({ accId }, KindredAPI.print)
k.Matchlist.get({ id }, KindredAPI.print)
/* Up to preference. */
k.Runes.get({ name }, KindredAPI.print)
k.Summoner.runes({ name }, KindredAPI.print)
k.Matchlist.get({ name }, KindredAPI.print) // full matchlist
k.Summoner.matchlist({ name }, KindredAPI.print)
k.Matchlist.recent({ name }, KindredAPI.print)
k.Summoner.matchHistory({ name }, KindredAPI.print) // recent matches (20)
var KindredAPI = require('kindred-api')
// var RIOT_API_KEY = require('whatever')
// or if you're using something like dotenv..
require('dotenv').config()
var RIOT_API_KEY = process.env.RIOT_API_KEY
var REGIONS = KindredAPI.REGIONS
var LIMITS = KindredAPI.LIMITS
var CACHE_TYPES = KindredAPI.CACHE_TYPES
/*
Default region for every method call is NA,
but you can set it during initialization as shown below.
You can also change it with 'setRegion(region)' as well.
To NOT use the built-in rate limiter, do NOT pass in anything
into limits. Same if you don't want to use the cache (cacheOptions).
*/
var k = new KindredAPI.Kindred({
key: RIOT_API_KEY,
defaultRegion: REGIONS.NORTH_AMERICA,
debug: true, // shows status code, urls, and relevant headers
limits: [ [10, 10], [500, 600] ], // user key
// 10 requests per 10 seconds, 500 requests per 10 minutes
// You can just pass in LIMITS.DEV, LIMITS.PROD, 'dev', or 'prod' instead though.
cacheOptions: CACHE_TYPES[0] // in memory
})
console.log(CACHE_TYPES)
// ['in-memory-cache', 'redis']
var rprint = KindredAPI.print
/*
The important thing about this wrapper is that it does not
take in parameters the usual way. Instead, the only parameter,
excluding the callback parameter, is an object of parameters.
*/
k.Summoner.get({ id: 354959 }, rprint)
k.Summoner.get({ id: 354959 }).then(data => console.log(data))
k.Match.get({ id: 2459973154, options: {
includeTimeline: false // of course, option params must be the same as the ones in Riot Docs
}}, rprint)
k.League.challengers({ region: 'na', queue: 'RANKED_FLEX_SR' }, rprint)
/*
All functions essentially have the following form:
functionName({ arg1, arg2...argN, options: {} }, optionalCallback) -> promise
If a method does not have the `options` parameter within my code, that simply means
there are no possible query parameters that you can pass in to that method.
*/
/*
Making any form of parameter error will inform you
what parameters you can pass in so you hopefully
don't have to refer to the documentation as much.
*/
k.Summoner.get(rprint)
// getSummoner request FAILED; required params `id` (int) or `name` (string) not passed in
k.ChampionMastery.get(rprint)
// getChampMastery request FAILED; required params `playerId` (int) AND `championId` (int) not passed in
/*
Notice the OR and the AND!!
Note: getChampMastery is the only method that can't take in an 'id' parameter,
because it requires both a 'playerId' and a 'championId'!
*/
/*
Let me reiterate: the first parameter of all endpoint methods will ALWAYS be an object.
However, when the parameters are satisfied by default parameters and/or
only have optional parameters, you can simply pass your callback in.
*/
k.League.challengers(rprint) // default region, default solo queue mode, valid
k.Static.runes(rprint) // only optional arguments & not passing in any optional arguments, valid
/*
getSummoners & getSummoner target both the by-name and by-id endpoints.
In the case of the summoner endpoints, it made a lot more sense for the two
functions to target both the by-name and by-id summoner endpoints.
*/
k.Summoner.get({ name: 'Contractz' }, rprint)
k.Summoner.get({ id: 354959 }, rprint)
/*
There are aliases for the `id` param.
For example, for summoners, you have summonerId and playerId.
*/
k.Summoner.get({ summonerId: 354959 }, rprint)
k.Summoner
.get({ summonerId: 354959 })
.then(json => console.log(json))
.catch(err => console.error(err))
k.Match.get({ id: 2459973154 }, rprint)
k.Match
.get({ matchId: 2459973154 })
.then(data => console.log(data))
.catch(err => console.error(err))
/* Every method has an optional 'region' parameter. */
var params = { name: 'sktt1peanut', region: REGIONS.KOREA }
k.Summoner.get(params, rprint) // peanut's data
/* Changing the default region! */
k.setRegion(REGIONS.KOREA)
/* Note that you can use spaces in the name. */
var fakerIgn = { name: 'hide on bush' }
var fakerId
k.Summoner.get(fakerIgn, function (err, data) {
fakerId = data.id
console.log('fakerId:', fakerId)
})
/*
Note that the player runes endpoint only accepts
a comma-separated list of integers.
*/
k.setRegion(REGIONS.NORTH_AMERICA)
k.Runes.get({ id: 354959 }, rprint)
k.Runes
.get({ id: 354959 })
.then(json => console.log(json))
.catch(err => console.error(err))
/*
But what if you want to quickly get the rune page of
some random summoner given their name?
You'd chain it like in many other clients:
Get the id from the name, get the runes from the id.
*/
var name = 'Richelle'
k.Summoner.get({ name }, function (err, data) {
if (data) k.Runes.get({ id: data.id }, rprint)
else console.error(err)
})
// or with promises
k.Summoner
.get({ name })
.then(data => k.Runes.get({ id: data.accountId }))
.then(data => console.log(data))
.catch(err => console.error(err))
/* I find that inconvenient, and so I just chain it for you in my code. */
// all methods that target endpoints that only accept ids
k.Runes.get({ name: 'Richelle' }, rprint)
k.Game.get({ name: 'Richelle' }, rprint)
k.League.get({ name: '5tunt' }, rprint)
k.CurrentGame.get({ name: 'Fràe', region: REGIONS.OCEANIA }, rprint)
k.League.get({ name: '5tunt' })
.then(data => console.log(data))
var name = 'Grigne'
k.Runes.get({ name })
.then(data => console.log(data))
k.Masteries.get({ name })
.then(data => console.log(data))
/*
Functions will have an options parameter that you can pass in query
strings when applicable. Values of options should match the
endpoint's 'Query Parameters'. Check the methods to see which methods
you can pass in options to.
Some are required, and some are not. I often take care of the ones
that are required by using the most sensible defaults.
For example, the required parameter for many methods is 'type' (of queue).
I made it so that the default is 'RANKED_SOLO_5x5' (or 'TEAM_BUILDER_RANKED_SOLO')
if 'type' is not passed in.
*/
k.League.challengers({ region: REGIONS.NORTH_AMERICA }, rprint) // get challengers from ranked solo queue ladder
k.League.challengers({ region: REGIONS.NORTH_AMERICA, queue: 'RANKED_FLEX_SR' }, rprint) // get challengers from ranked flex ladder
k.Match.get({ id: 2459973154 }, rprint) // includes timeline by default
k.Match.get({ id: 2459973154, options: { includeTimeline: false } }, rprint)
/*
However, for getMatchlist, the endpoint uses an optional
'queue' instead of 'type' to allow multiple options.
I set the default in this case to TEAM_BUILDER_RANKED_SOLO.
*/
var name = 'Contractz'
var region = REGIONS.NORTH_AMERICA
k.Matchlist.get({ name, region, options: {
/*
According to Riot API, query parameters that can accept multiple values
must be a comma separated list (or a single value), which is why one can do the below join.
However, both these options are inconvenient, and so I check if you pass in array values
for every option parameter, and manually join it for you. You can still pass in string values
if you want though.
Note, for arrays of values that are conceptually integers,
both strings and integers work because they're joined together as a string anyways.
*/
// queue: [QUEUES.RANKED_SOLO_5x5, QUEUES.RANKED_FLEX_SR].join(','), STILL VALId
// champion: '67' // '267,67' or ['267', '67'].join(',') STILL VALId
queue: [QUEUES.TEAM_BUILDER_RANKED_SOLO, QUEUES.RANKED_FLEX_SR], // valid
champion: [236, 432, 81, '432', 7], // valid
season: 6
} }, rprint)
/* The above example with promises. */
var options = {
queue: [QUEUES.TEAM_BUILDER_RANKED_SOLO, QUEUES.RANKED_FLEX_SR],
champion: [236, 432, 81, '432', 7],
season: 6
}
k.Matchlist
.get({ name, region, options })
.then(data => console.log(data))
.catch(err => console.error(err))
var furyMasteryId = 6111
k.Static.mastery({ id: furyMasteryId }, rprint)
var msRuneId = 10002
k.Static.rune({ id: msRuneId }, rprint)
So basically, I'm using psuedonym117's Python wrapper's rate limiter class and modified it a bunch to make it work. I'm simply doing a primitive rate-limiting-by-timestamps approach, nothing fancy.
You can test out the rate limiter (and see that it supports simultaneous requests to multiple regions) with the following code:
var num = 45 // # of requests
function count(err, data) {
if (data) --num
if (err) console.error(err)
if (num == 0) console.timeEnd('api')
}
console.time('api')
for (var i = 0; i < 15; ++i) {
k.Champion.all({ region: 'na' }, count)
k.Champion.all({ region: 'kr' }, count)
k.Champion.all({ region: 'euw' }, count)
}
This should output something like api: 20779.116ms
.
To test that it works with retry headers, just run the program while sending a few requests from your browser to intentionally rate limit yourself.
Because of these lines, if (data) --num
and if (num == 0) console.timeEnd('api')
, you can tell if all your requests went through.
April 2 I have added caching support. Right now, the library supports in-memory caching as well as caching with Redis. These are the default timers that made sense to me.
const endpointCacheTimers = {
// defaults
CHAMPION: cacheTimers.MONTH,
CHAMPION_MASTERY: cacheTimers.SIX_HOURS,
CURRENT_GAME: cacheTimers.NONE,
FEATURED_GAMES: cacheTimers.NONE,
GAME: cacheTimers.HOUR,
LEAGUE: cacheTimers.SIX_HOURS,
STATIC: cacheTimers.MONTH,
STATUS: cacheTimers.NONE,
MATCH: cacheTimers.MONTH,
MATCH_LIST: cacheTimers.ONE_HOUR,
RUNES_MASTERIES: cacheTimers.WEEK,
STATS: cacheTimers.HOUR,
SUMMONER: cacheTimers.DAY
}
If you pass in cacheOptions, but not how long you want each type of request to be cached (cacheTTL object), then by default you'll use the above timers.
To pass in your own custom timers, initialize Kindred like this:
import TIME_CONSTANTS from KindredAPI.TIME_CONSTANTS // for convenience, has a bunch of set timers in seconds
var k = new KindredAPI.Kindred({
key: RIOT_API_KEY,
defaultRegion: REGIONS.NORTH_AMERICA,
debug: true, // you can see if you're retrieving from cache with lack of requests showing
limits: [ [10, 10], [500, 600] ],
cacheOptions: CACHE_TYPES[0], // in-memory
cacheTTL: {
// All values in SECONDS.
CHAMPION: whatever,
CHAMPION_MASTERY: whatever,
CURRENT_GAME: whatever,
FEATURED_GAMES: whatever,
GAME: whatever,
LEAGUE: whatever,
STATIC: TIME_CONSTANTS.MONTH,
STATUS: whatever,
MATCH: whatever,
MATCH_LIST: whatever,
RUNES_MASTERIES: whatever,
STATS: whatever,
SUMMONER: TIME_CONSTANTS.DAY
}
})
Some people might disagree with how I formed my functions.
It's actually not really idiomatic JavaScript, and with an object inside an object it gets ugly really fast.
The benefits of this approach is that it's implementing Python's named parameters in a way, which was my original goal with this project.
However, the problem is that some of the functions could be simplified a lot as they only have one parameter and no options such as grabbing a summoner by their summoner Id. You would want something like:
getSummonerById(id, region, cb)
I decided to make the first parameter always an object for my main library methods though because it made my functions very consistent with each other, and so I wouldn't have to look at the method documentation as much.
It was very easy to switch between functions and have the call still be successful with the same parameters.
You would simply have to define new functions within the class that return calls to my methods. I have a few examples within my code.
this.Ex = {
getSummonerByAccId: this.getSummonerByAccId.bind(this),
getMatchlistByName: this.getMatchlistByName.bind(this),
getRunesBySummonerId: this.getRunesBySummonerId.bind(this),
getRunesByAccountId: this.getRunesByAccountId.bind(this)
staticRuneList: this.staticRuneList.bind(this)
}
getSummonerByAccId(accId, region, cb) {
return this.Summoner.get({
region,
accId
}, cb)
}
getMatchlistByName(name, region, options, cb) {
return this.Matchlist.get({
region,
name,
options
}, cb)
}
getRunesBySummonerId(id, region, cb) {
return this.Runes.get({
region,
id
}, cb)
}
getRunesByAccountId(accId, region, cb) {
return this.Runes.get({
region,
accId
}, cb)
}
staticRuneList(region, options, cb) {
return this.Static.runes({
region, options
}, cb)
}
It could still be kinda funky, but now you can call the functions like this:
k.Ex
.getMatchlistByName('Contractz')
.then(data => console.log(data))
.catch(err => console.error(err))
k.Ex
.getRunesByAccountId(47776491)
.then(data => console.log(data))
.catch(err => console.error(err))
k.Ex
.getSummonerByAccId(47776491)
.then(data => console.log(data))
.catch(err => console.error(err))
k.Ex.getRunesByAccountId(47776491, 'na', KindredAPI.print)
k.Ex.getRunesBySummonerId(32932398, 'na', KindredAPI.print)
k.Ex.staticRuneList('na', {}, KindredAPI.print)
k.Ex.staticRuneList('na').then(data => console.log(data))
k.Ex
.getMatchlistByName('Contractz', 'na', {
season: 3, queue: [41, 42]
})
.then(data => console.log(data))
.catch(err => console.error(err))
k.Ex.staticRuneList('na', { runeListData: 'all' }, KindredAPI.print)
You can decide on how you want to namespace everything though.
Right now, the code is also quite messy and there is a lot of repeated code. Function definitions are quite long because I include many aliases as well. I haven't thought of an elegant way to make a magic function that manages to work for every single endpoint request yet.
Any help and/or advice is appreciated!
FAQs
Node.js League of Legends v3 API wrapper with built-in rate-limiting (enforced per region, burst/spread, follows retry headers, app/method rate-limiting), caching (in-memory, Redis), automatic retries, and parameter checking.
The npm package kindred-api receives a total of 51 weekly downloads. As such, kindred-api popularity was classified as not popular.
We found that kindred-api demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.