diff --git a/package.json b/package.json index 1adf70ef..3d1d35f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/aio-cli-plugin-api-mesh", - "version": "5.6.6", + "version": "5.7.0-beta.1", "description": "Adobe I/O CLI plugin to develop and manage API mesh sources", "keywords": [ "oclif-plugin" @@ -38,7 +38,7 @@ "version": "oclif-dev readme && git add README.md" }, "dependencies": { - "@adobe-apimesh/mesh-builder": "^2.4.1", + "@adobe-apimesh/mesh-builder": "^2.5.1", "@adobe/aio-cli-lib-console": "^5.0.0", "@adobe/aio-lib-core-config": "^5.0.0", "@adobe/aio-lib-core-logging": "^3.0.0", @@ -48,6 +48,7 @@ "@adobe/plugin-on-fetch": "0.1.1", "@adobe/plugin-source-headers": "^0.0.2", "@envelop/disable-introspection": "^6.0.0", + "@escape.tech/graphql-armor": "3.2.0", "@graphql-mesh/cli": "0.82.30", "@graphql-mesh/graphql": "0.34.13", "@graphql-mesh/http": "^0.96.9", diff --git a/src/commands/__fixtures__/sample_mesh_with_queryConfig.json b/src/commands/__fixtures__/sample_mesh_with_queryConfig.json new file mode 100644 index 00000000..bfdbaa35 --- /dev/null +++ b/src/commands/__fixtures__/sample_mesh_with_queryConfig.json @@ -0,0 +1,23 @@ +{ + "meshConfig": { + "sources": [ + { + "name": "", + "handler": { + "graphql": { + "endpoint": "" + } + } + } + ], + "queryConfig": { + "maxDepth": { "enabled": true, "limit": 4 }, + "maxAliases": { "enabled": true, "limit": 10 }, + "maxTokens": { "enabled": true, "limit": 500 }, + "maxDirectives": { "enabled": true, "limit": 20 }, + "costLimit": { "enabled": true, "maxCost": 1000 }, + "blockFieldSuggestion": { "enabled": true, "mask": "[hidden]" }, + "maskErrors": { "enabled": true, "message": "Something went wrong." } + } + } +} diff --git a/src/commands/api-mesh/__tests__/create.test.js b/src/commands/api-mesh/__tests__/create.test.js index 380f9e29..7e88b027 100644 --- a/src/commands/api-mesh/__tests__/create.test.js +++ b/src/commands/api-mesh/__tests__/create.test.js @@ -32,6 +32,7 @@ jest.mock('../../../lib/smsClient'); const CreateCommand = require('../create'); const sampleCreateMeshConfig = require('../../__fixtures__/sample_mesh.json'); const meshConfigWithComposerFiles = require('../../__fixtures__/sample_mesh_with_composer_files.json'); +const sampleMeshWithQueryConfig = require('../../__fixtures__/sample_mesh_with_queryConfig.json'); const { initSdk, promptConfirm, interpolateMesh, importFiles } = require('../../../helpers'); const { getMesh, @@ -407,6 +408,77 @@ describe('create command tests', () => { expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`[]`); }); + test('should pass queryConfig fields through to createMesh without modification', async () => { + createMesh.mockResolvedValueOnce({ + mesh: { + meshId: 'dummy_mesh_id', + meshConfig: sampleMeshWithQueryConfig.meshConfig, + }, + }); + parseSpy.mockResolvedValueOnce({ + args: { file: 'src/commands/__fixtures__/sample_mesh_with_queryConfig.json' }, + flags: { + ignoreCache: mockIgnoreCacheFlag, + autoConfirmAction: mockAutoApproveAction, + }, + }); + await CreateCommand.run(); + expect(createMesh.mock.calls[0]).toMatchInlineSnapshot(` + [ + "CODE1234@AdobeOrg", + "5678", + "123456789", + "Workspace01", + "ORG01", + "Project01", + { + "meshConfig": { + "queryConfig": { + "blockFieldSuggestion": { + "enabled": true, + "mask": "[hidden]", + }, + "costLimit": { + "enabled": true, + "maxCost": 1000, + }, + "maskErrors": { + "enabled": true, + "message": "Something went wrong.", + }, + "maxAliases": { + "enabled": true, + "limit": 10, + }, + "maxDepth": { + "enabled": true, + "limit": 4, + }, + "maxDirectives": { + "enabled": true, + "limit": 20, + }, + "maxTokens": { + "enabled": true, + "limit": 500, + }, + }, + "sources": [ + { + "handler": { + "graphql": { + "endpoint": "", + }, + }, + "name": "", + }, + ], + }, + }, + ] + `); + }); + test('should create and return Ti mesh url if a valid mesh config file for TI client is provided', async () => { let fetchedMeshConfig = sampleCreateMeshConfig; fetchedMeshConfig.meshId = 'dummy_id'; diff --git a/src/commands/api-mesh/__tests__/run.test.js b/src/commands/api-mesh/__tests__/run.test.js index dd560c92..4eb52d0b 100644 --- a/src/commands/api-mesh/__tests__/run.test.js +++ b/src/commands/api-mesh/__tests__/run.test.js @@ -1104,6 +1104,26 @@ describe('run command tests', () => { ); }); + test('should successfully run the mesh with queryConfig', async () => { + const cliInput = { + args: { file: 'src/commands/__fixtures__/sample_mesh_with_queryConfig.json' }, + flags: { + port: 5000, + debug: false, + inspectPort: 9229, + }, + }; + parseSpy.mockResolvedValueOnce(cliInput); + + await RunCommand.run(); + expect(start).toHaveBeenCalledWith( + expect.anything(), + cliInput.flags.port, + cliInput.flags.debug, + cliInput.flags.inspectPort, + ); + }); + test('should escape variables that are preceded by backslash symbol', async () => { const cliInput = { args: { file: 'src/commands/__fixtures__/sample_mesh_with_escaped_secrets.json' }, diff --git a/src/commands/api-mesh/__tests__/update.test.js b/src/commands/api-mesh/__tests__/update.test.js index ffda2d18..7a2cc789 100644 --- a/src/commands/api-mesh/__tests__/update.test.js +++ b/src/commands/api-mesh/__tests__/update.test.js @@ -137,6 +137,155 @@ describe('update command tests', () => { expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`[]`); }); + test('should pass queryConfig with all fields enabled through to updateMesh', async () => { + const meshWithQueryConfigEnabled = { + meshConfig: { + sources: [{ name: '', handler: { graphql: { endpoint: '' } } }], + queryConfig: { + maxDepth: { enabled: true, limit: 4 }, + maxAliases: { enabled: true, limit: 10 }, + maxTokens: { enabled: true, limit: 500 }, + maxDirectives: { enabled: true, limit: 20 }, + costLimit: { enabled: true, maxCost: 1000 }, + blockFieldSuggestion: { enabled: true, mask: '[hidden]' }, + maskErrors: { enabled: true, message: 'Something went wrong.' }, + }, + }, + }; + readFile.mockResolvedValueOnce(JSON.stringify(meshWithQueryConfigEnabled)); + parseSpy.mockResolvedValueOnce({ + args: { file: 'src/commands/__fixtures__/sample_mesh_with_queryConfig.json' }, + flags: { ignoreCache: mockIgnoreCacheFlag, autoConfirmAction: mockAutoApproveAction }, + }); + await UpdateCommand.run(); + expect(updateMesh.mock.calls[0]).toMatchInlineSnapshot(` + [ + "CODE1234@AdobeOrg", + "5678", + "123456789", + "Workspace01", + "ORG01", + "Project01", + "mesh_id", + { + "meshConfig": { + "queryConfig": { + "blockFieldSuggestion": { + "enabled": true, + "mask": "[hidden]", + }, + "costLimit": { + "enabled": true, + "maxCost": 1000, + }, + "maskErrors": { + "enabled": true, + "message": "Something went wrong.", + }, + "maxAliases": { + "enabled": true, + "limit": 10, + }, + "maxDepth": { + "enabled": true, + "limit": 4, + }, + "maxDirectives": { + "enabled": true, + "limit": 20, + }, + "maxTokens": { + "enabled": true, + "limit": 500, + }, + }, + "sources": [ + { + "handler": { + "graphql": { + "endpoint": "", + }, + }, + "name": "", + }, + ], + }, + }, + ] + `); + }); + + test('should pass queryConfig with all fields disabled through to updateMesh', async () => { + const meshWithQueryConfigDisabled = { + meshConfig: { + sources: [{ name: '', handler: { graphql: { endpoint: '' } } }], + queryConfig: { + maxDepth: { enabled: false }, + maxAliases: { enabled: false }, + maxTokens: { enabled: false }, + maxDirectives: { enabled: false }, + costLimit: { enabled: false }, + blockFieldSuggestion: { enabled: false }, + maskErrors: { enabled: false }, + }, + }, + }; + readFile.mockResolvedValueOnce(JSON.stringify(meshWithQueryConfigDisabled)); + parseSpy.mockResolvedValueOnce({ + args: { file: 'src/commands/__fixtures__/sample_mesh.json' }, + flags: { ignoreCache: mockIgnoreCacheFlag, autoConfirmAction: mockAutoApproveAction }, + }); + await UpdateCommand.run(); + expect(updateMesh.mock.calls[0]).toMatchInlineSnapshot(` + [ + "CODE1234@AdobeOrg", + "5678", + "123456789", + "Workspace01", + "ORG01", + "Project01", + "mesh_id", + { + "meshConfig": { + "queryConfig": { + "blockFieldSuggestion": { + "enabled": false, + }, + "costLimit": { + "enabled": false, + }, + "maskErrors": { + "enabled": false, + }, + "maxAliases": { + "enabled": false, + }, + "maxDepth": { + "enabled": false, + }, + "maxDirectives": { + "enabled": false, + }, + "maxTokens": { + "enabled": false, + }, + }, + "sources": [ + { + "handler": { + "graphql": { + "endpoint": "", + }, + }, + "name": "", + }, + ], + }, + }, + ] + `); + }); + test('should pass with valid args and ignoreCache flag', async () => { let sampleMesh = { meshConfig: { diff --git a/src/plugins/queryConfig/__tests__/queryConfig.test.js b/src/plugins/queryConfig/__tests__/queryConfig.test.js new file mode 100644 index 00000000..08adaa46 --- /dev/null +++ b/src/plugins/queryConfig/__tests__/queryConfig.test.js @@ -0,0 +1,167 @@ +const { EnvelopArmorPlugin } = require('@escape.tech/graphql-armor'); +const useQueryConfig = require('../index'); + +jest.mock('@escape.tech/graphql-armor', () => ({ + EnvelopArmorPlugin: jest.fn(() => ({ pluginName: 'EnvelopArmor' })), +})); + +beforeEach(() => { + EnvelopArmorPlugin.mockClear(); +}); + +describe('useQueryConfig', () => { + describe('no config — all protections disabled', () => { + it('disables all protections when called with undefined', () => { + useQueryConfig(undefined); + expect(EnvelopArmorPlugin).toHaveBeenCalledWith({ + costLimit: { enabled: false }, + maxDepth: { enabled: false }, + maxAliases: { enabled: false }, + maxTokens: { enabled: false }, + maxDirectives: { enabled: false }, + blockFieldSuggestion: { enabled: false }, + }); + }); + + it('disables all protections when called with empty object', () => { + useQueryConfig({}); + expect(EnvelopArmorPlugin).toHaveBeenCalledWith({ + costLimit: { enabled: false }, + maxDepth: { enabled: false }, + maxAliases: { enabled: false }, + maxTokens: { enabled: false }, + maxDirectives: { enabled: false }, + blockFieldSuggestion: { enabled: false }, + }); + }); + }); + + describe('maxDepth', () => { + it('passes enabled and limit (mapped to n) to armor', () => { + useQueryConfig({ maxDepth: { enabled: true, limit: 5 } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].maxDepth).toEqual({ enabled: true, n: 5 }); + }); + + it('passes enabled: false with no limit', () => { + useQueryConfig({ maxDepth: { enabled: false } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].maxDepth).toEqual({ + enabled: false, + n: undefined, + }); + }); + + it('passes enabled: false with limit retained', () => { + useQueryConfig({ maxDepth: { enabled: false, limit: 3 } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].maxDepth).toEqual({ enabled: false, n: 3 }); + }); + }); + + describe('maxAliases', () => { + it('passes enabled and limit (mapped to n)', () => { + useQueryConfig({ maxAliases: { enabled: true, limit: 10 } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].maxAliases).toEqual({ enabled: true, n: 10 }); + }); + + it('passes n: undefined when enabled but no limit — armor uses its default', () => { + useQueryConfig({ maxAliases: { enabled: true } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].maxAliases).toEqual({ + enabled: true, + n: undefined, + }); + }); + }); + + describe('maxTokens', () => { + it('passes enabled and limit (mapped to n)', () => { + useQueryConfig({ maxTokens: { enabled: true, limit: 500 } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].maxTokens).toEqual({ enabled: true, n: 500 }); + }); + }); + + describe('maxDirectives', () => { + it('passes enabled and limit (mapped to n)', () => { + useQueryConfig({ maxDirectives: { enabled: true, limit: 25 } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].maxDirectives).toEqual({ enabled: true, n: 25 }); + }); + }); + + describe('costLimit', () => { + it('passes enabled and maxCost', () => { + useQueryConfig({ costLimit: { enabled: true, maxCost: 2000 } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].costLimit).toEqual({ + enabled: true, + maxCost: 2000, + }); + }); + + it('passes enabled: false with maxCost retained', () => { + useQueryConfig({ costLimit: { enabled: false, maxCost: 1000 } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].costLimit).toEqual({ + enabled: false, + maxCost: 1000, + }); + }); + }); + + describe('blockFieldSuggestion', () => { + function getPlugin(queryConfig) { + const [, plugin] = useQueryConfig(queryConfig); + return plugin; + } + + function runValidate(plugin, messages) { + const errors = messages.map(m => Object.assign(new Error(m), {})); + let result = errors; + plugin.onValidate()({ + valid: false, + result: errors, + setResult: e => { + result = e; + }, + }); + return result.map(e => e.message); + } + + it('should always pass { enabled: false } to armor', () => { + useQueryConfig({ blockFieldSuggestion: { enabled: true, mask: '[hidden]' } }); + expect(EnvelopArmorPlugin.mock.calls[0][0].blockFieldSuggestion).toEqual({ enabled: false }); + }); + + it('should strip "Did you mean" hint when enabled', () => { + const out = runValidate(getPlugin({ blockFieldSuggestion: { enabled: true } }), [ + 'Cannot query field "continentz". Did you mean "continents"?', + ]); + expect(out[0]).toBe('Cannot query field "continentz".'); + }); + + it('should use custom mask when provided', () => { + const out = runValidate( + getPlugin({ blockFieldSuggestion: { enabled: true, mask: '[hidden]' } }), + ['Cannot query field "foo". Did you mean "bar"?'], + ); + expect(out[0]).toBe('Cannot query field "foo". [hidden]'); + }); + + it('should not strip hint when disabled', () => { + const out = runValidate(getPlugin({ blockFieldSuggestion: { enabled: false } }), [ + 'Cannot query field "x". Did you mean "y"?', + ]); + expect(out[0]).toBe('Cannot query field "x". Did you mean "y"?'); + }); + + it('should strip multi-candidate hint with custom mask', () => { + const out = runValidate( + getPlugin({ blockFieldSuggestion: { enabled: true, mask: '[hidden]' } }), + ['Cannot query field "nam". Did you mean "name", "names", or "named"?'], + ); + expect(out[0]).toBe('Cannot query field "nam". [hidden]'); + }); + }); + + describe('maskErrors', () => { + it('is not passed to EnvelopArmorPlugin — handled separately via yoga maskedErrors', () => { + useQueryConfig({ maskErrors: { enabled: true, message: 'Unexpected error.' } }); + expect(EnvelopArmorPlugin.mock.calls[0][0]).not.toHaveProperty('maskErrors'); + }); + }); +}); diff --git a/src/plugins/queryConfig/index.js b/src/plugins/queryConfig/index.js new file mode 100644 index 00000000..d83b2820 --- /dev/null +++ b/src/plugins/queryConfig/index.js @@ -0,0 +1,51 @@ +const { EnvelopArmorPlugin } = require('@escape.tech/graphql-armor'); + +// All protections are opt-in — nothing enabled unless explicitly configured. +// blockFieldSuggestion uses a custom onValidate hook: armor's graphql@^16.10.0 creates a +// duplicate class under nohoist, breaking its instanceof GraphQLError check. +function useQueryConfig(queryConfig) { + return [ + EnvelopArmorPlugin({ + costLimit: queryConfig?.costLimit + ? { enabled: queryConfig.costLimit.enabled, maxCost: queryConfig.costLimit.maxCost } + : { enabled: false }, + maxDepth: queryConfig?.maxDepth + ? { enabled: queryConfig.maxDepth.enabled, n: queryConfig.maxDepth.limit } + : { enabled: false }, + maxAliases: queryConfig?.maxAliases + ? { enabled: queryConfig.maxAliases.enabled, n: queryConfig.maxAliases.limit } + : { enabled: false }, + maxTokens: queryConfig?.maxTokens + ? { enabled: queryConfig.maxTokens.enabled, n: queryConfig.maxTokens.limit } + : { enabled: false }, + maxDirectives: queryConfig?.maxDirectives + ? { enabled: queryConfig.maxDirectives.enabled, n: queryConfig.maxDirectives.limit } + : { enabled: false }, + blockFieldSuggestion: { enabled: false }, + }), + blockFieldSuggestionPlugin(queryConfig), + ]; +} + +function blockFieldSuggestionPlugin(queryConfig) { + const enabled = queryConfig?.blockFieldSuggestion?.enabled === true; + const mask = queryConfig?.blockFieldSuggestion?.mask ?? ''; + return { + onValidate() { + return function onValidateEnd({ valid, result, setResult }) { + if (!valid && enabled) { + setResult( + result.map(error => { + if (typeof error.message === 'string') { + error.message = error.message.replace(/Did you mean ".+"\?/g, mask).trim(); + } + return error; + }), + ); + } + }; + }, + }; +} + +module.exports = useQueryConfig; diff --git a/src/server.js b/src/server.js index 7e9e4378..f66d794a 100644 --- a/src/server.js +++ b/src/server.js @@ -4,13 +4,14 @@ import { KvStateApiImpl } from './state'; const { getCorsOptions } = require('./cors'); const { createYoga } = require('graphql-yoga'); -const { GraphQLError } = require('graphql/error'); const { loadMeshSecrets, getSecretsHandler } = require('./secrets'); const useComplianceHeaders = require('./plugins/complianceHeaders'); const UseHttpDetailsExtensions = require('./plugins/httpDetailsExtensions'); +const useQueryConfig = require('./plugins/queryConfig'); const useSourceHeaders = require('@adobe/plugin-source-headers'); const { useDisableIntrospection } = require('@envelop/disable-introspection'); +const { maskError } = require('./utils/maskError'); let meshInstance$; @@ -31,6 +32,8 @@ async function buildMeshInstance(meshArtifacts, meshConfig) { options.additionalEnvelopPlugins.push(useDisableIntrospection()); } + options.additionalEnvelopPlugins.push(...useQueryConfig(meshConfig.queryConfig)); + return getMesh(options).then(mesh => { const id = mesh.pubsub.subscribe('destroy', () => { meshInstance$ = undefined; @@ -67,18 +70,14 @@ async function buildYogaServer(env, tenantMesh, meshConfig, meshSecrets) { state: stateApi, }), maskedErrors: { - maskError: maskError, + maskError: error => + maskError(error, { + mask: meshConfig.queryConfig?.maskErrors?.enabled === true, + message: meshConfig.queryConfig?.maskErrors?.message, + }), }, logging: 'debug', }); } -const maskError = error => { - if (error instanceof GraphQLError && error.extensions?.http?.headers) { - delete error.extensions.http.headers; - } - - return error; -}; - module.exports = { buildServer }; diff --git a/src/utils/__tests__/maskError.test.js b/src/utils/__tests__/maskError.test.js new file mode 100644 index 00000000..edf3c38b --- /dev/null +++ b/src/utils/__tests__/maskError.test.js @@ -0,0 +1,80 @@ +const { GraphQLError } = require('graphql/error'); +const { maskError } = require('../maskError'); + +describe('maskError', () => { + describe('header stripping', () => { + it('should remove http.headers from GraphQLError extensions', () => { + const error = new GraphQLError('oops', { + extensions: { http: { headers: { 'set-cookie': 'x' } } }, + }); + maskError(error, { mask: false }); + expect(error.extensions?.http?.headers).toBeUndefined(); + }); + + it('should leave other extensions intact when http.headers is absent', () => { + const error = new GraphQLError('oops', { extensions: { code: 'SOME_CODE' } }); + maskError(error, { mask: false }); + expect(error.extensions?.code).toBe('SOME_CODE'); + }); + + it('should return non-GraphQL errors unchanged when mask is false', () => { + const error = new Error('plain error'); + const result = maskError(error, { mask: false }); + expect(result).toBe(error); + }); + }); + + describe('mask: true (default)', () => { + it('should pass through errors with extensions.code', () => { + const error = new GraphQLError('unauthorized', { extensions: { code: 'UNAUTHENTICATED' } }); + const result = maskError(error); + expect(result).toBe(error); + }); + + it('should replace unknown GraphQL errors with generic message', () => { + const error = new GraphQLError('internal details'); + const result = maskError(error); + expect(result.message).toBe('Oops. Something went wrong.'); + }); + + it('should replace non-GraphQL errors with generic message', () => { + const error = new Error('internal details'); + const result = maskError(error); + expect(result.message).toBe('Oops. Something went wrong.'); + }); + + it('should use custom message from opts.message when provided', () => { + const error = new GraphQLError('internal details'); + const result = maskError(error, { message: 'Something went wrong.' }); + expect(result.message).toBe('Something went wrong.'); + }); + + it('should fall back to generic message when opts.message is not provided', () => { + const error = new GraphQLError('internal details'); + const result = maskError(error, { mask: true }); + expect(result.message).toBe('Oops. Something went wrong.'); + }); + + it('should strip http.headers before masking', () => { + const error = new GraphQLError('oops', { + extensions: { http: { headers: { 'set-cookie': 'x' } } }, + }); + maskError(error); + expect(error.extensions?.http?.headers).toBeUndefined(); + }); + }); + + describe('mask: false', () => { + it('should return the original error message unchanged', () => { + const error = new GraphQLError('internal details'); + const result = maskError(error, { mask: false }); + expect(result.message).toBe('internal details'); + }); + + it('should ignore opts.message when masking is disabled', () => { + const error = new GraphQLError('internal details'); + const result = maskError(error, { mask: false, message: 'Should be ignored.' }); + expect(result.message).toBe('internal details'); + }); + }); +}); diff --git a/src/utils/maskError.js b/src/utils/maskError.js new file mode 100644 index 00000000..fb963582 --- /dev/null +++ b/src/utils/maskError.js @@ -0,0 +1,21 @@ +const { GraphQLError } = require('graphql/error'); + +// Always strips http.headers from extensions (prevents duplicate Set-Cookie → Cloudflare error). +// When mask is true (default): errors with extensions.code pass through; all others are replaced +// with opts.message to avoid leaking stack traces or internal details. +const maskError = (error, opts) => { + if (error instanceof GraphQLError && error.extensions?.http?.headers) { + delete error.extensions.http.headers; + } + + if (opts?.mask !== false) { + if (error instanceof GraphQLError && error.extensions?.code) { + return error; + } + return new GraphQLError(opts?.message || 'Oops. Something went wrong.'); + } + + return error; +}; + +module.exports = { maskError }; diff --git a/yarn.lock b/yarn.lock index af332082..77a389ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@adobe-apimesh/mesh-builder@^2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@adobe-apimesh/mesh-builder/-/mesh-builder-2.4.1.tgz#07429a90777bdd44292fe75b9f619f538249e490" - integrity sha512-286JqaZUfc2bOjR59Qp8ZHU+di/XoKS5QME3bwj8UoyDo6obw6g4L6uuiZqCwTnlHkucHdj2JgOWvvRNfgu04A== +"@adobe-apimesh/mesh-builder@^2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@adobe-apimesh/mesh-builder/-/mesh-builder-2.5.1.tgz#0d8b7c924d996d33b11aded5ef9884745aed90d8" + integrity sha512-POh0t2ozeFkZm5N145e1U24k1IlhySgY+gN8fAr0eO5t6zuRq8EoBcSH/8k2yPPtXb2u/igUj2BOwui5c8p7UQ== dependencies: "@fastify/request-context" "^4.1.0" eslint "^8.39.0" @@ -1079,6 +1079,16 @@ "@envelop/types" "5.0.0" tslib "^2.5.0" +"@envelop/core@^5.2.3": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@envelop/core/-/core-5.5.1.tgz#ca30c927b3a7d7f118d36111e17b355eedae9ff4" + integrity sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw== + dependencies: + "@envelop/instrumentation" "^1.0.0" + "@envelop/types" "^5.2.1" + "@whatwg-node/promise-helpers" "^1.2.4" + tslib "^2.5.0" + "@envelop/disable-introspection@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@envelop/disable-introspection/-/disable-introspection-6.0.0.tgz#f611514ba7505f6e38fb171b3c765d837d687d4d" @@ -1094,6 +1104,14 @@ "@graphql-tools/utils" "^8.8.0" tslib "^2.5.0" +"@envelop/instrumentation@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@envelop/instrumentation/-/instrumentation-1.0.0.tgz#43268392e065d8ba975cacbdf4fc297dfe3e11e5" + integrity sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw== + dependencies: + "@whatwg-node/promise-helpers" "^1.2.1" + tslib "^2.5.0" + "@envelop/types@3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@envelop/types/-/types-3.0.2.tgz#a4b29375b7fcee39bb5830f87f66bbc815cf305e" @@ -1108,6 +1126,14 @@ dependencies: tslib "^2.5.0" +"@envelop/types@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@envelop/types/-/types-5.2.1.tgz#6bc9713f2aea56d7de3ea39e8bb70035c0475b36" + integrity sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg== + dependencies: + "@whatwg-node/promise-helpers" "^1.0.0" + tslib "^2.5.0" + "@envelop/validation-cache@^5.1.2": version "5.1.3" resolved "https://registry.yarnpkg.com/@envelop/validation-cache/-/validation-cache-5.1.3.tgz#8348453183af348147e2b690a431b6ca81d2a6bc" @@ -1242,6 +1268,85 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== +"@escape.tech/graphql-armor-block-field-suggestions@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@escape.tech/graphql-armor-block-field-suggestions/-/graphql-armor-block-field-suggestions-3.0.1.tgz#a1d234fa2242cb04388dace24aa8926326ba5fa1" + integrity sha512-pZ+5aFgGW/pUul7nDOZ3PoeWAd9kLDspQ0R+fpz2aTjdIT0yI+f+ZbAGeTVmr5RypRDwjwokG/HjWoxTYTcRwQ== + dependencies: + graphql "^16.10.0" + optionalDependencies: + "@envelop/core" "^5.2.3" + +"@escape.tech/graphql-armor-cost-limit@2.4.3": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@escape.tech/graphql-armor-cost-limit/-/graphql-armor-cost-limit-2.4.3.tgz#dfc7803d54984ff61b33a45a8d0308355db532e6" + integrity sha512-fLZTlJjrjinpNhbv5VP6f8Ce4MiQzbcHtCNaCPVaHqArTEbN7vDnlVLiH+VcmZBmOwU6Si97lKdlOXWNgB5ytw== + dependencies: + graphql "^16.10.0" + optionalDependencies: + "@envelop/core" "^5.2.3" + "@escape.tech/graphql-armor-types" "0.7.0" + +"@escape.tech/graphql-armor-max-aliases@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@escape.tech/graphql-armor-max-aliases/-/graphql-armor-max-aliases-2.6.2.tgz#6c1951749e86c094e2562c21975909eaf0b5ba62" + integrity sha512-SDk7pAzY6gutsdZ3NlyY55RrytrCPxJJxSN/DBfIGKphTrfBvKQWTnioQ9OlLP9kPjCE6XM5UWwGt7uqbpKSYA== + dependencies: + graphql "^16.10.0" + optionalDependencies: + "@envelop/core" "^5.2.3" + "@escape.tech/graphql-armor-types" "0.7.0" + +"@escape.tech/graphql-armor-max-depth@2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@escape.tech/graphql-armor-max-depth/-/graphql-armor-max-depth-2.4.2.tgz#a11a5dd5551f1f1f07b6dc1797bd4d3358fa31b7" + integrity sha512-J9fbW1+W4u3GAcf19wwS0zrNGICCbWn/glvopCoC11Ga0reXvGwgr8EcyuHjTFLL7+pPvWAeVhP4qo6hybcB9w== + dependencies: + graphql "^16.10.0" + optionalDependencies: + "@envelop/core" "^5.2.3" + "@escape.tech/graphql-armor-types" "0.7.0" + +"@escape.tech/graphql-armor-max-directives@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@escape.tech/graphql-armor-max-directives/-/graphql-armor-max-directives-2.3.1.tgz#3c6f658c9a3a31b439ee9917078b8f47e73f5932" + integrity sha512-YieT/NrTgZdLu3hzWfCgpmUKO1o/7Im9C4TI0e4O85Wa+zLit/DzHO4ZCIY//w8nkBf1KStkA1pD/v0daEfY5Q== + dependencies: + graphql "^16.10.0" + optionalDependencies: + "@envelop/core" "^5.2.3" + "@escape.tech/graphql-armor-types" "0.7.0" + +"@escape.tech/graphql-armor-max-tokens@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@escape.tech/graphql-armor-max-tokens/-/graphql-armor-max-tokens-2.5.1.tgz#915434e5735529b485461ffda88331aa03ea424a" + integrity sha512-XHui2npOz7Jn8shBZqfyeocWhdl0pUbKiaWmvbF+5rvNoRIGMgwMtaVhmf9ia8oGGbd+cx5EYo1v+oKHzIm79w== + dependencies: + graphql "^16.10.0" + optionalDependencies: + "@envelop/core" "^5.2.3" + "@escape.tech/graphql-armor-types" "0.7.0" + +"@escape.tech/graphql-armor-types@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@escape.tech/graphql-armor-types/-/graphql-armor-types-0.7.0.tgz#391b4a5136db63a6ac2d9a6174fa6f5be6198d0b" + integrity sha512-RHxyyp6PDgS6NAPnnmB6JdmUJ6oqhpSHFbsglGWeCcnNzceA5AkQFpir7VIDbVyS8LNC1xhipOtk7f9ycrIemQ== + dependencies: + graphql "^16.0.0" + +"@escape.tech/graphql-armor@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@escape.tech/graphql-armor/-/graphql-armor-3.2.0.tgz#d668e486b63760d9931128725552863f352ce6d3" + integrity sha512-0tG2YOIPiDg2Oe7uEqkYl808VK9OKRyXx4NBa3w+N9XV6NaQ4XxGkDJMtps0lx5DANe10d6nbf6Ml2Z6vekPwA== + dependencies: + "@escape.tech/graphql-armor-block-field-suggestions" "3.0.1" + "@escape.tech/graphql-armor-cost-limit" "2.4.3" + "@escape.tech/graphql-armor-max-aliases" "2.6.2" + "@escape.tech/graphql-armor-max-depth" "2.4.2" + "@escape.tech/graphql-armor-max-directives" "2.3.1" + "@escape.tech/graphql-armor-max-tokens" "2.5.1" + graphql "^16.10.0" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -3624,7 +3729,7 @@ fast-querystring "^1.1.1" tslib "^2.6.3" -"@whatwg-node/promise-helpers@^1.0.0": +"@whatwg-node/promise-helpers@^1.0.0", "@whatwg-node/promise-helpers@^1.2.1", "@whatwg-node/promise-helpers@^1.2.4": version "1.3.2" resolved "https://registry.yarnpkg.com/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz#3b54987ad6517ef6db5920c66a6f0dada606587d" integrity sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA== @@ -6497,6 +6602,11 @@ graphql-yoga@^5.0.0: lru-cache "^10.0.0" tslib "^2.5.2" +graphql@^16.0.0, graphql@^16.10.0: + version "16.14.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.14.0.tgz#f1128a74b16a34d1245c8436bb07b488d87b83e1" + integrity sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q== + graphql@^16.6.0: version "16.8.1" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"