{"version":3,"sources":["meteor://💻app/packages/rate-limit/rate-limit.js"],"names":["module","export","RateLimiter","Meteor","link","v","Random","DEFAULT_INTERVAL_TIME_IN_MILLISECONDS","DEFAULT_REQUESTS_PER_INTERVAL","hasOwn","Object","prototype","hasOwnProperty","Rule","constructor","options","matchers","id","_matchers","_lastResetTime","Date","getTime","counters","match","input","entries","every","key","matcher","call","_generateKeyString","filter","reduce","returnString","apply","timeSinceLastReset","timeToNextReset","intervalTime","resetCounter","_executeCallback","reply","ruleInput","callback","e","console","error","rules","check","allowed","timeToReset","numInvocationsLeft","Infinity","matchedRules","_findAllMatchingRules","forEach","rule","ruleResult","numInvocations","numRequestsAllowed","addRule","bindEnvironment","newRule","increment","values","removeRule"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAAA,MAAM,CAACC,MAAP,CAAc;AAACC,aAAW,EAAC,MAAIA;AAAjB,CAAd;AAA6C,IAAIC,MAAJ;AAAWH,MAAM,CAACI,IAAP,CAAY,eAAZ,EAA4B;AAACD,QAAM,CAACE,CAAD,EAAG;AAACF,UAAM,GAACE,CAAP;AAAS;;AAApB,CAA5B,EAAkD,CAAlD;AAAqD,IAAIC,MAAJ;AAAWN,MAAM,CAACI,IAAP,CAAY,eAAZ,EAA4B;AAACE,QAAM,CAACD,CAAD,EAAG;AAACC,UAAM,GAACD,CAAP;AAAS;;AAApB,CAA5B,EAAkD,CAAlD;AAGxH;AACA,MAAME,qCAAqC,GAAG,IAA9C,C,CACA;;AACA,MAAMC,6BAA6B,GAAG,EAAtC;AAEA,MAAMC,MAAM,GAAGC,MAAM,CAACC,SAAP,CAAiBC,cAAhC,C,CAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACA,MAAMC,IAAN,CAAW;AACTC,aAAW,CAACC,OAAD,EAAUC,QAAV,EAAoB;AAC7B,SAAKC,EAAL,GAAUX,MAAM,CAACW,EAAP,EAAV;AAEA,SAAKF,OAAL,GAAeA,OAAf;AAEA,SAAKG,SAAL,GAAiBF,QAAjB;AAEA,SAAKG,cAAL,GAAsB,IAAIC,IAAJ,GAAWC,OAAX,EAAtB,CAP6B,CAS7B;;AACA,SAAKC,QAAL,GAAgB,EAAhB;AACD,GAZQ,CAaT;AACA;AACA;;;AACAC,OAAK,CAACC,KAAD,EAAQ;AACX,WAAOd,MAAM,CACVe,OADI,CACI,KAAKP,SADT,EAEJQ,KAFI,CAEE,CAAC,CAACC,GAAD,EAAMC,OAAN,CAAD,KAAoB;AACzB,UAAIA,OAAO,KAAK,IAAhB,EAAsB;AACpB,YAAI,CAACnB,MAAM,CAACoB,IAAP,CAAYL,KAAZ,EAAmBG,GAAnB,CAAL,EAA8B;AAC5B,iBAAO,KAAP;AACD,SAFD,MAEO,IAAI,OAAOC,OAAP,KAAmB,UAAvB,EAAmC;AACxC,cAAI,CAAEA,OAAO,CAACJ,KAAK,CAACG,GAAD,CAAN,CAAb,EAA4B;AAC1B,mBAAO,KAAP;AACD;AACF,SAJM,MAIA,IAAIC,OAAO,KAAKJ,KAAK,CAACG,GAAD,CAArB,EAA4B;AACjC,iBAAO,KAAP;AACD;AACF;;AACD,aAAO,IAAP;AACD,KAfI,CAAP;AAgBD,GAjCQ,CAmCT;AACA;AACA;;;AACAG,oBAAkB,CAACN,KAAD,EAAQ;AACxB,WAAOd,MAAM,CAACe,OAAP,CAAe,KAAKP,SAApB,EACJa,MADI,CACG,CAAC,CAACJ,GAAD,CAAD,KAAW,KAAKT,SAAL,CAAeS,GAAf,MAAwB,IADtC,EAEJK,MAFI,CAEG,CAACC,YAAD,EAAe,CAACN,GAAD,EAAMC,OAAN,CAAf,KAAkC;AACxC,UAAI,OAAOA,OAAP,KAAmB,UAAvB,EAAmC;AACjC,YAAIA,OAAO,CAACJ,KAAK,CAACG,GAAD,CAAN,CAAX,EAAyB;AACvBM,sBAAY,IAAIN,GAAG,GAAGH,KAAK,CAACG,GAAD,CAA3B;AACD;AACF,OAJD,MAIO;AACLM,oBAAY,IAAIN,GAAG,GAAGH,KAAK,CAACG,GAAD,CAA3B;AACD;;AACD,aAAOM,YAAP;AACD,KAXI,EAWF,EAXE,CAAP;AAYD,GAnDQ,CAqDT;AACA;;;AACAC,OAAK,CAACV,KAAD,EAAQ;AACX,UAAMG,GAAG,GAAG,KAAKG,kBAAL,CAAwBN,KAAxB,CAAZ;;AACA,UAAMW,kBAAkB,GAAG,IAAIf,IAAJ,GAAWC,OAAX,KAAuB,KAAKF,cAAvD;;AACA,UAAMiB,eAAe,GAAG,KAAKrB,OAAL,CAAasB,YAAb,GAA4BF,kBAApD;AACA,WAAO;AACLR,SADK;AAELQ,wBAFK;AAGLC;AAHK,KAAP;AAKD,GAhEQ,CAkET;AACA;AACA;;;AACAE,cAAY,GAAG;AACb;AACA,SAAKhB,QAAL,GAAgB,EAAhB;AACA,SAAKH,cAAL,GAAsB,IAAIC,IAAJ,GAAWC,OAAX,EAAtB;AACD;;AAEDkB,kBAAgB,CAACC,KAAD,EAAQC,SAAR,EAAmB;AACjC,QAAI;AACF,UAAI,KAAK1B,OAAL,CAAa2B,QAAjB,EAA2B;AACzB,aAAK3B,OAAL,CAAa2B,QAAb,CAAsBF,KAAtB,EAA6BC,SAA7B;AACD;AACF,KAJD,CAIE,OAAOE,CAAP,EAAU;AACV;AACAC,aAAO,CAACC,KAAR,CAAcF,CAAd;AACD;AACF;;AApFQ;;AAuFX,MAAMzC,WAAN,CAAkB;AAChB;AACAY,aAAW,GAAG;AACZ;AACA;AACA;AAEA,SAAKgC,KAAL,GAAa,EAAb;AACD;AAED;;;;;;;;;;;;;;;;AAcAC,OAAK,CAACvB,KAAD,EAAQ;AACX,UAAMgB,KAAK,GAAG;AACZQ,aAAO,EAAE,IADG;AAEZC,iBAAW,EAAE,CAFD;AAGZC,wBAAkB,EAAEC;AAHR,KAAd;;AAMA,UAAMC,YAAY,GAAG,KAAKC,qBAAL,CAA2B7B,KAA3B,CAArB;;AACA4B,gBAAY,CAACE,OAAb,CAAsBC,IAAD,IAAU;AAC7B,YAAMC,UAAU,GAAGD,IAAI,CAACrB,KAAL,CAAWV,KAAX,CAAnB;AACA,UAAIiC,cAAc,GAAGF,IAAI,CAACjC,QAAL,CAAckC,UAAU,CAAC7B,GAAzB,CAArB;;AAEA,UAAI6B,UAAU,CAACpB,eAAX,GAA6B,CAAjC,EAAoC;AAClC;AACAmB,YAAI,CAACjB,YAAL;AACAkB,kBAAU,CAACrB,kBAAX,GAAgC,IAAIf,IAAJ,GAAWC,OAAX,KAC9BkC,IAAI,CAACpC,cADP;AAEAqC,kBAAU,CAACpB,eAAX,GAA6BmB,IAAI,CAACxC,OAAL,CAAasB,YAA1C;AACAoB,sBAAc,GAAG,CAAjB;AACD;;AAED,UAAIA,cAAc,GAAGF,IAAI,CAACxC,OAAL,CAAa2C,kBAAlC,EAAsD;AACpD;AACA;AACA;AACA;AACA,YAAIlB,KAAK,CAACS,WAAN,GAAoBO,UAAU,CAACpB,eAAnC,EAAoD;AAClDI,eAAK,CAACS,WAAN,GAAoBO,UAAU,CAACpB,eAA/B;AACD;;AACDI,aAAK,CAACQ,OAAN,GAAgB,KAAhB;AACAR,aAAK,CAACU,kBAAN,GAA2B,CAA3B;;AACAK,YAAI,CAAChB,gBAAL,CAAsBC,KAAtB,EAA6BhB,KAA7B;AACD,OAXD,MAWO;AACL;AACA;AACA,YAAI+B,IAAI,CAACxC,OAAL,CAAa2C,kBAAb,GAAkCD,cAAlC,GACFjB,KAAK,CAACU,kBADJ,IAC0BV,KAAK,CAACQ,OADpC,EAC6C;AAC3CR,eAAK,CAACS,WAAN,GAAoBO,UAAU,CAACpB,eAA/B;AACAI,eAAK,CAACU,kBAAN,GAA2BK,IAAI,CAACxC,OAAL,CAAa2C,kBAAb,GACzBD,cADF;AAED;;AACDF,YAAI,CAAChB,gBAAL,CAAsBC,KAAtB,EAA6BhB,KAA7B;AACD;AACF,KAnCD;AAoCA,WAAOgB,KAAP;AACD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BAmB,SAAO,CAACJ,IAAD,EAAOG,kBAAP,EAA2BrB,YAA3B,EAAyCK,QAAzC,EAAmD;AACxD,UAAM3B,OAAO,GAAG;AACd2C,wBAAkB,EAAEA,kBAAkB,IAAIlD,6BAD5B;AAEd6B,kBAAY,EAAEA,YAAY,IAAI9B,qCAFhB;AAGdmC,cAAQ,EAAEA,QAAQ,IAAIvC,MAAM,CAACyD,eAAP,CAAuBlB,QAAvB;AAHR,KAAhB;AAMA,UAAMmB,OAAO,GAAG,IAAIhD,IAAJ,CAASE,OAAT,EAAkBwC,IAAlB,CAAhB;AACA,SAAKT,KAAL,CAAWe,OAAO,CAAC5C,EAAnB,IAAyB4C,OAAzB;AACA,WAAOA,OAAO,CAAC5C,EAAf;AACD;AAED;;;;;;;AAKA6C,WAAS,CAACtC,KAAD,EAAQ;AACf;AACA,UAAM4B,YAAY,GAAG,KAAKC,qBAAL,CAA2B7B,KAA3B,CAArB;;AACA4B,gBAAY,CAACE,OAAb,CAAsBC,IAAD,IAAU;AAC7B,YAAMC,UAAU,GAAGD,IAAI,CAACrB,KAAL,CAAWV,KAAX,CAAnB;;AAEA,UAAIgC,UAAU,CAACrB,kBAAX,GAAgCoB,IAAI,CAACxC,OAAL,CAAasB,YAAjD,EAA+D;AAC7D;AACAkB,YAAI,CAACjB,YAAL;AACD,OAN4B,CAQ7B;AACA;;;AACA,UAAI7B,MAAM,CAACoB,IAAP,CAAY0B,IAAI,CAACjC,QAAjB,EAA2BkC,UAAU,CAAC7B,GAAtC,CAAJ,EAAgD;AAC9C4B,YAAI,CAACjC,QAAL,CAAckC,UAAU,CAAC7B,GAAzB;AACD,OAFD,MAEO;AACL4B,YAAI,CAACjC,QAAL,CAAckC,UAAU,CAAC7B,GAAzB,IAAgC,CAAhC;AACD;AACF,KAfD;AAgBD,GArIe,CAuIhB;;;AACA0B,uBAAqB,CAAC7B,KAAD,EAAQ;AAC3B,WAAOd,MAAM,CAACqD,MAAP,CAAc,KAAKjB,KAAnB,EAA0Bf,MAA1B,CAAiCwB,IAAI,IAAIA,IAAI,CAAChC,KAAL,CAAWC,KAAX,CAAzC,CAAP;AACD;AAED;;;;;;;;AAMAwC,YAAU,CAAC/C,EAAD,EAAK;AACb,QAAI,KAAK6B,KAAL,CAAW7B,EAAX,CAAJ,EAAoB;AAClB,aAAO,KAAK6B,KAAL,CAAW7B,EAAX,CAAP;AACA,aAAO,IAAP;AACD;;AACD,WAAO,KAAP;AACD;;AAxJe,C","file":"/packages/rate-limit.js","sourcesContent":["import { Meteor } from 'meteor/meteor';\nimport { Random } from 'meteor/random';\n\n// Default time interval (in milliseconds) to reset rate limit counters\nconst DEFAULT_INTERVAL_TIME_IN_MILLISECONDS = 1000;\n// Default number of events allowed per time interval\nconst DEFAULT_REQUESTS_PER_INTERVAL = 10;\n\nconst hasOwn = Object.prototype.hasOwnProperty;\n\n// A rule is defined by an options object that contains two fields,\n// `numRequestsAllowed` which is the number of events allowed per interval, and\n// an `intervalTime` which is the amount of time in milliseconds before the\n// rate limit restarts its internal counters, and by a matchers object. A\n// matchers object is a POJO that contains a set of keys with values that\n// define the entire set of inputs that match for each key. The values can\n// either be null (optional), a primitive or a function that returns a boolean\n// of whether the provided input's value matches for this key.\n//\n// Rules are uniquely assigned an `id` and they store a dictionary of counters,\n// which are records used to keep track of inputs that match the rule. If a\n// counter reaches the `numRequestsAllowed` within a given `intervalTime`, a\n// rate limit is reached and future inputs that map to that counter will\n// result in errors being returned to the client.\nclass Rule {\n constructor(options, matchers) {\n this.id = Random.id();\n\n this.options = options;\n\n this._matchers = matchers;\n\n this._lastResetTime = new Date().getTime();\n\n // Dictionary of input keys to counters\n this.counters = {};\n }\n // Determine if this rule applies to the given input by comparing all\n // rule.matchers. If the match fails, search short circuits instead of\n // iterating through all matchers.\n match(input) {\n return Object\n .entries(this._matchers)\n .every(([key, matcher]) => {\n if (matcher !== null) {\n if (!hasOwn.call(input, key)) {\n return false;\n } else if (typeof matcher === 'function') {\n if (!(matcher(input[key]))) {\n return false;\n }\n } else if (matcher !== input[key]) {\n return false;\n }\n }\n return true;\n });\n }\n\n // Generates unique key string for provided input by concatenating all the\n // keys in the matcher with the corresponding values in the input.\n // Only called if rule matches input.\n _generateKeyString(input) {\n return Object.entries(this._matchers)\n .filter(([key]) => this._matchers[key] !== null)\n .reduce((returnString, [key, matcher]) => {\n if (typeof matcher === 'function') {\n if (matcher(input[key])) {\n returnString += key + input[key];\n }\n } else {\n returnString += key + input[key];\n }\n return returnString;\n }, '');\n }\n\n // Applies the provided input and returns the key string, time since counters\n // were last reset and time to next reset.\n apply(input) {\n const key = this._generateKeyString(input);\n const timeSinceLastReset = new Date().getTime() - this._lastResetTime;\n const timeToNextReset = this.options.intervalTime - timeSinceLastReset;\n return {\n key,\n timeSinceLastReset,\n timeToNextReset,\n };\n }\n\n // Reset counter dictionary for this specific rule. Called once the\n // timeSinceLastReset has exceeded the intervalTime. _lastResetTime is\n // set to be the current time in milliseconds.\n resetCounter() {\n // Delete the old counters dictionary to allow for garbage collection\n this.counters = {};\n this._lastResetTime = new Date().getTime();\n }\n\n _executeCallback(reply, ruleInput) {\n try {\n if (this.options.callback) {\n this.options.callback(reply, ruleInput);\n }\n } catch (e) {\n // Do not throw error here\n console.error(e);\n }\n }\n}\n\nclass RateLimiter {\n // Initialize rules to be an empty dictionary.\n constructor() {\n // Dictionary of all rules associated with this RateLimiter, keyed by their\n // id. Each rule object stores the rule pattern, number of events allowed,\n // last reset time and the rule reset interval in milliseconds.\n\n this.rules = {};\n }\n\n /**\n * Checks if this input has exceeded any rate limits.\n * @param {object} input dictionary containing key-value pairs of attributes\n * that match to rules\n * @return {object} Returns object of following structure\n * { 'allowed': boolean - is this input allowed\n * 'timeToReset': integer | Infinity - returns time until counters are reset\n * in milliseconds\n * 'numInvocationsLeft': integer | Infinity - returns number of calls left\n * before limit is reached\n * }\n * If multiple rules match, the least number of invocations left is returned.\n * If the rate limit has been reached, the longest timeToReset is returned.\n */\n check(input) {\n const reply = {\n allowed: true,\n timeToReset: 0,\n numInvocationsLeft: Infinity,\n };\n\n const matchedRules = this._findAllMatchingRules(input);\n matchedRules.forEach((rule) => {\n const ruleResult = rule.apply(input);\n let numInvocations = rule.counters[ruleResult.key];\n\n if (ruleResult.timeToNextReset < 0) {\n // Reset all the counters since the rule has reset\n rule.resetCounter();\n ruleResult.timeSinceLastReset = new Date().getTime() -\n rule._lastResetTime;\n ruleResult.timeToNextReset = rule.options.intervalTime;\n numInvocations = 0;\n }\n\n if (numInvocations > rule.options.numRequestsAllowed) {\n // Only update timeToReset if the new time would be longer than the\n // previously set time. This is to ensure that if this input triggers\n // multiple rules, we return the longest period of time until they can\n // successfully make another call\n if (reply.timeToReset < ruleResult.timeToNextReset) {\n reply.timeToReset = ruleResult.timeToNextReset;\n }\n reply.allowed = false;\n reply.numInvocationsLeft = 0;\n rule._executeCallback(reply, input);\n } else {\n // If this is an allowed attempt and we haven't failed on any of the\n // other rules that match, update the reply field.\n if (rule.options.numRequestsAllowed - numInvocations <\n reply.numInvocationsLeft && reply.allowed) {\n reply.timeToReset = ruleResult.timeToNextReset;\n reply.numInvocationsLeft = rule.options.numRequestsAllowed -\n numInvocations;\n }\n rule._executeCallback(reply, input);\n }\n });\n return reply;\n }\n\n /**\n * Adds a rule to dictionary of rules that are checked against on every call.\n * Only inputs that pass all of the rules will be allowed. Returns unique rule\n * id that can be passed to `removeRule`.\n * @param {object} rule Input dictionary defining certain attributes and\n * rules associated with them.\n * Each attribute's value can either be a value, a function or null. All\n * functions must return a boolean of whether the input is matched by that\n * attribute's rule or not\n * @param {integer} numRequestsAllowed Optional. Number of events allowed per\n * interval. Default = 10.\n * @param {integer} intervalTime Optional. Number of milliseconds before\n * rule's counters are reset. Default = 1000.\n * @param {function} callback Optional. Function to be called after a\n * rule is executed. Two objects will be passed to this function.\n * The first one is the result of RateLimiter.prototype.check\n * The second is the input object of the rule, it has the following structure:\n * {\n * 'type': string - either 'method' or 'subscription'\n * 'name': string - the name of the method or subscription being called\n * 'userId': string - the user ID attempting the method or subscription\n * 'connectionId': string - a string representing the user's DDP connection\n * 'clientAddress': string - the IP address of the user\n * }\n * @return {string} Returns unique rule id\n */\n addRule(rule, numRequestsAllowed, intervalTime, callback) {\n const options = {\n numRequestsAllowed: numRequestsAllowed || DEFAULT_REQUESTS_PER_INTERVAL,\n intervalTime: intervalTime || DEFAULT_INTERVAL_TIME_IN_MILLISECONDS,\n callback: callback && Meteor.bindEnvironment(callback),\n };\n\n const newRule = new Rule(options, rule);\n this.rules[newRule.id] = newRule;\n return newRule.id;\n }\n\n /**\n * Increment counters in every rule that match to this input\n * @param {object} input Dictionary object containing attributes that may\n * match to rules\n */\n increment(input) {\n // Only increment rule counters that match this input\n const matchedRules = this._findAllMatchingRules(input);\n matchedRules.forEach((rule) => {\n const ruleResult = rule.apply(input);\n\n if (ruleResult.timeSinceLastReset > rule.options.intervalTime) {\n // Reset all the counters since the rule has reset\n rule.resetCounter();\n }\n\n // Check whether the key exists, incrementing it if so or otherwise\n // adding the key and setting its value to 1\n if (hasOwn.call(rule.counters, ruleResult.key)) {\n rule.counters[ruleResult.key]++;\n } else {\n rule.counters[ruleResult.key] = 1;\n }\n });\n }\n\n // Returns an array of all rules that apply to provided input\n _findAllMatchingRules(input) {\n return Object.values(this.rules).filter(rule => rule.match(input));\n }\n\n /**\n * Provides a mechanism to remove rules from the rate limiter. Returns boolean\n * about success.\n * @param {string} id Rule id returned from #addRule\n * @return {boolean} Returns true if rule was found and deleted, else false.\n */\n removeRule(id) {\n if (this.rules[id]) {\n delete this.rules[id];\n return true;\n }\n return false;\n }\n}\n\nexport { RateLimiter };\n"]}