From 58b5313e149dff4cee5d33c2cb78cb2267d79fb6 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Fri, 5 Dec 2025 12:40:48 +0000 Subject: [PATCH 01/16] bump ts to latest --- package-lock.json | 15 ++++++++------- package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24eed05..058a4cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "ts-node": "^10.9.1", "tsc-alias": "^1.8.8", "tsconfig-paths": "^4.2.0", - "typescript": "^5.2.2" + "typescript": "^5.9.3" } }, "node_modules/@colony/core": { @@ -3169,10 +3169,11 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5601,9 +5602,9 @@ } }, "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true }, "uid-safe": { diff --git a/package.json b/package.json index 5054dec..8f53ac5 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "ts-node": "^10.9.1", "tsc-alias": "^1.8.8", "tsconfig-paths": "^4.2.0", - "typescript": "^5.2.2" + "typescript": "^5.9.3" }, "scripts": { "dev": "NODE_ENV=dev nodemon", From 4e57a3d1e6148e0432f216a4038b61851e3d9812 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Fri, 5 Dec 2025 13:11:22 +0000 Subject: [PATCH 02/16] ensure auth runs only for target operation, not all --- src/helpers.ts | 122 ++++++++++++++-------- src/routes/graphql/graphql.ts | 40 ++++--- src/routes/graphql/mutations.ts | 180 ++++++++++++++++---------------- src/routes/graphql/queries.ts | 75 ++++++------- src/types.ts | 6 ++ 5 files changed, 235 insertions(+), 188 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 3e7de80..78d1478 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,11 +1,21 @@ -import { parse } from 'graphql'; -import dotenv from "dotenv"; +import { + DocumentNode, + Kind, + OperationDefinitionNode, + OperationTypeNode, + parse, +} from 'graphql'; +import dotenv from 'dotenv'; import { default as fetch, Request as NodeFetchRequst } from 'node-fetch'; -import { Response as ExpressResponse, Request } from 'express-serve-static-core'; +import { + Response as ExpressResponse, + Request, +} from 'express-serve-static-core'; import { RequestError } from './RequestError'; import { OperationTypes, + ParsedOperation, StaticOriginCallback, HttpStatuses, Response, @@ -20,13 +30,9 @@ const BLOCK_TIME = Number(process.env.DEFAULT_BLOCK_TIME) * 1000 || 5000; export const isDevMode = (): boolean => process.env.NODE_ENV !== 'prod'; -export const detectOperation = (body: Record): { - operationType: OperationTypes, - operations: string[], - variables?: string, -} => { - let isMutation = false; - +export const parseTargetOperation = ( + body: Record, +): ParsedOperation => { if (!body) { throw new RequestError('no body'); } @@ -34,11 +40,7 @@ export const detectOperation = (body: Record): { throw new RequestError('graphql request malformed'); } - if (JSON.stringify(body).includes(OperationTypes.Mutation)) { - isMutation = true; - } - - let parsedQuery: any; + let parsedQuery: DocumentNode | undefined; try { parsedQuery = parse(body.query); } catch (error) { @@ -49,29 +51,63 @@ export const detectOperation = (body: Record): { throw new RequestError('graphql request malformed'); } - const [{ operation: operationType }] = parsedQuery.definitions || [{}]; - if (operationType === OperationTypes.Mutation) { - isMutation = true; + const allOperations = parsedQuery.definitions.filter( + (def) => def.kind === Kind.OPERATION_DEFINITION, + ); + + let targetOperation: OperationDefinitionNode | undefined; + + if (body.operationName) { + targetOperation = allOperations.find( + (o) => o.name && o.name.value === body.operationName, + ); + } else { + if (allOperations.length !== 1) { + throw new RequestError('graphql request malformed'); + } + targetOperation = allOperations[0]; + } + + if (!targetOperation) { + throw new RequestError('graphql request malformed'); } - const operationNames = parsedQuery.definitions[0].selectionSet.selections.map( - (selection: any) => selection.name.value, + const type = + targetOperation.operation === OperationTypeNode.MUTATION + ? OperationTypes.Mutation + : OperationTypes.Query; + + // The first field will contain the query/mutation/subscription name + const firstField = targetOperation.selectionSet.selections.find( + (selection) => selection.kind === Kind.FIELD, ); + if (!firstField || firstField.kind !== Kind.FIELD) { + throw new RequestError('graphql request malformed'); + } + return { - operationType: isMutation ? OperationTypes.Mutation : OperationTypes.Query, - operations: operationNames, - variables: body.variables ? JSON.stringify(body.variables) : undefined, + type, + field: firstField.name.value, + variables: body.variables, }; }; -export const getStaticOrigin = (origin?: string, callback?: StaticOriginCallback): string | undefined => { +export const getStaticOrigin = ( + origin?: string, + callback?: StaticOriginCallback, +): string | undefined => { let isAllowedOrigin = false; if (isDevMode()) { - if (origin?.includes('http://localhost') || origin?.includes('https://localhost') || origin?.includes('http://127') || origin?.includes('https://127')) { + if ( + origin?.includes('http://localhost') || + origin?.includes('https://localhost') || + origin?.includes('http://127') || + origin?.includes('https://127') + ) { isAllowedOrigin = true; } - }; + } if (origin === process.env.ORIGIN_URL) { isAllowedOrigin = true; } @@ -86,21 +122,25 @@ export const sendResponse = ( request: Request, message?: Response, status: HttpStatuses = HttpStatuses.OK, -) => response.set({ - [Headers.AllowOrigin]: getStaticOrigin(request.headers.origin), - [Headers.ContentType]: ContentTypes.Json, - [Headers.PoweredBy]: 'Colony', -}).status(status).json(message); +) => + response + .set({ + [Headers.AllowOrigin]: getStaticOrigin(request.headers.origin), + [Headers.ContentType]: ContentTypes.Json, + [Headers.PoweredBy]: 'Colony', + }) + .status(status) + .json(message); export const getRemoteIpAddress = (request: Request): string => typeof request.headers[Headers.ForwardedFor] === 'string' ? request.headers[Headers.ForwardedFor] : request.headers[Headers.ForwardedFor]?.join(';') || - request.ip || - request.ips.join(';') || - request.connection.remoteAddress || - request.socket.remoteAddress || - ''; + request.ip || + request.ips.join(';') || + request.connection.remoteAddress || + request.socket.remoteAddress || + ''; export const resetSession = (request: Request): void => { request.session.auth = undefined; @@ -117,7 +157,7 @@ export const logger = (...args: any[]): void => { export const graphqlRequest = async ( queryOrMutation: string, - variables?: Record + variables?: Record, ) => { const options = { method: ServerMethods.Post.toUpperCase(), @@ -150,16 +190,16 @@ export const graphqlRequest = async ( }; export const delay = async (timeout: number) => { - return new Promise(resolve => { + return new Promise((resolve) => { setTimeout(resolve, timeout); }); -} +}; export const tryFetchGraphqlQuery = async ( queryOrMutation: string, variables?: Record, maxRetries: number = 3, - blockTime: number = BLOCK_TIME + blockTime: number = BLOCK_TIME, ) => { let currentTry = 0; while (true) { @@ -182,4 +222,4 @@ export const tryFetchGraphqlQuery = async ( throw new Error('Could not fetch graphql data in time'); } } -} +}; diff --git a/src/routes/graphql/graphql.ts b/src/routes/graphql/graphql.ts index 93ed753..5b15934 100644 --- a/src/routes/graphql/graphql.ts +++ b/src/routes/graphql/graphql.ts @@ -8,7 +8,7 @@ import { sendResponse, getRemoteIpAddress, logger, - detectOperation, + parseTargetOperation, } from '~helpers'; import { ResponseTypes, @@ -16,6 +16,7 @@ import { ContentTypes, Headers, OperationTypes, + ParsedOperation, Urls, ServerMethods, } from '~types'; @@ -38,14 +39,21 @@ export const operationExecutionHandler: RequestHandler = async ( return nextFn(); } - const userAddress = request.session.auth?.address || ''; + const userAddress = request.session.auth?.address; const requestRemoteAddress = getRemoteIpAddress(request); try { + const parsedOperation = parseTargetOperation(request.body); + response.locals.parsedOperation = parsedOperation; + response.locals.canExecuteMutation = await addressCanExecuteMutation( - request, + parsedOperation, + userAddress, + ); + response.locals.canExecuteQuery = await addressCanExecuteQuery( + parsedOperation, + userAddress, ); - response.locals.canExecuteQuery = await addressCanExecuteQuery(request); return nextFn(); } catch (error: any) { logger( @@ -76,13 +84,14 @@ export const graphQlProxyRouteHandler: Options = { const userAddress = request.session.auth?.address || ''; const requestRemoteAddress = getRemoteIpAddress(request); try { - if (request?.body?.query) { - /* - * Used for UI only, the real magic with detection happens in operationExecutionHandler - */ - const { operationType, operations, variables } = detectOperation( - request.body, - ); + const parsedOperation = response.locals.parsedOperation as + | ParsedOperation + | undefined; + + if (parsedOperation) { + const { type: operationType, field, variables } = parsedOperation; + + console.log('operationType in proxy', operationType); /* * Mutations need to be handled on a case by case basis @@ -101,12 +110,17 @@ export const graphQlProxyRouteHandler: Options = { operationType === OperationTypes.Query && response.locals.canExecuteQuery; + console.log('canExecuteQuery', { + first: operationType === OperationTypes.Query, + second: response.locals.canExecuteQuery, + }); + const canExecute = canExecuteMutation || canExecuteQuery; logger( `${ userAuthenticated ? `auth` : 'non-auth' - } ${operationType} ${operations} ${JSON.stringify(variables).slice( + } ${operationType} ${field} ${JSON.stringify(variables).slice( 0, 500, )}${ @@ -122,6 +136,8 @@ export const graphQlProxyRouteHandler: Options = { }`, ); + console.log('canExecute', canExecute); + // allowed if (canExecute) { proxyRequest.setHeader(Headers.WalletAddress, userAddress); diff --git a/src/routes/graphql/mutations.ts b/src/routes/graphql/mutations.ts index cde12bd..2302094 100644 --- a/src/routes/graphql/mutations.ts +++ b/src/routes/graphql/mutations.ts @@ -1,8 +1,7 @@ -import { Request } from 'express-serve-static-core'; import { ColonyRole, Id } from '@colony/core'; -import { logger, detectOperation, tryFetchGraphqlQuery } from '~helpers'; -import { MutationOperations, UserRole } from '~types'; +import { logger, tryFetchGraphqlQuery } from '~helpers'; +import { MutationOperations, ParsedOperation, UserRole } from '~types'; import { getAllColonyRoles, getColonyAction, @@ -12,49 +11,49 @@ import { getTransaction, } from '~queries'; -const hasMutationPermissions = async ( - operationName: string, - request: Request, +const hasMutationPermission = async ( + field: string, + variables: Record, + userAddress: string | undefined, ): Promise => { - const userAddress = request.session.auth?.address; - const { variables = '{}' } = detectOperation(request.body); + const input = (variables.input ?? {}) as Record; try { - switch (operationName) { + switch (field) { /* * Users */ case MutationOperations.CreateUniqueUser: case MutationOperations.UpdateUserProfile: case MutationOperations.CreateUserNotificationsData: { - const { - input: { id }, - } = JSON.parse(variables); - return userAddress && id && id.toLowerCase() === userAddress.toLowerCase(); + const id = input.id as string | undefined; + return !!( + userAddress && + id && + id.toLowerCase() === userAddress.toLowerCase() + ); } case MutationOperations.InitializeUser: { // This is always allowed as the actual check is happening in the lambda return true; } case MutationOperations.UpdateUserNotificationsData: { - const { - input: { userAddress: mutationUserAddress }, - } = JSON.parse(variables); - + const mutationUserAddress = input.userAddress as string | undefined; return ( userAddress?.toLowerCase() === mutationUserAddress?.toLowerCase() ); } case MutationOperations.CreateTransaction: { - const { - input: { from }, - } = JSON.parse(variables); - return userAddress && from && from.toLowerCase() === userAddress.toLowerCase(); + const from = input.from as string | undefined; + return !!( + userAddress && + from && + from.toLowerCase() === userAddress.toLowerCase() + ); } case MutationOperations.UpdateTransaction: { - const { - input: { id, from }, - } = JSON.parse(variables); + const id = input.id as string | undefined; + const from = input.from as string | undefined; try { const data = await tryFetchGraphqlQuery(getTransaction, { @@ -62,10 +61,14 @@ const hasMutationPermissions = async ( }); // A user should only be allowed to update transactions made by them. - return ( - from && userAddress && - from.toLowerCase() === userAddress.toLowerCase() && // The logged in user is the same as the "from" in the mutation - data.from && data.from.toLowerCase() === userAddress.toLowerCase() // The logged in user is the same as the "from" in the fetched transaction + return !!( + ( + from && + userAddress && + from.toLowerCase() === userAddress.toLowerCase() && // The logged in user is the same as the "from" in the mutation + data.from && + data.from.toLowerCase() === userAddress.toLowerCase() + ) // The logged in user is the same as the "from" in the fetched transaction ); } catch (error) { // silent @@ -73,20 +76,20 @@ const hasMutationPermissions = async ( } } case MutationOperations.CreateUserTokens: { - const { - input: { userID }, - } = JSON.parse(variables); - return userID && userAddress && userID.toLowerCase() === userAddress.toLowerCase(); + const userID = input.userID as string | undefined; + return !!( + userID && + userAddress && + userID.toLowerCase() === userAddress.toLowerCase() + ); } /* * Colony */ case MutationOperations.UpdateColonyMetadata: { - const { - input: { id: colonyAddress }, - } = JSON.parse(variables); - if (!userAddress) { - return false; + const colonyAddress = input.id as string | undefined; + if (!userAddress || !colonyAddress) { + return false; } try { const data = await tryFetchGraphqlQuery(getColonyRole, { @@ -99,38 +102,43 @@ const hasMutationPermissions = async ( } } case MutationOperations.CreateColonyContributor: { - const { - input: { contributorAddress }, - } = JSON.parse(variables); - return contributorAddress && userAddress && contributorAddress.toLowerCase() === userAddress.toLowerCase(); + const contributorAddress = input.contributorAddress as + | string + | undefined; + return !!( + contributorAddress && + userAddress && + contributorAddress.toLowerCase() === userAddress.toLowerCase() + ); } case MutationOperations.UpdateColonyContributor: { - const { - input: { id: combinedContributorId }, - } = JSON.parse(variables); + const combinedContributorId = input.id as string | undefined; + if (!combinedContributorId || !userAddress) { + return false; + } const [, contributorWalletAddress] = combinedContributorId.split('_'); - return ( - contributorWalletAddress && userAddress && + return !!( + contributorWalletAddress && contributorWalletAddress.toLowerCase() === userAddress.toLowerCase() ); } case MutationOperations.CreateColonyEtherealMetadata: { - const { - input: { initiatorAddress }, - } = JSON.parse(variables); - return initiatorAddress && userAddress && initiatorAddress?.toLowerCase() === userAddress?.toLowerCase(); + const initiatorAddress = input.initiatorAddress as string | undefined; + return !!( + initiatorAddress && + userAddress && + initiatorAddress.toLowerCase() === userAddress.toLowerCase() + ); } /* * Domains */ case MutationOperations.CreateDomain: { - const { - input: { colonyId: colonyAddress }, - } = JSON.parse(variables); + const colonyAddress = input.colonyId as string | undefined; + if (!userAddress || !colonyAddress) { + return false; + } try { - if (!userAddress) { - return false; - } const data = await tryFetchGraphqlQuery(getColonyRole, { combinedId: `${colonyAddress}_1_${userAddress}_roles`, }); @@ -148,15 +156,16 @@ const hasMutationPermissions = async ( */ case MutationOperations.CreateColonyActionMetadata: case MutationOperations.UpdateColonyAction: { - const { - input: { id: actionId }, - } = JSON.parse(variables); + const actionId = input.id as string | undefined; + if (!actionId || !userAddress) { + return false; + } try { const data = await tryFetchGraphqlQuery(getColonyAction, { actionId, }); - return ( - data.initiatorAddress && userAddress && + return !!( + data.initiatorAddress && data.initiatorAddress.toLowerCase() === userAddress.toLowerCase() ); } catch (error) { @@ -168,13 +177,11 @@ const hasMutationPermissions = async ( * Tokens */ case MutationOperations.CreateColonyTokens: { - const { - input: { colonyID: colonyAddress }, - } = JSON.parse(variables); + const colonyAddress = input.colonyID as string | undefined; + if (!userAddress || !colonyAddress) { + return false; + } try { - if (!userAddress) { - return false; - } const data = await tryFetchGraphqlQuery(getColonyRole, { combinedId: `${colonyAddress}_1_${userAddress}_roles`, }); @@ -185,13 +192,11 @@ const hasMutationPermissions = async ( } } case MutationOperations.DeleteColonyTokens: { - const { - input: { id: tokenColonyId }, - } = JSON.parse(variables); + const tokenColonyId = input.id as string | undefined; + if (!userAddress || !tokenColonyId) { + return false; + } try { - if (!userAddress) { - return false; - } const tokenData = await tryFetchGraphqlQuery(getColonyTokens, { tokenColonyId, }); @@ -217,9 +222,10 @@ const hasMutationPermissions = async ( return true; } case MutationOperations.UpdateStreamingPaymentMetadata: { - const { - input: { id: streamingPaymentId }, - } = JSON.parse(variables); + const streamingPaymentId = input.id as string | undefined; + if (!streamingPaymentId || !userAddress) { + return false; + } try { // We need to check if the user has permissions in the domain the streaming payment was created in or the root domain const { nativeDomainId } = await tryFetchGraphqlQuery( @@ -278,7 +284,9 @@ const hasMutationPermissions = async ( } } catch (error) { logger( - `Error when attempting to check if user ${userAddress} can execute mutation ${operationName} with variables ${variables}`, + `Error when attempting to check if user ${userAddress} can execute mutation ${field} with variables ${JSON.stringify( + variables, + )}`, error, ); /* @@ -289,21 +297,13 @@ const hasMutationPermissions = async ( }; const addressCanExecuteMutation = async ( - request: Request, + parsedOperation: ParsedOperation, + userAddress: string | undefined, ): Promise => { - try { - const { operations } = detectOperation(request.body); + const { field, variables } = parsedOperation; - if (!operations.length) { - return false; - } - const canExecuteAllOperations = await Promise.all( - operations.map( - async (operationName) => - await hasMutationPermissions(operationName, request), - ), - ); - return canExecuteAllOperations.every((canExecute) => canExecute); + try { + return await hasMutationPermission(field, variables ?? {}, userAddress); } catch (error) { /* * If anything fails just prevent the mutation from executing diff --git a/src/routes/graphql/queries.ts b/src/routes/graphql/queries.ts index b23869f..305987e 100644 --- a/src/routes/graphql/queries.ts +++ b/src/routes/graphql/queries.ts @@ -1,57 +1,42 @@ -import { Request } from 'express-serve-static-core'; +import { logger } from '~helpers'; +import { ParsedOperation, QueryOperations } from '~types'; -import { logger, detectOperation } from '~helpers'; -import { QueryOperations } from '~types'; +const hasQueryPermission = ( + field: string, + userAddress: string | undefined, +): boolean => { + switch (field) { + /* + * GetUserNotificationsHMAC will fail if no userAddress is provided + */ + case QueryOperations.GetUserNotificationsHMAC: { + if (!userAddress) { + return false; + } + return true; + } + default: { + // By default all queries are permitted + return true; + } + } +}; -const hasQueryPermissions = async ( - operationName: string, - request: Request, +const addressCanExecuteQuery = async ( + parsedOperation: ParsedOperation, + userAddress: string | undefined, ): Promise => { - const userAddress = request.session.auth?.address; - const { variables = '{}' } = detectOperation(request.body); + const { field, variables } = parsedOperation; try { - switch (operationName) { - /* - * GetUserNotificationsHMAC will fail if no userAddress is provided - */ - case QueryOperations.GetUserNotificationsHMAC: { - if (!userAddress) { - return false; - } - - return true; - } - default: { - // By default all queries are permitted - return true; - } - } + return hasQueryPermission(field, userAddress); } catch (error) { logger( - `Error when attempting to check if user ${userAddress} can execute query ${operationName} with variables ${variables}`, + `Error when attempting to check if user ${userAddress} can execute query ${field} with variables ${JSON.stringify( + variables, + )}`, error, ); - // By default all queries are permitted - return true; - } -}; - -const addressCanExecuteQuery = async (request: Request): Promise => { - try { - const { operations } = detectOperation(request.body); - - if (!operations.length) { - return true; - } - const canExecuteAllOperations = await Promise.all( - operations.map( - async (operationName) => - await hasQueryPermissions(operationName, request), - ), - ); - return canExecuteAllOperations.every((canExecute) => canExecute); - } catch (error) { /* * If anything fails still allow the query to execute as by default all queries are permitted */ diff --git a/src/types.ts b/src/types.ts index 998de5c..4aae32a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -152,3 +152,9 @@ export type UserRole = { role_5: boolean | null; role_6: boolean | null; }; + +export interface ParsedOperation { + type: OperationTypes; + field: string; + variables: Record | undefined; +} From 42a037cc98d456fbf280ffad7c678aac268c3cb7 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Fri, 5 Dec 2025 13:22:02 +0000 Subject: [PATCH 03/16] fetch gql schema at init --- src/index.ts | 32 ++++++++++++++++++++++---------- src/schema.ts | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 src/schema.ts diff --git a/src/index.ts b/src/index.ts index 730096b..7934533 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,31 @@ -import dotenv from "dotenv"; +import dotenv from 'dotenv'; import proxyServerInstance from './server'; import { handleWsUpgrade } from './routes/graphql/ws'; +import { initSchema } from './schema'; dotenv.config(); const port = process.env.DEFAULT_PORT || 3005; -const proxy = proxyServerInstance(); -const server = proxy.listen(port, () => { - /* - * @NOTE Use console log here as to ensure it will always be logged - */ - console.log(`Authentication proxy listening on port ${port}`); -}); +(async () => { + try { + await initSchema(); + console.log('GraphQL schema loaded successfully'); + } catch (error) { + console.error('Failed to load GraphQL schema:', error); + process.exit(1); + } -// Custom websocker upgrade proxy handler -server.on('upgrade', handleWsUpgrade) + const proxy = proxyServerInstance(); + + const server = proxy.listen(port, () => { + /* + * @NOTE Use console log here as to ensure it will always be logged + */ + console.log(`Authentication proxy listening on port ${port}`); + }); + + // Custom websocker upgrade proxy handler + server.on('upgrade', handleWsUpgrade); +})(); diff --git a/src/schema.ts b/src/schema.ts new file mode 100644 index 0000000..a35bee7 --- /dev/null +++ b/src/schema.ts @@ -0,0 +1,24 @@ +import { + GraphQLSchema, + buildClientSchema, + getIntrospectionQuery, + IntrospectionQuery, +} from 'graphql'; + +import { graphqlRequest } from './helpers'; + +let schema: GraphQLSchema | null = null; + +export const initSchema = async (): Promise => { + const introspectionQuery = getIntrospectionQuery(); + + const result = await graphqlRequest(introspectionQuery); + + if (!result?.data) { + throw new Error('Failed to fetch GraphQL schema: no data returned'); + } + + schema = buildClientSchema(result.data as IntrospectionQuery); +}; + +export const getSchema = (): GraphQLSchema | null => schema; From 215d59efc99514aaa5387003e3292574712cb489 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Fri, 5 Dec 2025 16:29:52 +0000 Subject: [PATCH 04/16] rough field-based auth --- src/RequestError.ts | 15 - src/helpers.ts | 73 ----- src/routes/graphql/graphql.ts | 182 ++++-------- src/routes/graphql/mutations.ts | 315 --------------------- src/rules.ts | 480 ++++++++++++++++++++++++++++++++ src/types.ts | 79 ------ src/validateRequest.ts | 165 +++++++++++ 7 files changed, 700 insertions(+), 609 deletions(-) delete mode 100644 src/RequestError.ts delete mode 100644 src/routes/graphql/mutations.ts create mode 100644 src/rules.ts create mode 100644 src/validateRequest.ts diff --git a/src/RequestError.ts b/src/RequestError.ts deleted file mode 100644 index 72084f0..0000000 --- a/src/RequestError.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Response, ResponseTypes } from '~types'; - -export class RequestError extends Error { - response: Response; - - constructor(message: string, data: string | number | boolean | string[] | number[] | boolean[] = '') { - super(message); - this.name = 'RequestError'; - this.response = { - message, - type: ResponseTypes.Error, - data, - }; - } -} diff --git a/src/helpers.ts b/src/helpers.ts index 78d1478..cffa68e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,10 +1,3 @@ -import { - DocumentNode, - Kind, - OperationDefinitionNode, - OperationTypeNode, - parse, -} from 'graphql'; import dotenv from 'dotenv'; import { default as fetch, Request as NodeFetchRequst } from 'node-fetch'; @@ -12,10 +5,7 @@ import { Response as ExpressResponse, Request, } from 'express-serve-static-core'; -import { RequestError } from './RequestError'; import { - OperationTypes, - ParsedOperation, StaticOriginCallback, HttpStatuses, Response, @@ -30,69 +20,6 @@ const BLOCK_TIME = Number(process.env.DEFAULT_BLOCK_TIME) * 1000 || 5000; export const isDevMode = (): boolean => process.env.NODE_ENV !== 'prod'; -export const parseTargetOperation = ( - body: Record, -): ParsedOperation => { - if (!body) { - throw new RequestError('no body'); - } - if (!body?.query) { - throw new RequestError('graphql request malformed'); - } - - let parsedQuery: DocumentNode | undefined; - try { - parsedQuery = parse(body.query); - } catch (error) { - // silent - } - - if (!parsedQuery) { - throw new RequestError('graphql request malformed'); - } - - const allOperations = parsedQuery.definitions.filter( - (def) => def.kind === Kind.OPERATION_DEFINITION, - ); - - let targetOperation: OperationDefinitionNode | undefined; - - if (body.operationName) { - targetOperation = allOperations.find( - (o) => o.name && o.name.value === body.operationName, - ); - } else { - if (allOperations.length !== 1) { - throw new RequestError('graphql request malformed'); - } - targetOperation = allOperations[0]; - } - - if (!targetOperation) { - throw new RequestError('graphql request malformed'); - } - - const type = - targetOperation.operation === OperationTypeNode.MUTATION - ? OperationTypes.Mutation - : OperationTypes.Query; - - // The first field will contain the query/mutation/subscription name - const firstField = targetOperation.selectionSet.selections.find( - (selection) => selection.kind === Kind.FIELD, - ); - - if (!firstField || firstField.kind !== Kind.FIELD) { - throw new RequestError('graphql request malformed'); - } - - return { - type, - field: firstField.name.value, - variables: body.variables, - }; -}; - export const getStaticOrigin = ( origin?: string, callback?: StaticOriginCallback, diff --git a/src/routes/graphql/graphql.ts b/src/routes/graphql/graphql.ts index 5b15934..94e756b 100644 --- a/src/routes/graphql/graphql.ts +++ b/src/routes/graphql/graphql.ts @@ -2,27 +2,25 @@ import dotenv from 'dotenv'; import { fixRequestBody, Options, RequestHandler } from 'http-proxy-middleware'; import { Response, Request, NextFunction } from 'express-serve-static-core'; import { ClientRequest, IncomingMessage } from 'http'; +import { parse } from 'graphql'; import { getStaticOrigin, sendResponse, getRemoteIpAddress, logger, - parseTargetOperation, } from '~helpers'; import { ResponseTypes, HttpStatuses, ContentTypes, Headers, - OperationTypes, - ParsedOperation, Urls, ServerMethods, } from '~types'; - -import addressCanExecuteMutation from './mutations'; -import addressCanExecuteQuery from './queries'; +import { validateRequest } from '../../validateRequest'; +import { rules } from '../../rules'; +import { getSchema } from '../../schema'; dotenv.config(); @@ -43,16 +41,25 @@ export const operationExecutionHandler: RequestHandler = async ( const requestRemoteAddress = getRemoteIpAddress(request); try { - const parsedOperation = parseTargetOperation(request.body); - response.locals.parsedOperation = parsedOperation; + const schema = getSchema(); + if (!schema) { + throw new Error('Schema not initialized'); + } - response.locals.canExecuteMutation = await addressCanExecuteMutation( - parsedOperation, - userAddress, - ); - response.locals.canExecuteQuery = await addressCanExecuteQuery( - parsedOperation, + const document = parse(request.body.query); + const ctx = { userAddress, + variables: request.body.variables ?? {}, + }; + + /* + * @NOTE Handle async GraphQL logic to decide if we allow an operation or not + */ + response.locals.canExecute = await validateRequest( + document, + schema, + rules, + ctx, ); return nextFn(); } catch (error: any) { @@ -63,7 +70,16 @@ export const operationExecutionHandler: RequestHandler = async ( request.body ? JSON.stringify(request.body) : '' } from ${requestRemoteAddress}`, ); - return sendResponse(response, request, error, HttpStatuses.SERVER_ERROR); + return sendResponse( + response, + request, + { + message: error?.message || 'graphql parsing error', + type: ResponseTypes.Error, + data: '', + }, + HttpStatuses.SERVER_ERROR, + ); } }; @@ -83,122 +99,34 @@ export const graphQlProxyRouteHandler: Options = { const userAuthenticated = !!request.session.auth; const userAddress = request.session.auth?.address || ''; const requestRemoteAddress = getRemoteIpAddress(request); - try { - const parsedOperation = response.locals.parsedOperation as - | ParsedOperation - | undefined; - - if (parsedOperation) { - const { type: operationType, field, variables } = parsedOperation; - - console.log('operationType in proxy', operationType); - - /* - * Mutations need to be handled on a case by case basis - * Some are allowed without auth (cache refresh ones) - * Others based on if the user has the appropriate address and/or role - */ - const canExecuteMutation = - operationType === OperationTypes.Mutation && - response.locals.canExecuteMutation; - - /* - * By default, all queries are allowed - * However, some will not execute correctly if a user address is not provided - */ - const canExecuteQuery = - operationType === OperationTypes.Query && - response.locals.canExecuteQuery; - console.log('canExecuteQuery', { - first: operationType === OperationTypes.Query, - second: response.locals.canExecuteQuery, - }); + const canExecute = response.locals.canExecute; - const canExecute = canExecuteMutation || canExecuteQuery; - - logger( - `${ - userAuthenticated ? `auth` : 'non-auth' - } ${operationType} ${field} ${JSON.stringify(variables).slice( - 0, - 500, - )}${ - JSON.stringify(variables).length > 499 - ? ` [+${JSON.stringify(variables).length - 499} chars more]` - : '' - }${ - userAddress ? ` from ${userAddress}` : '' - } at ${requestRemoteAddress} was ${ - canExecute - ? '\x1b[32m ALLOWED \x1b[0m' - : '\x1b[31m FORBIDDEN \x1b[0m' - }`, - ); - - console.log('canExecute', canExecute); - - // allowed - if (canExecute) { - proxyRequest.setHeader(Headers.WalletAddress, userAddress); - return fixRequestBody(proxyRequest, request); - } - - // forbidden - return sendResponse( - response, - request, - { - message: 'forbidden', - type: ResponseTypes.Auth, - data: '', - }, - HttpStatuses.FORBIDDEN, - ); - } + logger( + `${userAuthenticated ? 'auth' : 'non-auth'} request${ + userAddress ? ` from ${userAddress}` : '' + } at ${requestRemoteAddress} was ${ + canExecute ? '\x1b[32m ALLOWED \x1b[0m' : '\x1b[31m FORBIDDEN \x1b[0m' + }`, + ); - /* - * Malformed request - */ - logger( - `${userAuthenticated ? `auth` : 'non-auth'} request malformed graphql ${ - request.body ? JSON.stringify(request.body) : '' - }${ - userAddress ? ` from ${userAddress}` : '' - } at ${requestRemoteAddress}`, - ); - return sendResponse( - response, - request, - { - message: 'malformed graphql request', - type: ResponseTypes.Error, - data: '', - }, - HttpStatuses.SERVER_ERROR, - ); - } catch (error: any) { - /* - * GraphQL error (comes from the AppSync endopoint) - */ - logger( - `${userAuthenticated ? `auth` : 'non-auth'} graphql proxy error ${ - error?.message - } ${request.body ? JSON.stringify(request.body) : ''}${ - userAddress ? ` from ${userAddress}` : '' - } at ${requestRemoteAddress}`, - ); - return sendResponse( - response, - request, - { - message: 'graphql error', - type: ResponseTypes.Error, - data: error?.message || '', - }, - HttpStatuses.SERVER_ERROR, - ); + // Allowed + if (canExecute) { + proxyRequest.setHeader(Headers.WalletAddress, userAddress); + return fixRequestBody(proxyRequest, request); } + + // Forbidden + return sendResponse( + response, + request, + { + message: 'forbidden', + type: ResponseTypes.Auth, + data: '', + }, + HttpStatuses.FORBIDDEN, + ); }, // selfHandleResponse: true, onProxyRes: (proxyResponse: IncomingMessage, request: Request) => { diff --git a/src/routes/graphql/mutations.ts b/src/routes/graphql/mutations.ts deleted file mode 100644 index 2302094..0000000 --- a/src/routes/graphql/mutations.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { ColonyRole, Id } from '@colony/core'; - -import { logger, tryFetchGraphqlQuery } from '~helpers'; -import { MutationOperations, ParsedOperation, UserRole } from '~types'; -import { - getAllColonyRoles, - getColonyAction, - getColonyRole, - getColonyTokens, - getStreamingPayment, - getTransaction, -} from '~queries'; - -const hasMutationPermission = async ( - field: string, - variables: Record, - userAddress: string | undefined, -): Promise => { - const input = (variables.input ?? {}) as Record; - - try { - switch (field) { - /* - * Users - */ - case MutationOperations.CreateUniqueUser: - case MutationOperations.UpdateUserProfile: - case MutationOperations.CreateUserNotificationsData: { - const id = input.id as string | undefined; - return !!( - userAddress && - id && - id.toLowerCase() === userAddress.toLowerCase() - ); - } - case MutationOperations.InitializeUser: { - // This is always allowed as the actual check is happening in the lambda - return true; - } - case MutationOperations.UpdateUserNotificationsData: { - const mutationUserAddress = input.userAddress as string | undefined; - return ( - userAddress?.toLowerCase() === mutationUserAddress?.toLowerCase() - ); - } - case MutationOperations.CreateTransaction: { - const from = input.from as string | undefined; - return !!( - userAddress && - from && - from.toLowerCase() === userAddress.toLowerCase() - ); - } - case MutationOperations.UpdateTransaction: { - const id = input.id as string | undefined; - const from = input.from as string | undefined; - - try { - const data = await tryFetchGraphqlQuery(getTransaction, { - transactionId: id, - }); - - // A user should only be allowed to update transactions made by them. - return !!( - ( - from && - userAddress && - from.toLowerCase() === userAddress.toLowerCase() && // The logged in user is the same as the "from" in the mutation - data.from && - data.from.toLowerCase() === userAddress.toLowerCase() - ) // The logged in user is the same as the "from" in the fetched transaction - ); - } catch (error) { - // silent - return false; - } - } - case MutationOperations.CreateUserTokens: { - const userID = input.userID as string | undefined; - return !!( - userID && - userAddress && - userID.toLowerCase() === userAddress.toLowerCase() - ); - } - /* - * Colony - */ - case MutationOperations.UpdateColonyMetadata: { - const colonyAddress = input.id as string | undefined; - if (!userAddress || !colonyAddress) { - return false; - } - try { - const data = await tryFetchGraphqlQuery(getColonyRole, { - combinedId: `${colonyAddress}_1_${userAddress}_roles`, - }); - return !!data[`role_${ColonyRole.Root}`]; - } catch (error) { - // silent - return false; - } - } - case MutationOperations.CreateColonyContributor: { - const contributorAddress = input.contributorAddress as - | string - | undefined; - return !!( - contributorAddress && - userAddress && - contributorAddress.toLowerCase() === userAddress.toLowerCase() - ); - } - case MutationOperations.UpdateColonyContributor: { - const combinedContributorId = input.id as string | undefined; - if (!combinedContributorId || !userAddress) { - return false; - } - const [, contributorWalletAddress] = combinedContributorId.split('_'); - return !!( - contributorWalletAddress && - contributorWalletAddress.toLowerCase() === userAddress.toLowerCase() - ); - } - case MutationOperations.CreateColonyEtherealMetadata: { - const initiatorAddress = input.initiatorAddress as string | undefined; - return !!( - initiatorAddress && - userAddress && - initiatorAddress.toLowerCase() === userAddress.toLowerCase() - ); - } - /* - * Domains - */ - case MutationOperations.CreateDomain: { - const colonyAddress = input.colonyId as string | undefined; - if (!userAddress || !colonyAddress) { - return false; - } - try { - const data = await tryFetchGraphqlQuery(getColonyRole, { - combinedId: `${colonyAddress}_1_${userAddress}_roles`, - }); - return !!data[`role_${ColonyRole.Architecture}`]; - } catch (error) { - // silent - return false; - } - } - case MutationOperations.UpdateDomainMetadata: { - return true; - } - /* - * Actions, Mutations - */ - case MutationOperations.CreateColonyActionMetadata: - case MutationOperations.UpdateColonyAction: { - const actionId = input.id as string | undefined; - if (!actionId || !userAddress) { - return false; - } - try { - const data = await tryFetchGraphqlQuery(getColonyAction, { - actionId, - }); - return !!( - data.initiatorAddress && - data.initiatorAddress.toLowerCase() === userAddress.toLowerCase() - ); - } catch (error) { - // silent - return false; - } - } - /* - * Tokens - */ - case MutationOperations.CreateColonyTokens: { - const colonyAddress = input.colonyID as string | undefined; - if (!userAddress || !colonyAddress) { - return false; - } - try { - const data = await tryFetchGraphqlQuery(getColonyRole, { - combinedId: `${colonyAddress}_1_${userAddress}_roles`, - }); - return !!data[`role_${ColonyRole.Root}`]; - } catch (error) { - // silent - return false; - } - } - case MutationOperations.DeleteColonyTokens: { - const tokenColonyId = input.id as string | undefined; - if (!userAddress || !tokenColonyId) { - return false; - } - try { - const tokenData = await tryFetchGraphqlQuery(getColonyTokens, { - tokenColonyId, - }); - - if (tokenData?.colonyID) { - const data = await tryFetchGraphqlQuery(getColonyRole, { - combinedId: `${tokenData.colonyID}_1_${userAddress}_roles`, - }); - return !!data[`role_${ColonyRole.Root}`]; - } - return false; - } catch (error) { - // silent - return false; - } - } - /** - * Expenditures - */ - case MutationOperations.CreateExpenditureMetadata: - case MutationOperations.CreateStreamingPaymentMetadata: - case MutationOperations.CreateAnnotation: { - return true; - } - case MutationOperations.UpdateStreamingPaymentMetadata: { - const streamingPaymentId = input.id as string | undefined; - if (!streamingPaymentId || !userAddress) { - return false; - } - try { - // We need to check if the user has permissions in the domain the streaming payment was created in or the root domain - const { nativeDomainId } = await tryFetchGraphqlQuery( - getStreamingPayment, - { - streamingPaymentId, - }, - ); - const [colonyAddress] = streamingPaymentId.split('_'); - - const { items: userRoles }: { items: UserRole[] } = - await tryFetchGraphqlQuery(getAllColonyRoles, { - targetAddress: userAddress, - colonyAddress, - }); - - return userRoles.some((item) => { - const [, roleDomainId] = item.id.split('_'); - const matchesDomain = - roleDomainId === String(nativeDomainId) || - roleDomainId === String(Id.RootDomain); - const hasRole = !!item[`role_${ColonyRole.Administration}`]; - return matchesDomain && hasRole; - }); - } catch (error) { - // silent - return false; - } - } - /** - * Metadata can be created as part of the motion process, so we need to allow - * those mutations even for users with no permissions - */ - case MutationOperations.CreateColonyMetadata: - case MutationOperations.CreateDomainMetadata: - /* - * Always allow, it's just updating cache, anybody can trigger it - */ - case MutationOperations.CreateColonyDecision: - case MutationOperations.ValidateUserInvite: - case MutationOperations.GetTokenFromEverywhere: - case MutationOperations.UpdateContributorsWithReputation: { - return true; - } - /* - * Bridge XYZ mutation, always allow - */ - case MutationOperations.BridgeXYZMutation: - case MutationOperations.BridgeCreateBankAccount: - case MutationOperations.BridgeUpdateBankAccount: { - return true; - } - default: { - return false; - } - } - } catch (error) { - logger( - `Error when attempting to check if user ${userAddress} can execute mutation ${field} with variables ${JSON.stringify( - variables, - )}`, - error, - ); - /* - * If anything fails just prevent the mutation from executing - */ - return false; - } -}; - -const addressCanExecuteMutation = async ( - parsedOperation: ParsedOperation, - userAddress: string | undefined, -): Promise => { - const { field, variables } = parsedOperation; - - try { - return await hasMutationPermission(field, variables ?? {}, userAddress); - } catch (error) { - /* - * If anything fails just prevent the mutation from executing - */ - return false; - } -}; - -export default addressCanExecuteMutation; diff --git a/src/rules.ts b/src/rules.ts new file mode 100644 index 0000000..5e388c6 --- /dev/null +++ b/src/rules.ts @@ -0,0 +1,480 @@ +import { ColonyRole, Id } from '@colony/core'; + +import { tryFetchGraphqlQuery } from './helpers'; +import { + getColonyAction, + getColonyRole, + getColonyTokens, + getStreamingPayment, + getTransaction, + getAllColonyRoles, +} from './queries'; +import { UserRole } from './types'; + +export type AuthContext = { + userAddress: string | undefined; + variables: Record; + path: string[]; +}; + +export type FieldRule = + | true + | false + | ((ctx: AuthContext) => boolean | Promise); + +// TypeRules can be: +// - true: all fields allowed +// - false: all fields blocked +// - { field: rule }: field-level rules, missing = blocked +export type TypeRules = true | false | Record; + +export type PathRules = Record; + +export interface RulesConfig { + types: Record; + paths: PathRules; +} + +// ============================================================ +// HELPERS +// ============================================================ + +// Helper to check if a field in input matches userAddress +const isOwnUser = + (field: string = 'id') => + (ctx: AuthContext): boolean => { + const input = (ctx.variables.input ?? {}) as Record; + const value = (input[field] ?? ctx.variables[field]) as string | undefined; + return !!( + ctx.userAddress && + value && + value.toLowerCase() === ctx.userAddress.toLowerCase() + ); + }; + +// Helper to require authentication +const requiresAuth = (ctx: AuthContext): boolean => !!ctx.userAddress; + +// Helper to check if user has a specific role in a colony (root domain) +const hasColonyRole = + (colonyField: string, role: ColonyRole) => + async (ctx: AuthContext): Promise => { + const input = (ctx.variables.input ?? {}) as Record; + const colonyAddress = input[colonyField] as string | undefined; + if (!ctx.userAddress || !colonyAddress) return false; + try { + const data = await tryFetchGraphqlQuery(getColonyRole, { + combinedId: `${colonyAddress}_1_${ctx.userAddress}_roles`, + }); + return !!data[`role_${role}`]; + } catch { + return false; + } + }; + +// Helper to check if user is the initiator of an action +const isActionInitiator = + (actionField: string = 'id') => + async (ctx: AuthContext): Promise => { + const input = (ctx.variables.input ?? {}) as Record; + const actionId = input[actionField] as string | undefined; + if (!ctx.userAddress || !actionId) return false; + try { + const data = await tryFetchGraphqlQuery(getColonyAction, { actionId }); + return ( + data.initiatorAddress?.toLowerCase() === ctx.userAddress.toLowerCase() + ); + } catch { + return false; + } + }; + +export const rules: RulesConfig = { + // ============================================================ + // TYPE RULES + // ============================================================ + types: { + Query: { + getTokenFromEverywhere: true, + getUserReputation: true, + getUserTokenBalance: true, + getMotionState: true, + getVoterRewards: true, + getMotionTimeoutPeriods: true, + getSafeTransactionStatus: true, + getDomainBalance: true, + cacheAllDomainBalance: true, + searchColonyContributors: true, + searchColonyActions: true, + getProfile: true, + getToken: true, + listTokens: true, + getContributorReputation: true, + getColonyContributor: true, + getColony: true, + listColonies: true, + getColonyMemberInvite: true, + getColonyMetadata: true, + getTransaction: true, + getUser: true, + listUsers: true, + getDomain: true, + getDomainMetadata: true, + getColonyFundsClaim: true, + getVoterRewardsHistory: true, + getMotionMessage: true, + listMotionMessages: true, + getMultiSigUserSignature: true, + getColonyMultiSig: true, + listColonyMultiSigs: true, + getColonyMotion: true, + listColonyMotions: true, + getColonyExtension: true, + getCurrentVersion: true, + listCurrentVersions: true, + getCurrentNetworkInverseFee: true, + getColonyAction: true, + listColonyActions: true, + getColonyActionMetadata: true, + listColonyActionMetadata: true, + getColonyDecision: true, + getColonyRole: true, + getColonyHistoricRole: true, + + getExpenditure: true, + listExpenditures: true, + getExpenditureMetadata: true, + listExpenditureMetadata: true, + getStreamingPayment: true, + listStreamingPayments: true, + getStreamingPaymentMetadata: true, + listStreamingPaymentMetadata: true, + getAnnotation: true, + getReputationMiningCycleMetadata: true, + getPrivateBetaInviteCode: true, + getSafeTransaction: true, + getSafeTransactionData: true, + getExtensionInstallationsCount: true, + listExtensionInstallationsCounts: true, + getUserStake: true, + getColonyTokens: true, + listColonyTokens: true, + getUserTokens: true, + listUserTokens: true, + tokenExhangeRateByTokenId: true, + cacheTotalBalanceByColonyAddress: true, + getProfileByUsername: true, + getTokenByAddress: true, + getTokensByType: true, + getUserReputationInColony: true, + getContributorsByAddress: true, + getContributorsByColony: true, + getColonyByAddress: true, + getColonyByName: true, + getColoniesByNativeTokenId: true, + getColonyByType: true, + getTransactionsByUser: true, + getTransactionsByUserAndGroup: true, + getUserByAddress: true, + getLiquidationAddressesByUserAddress: true, + getUserByLiquidationAddress: true, + getDomainsByColony: true, + getDomainByNativeSkillId: true, + getFundsClaimsByColony: true, + getUserVoterRewards: true, + getMotionVoterRewards: true, + getMotionMessageByMotionId: true, + getMultiSigUserSignatureByMultiSigId: true, + getMultiSigByColonyAddress: true, + getMultiSigByTransactionHash: true, + getMultiSigByExpenditureId: true, + getMotionByTransactionHash: true, + getMotionByExpenditureId: true, + getExtensionByColonyAndHash: true, + getExtensionsByHash: true, + getCurrentVersionByKey: true, + getActionsByColony: true, + getColonyActionByMotionId: true, + getColonyActionByMultiSigId: true, + getActionByExpenditureId: true, + getColonyDecisionByActionId: true, + getColonyDecisionByColonyAddress: true, + getRoleByDomainAndColony: true, + getRoleByTargetAddressAndColony: true, + getRoleByColony: true, + getColonyHistoricRoleByDate: true, + getExpendituresByColony: true, + getExpendituresByNativeFundingPotIdAndColony: true, + getUserStakes: true, + + // Requires authentication + bridgeCheckKYC: requiresAuth, + bridgeGetDrainsHistory: requiresAuth, + bridgeGetUserLiquidationAddress: requiresAuth, + bridgeGetGatewayFee: requiresAuth, + getUserNotificationsHMAC: requiresAuth, + getLiquidationAddress: requiresAuth, + + // Blocked + getNotificationsData: false, + listNotificationsData: false, + getUserByBridgeCustomerId: false, + listPrivateBetaInviteCodes: false, + listProfiles: false, + listColonyRoles: false, + listColonyHistoricRoles: false, + listDomainMetadata: false, + getCacheTotalBalance: false, + listCacheTotalBalances: false, + getTokenExchangeRate: false, + listTokenExchangeRates: false, + listColonyMemberInvites: false, + listLiquidationAddresses: false, + listReputationMiningCycleMetadata: false, + getIngestorStats: false, + listIngestorStats: false, + listSafeTransactionData: false, + listVoterRewardsHistories: false, + listColonyMetadata: false, + listContributorReputations: false, + listColonyContributors: false, + listTransactions: false, + listDomains: false, + listColonyFundsClaims: false, + getContractEvent: false, + listContractEvents: false, + listColonyExtensions: false, + listMultiSigUserSignatures: false, + listUserStakes: false, + listAnnotations: false, + listSafeTransactions: false, + listColonyDecisions: false, + listCurrentNetworkInverseFees: false, + }, + + Mutation: { + createUniqueUser: isOwnUser(), + updateProfile: isOwnUser(), + createUserNotificationsData: isOwnUser(), + updateNotificationsData: isOwnUser('userAddress'), + createTransaction: isOwnUser('from'), + updateTransaction: async (ctx) => { + const input = (ctx.variables.input ?? {}) as Record; + const id = input.id as string | undefined; + const from = input.from as string | undefined; + if (!ctx.userAddress || !id || !from) return false; + if (from.toLowerCase() !== ctx.userAddress.toLowerCase()) return false; + try { + const data = await tryFetchGraphqlQuery(getTransaction, { + transactionId: id, + }); + return data.from?.toLowerCase() === ctx.userAddress.toLowerCase(); + } catch { + return false; + } + }, + createUserTokens: isOwnUser('userID'), + createColonyContributor: isOwnUser('contributorAddress'), + updateColonyContributor: (ctx) => { + const input = (ctx.variables.input ?? {}) as Record; + const combinedId = input.id as string | undefined; + if (!combinedId || !ctx.userAddress) return false; + const [, contributorAddress] = combinedId.split('_'); + return ( + contributorAddress?.toLowerCase() === ctx.userAddress.toLowerCase() + ); + }, + createColonyEtherealMetadata: isOwnUser('initiatorAddress'), + + initializeUser: requiresAuth, + createColonyMetadata: requiresAuth, + createDomainMetadata: requiresAuth, + updateDomainMetadata: requiresAuth, + createExpenditureMetadata: requiresAuth, + createStreamingPaymentMetadata: requiresAuth, + createAnnotation: requiresAuth, + createColonyDecision: requiresAuth, + bridgeXYZMutation: requiresAuth, + bridgeCreateBankAccount: requiresAuth, + bridgeUpdateBankAccount: requiresAuth, + + updateColonyMetadata: hasColonyRole('id', ColonyRole.Root), + createDomain: hasColonyRole('colonyId', ColonyRole.Architecture), + createColonyTokens: hasColonyRole('colonyID', ColonyRole.Root), + deleteColonyTokens: async (ctx) => { + const input = (ctx.variables.input ?? {}) as Record; + const tokenColonyId = input.id as string | undefined; + if (!ctx.userAddress || !tokenColonyId) return false; + try { + const tokenData = await tryFetchGraphqlQuery(getColonyTokens, { + tokenColonyId, + }); + if (!tokenData?.colonyID) return false; + const roleData = await tryFetchGraphqlQuery(getColonyRole, { + combinedId: `${tokenData.colonyID}_1_${ctx.userAddress}_roles`, + }); + return !!roleData[`role_${ColonyRole.Root}`]; + } catch { + return false; + } + }, + createColonyActionMetadata: isActionInitiator(), + updateColonyAction: isActionInitiator(), + updateStreamingPaymentMetadata: async (ctx) => { + const input = (ctx.variables.input ?? {}) as Record; + const streamingPaymentId = input.id as string | undefined; + if (!ctx.userAddress || !streamingPaymentId) return false; + try { + const { nativeDomainId } = await tryFetchGraphqlQuery( + getStreamingPayment, + { streamingPaymentId }, + ); + const [colonyAddress] = streamingPaymentId.split('_'); + const { items: userRoles }: { items: UserRole[] } = + await tryFetchGraphqlQuery(getAllColonyRoles, { + targetAddress: ctx.userAddress, + colonyAddress, + }); + return userRoles.some((item) => { + const [, roleDomainId] = item.id.split('_'); + const matchesDomain = + roleDomainId === String(nativeDomainId) || + roleDomainId === String(Id.RootDomain); + const hasRole = !!item[`role_${ColonyRole.Administration}`]; + return matchesDomain && hasRole; + }); + } catch { + return false; + } + }, + + validateUserInvite: true, + getTokenFromEverywhere: true, + updateContributorsWithReputation: true, + }, + + User: { + id: true, + tokens: true, + profileId: true, + profile: true, + roles: true, + transactionHistory: true, + liquidationAddresses: true, + createdAt: true, + updatedAt: true, + }, + Profile: { + id: true, + avatar: true, + thumbnail: true, + displayName: true, + displayNameChanged: true, + bio: true, + location: true, + website: true, + meta: true, + }, + Colony: true, + ColonyMetadata: true, + Domain: true, + DomainMetadata: true, + Token: true, + Transaction: true, + ColonyAction: true, + ColonyActionMetadata: true, + ColonyMotion: true, + ColonyMultiSig: true, + ColonyExtension: true, + ColonyContributor: true, + ContributorReputation: true, + Expenditure: true, + ExpenditureMetadata: true, + StreamingPayment: true, + StreamingPaymentMetadata: true, + Annotation: true, + ColonyDecision: true, + ColonyRole: true, + ColonyHistoricRole: true, + ColonyFundsClaim: true, + ColonyMemberInvite: true, + ColonyTokens: true, + UserTokens: true, + UserStake: true, + VoterRewardsHistory: true, + MotionMessage: true, + MultiSigUserSignature: true, + ContractEvent: true, + CurrentVersion: true, + CurrentNetworkInverseFee: true, + IngestorStats: true, + ReputationMiningCycleMetadata: true, + PrivateBetaInviteCode: true, + SafeTransaction: true, + SafeTransactionData: true, + ExtensionInstallationsCount: true, + TokenExchangeRate: true, + CacheTotalBalance: true, + LiquidationAddress: true, + + // Connection types + ModelColonyConnection: true, + ModelUserConnection: true, + ModelTokenConnection: true, + ModelDomainConnection: true, + ModelColonyActionConnection: true, + ModelColonyContributorConnection: true, + ModelTransactionConnection: true, + ModelProfileConnection: true, + ModelColonyRoleConnection: true, + ModelExpenditureConnection: true, + ModelStreamingPaymentConnection: true, + ModelColonyMotionConnection: true, + ModelColonyMultiSigConnection: true, + ModelColonyExtensionConnection: true, + ModelAnnotationConnection: true, + ModelColonyDecisionConnection: true, + ModelColonyFundsClaimConnection: true, + ModelVoterRewardsHistoryConnection: true, + ModelMotionMessageConnection: true, + ModelMultiSigUserSignatureConnection: true, + ModelContractEventConnection: true, + ModelColonyMemberInviteConnection: true, + ModelColonyMetadataConnection: true, + ModelDomainMetadataConnection: true, + ModelColonyActionMetadataConnection: true, + ModelColonyHistoricRoleConnection: true, + ModelIngestorStatsConnection: true, + ModelExpenditureMetadataConnection: true, + ModelStreamingPaymentMetadataConnection: true, + ModelReputationMiningCycleMetadataConnection: true, + ModelPrivateBetaInviteCodeConnection: true, + ModelSafeTransactionConnection: true, + ModelSafeTransactionDataConnection: true, + ModelExtensionInstallationsCountConnection: true, + ModelUserStakeConnection: true, + ModelColonyTokensConnection: true, + ModelUserTokensConnection: true, + ModelTokenExchangeRateConnection: true, + ModelCacheTotalBalanceConnection: true, + ModelContributorReputationConnection: true, + ModelCurrentVersionConnection: true, + ModelCurrentNetworkInverseFeeConnection: true, + ModelColonyMultiSigFilterInput: true, + SearchableColonyContributorConnection: true, + SearchableColonyActionConnection: true, + + NotificationsData: false, + }, + + // ============================================================ + // PATH RULES (overrides) + // Check exact path, then parent path + // ============================================================ + paths: { + 'getUserByAddress.bridgeCustomerId': isOwnUser(), + 'getUserByAddress.notificationsData': isOwnUser(), + 'getUserByAddress.privateBetaInviteCode': isOwnUser(), + 'getUserByAddress.profile.email': isOwnUser(), + }, +}; diff --git a/src/types.ts b/src/types.ts index 4aae32a..503d9cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,78 +1,5 @@ import { RequestHandler } from 'http-proxy-middleware'; -export enum OperationTypes { - Query = 'query', - Mutation = 'mutation', -} - -export enum DefinitionTypes { - Operation = 'OperationDefinition', - Fragment = 'FragmentDefinition', -} - -export enum MutationOperations { - /* - * User - */ - CreateUniqueUser = 'createUniqueUser', - UpdateUserProfile = 'updateProfile', - UpdateUserNotificationsData = 'updateNotificationsData', - CreateTransaction = 'createTransaction', - UpdateTransaction = 'updateTransaction', - CreateUserTokens = 'createUserTokens', - CreateUserNotificationsData = 'createUserNotificationsData', - InitializeUser = 'initializeUser', - /* - * Colony - */ - CreateColonyMetadata = 'createColonyMetadata', - UpdateColonyMetadata = 'updateColonyMetadata', - ValidateUserInvite = 'validateUserInvite', - CreateColonyContributor = 'createColonyContributor', - UpdateColonyContributor = 'updateColonyContributor', - UpdateContributorsWithReputation = 'updateContributorsWithReputation', - CreateColonyEtherealMetadata = 'createColonyEtherealMetadata', - /* - * Domains - */ - CreateDomain = 'createDomain', - CreateDomainMetadata = 'createDomainMetadata', - UpdateDomainMetadata = 'updateDomainMetadata', - /* - * Actions, Mutations - */ - CreateColonyActionMetadata = 'createColonyActionMetadata', - UpdateColonyAction = 'updateColonyAction', - CreateAnnotation = 'createAnnotation', - CreateColonyDecision = 'createColonyDecision', - /* - * Tokens - */ - GetTokenFromEverywhere = 'getTokenFromEverywhere', - CreateColonyTokens = 'createColonyTokens', - DeleteColonyTokens = 'deleteColonyTokens', - /* - * Expenditures - */ - CreateExpenditureMetadata = 'createExpenditureMetadata', - CreateStreamingPaymentMetadata = 'createStreamingPaymentMetadata', - UpdateStreamingPaymentMetadata = 'updateStreamingPaymentMetadata', - /* - * Bridge / Crypto-to-fiat - */ - BridgeXYZMutation = 'bridgeXYZMutation', - BridgeCreateBankAccount = 'bridgeCreateBankAccount', - BridgeUpdateBankAccount = 'bridgeUpdateBankAccount', -} - -// All queries are allowed by default, add exceptions with specific rules here -export enum QueryOperations { - /* - * Notifications - */ - GetUserNotificationsHMAC = 'getUserNotificationsHMAC', -} - export enum HttpStatuses { OK = 200, BAD_REQUEST = 400, @@ -152,9 +79,3 @@ export type UserRole = { role_5: boolean | null; role_6: boolean | null; }; - -export interface ParsedOperation { - type: OperationTypes; - field: string; - variables: Record | undefined; -} diff --git a/src/validateRequest.ts b/src/validateRequest.ts new file mode 100644 index 0000000..b2f7141 --- /dev/null +++ b/src/validateRequest.ts @@ -0,0 +1,165 @@ +import { + DocumentNode, + GraphQLSchema, + TypeInfo, + visit, + visitWithTypeInfo, +} from 'graphql'; +import { RulesConfig, AuthContext, FieldRule } from './rules'; +import { logger } from './helpers'; + +export async function validateRequest( + document: DocumentNode, + schema: GraphQLSchema, + rules: RulesConfig, + ctx: Omit, +): Promise { + let allowed = true; + const pendingChecks: Array<{ + key: string; + check: () => Promise; + }> = []; + const typeInfo = new TypeInfo(schema); + const path: string[] = []; + + logger('[validateRequest] Starting validation'); + + visit( + document, + visitWithTypeInfo(typeInfo, { + Field: { + enter(node) { + const parentType = typeInfo.getParentType(); + const fieldName = node.name.value; + + path.push(fieldName); + + if (!parentType) return; + + const typeName = parentType.name; + const key = `${typeName}.${fieldName}`; + const currentPath = [...path]; + const pathString = currentPath.join('.'); + const ctxWithPath: AuthContext = { ...ctx, path: currentPath }; + + // 1. Check path rules (exact → parent) + const parentPath = currentPath.slice(0, -1).join('.'); + const pathRule: FieldRule | undefined = + rules.paths[pathString] ?? rules.paths[parentPath]; + const matchedPath = + rules.paths[pathString] !== undefined ? pathString : parentPath; + + if (pathRule !== undefined) { + logger( + `[validateRequest] ${key} (path: ${pathString}) - PATH RULE (${matchedPath}): ${ + typeof pathRule === 'function' ? 'function' : pathRule + }`, + ); + + if (pathRule === false) { + logger(`[validateRequest] BLOCKED by path rule: ${matchedPath}`); + allowed = false; + } else if (pathRule === true) { + // Allowed + } else { + pendingChecks.push({ + key: `path:${matchedPath}`, + check: () => Promise.resolve(pathRule(ctxWithPath)), + }); + } + return; + } + + // 2. Check type rules + const typeRules = rules.types[typeName]; + + // Type: true = all fields allowed + if (typeRules === true) { + logger(`[validateRequest] ${key} - type allows all fields`); + return; + } + + // Type: false = all fields blocked + if (typeRules === false) { + logger( + `[validateRequest] BLOCKED: ${key} - type blocks all fields`, + ); + allowed = false; + return; + } + + // Type: { field: rule } = check specific field + if (typeof typeRules === 'object' && typeRules !== null) { + const fieldRule = typeRules[fieldName]; + + if (fieldRule === undefined) { + logger(`[validateRequest] BLOCKED: ${key} - not in allowed list`); + allowed = false; + return; + } + + logger( + `[validateRequest] ${key} - type rule: ${ + typeof fieldRule === 'function' ? 'function' : fieldRule + }`, + ); + + if (fieldRule === false) { + logger(`[validateRequest] BLOCKED: ${key} - rule is false`); + allowed = false; + } else if (fieldRule === true) { + // Allowed + } else { + pendingChecks.push({ + key, + check: () => Promise.resolve(fieldRule(ctxWithPath)), + }); + } + return; + } + + // No rules for this type = BLOCKED + logger(`[validateRequest] BLOCKED: ${key} - no rules for type`); + allowed = false; + }, + leave() { + path.pop(); + }, + }, + }), + ); + + if (!allowed) { + logger('[validateRequest] Blocked during field traversal'); + return false; + } + + if (pendingChecks.length === 0) { + logger('[validateRequest] All checks passed'); + return true; + } + + logger( + `[validateRequest] Running ${pendingChecks.length} async checks in parallel`, + ); + + const results = await Promise.all( + pendingChecks.map(async ({ key, check }) => { + const result = await check(); + logger( + `[validateRequest] Async check ${key}: ${ + result ? 'ALLOWED' : 'BLOCKED' + }`, + ); + return result; + }), + ); + + const allPassed = results.every(Boolean); + logger( + `[validateRequest] ${ + allPassed ? 'All checks passed' : 'Some checks failed' + }`, + ); + return allPassed; +} From 46cfa8f58f290dea4c61ee27968de559a1e881ff Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Fri, 5 Dec 2025 16:30:00 +0000 Subject: [PATCH 05/16] delete unused file --- src/routes/graphql/queries.ts | 47 ----------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/routes/graphql/queries.ts diff --git a/src/routes/graphql/queries.ts b/src/routes/graphql/queries.ts deleted file mode 100644 index 305987e..0000000 --- a/src/routes/graphql/queries.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { logger } from '~helpers'; -import { ParsedOperation, QueryOperations } from '~types'; - -const hasQueryPermission = ( - field: string, - userAddress: string | undefined, -): boolean => { - switch (field) { - /* - * GetUserNotificationsHMAC will fail if no userAddress is provided - */ - case QueryOperations.GetUserNotificationsHMAC: { - if (!userAddress) { - return false; - } - return true; - } - default: { - // By default all queries are permitted - return true; - } - } -}; - -const addressCanExecuteQuery = async ( - parsedOperation: ParsedOperation, - userAddress: string | undefined, -): Promise => { - const { field, variables } = parsedOperation; - - try { - return hasQueryPermission(field, userAddress); - } catch (error) { - logger( - `Error when attempting to check if user ${userAddress} can execute query ${field} with variables ${JSON.stringify( - variables, - )}`, - error, - ); - /* - * If anything fails still allow the query to execute as by default all queries are permitted - */ - return true; - } -}; - -export default addressCanExecuteQuery; From f0f0d97050779622cbbccf272b50dc440530ea12 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Mon, 8 Dec 2025 22:20:50 +0000 Subject: [PATCH 06/16] Make validateRequest work with fragments (spread & inline) --- src/validateRequest.ts | 278 ++++++++++++++++++++++++++++++----------- 1 file changed, 205 insertions(+), 73 deletions(-) diff --git a/src/validateRequest.ts b/src/validateRequest.ts index b2f7141..ba29406 100644 --- a/src/validateRequest.ts +++ b/src/validateRequest.ts @@ -8,6 +8,101 @@ import { import { RulesConfig, AuthContext, FieldRule } from './rules'; import { logger } from './helpers'; +interface FragmentSpreadInfo { + atPath: string[]; + inFragment: string | null; +} + +/** + * Collects where each fragment is spread in the document. + * Tracks the path at the spread location and whether it's inside another fragment. + */ +function collectFragmentSpreads( + document: DocumentNode, +): Map { + const spreads = new Map(); + let currentFragment: string | null = null; + const path: string[] = []; + + visit(document, { + OperationDefinition: { + enter() { + currentFragment = null; + }, + }, + FragmentDefinition: { + enter(node) { + currentFragment = node.name.value; + }, + leave() { + currentFragment = null; + }, + }, + Field: { + enter(node) { + path.push(node.name.value); + }, + leave() { + path.pop(); + }, + }, + FragmentSpread(node) { + const name = node.name.value; + if (!spreads.has(name)) { + spreads.set(name, []); + } + spreads.get(name)!.push({ + atPath: [...path], + inFragment: currentFragment, + }); + }, + }); + + return spreads; +} + +/** + * Resolves the full paths where a fragment's fields should be validated. + * Handles nested fragments by recursively resolving parent fragment contexts. + */ +function resolveFragmentContexts( + spreads: Map, +): Map { + const resolved = new Map(); + + function resolvePaths( + fragmentName: string, + visited: Set, + ): string[][] { + if (visited.has(fragmentName)) return []; // Prevent cycles + visited.add(fragmentName); + + const fragmentSpreads = spreads.get(fragmentName) || []; + const paths: string[][] = []; + + for (const spread of fragmentSpreads) { + if (spread.inFragment === null) { + // Spread directly in operation + paths.push(spread.atPath); + } else { + // Spread inside another fragment - resolve parent paths first + const parentPaths = resolvePaths(spread.inFragment, new Set(visited)); + for (const parentPath of parentPaths) { + paths.push([...parentPath, ...spread.atPath]); + } + } + } + + return paths; + } + + for (const fragmentName of spreads.keys()) { + resolved.set(fragmentName, resolvePaths(fragmentName, new Set())); + } + + return resolved; +} + export async function validateRequest( document: DocumentNode, schema: GraphQLSchema, @@ -20,109 +115,146 @@ export async function validateRequest( check: () => Promise; }> = []; const typeInfo = new TypeInfo(schema); - const path: string[] = []; logger('[validateRequest] Starting validation'); + // Build fragment context map (handles nested fragments) + const fragmentSpreads = collectFragmentSpreads(document); + const fragmentContexts = resolveFragmentContexts(fragmentSpreads); + + const path: string[] = []; + let activeFragment: string | null = null; + visit( document, visitWithTypeInfo(typeInfo, { + FragmentDefinition: { + enter(node) { + activeFragment = node.name.value; + }, + leave() { + activeFragment = null; + }, + }, Field: { enter(node) { const parentType = typeInfo.getParentType(); const fieldName = node.name.value; + console.log('Entering Field:', { + fieldName, + parentType: parentType?.name, + }); + path.push(fieldName); + if (fieldName === '__typename') return; if (!parentType) return; const typeName = parentType.name; const key = `${typeName}.${fieldName}`; - const currentPath = [...path]; - const pathString = currentPath.join('.'); - const ctxWithPath: AuthContext = { ...ctx, path: currentPath }; - - // 1. Check path rules (exact → parent) - const parentPath = currentPath.slice(0, -1).join('.'); - const pathRule: FieldRule | undefined = - rules.paths[pathString] ?? rules.paths[parentPath]; - const matchedPath = - rules.paths[pathString] !== undefined ? pathString : parentPath; - - if (pathRule !== undefined) { - logger( - `[validateRequest] ${key} (path: ${pathString}) - PATH RULE (${matchedPath}): ${ - typeof pathRule === 'function' ? 'function' : pathRule - }`, - ); - - if (pathRule === false) { - logger(`[validateRequest] BLOCKED by path rule: ${matchedPath}`); - allowed = false; - } else if (pathRule === true) { - // Allowed - } else { - pendingChecks.push({ - key: `path:${matchedPath}`, - check: () => Promise.resolve(pathRule(ctxWithPath)), - }); - } - return; + + // Build full paths including fragment spread context + let fullPaths: string[][]; + if (activeFragment) { + const contexts = fragmentContexts.get(activeFragment) || [[]]; + fullPaths = contexts.map((ctx) => [...ctx, ...path]); + } else { + fullPaths = [[...path]]; } - // 2. Check type rules - const typeRules = rules.types[typeName]; + console.log({ fullPaths, activeFragment }); - // Type: true = all fields allowed - if (typeRules === true) { - logger(`[validateRequest] ${key} - type allows all fields`); - return; - } + // Validate against each full path + // If fragment is spread in multiple places, all must pass + for (const currentPath of fullPaths) { + const ctxWithPath: AuthContext = { ...ctx, path: currentPath }; + const pathString = currentPath.join('.'); + const parentPath = currentPath.slice(0, -1).join('.'); - // Type: false = all fields blocked - if (typeRules === false) { - logger( - `[validateRequest] BLOCKED: ${key} - type blocks all fields`, - ); - allowed = false; - return; - } + // 1. Check path rules (exact → parent) + const pathRule: FieldRule | undefined = + rules.paths[pathString] ?? rules.paths[parentPath]; + const matchedPath = + rules.paths[pathString] !== undefined ? pathString : parentPath; - // Type: { field: rule } = check specific field - if (typeof typeRules === 'object' && typeRules !== null) { - const fieldRule = typeRules[fieldName]; + if (pathRule !== undefined) { + logger( + `[validateRequest] ${key} (path: ${pathString}) - PATH RULE (${matchedPath}): ${ + typeof pathRule === 'function' ? 'function' : pathRule + }`, + ); - if (fieldRule === undefined) { - logger(`[validateRequest] BLOCKED: ${key} - not in allowed list`); - allowed = false; - return; + if (pathRule === false) { + logger( + `[validateRequest] BLOCKED by path rule: ${matchedPath}`, + ); + allowed = false; + } else if (pathRule === true) { + // Allowed + } else { + pendingChecks.push({ + key: `path:${matchedPath}`, + check: () => Promise.resolve(pathRule(ctxWithPath)), + }); + } + continue; // Path rule handled this path, skip type check } - logger( - `[validateRequest] ${key} - type rule: ${ - typeof fieldRule === 'function' ? 'function' : fieldRule - }`, - ); + // 2. Check type rules + const typeRules = rules.types[typeName]; + + if (typeRules === true) { + logger(`[validateRequest] ${key} - type allows all fields`); + continue; + } - if (fieldRule === false) { - logger(`[validateRequest] BLOCKED: ${key} - rule is false`); + if (typeRules === false) { + logger( + `[validateRequest] BLOCKED: ${key} - type blocks all fields`, + ); allowed = false; - } else if (fieldRule === true) { - // Allowed - } else { - pendingChecks.push({ - key, - check: () => Promise.resolve(fieldRule(ctxWithPath)), - }); + continue; } - return; - } - // No rules for this type = BLOCKED - logger(`[validateRequest] BLOCKED: ${key} - no rules for type`); - allowed = false; + if (typeof typeRules === 'object' && typeRules !== null) { + const fieldRule = typeRules[fieldName]; + + if (fieldRule === undefined) { + logger( + `[validateRequest] BLOCKED: ${key} - not in allowed list`, + ); + allowed = false; + continue; + } + + logger( + `[validateRequest] ${key} - type rule: ${ + typeof fieldRule === 'function' ? 'function' : fieldRule + }`, + ); + + if (fieldRule === false) { + logger(`[validateRequest] BLOCKED: ${key} - rule is false`); + allowed = false; + } else if (fieldRule === true) { + // Allowed + } else { + pendingChecks.push({ + key, + check: () => Promise.resolve(fieldRule(ctxWithPath)), + }); + } + continue; + } + + // No rules for this type = BLOCKED + logger(`[validateRequest] BLOCKED: ${key} - no rules for type`); + allowed = false; + } }, - leave() { + leave(node) { + console.log('Leaving Field:', { fieldName: node.name.value }); path.pop(); }, }, From 5516ad48f656b1b55cdd311fbe28bcef343830d9 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Mon, 8 Dec 2025 22:21:11 +0000 Subject: [PATCH 07/16] Add missing types to rules to make UI work --- src/rules.ts | 86 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/src/rules.ts b/src/rules.ts index 5e388c6..aad8918 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -352,6 +352,7 @@ export const rules: RulesConfig = { getTokenFromEverywhere: true, updateContributorsWithReputation: true, }, + Subscription: true, User: { id: true, @@ -416,6 +417,75 @@ export const rules: RulesConfig = { TokenExchangeRate: true, CacheTotalBalance: true, LiquidationAddress: true, + InitializeUserReturn: true, + FailedTransaction: true, + + // Return/Result types + DomainBalanceInOut: true, + TimeframeDomainBalanceInOut: true, + DomainBalanceReturn: true, + CacheAllDomainBalanceReturn: true, + MarketPrice: true, + BridgeCheckKYCReturn: true, + BridgeGatewayFeeReturn: true, + BridgeDrainReceipt: true, + BridgeDrain: true, + BridgeIbanBankAccount: true, + BridgeUsBankAccount: true, + BridgeBankAccount: true, + BridgeCreateBankAccountReturn: true, + BridgeUpdateBankAccountReturn: true, + BridgeXYZMutationReturn: true, + TokenFromEverywhereReturn: true, + GetUserTokenBalanceReturn: true, + GetMotionTimeoutPeriodsReturn: true, + VoterRewardsReturn: true, + + // Nested/Embedded types + NativeTokenStatus: true, + ColonyStatus: true, + ChainMetadata: true, + ProfileMetadata: true, + ColonyUnclaimedStake: true, + ExternalLink: true, + ColonyObjective: true, + ColonyMetadataEtherealData: true, + ColonyMetadataChangelog: true, + TransactionGroup: true, + TransactionError: true, + DomainMetadataChangelog: true, + ColonyChainFundsClaim: true, + ColonyBalance: true, + ColonyBalances: true, + MotionStakeValues: true, + MotionStakes: true, + UserMotionStakes: true, + StakerRewards: true, + VoterRecord: true, + MotionStateHistory: true, + ExpenditureFundingItem: true, + VotingReputationParams: true, + MultiSigDomainConfig: true, + MultiSigParams: true, + StakedExpenditureParams: true, + ExtensionParams: true, + Payment: true, + ApprovedTokenChanges: true, + ExpenditureSlotChanges: true, + ArbitraryTxAbi: true, + ColonyActionRoles: true, + ColonyActionArbitraryTransaction: true, + ExpenditureSlot: true, + ExpenditurePayout: true, + ExpenditureBalance: true, + ExpenditureStage: true, + SimpleTargetProfile: true, + SimpleTarget: true, + NFT: true, + NFTProfile: true, + FunctionParam: true, + NFTData: true, + Safe: true, // Connection types ModelColonyConnection: true, @@ -460,9 +530,16 @@ export const rules: RulesConfig = { ModelContributorReputationConnection: true, ModelCurrentVersionConnection: true, ModelCurrentNetworkInverseFeeConnection: true, + ModelNotificationsDataConnection: true, + ModelLiquidationAddressConnection: true, ModelColonyMultiSigFilterInput: true, SearchableColonyContributorConnection: true, SearchableColonyActionConnection: true, + SearchableAggregateResult: true, + SearchableAggregateGenericResult: true, + SearchableAggregateScalarResult: true, + SearchableAggregateBucketResult: true, + SearchableAggregateBucketResultItem: true, NotificationsData: false, }, @@ -472,9 +549,10 @@ export const rules: RulesConfig = { // Check exact path, then parent path // ============================================================ paths: { - 'getUserByAddress.bridgeCustomerId': isOwnUser(), - 'getUserByAddress.notificationsData': isOwnUser(), - 'getUserByAddress.privateBetaInviteCode': isOwnUser(), - 'getUserByAddress.profile.email': isOwnUser(), + 'getUserByAddress.items.bridgeCustomerId': isOwnUser(), + 'getUserByAddress.items.notificationsData': isOwnUser(), + 'getUserByAddress.items.privateBetaInviteCode': isOwnUser(), + 'getUserByAddress.items.profile': isOwnUser(), + 'getUserByAddress.items.profile.meta': isOwnUser(), }, }; From a54b796cb57d8a59ab40066bec6d95d3df39641b Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Tue, 9 Dec 2025 21:52:36 +0000 Subject: [PATCH 08/16] Drop http-proxy-middleware. Use schema wrapping with graphql-shield --- package-lock.json | 1119 +++++++++++++++++++++++++++++++++++++------- package.json | 5 +- src/permissions.ts | 92 ++++ src/schema.ts | 39 +- src/server.ts | 49 +- src/types.ts | 6 +- tsconfig.json | 3 - 7 files changed, 1105 insertions(+), 208 deletions(-) create mode 100644 src/permissions.ts diff --git a/package-lock.json b/package-lock.json index 058a4cd..3fad0e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,13 @@ "license": "MIT", "dependencies": { "@colony/core": "^2.0.1", + "@graphql-tools/executor-http": "^3.0.7", + "@graphql-tools/wrap": "^11.1.2", "cors": "^2.8.5", "express": "^4.18.2", "express-session": "^1.17.3", - "http-proxy-middleware": "^2.0.6", + "graphql-middleware": "^6.1.35", + "graphql-shield": "^7.6.5", "node-fetch": "2.6", "siwe": "^2.1.4", "ws": "^8.16.0" @@ -35,6 +38,15 @@ "typescript": "^5.9.3" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@colony/core": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@colony/core/-/core-2.0.1.tgz", @@ -59,6 +71,47 @@ "node": ">=12" } }, + "node_modules/@envelop/core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.4.0.tgz", + "integrity": "sha512-/1fat63pySE8rw/dZZArEVytLD90JApY85deDJ0/34gm+yhQ3k70CloSUevxoOE4YCGveG3s9SJJfQeeB4NAtQ==", + "license": "MIT", + "dependencies": { + "@envelop/instrumentation": "^1.0.0", + "@envelop/types": "^5.2.1", + "@whatwg-node/promise-helpers": "^1.2.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/instrumentation": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz", + "integrity": "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==", + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.2.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/types": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz", + "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==", + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@ethersproject/abi": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", @@ -781,6 +834,199 @@ "@ethersproject/strings": "^5.7.0" } }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/@graphql-hive/signal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@graphql-hive/signal/-/signal-2.0.0.tgz", + "integrity": "sha512-Pz8wB3K0iU6ae9S1fWfsmJX24CcGeTo6hE7T44ucmV/ALKRj+bxClmqrYcDT7v3f0d12Rh4FAXBb6gon+WkDpQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@graphql-tools/batch-execute": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-10.0.4.tgz", + "integrity": "sha512-t8E0ILelbaIju0aNujMkKetUmbv3/07nxGSv0kEGLBk9GNtEmQ/Bjj8ZTo2WN35/Fy70zCHz2F/48Nx/Ec48cA==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^10.10.3", + "@whatwg-node/promise-helpers": "^1.3.2", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/delegate": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-12.0.2.tgz", + "integrity": "sha512-1X93onxNgOzRvnZ8Xulwi6gNuBeuDxvGYOjUHEZyesPCsaWsyiVj1Wk6Pw/DTPGLy70sOFUKQGcaZbWnDORM2w==", + "license": "MIT", + "dependencies": { + "@graphql-tools/batch-execute": "^10.0.4", + "@graphql-tools/executor": "^1.4.13", + "@graphql-tools/schema": "^10.0.29", + "@graphql-tools/utils": "^10.10.3", + "@repeaterjs/repeater": "^3.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.5.0.tgz", + "integrity": "sha512-3HzAxfexmynEWwRB56t/BT+xYKEYLGPvJudR1jfs+XZX8bpfqujEhqVFoxmkpEE8BbFcKuBNoQyGkTi1eFJ+hA==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^10.11.0", + "@graphql-typed-document-node/core": "^3.2.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-common": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-1.0.5.tgz", + "integrity": "sha512-gsBRxP4ui8s7/ppKGCJUQ9xxTNoFpNYmEirgM52EHo74hL5hrpS5o4zOmBH33+9t2ZasBziIfupYtLNa0DgK0g==", + "license": "MIT", + "dependencies": { + "@envelop/core": "^5.4.0", + "@graphql-tools/utils": "^10.10.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-http": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-3.0.7.tgz", + "integrity": "sha512-sHjtiUZmRtkjhpSzMhxT2ywAGzHjuB1rHsiaSLAq8U5BQg5WoLakKYD7BajgVHwNbfWEc+NnFiJI7ldyhiciiQ==", + "license": "MIT", + "dependencies": { + "@graphql-hive/signal": "^2.0.0", + "@graphql-tools/executor-common": "^1.0.5", + "@graphql-tools/utils": "^10.10.3", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.3.2", + "meros": "^1.3.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/merge": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.6.tgz", + "integrity": "sha512-bTnP+4oom4nDjmkS3Ykbe+ljAp/RIiWP3R35COMmuucS24iQxGLa9Hn8VMkLIoaoPxgz6xk+dbC43jtkNsFoBw==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^10.11.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema": { + "version": "10.0.30", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.30.tgz", + "integrity": "sha512-yPXU17uM/LR90t92yYQqn9mAJNOVZJc0nQtYeZyZeQZeQjwIGlTubvvoDL0fFVk+wZzs4YQOgds2NwSA4npodA==", + "license": "MIT", + "dependencies": { + "@graphql-tools/merge": "^9.1.6", + "@graphql-tools/utils": "^10.11.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/utils": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.11.0.tgz", + "integrity": "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/wrap": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-11.1.2.tgz", + "integrity": "sha512-TcKZzUzJNmuyMBQ1oMdnxhBUUacN/5VEJu0/1KVce2aIzCwTTaN9JTU3MgjO7l5Ixn4QLkc6XbxYNv0cHDQgtQ==", + "license": "MIT", + "dependencies": { + "@graphql-tools/delegate": "^12.0.2", + "@graphql-tools/schema": "^10.0.29", + "@graphql-tools/utils": "^10.10.3", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -879,6 +1125,12 @@ "node": ">=14" } }, + "node_modules/@repeaterjs/repeater": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", + "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==", + "license": "MIT" + }, "node_modules/@spruceid/siwe-parser": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-2.0.2.tgz", @@ -945,7 +1197,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "devOptional": true, + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -955,7 +1207,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "devOptional": true, + "dev": true, "dependencies": { "@types/node": "*" } @@ -973,7 +1225,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "devOptional": true, + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -985,7 +1237,7 @@ "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "devOptional": true, + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1016,26 +1268,25 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "devOptional": true + "dev": true }, - "node_modules/@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", - "dependencies": { - "@types/node": "*" - } + "node_modules/@types/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", + "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "devOptional": true + "dev": true }, "node_modules/@types/node": { "version": "20.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1054,19 +1305,19 @@ "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", - "devOptional": true + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "devOptional": true + "dev": true }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "devOptional": true, + "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -1076,7 +1327,7 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "devOptional": true, + "dev": true, "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -1092,6 +1343,65 @@ "@types/node": "*" } }, + "node_modules/@types/yup": { + "version": "0.29.13", + "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.13.tgz", + "integrity": "sha512-qRyuv+P/1t1JK1rA+elmK1MmCL1BapEzKKfbEhDBV/LMMse4lmhZ/XbgETI39JveDJRpLjmToOI6uFtMW/WR2g==", + "license": "MIT" + }, + "node_modules/@whatwg-node/disposablestack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", + "integrity": "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==", + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.10.13", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.13.tgz", + "integrity": "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==", + "license": "MIT", + "dependencies": { + "@whatwg-node/node-fetch": "^0.8.3", + "urlpattern-polyfill": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/node-fetch": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.8.4.tgz", + "integrity": "sha512-AlKLc57loGoyYlrzDbejB9EeR+pfdJdGzbYnkEuZaGekFboBwzfVYVMsy88PMriqPI1ORpiGYGgSSWpx7a2sDA==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.1.1", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/promise-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz", + "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1246,6 +1556,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1402,6 +1713,18 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", + "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1416,6 +1739,12 @@ "node": ">= 8" } }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1603,11 +1932,6 @@ "@ethersproject/wordlists": "5.7.0" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -1741,6 +2065,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1765,25 +2090,6 @@ "node": ">= 0.8" } }, - "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -1959,11 +2265,123 @@ "version": "16.8.1", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-middleware": { + "version": "6.1.35", + "resolved": "https://registry.npmjs.org/graphql-middleware/-/graphql-middleware-6.1.35.tgz", + "integrity": "sha512-azawK7ApUYtcuPGRGBR9vDZu795pRuaFhO5fgomdJppdfKRt7jwncuh0b7+D3i574/4B+16CNWgVpnGVlg3ZCg==", + "license": "MIT", + "dependencies": { + "@graphql-tools/delegate": "^8.8.1", + "@graphql-tools/schema": "^8.5.1" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/graphql-middleware/node_modules/@graphql-tools/batch-execute": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.1.tgz", + "integrity": "sha512-hRVDduX0UDEneVyEWtc2nu5H2PxpfSfM/riUlgZvo/a/nG475uyehxR5cFGvTEPEQUKY3vGIlqvtRigzqTfCew==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "8.9.0", + "dataloader": "2.1.0", + "tslib": "^2.4.0", + "value-or-promise": "1.0.11" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-middleware/node_modules/@graphql-tools/delegate": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-8.8.1.tgz", + "integrity": "sha512-NDcg3GEQmdEHlnF7QS8b4lM1PSF+DKeFcIlLEfZFBvVq84791UtJcDj8734sIHLukmyuAxXMfA1qLd2l4lZqzA==", + "license": "MIT", + "dependencies": { + "@graphql-tools/batch-execute": "8.5.1", + "@graphql-tools/schema": "8.5.1", + "@graphql-tools/utils": "8.9.0", + "dataloader": "2.1.0", + "tslib": "~2.4.0", + "value-or-promise": "1.0.11" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-middleware/node_modules/@graphql-tools/merge": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.1.tgz", + "integrity": "sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "8.9.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-middleware/node_modules/@graphql-tools/schema": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.5.1.tgz", + "integrity": "sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg==", + "license": "MIT", + "dependencies": { + "@graphql-tools/merge": "8.3.1", + "@graphql-tools/utils": "8.9.0", + "tslib": "^2.4.0", + "value-or-promise": "1.0.11" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-middleware/node_modules/@graphql-tools/utils": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.9.0.tgz", + "integrity": "sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-middleware/node_modules/dataloader": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.1.0.tgz", + "integrity": "sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ==", + "license": "MIT" + }, + "node_modules/graphql-middleware/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "license": "0BSD" + }, + "node_modules/graphql-shield": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/graphql-shield/-/graphql-shield-7.6.5.tgz", + "integrity": "sha512-etbzf7UIhQW6vadn/UR+ds0LJOceO8ITDXwbUkQMlP2KqPgSKTZRE2zci+AUfqP+cpV9zDQdbTJfPfW5OCEamg==", + "license": "MIT", + "dependencies": { + "@types/yup": "0.29.13", + "object-hash": "^3.0.0", + "tslib": "^2.4.0", + "yup": "^0.32.0" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql-middleware": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^6.0.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2053,42 +2471,6 @@ "node": ">= 0.8" } }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2144,6 +2526,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2161,6 +2544,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2172,21 +2556,11 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2229,6 +2603,18 @@ "node": ">=6" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2269,6 +2655,23 @@ "node": ">= 8" } }, + "node_modules/meros": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.2.tgz", + "integrity": "sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A==", + "license": "MIT", + "engines": { + "node": ">=13" + }, + "peerDependencies": { + "@types/node": ">=13" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2281,6 +2684,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -2379,6 +2783,12 @@ "url": "https://github.com/sponsors/raouldeheer" } }, + "node_modules/nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2481,6 +2891,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -2568,6 +2987,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -2587,6 +3007,12 @@ "node": ">=12" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2684,11 +3110,6 @@ "node": ">=8.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3050,6 +3471,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -3065,6 +3487,12 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -3156,6 +3584,12 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3202,7 +3636,8 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true }, "node_modules/unpipe": { "version": "1.0.0", @@ -3220,6 +3655,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -3239,6 +3680,15 @@ "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" }, + "node_modules/value-or-promise": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", + "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3395,9 +3845,32 @@ "engines": { "node": ">=6" } + }, + "node_modules/yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + }, + "engines": { + "node": ">=10" + } } }, "dependencies": { + "@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==" + }, "@colony/core": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@colony/core/-/core-2.0.1.tgz", @@ -3413,6 +3886,35 @@ "@jridgewell/trace-mapping": "0.3.9" } }, + "@envelop/core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.4.0.tgz", + "integrity": "sha512-/1fat63pySE8rw/dZZArEVytLD90JApY85deDJ0/34gm+yhQ3k70CloSUevxoOE4YCGveG3s9SJJfQeeB4NAtQ==", + "requires": { + "@envelop/instrumentation": "^1.0.0", + "@envelop/types": "^5.2.1", + "@whatwg-node/promise-helpers": "^1.2.4", + "tslib": "^2.5.0" + } + }, + "@envelop/instrumentation": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz", + "integrity": "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==", + "requires": { + "@whatwg-node/promise-helpers": "^1.2.1", + "tslib": "^2.5.0" + } + }, + "@envelop/types": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz", + "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==", + "requires": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.5.0" + } + }, "@ethersproject/abi": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", @@ -3823,6 +4325,128 @@ "@ethersproject/strings": "^5.7.0" } }, + "@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==" + }, + "@graphql-hive/signal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@graphql-hive/signal/-/signal-2.0.0.tgz", + "integrity": "sha512-Pz8wB3K0iU6ae9S1fWfsmJX24CcGeTo6hE7T44ucmV/ALKRj+bxClmqrYcDT7v3f0d12Rh4FAXBb6gon+WkDpQ==" + }, + "@graphql-tools/batch-execute": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-10.0.4.tgz", + "integrity": "sha512-t8E0ILelbaIju0aNujMkKetUmbv3/07nxGSv0kEGLBk9GNtEmQ/Bjj8ZTo2WN35/Fy70zCHz2F/48Nx/Ec48cA==", + "requires": { + "@graphql-tools/utils": "^10.10.3", + "@whatwg-node/promise-helpers": "^1.3.2", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + } + }, + "@graphql-tools/delegate": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-12.0.2.tgz", + "integrity": "sha512-1X93onxNgOzRvnZ8Xulwi6gNuBeuDxvGYOjUHEZyesPCsaWsyiVj1Wk6Pw/DTPGLy70sOFUKQGcaZbWnDORM2w==", + "requires": { + "@graphql-tools/batch-execute": "^10.0.4", + "@graphql-tools/executor": "^1.4.13", + "@graphql-tools/schema": "^10.0.29", + "@graphql-tools/utils": "^10.10.3", + "@repeaterjs/repeater": "^3.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + } + }, + "@graphql-tools/executor": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.5.0.tgz", + "integrity": "sha512-3HzAxfexmynEWwRB56t/BT+xYKEYLGPvJudR1jfs+XZX8bpfqujEhqVFoxmkpEE8BbFcKuBNoQyGkTi1eFJ+hA==", + "requires": { + "@graphql-tools/utils": "^10.11.0", + "@graphql-typed-document-node/core": "^3.2.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/executor-common": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-1.0.5.tgz", + "integrity": "sha512-gsBRxP4ui8s7/ppKGCJUQ9xxTNoFpNYmEirgM52EHo74hL5hrpS5o4zOmBH33+9t2ZasBziIfupYtLNa0DgK0g==", + "requires": { + "@envelop/core": "^5.4.0", + "@graphql-tools/utils": "^10.10.3" + } + }, + "@graphql-tools/executor-http": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-3.0.7.tgz", + "integrity": "sha512-sHjtiUZmRtkjhpSzMhxT2ywAGzHjuB1rHsiaSLAq8U5BQg5WoLakKYD7BajgVHwNbfWEc+NnFiJI7ldyhiciiQ==", + "requires": { + "@graphql-hive/signal": "^2.0.0", + "@graphql-tools/executor-common": "^1.0.5", + "@graphql-tools/utils": "^10.10.3", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.3.2", + "meros": "^1.3.2", + "tslib": "^2.8.1" + } + }, + "@graphql-tools/merge": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.6.tgz", + "integrity": "sha512-bTnP+4oom4nDjmkS3Ykbe+ljAp/RIiWP3R35COMmuucS24iQxGLa9Hn8VMkLIoaoPxgz6xk+dbC43jtkNsFoBw==", + "requires": { + "@graphql-tools/utils": "^10.11.0", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/schema": { + "version": "10.0.30", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.30.tgz", + "integrity": "sha512-yPXU17uM/LR90t92yYQqn9mAJNOVZJc0nQtYeZyZeQZeQjwIGlTubvvoDL0fFVk+wZzs4YQOgds2NwSA4npodA==", + "requires": { + "@graphql-tools/merge": "^9.1.6", + "@graphql-tools/utils": "^10.11.0", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/utils": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.11.0.tgz", + "integrity": "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==", + "requires": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/wrap": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-11.1.2.tgz", + "integrity": "sha512-TcKZzUzJNmuyMBQ1oMdnxhBUUacN/5VEJu0/1KVce2aIzCwTTaN9JTU3MgjO7l5Ixn4QLkc6XbxYNv0cHDQgtQ==", + "requires": { + "@graphql-tools/delegate": "^12.0.2", + "@graphql-tools/schema": "^10.0.29", + "@graphql-tools/utils": "^10.10.3", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.8.1" + } + }, + "@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "requires": {} + }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3897,6 +4521,11 @@ "dev": true, "optional": true }, + "@repeaterjs/repeater": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", + "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==" + }, "@spruceid/siwe-parser": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-2.0.2.tgz", @@ -3963,7 +4592,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "devOptional": true, + "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -3973,7 +4602,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "devOptional": true, + "dev": true, "requires": { "@types/node": "*" } @@ -3991,7 +4620,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "devOptional": true, + "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -4003,7 +4632,7 @@ "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "devOptional": true, + "dev": true, "requires": { "@types/node": "*", "@types/qs": "*", @@ -4033,26 +4662,24 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "devOptional": true + "dev": true }, - "@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", - "requires": { - "@types/node": "*" - } + "@types/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==" }, "@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "devOptional": true + "dev": true }, "@types/node": { "version": "20.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "devOptional": true, "requires": { "undici-types": "~5.26.4" } @@ -4071,19 +4698,19 @@ "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", - "devOptional": true + "dev": true }, "@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "devOptional": true + "dev": true }, "@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "devOptional": true, + "dev": true, "requires": { "@types/mime": "^1", "@types/node": "*" @@ -4093,7 +4720,7 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "devOptional": true, + "dev": true, "requires": { "@types/http-errors": "*", "@types/mime": "*", @@ -4109,6 +4736,48 @@ "@types/node": "*" } }, + "@types/yup": { + "version": "0.29.13", + "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.13.tgz", + "integrity": "sha512-qRyuv+P/1t1JK1rA+elmK1MmCL1BapEzKKfbEhDBV/LMMse4lmhZ/XbgETI39JveDJRpLjmToOI6uFtMW/WR2g==" + }, + "@whatwg-node/disposablestack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", + "integrity": "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==", + "requires": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.6.3" + } + }, + "@whatwg-node/fetch": { + "version": "0.10.13", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.13.tgz", + "integrity": "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==", + "requires": { + "@whatwg-node/node-fetch": "^0.8.3", + "urlpattern-polyfill": "^10.0.0" + } + }, + "@whatwg-node/node-fetch": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.8.4.tgz", + "integrity": "sha512-AlKLc57loGoyYlrzDbejB9EeR+pfdJdGzbYnkEuZaGekFboBwzfVYVMsy88PMriqPI1ORpiGYGgSSWpx7a2sDA==", + "requires": { + "@fastify/busboy": "^3.1.1", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.6.3" + } + }, + "@whatwg-node/promise-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz", + "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==", + "requires": { + "tslib": "^2.6.3" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4230,6 +4899,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -4345,6 +5015,14 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "cross-inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", + "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", + "requires": { + "tslib": "^2.4.0" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4356,6 +5034,11 @@ "which": "^2.0.1" } }, + "dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4504,11 +5187,6 @@ "@ethersproject/wordlists": "5.7.0" } }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, "express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -4627,6 +5305,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -4645,11 +5324,6 @@ "unpipe": "~1.0.0" } }, - "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" - }, "foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -4771,8 +5445,91 @@ "graphql": { "version": "16.8.1", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", - "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", - "dev": true + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==" + }, + "graphql-middleware": { + "version": "6.1.35", + "resolved": "https://registry.npmjs.org/graphql-middleware/-/graphql-middleware-6.1.35.tgz", + "integrity": "sha512-azawK7ApUYtcuPGRGBR9vDZu795pRuaFhO5fgomdJppdfKRt7jwncuh0b7+D3i574/4B+16CNWgVpnGVlg3ZCg==", + "requires": { + "@graphql-tools/delegate": "^8.8.1", + "@graphql-tools/schema": "^8.5.1" + }, + "dependencies": { + "@graphql-tools/batch-execute": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.1.tgz", + "integrity": "sha512-hRVDduX0UDEneVyEWtc2nu5H2PxpfSfM/riUlgZvo/a/nG475uyehxR5cFGvTEPEQUKY3vGIlqvtRigzqTfCew==", + "requires": { + "@graphql-tools/utils": "8.9.0", + "dataloader": "2.1.0", + "tslib": "^2.4.0", + "value-or-promise": "1.0.11" + } + }, + "@graphql-tools/delegate": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-8.8.1.tgz", + "integrity": "sha512-NDcg3GEQmdEHlnF7QS8b4lM1PSF+DKeFcIlLEfZFBvVq84791UtJcDj8734sIHLukmyuAxXMfA1qLd2l4lZqzA==", + "requires": { + "@graphql-tools/batch-execute": "8.5.1", + "@graphql-tools/schema": "8.5.1", + "@graphql-tools/utils": "8.9.0", + "dataloader": "2.1.0", + "tslib": "~2.4.0", + "value-or-promise": "1.0.11" + } + }, + "@graphql-tools/merge": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.1.tgz", + "integrity": "sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==", + "requires": { + "@graphql-tools/utils": "8.9.0", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/schema": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.5.1.tgz", + "integrity": "sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg==", + "requires": { + "@graphql-tools/merge": "8.3.1", + "@graphql-tools/utils": "8.9.0", + "tslib": "^2.4.0", + "value-or-promise": "1.0.11" + } + }, + "@graphql-tools/utils": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.9.0.tgz", + "integrity": "sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg==", + "requires": { + "tslib": "^2.4.0" + } + }, + "dataloader": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.1.0.tgz", + "integrity": "sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ==" + }, + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + } + } + }, + "graphql-shield": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/graphql-shield/-/graphql-shield-7.6.5.tgz", + "integrity": "sha512-etbzf7UIhQW6vadn/UR+ds0LJOceO8ITDXwbUkQMlP2KqPgSKTZRE2zci+AUfqP+cpV9zDQdbTJfPfW5OCEamg==", + "requires": { + "@types/yup": "0.29.13", + "object-hash": "^3.0.0", + "tslib": "^2.4.0", + "yup": "^0.32.0" + } }, "has-flag": { "version": "3.0.0", @@ -4839,28 +5596,6 @@ "toidentifier": "1.0.1" } }, - "http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", - "requires": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - } - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4903,7 +5638,8 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -4915,6 +5651,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -4922,12 +5659,8 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true }, "isexe": { "version": "2.0.0", @@ -4957,6 +5690,16 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4988,6 +5731,12 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "meros": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.2.tgz", + "integrity": "sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A==", + "requires": {} + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4997,6 +5746,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "requires": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -5064,6 +5814,11 @@ "integrity": "sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==", "dev": true }, + "nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -5132,6 +5887,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, "object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -5193,7 +5953,8 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true }, "plimit-lit": { "version": "1.6.1", @@ -5204,6 +5965,11 @@ "queue-lit": "^1.5.1" } }, + "property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5263,11 +6029,6 @@ "picomatch": "^2.2.1" } }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5523,6 +6284,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "requires": { "is-number": "^7.0.0" } @@ -5532,6 +6294,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -5592,6 +6359,11 @@ "strip-bom": "^3.0.0" } }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -5624,7 +6396,8 @@ "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true }, "unpipe": { "version": "1.0.0", @@ -5639,6 +6412,11 @@ "punycode": "^2.1.0" } }, + "urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5655,6 +6433,11 @@ "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" }, + "value-or-promise": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", + "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5765,6 +6548,20 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true + }, + "yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + } } } } diff --git a/package.json b/package.json index 8f53ac5..91f8ec4 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ "main": "build/index.js", "dependencies": { "@colony/core": "^2.0.1", + "@graphql-tools/executor-http": "^3.0.7", + "@graphql-tools/wrap": "^11.1.2", "cors": "^2.8.5", "express": "^4.18.2", "express-session": "^1.17.3", - "http-proxy-middleware": "^2.0.6", + "graphql-middleware": "^6.1.35", + "graphql-shield": "^7.6.5", "node-fetch": "2.6", "siwe": "^2.1.4", "ws": "^8.16.0" diff --git a/src/permissions.ts b/src/permissions.ts new file mode 100644 index 0000000..65af0bd --- /dev/null +++ b/src/permissions.ts @@ -0,0 +1,92 @@ +import { shield, rule, allow } from 'graphql-shield'; +import { Path } from 'graphql/jsutils/Path'; +import { FieldNode, GraphQLResolveInfo, ValueNode } from 'graphql'; + +const getPathArray = (path: Path | undefined): (string | number)[] => { + const segments: (string | number)[] = []; + let current = path; + while (current) { + segments.unshift(current.key); + current = current.prev; + } + return segments; +}; + +const getRootFieldNode = ( + info: GraphQLResolveInfo, + rootField: string | number, +): FieldNode | undefined => { + for (const selection of info.operation.selectionSet.selections) { + if (selection.kind === 'Field' && selection.name.value === rootField) { + return selection; + } + } + return undefined; +}; + +const resolveValue = ( + node: ValueNode, + variables: Record, +): unknown => { + switch (node.kind) { + case 'Variable': + return variables[node.name.value]; + case 'IntValue': + return parseInt(node.value, 10); + case 'FloatValue': + return parseFloat(node.value); + case 'StringValue': + return node.value; + case 'BooleanValue': + return node.value; + case 'NullValue': + return null; + case 'EnumValue': + return node.value; + case 'ListValue': + return node.values.map((v) => resolveValue(v, variables)); + case 'ObjectValue': + return Object.fromEntries( + node.fields.map((f) => [ + f.name.value, + resolveValue(f.value, variables), + ]), + ); + } +}; + +const getRootFieldArgs = ( + info: GraphQLResolveInfo, + rootFieldNode: FieldNode, +): Record => { + const args: Record = {}; + for (const arg of rootFieldNode.arguments ?? []) { + args[arg.name.value] = resolveValue(arg.value, info.variableValues); + } + return args; +}; + +const canAccessEmail = rule()((parent, args, ctx, info) => { + const pathArray = getPathArray(info.path); + const rootField = pathArray[0]; + const rootFieldNode = getRootFieldNode(info, rootField); + const rootArgs = rootFieldNode ? getRootFieldArgs(info, rootFieldNode) : {}; + + if (!ctx.userAddress || rootField !== 'getUserByAddress') { + return false; + } + + return String(rootArgs.id).toLowerCase() === ctx.userAddress.toLowerCase(); +}); + +export const permissions = shield( + { + Profile: { + email: canAccessEmail, + }, + }, + { + fallbackRule: allow, + allowExternalErrors: true, + }, +); diff --git a/src/schema.ts b/src/schema.ts index a35bee7..7c205d3 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,24 +1,33 @@ -import { - GraphQLSchema, - buildClientSchema, - getIntrospectionQuery, - IntrospectionQuery, -} from 'graphql'; +import { GraphQLSchema, print } from 'graphql'; +import { wrapSchema, schemaFromExecutor } from '@graphql-tools/wrap'; +import { AsyncExecutor } from '@graphql-tools/utils'; +import { applyMiddleware } from 'graphql-middleware'; import { graphqlRequest } from './helpers'; +import { permissions } from './permissions'; -let schema: GraphQLSchema | null = null; +let schema: GraphQLSchema; -export const initSchema = async (): Promise => { - const introspectionQuery = getIntrospectionQuery(); +const createAppSyncExecutor = (): AsyncExecutor => { + return async ({ document, variables, context }) => { + const query = print(document); + const userAddress = (context as { userAddress?: string } | undefined) + ?.userAddress; + const result = await graphqlRequest(query, variables, userAddress); + return result; + }; +}; - const result = await graphqlRequest(introspectionQuery); +export const initSchema = async (): Promise => { + const executor = createAppSyncExecutor(); + const remoteSchema = await schemaFromExecutor(executor); - if (!result?.data) { - throw new Error('Failed to fetch GraphQL schema: no data returned'); - } + const wrappedSchema = wrapSchema({ + schema: remoteSchema, + executor, + }); - schema = buildClientSchema(result.data as IntrospectionQuery); + schema = applyMiddleware(wrappedSchema, permissions); }; -export const getSchema = (): GraphQLSchema | null => schema; +export const getSchema = (): GraphQLSchema => schema; diff --git a/src/server.ts b/src/server.ts index a497d4d..4aa2e33 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,13 +1,12 @@ -import dotenv from "dotenv"; -import express from "express"; -import cors from "cors"; +import dotenv from 'dotenv'; +import express from 'express'; +import cors from 'cors'; import routes from '~routes'; import { getStaticOrigin, isDevMode } from './helpers'; import ExpressSession from './ExpressSession'; -import { operationExecutionHandler } from '~routes'; -import { Headers } from "~types"; +import { Headers } from '~types'; dotenv.config(); @@ -23,41 +22,41 @@ const proxyServerInstace = () => { // If there weren't any headers, or we're in devmode, just return return next(); } - let xForwardedHeadersAsString = ""; + let xForwardedHeadersAsString = ''; // So there were headers, and we're not in devmode. - if (typeof xForwardedHeaders === "string") { + if (typeof xForwardedHeaders === 'string') { xForwardedHeadersAsString = xForwardedHeaders; } else { xForwardedHeadersAsString = xForwardedHeaders.join(', '); } - if (xForwardedHeadersAsString.split(', ').at(-1) === 'https'){ + if (xForwardedHeadersAsString.split(', ').at(-1) === 'https') { req.headers[Headers.ForwardedProto] = 'https'; } return next(); }); - proxyServer.use(express.json({limit: '1mb'})); + proxyServer.use(express.json({ limit: '1mb' })); - proxyServer.use(cors({ - origin: getStaticOrigin, - credentials: true, - })); + proxyServer.use( + cors({ + origin: getStaticOrigin, + credentials: true, + }), + ); proxyServer.set('trust proxy', true); - proxyServer.use(ExpressSession({ - name: process.env.COOKIE_NAME, - secret: process.env.COOKIE_SECRET || 'pleasechangemebeforegoingintoproduction', - resave: false, - saveUninitialized: true, - cookie: { secure: !isDevMode(), sameSite: true }, - })); - - /* - * @NOTE Handle async GraphQL logic to decide if we allow a operation or not - */ - proxyServer.use(operationExecutionHandler); + proxyServer.use( + ExpressSession({ + name: process.env.COOKIE_NAME, + secret: + process.env.COOKIE_SECRET || 'pleasechangemebeforegoingintoproduction', + resave: false, + saveUninitialized: true, + cookie: { secure: !isDevMode(), sameSite: true }, + }), + ); /* * Initialize routes diff --git a/src/types.ts b/src/types.ts index 503d9cc..fb05a63 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { RequestHandler } from 'http-proxy-middleware'; +import { RequestHandler } from 'express'; export enum HttpStatuses { OK = 200, @@ -16,7 +16,7 @@ export enum ResponseTypes { Status = 'status', } -export type Response = { +export type ApiResponse = { message: string; type: ResponseTypes; data?: string | number | boolean | string[] | number[] | boolean[]; @@ -53,6 +53,7 @@ export type StaticOrigin = | string | RegExp | (boolean | string | RegExp)[]; + export type StaticOriginCallback = ( err: Error | null, origin?: StaticOrigin | undefined, @@ -69,7 +70,6 @@ export interface RouteHandler { url: Urls; handler: RequestHandler; } - export type UserRole = { id: string; role_0: boolean | null; diff --git a/tsconfig.json b/tsconfig.json index 8c7d1b4..6f3a371 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,9 +39,6 @@ "~routes": [ "src/routes" ], - "~queries": [ - "src/queries" - ], }, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ From cfbf2952b47d57e06191111a24ed171000734ec7 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Tue, 9 Dec 2025 21:53:57 +0000 Subject: [PATCH 09/16] Switch validation to shield permissions --- src/queries.ts | 65 -------- src/routes/graphql/graphql.ts | 168 +++++-------------- src/validateRequest.ts | 297 ---------------------------------- 3 files changed, 44 insertions(+), 486 deletions(-) delete mode 100644 src/queries.ts delete mode 100644 src/validateRequest.ts diff --git a/src/queries.ts b/src/queries.ts deleted file mode 100644 index d363bcb..0000000 --- a/src/queries.ts +++ /dev/null @@ -1,65 +0,0 @@ -export const getColonyAction = /* GraphQL */ ` - query GetColonyAction($actionId: ID!) { - getColonyAction(id: $actionId) { - id - initiatorAddress - } - } -`; - -export const getColonyRole = /* GraphQL */ ` - query GetUserRolesInColony($combinedId: ID!) { - getColonyRole(id: $combinedId) { - role_0 - role_1 - role_2 - role_3 - role_5 - role_6 - } - } -`; - -export const getAllColonyRoles = /* GraphQL */ ` - query GetAllColonyRoles($targetAddress: ID!, $colonyAddress: ID!) { - getRoleByTargetAddressAndColony( - targetAddress: $targetAddress - colonyAddress: { eq: $colonyAddress } - ) { - items { - id - role_0 - role_1 - role_2 - role_3 - role_5 - role_6 - } - } - } -`; - -export const getColonyTokens = /* GraphQL */ ` - query GetColonyFromToken($tokenColonyId: ID!) { - getColonyTokens(id: $tokenColonyId) { - colonyID - } - } -`; - -export const getStreamingPayment = /* GraphQL */ ` - query GetStreamingPayment($streamingPaymentId: ID!) { - getStreamingPayment(id: $streamingPaymentId) { - id - nativeDomainId - } - } -`; - -export const getTransaction = /* GraphQL */ ` - query GetTransaction($transactionId: ID!) { - getTransaction(id: $transactionId) { - from - } - } -`; diff --git a/src/routes/graphql/graphql.ts b/src/routes/graphql/graphql.ts index 94e756b..4fb01aa 100644 --- a/src/routes/graphql/graphql.ts +++ b/src/routes/graphql/graphql.ts @@ -1,145 +1,65 @@ -import dotenv from 'dotenv'; -import { fixRequestBody, Options, RequestHandler } from 'http-proxy-middleware'; -import { Response, Request, NextFunction } from 'express-serve-static-core'; -import { ClientRequest, IncomingMessage } from 'http'; -import { parse } from 'graphql'; +import { Response, Request } from 'express-serve-static-core'; +import { graphql } from 'graphql'; -import { - getStaticOrigin, - sendResponse, - getRemoteIpAddress, - logger, -} from '~helpers'; -import { - ResponseTypes, - HttpStatuses, - ContentTypes, - Headers, - Urls, - ServerMethods, -} from '~types'; -import { validateRequest } from '../../validateRequest'; -import { rules } from '../../rules'; +import { getStaticOrigin, getRemoteIpAddress, logger } from '~helpers'; +import { HttpStatuses, ContentTypes, Headers } from '~types'; import { getSchema } from '../../schema'; -dotenv.config(); - -export const operationExecutionHandler: RequestHandler = async ( - request: Request, - response: Response, - nextFn: NextFunction, -) => { - // short circut early - if ( - request.path !== Urls.GraphQL || - request.method !== ServerMethods.Post.toUpperCase() - ) { - return nextFn(); - } - +export const handleGraphQL = async (request: Request, response: Response) => { const userAddress = request.session.auth?.address; + const userAuthenticated = !!request.session.auth; const requestRemoteAddress = getRemoteIpAddress(request); - try { - const schema = getSchema(); - if (!schema) { - throw new Error('Schema not initialized'); - } + const schema = getSchema(); - const document = parse(request.body.query); - const ctx = { - userAddress, - variables: request.body.variables ?? {}, - }; + const { query, variables, operationName } = request.body; - /* - * @NOTE Handle async GraphQL logic to decide if we allow an operation or not - */ - response.locals.canExecute = await validateRequest( - document, + try { + const result = await graphql({ schema, - rules, - ctx, - ); - return nextFn(); - } catch (error: any) { - logger( - `${ - userAddress ? `auth-${userAddress}` : 'non-auth' - } request malformed graphql ${ - request.body ? JSON.stringify(request.body) : '' - } from ${requestRemoteAddress}`, - ); - return sendResponse( - response, - request, - { - message: error?.message || 'graphql parsing error', - type: ResponseTypes.Error, - data: '', + source: query, + variableValues: variables, + operationName, + contextValue: { + userAddress, }, - HttpStatuses.SERVER_ERROR, - ); - } -}; - -export const graphQlProxyRouteHandler: Options = { - target: process.env.APPSYNC_API, - changeOrigin: true, - headers: { - [Headers.ApiKey]: process.env.APPSYNC_API_KEY || '', - [Headers.ContentType]: ContentTypes.Json, - }, - pathRewrite: { '^/graphql': '' }, - onProxyReq: ( - proxyRequest: ClientRequest, - request: Request, - response: Response, - ) => { - const userAuthenticated = !!request.session.auth; - const userAddress = request.session.auth?.address || ''; - const requestRemoteAddress = getRemoteIpAddress(request); + }); - const canExecute = response.locals.canExecute; + const hasErrors = result.errors && result.errors.length > 0; logger( `${userAuthenticated ? 'auth' : 'non-auth'} request${ userAddress ? ` from ${userAddress}` : '' - } at ${requestRemoteAddress} was ${ - canExecute ? '\x1b[32m ALLOWED \x1b[0m' : '\x1b[31m FORBIDDEN \x1b[0m' + } at ${requestRemoteAddress} ${ + hasErrors ? '\x1b[31m ERROR \x1b[0m' : '\x1b[32m OK \x1b[0m' }`, ); - // Allowed - if (canExecute) { - proxyRequest.setHeader(Headers.WalletAddress, userAddress); - return fixRequestBody(proxyRequest, request); - } + return response + .set({ + [Headers.AllowOrigin]: getStaticOrigin(request.headers.origin), + [Headers.ContentType]: ContentTypes.Json, + [Headers.PoweredBy]: 'Colony', + }) + .status(HttpStatuses.OK) + .json(result); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : 'GraphQL execution error'; - // Forbidden - return sendResponse( - response, - request, - { - message: 'forbidden', - type: ResponseTypes.Auth, - data: '', - }, - HttpStatuses.FORBIDDEN, - ); - }, - // selfHandleResponse: true, - onProxyRes: (proxyResponse: IncomingMessage, request: Request) => { - proxyResponse.headers[Headers.AllowOrigin] = getStaticOrigin( - request.headers.origin, + logger( + `${userAuthenticated ? 'auth' : 'non-auth'} request${ + userAddress ? ` from ${userAddress}` : '' + } at ${requestRemoteAddress} \x1b[31m EXCEPTION \x1b[0m: ${message}`, ); - proxyResponse.headers[Headers.PoweredBy] = 'Colony'; - }, - logProvider: () => ({ - log: logger, - info: logger, - error: logger, - warn: logger, - debug: logger, - }), + + return response + .set({ + [Headers.AllowOrigin]: getStaticOrigin(request.headers.origin), + [Headers.ContentType]: ContentTypes.Json, + [Headers.PoweredBy]: 'Colony', + }) + .status(HttpStatuses.SERVER_ERROR) + .json({ errors: [{ message }] }); + } }; diff --git a/src/validateRequest.ts b/src/validateRequest.ts deleted file mode 100644 index ba29406..0000000 --- a/src/validateRequest.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { - DocumentNode, - GraphQLSchema, - TypeInfo, - visit, - visitWithTypeInfo, -} from 'graphql'; -import { RulesConfig, AuthContext, FieldRule } from './rules'; -import { logger } from './helpers'; - -interface FragmentSpreadInfo { - atPath: string[]; - inFragment: string | null; -} - -/** - * Collects where each fragment is spread in the document. - * Tracks the path at the spread location and whether it's inside another fragment. - */ -function collectFragmentSpreads( - document: DocumentNode, -): Map { - const spreads = new Map(); - let currentFragment: string | null = null; - const path: string[] = []; - - visit(document, { - OperationDefinition: { - enter() { - currentFragment = null; - }, - }, - FragmentDefinition: { - enter(node) { - currentFragment = node.name.value; - }, - leave() { - currentFragment = null; - }, - }, - Field: { - enter(node) { - path.push(node.name.value); - }, - leave() { - path.pop(); - }, - }, - FragmentSpread(node) { - const name = node.name.value; - if (!spreads.has(name)) { - spreads.set(name, []); - } - spreads.get(name)!.push({ - atPath: [...path], - inFragment: currentFragment, - }); - }, - }); - - return spreads; -} - -/** - * Resolves the full paths where a fragment's fields should be validated. - * Handles nested fragments by recursively resolving parent fragment contexts. - */ -function resolveFragmentContexts( - spreads: Map, -): Map { - const resolved = new Map(); - - function resolvePaths( - fragmentName: string, - visited: Set, - ): string[][] { - if (visited.has(fragmentName)) return []; // Prevent cycles - visited.add(fragmentName); - - const fragmentSpreads = spreads.get(fragmentName) || []; - const paths: string[][] = []; - - for (const spread of fragmentSpreads) { - if (spread.inFragment === null) { - // Spread directly in operation - paths.push(spread.atPath); - } else { - // Spread inside another fragment - resolve parent paths first - const parentPaths = resolvePaths(spread.inFragment, new Set(visited)); - for (const parentPath of parentPaths) { - paths.push([...parentPath, ...spread.atPath]); - } - } - } - - return paths; - } - - for (const fragmentName of spreads.keys()) { - resolved.set(fragmentName, resolvePaths(fragmentName, new Set())); - } - - return resolved; -} - -export async function validateRequest( - document: DocumentNode, - schema: GraphQLSchema, - rules: RulesConfig, - ctx: Omit, -): Promise { - let allowed = true; - const pendingChecks: Array<{ - key: string; - check: () => Promise; - }> = []; - const typeInfo = new TypeInfo(schema); - - logger('[validateRequest] Starting validation'); - - // Build fragment context map (handles nested fragments) - const fragmentSpreads = collectFragmentSpreads(document); - const fragmentContexts = resolveFragmentContexts(fragmentSpreads); - - const path: string[] = []; - let activeFragment: string | null = null; - - visit( - document, - visitWithTypeInfo(typeInfo, { - FragmentDefinition: { - enter(node) { - activeFragment = node.name.value; - }, - leave() { - activeFragment = null; - }, - }, - Field: { - enter(node) { - const parentType = typeInfo.getParentType(); - const fieldName = node.name.value; - - console.log('Entering Field:', { - fieldName, - parentType: parentType?.name, - }); - - path.push(fieldName); - - if (fieldName === '__typename') return; - if (!parentType) return; - - const typeName = parentType.name; - const key = `${typeName}.${fieldName}`; - - // Build full paths including fragment spread context - let fullPaths: string[][]; - if (activeFragment) { - const contexts = fragmentContexts.get(activeFragment) || [[]]; - fullPaths = contexts.map((ctx) => [...ctx, ...path]); - } else { - fullPaths = [[...path]]; - } - - console.log({ fullPaths, activeFragment }); - - // Validate against each full path - // If fragment is spread in multiple places, all must pass - for (const currentPath of fullPaths) { - const ctxWithPath: AuthContext = { ...ctx, path: currentPath }; - const pathString = currentPath.join('.'); - const parentPath = currentPath.slice(0, -1).join('.'); - - // 1. Check path rules (exact → parent) - const pathRule: FieldRule | undefined = - rules.paths[pathString] ?? rules.paths[parentPath]; - const matchedPath = - rules.paths[pathString] !== undefined ? pathString : parentPath; - - if (pathRule !== undefined) { - logger( - `[validateRequest] ${key} (path: ${pathString}) - PATH RULE (${matchedPath}): ${ - typeof pathRule === 'function' ? 'function' : pathRule - }`, - ); - - if (pathRule === false) { - logger( - `[validateRequest] BLOCKED by path rule: ${matchedPath}`, - ); - allowed = false; - } else if (pathRule === true) { - // Allowed - } else { - pendingChecks.push({ - key: `path:${matchedPath}`, - check: () => Promise.resolve(pathRule(ctxWithPath)), - }); - } - continue; // Path rule handled this path, skip type check - } - - // 2. Check type rules - const typeRules = rules.types[typeName]; - - if (typeRules === true) { - logger(`[validateRequest] ${key} - type allows all fields`); - continue; - } - - if (typeRules === false) { - logger( - `[validateRequest] BLOCKED: ${key} - type blocks all fields`, - ); - allowed = false; - continue; - } - - if (typeof typeRules === 'object' && typeRules !== null) { - const fieldRule = typeRules[fieldName]; - - if (fieldRule === undefined) { - logger( - `[validateRequest] BLOCKED: ${key} - not in allowed list`, - ); - allowed = false; - continue; - } - - logger( - `[validateRequest] ${key} - type rule: ${ - typeof fieldRule === 'function' ? 'function' : fieldRule - }`, - ); - - if (fieldRule === false) { - logger(`[validateRequest] BLOCKED: ${key} - rule is false`); - allowed = false; - } else if (fieldRule === true) { - // Allowed - } else { - pendingChecks.push({ - key, - check: () => Promise.resolve(fieldRule(ctxWithPath)), - }); - } - continue; - } - - // No rules for this type = BLOCKED - logger(`[validateRequest] BLOCKED: ${key} - no rules for type`); - allowed = false; - } - }, - leave(node) { - console.log('Leaving Field:', { fieldName: node.name.value }); - path.pop(); - }, - }, - }), - ); - - if (!allowed) { - logger('[validateRequest] Blocked during field traversal'); - return false; - } - - if (pendingChecks.length === 0) { - logger('[validateRequest] All checks passed'); - return true; - } - - logger( - `[validateRequest] Running ${pendingChecks.length} async checks in parallel`, - ); - - const results = await Promise.all( - pendingChecks.map(async ({ key, check }) => { - const result = await check(); - logger( - `[validateRequest] Async check ${key}: ${ - result ? 'ALLOWED' : 'BLOCKED' - }`, - ); - return result; - }), - ); - - const allPassed = results.every(Boolean); - logger( - `[validateRequest] ${ - allPassed ? 'All checks passed' : 'Some checks failed' - }`, - ); - return allPassed; -} From c99ae0be33eae2d3d1537d7ab076b4b943f75f0d Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Tue, 9 Dec 2025 22:41:20 +0000 Subject: [PATCH 10/16] First set of new-style permissions --- src/helpers.ts | 69 +++++++++++------------- src/permissions.ts | 125 ++++++++++++++++++++++++++++++++++++++++++-- src/queries.ts | 65 +++++++++++++++++++++++ src/routes/index.ts | 10 ++-- 4 files changed, 219 insertions(+), 50 deletions(-) create mode 100644 src/queries.ts diff --git a/src/helpers.ts b/src/helpers.ts index cffa68e..68ecfb8 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -8,7 +8,7 @@ import { import { StaticOriginCallback, HttpStatuses, - Response, + ApiResponse, Headers, ContentTypes, ServerMethods, @@ -47,7 +47,7 @@ export const getStaticOrigin = ( export const sendResponse = ( response: ExpressResponse, request: Request, - message?: Response, + message?: ApiResponse, status: HttpStatuses = HttpStatuses.OK, ) => response @@ -84,14 +84,21 @@ export const logger = (...args: any[]): void => { export const graphqlRequest = async ( queryOrMutation: string, - variables?: Record, + variables?: Record, + walletAddress?: string, ) => { + const headers: Record = { + [Headers.ApiKey]: process.env.APPSYNC_API_KEY || '', + [Headers.ContentType]: ContentTypes.Json, + }; + + if (walletAddress) { + headers[Headers.WalletAddress] = walletAddress; + } + const options = { method: ServerMethods.Post.toUpperCase(), - headers: { - [Headers.ApiKey]: process.env.APPSYNC_API_KEY || '', - [Headers.ContentType]: ContentTypes.Json, - }, + headers, body: JSON.stringify({ query: queryOrMutation, variables, @@ -100,12 +107,9 @@ export const graphqlRequest = async ( const request = new NodeFetchRequst(process.env.APPSYNC_API || '', options); - let body; - let response; - try { - response = await fetch(request); - body = await response.json(); + const response = await fetch(request); + const body = await response.json(); return body; } catch (error) { /* @@ -116,37 +120,26 @@ export const graphqlRequest = async ( } }; -export const delay = async (timeout: number) => { - return new Promise((resolve) => { - setTimeout(resolve, timeout); - }); -}; +const MAX_RETRIES = 3; -export const tryFetchGraphqlQuery = async ( - queryOrMutation: string, - variables?: Record, - maxRetries: number = 3, - blockTime: number = BLOCK_TIME, -) => { - let currentTry = 0; - while (true) { - const result = await graphqlRequest(queryOrMutation, variables); +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - /* - * @NOTE That this limits to only fetching one operation at a time - */ +export const fetchWithRetry = async ( + query: string, + variables: Record, +): Promise => { + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + const result = await graphqlRequest(query, variables); if (result?.data) { - const { data } = result; - if (data[Object.keys(data)[0]]) { - return data[Object.keys(data)[0]]; + const data = result.data; + const value = data[Object.keys(data)[0]]; + if (value) { + return value as T; } } - - if (currentTry < maxRetries) { - await delay(blockTime); - currentTry += 1; - } else { - throw new Error('Could not fetch graphql data in time'); + if (attempt < MAX_RETRIES) { + await delay(BLOCK_TIME); } } + return null; }; diff --git a/src/permissions.ts b/src/permissions.ts index 65af0bd..b3ba5a9 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -1,6 +1,11 @@ -import { shield, rule, allow } from 'graphql-shield'; +import { shield, rule, allow, deny, and } from 'graphql-shield'; import { Path } from 'graphql/jsutils/Path'; import { FieldNode, GraphQLResolveInfo, ValueNode } from 'graphql'; +import { ColonyRole } from '@colony/core'; + +import { fetchWithRetry } from './helpers'; +import { getColonyRole, getTransaction } from './queries'; +import { UserRole } from '~types'; const getPathArray = (path: Path | undefined): (string | number)[] => { const segments: (string | number)[] = []; @@ -66,21 +71,131 @@ const getRootFieldArgs = ( return args; }; -const canAccessEmail = rule()((parent, args, ctx, info) => { +const getByPath = (obj: Record, path: string): unknown => { + return path.split('.').reduce((acc, key) => { + if (acc && typeof acc === 'object') { + return (acc as Record)[key]; + } + return undefined; + }, obj); +}; + +const isAuthenticated = rule()((_parent, _args, ctx) => { + return Boolean(ctx.userAddress); +}); + +const matchesUserAddress = (path: string) => + rule()((_parent, args, ctx) => { + if (!ctx.userAddress) { + return false; + } + const value = getByPath(args, path); + return String(value).toLowerCase() === ctx.userAddress.toLowerCase(); + }); + +const ownsTransaction = rule()(async (_parent, args, ctx) => { + if (!ctx.userAddress) { + return false; + } + const input = args.input as Record; + const transaction = await fetchWithRetry<{ from: string }>(getTransaction, { + transactionId: input.id, + }); + return transaction?.from?.toLowerCase() === ctx.userAddress.toLowerCase(); +}); + +const isOwnContributor = rule()((_parent, args, ctx) => { + if (!ctx.userAddress) { + return false; + } + const input = args.input as Record; + const [, contributorAddress] = String(input.id).split('_'); + return contributorAddress?.toLowerCase() === ctx.userAddress.toLowerCase(); +}); + +const hasColonyRole = (colonyAddressPath: string, role: ColonyRole) => + rule()(async (_parent, args, ctx) => { + if (!ctx.userAddress) { + return false; + } + const colonyAddress = getByPath(args, colonyAddressPath); + if (!colonyAddress) { + return false; + } + const combinedId = `${colonyAddress}_1_${ctx.userAddress}_roles`; + const data = await fetchWithRetry(getColonyRole, { + combinedId, + }); + return !!data?.[`role_${role}` as keyof UserRole]; + }); + +const inputAllowsOnly = (allowedFields: string[]) => + rule()((_parent, args) => { + const input = args.input as Record; + const providedFields = Object.keys(input).filter((key) => key !== 'id'); + return providedFields.every((field) => allowedFields.includes(field)); + }); + +const canAccessEmail = rule()((_parent, _args, ctx, info) => { + if (!ctx.userAddress) { + return false; + } + const pathArray = getPathArray(info.path); const rootField = pathArray[0]; const rootFieldNode = getRootFieldNode(info, rootField); const rootArgs = rootFieldNode ? getRootFieldArgs(info, rootFieldNode) : {}; - if (!ctx.userAddress || rootField !== 'getUserByAddress') { - return false; + if (rootField === 'getUserByAddress') { + return String(rootArgs.id).toLowerCase() === ctx.userAddress.toLowerCase(); + } + + if (rootField === 'updateProfile') { + const input = rootArgs.input as Record; + return String(input.id).toLowerCase() === ctx.userAddress.toLowerCase(); } - return String(rootArgs.id).toLowerCase() === ctx.userAddress.toLowerCase(); + return false; }); export const permissions = shield( { + Query: {}, + Mutation: { + '*': deny, + + validateUserInvite: allow, + updateContributorsWithReputation: allow, + + updateProfile: and( + matchesUserAddress('input.id'), + inputAllowsOnly(['hasCompletedKYCFlow', 'preferredCurrency']), + ), + createUniqueUser: matchesUserAddress('input.id'), + createUserNotificationsData: matchesUserAddress('input.id'), + updateNotificationsData: matchesUserAddress('input.userAddress'), + createTransaction: matchesUserAddress('input.from'), + updateTransaction: ownsTransaction, + createUserTokens: matchesUserAddress('input.userID'), + createColonyContributor: matchesUserAddress('input.contributorAddress'), + updateColonyContributor: isOwnContributor, + createColonyEtherealMetadata: matchesUserAddress( + 'input.initiatorAddress', + ), + updateColonyMetadata: hasColonyRole('input.id', ColonyRole.Root), + + initializeUser: isAuthenticated, + createColonyMetadata: isAuthenticated, + createDomainMetadata: isAuthenticated, + updateDomainMetadata: isAuthenticated, + createExpenditureMetadata: isAuthenticated, + createStreamingPaymentMetadata: isAuthenticated, + createAnnotation: isAuthenticated, + createColonyDecision: isAuthenticated, + bridgeXYZMutation: isAuthenticated, + bridgeCreateBankAccount: isAuthenticated, + bridgeUpdateBankAccount: isAuthenticated, + }, Profile: { email: canAccessEmail, }, diff --git a/src/queries.ts b/src/queries.ts new file mode 100644 index 0000000..d363bcb --- /dev/null +++ b/src/queries.ts @@ -0,0 +1,65 @@ +export const getColonyAction = /* GraphQL */ ` + query GetColonyAction($actionId: ID!) { + getColonyAction(id: $actionId) { + id + initiatorAddress + } + } +`; + +export const getColonyRole = /* GraphQL */ ` + query GetUserRolesInColony($combinedId: ID!) { + getColonyRole(id: $combinedId) { + role_0 + role_1 + role_2 + role_3 + role_5 + role_6 + } + } +`; + +export const getAllColonyRoles = /* GraphQL */ ` + query GetAllColonyRoles($targetAddress: ID!, $colonyAddress: ID!) { + getRoleByTargetAddressAndColony( + targetAddress: $targetAddress + colonyAddress: { eq: $colonyAddress } + ) { + items { + id + role_0 + role_1 + role_2 + role_3 + role_5 + role_6 + } + } + } +`; + +export const getColonyTokens = /* GraphQL */ ` + query GetColonyFromToken($tokenColonyId: ID!) { + getColonyTokens(id: $tokenColonyId) { + colonyID + } + } +`; + +export const getStreamingPayment = /* GraphQL */ ` + query GetStreamingPayment($streamingPaymentId: ID!) { + getStreamingPayment(id: $streamingPaymentId) { + id + nativeDomainId + } + } +`; + +export const getTransaction = /* GraphQL */ ` + query GetTransaction($transactionId: ID!) { + getTransaction(id: $transactionId) { + from + } + } +`; diff --git a/src/routes/index.ts b/src/routes/index.ts index f981368..48c04cb 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,3 @@ -import { createProxyMiddleware } from "http-proxy-middleware"; - import { RouteHandler, ServerMethods, Urls } from '~types'; import { handleHealthRoute } from './health'; @@ -9,9 +7,7 @@ import { handleDeauthRoute, handleCheck, } from './auth'; -import { graphQlProxyRouteHandler, operationExecutionHandler } from './graphql'; - -export { operationExecutionHandler }; +import { handleGraphQL } from './graphql'; const routes: RouteHandler[] = [ /* @@ -49,9 +45,9 @@ const routes: RouteHandler[] = [ * GraphQL */ { - method: ServerMethods.Use, + method: ServerMethods.Post, url: Urls.GraphQL, - handler: createProxyMiddleware(graphQlProxyRouteHandler), + handler: handleGraphQL, }, ]; From 61dcfd5ebfdc48a767a6f62c3c73b3c08eaae6db Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Tue, 9 Dec 2025 23:09:06 +0000 Subject: [PATCH 11/16] Add permissions checks for all relevant mutations --- src/permissions.ts | 97 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 13 deletions(-) diff --git a/src/permissions.ts b/src/permissions.ts index b3ba5a9..37d92b3 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -1,10 +1,17 @@ import { shield, rule, allow, deny, and } from 'graphql-shield'; import { Path } from 'graphql/jsutils/Path'; import { FieldNode, GraphQLResolveInfo, ValueNode } from 'graphql'; -import { ColonyRole } from '@colony/core'; +import { ColonyRole, Id } from '@colony/core'; import { fetchWithRetry } from './helpers'; -import { getColonyRole, getTransaction } from './queries'; +import { + getAllColonyRoles, + getColonyAction, + getColonyRole, + getColonyTokens, + getStreamingPayment, + getTransaction, +} from './queries'; import { UserRole } from '~types'; const getPathArray = (path: Path | undefined): (string | number)[] => { @@ -97,9 +104,9 @@ const ownsTransaction = rule()(async (_parent, args, ctx) => { if (!ctx.userAddress) { return false; } - const input = args.input as Record; + const { id } = args.input as { id: string }; const transaction = await fetchWithRetry<{ from: string }>(getTransaction, { - transactionId: input.id, + transactionId: id, }); return transaction?.from?.toLowerCase() === ctx.userAddress.toLowerCase(); }); @@ -108,8 +115,8 @@ const isOwnContributor = rule()((_parent, args, ctx) => { if (!ctx.userAddress) { return false; } - const input = args.input as Record; - const [, contributorAddress] = String(input.id).split('_'); + const { id } = args.input as { id: string }; + const [, contributorAddress] = id.split('_'); return contributorAddress?.toLowerCase() === ctx.userAddress.toLowerCase(); }); @@ -136,7 +143,68 @@ const inputAllowsOnly = (allowedFields: string[]) => return providedFields.every((field) => allowedFields.includes(field)); }); -const canAccessEmail = rule()((_parent, _args, ctx, info) => { +const canDeleteColonyTokens = rule()(async (_parent, args, ctx) => { + if (!ctx.userAddress) { + return false; + } + const { id } = args.input as { id: string }; + const tokenData = await fetchWithRetry<{ colonyID: string }>( + getColonyTokens, + { tokenColonyId: id }, + ); + if (!tokenData?.colonyID) { + return false; + } + const combinedId = `${tokenData.colonyID}_1_${ctx.userAddress}_roles`; + const data = await fetchWithRetry(getColonyRole, { combinedId }); + return !!data?.[`role_${ColonyRole.Root}`]; +}); + +const isActionInitiator = rule()(async (_parent, args, ctx) => { + if (!ctx.userAddress) { + return false; + } + const { id } = args.input as { id: string }; + const action = await fetchWithRetry<{ initiatorAddress: string }>( + getColonyAction, + { actionId: id }, + ); + return ( + action?.initiatorAddress?.toLowerCase() === ctx.userAddress.toLowerCase() + ); +}); + +const canUpdateStreamingPaymentMetadata = rule()(async (_parent, args, ctx) => { + if (!ctx.userAddress) { + return false; + } + const { id: streamingPaymentId } = args.input as { id: string }; + const streamingPayment = await fetchWithRetry<{ nativeDomainId: number }>( + getStreamingPayment, + { streamingPaymentId }, + ); + if (!streamingPayment) { + return false; + } + const [colonyAddress] = streamingPaymentId.split('_'); + const rolesData = await fetchWithRetry<{ items: UserRole[] }>( + getAllColonyRoles, + { targetAddress: ctx.userAddress, colonyAddress }, + ); + if (!rolesData?.items) { + return false; + } + return rolesData.items.some((item) => { + const [, roleDomainId] = item.id.split('_'); + const matchesDomain = + roleDomainId === String(streamingPayment.nativeDomainId) || + roleDomainId === String(Id.RootDomain); + const hasRole = !!item[`role_${ColonyRole.Administration}`]; + return matchesDomain && hasRole; + }); +}); + +const isOwnUser = rule()((_parent, _args, ctx, info) => { if (!ctx.userAddress) { return false; } @@ -150,11 +218,6 @@ const canAccessEmail = rule()((_parent, _args, ctx, info) => { return String(rootArgs.id).toLowerCase() === ctx.userAddress.toLowerCase(); } - if (rootField === 'updateProfile') { - const input = rootArgs.input as Record; - return String(input.id).toLowerCase() === ctx.userAddress.toLowerCase(); - } - return false; }); @@ -183,6 +246,9 @@ export const permissions = shield( 'input.initiatorAddress', ), updateColonyMetadata: hasColonyRole('input.id', ColonyRole.Root), + createColonyTokens: hasColonyRole('input.colonyID', ColonyRole.Root), + deleteColonyTokens: canDeleteColonyTokens, + createColonyActionMetadata: isActionInitiator, initializeUser: isAuthenticated, createColonyMetadata: isAuthenticated, @@ -190,6 +256,7 @@ export const permissions = shield( updateDomainMetadata: isAuthenticated, createExpenditureMetadata: isAuthenticated, createStreamingPaymentMetadata: isAuthenticated, + updateStreamingPaymentMetadata: canUpdateStreamingPaymentMetadata, createAnnotation: isAuthenticated, createColonyDecision: isAuthenticated, bridgeXYZMutation: isAuthenticated, @@ -197,7 +264,11 @@ export const permissions = shield( bridgeUpdateBankAccount: isAuthenticated, }, Profile: { - email: canAccessEmail, + email: isOwnUser, + }, + User: { + bridgeCustomerId: isOwnUser, + privateBetaInviteCode: isOwnUser, }, }, { From 0c0d4c0025832cdc576bb90b5e9b068e97530a28 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Tue, 9 Dec 2025 23:17:21 +0000 Subject: [PATCH 12/16] Add permissions to queries --- src/permissions.ts | 62 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/permissions.ts b/src/permissions.ts index 37d92b3..1dedb1c 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -48,13 +48,11 @@ const resolveValue = ( case 'FloatValue': return parseFloat(node.value); case 'StringValue': - return node.value; case 'BooleanValue': + case 'EnumValue': return node.value; case 'NullValue': return null; - case 'EnumValue': - return node.value; case 'ListValue': return node.values.map((v) => resolveValue(v, variables)); case 'ObjectValue': @@ -223,7 +221,62 @@ const isOwnUser = rule()((_parent, _args, ctx, info) => { export const permissions = shield( { - Query: {}, + Query: { + '*': deny, + + bridgeCheckKYC: isAuthenticated, + bridgeGetDrainsHistory: isAuthenticated, + bridgeGetUserLiquidationAddress: isAuthenticated, + getProfileByEmail: isAuthenticated, + + bridgeGetGatewayFee: allow, + cacheTotalBalanceByColonyAddress: allow, + getActionsByColony: allow, + getColoniesByNativeTokenId: allow, + getColony: allow, + getColonyAction: allow, + getColonyActionByMotionId: allow, + getColonyByAddress: allow, + getColonyByName: allow, + getColonyByType: allow, + getColonyContributor: allow, + getColonyDecisionByColonyAddress: allow, + getColonyHistoricRole: allow, + getColonyMemberInvite: allow, + getColonyMotion: allow, + getContributorsByAddress: allow, + getCurrentVersionByKey: allow, + getDomainBalance: allow, + getExpenditure: allow, + getExtensionByColonyAndHash: allow, + getExtensionInstallationsCount: allow, + getMotionByTransactionHash: allow, + getMotionState: allow, + getMotionTimeoutPeriods: allow, + getPrivateBetaInviteCode: allow, + getProfileByUsername: allow, + getReputationMiningCycleMetadata: allow, + getRoleByDomainAndColony: allow, + getSafeTransactionStatus: allow, + getTokenByAddress: allow, + getTokenFromEverywhere: allow, + getTransaction: allow, + getTransactionsByUser: allow, + getTransactionsByUserAndGroup: allow, + getUserByAddress: allow, + getUserByLiquidationAddress: allow, + getUserNotificationsHMAC: allow, + getUserReputation: allow, + getUserStakes: allow, + getUserTokenBalance: allow, + getVoterRewards: allow, + listCurrentNetworkInverseFees: allow, + listCurrentVersions: allow, + listTokens: allow, + listUsers: allow, + searchColonyActions: allow, + searchColonyContributors: allow, + }, Mutation: { '*': deny, @@ -249,7 +302,6 @@ export const permissions = shield( createColonyTokens: hasColonyRole('input.colonyID', ColonyRole.Root), deleteColonyTokens: canDeleteColonyTokens, createColonyActionMetadata: isActionInitiator, - initializeUser: isAuthenticated, createColonyMetadata: isAuthenticated, createDomainMetadata: isAuthenticated, From 82faae108d6bc1f15f300489c894b5d3c895a01a Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Tue, 9 Dec 2025 23:42:52 +0000 Subject: [PATCH 13/16] protect colonyMemberInvite, add auth to more queries --- src/permissions.ts | 5 +- src/rules.ts | 558 --------------------------------------------- 2 files changed, 4 insertions(+), 559 deletions(-) delete mode 100644 src/rules.ts diff --git a/src/permissions.ts b/src/permissions.ts index 1dedb1c..e63225a 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -228,6 +228,7 @@ export const permissions = shield( bridgeGetDrainsHistory: isAuthenticated, bridgeGetUserLiquidationAddress: isAuthenticated, getProfileByEmail: isAuthenticated, + getUserNotificationsHMAC: isAuthenticated, bridgeGetGatewayFee: allow, cacheTotalBalanceByColonyAddress: allow, @@ -265,7 +266,6 @@ export const permissions = shield( getTransactionsByUserAndGroup: allow, getUserByAddress: allow, getUserByLiquidationAddress: allow, - getUserNotificationsHMAC: allow, getUserReputation: allow, getUserStakes: allow, getUserTokenBalance: allow, @@ -322,6 +322,9 @@ export const permissions = shield( bridgeCustomerId: isOwnUser, privateBetaInviteCode: isOwnUser, }, + Colony: { + colonyMemberInvite: deny, + }, }, { fallbackRule: allow, diff --git a/src/rules.ts b/src/rules.ts deleted file mode 100644 index aad8918..0000000 --- a/src/rules.ts +++ /dev/null @@ -1,558 +0,0 @@ -import { ColonyRole, Id } from '@colony/core'; - -import { tryFetchGraphqlQuery } from './helpers'; -import { - getColonyAction, - getColonyRole, - getColonyTokens, - getStreamingPayment, - getTransaction, - getAllColonyRoles, -} from './queries'; -import { UserRole } from './types'; - -export type AuthContext = { - userAddress: string | undefined; - variables: Record; - path: string[]; -}; - -export type FieldRule = - | true - | false - | ((ctx: AuthContext) => boolean | Promise); - -// TypeRules can be: -// - true: all fields allowed -// - false: all fields blocked -// - { field: rule }: field-level rules, missing = blocked -export type TypeRules = true | false | Record; - -export type PathRules = Record; - -export interface RulesConfig { - types: Record; - paths: PathRules; -} - -// ============================================================ -// HELPERS -// ============================================================ - -// Helper to check if a field in input matches userAddress -const isOwnUser = - (field: string = 'id') => - (ctx: AuthContext): boolean => { - const input = (ctx.variables.input ?? {}) as Record; - const value = (input[field] ?? ctx.variables[field]) as string | undefined; - return !!( - ctx.userAddress && - value && - value.toLowerCase() === ctx.userAddress.toLowerCase() - ); - }; - -// Helper to require authentication -const requiresAuth = (ctx: AuthContext): boolean => !!ctx.userAddress; - -// Helper to check if user has a specific role in a colony (root domain) -const hasColonyRole = - (colonyField: string, role: ColonyRole) => - async (ctx: AuthContext): Promise => { - const input = (ctx.variables.input ?? {}) as Record; - const colonyAddress = input[colonyField] as string | undefined; - if (!ctx.userAddress || !colonyAddress) return false; - try { - const data = await tryFetchGraphqlQuery(getColonyRole, { - combinedId: `${colonyAddress}_1_${ctx.userAddress}_roles`, - }); - return !!data[`role_${role}`]; - } catch { - return false; - } - }; - -// Helper to check if user is the initiator of an action -const isActionInitiator = - (actionField: string = 'id') => - async (ctx: AuthContext): Promise => { - const input = (ctx.variables.input ?? {}) as Record; - const actionId = input[actionField] as string | undefined; - if (!ctx.userAddress || !actionId) return false; - try { - const data = await tryFetchGraphqlQuery(getColonyAction, { actionId }); - return ( - data.initiatorAddress?.toLowerCase() === ctx.userAddress.toLowerCase() - ); - } catch { - return false; - } - }; - -export const rules: RulesConfig = { - // ============================================================ - // TYPE RULES - // ============================================================ - types: { - Query: { - getTokenFromEverywhere: true, - getUserReputation: true, - getUserTokenBalance: true, - getMotionState: true, - getVoterRewards: true, - getMotionTimeoutPeriods: true, - getSafeTransactionStatus: true, - getDomainBalance: true, - cacheAllDomainBalance: true, - searchColonyContributors: true, - searchColonyActions: true, - getProfile: true, - getToken: true, - listTokens: true, - getContributorReputation: true, - getColonyContributor: true, - getColony: true, - listColonies: true, - getColonyMemberInvite: true, - getColonyMetadata: true, - getTransaction: true, - getUser: true, - listUsers: true, - getDomain: true, - getDomainMetadata: true, - getColonyFundsClaim: true, - getVoterRewardsHistory: true, - getMotionMessage: true, - listMotionMessages: true, - getMultiSigUserSignature: true, - getColonyMultiSig: true, - listColonyMultiSigs: true, - getColonyMotion: true, - listColonyMotions: true, - getColonyExtension: true, - getCurrentVersion: true, - listCurrentVersions: true, - getCurrentNetworkInverseFee: true, - getColonyAction: true, - listColonyActions: true, - getColonyActionMetadata: true, - listColonyActionMetadata: true, - getColonyDecision: true, - getColonyRole: true, - getColonyHistoricRole: true, - - getExpenditure: true, - listExpenditures: true, - getExpenditureMetadata: true, - listExpenditureMetadata: true, - getStreamingPayment: true, - listStreamingPayments: true, - getStreamingPaymentMetadata: true, - listStreamingPaymentMetadata: true, - getAnnotation: true, - getReputationMiningCycleMetadata: true, - getPrivateBetaInviteCode: true, - getSafeTransaction: true, - getSafeTransactionData: true, - getExtensionInstallationsCount: true, - listExtensionInstallationsCounts: true, - getUserStake: true, - getColonyTokens: true, - listColonyTokens: true, - getUserTokens: true, - listUserTokens: true, - tokenExhangeRateByTokenId: true, - cacheTotalBalanceByColonyAddress: true, - getProfileByUsername: true, - getTokenByAddress: true, - getTokensByType: true, - getUserReputationInColony: true, - getContributorsByAddress: true, - getContributorsByColony: true, - getColonyByAddress: true, - getColonyByName: true, - getColoniesByNativeTokenId: true, - getColonyByType: true, - getTransactionsByUser: true, - getTransactionsByUserAndGroup: true, - getUserByAddress: true, - getLiquidationAddressesByUserAddress: true, - getUserByLiquidationAddress: true, - getDomainsByColony: true, - getDomainByNativeSkillId: true, - getFundsClaimsByColony: true, - getUserVoterRewards: true, - getMotionVoterRewards: true, - getMotionMessageByMotionId: true, - getMultiSigUserSignatureByMultiSigId: true, - getMultiSigByColonyAddress: true, - getMultiSigByTransactionHash: true, - getMultiSigByExpenditureId: true, - getMotionByTransactionHash: true, - getMotionByExpenditureId: true, - getExtensionByColonyAndHash: true, - getExtensionsByHash: true, - getCurrentVersionByKey: true, - getActionsByColony: true, - getColonyActionByMotionId: true, - getColonyActionByMultiSigId: true, - getActionByExpenditureId: true, - getColonyDecisionByActionId: true, - getColonyDecisionByColonyAddress: true, - getRoleByDomainAndColony: true, - getRoleByTargetAddressAndColony: true, - getRoleByColony: true, - getColonyHistoricRoleByDate: true, - getExpendituresByColony: true, - getExpendituresByNativeFundingPotIdAndColony: true, - getUserStakes: true, - - // Requires authentication - bridgeCheckKYC: requiresAuth, - bridgeGetDrainsHistory: requiresAuth, - bridgeGetUserLiquidationAddress: requiresAuth, - bridgeGetGatewayFee: requiresAuth, - getUserNotificationsHMAC: requiresAuth, - getLiquidationAddress: requiresAuth, - - // Blocked - getNotificationsData: false, - listNotificationsData: false, - getUserByBridgeCustomerId: false, - listPrivateBetaInviteCodes: false, - listProfiles: false, - listColonyRoles: false, - listColonyHistoricRoles: false, - listDomainMetadata: false, - getCacheTotalBalance: false, - listCacheTotalBalances: false, - getTokenExchangeRate: false, - listTokenExchangeRates: false, - listColonyMemberInvites: false, - listLiquidationAddresses: false, - listReputationMiningCycleMetadata: false, - getIngestorStats: false, - listIngestorStats: false, - listSafeTransactionData: false, - listVoterRewardsHistories: false, - listColonyMetadata: false, - listContributorReputations: false, - listColonyContributors: false, - listTransactions: false, - listDomains: false, - listColonyFundsClaims: false, - getContractEvent: false, - listContractEvents: false, - listColonyExtensions: false, - listMultiSigUserSignatures: false, - listUserStakes: false, - listAnnotations: false, - listSafeTransactions: false, - listColonyDecisions: false, - listCurrentNetworkInverseFees: false, - }, - - Mutation: { - createUniqueUser: isOwnUser(), - updateProfile: isOwnUser(), - createUserNotificationsData: isOwnUser(), - updateNotificationsData: isOwnUser('userAddress'), - createTransaction: isOwnUser('from'), - updateTransaction: async (ctx) => { - const input = (ctx.variables.input ?? {}) as Record; - const id = input.id as string | undefined; - const from = input.from as string | undefined; - if (!ctx.userAddress || !id || !from) return false; - if (from.toLowerCase() !== ctx.userAddress.toLowerCase()) return false; - try { - const data = await tryFetchGraphqlQuery(getTransaction, { - transactionId: id, - }); - return data.from?.toLowerCase() === ctx.userAddress.toLowerCase(); - } catch { - return false; - } - }, - createUserTokens: isOwnUser('userID'), - createColonyContributor: isOwnUser('contributorAddress'), - updateColonyContributor: (ctx) => { - const input = (ctx.variables.input ?? {}) as Record; - const combinedId = input.id as string | undefined; - if (!combinedId || !ctx.userAddress) return false; - const [, contributorAddress] = combinedId.split('_'); - return ( - contributorAddress?.toLowerCase() === ctx.userAddress.toLowerCase() - ); - }, - createColonyEtherealMetadata: isOwnUser('initiatorAddress'), - - initializeUser: requiresAuth, - createColonyMetadata: requiresAuth, - createDomainMetadata: requiresAuth, - updateDomainMetadata: requiresAuth, - createExpenditureMetadata: requiresAuth, - createStreamingPaymentMetadata: requiresAuth, - createAnnotation: requiresAuth, - createColonyDecision: requiresAuth, - bridgeXYZMutation: requiresAuth, - bridgeCreateBankAccount: requiresAuth, - bridgeUpdateBankAccount: requiresAuth, - - updateColonyMetadata: hasColonyRole('id', ColonyRole.Root), - createDomain: hasColonyRole('colonyId', ColonyRole.Architecture), - createColonyTokens: hasColonyRole('colonyID', ColonyRole.Root), - deleteColonyTokens: async (ctx) => { - const input = (ctx.variables.input ?? {}) as Record; - const tokenColonyId = input.id as string | undefined; - if (!ctx.userAddress || !tokenColonyId) return false; - try { - const tokenData = await tryFetchGraphqlQuery(getColonyTokens, { - tokenColonyId, - }); - if (!tokenData?.colonyID) return false; - const roleData = await tryFetchGraphqlQuery(getColonyRole, { - combinedId: `${tokenData.colonyID}_1_${ctx.userAddress}_roles`, - }); - return !!roleData[`role_${ColonyRole.Root}`]; - } catch { - return false; - } - }, - createColonyActionMetadata: isActionInitiator(), - updateColonyAction: isActionInitiator(), - updateStreamingPaymentMetadata: async (ctx) => { - const input = (ctx.variables.input ?? {}) as Record; - const streamingPaymentId = input.id as string | undefined; - if (!ctx.userAddress || !streamingPaymentId) return false; - try { - const { nativeDomainId } = await tryFetchGraphqlQuery( - getStreamingPayment, - { streamingPaymentId }, - ); - const [colonyAddress] = streamingPaymentId.split('_'); - const { items: userRoles }: { items: UserRole[] } = - await tryFetchGraphqlQuery(getAllColonyRoles, { - targetAddress: ctx.userAddress, - colonyAddress, - }); - return userRoles.some((item) => { - const [, roleDomainId] = item.id.split('_'); - const matchesDomain = - roleDomainId === String(nativeDomainId) || - roleDomainId === String(Id.RootDomain); - const hasRole = !!item[`role_${ColonyRole.Administration}`]; - return matchesDomain && hasRole; - }); - } catch { - return false; - } - }, - - validateUserInvite: true, - getTokenFromEverywhere: true, - updateContributorsWithReputation: true, - }, - Subscription: true, - - User: { - id: true, - tokens: true, - profileId: true, - profile: true, - roles: true, - transactionHistory: true, - liquidationAddresses: true, - createdAt: true, - updatedAt: true, - }, - Profile: { - id: true, - avatar: true, - thumbnail: true, - displayName: true, - displayNameChanged: true, - bio: true, - location: true, - website: true, - meta: true, - }, - Colony: true, - ColonyMetadata: true, - Domain: true, - DomainMetadata: true, - Token: true, - Transaction: true, - ColonyAction: true, - ColonyActionMetadata: true, - ColonyMotion: true, - ColonyMultiSig: true, - ColonyExtension: true, - ColonyContributor: true, - ContributorReputation: true, - Expenditure: true, - ExpenditureMetadata: true, - StreamingPayment: true, - StreamingPaymentMetadata: true, - Annotation: true, - ColonyDecision: true, - ColonyRole: true, - ColonyHistoricRole: true, - ColonyFundsClaim: true, - ColonyMemberInvite: true, - ColonyTokens: true, - UserTokens: true, - UserStake: true, - VoterRewardsHistory: true, - MotionMessage: true, - MultiSigUserSignature: true, - ContractEvent: true, - CurrentVersion: true, - CurrentNetworkInverseFee: true, - IngestorStats: true, - ReputationMiningCycleMetadata: true, - PrivateBetaInviteCode: true, - SafeTransaction: true, - SafeTransactionData: true, - ExtensionInstallationsCount: true, - TokenExchangeRate: true, - CacheTotalBalance: true, - LiquidationAddress: true, - InitializeUserReturn: true, - FailedTransaction: true, - - // Return/Result types - DomainBalanceInOut: true, - TimeframeDomainBalanceInOut: true, - DomainBalanceReturn: true, - CacheAllDomainBalanceReturn: true, - MarketPrice: true, - BridgeCheckKYCReturn: true, - BridgeGatewayFeeReturn: true, - BridgeDrainReceipt: true, - BridgeDrain: true, - BridgeIbanBankAccount: true, - BridgeUsBankAccount: true, - BridgeBankAccount: true, - BridgeCreateBankAccountReturn: true, - BridgeUpdateBankAccountReturn: true, - BridgeXYZMutationReturn: true, - TokenFromEverywhereReturn: true, - GetUserTokenBalanceReturn: true, - GetMotionTimeoutPeriodsReturn: true, - VoterRewardsReturn: true, - - // Nested/Embedded types - NativeTokenStatus: true, - ColonyStatus: true, - ChainMetadata: true, - ProfileMetadata: true, - ColonyUnclaimedStake: true, - ExternalLink: true, - ColonyObjective: true, - ColonyMetadataEtherealData: true, - ColonyMetadataChangelog: true, - TransactionGroup: true, - TransactionError: true, - DomainMetadataChangelog: true, - ColonyChainFundsClaim: true, - ColonyBalance: true, - ColonyBalances: true, - MotionStakeValues: true, - MotionStakes: true, - UserMotionStakes: true, - StakerRewards: true, - VoterRecord: true, - MotionStateHistory: true, - ExpenditureFundingItem: true, - VotingReputationParams: true, - MultiSigDomainConfig: true, - MultiSigParams: true, - StakedExpenditureParams: true, - ExtensionParams: true, - Payment: true, - ApprovedTokenChanges: true, - ExpenditureSlotChanges: true, - ArbitraryTxAbi: true, - ColonyActionRoles: true, - ColonyActionArbitraryTransaction: true, - ExpenditureSlot: true, - ExpenditurePayout: true, - ExpenditureBalance: true, - ExpenditureStage: true, - SimpleTargetProfile: true, - SimpleTarget: true, - NFT: true, - NFTProfile: true, - FunctionParam: true, - NFTData: true, - Safe: true, - - // Connection types - ModelColonyConnection: true, - ModelUserConnection: true, - ModelTokenConnection: true, - ModelDomainConnection: true, - ModelColonyActionConnection: true, - ModelColonyContributorConnection: true, - ModelTransactionConnection: true, - ModelProfileConnection: true, - ModelColonyRoleConnection: true, - ModelExpenditureConnection: true, - ModelStreamingPaymentConnection: true, - ModelColonyMotionConnection: true, - ModelColonyMultiSigConnection: true, - ModelColonyExtensionConnection: true, - ModelAnnotationConnection: true, - ModelColonyDecisionConnection: true, - ModelColonyFundsClaimConnection: true, - ModelVoterRewardsHistoryConnection: true, - ModelMotionMessageConnection: true, - ModelMultiSigUserSignatureConnection: true, - ModelContractEventConnection: true, - ModelColonyMemberInviteConnection: true, - ModelColonyMetadataConnection: true, - ModelDomainMetadataConnection: true, - ModelColonyActionMetadataConnection: true, - ModelColonyHistoricRoleConnection: true, - ModelIngestorStatsConnection: true, - ModelExpenditureMetadataConnection: true, - ModelStreamingPaymentMetadataConnection: true, - ModelReputationMiningCycleMetadataConnection: true, - ModelPrivateBetaInviteCodeConnection: true, - ModelSafeTransactionConnection: true, - ModelSafeTransactionDataConnection: true, - ModelExtensionInstallationsCountConnection: true, - ModelUserStakeConnection: true, - ModelColonyTokensConnection: true, - ModelUserTokensConnection: true, - ModelTokenExchangeRateConnection: true, - ModelCacheTotalBalanceConnection: true, - ModelContributorReputationConnection: true, - ModelCurrentVersionConnection: true, - ModelCurrentNetworkInverseFeeConnection: true, - ModelNotificationsDataConnection: true, - ModelLiquidationAddressConnection: true, - ModelColonyMultiSigFilterInput: true, - SearchableColonyContributorConnection: true, - SearchableColonyActionConnection: true, - SearchableAggregateResult: true, - SearchableAggregateGenericResult: true, - SearchableAggregateScalarResult: true, - SearchableAggregateBucketResult: true, - SearchableAggregateBucketResultItem: true, - - NotificationsData: false, - }, - - // ============================================================ - // PATH RULES (overrides) - // Check exact path, then parent path - // ============================================================ - paths: { - 'getUserByAddress.items.bridgeCustomerId': isOwnUser(), - 'getUserByAddress.items.notificationsData': isOwnUser(), - 'getUserByAddress.items.privateBetaInviteCode': isOwnUser(), - 'getUserByAddress.items.profile': isOwnUser(), - 'getUserByAddress.items.profile.meta': isOwnUser(), - }, -}; From fd71dff80ccde083cc1c57170a50bac84b84330e Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Wed, 10 Dec 2025 00:35:26 +0000 Subject: [PATCH 14/16] Fix permissions for updating user mutation --- src/permissions.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/permissions.ts b/src/permissions.ts index e63225a..40a0a7b 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -1,4 +1,4 @@ -import { shield, rule, allow, deny, and } from 'graphql-shield'; +import { shield, rule, allow, deny } from 'graphql-shield'; import { Path } from 'graphql/jsutils/Path'; import { FieldNode, GraphQLResolveInfo, ValueNode } from 'graphql'; import { ColonyRole, Id } from '@colony/core'; @@ -134,13 +134,6 @@ const hasColonyRole = (colonyAddressPath: string, role: ColonyRole) => return !!data?.[`role_${role}` as keyof UserRole]; }); -const inputAllowsOnly = (allowedFields: string[]) => - rule()((_parent, args) => { - const input = args.input as Record; - const providedFields = Object.keys(input).filter((key) => key !== 'id'); - return providedFields.every((field) => allowedFields.includes(field)); - }); - const canDeleteColonyTokens = rule()(async (_parent, args, ctx) => { if (!ctx.userAddress) { return false; @@ -283,10 +276,7 @@ export const permissions = shield( validateUserInvite: allow, updateContributorsWithReputation: allow, - updateProfile: and( - matchesUserAddress('input.id'), - inputAllowsOnly(['hasCompletedKYCFlow', 'preferredCurrency']), - ), + updateProfile: matchesUserAddress('input.id'), createUniqueUser: matchesUserAddress('input.id'), createUserNotificationsData: matchesUserAddress('input.id'), updateNotificationsData: matchesUserAddress('input.userAddress'), @@ -321,9 +311,11 @@ export const permissions = shield( User: { bridgeCustomerId: isOwnUser, privateBetaInviteCode: isOwnUser, + userPrivateBetaInviteCodeId: deny, }, Colony: { colonyMemberInvite: deny, + colonyMemberInviteCode: deny, }, }, { From d1e49aaae90cd7d4586a22a15b27457239e284e4 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Wed, 10 Dec 2025 00:47:06 +0000 Subject: [PATCH 15/16] Align forbidden response with previous implementation --- src/routes/graphql/graphql.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/routes/graphql/graphql.ts b/src/routes/graphql/graphql.ts index 4fb01aa..aaa2774 100644 --- a/src/routes/graphql/graphql.ts +++ b/src/routes/graphql/graphql.ts @@ -2,7 +2,7 @@ import { Response, Request } from 'express-serve-static-core'; import { graphql } from 'graphql'; import { getStaticOrigin, getRemoteIpAddress, logger } from '~helpers'; -import { HttpStatuses, ContentTypes, Headers } from '~types'; +import { HttpStatuses, ContentTypes, Headers, ResponseTypes } from '~types'; import { getSchema } from '../../schema'; export const handleGraphQL = async (request: Request, response: Response) => { @@ -26,15 +26,37 @@ export const handleGraphQL = async (request: Request, response: Response) => { }); const hasErrors = result.errors && result.errors.length > 0; + const hasPermissionError = result.errors?.some( + (error) => error.message === 'Not Authorised!', + ); logger( `${userAuthenticated ? 'auth' : 'non-auth'} request${ userAddress ? ` from ${userAddress}` : '' } at ${requestRemoteAddress} ${ - hasErrors ? '\x1b[31m ERROR \x1b[0m' : '\x1b[32m OK \x1b[0m' + hasPermissionError + ? '\x1b[31m FORBIDDEN \x1b[0m' + : hasErrors + ? '\x1b[31m ERROR \x1b[0m' + : '\x1b[32m OK \x1b[0m' }`, ); + if (hasPermissionError) { + return response + .set({ + [Headers.AllowOrigin]: getStaticOrigin(request.headers.origin), + [Headers.ContentType]: ContentTypes.Json, + [Headers.PoweredBy]: 'Colony', + }) + .status(HttpStatuses.FORBIDDEN) + .json({ + message: 'forbidden', + type: ResponseTypes.Auth, + data: '', + }); + } + return response .set({ [Headers.AllowOrigin]: getStaticOrigin(request.headers.origin), From af789e3037331a6e98d62c46dd43bb0f9885ed25 Mon Sep 17 00:00:00 2001 From: Jakub Zajac Date: Wed, 10 Dec 2025 12:02:51 +0000 Subject: [PATCH 16/16] Allow reading Profile.email when updating own profile --- src/permissions.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/permissions.ts b/src/permissions.ts index 40a0a7b..af400d2 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -1,4 +1,4 @@ -import { shield, rule, allow, deny } from 'graphql-shield'; +import { shield, rule, allow, deny, or } from 'graphql-shield'; import { Path } from 'graphql/jsutils/Path'; import { FieldNode, GraphQLResolveInfo, ValueNode } from 'graphql'; import { ColonyRole, Id } from '@colony/core'; @@ -212,6 +212,24 @@ const isOwnUser = rule()((_parent, _args, ctx, info) => { return false; }); +const isUpdatingOwnProfile = rule()((_parent, _args, ctx, info) => { + if (!ctx.userAddress) { + return false; + } + + const pathArray = getPathArray(info.path); + const rootField = pathArray[0]; + const rootFieldNode = getRootFieldNode(info, rootField); + const rootArgs = rootFieldNode ? getRootFieldArgs(info, rootFieldNode) : {}; + + if (rootField === 'updateProfile') { + const input = rootArgs.input as { id: string }; + return input.id.toLowerCase() === ctx.userAddress.toLowerCase(); + } + + return false; +}); + export const permissions = shield( { Query: { @@ -306,7 +324,7 @@ export const permissions = shield( bridgeUpdateBankAccount: isAuthenticated, }, Profile: { - email: isOwnUser, + email: or(isOwnUser, isUpdatingOwnProfile), }, User: { bridgeCustomerId: isOwnUser,