Repository navigation
GraphQL queries bypass the per-route scope their REST equivalents require #618
Description
Background
Mercurius registers /graphql itself, with no Fastify route config:
await app.register(mercurius, {
schema, resolvers, graphiql: true,
path: '/graphql',
…
})So routeConfig.scope is undefined for every GraphQL request. The API-key hook still demands a key — /graphql is not marked public, so an unauthenticated request is rejected — but the scope loop has nothing to check:
const requiredScopes = routeConfig.scopes ?? (routeConfig.scope ? [routeConfig.scope] : [])
for (const scope of requiredScopes) { … } // empty, so never runsMeanwhile the REST routes serving the same data each declare one:
| GraphQL field | REST equivalent | Scope REST requires |
|---|---|---|
getPrice |
/price/:assetA/:assetB |
read:prices |
getBestRoute |
/price/:assetA/:assetB/route |
read:prices |
getPriceHistory |
/candles/:assetA/:assetB |
read:history |
oracle |
/compare/:asset |
read:prices |
screener |
/screener |
read:prices |
priceUpdated (subscription) |
/ws |
subscribe |
A key issued with only read:prices can read history through getPriceHistory; a key issued with no scopes at all can read everything. Scopes are the mechanism by which a key is restricted, so on this route they currently do nothing.
This is not new — it has been true since GraphQL was added — but it widens every time a resolver is added, most recently screener in #609.
What to build
Enforce the same scope per GraphQL field as its REST equivalent. The shape that avoids a second list to keep in sync is to put the requirement next to the resolver and check it in one place, rather than hand-maintaining a field→scope map somewhere else:
- a per-field scope declaration (a wrapper around each resolver, or a Mercurius
preExecutionhook that reads the selected fields), - resolved against
req.apiKey.scopeswithhasScopefromsrc/api/auth.ts— the same function the REST hook uses, so the two cannot disagree about what a scope means, - a
403 insufficient_scopeshaped like the REST one, surfaced throughformatGraphQLErrors.
A document selecting several fields should need every scope those fields require.
Acceptance criteria
- Each query, mutation and subscription field declares the scope it needs, matching its REST equivalent in the table above
- A key lacking the scope is refused with the same code REST uses, not a generic GraphQL error
- A multi-field document requires the union of its fields' scopes
- A test per field asserting refusal with an insufficient key and success with a sufficient one
- A test that fails if a new resolver is added with no scope declared — the registry and the schema must not drift
-
docs/graphql.mddocuments the scope each field needs
Notes
hasScope is in src/api/auth.ts; do not reimplement it. src/__tests__/graphqlScreener.test.ts and src/__tests__/graphqlPairs.test.ts show how to drive the GraphQL layer in tests.
The last criterion is the one that matters most — without it this comes back the next time someone adds a resolver.
Activity
sparkysolutions10-pixel commentedon Oct 24, 2026
Hi @Miracle656, I would like to work on this issue. Please assign it to me.