diff --git a/.changesets/10894.md b/.changesets/10894.md new file mode 100644 index 000000000000..7ab04f310bde --- /dev/null +++ b/.changesets/10894.md @@ -0,0 +1,34 @@ +feat(trusted-docs): Allows useRedwoodTrustedDocuments to set more custom UsePersistedOperationsOptions (#10894) by @dthyresson + +Allows useRedwoodTrustedDocuments to set more custom UsePersistedOperationsOptions + +Allows the useRedwoodTrustedDocuments plugin to define: + +```ts + /** + * Whether to allow execution of arbitrary GraphQL operations aside from persisted operations. + */ + allowArbitraryOperations?: boolean | AllowArbitraryOperationsHandler; + /** + * The path to the persisted operation id + */ + extractPersistedOperationId?: ExtractPersistedOperationId; + + /** + * Whether to skip validation of the persisted operation + */ + skipDocumentValidation?: boolean; + + /** + * Custom errors to be thrown + */ + customErrors?: CustomPersistedQueryErrors; +``` + +This can let you override to allow certain ops or skip validation etc: + +> If you validate your persisted operations while building your store, we recommend to skip the validation on the server. So this will reduce the work done by the server and the latency of the requests. + +The allow authenticated request is still considered, but `allowArbitraryOperations` can override. + +Omitted `getPersistedOperation` as this extracts hash from store. diff --git a/docs/docs/graphql/trusted-documents.md b/docs/docs/graphql/trusted-documents.md index a53b9d07a59c..de4a41a5cdf2 100644 --- a/docs/docs/graphql/trusted-documents.md +++ b/docs/docs/graphql/trusted-documents.md @@ -118,6 +118,8 @@ As part of GraphQL type and codegen, the `trustedDocumentsStore` is created in ` This is the same information that is created in `web/src/graphql/persisted-documents.json` but wrapped in a `store` that can be easily imported and passed to the GraphQL Handler. +#### Store + To enable trusted documents, configure `trustedDocuments` with the store. ```ts title=api/src/functions/graphql.ts @@ -140,9 +142,23 @@ export const handler = createGraphQLHandler({ }) ``` -If you'd like to customize the message when a query is not permitted, you can set the `persistedQueryOnly` configuration setting in `customErrors`: +#### Disable + +You can disable the trustedDocuments `useRedwoodTrustedDocuments` plugin. The `store` is then optional. ``` + trustedDocuments: { + disabled: true, + } +``` + +#### Custom Errors + +The `persistedQueryOnly` error message defaults to `'Use Trusted Only!'`. + +If you'd like to customize the message when a query is not permitted, you can set the `persistedQueryOnly` configuration setting in `customErrors`: + +```ts trustedDocuments: { store, customErrors: { @@ -150,3 +166,60 @@ If you'd like to customize the message when a query is not permitted, you can se }, } ``` + +You can also define a function to returns a `GraphQLError`. This function has access to the `payload`. + +```ts + trustedDocuments: { + store, + customErrors: { + persistedQueryOnly: (payload) => { + console.log('payload', payload) + return new GraphQLError('Sorry!') + }, + }, + } +``` + +In addition to the `persistedQueryOnly` custom error option, you can define error message for: + + * `notFound` - Error to be thrown when the persisted operation is not found + * `keyNotFound` - Error to be thrown when the extraction of the persisted operation id failed + + +#### Skipping validation of persisted operations + +If you validate your persisted operations while building your store, we recommend to skip the validation on the server. So this will reduce the work done by the server and the latency of the requests. + +``` + trustedDocuments: { + store, + skipDocumentValidation: true, + } +``` + + +#### Allowing arbitrary GraphQL operations + +Sometimes it is handy to allow non-persisted operations aside from the persisted ones. E.g. you want to allow developers to execute arbitrary GraphQL operations on your production server. + +:::note Info +To support authentication, the `redwood.currentUser` query is always allowed. + +Even if you define `allowArbitraryOperations` the plugin will always check for this request, so you don't need to add this check to any custom logic. +::: + +This can be achieved using the `allowArbitraryOperations` option. + +:::warning Important +Override this option with caution! +::: + +For example, you can get a header from the request and allow: + +```ts +allowArbitraryOperations: (request) => { + return request.headers.get('x-allow-arbitrary-operations') === 'true' +} +``` + diff --git a/packages/graphql-server/src/plugins/useRedwoodTrustedDocuments.ts b/packages/graphql-server/src/plugins/useRedwoodTrustedDocuments.ts index fa18c83ff700..6a60f93f0416 100644 --- a/packages/graphql-server/src/plugins/useRedwoodTrustedDocuments.ts +++ b/packages/graphql-server/src/plugins/useRedwoodTrustedDocuments.ts @@ -1,14 +1,28 @@ import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations' -import type { CustomPersistedQueryErrors } from '@graphql-yoga/plugin-persisted-operations' +import type { UsePersistedOperationsOptions } from '@graphql-yoga/plugin-persisted-operations' import type { Plugin } from 'graphql-yoga' import type { RedwoodGraphQLContext } from '../types' -export type RedwoodTrustedDocumentOptions = { - store: Record +export type RedwoodTrustedDocumentOptions = Omit< + UsePersistedOperationsOptions, + 'getPersistedOperation' +> & { + /** + * Whether to disable the plugin + * @default false + */ disabled?: boolean - customErrors?: CustomPersistedQueryErrors -} + + /** + * The store to get the persisted operation hash from + * Required when the plugin is not disabled + */ + store?: Readonly> +} & ( + | { disabled: true; store?: Readonly> } + | { disabled?: false; store: Readonly> } + ) const REDWOOD__AUTH_GET_CURRENT_USER_QUERY = '{"query":"query __REDWOOD__AUTH_GET_CURRENT_USER { redwood { currentUser } }"}' @@ -44,14 +58,28 @@ export const useRedwoodTrustedDocuments = ( options: RedwoodTrustedDocumentOptions, ): Plugin => { return usePersistedOperations({ + ...options, customErrors: { persistedQueryOnly: 'Use Trusted Only!', ...options.customErrors, }, getPersistedOperation(sha256Hash: string) { - return options.store[sha256Hash] + return options.store ? options.store[sha256Hash] : null }, allowArbitraryOperations: async (request) => { + if (options.allowArbitraryOperations !== undefined) { + if (typeof options.allowArbitraryOperations === 'boolean') { + if (options.allowArbitraryOperations) { + return true + } + } + if (typeof options.allowArbitraryOperations === 'function') { + const result = await options.allowArbitraryOperations(request) + if (result === true) { + return true + } + } + } return allowRedwoodAuthCurrentUserQuery(request) }, })