Skip to content

P0: Add GraphQL query-cost controls via graphql-armor (#164)#183

Open
dkijania wants to merge 1 commit into
mainfrom
feat/graphql-armor
Open

P0: Add GraphQL query-cost controls via graphql-armor (#164)#183
dkijania wants to merge 1 commit into
mainfrom
feat/graphql-armor

Conversation

@dkijania

Copy link
Copy Markdown
Contributor

What & why

Part of the production-readiness epic (#163). Closes #164.

The public GraphQL endpoint had no query-cost controls. The list fields (events, actions, blocks) return unbounded lists, so a deeply-nested or heavily-aliased query is a trivial DoS against the backing Postgres — it can be made arbitrarily expensive before any limit applies. This is the highest-impact P0: the "crafted query = outage" mitigation.

Changes

  • New src/server/graphql-armor.ts — builds graphql-armor validation plugins from conservative, env-tunable limits (isolated and unit-testable).
  • buildPlugins runs them ahead of execution, so abusive shapes are rejected during validation.
Env var Default Limit
GRAPHQL_MAX_DEPTH 10 Selection-set nesting depth
GRAPHQL_MAX_ALIASES 15 Aliases per operation
GRAPHQL_MAX_TOKENS 1000 Lexical tokens per document
GRAPHQL_MAX_COST 5000 Depth/field cost heuristic
  • Field suggestions are always blocked so errors don't leak schema shape (complements useDisableIntrospection). Introspection is ignored by the depth/cost rules so GraphiQL still works when explicitly enabled.
  • The deepest query this API legitimately serves is ~5 levels — comfortably within the defaults. Malformed env values fall back to the safe default rather than disabling a protection.

Why the individual plugins, not the meta package

@escape.tech/graphql-armor (meta) requires @envelop/core v5, but this stack is pinned to v4 (Yoga 4). The individual @escape.tech/graphql-armor-* sub-plugins have no conflicting peer deps and work on the v4 stack. Switching to the meta integration can follow the Yoga 5 upgrade (#176).

Testing

  • npm run build — clean
  • npm run test:unit — all pass, including an end-to-end test through Yoga asserting an over-depth query is rejected before execution, plus config parsing/fallback tests
  • npm run lint — clean
  • npx prettier --debug-check . — exit 0

🤖 Generated with Claude Code

The public GraphQL endpoint had no query-cost controls. The list fields
(`events`, `actions`, `blocks`) return unbounded lists, so a deeply-nested or
heavily-aliased query is a trivial denial-of-service against the backing
Postgres — it can be made arbitrarily expensive before any limit applies.

Add graphql-armor validation plugins, wired ahead of execution in
`buildPlugins`, with conservative, env-tunable limits:

- GRAPHQL_MAX_DEPTH   (selection-set nesting, default 10)
- GRAPHQL_MAX_ALIASES (aliases per operation, default 15)
- GRAPHQL_MAX_TOKENS  (lexical tokens per document, default 1000)
- GRAPHQL_MAX_COST    (depth/field cost heuristic, default 5000)

Field suggestions are always blocked so error messages don't leak schema
shape (complementing `useDisableIntrospection`); introspection is ignored by
the depth/cost rules so GraphiQL still works when explicitly enabled. The
deepest query this API legitimately serves is ~5 levels, well within the
defaults. Malformed env values fall back to the safe default rather than
disabling a protection.

The individual `@escape.tech/graphql-armor-*` plugins are used (not the meta
package) because the meta package requires @envelop/core v5 while this stack is
pinned to v4 (Yoga 4); the sub-plugins have no conflicting peer deps. The full
graphql-armor meta integration can follow the Yoga 5 upgrade (#176).

Unit tests cover config parsing/fallbacks and prove end-to-end through Yoga
that an over-depth query is rejected before execution. Docs, env example, and
env type declarations updated.

Closes #164.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSuak9smCHbp4N17xjjLF6
@dkijania dkijania added production-readiness Work toward making the API production-ready / publicly available P0 Blocker for public availability labels Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P0 Blocker for public availability production-readiness Work toward making the API production-ready / publicly available

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P0: Add GraphQL query-cost controls (depth / alias / complexity limits)

1 participant