diff --git a/.changeset/graphql-yoga-2231-dependencies.md b/.changeset/graphql-yoga-2231-dependencies.md new file mode 100644 index 0000000000..c58041305f --- /dev/null +++ b/.changeset/graphql-yoga-2231-dependencies.md @@ -0,0 +1,6 @@ +--- +'graphql-yoga': patch +--- +dependencies updates: + - Updated dependency [`@envelop/parser-cache@^5.0.4` ↗︎](https://www.npmjs.com/package/@envelop/parser-cache/v/5.0.4) (from `5.0.4`, in `dependencies`) + - Updated dependency [`@envelop/validation-cache@^5.0.5` ↗︎](https://www.npmjs.com/package/@envelop/validation-cache/v/5.0.5) (from `5.0.4`, in `dependencies`) diff --git a/package.json b/package.json index 795153cf5a..1b6aef4424 100644 --- a/package.json +++ b/package.json @@ -99,17 +99,17 @@ "weak-napi": "2.0.2", "wrangler": "2.6.1" }, - "resolutions": { - "graphql": "16.6.0", - "@envelop/core": "3.0.4", - "@changesets/assemble-release-plan": "5.2.1" - }, "pnpm": { "patchedDependencies": { "@changesets/assemble-release-plan@5.2.1": "patches/@changesets__assemble-release-plan@5.2.1.patch", "@graphiql/react@0.13.3": "patches/@graphiql__react@0.13.3.patch", "formdata-node@4.4.1": "patches/formdata-node@4.4.1.patch", "nextra-theme-docs@2.0.0-beta.43": "patches/nextra-theme-docs@2.0.0-beta.43.patch" + }, + "overrides": { + "graphql": "16.6.0", + "@envelop/core": "3.0.4", + "@changesets/assemble-release-plan": "5.2.1" } } } diff --git a/packages/graphql-yoga/package.json b/packages/graphql-yoga/package.json index 2a16c88fff..52612c4ea8 100644 --- a/packages/graphql-yoga/package.json +++ b/packages/graphql-yoga/package.json @@ -50,8 +50,8 @@ }, "dependencies": { "@envelop/core": "3.0.4", - "@envelop/parser-cache": "5.0.4", - "@envelop/validation-cache": "5.0.4", + "@envelop/parser-cache": "^5.0.4", + "@envelop/validation-cache": "^5.0.5", "@graphql-tools/executor": "0.0.9", "@graphql-tools/schema": "^9.0.0", "@graphql-tools/utils": "^9.0.1", diff --git a/packages/plugins/disable-introspection/__tests__/disable-introspection.spec.ts b/packages/plugins/disable-introspection/__tests__/disable-introspection.spec.ts new file mode 100644 index 0000000000..f6b8cabd78 --- /dev/null +++ b/packages/plugins/disable-introspection/__tests__/disable-introspection.spec.ts @@ -0,0 +1,108 @@ +import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection' +import { createYoga, createSchema } from 'graphql-yoga' + +describe('disable introspection', () => { + test('can disable introspection', async () => { + const schema = createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + _: Boolean + } + `, + }) + + const yoga = createYoga({ schema, plugins: [useDisableIntrospection()] }) + + const response = await yoga.fetch('http://yoga/graphql', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ query: `{ __schema { types { name } } }` }), + }) + + expect(response.status).toEqual(200) + const result = await response.json() + expect(result.data).toEqual(undefined) + expect(result.errors).toHaveLength(2) + }) + + test('can disable introspection conditionally', async () => { + const schema = createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + _: Boolean + } + `, + }) + + const yoga = createYoga({ + schema, + plugins: [ + useDisableIntrospection({ + isDisabled: () => true, + }), + ], + }) + + const response = await yoga.fetch('http://yoga/graphql', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ query: `{ __schema { types { name } } }` }), + }) + + expect(response.status).toEqual(200) + const result = await response.json() + expect(result.data).toEqual(undefined) + expect(result.errors).toHaveLength(2) + }) + + test('can disable introspection based on headers', async () => { + const schema = createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + _: Boolean + } + `, + }) + + const yoga = createYoga({ + schema, + plugins: [ + useDisableIntrospection({ + isDisabled: (request) => + request.headers.get('x-disable-introspection') === '1', + }), + ], + // uncomment this and the tests will pass + // validationCache: false, + }) + + // First request uses the header to disable introspection + let response = await yoga.fetch('http://yoga/graphql', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-disable-introspection': '1', + }, + body: JSON.stringify({ query: `{ __schema { types { name } } }` }), + }) + + expect(response.status).toEqual(200) + let result = await response.json() + expect(result.data).toEqual(undefined) + expect(result.errors).toHaveLength(2) + + // Seconds request does not disable introspection + response = await yoga.fetch('http://yoga/graphql', { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ query: `{ __schema { types { name } } }` }), + }) + + expect(response.status).toEqual(200) + result = await response.json() + expect(result.data).toBeDefined() + expect(result.errors).toBeUndefined() + }) +}) diff --git a/packages/plugins/disable-introspection/package.json b/packages/plugins/disable-introspection/package.json new file mode 100644 index 0000000000..1cb133d7f2 --- /dev/null +++ b/packages/plugins/disable-introspection/package.json @@ -0,0 +1,50 @@ +{ + "name": "@graphql-yoga/plugin-disable-introspection", + "version": "0.0.0", + "description": "Disable Introspection plugin for GraphQL Yoga.", + "repository": { + "type": "git", + "url": "https://github.com/dotansimha/graphql-yoga.git", + "directory": "packages/plugins/disable-introspection" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "scripts": { + "check": "tsc --pretty --noEmit" + }, + "author": "Laurin Quast ", + "license": "MIT", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "peerDependencies": { + "graphql-yoga": "^3.1.1", + "graphql": "^15.2.0 || ^16.0.0" + }, + "devDependencies": { + "graphql-yoga": "workspace:*" + }, + "type": "module" +} diff --git a/packages/plugins/disable-introspection/src/index.ts b/packages/plugins/disable-introspection/src/index.ts new file mode 100644 index 0000000000..0a96d7ea8e --- /dev/null +++ b/packages/plugins/disable-introspection/src/index.ts @@ -0,0 +1,27 @@ +import type { Plugin, PromiseOrValue } from 'graphql-yoga' +import { NoSchemaIntrospectionCustomRule } from 'graphql' + +type UseDisableIntrospectionArgs = { + isDisabled?: (request: Request) => PromiseOrValue +} + +const store = new WeakMap() + +export const useDisableIntrospection = ( + props?: UseDisableIntrospectionArgs, +): Plugin => { + return { + async onRequest({ request }) { + const isDisabled = props?.isDisabled + ? await props.isDisabled(request) + : true + store.set(request, isDisabled) + }, + onValidate({ addValidationRule, context }) { + const isDisabled = store.get(context.request) ?? true + if (isDisabled) { + addValidationRule(NoSchemaIntrospectionCustomRule) + } + }, + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd68bdad4a..edae890158 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -869,8 +869,8 @@ importers: '@envelop/core': 3.0.4 '@envelop/disable-introspection': 4.0.4 '@envelop/live-query': 5.0.4 - '@envelop/parser-cache': 5.0.4 - '@envelop/validation-cache': 5.0.4 + '@envelop/parser-cache': ^5.0.4 + '@envelop/validation-cache': ^5.0.5 '@graphql-tools/executor': 0.0.9 '@graphql-tools/schema': ^9.0.0 '@graphql-tools/utils': ^9.0.1 @@ -892,7 +892,7 @@ importers: dependencies: '@envelop/core': 3.0.4 '@envelop/parser-cache': 5.0.4_a6sekiasy2tqr6d5gj7n2wtjli - '@envelop/validation-cache': 5.0.4_a6sekiasy2tqr6d5gj7n2wtjli + '@envelop/validation-cache': 5.0.5_a6sekiasy2tqr6d5gj7n2wtjli '@graphql-tools/executor': 0.0.9_graphql@16.6.0 '@graphql-tools/schema': 9.0.4_graphql@16.6.0 '@graphql-tools/utils': 9.0.1_graphql@16.6.0 @@ -961,6 +961,13 @@ importers: '@whatwg-node/fetch': 0.5.3 publishDirectory: dist + packages/plugins/disable-introspection: + specifiers: + graphql-yoga: workspace:* + devDependencies: + graphql-yoga: link:../../graphql-yoga + publishDirectory: dist + packages/plugins/persisted-operations: specifiers: '@types/lru-cache': 7.10.9 @@ -4209,8 +4216,8 @@ packages: dependencies: tslib: 2.4.1 - /@envelop/validation-cache/5.0.4_a6sekiasy2tqr6d5gj7n2wtjli: - resolution: {integrity: sha512-7b4BWtNMxSdXspwzFN2qmkEaaHfmuDz60uMlVFaMN4nA1Vc5duAV7GQWfAKl56VoePU6UwQ0i49Dm/plJfwxIQ==} + /@envelop/validation-cache/5.0.5_a6sekiasy2tqr6d5gj7n2wtjli: + resolution: {integrity: sha512-69sq5H7hvxE+7VV60i0bgnOiV1PX9GEJHKrBrVvyEZAXqYojKO3DP9jnLGryiPgVaBjN5yw12ge0l0s2gXbolQ==} peerDependencies: '@envelop/core': ^3.0.4 graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 diff --git a/website/src/pages/docs/features/introspection.mdx b/website/src/pages/docs/features/introspection.mdx index 5466a72079..931009b431 100644 --- a/website/src/pages/docs/features/introspection.mdx +++ b/website/src/pages/docs/features/introspection.mdx @@ -20,7 +20,7 @@ GraphQL schema introspection is also a feature that allows clients to ask a Grap ```ts "Disabling GraphQL schema introspection with a plugin" {7} import { createYoga } from 'graphql-yoga' -import { useDisableIntrospection } from '@envelop/disable-introspection' +import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection' // Provide your schema const yoga = createYoga({ @@ -34,6 +34,33 @@ server.listen(4000, () => { }) ``` +## Disable Introspection based on the GraphQL Request + +Somnetimes you want to allow introspectition for certain users. +You can access the `Request` object and determine based on that whether introspection should be enabled or not. +E.g. you can check the headers. + +```ts "Disabling GraphQL schema introspection conditionally" {7} +import { createYoga } from 'graphql-yoga' +import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection' + +// Provide your schema +const yoga = createYoga({ + graphiql: false, + plugins: [ + useDisableIntrospection({ + isDisabled: (request) => + request.headers.get('x-allow-introspection') !== 'secret-access-key' + }) + ] +}) + +const server = createServer(yoga) +server.listen(4000, () => { + console.info('Server is running on http://localhost:4000/graphql') +}) +``` + ## Disabling Field Suggestions When executing invalid GraphQL operation the GraphQL engine will try to construct smart suggestions that hint typos in the executed GraphQL document.