@metamask-previews/rate-limit-controller 3.0.0-preview.3dbf14f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [3.0.0]
10
+ ### Changed
11
+ - **BREAKING:** Allow `RateLimitController` to define a rate-limit per method ([#1355](https://github.com/MetaMask/core/pull/1355))
12
+ - The constructor `implementations` option now maps API names to objects with a `method` property, rather than mapping to a function. This object may also have `rateLimitCount` and `rateLimitTimeout` properties, allowing custom rate limits for that method.
13
+ - **BREAKING:** Bump to Node 16 ([#1262](https://github.com/MetaMask/core/pull/1262))
14
+
15
+ ## [2.0.1]
16
+ ### Changed
17
+ - deps: eth-rpc-errors@4.0.0->4.0.2 ([#1215](https://github.com/MetaMask/core/pull/1215))
18
+
19
+ ## [2.0.0]
20
+ ### Removed
21
+ - **BREAKING:** Remove `isomorphic-fetch` ([#1106](https://github.com/MetaMask/controllers/pull/1106))
22
+ - Consumers must now import `isomorphic-fetch` or another polyfill themselves if they are running in an environment without `fetch`
23
+
24
+ ## [1.0.2]
25
+ ### Changed
26
+ - Rename this repository to `core` ([#1031](https://github.com/MetaMask/controllers/pull/1031))
27
+ - Update `@metamask/controller-utils` package ([#1041](https://github.com/MetaMask/controllers/pull/1041))
28
+
29
+ ## [1.0.1]
30
+ ### Changed
31
+ - Relax dependency on `@metamask/base-controller` (use `^` instead of `~`) ([#998](https://github.com/MetaMask/core/pull/998))
32
+
33
+ ## [1.0.0]
34
+ ### Added
35
+ - Initial release
36
+ - As a result of converting our shared controllers repo into a monorepo ([#831](https://github.com/MetaMask/core/pull/831)), we've created this package from select parts of [`@metamask/controllers` v33.0.0](https://github.com/MetaMask/core/tree/v33.0.0), namely:
37
+ - Everything in `src/ratelimit`
38
+
39
+ All changes listed after this point were applied to this package following the monorepo conversion.
40
+
41
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/rate-limit-controller@3.0.0...HEAD
42
+ [3.0.0]: https://github.com/MetaMask/core/compare/@metamask/rate-limit-controller@2.0.1...@metamask/rate-limit-controller@3.0.0
43
+ [2.0.1]: https://github.com/MetaMask/core/compare/@metamask/rate-limit-controller@2.0.0...@metamask/rate-limit-controller@2.0.1
44
+ [2.0.0]: https://github.com/MetaMask/core/compare/@metamask/rate-limit-controller@1.0.2...@metamask/rate-limit-controller@2.0.0
45
+ [1.0.2]: https://github.com/MetaMask/core/compare/@metamask/rate-limit-controller@1.0.1...@metamask/rate-limit-controller@1.0.2
46
+ [1.0.1]: https://github.com/MetaMask/core/compare/@metamask/rate-limit-controller@1.0.0...@metamask/rate-limit-controller@1.0.1
47
+ [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/rate-limit-controller@1.0.0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 MetaMask
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # `@metamask/rate-limit-controller`
2
+
3
+ Contains logic for rate-limiting API endpoints by requesting origin.
4
+
5
+ ## Installation
6
+
7
+ `yarn add @metamask/rate-limit-controller`
8
+
9
+ or
10
+
11
+ `npm install @metamask/rate-limit-controller`
12
+
13
+ ## Contributing
14
+
15
+ This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
@@ -0,0 +1,94 @@
1
+ import type { RestrictedControllerMessenger } from '@metamask/base-controller';
2
+ import { BaseControllerV2 as BaseController } from '@metamask/base-controller';
3
+ import type { Patch } from 'immer';
4
+ /**
5
+ * @type RateLimitedApi
6
+ * @property method - The method that is rate-limited.
7
+ * @property rateLimitTimeout - The time window in which the rate limit is applied (in ms).
8
+ * @property rateLimitCount - The amount of calls an origin can make in the rate limit time window.
9
+ */
10
+ export declare type RateLimitedApi = {
11
+ method: (...args: any[]) => any;
12
+ rateLimitTimeout?: number;
13
+ rateLimitCount?: number;
14
+ };
15
+ /**
16
+ * @type RateLimitState
17
+ * @property requests - Object containing number of requests in a given interval for each origin and api type combination
18
+ */
19
+ export declare type RateLimitState<RateLimitedApis extends Record<string, RateLimitedApi>> = {
20
+ requests: Record<keyof RateLimitedApis, Record<string, number>>;
21
+ };
22
+ declare const name = "RateLimitController";
23
+ export declare type RateLimitStateChange<RateLimitedApis extends Record<string, RateLimitedApi>> = {
24
+ type: `${typeof name}:stateChange`;
25
+ payload: [RateLimitState<RateLimitedApis>, Patch[]];
26
+ };
27
+ export declare type GetRateLimitState<RateLimitedApis extends Record<string, RateLimitedApi>> = {
28
+ type: `${typeof name}:getState`;
29
+ handler: () => RateLimitState<RateLimitedApis>;
30
+ };
31
+ export declare type CallApi<RateLimitedApis extends Record<string, RateLimitedApi>> = {
32
+ type: `${typeof name}:call`;
33
+ handler: RateLimitController<RateLimitedApis>['call'];
34
+ };
35
+ export declare type RateLimitControllerActions<RateLimitedApis extends Record<string, RateLimitedApi>> = GetRateLimitState<RateLimitedApis> | CallApi<RateLimitedApis>;
36
+ export declare type RateLimitMessenger<RateLimitedApis extends Record<string, RateLimitedApi>> = RestrictedControllerMessenger<typeof name, RateLimitControllerActions<RateLimitedApis>, RateLimitStateChange<RateLimitedApis>, never, never>;
37
+ /**
38
+ * Controller with logic for rate-limiting API endpoints per requesting origin.
39
+ */
40
+ export declare class RateLimitController<RateLimitedApis extends Record<string, RateLimitedApi>> extends BaseController<typeof name, RateLimitState<RateLimitedApis>, RateLimitMessenger<RateLimitedApis>> {
41
+ private readonly implementations;
42
+ private readonly rateLimitTimeout;
43
+ private readonly rateLimitCount;
44
+ /**
45
+ * Creates a RateLimitController instance.
46
+ *
47
+ * @param options - Constructor options.
48
+ * @param options.messenger - A reference to the messaging system.
49
+ * @param options.state - Initial state to set on this controller.
50
+ * @param options.implementations - Mapping from API type to API implementation.
51
+ * @param options.rateLimitTimeout - The time window in which the rate limit is applied (in ms).
52
+ * @param options.rateLimitCount - The amount of calls an origin can make in the rate limit time window.
53
+ */
54
+ constructor({ rateLimitTimeout, rateLimitCount, messenger, state, implementations, }: {
55
+ rateLimitTimeout?: number;
56
+ rateLimitCount?: number;
57
+ messenger: RateLimitMessenger<RateLimitedApis>;
58
+ state?: Partial<RateLimitState<RateLimitedApis>>;
59
+ implementations: RateLimitedApis;
60
+ });
61
+ /**
62
+ * Calls an API if the requesting origin is not rate-limited.
63
+ *
64
+ * @param origin - The requesting origin.
65
+ * @param type - The type of API call to make.
66
+ * @param args - Arguments for the API call.
67
+ * @returns `false` if rate-limited, and `true` otherwise.
68
+ */
69
+ call<ApiType extends keyof RateLimitedApis>(origin: string, type: ApiType, ...args: Parameters<RateLimitedApis[ApiType]['method']>): Promise<ReturnType<RateLimitedApis[ApiType]['method']>>;
70
+ /**
71
+ * Checks whether an origin is rate limited for the a specific API.
72
+ *
73
+ * @param api - The API the origin is trying to access.
74
+ * @param origin - The origin trying to access the API.
75
+ * @returns `true` if rate-limited, and `false` otherwise.
76
+ */
77
+ private isRateLimited;
78
+ /**
79
+ * Records that an origin has made a request to call an API, for rate-limiting purposes.
80
+ *
81
+ * @param api - The API the origin is trying to access.
82
+ * @param origin - The origin trying to access the API.
83
+ */
84
+ private recordRequest;
85
+ /**
86
+ * Resets the request count for a given origin and API combination, for rate-limiting purposes.
87
+ *
88
+ * @param api - The API in question.
89
+ * @param origin - The origin in question.
90
+ */
91
+ private resetRequestCount;
92
+ }
93
+ export {};
94
+ //# sourceMappingURL=RateLimitController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RateLimitController.d.ts","sourceRoot":"","sources":["../src/RateLimitController.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.RateLimitController = void 0;
13
+ const base_controller_1 = require("@metamask/base-controller");
14
+ const eth_rpc_errors_1 = require("eth-rpc-errors");
15
+ const name = 'RateLimitController';
16
+ const metadata = {
17
+ requests: { persist: false, anonymous: false },
18
+ };
19
+ /**
20
+ * Controller with logic for rate-limiting API endpoints per requesting origin.
21
+ */
22
+ class RateLimitController extends base_controller_1.BaseControllerV2 {
23
+ /**
24
+ * Creates a RateLimitController instance.
25
+ *
26
+ * @param options - Constructor options.
27
+ * @param options.messenger - A reference to the messaging system.
28
+ * @param options.state - Initial state to set on this controller.
29
+ * @param options.implementations - Mapping from API type to API implementation.
30
+ * @param options.rateLimitTimeout - The time window in which the rate limit is applied (in ms).
31
+ * @param options.rateLimitCount - The amount of calls an origin can make in the rate limit time window.
32
+ */
33
+ constructor({ rateLimitTimeout = 5000, rateLimitCount = 1, messenger, state, implementations, }) {
34
+ const defaultState = {
35
+ requests: Object.keys(implementations).reduce((acc, key) => (Object.assign(Object.assign({}, acc), { [key]: {} })), {}),
36
+ };
37
+ super({
38
+ name,
39
+ metadata,
40
+ messenger,
41
+ state: Object.assign(Object.assign({}, defaultState), state),
42
+ });
43
+ this.implementations = implementations;
44
+ this.rateLimitTimeout = rateLimitTimeout;
45
+ this.rateLimitCount = rateLimitCount;
46
+ this.messagingSystem.registerActionHandler(`${name}:call`, ((origin, type, ...args) => this.call(origin, type, ...args)));
47
+ }
48
+ /**
49
+ * Calls an API if the requesting origin is not rate-limited.
50
+ *
51
+ * @param origin - The requesting origin.
52
+ * @param type - The type of API call to make.
53
+ * @param args - Arguments for the API call.
54
+ * @returns `false` if rate-limited, and `true` otherwise.
55
+ */
56
+ call(origin, type, ...args) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ if (this.isRateLimited(type, origin)) {
59
+ throw eth_rpc_errors_1.ethErrors.rpc.limitExceeded({
60
+ message: `"${type.toString()}" is currently rate-limited. Please try again later.`,
61
+ });
62
+ }
63
+ this.recordRequest(type, origin);
64
+ const implementation = this.implementations[type].method;
65
+ if (!implementation) {
66
+ throw new Error('Invalid api type');
67
+ }
68
+ return implementation(...args);
69
+ });
70
+ }
71
+ /**
72
+ * Checks whether an origin is rate limited for the a specific API.
73
+ *
74
+ * @param api - The API the origin is trying to access.
75
+ * @param origin - The origin trying to access the API.
76
+ * @returns `true` if rate-limited, and `false` otherwise.
77
+ */
78
+ isRateLimited(api, origin) {
79
+ var _a;
80
+ const rateLimitCount = (_a = this.implementations[api].rateLimitCount) !== null && _a !== void 0 ? _a : this.rateLimitCount;
81
+ return this.state.requests[api][origin] >= rateLimitCount;
82
+ }
83
+ /**
84
+ * Records that an origin has made a request to call an API, for rate-limiting purposes.
85
+ *
86
+ * @param api - The API the origin is trying to access.
87
+ * @param origin - The origin trying to access the API.
88
+ */
89
+ recordRequest(api, origin) {
90
+ var _a;
91
+ const rateLimitTimeout = (_a = this.implementations[api].rateLimitTimeout) !== null && _a !== void 0 ? _a : this.rateLimitTimeout;
92
+ this.update((state) => {
93
+ var _a;
94
+ const previous = (_a = state.requests[api][origin]) !== null && _a !== void 0 ? _a : 0;
95
+ state.requests[api][origin] = previous + 1;
96
+ if (previous === 0) {
97
+ setTimeout(() => this.resetRequestCount(api, origin), rateLimitTimeout);
98
+ }
99
+ });
100
+ }
101
+ /**
102
+ * Resets the request count for a given origin and API combination, for rate-limiting purposes.
103
+ *
104
+ * @param api - The API in question.
105
+ * @param origin - The origin in question.
106
+ */
107
+ resetRequestCount(api, origin) {
108
+ this.update((state) => {
109
+ state.requests[api][origin] = 0;
110
+ });
111
+ }
112
+ }
113
+ exports.RateLimitController = RateLimitController;
114
+ //# sourceMappingURL=RateLimitController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RateLimitController.js","sourceRoot":"","sources":["../src/RateLimitController.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseControllerV2 as BaseController } from '@metamask/base-controller';\nimport { ethErrors } from 'eth-rpc-errors';\nimport type { Patch } from 'immer';\n\n/**\n * @type RateLimitedApi\n * @property method - The method that is rate-limited.\n * @property rateLimitTimeout - The time window in which the rate limit is applied (in ms).\n * @property rateLimitCount - The amount of calls an origin can make in the rate limit time window.\n */\nexport type RateLimitedApi = {\n method: (...args: any[]) => any;\n rateLimitTimeout?: number;\n rateLimitCount?: number;\n};\n\n/**\n * @type RateLimitState\n * @property requests - Object containing number of requests in a given interval for each origin and api type combination\n */\nexport type RateLimitState<\n RateLimitedApis extends Record<string, RateLimitedApi>,\n> = {\n requests: Record<keyof RateLimitedApis, Record<string, number>>;\n};\n\nconst name = 'RateLimitController';\n\nexport type RateLimitStateChange<\n RateLimitedApis extends Record<string, RateLimitedApi>,\n> = {\n type: `${typeof name}:stateChange`;\n payload: [RateLimitState<RateLimitedApis>, Patch[]];\n};\n\nexport type GetRateLimitState<\n RateLimitedApis extends Record<string, RateLimitedApi>,\n> = {\n type: `${typeof name}:getState`;\n handler: () => RateLimitState<RateLimitedApis>;\n};\n\nexport type CallApi<RateLimitedApis extends Record<string, RateLimitedApi>> = {\n type: `${typeof name}:call`;\n handler: RateLimitController<RateLimitedApis>['call'];\n};\n\nexport type RateLimitControllerActions<\n RateLimitedApis extends Record<string, RateLimitedApi>,\n> = GetRateLimitState<RateLimitedApis> | CallApi<RateLimitedApis>;\n\nexport type RateLimitMessenger<\n RateLimitedApis extends Record<string, RateLimitedApi>,\n> = RestrictedControllerMessenger<\n typeof name,\n RateLimitControllerActions<RateLimitedApis>,\n RateLimitStateChange<RateLimitedApis>,\n never,\n never\n>;\n\nconst metadata = {\n requests: { persist: false, anonymous: false },\n};\n\n/**\n * Controller with logic for rate-limiting API endpoints per requesting origin.\n */\nexport class RateLimitController<\n RateLimitedApis extends Record<string, RateLimitedApi>,\n> extends BaseController<\n typeof name,\n RateLimitState<RateLimitedApis>,\n RateLimitMessenger<RateLimitedApis>\n> {\n private readonly implementations;\n\n private readonly rateLimitTimeout;\n\n private readonly rateLimitCount;\n\n /**\n * Creates a RateLimitController instance.\n *\n * @param options - Constructor options.\n * @param options.messenger - A reference to the messaging system.\n * @param options.state - Initial state to set on this controller.\n * @param options.implementations - Mapping from API type to API implementation.\n * @param options.rateLimitTimeout - The time window in which the rate limit is applied (in ms).\n * @param options.rateLimitCount - The amount of calls an origin can make in the rate limit time window.\n */\n constructor({\n rateLimitTimeout = 5000,\n rateLimitCount = 1,\n messenger,\n state,\n implementations,\n }: {\n rateLimitTimeout?: number;\n rateLimitCount?: number;\n messenger: RateLimitMessenger<RateLimitedApis>;\n state?: Partial<RateLimitState<RateLimitedApis>>;\n implementations: RateLimitedApis;\n }) {\n const defaultState = {\n requests: Object.keys(implementations).reduce(\n (acc, key) => ({ ...acc, [key]: {} }),\n {} as Record<keyof RateLimitedApis, Record<string, number>>,\n ),\n };\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.implementations = implementations;\n this.rateLimitTimeout = rateLimitTimeout;\n this.rateLimitCount = rateLimitCount;\n\n this.messagingSystem.registerActionHandler(\n `${name}:call` as const,\n ((\n origin: string,\n type: keyof RateLimitedApis,\n ...args: Parameters<RateLimitedApis[keyof RateLimitedApis]['method']>\n ) => this.call(origin, type, ...args)) as any,\n );\n }\n\n /**\n * Calls an API if the requesting origin is not rate-limited.\n *\n * @param origin - The requesting origin.\n * @param type - The type of API call to make.\n * @param args - Arguments for the API call.\n * @returns `false` if rate-limited, and `true` otherwise.\n */\n async call<ApiType extends keyof RateLimitedApis>(\n origin: string,\n type: ApiType,\n ...args: Parameters<RateLimitedApis[ApiType]['method']>\n ): Promise<ReturnType<RateLimitedApis[ApiType]['method']>> {\n if (this.isRateLimited(type, origin)) {\n throw ethErrors.rpc.limitExceeded({\n message: `\"${type.toString()}\" is currently rate-limited. Please try again later.`,\n });\n }\n this.recordRequest(type, origin);\n\n const implementation = this.implementations[type].method;\n\n if (!implementation) {\n throw new Error('Invalid api type');\n }\n\n return implementation(...args);\n }\n\n /**\n * Checks whether an origin is rate limited for the a specific API.\n *\n * @param api - The API the origin is trying to access.\n * @param origin - The origin trying to access the API.\n * @returns `true` if rate-limited, and `false` otherwise.\n */\n private isRateLimited(api: keyof RateLimitedApis, origin: string) {\n const rateLimitCount =\n this.implementations[api].rateLimitCount ?? this.rateLimitCount;\n return this.state.requests[api][origin] >= rateLimitCount;\n }\n\n /**\n * Records that an origin has made a request to call an API, for rate-limiting purposes.\n *\n * @param api - The API the origin is trying to access.\n * @param origin - The origin trying to access the API.\n */\n private recordRequest(api: keyof RateLimitedApis, origin: string) {\n const rateLimitTimeout =\n this.implementations[api].rateLimitTimeout ?? this.rateLimitTimeout;\n this.update((state) => {\n const previous = (state as any).requests[api][origin] ?? 0;\n (state as any).requests[api][origin] = previous + 1;\n\n if (previous === 0) {\n setTimeout(() => this.resetRequestCount(api, origin), rateLimitTimeout);\n }\n });\n }\n\n /**\n * Resets the request count for a given origin and API combination, for rate-limiting purposes.\n *\n * @param api - The API in question.\n * @param origin - The origin in question.\n */\n private resetRequestCount(api: keyof RateLimitedApis, origin: string) {\n this.update((state) => {\n (state as any).requests[api][origin] = 0;\n });\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export * from './RateLimitController';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./RateLimitController"), exports);
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,wDAAsC","sourcesContent":["export * from './RateLimitController';\n"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@metamask-previews/rate-limit-controller",
3
+ "version": "3.0.0-preview.3dbf14f",
4
+ "description": "Contains logic for rate-limiting API endpoints by requesting origin",
5
+ "keywords": [
6
+ "MetaMask",
7
+ "Ethereum"
8
+ ],
9
+ "homepage": "https://github.com/MetaMask/core/tree/main/packages/rate-limit-controller#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/MetaMask/core/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/MetaMask/core.git"
16
+ },
17
+ "license": "MIT",
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "files": [
21
+ "dist/"
22
+ ],
23
+ "scripts": {
24
+ "build:docs": "typedoc",
25
+ "changelog:validate": "../../scripts/validate-changelog.sh @metamask/rate-limit-controller",
26
+ "publish:preview": "yarn npm publish --tag preview",
27
+ "test": "jest",
28
+ "test:watch": "jest --watch"
29
+ },
30
+ "dependencies": {
31
+ "@metamask-previews/base-controller": "3.2.0-preview.3dbf14f",
32
+ "eth-rpc-errors": "^4.0.2",
33
+ "immer": "^9.0.6"
34
+ },
35
+ "devDependencies": {
36
+ "@metamask/auto-changelog": "^3.1.0",
37
+ "@types/jest": "^27.4.1",
38
+ "deepmerge": "^4.2.2",
39
+ "jest": "^27.5.1",
40
+ "ts-jest": "^27.1.4",
41
+ "typedoc": "^0.22.15",
42
+ "typedoc-plugin-missing-exports": "^0.22.6",
43
+ "typescript": "~4.6.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=16.0.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "registry": "https://registry.npmjs.org/"
51
+ }
52
+ }