graphql-complexity-limit-tester
Crafts over-limit depth and complexity queries then asserts rejection before execution, verifying that graphql-depth-limit, graphql-cost-analysis, and graphql-armor (max-depth / cost-limit / max-tokens plugins) are actually enforced and not just configured. Use when auditing a GraphQL service for DoS exposure after depth or cost limits have been added as mitigations, or when adding tests that prove the limits in CI before a production deployment.
Works with
---
name: graphql-complexity-limit-tester
description: Crafts over-limit depth and complexity queries then asserts rejection before execution, verifying that graphql-depth-limit, graphql-cost-analysis, and graphql-armor (max-depth / cost-limit / max-tokens plugins) are actually enforced and not just configured. Use when auditing a GraphQL service for DoS exposure after depth or cost limits have been added as mitigations, or when adding tests that prove the limits in CI before a production deployment.
license: MIT
---
# graphql-complexity-limit-tester
## Overview
`introspection-attack-surface-reference` names query-depth limiting and
query-cost limiting as key DoS mitigations, but nothing in that catalog
executes a test. This skill closes that gap: it authors tests that send
an over-limit query and assert a validation error is returned _before_
any resolver runs.
Three library families are covered:
- `graphql-depth-limit` (npm: `graphql-depth-limit`, canonical repo
`github.com/stems/graphql-depth-limit`, per npm registry
[registry.npmjs.org/graphql-depth-limit](https://registry.npmjs.org/graphql-depth-limit/latest))
- `depthLimit(n)` validation rule, drop-in for Apollo or express-graphql.
- `graphql-cost-analysis` (npm: `graphql-cost-analysis`, repo
`github.com/pa-bru/graphql-cost-analysis`, per
[github.com/pa-bru/graphql-cost-analysis](https://github.com/pa-bru/graphql-cost-analysis))
- `costAnalysis({ maximumCost })` validation rule; queries above the
threshold are rejected.
- `@escape.tech/graphql-armor` (repo
`github.com/Escape-Technologies/graphql-armor`, docs
[escape.tech/graphql-armor/docs](https://escape.tech/graphql-armor/docs/getting-started))
- `max-depth` plugin: default `n = 6` (per
[escape.tech/graphql-armor/docs/plugins/max-depth](https://escape.tech/graphql-armor/docs/plugins/max-depth))
- `cost-limit` plugin: default `maxCost = 5000` (per
[escape.tech/graphql-armor/docs/plugins/cost-limit](https://escape.tech/graphql-armor/docs/plugins/cost-limit))
- `max-tokens` plugin: default `n = 1000` (per
[escape.tech/graphql-armor/docs/plugins/max-tokens](https://escape.tech/graphql-armor/docs/plugins/max-tokens))
Differentiation vs. `apollo-server-test`: that skill covers resolver
correctness + production-config gates (introspection, APQ,
hideSchemaDetails). This skill is scoped exclusively to depth/complexity
DoS tests - over-limit query construction, validation-layer rejection
assertion, and the cross-library matrix.
## Hard stop: no limit configured
If the server under test has no depth or complexity limit configured at
all (no `depthLimit`/`costAnalysis` validation rule, no
`ApolloArmor`/`EnvelopArmorPlugin` installed), halt immediately:
```
HALT: no depth/complexity limit configured.
Tests would pass vacuously - no enforcement exists to verify.
Install graphql-depth-limit, graphql-cost-analysis, or
@escape.tech/graphql-armor first, then re-run this skill.
```
Do not write tests that assert on a server with no limit - they will
produce false positives.
## Step 1 - Install
Choose the library that matches the project.
```bash
# graphql-depth-limit (express-graphql / Apollo standalone rule)
npm install --save-dev graphql-depth-limit
# graphql-cost-analysis (Apollo standalone rule)
npm install --save-dev graphql-cost-analysis
# graphql-armor (Apollo or Envelop/Yoga - bundles all plugins)
npm install --save @escape.tech/graphql-armor
```
## Step 2 - Identify the configured limit
Read the server setup to find the active limit value before crafting
queries. Common locations:
| Library | Where the limit lives |
|---|---|
| `graphql-depth-limit` | `depthLimit(N)` in `validationRules` array |
| `graphql-cost-analysis` | `costAnalysis({ maximumCost: N })` in `validationRules` |
| `graphql-armor` max-depth | `armor.protect()` or `new ApolloArmor({ maxDepth: { n: N } })` |
| `graphql-armor` cost-limit | `new ApolloArmor({ costLimit: { maxCost: N } })` |
If the limit is not explicit, use the library default: `n = 6` for
graphql-armor max-depth (per
[escape.tech/graphql-armor/docs/plugins/max-depth](https://escape.tech/graphql-armor/docs/plugins/max-depth)),
`maxCost = 5000` for graphql-armor cost-limit (per
[escape.tech/graphql-armor/docs/plugins/cost-limit](https://escape.tech/graphql-armor/docs/plugins/cost-limit)).
## Step 3 - Craft over-limit queries
### Depth query
Build a query whose nesting depth is `configuredLimit + 1`. If the
schema is `User { friends: [User] }` and the depth limit is 5:
```graphql
query DepthBust {
user { # depth 1
friends { # depth 2
friends { # depth 3
friends { # depth 4
friends { # depth 5
friends { id } # depth 6 -> over limit
}
}
}
}
}
}
```
For schemas without recursive types, chain any nested relationship
until the depth exceeds the limit.
### Cost/complexity query
For `graphql-cost-analysis`, assign costs via the schema directive
`@cost(complexity: N)` or via `costMap`. To construct an over-limit
query without schema changes, use a fan-out pattern whose calculated
cost exceeds `maximumCost`. Per
[github.com/pa-bru/graphql-cost-analysis](https://github.com/pa-bru/graphql-cost-analysis),
`defaultCost` applies to each field when no explicit cost is set; repeat
high-cost fields until the sum exceeds the threshold:
```graphql
query CostBust {
users { id name email createdAt updatedAt roles permissions profile
settings { notifications theme language timezone } }
}
```
For `graphql-armor` cost-limit, the default costs are: `objectCost = 2`,
`scalarCost = 1`, `depthCostFactor = 1.5` (per
[escape.tech/graphql-armor/docs/plugins/cost-limit](https://escape.tech/graphql-armor/docs/plugins/cost-limit)).
A query with cost above `maxCost = 5000` can be crafted by stacking
scalar fields at multiple nesting levels.
### Max-tokens query
For graphql-armor `max-tokens`, the default token limit is `n = 1000`
(per [escape.tech/graphql-armor/docs/plugins/max-tokens](https://escape.tech/graphql-armor/docs/plugins/max-tokens)).
Tokens include field names, arguments, braces, and directives. A query
with more than 1000 tokens can be constructed by repeating field
selections:
```graphql
query TokenBust {
users {
f1 f2 f3 f4 f5 f6 f7 f8 f9 f10
# ... repeat until token count > configured limit
}
}
```
## Step 4 - Write the tests
### graphql-depth-limit with Apollo Server
Per the `apollo-server-test` skill, use `executeOperation` for
in-process validation. The `depthLimit(n)` rule is passed as a
`validationRules` option (per npm registry description of
`graphql-depth-limit`).
```typescript
import { ApolloServer } from '@apollo/server';
import depthLimit from 'graphql-depth-limit';
import { typeDefs, resolvers } from './schema';
const DEPTH_LIMIT = 5;
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(DEPTH_LIMIT)],
});
test('rejects query exceeding depth limit', async () => {
const overDepthQuery = `
query DepthBust {
user { friends { friends { friends { friends { friends { id } } } } } }
}
`;
const resp = await server.executeOperation({ query: overDepthQuery });
if (resp.body.kind !== 'single') throw new Error('expected single');
// Validation errors are returned in errors[], not thrown
expect(resp.body.singleResult.errors).toBeDefined();
expect(resp.body.singleResult.data).toBeUndefined();
});
test('accepts query within depth limit', async () => {
const safeQuery = `
query SafeDepth {
user { friends { id } }
}
`;
const resp = await server.executeOperation({ query: safeQuery });
if (resp.body.kind !== 'single') throw new Error('expected single');
expect(resp.body.singleResult.errors).toBeUndefined();
});
```
### graphql-cost-analysis with Apollo Server
Per [github.com/pa-bru/graphql-cost-analysis](https://github.com/pa-bru/graphql-cost-analysis),
`costAnalysis` plugs into `validationRules` alongside any other rules:
```typescript
import { ApolloServer } from '@apollo/server';
import costAnalysis from 'graphql-cost-analysis';
const MAX_COST = 100;
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
costAnalysis({
maximumCost: MAX_COST,
defaultCost: 1,
variables: {},
}),
],
});
test('rejects query exceeding cost limit', async () => {
// Each field costs defaultCost=1; repeat fields to exceed MAX_COST
const fields = Array.from({ length: MAX_COST + 1 }, (_, i) => `field${i}`).join('\n ');
const overCostQuery = `query CostBust { users { ${fields} } }`;
const resp = await server.executeOperation({ query: overCostQuery });
if (resp.body.kind !== 'single') throw new Error('expected single');
expect(resp.body.singleResult.errors).toBeDefined();
});
```
The `createError(maximumCost, cost)` option (per
[github.com/pa-bru/graphql-cost-analysis](https://github.com/pa-bru/graphql-cost-analysis))
lets you assert on a custom error message if the project overrides the
default error format.
### graphql-armor (Apollo) - depth + cost + tokens
Per [escape.tech/graphql-armor/docs/getting-started](https://escape.tech/graphql-armor/docs/getting-started),
`ApolloArmor` spreads protection options into the server constructor:
```typescript
import { ApolloServer } from '@apollo/server';
import { ApolloArmor } from '@escape.tech/graphql-armor';
const armor = new ApolloArmor({
maxDepth: { n: 4 }, // override default 6
costLimit: { maxCost: 200 }, // override default 5000
maxTokens: { n: 50 }, // override default 1000
});
const server = new ApolloServer({
typeDefs,
resolvers,
...armor.protect(),
});
test('graphql-armor rejects over-depth query', async () => {
const query = `{ a { b { c { d { e { id } } } } } }`; // depth 6 > limit 4
const resp = await server.executeOperation({ query });
if (resp.body.kind !== 'single') throw new Error('expected single');
expect(resp.body.singleResult.errors).toBeDefined();
// With exposeLimits: true (default), error includes limit detail
// With exposeLimits: false, message is 'Query validation error.'
// Per escape.tech/graphql-armor/docs/plugins/max-depth
});
test('graphql-armor rejects over-cost query', async () => {
// objectCost=2, scalarCost=1, depthCostFactor=1.5 (defaults per
// escape.tech/graphql-armor/docs/plugins/cost-limit)
// Stack fields so calculated cost > maxCost=200
const query = `{ users { id name email createdAt updatedAt
profile { bio avatar roles permissions settings { a b c d e } } } }`;
const resp = await server.executeOperation({ query });
if (resp.body.kind !== 'single') throw new Error('expected single');
expect(resp.body.singleResult.errors).toBeDefined();
});
test('graphql-armor rejects over-token query', async () => {
// n=50 tokens; build a query with more than 50 tokens
const fields = Array.from({ length: 60 }, (_, i) => `f${i}`).join(' ');
const query = `{ users { ${fields} } }`;
const resp = await server.executeOperation({ query });
if (resp.body.kind !== 'single') throw new Error('expected single');
expect(resp.body.singleResult.errors).toBeDefined();
});
```
### graphql-armor (Envelop / GraphQL Yoga)
Per [escape.tech/graphql-armor/docs/getting-started](https://escape.tech/graphql-armor/docs/getting-started):
```typescript
import { envelop } from '@envelop/core';
import { EnvelopArmorPlugin } from '@escape.tech/graphql-armor';
const getEnveloped = envelop({
plugins: [
EnvelopArmorPlugin({
maxDepth: { n: 4 },
costLimit: { maxCost: 200 },
maxTokens: { n: 50 },
}),
],
});
```
Test via the Yoga HTTP layer using supertest (same pattern as
`apollo-server-test`).
## Step 5 - Assert rejection happens before execution
Confirm limits are enforced at validation, not resolver time. One way:
instrument a resolver with a side-effect counter and assert it was never
called on an over-limit query:
```typescript
let resolverCallCount = 0;
const server = new ApolloServer({
typeDefs,
resolvers: {
Query: {
users: () => {
resolverCallCount++;
return [];
},
},
},
validationRules: [depthLimit(3)],
});
test('resolver never called on over-limit query', async () => {
resolverCallCount = 0;
const resp = await server.executeOperation({
query: '{ users { friends { friends { friends { id } } } } }',
});
if (resp.body.kind !== 'single') throw new Error('expected single');
expect(resp.body.singleResult.errors).toBeDefined();
expect(resolverCallCount).toBe(0); // validation short-circuits execution
});
```
## Running
```bash
npm test # jest / vitest discover *.test.ts
npx jest --testPathPattern complexity -t "limit"
```
Run against the production configuration. Tests that pass in
`NODE_ENV=test` but fail in `NODE_ENV=production` (or vice versa) signal
a configuration drift problem. See the CI note in `apollo-server-test`.
## Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Writing depth tests without checking the actual configured limit | Query may be under-limit; test passes vacuously | Read `validationRules` / `ApolloArmor` config first |
| Testing at depth = limit (not limit+1) | Boundary is ambiguous; limit is exclusive or inclusive depending on library | Use limit+1 to be unambiguous |
| Asserting `errors[0].message` string exactly | Error text includes dynamic limit values; brittle | Assert `errors` is defined; check `extensions.code` if set |
| Skipping the resolver-call assertion | A misconfigured rule may reach resolvers silently | Instrument resolvers to confirm validation short-circuits |
| Using `depthLimit` and `costAnalysis` together without verifying precedence | First rule to reject wins; a low depth limit may mask cost-limit tests | Test each rule independently with the other absent |
| Assuming graphql-armor defaults when the project overrides them | Wrong threshold = vacuous pass | Always read the actual `ApolloArmor(...)` / `EnvelopArmorPlugin(...)` call |
## Limitations
- **Schema-dependent query construction.** Over-limit queries require
real field names from the schema under test. This skill provides
pattern templates - adapt them to the actual type graph.
- **`graphql-depth-limit` does not cover cost/fan-out attacks.** Depth 2
with 1000 siblings is not blocked. Pair with a cost rule.
- **`graphql-cost-analysis` is not actively maintained.** Check the
project's dependency health before adopting; `@escape.tech/graphql-armor`
cost-limit is the actively maintained alternative.
- **`max-tokens` counts tokens in the document AST, not resolver calls.**
It prevents parsing overhead but not algorithmic fan-out in resolvers.
Pair with cost-limit for full coverage.
- **`executeOperation` tests do not cover HTTP-level rate limiting.**
Network-layer limits (nginx, API gateway) need HTTP integration tests.
## References
- `graphql-depth-limit` (npm: `graphql-depth-limit`, canonical repo
`github.com/stems/graphql-depth-limit`):
[registry.npmjs.org/graphql-depth-limit](https://registry.npmjs.org/graphql-depth-limit/latest)
- `graphql-cost-analysis` (npm: `graphql-cost-analysis`):
[github.com/pa-bru/graphql-cost-analysis](https://github.com/pa-bru/graphql-cost-analysis)
- `@escape.tech/graphql-armor` (repo `github.com/Escape-Technologies/graphql-armor`):
[escape.tech/graphql-armor/docs/getting-started](https://escape.tech/graphql-armor/docs/getting-started)
- max-depth defaults: [escape.tech/graphql-armor/docs/plugins/max-depth](https://escape.tech/graphql-armor/docs/plugins/max-depth)
- cost-limit defaults: [escape.tech/graphql-armor/docs/plugins/cost-limit](https://escape.tech/graphql-armor/docs/plugins/cost-limit)
- max-tokens defaults: [escape.tech/graphql-armor/docs/plugins/max-tokens](https://escape.tech/graphql-armor/docs/plugins/max-tokens)
- Apollo Server `executeOperation` and `validationRules`:
[apollographql.com/docs/apollo-server/testing/testing](https://www.apollographql.com/docs/apollo-server/testing/testing)
- Attack surface context:
[`introspection-attack-surface-reference`](../introspection-attack-surface-reference/SKILL.md)
- Apollo testing patterns:
[`apollo-server-test`](../apollo-server-test/SKILL.md)More API Design skills
lark-event
larksuite/cli
Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses.
lark-contact
larksuite/cli
飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名,以及按关键词搜索当前用户可见的机器人 / 智能体(agent)。当用户提到一个名字要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。
lark-openapi-explorer
larksuite/cli
飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。

