import { pluginName , getStoreKey , signatureForLogging } from './common' ; import type { ApolloServerPlugin , GraphQLServiceContext , GraphQLRequestListener , GraphQLRequestContext , GraphQLServerListener , } from 'apollo-server-plugin-base' ; import { operationHash as operationSignature , defaultOperationRegistrySignature as defaultOperationRegistryNormalization , } from 'apollo-graphql' ; import { ForbiddenError , ApolloError } from 'apollo-server-errors' ; import Agent from './agent' ; import { InMemoryLRUCache } from 'apollo-server-caching' ; import loglevel from 'loglevel' ; import type { fetch } from 'apollo-server-env' ; type ForbidUnregisteredOperationsPredicate = ( requestContext : GraphQLRequestContext ) => boolean ; export interface OperationRegistryRequestContext { signature : string ; normalizedDocument : string ; } export interface Operation { signature : string ; document : string ; } export interface OperationManifest { version : number ; operations : Array < Operation > ; } export interface Options { debug ? : boolean ; fetcher ? : typeof fetch ; forbidUnregisteredOperations ? : boolean | ForbidUnregisteredOperationsPredicate ; dryRun ? : boolean ; onUnregisteredOperation ? : ( requestContext : GraphQLRequestContext , operationRegistryRequestContext : OperationRegistryRequestContext ) => void ; onForbiddenOperation ? : ( requestContext : GraphQLRequestContext , operationRegistryRequestContext : OperationRegistryRequestContext ) => void ; } export default function plugin ( options = Object . create ( null ) ) { let agent ; let store ; const logger = loglevel . getLogger ( `apollo-server:${ pluginName }` ) ; const dryRunPrefix = '[DRYRUN]' ; if ( options . debug === true ) { logger . enableAll ( ) ; } if ( options . dryRun === true && options . debug !== false ) { logger . enableAll ( ) ; logger . debug ( `${ dryRunPrefix } Operation registry logging enabled because options.dryRun is true.` ) ; if ( options . forbidUnregisteredOperations ) { logger . info ( `${ dryRunPrefix } Allowing all operations since options.dryRun is true. \
Operations will still be reported to Apollo trace warehouse as forbidden \
for observability purposes, but all operations will be permitted.` ) ; } } Object . freeze ( options ) ; return ( ) : ApolloServerPlugin => ( { async serverWillStart ( { apollo , } ) { logger . debug ( 'Initializing operation registry plugin.' ) ; const { graphRef , keyHash } = apollo ; if ( ! keyHash ) { const messageApolloConfigurationRequired = 'The Apollo API key must be set to use the operation registry.' ; throw new Error ( `${ pluginName }: ${ messageApolloConfigurationRequired }` ) ; } if ( ! graphRef ) { const messageApolloConfigurationRequired = 'APOLLO_GRAPH_REF must be set to use the operation registry.' ; throw new Error ( `${ pluginName }: ${ messageApolloConfigurationRequired }` ) ; } logger . debug ( `Operation registry is configured for '${ graphRef }'.` ) ; store = new InMemoryLRUCache ( { maxSize : Infinity } ) ; logger . debug ( 'Initializing operation registry agent...' ) ; agent = new Agent ( { apollo : { ... apollo , graphRef , keyHash , } , store , logger , fetcher : options . fetcher , } ) ; await agent . start ( ) ; return { async serverWillStop ( ) { agent . stop ( ) ; } , } ; } , async requestDidStart ( ) { return { async didResolveOperation ( requestContext ) { const documentFromRequestContext = requestContext . document ; if ( ! store ) { throw new Error ( 'Unable to access store.' ) ; } const normalizedDocument = defaultOperationRegistryNormalization ( documentFromRequestContext , requestContext . operationName || '' ) ; const signature = operationSignature ( normalizedDocument ) ; if ( ! signature ) { throw new ApolloError ( 'No document.' ) ; } const logSignature = signatureForLogging ( signature ) ; logger . debug ( `${ logSignature }: Looking up operation in local registry.` ) ; const storeFetch = await store . get ( getStoreKey ( signature ) ) ; if ( storeFetch ) { logger . debug ( `${ logSignature }: Permitting operation found in local registry.` ) ; requestContext . metrics . registeredOperation = true ; return ; } else { if ( typeof options . onUnregisteredOperation === 'function' ) { const onUnregisteredOperation = options . onUnregisteredOperation ; Promise . resolve ( ) . then ( ( ) => { onUnregisteredOperation ( requestContext , { signature , normalizedDocument , } ) ; } ) ; } } let shouldForbidOperation = typeof options . forbidUnregisteredOperations === 'boolean' ? options . forbidUnregisteredOperations : typeof options . forbidUnregisteredOperations === 'function' ; if ( typeof options . forbidUnregisteredOperations === 'function' ) { logger . debug ( `${ logSignature }: Calling 'forbidUnregisteredOperations' predicate function with requestContext...` ) ; try { const predicateResult = options . forbidUnregisteredOperations ( requestContext ) ; logger . debug ( `${ logSignature }: The 'forbidUnregisteredOperations' predicate function returned ${ predicateResult }` ) ; if ( typeof predicateResult === 'boolean' ) { shouldForbidOperation = predicateResult ; } else { logger . warn ( `${ logSignature } Predicate function did not return a boolean response. Got ${ predicateResult }` ) ; } } catch ( err ) { logger . error ( `${ logSignature }: An error occurred within the 'forbidUnregisteredOperations' predicate function: ${ err }` ) ; } } if ( ! shouldForbidOperation ) { return ; } if ( options . forbidUnregisteredOperations ) { logger . debug ( `${ logSignature } Reporting operation as forbidden to Apollo trace warehouse.` ) ; requestContext . metrics . forbiddenOperation = true ; } if ( shouldForbidOperation ) { if ( typeof options . onForbiddenOperation === 'function' ) { const onForbiddenOperation = options . onForbiddenOperation ; Promise . resolve ( ) . then ( ( ) => { onForbiddenOperation ( requestContext , { signature , normalizedDocument , } ) ; } ) ; } } if ( options . dryRun ) { logger . debug ( `${ dryRunPrefix } ${ logSignature }: Operation ${ requestContext . operationName } would have been forbidden.` ) ; return ; } logger . debug ( `${ logSignature }: Execution denied because 'forbidUnregisteredOperations' was enabled for this request and the operation was not found in the local operation registry.` ) ; const error = new ForbiddenError ( 'Execution forbidden: Operation not found in operation registry' ) ; Object . assign ( error . extensions , { operationSignature : signature , exception : { message : `Please register your operation with \`npx apollo client:push --variant="${ agent . graphVariant }"\`. See https://www.apollographql.com/docs/platform/operation-registry/ for more details.` , } , } ) ; throw error ; } , } ; } , } ) ; }