├── .gitignore ├── .npmignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.ts ├── middleware │ ├── apollo-cache.ts │ ├── express-cache.ts │ ├── monitoring.ts │ └── rate-limit.ts ├── query-test │ ├── pm-test.ts │ ├── query-test.ts │ └── testCase.ts └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !dist/* 3 | !dist/middleware/monitoring/* 4 | !dist/middleware/* 5 | !package.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | This package is intended to be a combined monitoring/rate limiting solution for GraphQL, with pre-query execution static complexity analysis. The primary opinionation of this cost analysis implementation is that the user's schema should be augmented with an @cost directive on relevant non-polymorphic fields and that lists should be provided with an @paginationLimit directive that detail the expected bounds of said list. These directives being applied to the schema are the primary vehicle for user configurability, along with a configuration object that defines default parameters such as whether or not you opt into usage of the monitoring portion of our package, default pagination limits to be applied to lists without an @paginationLimit directive assigned to them, maximum depth etc. 4 | 5 | This project derived significant inspiration from this paper by [IBM](https://arxiv.org/abs/2009.05632) and certain aspects of their specification for cost analysis, such as the @cost directive. 6 | 7 | # GraphQL Endpoint Monitor 8 | The GraphQL Endpoint Monitor is a package designed to log and monitor traffic metrics for a GraphQL endpoint. The package is available as a plugin for Apollo server or as an Express Middlware. It sends the collected data to our [Developer Portal](https://gleiphql.dev) for visualization. This package is intended to be used alongside our rate limiter package. 9 | 10 | ## Metrics Collected 11 | The `apolloEndpointMonitor` plugin and `expressEndpointMonitor` middleware collects the following metrics for each GraphQL request: 12 | 13 | 14 | | Metric | Type | Description | 15 | | :--------------- | :------- | :--------------------------------------------------------------------------------------- | 16 | | `depth` | Number | The depth of the GraphQL query. | 17 | | `ip` | String | The IP address of the client making the request. | 18 | | `url` | String | The URL of the GraphQL endpoint. | 19 | | `timestamp` | String | The timestamp when the request was made. | 20 | | `objectTypes` | {String} | An object containing the count of each GraphQL object type used in the query. | 21 | | `queryString` | String | The original GraphQL query string. | 22 | | `complexityScore`| Number | The complexity score of the GraphQL query. | 23 | | `blocked` | Boolean | If the query was blocked by the rate limiter. | 24 | | `complexityLimit`| Number | The complexity limit set by the user in the rate limiter. | 25 | 26 | These metrics will be sent to the web application for visualization and monitoring. 27 | 28 | ## Prerequisites 29 | 1. Signup/login to the [GleiphQL developer portal](https://gleiphql.dev). 30 | 31 | 2. Add the endpoint URL to your account. Make sure the endpoint url you enter in the developer portal matches the endpoint URL of your graphQL API. 32 | 33 | 3. Import and configure the [GleiphQL rate-limiting package](https://www.npmjs.com/package/gleiphql) 34 | 35 | ## Apollo Server Installation and Usage 36 | If you would like to monitor your endpoint with Apollo server, follow the instructions in this section. The `apolloEndpointMonitor` plugin is intended to be used alongside the `apolloRateLimiter` plugin and the `gleiphqlContext` function, with the apolloEndpointMonitor plugin placed alongside the apolloRateLimiter plugin in the Apollo server. 37 | 38 | Installation: 39 | ``` 40 | npm install gleiphql 41 | ``` 42 | 43 | Usage: 44 | ``` 45 | import { apolloRateLimiter, apolloEndpointMonitor, gleiphqlContext } from 'gleiphql'; 46 | import { startStandaloneServer } from '@apollo/server/standalone'; 47 | import { ApolloServer } from '@apollo/server'; 48 | 49 | 50 | // Configuration for the expressEndpointMonitor middleware. 51 | const monitorConfig = { 52 | gleiphqlUsername: process.env.GLEIPHQL_USERNAME, 53 | gleiphqlPassword: process.env.GLEIPHQL_PASSWORD, 54 | }; 55 | 56 | // Apply the apolloEndpointMonitor plugin along with the apolloRateLimiter middleware. 57 | const apolloServer = new ApolloServer({ 58 | schema: schema, 59 | plugins: [ 60 | apolloRateLimiter(apolloConfig), 61 | apolloEndpointMonitor(monitorConfig) 62 | ], 63 | }); 64 | 65 | // Add the gleipqlContext to the stand alone server 66 | const { url } = await startStandaloneServer(apolloServer, { 67 | context: gleiphqlContext 68 | }); 69 | console.log(`🚀 Server ready at ${url}`); 70 | ``` 71 | 72 | The `apolloEndpointMonitor` plugin takes a configuration object with the following properties: 73 | * `gleiphqlUsername`: The username to access the web application where the metrics will be displayed. 74 | * `gleiphqlPassword`: The password for the web application authentication. 75 | 76 | ## Express Installation and Usage 77 | If you would like to monitor your endpoint with Express, follow the instructions in this section. The `expressEndpointMonitor` middleware is intended to be used alongside the `expressRateLimiter` middleware, with the expressRateLimiter middleware placed AFTER the expressEndpointMonitor middleware in the middleware chain. 78 | 79 | Installation: 80 | 81 | ``` 82 | npm install gleiphql 83 | ``` 84 | Usage: 85 | ``` 86 | import express from 'express'; 87 | import { expressEndpointMonitor, expressRateLimiter } from 'gleiphql'; 88 | import { createHandler } from 'graphql-http/lib/use/express'; 89 | 90 | const app = express(); 91 | 92 | // Configuration for the expressEndpointMonitor middleware. 93 | const monitorConfig = { 94 | gleiphqlUsername: process.env.GLEIPHQL_USERNAME, 95 | gleiphqlPassword: process.env.GLEIPHQL_PASSWORD, 96 | }; 97 | 98 | // Apply the expressEndpointMonitor middleware before the expressRateLimiter middleware. 99 | app.use('/graphql', expressEndpointMonitor(monitorConfig), expressRateLimiter(rateLimitConfig), createHandler({ schema })); 100 | 101 | // Your other middleware and routes go here. 102 | // ... 103 | 104 | // Start the server 105 | app.listen(3000, () => { 106 | console.log('Server is running on port 3000.'); 107 | }); 108 | ``` 109 | 110 | The `expressEndpointMonitor` middleware takes a configuration object with the following properties: 111 | * `gleiphqlUsername`: The username to access the web application where the metrics will be displayed. 112 | * `gleiphqlPassword`: The password for the web application authentication. 113 | 114 | 115 | # GraphQL Rate-limiter 116 | 117 | The GraphQL Rate-limiter is a package that rate-limits incoming queries based on user defined @cost and @paginationLimit directives applied on the schema level. Infinitely recursive queries are handled by a user-defined depth limit. The default value for maximum depth is 10 if not explicitly configured. The package is available as an Express Middleware or as a plugin for Apollo server. This package can be used with or without the monitoring package. 118 | 119 | ## Prerequisites 120 | 121 | The primary opinionation of this complexity analysis is that the user augments any relevant non-abstract/polymorphic fields and relevant arguments (generally slicing arguments) with a @cost directive. Lists can be augmented with a @paginationLimit directive, or the default paginationLimit defined in the configuration object will be applied to any lists that are encountered. 122 | 123 | An example SDL with these specifications is as follows: 124 | 125 | ``` 126 | directive @cost(value: Int) on FIELD_DEFINITION | ARGUMENT_DEFINITION 127 | directive @paginationLimit(value: Int) on FIELD_DEFINITION 128 | 129 | type Related { 130 | content: [Content!]! 131 | } 132 | 133 | interface Content { 134 | id: ID! 135 | title: String! 136 | related: Related 137 | } 138 | 139 | type Post implements Content { 140 | id: ID! @cost(value: 3) 141 | title: String! @cost(value: 4) 142 | body: String! @cost(value: 10) 143 | tags: [String!]! @cost(value: 5) 144 | related: Related 145 | } 146 | 147 | type Image implements Content { 148 | id: ID! @cost(value: 5) 149 | title: String! @cost(value: 6) 150 | uri: String! @cost(value: 2) 151 | related: Related 152 | } 153 | 154 | union UnionContent = Post | Image 155 | 156 | type Query { 157 | content: [Content] @paginationLimit(value: 10) 158 | posts(limit: Int @cost(value:10): [Post] @cost(value: 3) @paginationLimit(value: 10)) 159 | images: [Image] @cost(value: 5) @paginationLimit(value: 10) 160 | related: [Related] @paginationLimit(value: 10) 161 | unionContent: [UnionContent] @paginationLimit(value: 10) 162 | } 163 | ``` 164 | 165 | 166 | Each field is augmented with a @cost directive that assigns a cost to each field of the schema based on user input. The @cost directive is strictly there to ensure the cost data is accessible to the complexity analysis portion of the package, it should have no functionality assigned to it beyond that. Without these directives a default value of 1 will be applied to each field. While the cost analysis will still run without augmenting the given schema, the results may not be usable as a heuristic for applying pre-query execution rate-limiting. 167 | 168 | ## Express Installation and Usage 169 | 170 | Installation: 171 | 172 | ``` 173 | npm install gleiphql 174 | ``` 175 | Usage: 176 | ``` 177 | import express from 'express'; 178 | import { expressRateLimiter } from 'gleiphql'; 179 | import { createHandler } from 'graphql-http/lib/use/express'; 180 | 181 | const app = express(); 182 | 183 | // Configuration for the expressRateLimiter middleware. 184 | 185 | Below is a sample configuration for the sample GraphQL endpoint, the complexityLimit, paginationLimit, refillTime, and refillAmount properties are all up to user interpretation/use case. 186 | 187 | const spacexConfig: RateLimitConfig = { 188 | complexityLimit: 3000, 189 | paginationLimit: 15, 190 | schema: spaceXSchema, 191 | refillTime: 300000, // 5 minutes 192 | refillAmount: 1000, 193 | redis: false 194 | maxDepth: 15 195 | } 196 | 197 | // Apply the expressRateLimiter middleware after the expressRateLimiter middleware, or you can elect to use the rate-limiter alone 198 | 199 | app.use('/graphql', expressEndpointMonitor(monitorConfig), expressRateLimiter(rateLimitConfig), createHandler({ schema })); 200 | 201 | // Your other middleware and routes go here. 202 | // ... 203 | 204 | // Start the server 205 | app.listen(3000, () => { 206 | console.log('Server is running on port 3000.'); 207 | }); 208 | ``` 209 | 210 | ## Apollo Server Installation and Usage 211 | 212 | If you would like to rate-limit on Apollo server, follow the instructions in this section. The `apolloRateLimiter` plugin is intended to work with the `gleiphqlContext` function. 213 | 214 | Installation: 215 | ``` 216 | npm install gleiphql 217 | ``` 218 | 219 | Usage: 220 | ``` 221 | import { apolloRateLimiter, gleiphqlContext } from 'gleiphql'; 222 | import { startStandaloneServer } from '@apollo/server/standalone'; 223 | 224 | 225 | // Configuration for the complexity analysis/rate-limiting middleware. 226 | 227 | const spacexConfig: RateLimitConfig = { 228 | complexityLimit: 3000, 229 | paginationLimit: 15, 230 | refillTime: 300000, // 5 minutes 231 | refillAmount: 1000, 232 | redis: false, 233 | maxDepth: 15, 234 | } 235 | 236 | 237 | // Apply the apolloRateLimiter plug-in. 238 | const apolloServer = new ApolloServer({ 239 | schema: schema, 240 | plugins: [ 241 | apolloRateLimiter(apolloConfig), 242 | ], 243 | }); 244 | 245 | // Add the gleipqlContext to the stand alone server 246 | const { url } = await startStandaloneServer(apolloServer, { 247 | context: gleiphqlContext 248 | }); 249 | console.log(`🚀 Server ready at ${url}`); 250 | ``` 251 | 252 | ## Contributing 253 | If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on our GitHub repository. 254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gleiphql", 3 | "version": "1.0.0", 4 | "description": "GraphQL Rate Limiting & Endpoint Monitoring Tool", 5 | "main": "./dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node dist/query-test/query-test", 9 | "build": "tsc", 10 | "dev": "node dist/query-test/query-test" 11 | }, 12 | "author": "Andrew Larkin, Jiecheng Dong, Yeong Sil Yoon, Kevin Phan", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@graphql-tools/load": "^8.0.0", 16 | "@graphql-tools/url-loader": "^8.0.0", 17 | "@types/express": "^4.17.17", 18 | "dotenv": "^16.0.3", 19 | "express": "^4.18.2", 20 | "graphql-yoga": "^3.9.1", 21 | "typescript": "^5.0.4" 22 | }, 23 | "dependencies": { 24 | "@apollo/server": "^4.9.1", 25 | "graphql": "^16.7.1", 26 | "node-fetch": "^3.3.1", 27 | "redis": "^4.6.7" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { expressEndpointMonitor, apolloEndpointMonitor } from './middleware/monitoring.js'; 2 | import { expressRateLimiter, apolloRateLimiter, gleiphqlContext }from './middleware/rate-limit.js'; 3 | 4 | export { expressEndpointMonitor, expressRateLimiter, apolloRateLimiter, apolloEndpointMonitor, gleiphqlContext } -------------------------------------------------------------------------------- /src/middleware/apollo-cache.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { RedisClientType, createClient } from 'redis'; 3 | import { TokenBucket } from '../types'; 4 | 5 | const redis = async function (config: any, complexityScore: number, requestContext: any) : Promise { 6 | const now: number = Date.now(); 7 | const refillRate: number = config.refillAmount / config.refillTime; 8 | let requestIP: string = config.requestContext.contextValue.clientIP; 9 | // fixes format of ip addresses 10 | if (requestIP.includes('::ffff:')) { 11 | requestIP = requestIP.replace('::ffff:', ''); 12 | } 13 | 14 | const client: RedisClientType = createClient(); 15 | await client.connect(); 16 | let currRequest: string | null = await client.get(requestIP); 17 | 18 | // if the info for the current request is not found in the cache, then an entry will be created for it 19 | if (currRequest === null) { 20 | await client.set(requestIP, JSON.stringify({ 21 | tokens: config.complexityLimit, 22 | lastRefillTime: now, 23 | })); 24 | currRequest = await client.get(requestIP); 25 | } 26 | 27 | // if the info for the current request is still not found in the cache, then we will throw an error 28 | if (currRequest === null) { 29 | await client.disconnect(); 30 | throw new GraphQLError('Redis Error in apollo-cache.ts'); 31 | } 32 | 33 | let parsedRequest: { 34 | tokens: number, 35 | lastRefillTime: number 36 | } = JSON.parse(currRequest); 37 | const timeElapsed: number = now - parsedRequest.lastRefillTime; 38 | const tokensToAdd: number = timeElapsed * refillRate; // decimals 39 | // const tokensToAdd = Math.floor(timeElapsed2 * refillRate); // no decimals 40 | 41 | parsedRequest.tokens = Math.min( 42 | parsedRequest.tokens + tokensToAdd, 43 | config.complexityLimit 44 | ); 45 | 46 | parsedRequest.lastRefillTime = now; 47 | await client.set(requestIP, JSON.stringify(parsedRequest)); 48 | 49 | currRequest = await client.get(requestIP); 50 | if (currRequest === null) { 51 | await client.disconnect(); 52 | throw new GraphQLError('Redis Error in apollo-cache.ts'); 53 | } 54 | 55 | parsedRequest = JSON.parse(currRequest); 56 | if (complexityScore >= parsedRequest.tokens) { 57 | requestContext.contextValue.blocked = true; 58 | console.log('Complexity or depth of this query is too high'); 59 | await client.disconnect(); 60 | throw new GraphQLError('Complexity or depth of this query is too high', { 61 | extensions: { 62 | cost: { 63 | requestedQueryCost: complexityScore, 64 | currentTokensAvailable: Number(parsedRequest.tokens.toFixed(2)), 65 | maximumTokensAvailable: config.complexityLimit, 66 | } 67 | }, 68 | }); 69 | } 70 | parsedRequest.tokens -= complexityScore; 71 | await client.set(requestIP, JSON.stringify(parsedRequest)); 72 | 73 | // disconnect from the redis client 74 | await client.disconnect(); 75 | } 76 | 77 | const nonRedis = function (config: any, complexityScore: number, tokenBucket: TokenBucket) : TokenBucket { 78 | const now: number = Date.now(); 79 | const refillRate: number = config.refillAmount / config.refillTime; 80 | let requestIP: string = config.requestContext.contextValue.clientIP; 81 | // fixes format of ip addresses 82 | if (requestIP.includes('::ffff:')) { 83 | requestIP = requestIP.replace('::ffff:', ''); 84 | } 85 | 86 | // if the info for the current request is not found in the cache, then an entry will be created for it 87 | if (!tokenBucket[requestIP]) { 88 | tokenBucket[requestIP] = { 89 | tokens: config.complexityLimit, 90 | lastRefillTime: now, 91 | } 92 | } 93 | const timeElapsed: number = now - tokenBucket[requestIP].lastRefillTime; 94 | const tokensToAdd: number = timeElapsed * refillRate; // decimals 95 | // const tokensToAdd = Math.floor(timeElapsed * refillRate); // no decimals 96 | 97 | tokenBucket[requestIP].tokens = Math.min( 98 | tokenBucket[requestIP].tokens + tokensToAdd, 99 | config.complexityLimit 100 | ); 101 | tokenBucket[requestIP].lastRefillTime = now; 102 | 103 | return tokenBucket; 104 | } 105 | 106 | const apolloCache = { 107 | redis, 108 | nonRedis 109 | }; 110 | 111 | export default apolloCache; -------------------------------------------------------------------------------- /src/middleware/express-cache.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import fetch from 'node-fetch'; 3 | import { createClient, RedisClientType } from 'redis'; 4 | import { TokenBucket } from '../types'; 5 | 6 | const redis = async function (config: any, complexityScore: number, req: Request, res: Response, next: NextFunction) : Promise { 7 | const now: number = Date.now(); 8 | const refillRate: number = config.refillAmount / config.refillTime; 9 | let requestIP: string = req.ip; 10 | // fixes format of ip addresses 11 | if (requestIP.includes('::ffff:')) { 12 | requestIP = requestIP.replace('::ffff:', ''); 13 | } 14 | 15 | const client: RedisClientType = createClient(); 16 | await client.connect(); 17 | let currRequest: string | null = await client.get(requestIP); 18 | 19 | // if the info for the current request is not found in the cache, then an entry will be created for it 20 | if (currRequest === null) { 21 | await client.set(requestIP, JSON.stringify({ 22 | tokens: config.complexityLimit, 23 | lastRefillTime: now, 24 | })) 25 | currRequest = await client.get(requestIP); 26 | } 27 | 28 | // if the info for the current request is still not found in the cache, then we will throw an error 29 | if (currRequest === null) { 30 | await client.disconnect(); 31 | return next(Error); 32 | } 33 | 34 | let parsedRequest: { 35 | tokens: number, 36 | lastRefillTime: number 37 | } = JSON.parse(currRequest); 38 | const timeElapsed: number = now - parsedRequest.lastRefillTime; 39 | const tokensToAdd: number = timeElapsed * refillRate; // decimals 40 | // const tokensToAdd = Math.floor(timeElapsed2 * refillRate); // no decimals 41 | 42 | parsedRequest.tokens = Math.min( 43 | parsedRequest.tokens + tokensToAdd, 44 | config.complexityLimit 45 | ); 46 | 47 | parsedRequest.lastRefillTime = now; 48 | await client.set(requestIP, JSON.stringify(parsedRequest)); 49 | 50 | currRequest = await client.get(requestIP); 51 | if (currRequest === null) { 52 | await client.disconnect(); 53 | return next(Error); 54 | } 55 | parsedRequest = JSON.parse(currRequest); 56 | if (complexityScore >= parsedRequest.tokens) { 57 | if (res.locals.gleiphqlData) { 58 | res.locals.gleiphqlData.complexityScore = complexityScore; 59 | try { 60 | await fetch('https://gleiphql.azurewebsites.net/api/data', { 61 | method: 'POST', 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | body: JSON.stringify(res.locals.gleiphqlData) 66 | }); 67 | } 68 | catch (err: unknown) { 69 | console.log('Unable to save to database'); 70 | } 71 | } 72 | const error = { 73 | errors: [ 74 | { 75 | message: `Token limit exceeded`, 76 | extensions: { 77 | cost: { 78 | requestedQueryCost: complexityScore, 79 | currentTokensAvailable: Number(parsedRequest.tokens.toFixed(2)), 80 | maximumTokensAvailable: config.complexityLimit, 81 | }, 82 | responseDetails: { 83 | status: 429, 84 | statusText: 'Too Many Requests', 85 | } 86 | } 87 | } 88 | ] 89 | } 90 | console.log('Complexity or depth of this query is too high'); 91 | await client.disconnect(); 92 | res.status(429).json(error); 93 | return next(Error); 94 | } 95 | parsedRequest.tokens -= complexityScore; 96 | if (res.locals.gleiphqlData) { 97 | res.locals.gleiphqlData.complexityScore = complexityScore; 98 | try { 99 | await fetch('https://gleiphql.azurewebsites.net/api/data', { 100 | method: 'POST', 101 | headers: { 102 | 'Content-Type': 'application/json', 103 | }, 104 | body: JSON.stringify(res.locals.gleiphqlData) 105 | }); 106 | } 107 | catch (err: unknown) { 108 | console.log('Unable to save to database'); 109 | } 110 | } 111 | await client.set(requestIP, JSON.stringify(parsedRequest)); 112 | 113 | // disconnect from the redis client 114 | await client.disconnect(); 115 | } 116 | 117 | const nonRedis = function (config: any, complexityScore: number, tokenBucket: TokenBucket, req: Request) : TokenBucket { 118 | const now: number = Date.now(); 119 | const refillRate: number = config.refillAmount / config.refillTime; 120 | let requestIP: string = req.ip; 121 | // fixes format of ip addresses 122 | if (requestIP.includes('::ffff:')) { 123 | requestIP = requestIP.replace('::ffff:', ''); 124 | } 125 | 126 | // if the info for the current request is not found in the cache, then an entry will be created for it 127 | if (!tokenBucket[requestIP]) { 128 | tokenBucket[requestIP] = { 129 | tokens: config.complexityLimit, 130 | lastRefillTime: now, 131 | } 132 | } 133 | const timeElapsed: number = now - tokenBucket[requestIP].lastRefillTime; 134 | const tokensToAdd: number = timeElapsed * refillRate; // decimals 135 | // const tokensToAdd = Math.floor(timeElapsed * refillRate); // no decimals 136 | 137 | tokenBucket[requestIP].tokens = Math.min( 138 | tokenBucket[requestIP].tokens + tokensToAdd, 139 | config.complexityLimit 140 | ); 141 | tokenBucket[requestIP].lastRefillTime = now; 142 | 143 | return tokenBucket; 144 | } 145 | 146 | const expressCache = { 147 | redis, 148 | nonRedis 149 | }; 150 | 151 | export default expressCache; -------------------------------------------------------------------------------- /src/middleware/monitoring.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { parse, visit, FieldNode, Kind, DocumentNode, DefinitionNode } from 'graphql'; 3 | import fetch from 'node-fetch'; 4 | import { MonitorConfig, EndpointData } from '../types'; 5 | 6 | // function to find all object types 7 | const extractObjectTypes = (query: DocumentNode): string[] => { 8 | const objectTypes: string[] = []; 9 | 10 | visit(query, { 11 | Field: { 12 | enter(node: FieldNode) { 13 | if (node.selectionSet) { 14 | const parentType: string = node.name.value; 15 | objectTypes.push(parentType); 16 | } 17 | }, 18 | } 19 | }); 20 | 21 | return objectTypes; 22 | }; 23 | 24 | // default endpointData 25 | const endpointData: EndpointData = { 26 | depth: 0, 27 | ip: '', 28 | url: '', 29 | timestamp: '', 30 | objectTypes: {}, 31 | queryString: '', 32 | complexityScore: 0, 33 | blocked: false, 34 | complexityLimit: 0, 35 | email: '', 36 | password: '', 37 | }; 38 | 39 | const expressEndpointMonitor = function (config: MonitorConfig) : (req: Request, res: Response, next: NextFunction) => Promise { 40 | return async (req: Request, res: Response, next: NextFunction) : Promise => { 41 | // default endpointData 42 | const endpointData: EndpointData = { 43 | depth: 0, 44 | ip: '', 45 | url: '', 46 | timestamp: '', 47 | objectTypes: {}, 48 | queryString: '', 49 | complexityScore: 0, 50 | blocked: false, 51 | complexityLimit: 0, 52 | email: '', 53 | password: '', 54 | }; 55 | if (req.body.query) { 56 | const query: DocumentNode = parse(req.body.query); 57 | 58 | endpointData.ip = req.ip; 59 | if (endpointData.ip.includes('::ffff:')) { 60 | endpointData.ip = endpointData.ip.replace('::ffff:', ''); 61 | } 62 | // when working with proxy servers or load balancers, the IP address may be forwarded 63 | // in a different request header such as X-Forwarded-For or X-Real-IP. In such cases, 64 | // you would need to check those headers to obtain the original client IP address. 65 | const host: string | undefined = req.get('host'); 66 | const url: string = `${req.protocol}://${host}${req.originalUrl}`; 67 | endpointData.url = url; 68 | endpointData.complexityScore = res.locals.complexityScore; 69 | endpointData.timestamp = Date(); 70 | endpointData.objectTypes = extractObjectTypes(query); 71 | endpointData.email = config.gleiphqlUsername; 72 | endpointData.password = config.gleiphqlPassword; 73 | // endpointData.depth = res.locals.complexityScore.depth.depth 74 | if (query.loc) { 75 | endpointData.queryString = query.loc.source.body; 76 | } 77 | res.locals.gleiphqlData = endpointData; 78 | } 79 | next(); 80 | } 81 | } 82 | 83 | const apolloEndpointMonitor = (config: MonitorConfig) => { 84 | return { 85 | async requestDidStart(requestContext: any) { 86 | return { 87 | async willSendResponse(requestContext: any) { 88 | // default endpointData 89 | const endpointData: EndpointData = { 90 | depth: 0, 91 | ip: '', 92 | url: '', 93 | timestamp: '', 94 | objectTypes: {}, 95 | queryString: '', 96 | complexityScore: 0, 97 | blocked: false, 98 | complexityLimit: 0, 99 | email: '', 100 | password: '', 101 | }; 102 | if (requestContext.operationName !== 'IntrospectionQuery') { 103 | const query: DocumentNode = requestContext.document; 104 | 105 | endpointData.ip = requestContext.contextValue.clientIP; 106 | if (endpointData.ip.includes('::ffff:')) { 107 | endpointData.ip = endpointData.ip.replace('::ffff:', ''); 108 | } 109 | endpointData.depth = requestContext.contextValue.depth.depth 110 | if (requestContext.contextValue.blocked) { 111 | endpointData.blocked = requestContext.contextValue.blocked; 112 | if (requestContext.contextValue.excessDepth) endpointData.depth = null; 113 | } 114 | endpointData.complexityLimit = requestContext.contextValue.complexityLimit; 115 | endpointData.url = requestContext.request.http.headers.get('referer'); 116 | endpointData.complexityScore = requestContext.contextValue.complexityScore; 117 | endpointData.timestamp = Date(); 118 | endpointData.objectTypes = extractObjectTypes(query); 119 | endpointData.email = config.gleiphqlUsername; 120 | endpointData.password = config.gleiphqlPassword; 121 | if (query.loc) { 122 | endpointData.queryString = query.loc.source.body; 123 | } 124 | try { 125 | await fetch('https://gleiphql.azurewebsites.net/api/data', { 126 | method: 'POST', 127 | headers: { 128 | 'Content-Type': 'application/json', 129 | }, 130 | body: JSON.stringify(endpointData) 131 | }); 132 | } 133 | catch (err: unknown) { 134 | console.log('Unable to save to database'); 135 | } 136 | } 137 | } 138 | }; 139 | } 140 | } 141 | }; 142 | 143 | export { expressEndpointMonitor, apolloEndpointMonitor } -------------------------------------------------------------------------------- /src/middleware/rate-limit.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { 3 | buildSchema, 4 | isAbstractType, 5 | GraphQLInterfaceType, 6 | isNonNullType, 7 | GraphQLType, 8 | GraphQLField, 9 | parse, 10 | GraphQLList, 11 | GraphQLObjectType, 12 | GraphQLSchema, 13 | TypeInfo, 14 | visit, 15 | visitWithTypeInfo, 16 | StringValueNode, 17 | getNamedType, 18 | GraphQLNamedType, 19 | getEnterLeaveForKind, 20 | GraphQLCompositeType, 21 | getNullableType, 22 | Kind, 23 | isListType, 24 | DocumentNode, 25 | DirectiveLocation, 26 | GraphQLUnionType, 27 | GraphQLNamedOutputType, 28 | getDirectiveValues, 29 | isCompositeType, 30 | GraphQLError, 31 | FragmentDefinitionNode, 32 | isInterfaceType, 33 | isUnionType, 34 | DefinitionNode, 35 | OperationDefinitionNode, 36 | isObjectType, 37 | } from 'graphql'; 38 | import graphql from 'graphql'; 39 | import fetch from 'node-fetch'; 40 | import expressCache from './express-cache.js'; 41 | import apolloCache from './apollo-cache.js'; 42 | import { TimeSeriesAggregationType } from 'redis'; 43 | import { infoPrefix } from 'graphql-yoga'; 44 | 45 | //To-dos 46 | 47 | //Resolve Relay convention breaking analysis <= check later 48 | //Resolve argument calls nested in list casing => should be resolved 49 | //Do some work with variables 50 | //Take into account @skip directives 51 | 52 | //relevant interfaces 53 | interface TokenBucket { 54 | [key: string]: { 55 | tokens: number; 56 | lastRefillTime: number; 57 | }; 58 | } 59 | 60 | interface DirectivesInfo { 61 | name: graphql.NameNode, 62 | arguments: readonly graphql.ConstArgumentNode[], 63 | } 64 | 65 | interface PaginationDirectives { 66 | name: string, 67 | value: any, 68 | } 69 | 70 | //start of class 71 | 72 | class ComplexityAnalysis { 73 | 74 | private config: any; 75 | private schema: GraphQLSchema; 76 | private parsedAst: DocumentNode; 77 | private currMult: number = 1; 78 | private complexityScore = 0; 79 | private typeComplexity = 0; 80 | private variables: any; 81 | private slicingArgs: string[] = ['limit', 'first', 'last'] 82 | private allowedDirectives: string[] = ['skip', 'include'] 83 | private excessDepth: boolean = false; 84 | private defaultPaginationLimit: number = 10; 85 | private defaultDepthLimit: number = 10; 86 | private depth: number = 1; 87 | 88 | constructor(schema: GraphQLSchema, parsedAst: DocumentNode, config: any, variables: any) { 89 | this.config = config; 90 | this.parsedAst = parsedAst; 91 | this.schema = schema 92 | this.variables = variables; 93 | this.defaultPaginationLimit = this.config.paginationLimit 94 | this.defaultDepthLimit = this.config.maxDepth 95 | } 96 | 97 | traverseAST () { 98 | if (this.parsedAst) { 99 | 100 | const schemaType = new TypeInfo(this.schema) 101 | 102 | //traverse selection sets propagating from document node 103 | 104 | visit(this.parsedAst, visitWithTypeInfo(schemaType, { 105 | enter: (node, key, parent, path, ancestors) => { 106 | 107 | if(node.kind === Kind.DOCUMENT) { 108 | 109 | let baseMult = 1; 110 | 111 | const fragDefStore: Record = {}; 112 | 113 | //define cost of fragment spreads 114 | 115 | for(let i = 0; i < node.definitions.length; i++) { 116 | const def = node.definitions[i] as DefinitionNode; 117 | if(def.kind === 'FragmentDefinition'){ 118 | let selectionSet; 119 | const fragDef = def as FragmentDefinitionNode; 120 | const typeCondition = fragDef.typeCondition.name.value 121 | const type = this.schema.getType(typeCondition); 122 | selectionSet = fragDef.selectionSet; 123 | const fragDepthState = {exceeded: false, depth: this.depth}; 124 | const fragDepth = this.checkDepth(selectionSet, fragDepthState); 125 | if(fragDepthState.exceeded) { 126 | this.excessDepth = true; 127 | return; 128 | } 129 | const totalFragCost = this.resolveSelectionSet(selectionSet, type, schemaType, baseMult, [node], node, fragDefStore); 130 | fragDefStore[typeCondition] = {totalFragCost, fragDepth}; 131 | } 132 | } 133 | 134 | 135 | for(let i = 0; i < node.definitions.length; i++) { 136 | const def = node.definitions[i] as DefinitionNode; 137 | if(def.kind === 'OperationDefinition'){ 138 | let selectionSet; 139 | const operationDef = def as OperationDefinitionNode; 140 | const operationType = operationDef.operation; 141 | const rootType = this.schema.getQueryType(); 142 | if(operationType === 'query') selectionSet = operationDef.selectionSet; 143 | const queryDepthState = {exceeded: false, depth: this.depth}; 144 | const queryDepth = this.checkDepth(selectionSet, queryDepthState) 145 | if(queryDepthState.exceeded) { 146 | this.excessDepth = true; 147 | return; 148 | } 149 | this.depth = queryDepthState.depth; 150 | const totalCost = this.resolveSelectionSet(selectionSet, rootType, schemaType, baseMult, [node], node, fragDefStore) 151 | this.typeComplexity += totalCost; 152 | } 153 | } 154 | } 155 | }, 156 | })) 157 | } 158 | 159 | this.complexityScore = this.typeComplexity; 160 | 161 | return {complexityScore: this.complexityScore, typeComplexity: this.typeComplexity, excessDepth: this.excessDepth, depth: this.depth}; 162 | } 163 | 164 | //helper function recursively resolves the cost of each selection set 165 | private resolveSelectionSet(selectionSet: any, objectType: any, schemaType: TypeInfo, mult: number = 1, ancestors : any[], document: DocumentNode, fragDefs: Record) { 166 | 167 | let cost = 0; 168 | const fragStore: Record = {}; 169 | 170 | selectionSet.selections.forEach((selection: any) => { 171 | 172 | if(selection.kind === Kind.FRAGMENT_SPREAD) { 173 | let fragSpreadMult = mult; 174 | let fragSpreadCost = 0; 175 | const fragName = selection.name.value; 176 | //@ts-ignore 177 | const fragDef = document?.definitions.find(def => def.kind === 'FragmentDefinition' && def.name.value === fragName); 178 | //@ts-ignore 179 | if(!fragDef) return; 180 | //@ts-ignore 181 | const typeName = fragDef.typeCondition.name.value; 182 | if(fragDefs[typeName]) fragSpreadCost = fragDefs[typeName].totalFragCost; 183 | 184 | fragStore[fragName] = fragSpreadCost * fragSpreadMult; 185 | } 186 | 187 | if(selection.kind === Kind.INLINE_FRAGMENT) { 188 | const typeName = selection.typeCondition.name.value; 189 | const type = this.schema.getType(typeName); 190 | 191 | ancestors.push(selection) 192 | 193 | const fragCost = this.resolveSelectionSet(selection.selectionSet, type, schemaType, mult, ancestors, document, fragDefs); 194 | fragStore[typeName] = fragStore[typeName] || 0; 195 | fragStore[typeName] = fragCost; 196 | 197 | ancestors.pop() 198 | } 199 | 200 | if(selection.kind === Kind.FIELD) { 201 | const fieldName = selection.name.value; 202 | let checkSkip: Record | undefined; 203 | let skip: boolean = false; 204 | 205 | if(this.variables) { 206 | checkSkip = this.checkSkip(selection); 207 | } 208 | 209 | let newMult = mult; 210 | 211 | const fieldDef = objectType.getFields()[fieldName]; 212 | if(!fieldDef) return; 213 | const fieldType = fieldDef.type; 214 | const nullableType = getNullableType(fieldType); 215 | const unwrappedType = getNamedType(fieldType); 216 | const subSelection = selection.selectionSet 217 | const slicingArguments = this.parseSlicingArguments(selection) 218 | let argCosts; 219 | 220 | if(slicingArguments && slicingArguments.length) { 221 | argCosts = this.parseArgumentDirectives(fieldDef, slicingArguments) 222 | } 223 | 224 | if(checkSkip && (checkSkip.skip === true || checkSkip.include === false)) skip = true; 225 | const costDirective = this.parseDirectives(fieldDef, 0); 226 | if(argCosts && argCosts.length) cost += Number(argCosts[0].directiveValue) * mult 227 | 228 | if(skip === false) costDirective.costDirective ? cost += Number(costDirective.costDirective) * mult : cost += mult; 229 | if(isListType(nullableType)) costDirective.paginationLimit ? newMult *= costDirective.paginationLimit : newMult *= this.defaultPaginationLimit 230 | if(isListType(nullableType) && slicingArguments.length) slicingArguments[0].argumentValue ? newMult = mult * slicingArguments[0].argumentValue : newMult = newMult; 231 | 232 | 233 | 234 | if(subSelection && (isInterfaceType(unwrappedType) || isObjectType(unwrappedType)) || isUnionType(unwrappedType)) { 235 | const types = isInterfaceType(unwrappedType) || isUnionType(unwrappedType) ? this.schema.getPossibleTypes(unwrappedType) : [unwrappedType]; 236 | const store: Record = {}; 237 | ancestors.push(objectType); 238 | 239 | types.forEach(type => { 240 | store[type.name] = store[type.name] || 0; 241 | store[type.name] = Math.max(store[type.name], this.resolveSelectionSet(subSelection, type, schemaType, newMult, ancestors, document, fragDefs)); 242 | }) 243 | 244 | const maxInterface = Object.values(store).reduce((a, b) => Math.max(a, b)); 245 | cost += maxInterface; 246 | 247 | ancestors.pop(); 248 | } 249 | } 250 | }) 251 | 252 | if(Object.values(fragStore).length) cost += Object.values(fragStore).reduce((a, b) => Math.max(a, b)); 253 | 254 | return cost; 255 | } 256 | 257 | //helper recursively finds the depth of fragments/queries, aborts if depth exceeds preconfigured depth limit 258 | private checkDepth(selection: any, state: { exceeded: boolean, depth: number }, depth: number = 2, limit: number = this.defaultDepthLimit) { 259 | if (state.exceeded) { 260 | return; 261 | } 262 | 263 | if (depth > limit + 1) { 264 | state.depth = depth; 265 | state.exceeded = true; 266 | return; 267 | } 268 | 269 | const selectionSet = selection.selections; 270 | selectionSet.forEach((selection: any) => { 271 | if (selection.selectionSet) { 272 | state.depth = depth; 273 | this.checkDepth(selection.selectionSet, state, depth + 1, limit); 274 | } 275 | }); 276 | } 277 | 278 | //finds @skip/@include directives exposed by query and disregards fields as appropriate 279 | private checkSkip(selection: any) { 280 | let variables: Record = {}; 281 | 282 | if(!selection.directives.length) return; 283 | 284 | for (let i = 0; i < selection.directives.length; i++) { 285 | const directive = selection.directives[i]; 286 | const directiveName = directive.name.value; 287 | 288 | if(!this.allowedDirectives.includes(directiveName)) continue; 289 | 290 | const directiveArguments = directive.arguments; 291 | 292 | if(!directiveArguments.length) continue; 293 | if(directiveArguments[0].value.kind !== 'Variable') continue; 294 | 295 | const variable = directiveArguments[0].value.name.value; 296 | if(this.variables[variable] === undefined) { 297 | continue; 298 | } 299 | 300 | if(directiveName === 'skip') variables[directiveName] = this.variables[variable]; 301 | if(directiveName === 'include') variables[directiveName] = this.variables[variable]; 302 | } 303 | 304 | return variables; 305 | } 306 | 307 | //detects and retrieves slicing arguments (first, last etc.) 308 | private parseSlicingArguments(selection: any) { 309 | if(!selection.arguments) return; 310 | 311 | const argumentDirectives = selection.arguments.flatMap((arg: any) => { 312 | const argName = arg.name.value; 313 | let argValue = arg.value.value; 314 | if(arg.value.kind === 'Variable') { 315 | if(this.variables) argValue = this.variables[arg.value.name.value] 316 | } 317 | return { 318 | argumentName: argName, 319 | argumentValue: argValue 320 | }; 321 | }) 322 | 323 | return argumentDirectives.filter((arg: any) => { 324 | if(!this.slicingArgs.includes(arg.argumentName)) { 325 | return; 326 | } 327 | return arg; 328 | }) 329 | } 330 | 331 | //helper retrieves @cost directives applied to arguments 332 | private parseArgumentDirectives(fieldDef: GraphQLField, args: any[]) { 333 | if(!fieldDef.astNode?.arguments) return 334 | 335 | const argumentDirectives = fieldDef.astNode.arguments.flatMap((arg: any) => { 336 | const argName = arg.name.value; 337 | return arg.directives?.map((directive: any) => ({ 338 | argName, 339 | directiveName: directive.name.value, 340 | //@ts-ignore 341 | directiveValue: directive.arguments?.find(arg => arg.name.value === 'value')?.value.value, 342 | })); 343 | }); 344 | const argumentCosts = argumentDirectives.filter((directive: any, index) => (directive.directiveName === 'cost' && args[index].argumentName === directive.argName)); 345 | return argumentCosts; 346 | } 347 | 348 | //helper retrieves @cost directives applied to fields 349 | private parseDirectives(fieldDef: GraphQLField, baseVal: number) { 350 | if(!fieldDef.astNode?.directives) return {costDirective: baseVal, paginationLimit: null}; 351 | 352 | const directives = this.getDirectives(fieldDef.astNode.directives); 353 | 354 | if(!directives.length) return {costDirective: baseVal, paginationLimit: null}; 355 | 356 | const costPaginationDirectives = this.getCostDirectives(directives, baseVal); 357 | 358 | if(costPaginationDirectives?.costDirective) baseVal = costPaginationDirectives.costDirective; 359 | 360 | return {costDirective: baseVal, paginationLimit: costPaginationDirectives?.paginationLimit}; 361 | } 362 | 363 | //sub helper for parseDirectives 364 | private getDirectives(astNodeDirectives: readonly graphql.ConstDirectiveNode[]) { 365 | const directives: DirectivesInfo[] = astNodeDirectives.map(directives => ({ 366 | name: directives.name, 367 | arguments: directives.arguments as readonly graphql.ConstArgumentNode[] 368 | })) 369 | 370 | return directives; 371 | } 372 | 373 | //sub-helper for parseDirectives 374 | private getCostDirectives(directives: DirectivesInfo[], baseVal: number) { 375 | if(!directives.length) return 376 | 377 | let listLimit = 0; 378 | 379 | for(let i = 0; i < directives.length; i++) { 380 | const costPaginationDirectives: PaginationDirectives[] = directives[i].arguments?.map((arg: any) => ({ 381 | name: directives[i].name.value, 382 | value: arg.value 383 | })) 384 | 385 | costPaginationDirectives.forEach((directives: PaginationDirectives) => { 386 | if(directives.name === 'cost' && directives.value) baseVal = directives.value.value; 387 | if(directives.name === 'paginationLimit' && directives.value) listLimit = directives.value.value; 388 | }) 389 | } 390 | 391 | return {costDirective: baseVal, paginationLimit: listLimit} 392 | } 393 | 394 | } 395 | 396 | //end of class 397 | 398 | // helper function to send data to web-app 399 | const sendData = async (endpointData: any) => { 400 | try { 401 | const response = await fetch('https://gleiphql.azurewebsites.net/api/data', { 402 | method: 'POST', 403 | headers: { 404 | 'Content-Type': 'application/json', 405 | }, 406 | body: JSON.stringify(endpointData) 407 | }); 408 | const data = await response.json(); 409 | } 410 | catch { 411 | console.log('Unable to save to database') 412 | } 413 | } 414 | 415 | const expressRateLimiter = function (config: any) { 416 | 417 | let tokenBucket: TokenBucket = {}; 418 | return async (req: Request, res: Response, next: NextFunction) => { 419 | if(req.body.query) { 420 | const builtSchema = config.schema 421 | const parsedAst = parse(req.body.query) 422 | 423 | let variables; 424 | if(req.body.variables) variables = req.body.variables; 425 | 426 | let requestIP = req.ip 427 | 428 | // fixes format of ip addresses 429 | if (requestIP.includes('::ffff:')) { 430 | requestIP = requestIP.replace('::ffff:', ''); 431 | } 432 | 433 | const analysis = new ComplexityAnalysis(builtSchema, parsedAst, config, variables); 434 | 435 | const complexityScore = analysis.traverseAST(); 436 | 437 | console.log('This is the complexity score:', complexityScore); 438 | 439 | //returns error if complexity heuristic reads complexity score over limit 440 | res.locals.complexityScore = complexityScore; 441 | res.locals.complexityLimit = config.complexityLimit; 442 | 443 | // if the user wants to use redis, a redis client will be created and used as a cache 444 | if (config.redis === true) { 445 | await expressCache.redis(config, complexityScore.complexityScore, req, res, next) 446 | } 447 | // if the user does not want to use redis, the cache will be saved in the "tokenBucket" object 448 | else if (config.redis !== true) { 449 | tokenBucket = expressCache.nonRedis(config, complexityScore.complexityScore, tokenBucket, req) 450 | if (complexityScore.complexityScore >= tokenBucket[requestIP].tokens || complexityScore.excessDepth === true) { 451 | if (res.locals.gleiphqlData) { 452 | res.locals.gleiphqlData.blocked = true 453 | res.locals.gleiphqlData.complexityLimit = config.complexityLimit 454 | res.locals.gleiphqlData.complexityScore = complexityScore 455 | complexityScore.excessDepth === true ? res.locals.gleiphqlData.depth = null : res.locals.gleiphqlData.depth = complexityScore.depth 456 | sendData(res.locals.gleiphqlData) 457 | } 458 | const error = { 459 | errors: [ 460 | { 461 | message: `Token limit exceeded`, 462 | extensions: { 463 | cost: { 464 | requestedQueryCost: complexityScore.complexityScore, 465 | currentTokensAvailable: Number(tokenBucket[requestIP].tokens.toFixed(2)), 466 | maximumTokensAvailable: config.complexityLimit, 467 | depthLimitExceeded: complexityScore.excessDepth, 468 | queryDepthLimit: config.maxDepth, 469 | }, 470 | responseDetails: { 471 | status: 429, 472 | statusText: 'Too Many Requests', 473 | } 474 | } 475 | } 476 | ] 477 | } 478 | console.log('Complexity or depth of this query is too high'); 479 | res.status(429).json(error); 480 | return next(Error); 481 | } 482 | tokenBucket[requestIP].tokens -= complexityScore.complexityScore; 483 | if (res.locals.gleiphqlData) { 484 | res.locals.gleiphqlData.complexityLimit = config.complexityLimit 485 | res.locals.gleiphqlData.complexityScore = complexityScore 486 | res.locals.gleiphqlData.depth = complexityScore.depth 487 | sendData(res.locals.gleiphqlData) 488 | } 489 | } 490 | }; 491 | return next(); 492 | } 493 | } 494 | 495 | const apolloRateLimiter = (config: any) => { 496 | 497 | let tokenBucket: TokenBucket = {}; 498 | return { 499 | async requestDidStart(requestContext: any) { 500 | return { 501 | async didResolveOperation(requestContext: any) { 502 | if (requestContext.operationName !== 'IntrospectionQuery') { 503 | const variables = requestContext.variables; 504 | const builtSchema = requestContext.schema 505 | const parsedAst = requestContext.document 506 | config.requestContext = requestContext 507 | let requestIP = requestContext.contextValue.clientIP 508 | 509 | // fixes format of ip addresses 510 | if (requestIP.includes('::ffff:')) { 511 | requestIP = requestIP.replace('::ffff:', ''); 512 | } 513 | 514 | const analysis = new ComplexityAnalysis(builtSchema, parsedAst, config, variables); 515 | 516 | const complexityScore = analysis.traverseAST(); 517 | requestContext.contextValue.complexityScore = complexityScore 518 | requestContext.contextValue.complexityLimit = config.complexityLimit 519 | requestContext.contextValue.depth = {depth: complexityScore.depth, excessDepth: complexityScore.excessDepth} 520 | console.log('This is the complexity score:', complexityScore); 521 | 522 | // if the user wants to use redis, a redis client will be created and used as a cache 523 | if (config.redis === true) { 524 | await apolloCache.redis(config, complexityScore.complexityScore, requestContext) 525 | } 526 | // if the user does not want to use redis, the cache will be saved in the "tokenBucket" object 527 | else if (config.redis !== true) { 528 | tokenBucket = apolloCache.nonRedis(config, complexityScore.complexityScore, tokenBucket) 529 | 530 | if (complexityScore.complexityScore >= tokenBucket[requestIP].tokens || complexityScore.excessDepth === true) { 531 | requestContext.contextValue.blocked = true 532 | if (complexityScore.excessDepth === true) requestContext.contextValue.excessDepth = true 533 | console.log('Complexity or depth of this query is too high'); 534 | throw new GraphQLError('Complexity or depth of this query is too high', { 535 | extensions: { 536 | cost: { 537 | requestedQueryCost: complexityScore.complexityScore, 538 | currentTokensAvailable: Number(tokenBucket[requestIP].tokens.toFixed(2)), 539 | maximumTokensAvailable: config.complexityLimit, 540 | depthLimitExceeded: complexityScore.excessDepth, 541 | queryDepthLimit: config.maxDepth, 542 | } 543 | }, 544 | }); 545 | 546 | } 547 | tokenBucket[requestIP].tokens -= complexityScore.complexityScore; 548 | } 549 | } 550 | }, 551 | }; 552 | }, 553 | } 554 | }; 555 | 556 | const gleiphqlContext = async ({ req }: { req: Request }) => { 557 | const clientIP = 558 | req.headers['x-forwarded-for'] || // For reverse proxies 559 | req.socket.remoteAddress; 560 | return { clientIP }; 561 | } 562 | 563 | export { expressRateLimiter, apolloRateLimiter, gleiphqlContext } -------------------------------------------------------------------------------- /src/query-test/pm-test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildSchema, 3 | GraphQLSchema, 4 | parse, 5 | TypeInfo, 6 | DocumentNode 7 | } from 'graphql'; 8 | 9 | const testSDLPolymorphism2: string = ` 10 | directive @cost(value: Int) on FIELD_DEFINITION | ARGUMENT_DEFINITION 11 | directive @paginationLimit(value: Int) on FIELD_DEFINITION 12 | 13 | type Related { 14 | content: [Content!]! 15 | } 16 | 17 | interface Content { 18 | id: ID! 19 | title: String! 20 | related: Related 21 | } 22 | 23 | type Post implements Content { 24 | id: ID! @cost(value: 3) 25 | title: String! @cost(value: 4) 26 | body: String! @cost(value: 10) 27 | tags: [String!]! @cost(value: 5) 28 | related: Related 29 | } 30 | 31 | type Image implements Content { 32 | id: ID! @cost(value: 5) 33 | title: String! @cost(value: 6) 34 | uri: String! @cost(value: 2) 35 | related: Related 36 | } 37 | 38 | union UnionContent = Post | Image 39 | 40 | type Query { 41 | content: [Content] @paginationLimit(value: 10) 42 | posts: [Post] @cost(value: 3) @paginationLimit(value: 10) 43 | images: [Image] @cost(value: 5) @paginationLimit(value: 10) 44 | related: [Related] @paginationLimit(value: 10) 45 | unionContent: [UnionContent] @paginationLimit(value: 10) 46 | } 47 | ` 48 | 49 | 50 | const testSDL: string = ` 51 | directive @cost(value: Int) on FIELD_DEFINITION | ARGUMENT_DEFINITION 52 | directive @paginationLimit(value: Int) on FIELD_DEFINITION 53 | type Author { 54 | id: ID! @cost(value: 1) 55 | name: String @cost(value: 200) => typeInfo vs resolveInfo 56 | books: [Book] @cost(value: 3) 57 | } 58 | type Book { 59 | id: ID! @cost(value: 1) 60 | title: String @cost(value: 2) 61 | author: Author @cost(value: 3) 62 | } 63 | type Query { 64 | authors: [Author] @cost(value: 2) 65 | books(limit: Int @cost(value:10)): [Book] @cost(value: 2) @paginationLimit(value: 5) 66 | } 67 | `; 68 | 69 | const testSDLPolymorphism: string = ` 70 | directive @cost(value: Int) on FIELD_DEFINITION | ARGUMENT_DEFINITION 71 | directive @paginationLimit(value: Int) on FIELD_DEFINITION 72 | type Author { 73 | id: ID! @cost(value: 1) 74 | name: String @cost(value: 200) 75 | books: [Book] @cost(value: 3) 76 | } 77 | type Book { 78 | id: ID! @cost(value: 1) 79 | title: String @cost(value: 2) 80 | author: Author @cost(value: 3) 81 | } 82 | union SearchResult = Author | Book 83 | type Query { 84 | authors: [Author] @cost(value: 2) 85 | books(limit: Int @cost(value:10)): [Book] @cost(value: 2) @paginationLimit(value: 5) 86 | search(term: String): [SearchResult] @paginationLimit(value: 10) 87 | } 88 | `; 89 | 90 | const testQueryPolymorphism: string = ` 91 | query SearchQuery { 92 | search(term: "example") { 93 | ... on Author { 94 | id 95 | name 96 | } 97 | ... on Book { 98 | id 99 | title 100 | } 101 | } 102 | } 103 | `; 104 | 105 | const testQuery: string = ` 106 | query { 107 | books(limit: 4) { 108 | id 109 | title 110 | author { 111 | name 112 | } 113 | } 114 | } 115 | `; 116 | 117 | const testQueryFrag: string = ` 118 | query { 119 | ...BookFields 120 | } 121 | fragment BookFields on Query { 122 | books(limit: 4) { 123 | id 124 | title 125 | author { 126 | name 127 | } 128 | } 129 | } 130 | `; 131 | 132 | const builtSchema: GraphQLSchema = buildSchema(testSDLPolymorphism2); 133 | const parsedAst: DocumentNode = parse(testQueryPolymorphism); 134 | const schemaType: TypeInfo = new TypeInfo(builtSchema); 135 | const pmTEST = { 136 | builtSchema, 137 | schemaType 138 | } 139 | export default pmTEST; -------------------------------------------------------------------------------- /src/query-test/query-test.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | config(); 3 | import express from 'express'; 4 | import { loadSchema } from '@graphql-tools/load'; 5 | import { UrlLoader } from '@graphql-tools/url-loader'; 6 | import { createYoga } from 'graphql-yoga'; 7 | import { ApolloServer, BaseContext } from '@apollo/server'; 8 | import { startStandaloneServer } from '@apollo/server/standalone'; 9 | import pmTEST from './pm-test.js'; 10 | import { expressMiddleware } from '@apollo/server/express4'; 11 | import { expressRateLimiter, expressEndpointMonitor, apolloRateLimiter, apolloEndpointMonitor, gleiphqlContext } from '../index.js'; 12 | import { MonitorConfig, RateLimitConfig, ApolloConfig} from '../types'; 13 | 14 | const app = express(); 15 | const port = process.env.PORT || 4000; 16 | 17 | //loadSchema can be any public graphql endpoint 18 | const spaceXSchema = await loadSchema('https://spacex-production.up.railway.app/', { loaders: [new UrlLoader()] }); 19 | const swapiSchema = await loadSchema('https://swapi-graphql.netlify.app/.netlify/functions/index', { loaders: [new UrlLoader()] }); 20 | const countriesSchema = await loadSchema('https://countries.trevorblades.com/graphql', { loaders: [new UrlLoader()] }); 21 | 22 | const spacex = createYoga({ 23 | schema: spaceXSchema, 24 | graphiql: true, 25 | graphqlEndpoint: '/spacex', 26 | }); 27 | const swapi = createYoga({ 28 | schema: swapiSchema, 29 | graphiql: true, 30 | graphqlEndpoint: '/starwars', 31 | }); 32 | const countries = createYoga({ 33 | schema: countriesSchema, 34 | graphiql: true, 35 | graphqlEndpoint: '/countries', 36 | }); 37 | const pm = createYoga({ 38 | schema: pmTEST.builtSchema, 39 | graphiql: true, 40 | graphqlEndpoint: '/pmtest', 41 | }); 42 | 43 | 44 | 45 | const monitorConfig: MonitorConfig = { 46 | gleiphqlUsername: 'andrew@gmail.com', // these are not in a dotenv file for example purposes only 47 | gleiphqlPassword: 'password', // these are not in a dotenv file for example purposes only 48 | } 49 | 50 | const spacexConfig: RateLimitConfig = { 51 | complexityLimit: 3000, 52 | paginationLimit: 10, 53 | schema: spaceXSchema, 54 | refillTime: 300000, // 5 minutes 55 | refillAmount: 1000, 56 | redis: false, 57 | maxDepth: 5 58 | } 59 | 60 | const swapiConfig: RateLimitConfig = { 61 | complexityLimit: 1000, 62 | paginationLimit: 10, 63 | schema: swapiSchema, 64 | refillTime: 300000, // 5 minutes 65 | refillAmount: 1000, 66 | redis: false, 67 | maxDepth: 5, 68 | } 69 | 70 | const countriesConfig: RateLimitConfig = { 71 | complexityLimit: 3000, 72 | paginationLimit: 10, 73 | schema: countriesSchema, 74 | refillTime: 86400000, // 24 hours 75 | refillAmount: 3000, 76 | redis: false, 77 | maxDepth: 5 78 | } 79 | 80 | const pmConfig: RateLimitConfig = { 81 | complexityLimit: 3000, 82 | paginationLimit: 10, 83 | schema: pmTEST.builtSchema, 84 | refillTime: 300000, // 5 minutes 85 | refillAmount: 1000, 86 | redis: false, 87 | maxDepth: 2 88 | } 89 | 90 | const apolloConfig: ApolloConfig = { 91 | complexityLimit: 3000, 92 | paginationLimit: 10, 93 | refillTime: 300000, // 5 minutes 94 | refillAmount: 1000, 95 | redis: false, 96 | maxDepth: 4 97 | } 98 | 99 | app.use(express.json()); 100 | 101 | 102 | const apolloServer: ApolloServer = new ApolloServer({ 103 | schema: swapiSchema, 104 | plugins: [ 105 | apolloRateLimiter(apolloConfig), 106 | apolloEndpointMonitor(monitorConfig) 107 | ], 108 | }); 109 | await apolloServer.start(); 110 | // const { url } = await startStandaloneServer(apolloServer, { 111 | // context: async ({ req }) => { 112 | // const clientIP = 113 | // req.headers['x-forwarded-for'] || // For reverse proxies 114 | // req.socket.remoteAddress; 115 | // return { clientIP }; 116 | // }, 117 | // }); 118 | // console.log(`🚀 Server ready at ${url}`); 119 | 120 | app.use('/graphql', expressMiddleware(apolloServer, { 121 | context: gleiphqlContext 122 | })) 123 | app.use('/spacex', expressEndpointMonitor(monitorConfig), expressRateLimiter(spacexConfig), spacex); 124 | app.use('/starwars', expressEndpointMonitor(monitorConfig), expressRateLimiter(swapiConfig), swapi); 125 | app.use('/countries', expressRateLimiter(countriesConfig), expressEndpointMonitor(monitorConfig), countries); 126 | app.use('/pmtest', expressEndpointMonitor(monitorConfig), expressRateLimiter(pmConfig), pm); 127 | 128 | app.listen(port, () => { 129 | console.info(`Server is running on http://localhost:${port}/spacex http://localhost:${port}/starwars http://localhost:${port}/graphql`); 130 | }); 131 | -------------------------------------------------------------------------------- /src/query-test/testCase.ts: -------------------------------------------------------------------------------- 1 | 2 | const testSDL: string = ` 3 | directive @cost(value: Int) on FIELD_DEFINITION | ARGUMENT_DEFINITION 4 | directive @paginationLimit(value: Int) on FIELD_DEFINITION 5 | 6 | type Author { 7 | id: ID! @cost(value: 1) 8 | name: String @cost(value: 200) => typeInfo vs resolveInfo 9 | books: [Book] @cost(value: 3) 10 | } 11 | 12 | type Book { 13 | id: ID! @cost(value: 1) 14 | title: String @cost(value: 2) 15 | author: Author @cost(value: 3) 16 | } 17 | 18 | type Query { 19 | authors: [Author] @cost(value: 2) 20 | books(limit: Int @cost(value:10)): [Book] @cost(value: 2) @paginationLimit(value: 5) 21 | } 22 | `; 23 | 24 | const interfaceTestQuery1: string = ` 25 | query { 26 | search { 27 | ... on Author { 28 | id 29 | name 30 | } 31 | ... on Book { 32 | id 33 | name 34 | } 35 | } 36 | } 37 | `; 38 | 39 | const interfaceTestQuery2: string = ` 40 | query { 41 | search { 42 | ... on Searchable { 43 | id 44 | name 45 | } 46 | } 47 | } 48 | `; 49 | 50 | const testSDLPolymorphism: string = ` 51 | directive @cost(value: Int) on FIELD_DEFINITION | ARGUMENT_DEFINITION 52 | directive @paginationLimit(value: Int) on FIELD_DEFINITION 53 | interface Searchable { 54 | id: ID! 55 | name: String 56 | } 57 | type Author implements Searchable { 58 | id: ID! @cost(value: 1) 59 | name: String @cost(value: 200) 60 | books: [Book] @cost(value: 3) 61 | } 62 | type Book implements Searchable { 63 | id: ID! @cost(value: 1) 64 | name: String @cost(value: 2) 65 | author: Author @cost(value: 3) 66 | } 67 | union SearchResult = Author | Book 68 | type Query { 69 | authors: [Author] @cost(value: 2) 70 | books(limit: Int @cost(value:10)): [Book] @cost(value: 2) @paginationLimit(value: 5) 71 | search: [Searchable] 72 | } 73 | ` 74 | 75 | //doesn't work, I assume the isInterface behavior might be overwriting isList, causing the mults to not be properly conserved 76 | 77 | const testQueryPolymorphism3: string = ` 78 | query { 79 | content { 80 | ... on Post { 81 | id 82 | title 83 | body 84 | tags 85 | } 86 | 87 | ... on Image { 88 | id 89 | title 90 | uri 91 | } 92 | } 93 | } 94 | `; 95 | 96 | const testQueryPolymorphism4: string = ` 97 | fragment postFields on Post { 98 | id 99 | title 100 | body 101 | tags 102 | related { 103 | content { 104 | id 105 | title 106 | } 107 | } 108 | } 109 | fragment imageFields on Image { 110 | id 111 | title 112 | uri 113 | related { 114 | content { 115 | id 116 | title 117 | } 118 | } 119 | } 120 | 121 | query { 122 | content { 123 | ...postFields 124 | ...imageFields 125 | } 126 | } 127 | `; 128 | 129 | const testQueryPolymorphism: string = ` 130 | query SearchQuery { 131 | search(term: "example") { 132 | ... on Author { 133 | id 134 | name 135 | } 136 | ... on Book { 137 | id 138 | title 139 | } 140 | } 141 | } 142 | `; 143 | 144 | const testQuery: string = ` 145 | query { 146 | books(limit: 4) { 147 | id 148 | title 149 | author { 150 | name 151 | } 152 | } 153 | } 154 | `; 155 | 156 | const testQueryInlineFrag: string = ` 157 | query { 158 | books(limit: 4) { 159 | id 160 | title 161 | author { 162 | ... on Author { 163 | name 164 | } 165 | } 166 | } 167 | } 168 | `; 169 | 170 | const testQueryFrag: string = ` 171 | query { 172 | ...BookFields 173 | } 174 | fragment BookFields on Query { 175 | books(limit: 4) { 176 | id 177 | title 178 | author { 179 | name 180 | } 181 | } 182 | } 183 | `; 184 | 185 | const testQueryPolymorphism8: string = ` 186 | fragment contentFields on Content { 187 | id 188 | title 189 | ... on Post { 190 | body 191 | tags 192 | } 193 | ... on Image { 194 | uri 195 | } 196 | related { 197 | content { 198 | id 199 | title 200 | } 201 | } 202 | } 203 | query { 204 | content { 205 | ...contentFields 206 | } 207 | } 208 | `; 209 | 210 | const testQueryPolymorphism7: string = ` 211 | query { 212 | unionContent { 213 | ... on Post { 214 | id 215 | title 216 | body 217 | tags 218 | related { 219 | content { 220 | id 221 | title 222 | } 223 | } 224 | } 225 | ... on Image { 226 | id 227 | title 228 | uri 229 | related { 230 | content { 231 | id 232 | title 233 | } 234 | } 235 | } 236 | } 237 | } 238 | `; 239 | 240 | const testQueryBasic: string = ` 241 | query { 242 | posts { 243 | id 244 | title 245 | } 246 | } 247 | `; 248 | 249 | const testQueryNested: string =` 250 | query { 251 | posts { 252 | id 253 | title 254 | related { 255 | content { 256 | ... on Post { 257 | id 258 | title 259 | } 260 | } 261 | } 262 | } 263 | } 264 | `; 265 | 266 | const testQueryPolymorphism2: string = ` 267 | query { 268 | content { 269 | id 270 | title 271 | related { 272 | content { 273 | id 274 | } 275 | } 276 | } 277 | } 278 | `; 279 | 280 | const testQuery7: string = ` 281 | query { 282 | content { 283 | id 284 | title 285 | ... on Post { 286 | related { 287 | content { 288 | id 289 | title 290 | } 291 | } 292 | } 293 | ... on Image { 294 | uri 295 | } 296 | } 297 | } 298 | `; 299 | 300 | const testQueryPolymorphism6: string = ` 301 | fragment postFields on Post { 302 | id 303 | title 304 | body 305 | tags 306 | related { 307 | content { 308 | ... on Post { 309 | id 310 | title 311 | } 312 | ... on Image { 313 | id 314 | title 315 | } 316 | } 317 | } 318 | } 319 | fragment imageFields on Image { 320 | id 321 | title 322 | uri 323 | related { 324 | content { 325 | ... on Post { 326 | id 327 | title 328 | } 329 | ... on Image { 330 | id 331 | title 332 | } 333 | } 334 | } 335 | } 336 | query { 337 | content { 338 | ...postFields 339 | ...imageFields 340 | } 341 | } 342 | `; 343 | 344 | const testSDLPolymorphism2: string = ` 345 | directive @cost(value: Int) on FIELD_DEFINITION | ARGUMENT_DEFINITION 346 | directive @paginationLimit(value: Int) on FIELD_DEFINITION 347 | 348 | type Related { 349 | content: [Content!]! 350 | } 351 | 352 | interface Content { 353 | id: ID! 354 | title: String! 355 | related: Related 356 | } 357 | 358 | type Post implements Content { 359 | id: ID! @cost(value: 3) 360 | title: String! @cost(value: 4) 361 | body: String! @cost(value: 10) 362 | tags: [String!]! @cost(value: 5) 363 | related: Related 364 | } 365 | 366 | type Image implements Content { 367 | id: ID! @cost(value: 5) 368 | title: String! @cost(value: 6) 369 | uri: String! @cost(value: 2) 370 | related: Related 371 | } 372 | 373 | union UnionContent = Post | Image 374 | 375 | type Query { 376 | content: [Content] @paginationLimit(value: 10) 377 | posts: [Post] @cost(value: 3) @paginationLimit(value: 10) 378 | images: [Image] @cost(value: 5) @paginationLimit(value: 10) 379 | related: [Related] @paginationLimit(value: 10) 380 | unionContent: [UnionContent] @paginationLimit(value: 10) 381 | } 382 | `; 383 | 384 | const testQueryPolymorphism5: string = ` 385 | query { 386 | posts { 387 | id 388 | title 389 | related { 390 | content { 391 | ... on Image { 392 | id 393 | title 394 | related { 395 | content { 396 | ... on Post { 397 | id 398 | title 399 | related { 400 | content { 401 | ... on Post { 402 | id 403 | title 404 | } 405 | } 406 | } 407 | } 408 | } 409 | } 410 | } 411 | } 412 | } 413 | } 414 | images { 415 | id 416 | title 417 | related { 418 | content { 419 | ... on Post { 420 | id 421 | title 422 | related { 423 | content { 424 | ... on Post { 425 | id 426 | title 427 | related { 428 | content { 429 | ... on Post { 430 | id 431 | title 432 | } 433 | } 434 | } 435 | } 436 | } 437 | } 438 | } 439 | } 440 | } 441 | } 442 | } 443 | `; 444 | 445 | const testQuery6: string = ` 446 | query TestQuery1 { 447 | unionContent { 448 | ... on Post { 449 | id 450 | title 451 | related { 452 | content { 453 | id 454 | title 455 | } 456 | } 457 | } 458 | ... on Image { 459 | id 460 | title 461 | uri 462 | } 463 | } 464 | } 465 | `; 466 | 467 | const testQuery8: string = ` 468 | query { 469 | unionContent { 470 | ... on Post { 471 | id 472 | title 473 | } 474 | ... on Image { 475 | id 476 | title 477 | } 478 | } 479 | } 480 | `; 481 | 482 | const testQuery9: string = ` 483 | query { 484 | unionContent { 485 | ... on Post { 486 | id 487 | title 488 | related { 489 | content { 490 | id 491 | title 492 | } 493 | } 494 | } 495 | ... on Image { 496 | id 497 | title 498 | uri 499 | } 500 | } 501 | } 502 | `; 503 | 504 | const testQuery10: string = ` 505 | query { 506 | posts { 507 | id 508 | title 509 | body 510 | tags 511 | } 512 | } 513 | `; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | 3 | export interface TokenBucket { 4 | [key: string]: { 5 | tokens: number; 6 | lastRefillTime: number; 7 | }; 8 | }; 9 | 10 | export interface RateLimitConfig { 11 | complexityLimit: number, 12 | paginationLimit: number, 13 | schema: GraphQLSchema, 14 | refillTime: number, 15 | refillAmount: number, 16 | redis?: boolean, 17 | maxDepth: number 18 | }; 19 | 20 | export interface ApolloConfig { 21 | complexityLimit: number, 22 | paginationLimit: number, 23 | refillTime: number, 24 | refillAmount: number, 25 | redis?: boolean, 26 | maxDepth: number 27 | }; 28 | 29 | export interface MonitorConfig { 30 | gleiphqlUsername: string, 31 | gleiphqlPassword: string, 32 | }; 33 | 34 | export interface EndpointData { 35 | depth: number | null; 36 | ip: string; 37 | url: string; 38 | timestamp: string; 39 | objectTypes: any; 40 | queryString: string; 41 | complexityScore: number; 42 | blocked: boolean; 43 | complexityLimit: number; 44 | email: string; 45 | password: string; 46 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "esnext", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "dist", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | "declarationDir": "dist", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": [ 110 | "src/**/*.ts", 111 | "src/**/*.js", 112 | "test/**/*.ts", 113 | "test/**/*.js" 114 | ], 115 | "exclude": [ 116 | "node_modules", 117 | "**/*.spec.ts", 118 | "dist", 119 | ] 120 | } 121 | --------------------------------------------------------------------------------