diff --git a/.github/workflows/tests-e2e.yaml b/.github/workflows/tests-e2e.yaml index 8f0e228b68..1b7422a0d2 100644 --- a/.github/workflows/tests-e2e.yaml +++ b/.github/workflows/tests-e2e.yaml @@ -27,7 +27,7 @@ jobs: - name: setup environment uses: ./.github/actions/setup with: - codegen: false + codegen: true actor: test-e2e cacheTurbo: false @@ -43,7 +43,7 @@ jobs: timeout-minutes: 10 run: | docker compose \ - --env-file docker/.end2end.env \ + --env-file integration-tests/.env \ -f docker/docker-compose.community.yml \ -f docker/docker-compose.end2end.yml \ up -d --wait @@ -65,7 +65,7 @@ jobs: docker --version docker ps --format json | jq . docker compose \ - --env-file docker/.end2end.env \ + --env-file integration-tests/.env \ -f docker/docker-compose.community.yml \ -f docker/docker-compose.end2end.yml \ logs diff --git a/cypress.config.ts b/cypress.config.ts index 1d57cfaaeb..4bd27e952a 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,19 +1,38 @@ +import * as fs from 'node:fs'; // eslint-disable-next-line import/no-extraneous-dependencies -- cypress SHOULD be a dev dependency -import fs from 'node:fs'; import { defineConfig } from 'cypress'; +import { initSeed } from './integration-tests/testkit/seed'; + +if (!process.env.RUN_AGAINST_LOCAL_SERVICES) { + const dotenv = await import('dotenv'); + dotenv.config({ path: import.meta.dirname + '/integration-tests/.env' }); +} const isCI = Boolean(process.env.CI); +export const seed = initSeed(); + export default defineConfig({ video: isCI, screenshotOnRunFailure: isCI, defaultCommandTimeout: 15_000, // sometimes the app takes longer to load, especially in the CI retries: 2, - env: { - POSTGRES_URL: 'postgresql://postgres:postgres@localhost:5432/registry', - }, e2e: { setupNodeEvents(on) { + on('task', { + async seedTarget() { + const owner = await seed.createOwner(); + const org = await owner.createOrg(); + const project = await org.createProject(); + const slug = `${org.organization.slug}/${project.project.slug}/${project.target.slug}`; + return { + slug, + refreshToken: owner.ownerRefreshToken, + email: owner.ownerEmail, + }; + }, + }); + on('after:spec', (_, results) => { if (results && results.video) { // Do we have failures for any retry attempts? diff --git a/cypress/e2e/preflight-script.cy.ts b/cypress/e2e/preflight-script.cy.ts new file mode 100644 index 0000000000..e5e0be29f3 --- /dev/null +++ b/cypress/e2e/preflight-script.cy.ts @@ -0,0 +1,212 @@ +beforeEach(() => { + cy.clearLocalStorage().then(async () => { + cy.task('seedTarget').then(({ slug, refreshToken }: any) => { + cy.setCookie('sRefreshToken', refreshToken); + + cy.visit(`/${slug}/laboratory`); + cy.get('[aria-label*="Preflight Script"]').click(); + }); + }); +}); + +describe('Preflight Script', () => { + it('mini script editor should be read only', () => { + cy.dataCy('toggle-preflight-script').click(); + // Wait loading disappears + cy.dataCy('preflight-script-editor-mini').should('not.contain', 'Loading'); + // Click + cy.dataCy('preflight-script-editor-mini').click(); + // And type + cy.dataCy('preflight-script-editor-mini').within(() => { + cy.get('textarea').type('🐝', { force: true }); + }); + cy.dataCy('preflight-script-editor-mini').should( + 'have.text', + 'Cannot edit in read-only editor', + ); + }); +}); + +describe('Preflight Script Modal', () => { + const script = 'console.log("Hello_world")'; + const env = '{"foo":123}'; + + const writeScript = (script: string) => { + cy.dataCy('preflight-script-editor').within(() => { + cy.get('textarea').type(script, { delay: 0, force: true }); + }); + }; + + beforeEach(() => { + cy.dataCy('preflight-script-modal-button').click(); + cy.dataCy('env-editor').within(() => { + cy.get('textarea').type(env, { + parseSpecialCharSequences: false, + force: true, + }); + }); + }); + + it('should save script and env variables when submitting', () => { + writeScript(script); + cy.dataCy('preflight-script-modal-submit').click(); + cy.dataCy('env-editor-mini').should('have.text', env); + cy.dataCy('toggle-preflight-script').click(); + cy.dataCy('preflight-script-editor-mini').should('have.text', script); + cy.reload(); + cy.get('[aria-label*="Preflight Script"]').click(); + cy.dataCy('env-editor-mini').should('have.text', env); + cy.dataCy('preflight-script-editor-mini').should('have.text', script); + }); + + it('should run script and show console/error output', () => { + writeScript(script); + cy.dataCy('run-preflight-script').click(); + cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)'); + + cy.dataCy('preflight-script-editor').within(() => { + cy.get('textarea').type('{CMD}{A}{Backspace}', { force: true }); + cy.get('textarea').type( + `console.info(1) +console.warn(true) +console.error('Fatal') +throw new TypeError('Test')`, + { force: true }, + ); + }); + cy.dataCy('run-preflight-script').click(); + // First log previous log message + cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)'); + // After the new logs + cy.dataCy('console-output').should( + 'contain', + [ + 'Info: 1 (Line: 1, Column: 1)', + 'Warn: true (Line: 2, Column: 1)', + 'Error: Fatal (Line: 3, Column: 1)', + 'TypeError: Test (Line: 4, Column: 7)', + ].join(''), + ); + }); + + it('should run script and update env variables', () => { + cy.intercept('test.com', { body: '"Fixture"' }); + cy.dataCy('preflight-script-editor').within(() => { + cy.get('textarea').type( + `const response = await fetch('test.com') +const data = await response.json() +console.log(response) +console.info(data) +lab.environment.set('my-test', data)`, + { force: true }, + ); + }); + + cy.dataCy('run-preflight-script').click(); + cy.dataCy('console-output').should( + 'contain', + ['Log: [object Response]', ' (Line: 3, Column: 1)', 'Info: Fixture'].join(''), + ); + cy.dataCy('env-editor').should( + 'include.text', + // replace space with   + '{ "foo": 123, "my-test": "Fixture"}'.replaceAll(' ', '\xa0'), + ); + }); + + it('`crypto-js` should works, since we removed `...Buffer` and `...Array` global variables', () => { + cy.dataCy('preflight-script-editor').within(() => { + cy.get('textarea').type('console.log(lab.CryptoJS.SHA256("🐝"))', { delay: 0, force: true }); + }); + cy.dataCy('run-preflight-script').click(); + cy.dataCy('console-output').should('contain', 'Info: Using crypto-js version:'); + cy.dataCy('console-output').should( + 'contain', + 'Log: d5b51e79e4be0c4f4d6b9a14e16ca864de96afe68459e60a794e80393a4809e8', + ); + }); + + it('should disallow eval', () => { + cy.dataCy('preflight-script-editor').within(() => { + cy.get('textarea').type('eval()', { delay: 0, force: true }); + }); + cy.dataCy('preflight-script-modal-submit').click(); + cy.get('body').contains('Usage of dangerous statement like eval() or Function("").'); + }); + + it('should disallow invalid code', () => { + cy.dataCy('preflight-script-editor').within(() => { + cy.get('textarea').type('🐝', { delay: 0, force: true }); + }); + cy.dataCy('preflight-script-modal-submit').click(); + cy.get('body').contains("[1:1]: Illegal character '}"); + }); +}); + +describe('Execution', () => { + it('should replace with env editor values', () => { + cy.dataCy('toggle-preflight-script').click(); + cy.get('[data-name="headers"]').click(); + cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type( + '{ "__test": "{{foo}} bar {{nonExist}}" }', + { + force: true, + parseSpecialCharSequences: false, + }, + ); + cy.dataCy('env-editor-mini').within(() => { + cy.get('textarea').type('{"foo":"injected"}', { + force: true, + parseSpecialCharSequences: false, + }); + }); + cy.intercept('/api/lab/foo/my-new-project/development', req => { + expect(req.headers.__test).to.equal('injected bar {{nonExist}}'); + }); + cy.get('body').type('{ctrl}{enter}'); + }); + + it('should execute script, update env editor and replace headers', () => { + cy.dataCy('toggle-preflight-script').click(); + cy.get('[data-name="headers"]').click(); + cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type( + '{ "__test": "{{foo}}" }', + { + force: true, + parseSpecialCharSequences: false, + }, + ); + cy.dataCy('preflight-script-modal-button').click(); + cy.dataCy('preflight-script-editor').within(() => { + cy.get('textarea').type(`lab.environment.set('foo', 92)`, { force: true }); + }); + cy.dataCy('preflight-script-modal-submit').click(); + cy.intercept('/api/lab/foo/my-new-project/development', req => { + expect(req.headers.__test).to.equal('92'); + }); + cy.get('.graphiql-execute-button').click(); + }); + + it('should not execute script if disabled', () => { + cy.get('[data-name="headers"]').click(); + cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type( + '{ "__test": "{{foo}}" }', + { + force: true, + parseSpecialCharSequences: false, + }, + ); + cy.dataCy('preflight-script-modal-button').click(); + cy.dataCy('preflight-script-editor').within(() => { + cy.get('textarea').type(`lab.environment.set('foo', 92)`, { force: true }); + }); + cy.dataCy('env-editor').within(() => { + cy.get('textarea').type(`{"foo":10}`, { force: true, parseSpecialCharSequences: false }); + }); + cy.dataCy('preflight-script-modal-submit').click(); + cy.intercept('/api/lab/foo/my-new-project/development', req => { + expect(req.headers.__test).to.equal('10'); + }); + cy.get('.graphiql-execute-button').click(); + }); +}); diff --git a/cypress/local.sh b/cypress/local.sh index b7ecdf5039..621802ee76 100755 --- a/cypress/local.sh +++ b/cypress/local.sh @@ -25,7 +25,7 @@ cd .. docker buildx bake -f docker/docker.hcl build --load echo "⬆️ Running all local containers..." -docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env --env-file ./docker/.end2end.env up -d --wait +docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env up -d --wait echo "✅ E2E tests environment is ready. To run tests now, use:" echo "" diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 548af3377a..f545e65eae 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "es5", - "lib": ["es5", "dom"], + "target": "es2021", + "lib": ["es2021", "dom"], "types": ["node", "cypress"] }, - "include": ["**/*.ts"] + "include": ["**/*.ts", "../integration-tests/testkit/**/*.ts"] } diff --git a/deployment/index.ts b/deployment/index.ts index 3ac1dbd509..a9b4a07bc9 100644 --- a/deployment/index.ts +++ b/deployment/index.ts @@ -16,7 +16,7 @@ import { deployKafka } from './services/kafka'; import { deployObservability } from './services/observability'; import { deploySchemaPolicy } from './services/policy'; import { deployPostgres } from './services/postgres'; -import { deployProxy } from './services/proxy'; +import { deployLabWorker, deployProxy } from './services/proxy'; import { deployRateLimit } from './services/rate-limit'; import { deployRedis } from './services/redis'; import { deployS3, deployS3Mirror } from './services/s3'; @@ -313,6 +313,13 @@ const proxy = deployProxy({ environment, }); +deployLabWorker({ + reverseProxy: proxy, + app, + environment, + path: '/worker.js', +}); + deployCloudFlareSecurityTransform({ environment, // Paths used by 3rd-party software. @@ -351,4 +358,4 @@ export const schemaApiServiceId = schema.service.id; export const webhooksApiServiceId = webhooks.service.id; export const appId = app.deployment.id; -export const publicIp = proxy!.status.loadBalancer.ingress[0].ip; +export const publicIp = proxy.get()!.status.loadBalancer.ingress[0].ip; diff --git a/deployment/services/app.ts b/deployment/services/app.ts index 81a37d9ab9..17e88284f3 100644 --- a/deployment/services/app.ts +++ b/deployment/services/app.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'crypto'; import * as pulumi from '@pulumi/pulumi'; import { serviceLocalEndpoint } from '../utils/local-endpoint'; import { ServiceDeployment } from '../utils/service-deployment'; @@ -71,6 +72,7 @@ export function deployApp({ AUTH_ORGANIZATION_OIDC: '1', MEMBER_ROLES_DEADLINE: appEnv.MEMBER_ROLES_DEADLINE, PORT: '3000', + LABORATORY_PREFLIGHT_WORKER_URL: `https://${environment.labWorkerDns}/worker.js?hash=${randomUUID()}`, }, port: 3000, }, diff --git a/deployment/services/environment.ts b/deployment/services/environment.ts index 3c7cd41464..677c90ca41 100644 --- a/deployment/services/environment.ts +++ b/deployment/services/environment.ts @@ -39,6 +39,7 @@ export function prepareEnvironment(input: { release: input.release, appDns, rootDns: input.rootDns, + labWorkerDns: `lab-worker.${input.rootDns}`, }; } diff --git a/deployment/services/proxy.ts b/deployment/services/proxy.ts index 9249c679f3..9aa5a7db32 100644 --- a/deployment/services/proxy.ts +++ b/deployment/services/proxy.ts @@ -7,6 +7,30 @@ import { GraphQL } from './graphql'; import { Observability } from './observability'; import { Usage } from './usage'; +export function deployLabWorker({ + reverseProxy, + app, + environment, + path, +}: { + reverseProxy: Proxy; + app: App; + environment: Environment; + path: string; +}) { + const dnsName = `lab-worker.${environment.rootDns}`; + reverseProxy.registerInternalProxy(dnsName, { + path, + service: app.service, + host: dnsName, + customRewrite: '/preflight-script-worker.js', + }); + + return { + workerUrl: `https://${dnsName}${path}`, + }; +} + export function deployProxy({ graphql, app, @@ -100,6 +124,5 @@ export function deployProxy({ service: usage.service, retriable: true, }, - ]) - .get(); + ]); } diff --git a/deployment/utils/reverse-proxy.ts b/deployment/utils/reverse-proxy.ts index c5d2290835..23a005ecc2 100644 --- a/deployment/utils/reverse-proxy.ts +++ b/deployment/utils/reverse-proxy.ts @@ -14,6 +14,67 @@ export class Proxy { private staticIp?: { address?: string; aksReservedIpResourceGroup?: string }, ) {} + registerInternalProxy( + dnsRecord: string, + route: { + path: string; + service: k8s.core.v1.Service; + host: string; + customRewrite: string; + }, + ) { + const cert = new k8s.apiextensions.CustomResource(`cert-${dnsRecord}`, { + apiVersion: 'cert-manager.io/v1', + kind: 'Certificate', + metadata: { + name: dnsRecord, + }, + spec: { + commonName: dnsRecord, + dnsNames: [dnsRecord], + issuerRef: { + name: this.tlsSecretName, + kind: 'ClusterIssuer', + }, + secretName: dnsRecord, + }, + }); + + new k8s.apiextensions.CustomResource( + `internal-proxy-${dnsRecord}`, + { + apiVersion: 'projectcontour.io/v1', + kind: 'HTTPProxy', + metadata: { + name: `internal-proxy-metadata-${dnsRecord}`, + }, + spec: { + virtualhost: { + fqdn: route.host, + tls: { + secretName: dnsRecord, + }, + }, + routes: [ + { + conditions: [{ prefix: route.path }], + services: [ + { + name: route.service.metadata.name, + port: route.service.spec.ports[0].port, + }, + ], + pathRewritePolicy: { + replacePrefix: [{ prefix: route.path, replacement: route.customRewrite }], + }, + }, + ], + }, + }, + { dependsOn: [cert, this.lbService!] }, + ); + } + registerService( dns: { record: string; apex?: boolean }, routes: { @@ -29,7 +90,7 @@ export class Proxy { withWwwDomain?: boolean; // https://projectcontour.io/docs/1.29/config/rate-limiting/#local-rate-limiting rateLimit?: { - // Max amount of request allowed with the "unit" paramter. + // Max amount of request allowed with the "unit" parameter. maxRequests: number; unit: 'second' | 'minute' | 'hour'; // defining the number of requests above the baseline rate that are allowed in a short period of time. diff --git a/docker/.end2end.env b/docker/.end2end.env deleted file mode 100644 index 7a5563f670..0000000000 --- a/docker/.end2end.env +++ /dev/null @@ -1,13 +0,0 @@ -export HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret -export HIVE_EMAIL_FROM=no-reply@graphql-hive.com -export HIVE_APP_BASE_URL=http://localhost:8080 -export SUPERTOKENS_API_KEY=wowverysecuremuchsecret -export CLICKHOUSE_USER=clickhouse -export CLICKHOUSE_PASSWORD=wowverysecuremuchsecret -export REDIS_PASSWORD=wowverysecuremuchsecret -export POSTGRES_PASSWORD=postgres -export POSTGRES_USER=postgres -export POSTGRES_DB=registry -export MINIO_ROOT_USER=minioadmin -export MINIO_ROOT_PASSWORD=minioadmin -export CDN_AUTH_PRIVATE_KEY=6b4721a99bd2ef6c00ce4328f34d95d7 diff --git a/docker/docker-compose.end2end.yml b/docker/docker-compose.end2end.yml index 4b7f2052c8..2a8b710bad 100644 --- a/docker/docker-compose.end2end.yml +++ b/docker/docker-compose.end2end.yml @@ -34,5 +34,9 @@ services: networks: - 'stack' + supertokens: + ports: + - '3567:3567' + networks: stack: {} diff --git a/docs/TESTING.md b/docs/TESTING.md index 0a42828678..fbcec6f70e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -68,29 +68,18 @@ To run integration tests locally, from the pre-build Docker image, follow: e2e Tests are based on Cypress, and matches files that ends with `.cy.ts`. The tests flow runs from a pre-build Docker image. -#### Running from Source Code +#### Running on built Docker images from source code To run e2e tests locally, from the local source code, follow: 1. Make sure you have Docker installed. If you are having issues, try to run `docker system prune` to clean the Docker caches. 2. Install all deps: `pnpm i` -3. Generate types: `pnpm graphql:generate` -4. Build source code: `pnpm build` -5. Set env vars: - ```bash - export COMMIT_SHA="local" - export RELEASE="local" - export BRANCH_NAME="local" - export BUILD_TYPE="" - export DOCKER_TAG=":local" - ``` -6. Compile a local Docker image by running: `docker buildx bake -f docker/docker.hcl build --load` -7. Run the e2e environment, by running: - `docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env up -d --wait` -8. Run Cypress: `pnpm test:e2e` +3. Move into the `cypress` folder (`cd cypress`) +4. Run `./local.sh` for building the project and starting the Docker containers +5. Follow the output instruction from the script for starting the tests -#### Running from Pre-Built Docker Image +#### Running from pre-built Docker image To run integration tests locally, from the pre-build Docker image, follow: @@ -105,7 +94,13 @@ To run integration tests locally, from the pre-build Docker image, follow: export DOCKER_TAG=":IMAGE_TAG_HERE" ``` 6. Run the e2e environment, by running: - `docker compose -f ./docker/docker-compose.community.yml --env-file ./integration-tests/.env up -d --wait` + ``` + docker compose \ + -f ./docker/docker-compose.community.yml \ + -f ./docker/docker-compose.end2end.yml \ + --env-file ./integration-tests/.env \ + up -d --wait + ``` 7. Run Cypress: `pnpm test:e2e` #### Docker Compose Configuration diff --git a/integration-tests/testkit/auth.ts b/integration-tests/testkit/auth.ts index 51b11b2414..107aedc849 100644 --- a/integration-tests/testkit/auth.ts +++ b/integration-tests/testkit/auth.ts @@ -131,6 +131,7 @@ const createSession = async ( */ return { access_token: data.accessToken.token, + refresh_token: data.refreshToken.token, }; } catch (e) { console.warn(`Failed to create session:`, e); @@ -148,15 +149,17 @@ const tokenResponsePromise: { [key: string]: Promise> | null; } = {}; -export function authenticate(email: string): Promise<{ access_token: string }>; +export function authenticate( + email: string, +): Promise<{ access_token: string; refresh_token: string }>; export function authenticate( email: string, oidcIntegrationId?: string, -): Promise<{ access_token: string }>; +): Promise<{ access_token: string; refresh_token: string }>; export function authenticate( email: string | string, oidcIntegrationId?: string, -): Promise<{ access_token: string }> { +): Promise<{ access_token: string; refresh_token: string }> { if (!tokenResponsePromise[email]) { tokenResponsePromise[email] = signUpUserViaEmail(email, password); } diff --git a/integration-tests/testkit/collections.ts b/integration-tests/testkit/collections.ts index 9cff9d4a04..e501162ac5 100644 --- a/integration-tests/testkit/collections.ts +++ b/integration-tests/testkit/collections.ts @@ -202,3 +202,22 @@ export const DeleteOperationMutation = graphql(` } } `); + +export const UpdatePreflightScriptMutation = graphql(` + mutation UpdatePreflightScript($input: UpdatePreflightScriptInput!) { + updatePreflightScript(input: $input) { + ok { + updatedTarget { + id + preflightScript { + id + sourceCode + } + } + } + error { + message + } + } + } +`); diff --git a/integration-tests/testkit/schema-policy.ts b/integration-tests/testkit/schema-policy.ts index 9602e7e986..663a62fd3f 100644 --- a/integration-tests/testkit/schema-policy.ts +++ b/integration-tests/testkit/schema-policy.ts @@ -1,5 +1,5 @@ -import { RuleInstanceSeverityLevel, SchemaPolicyInput } from 'testkit/gql/graphql'; import { graphql } from './gql'; +import { RuleInstanceSeverityLevel, SchemaPolicyInput } from './gql/graphql'; export const OrganizationAndProjectsWithSchemaPolicy = graphql(` query OrganizationAndProjectsWithSchemaPolicy($organization: String!) { diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index 5ee9b61503..83ebb22fca 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -1,13 +1,5 @@ import { humanId } from 'human-id'; import { createPool, sql } from 'slonik'; -import { - OrganizationAccessScope, - ProjectAccessScope, - ProjectType, - RegistryModel, - SchemaPolicyInput, - TargetAccessScope, -} from 'testkit/gql/graphql'; import type { Report } from '../../packages/libraries/core/src/client/usage.js'; import { authenticate, userEmail } from './auth'; import { @@ -17,6 +9,7 @@ import { DeleteOperationMutation, UpdateCollectionMutation, UpdateOperationMutation, + UpdatePreflightScriptMutation, } from './collections'; import { ensureEnv } from './env'; import { @@ -57,21 +50,29 @@ import { updateSchemaVersionStatus, updateTargetValidationSettings, } from './flow'; +import { + OrganizationAccessScope, + ProjectAccessScope, + ProjectType, + RegistryModel, + SchemaPolicyInput, + TargetAccessScope, +} from './gql/graphql'; import { execute } from './graphql'; import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy'; import { collect, CollectedOperation, legacyCollect } from './usage'; import { generateUnique } from './utils'; export function initSeed() { - const pg = { - user: ensureEnv('POSTGRES_USER'), - password: ensureEnv('POSTGRES_PASSWORD'), - host: ensureEnv('POSTGRES_HOST'), - port: ensureEnv('POSTGRES_PORT'), - db: ensureEnv('POSTGRES_DB'), - }; - function createConnectionPool() { + const pg = { + user: ensureEnv('POSTGRES_USER'), + password: ensureEnv('POSTGRES_PASSWORD'), + host: ensureEnv('POSTGRES_HOST'), + port: ensureEnv('POSTGRES_PORT'), + db: ensureEnv('POSTGRES_DB'), + }; + return createPool( `postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`, ); @@ -87,15 +88,18 @@ export function initSeed() { }, }; }, - authenticate: authenticate, + authenticate, generateEmail: () => userEmail(generateUnique()), async createOwner() { const ownerEmail = userEmail(generateUnique()); - const ownerToken = await authenticate(ownerEmail).then(r => r.access_token); + const auth = await authenticate(ownerEmail); + const ownerRefreshToken = auth.refresh_token; + const ownerToken = auth.access_token; return { ownerEmail, ownerToken, + ownerRefreshToken, async createOrg() { const orgSlug = generateUnique(); const orgResult = await createOrganization({ slug: orgSlug }, ownerToken).then(r => @@ -296,6 +300,30 @@ export function initSeed() { return result.createDocumentCollection; }, + async updatePreflightScript({ + sourceCode, + token = ownerToken, + }: { + sourceCode: string; + token?: string; + }) { + const result = await execute({ + document: UpdatePreflightScriptMutation, + variables: { + input: { + selector: { + organizationSlug: organization.slug, + projectSlug: project.slug, + targetSlug: target.slug, + }, + sourceCode, + }, + }, + authToken: token, + }).then(r => r.expectNoGraphQLErrors()); + + return result.updatePreflightScript; + }, async updateDocumentCollection({ collectionId, name, diff --git a/integration-tests/tests/api/collections/document-preflight-scripts.spec.ts b/integration-tests/tests/api/collections/document-preflight-scripts.spec.ts new file mode 100644 index 0000000000..9d9e657128 --- /dev/null +++ b/integration-tests/tests/api/collections/document-preflight-scripts.spec.ts @@ -0,0 +1,41 @@ +import { ProjectType } from 'testkit/gql/graphql'; +import { initSeed } from '../../../testkit/seed'; + +describe('Preflight Script', () => { + describe('CRUD', () => { + const rawJs = 'console.log("Hello World")'; + + it.concurrent('Update a Preflight Script', async () => { + const { updatePreflightScript } = await initSeed() + .createOwner() + .then(r => r.createOrg()) + .then(r => r.createProject(ProjectType.Single)); + + const { error, ok } = await updatePreflightScript({ sourceCode: rawJs }); + expect(error).toEqual(null); + expect(ok?.updatedTarget.preflightScript?.id).toBeDefined(); + expect(ok?.updatedTarget.preflightScript?.sourceCode).toBe(rawJs); + }); + + describe('Permissions Check', () => { + it('Prevent updating a Preflight Script without the write permission to the target', async () => { + const { updatePreflightScript, createTargetAccessToken } = await initSeed() + .createOwner() + .then(r => r.createOrg()) + .then(r => r.createProject(ProjectType.Single)); + + const { secret: readOnlyToken } = await createTargetAccessToken({ mode: 'readOnly' }); + + await expect( + updatePreflightScript({ sourceCode: rawJs, token: readOnlyToken }), + ).rejects.toEqual( + expect.objectContaining({ + message: expect.stringContaining( + `No access (reason: "Missing permission for performing 'laboratory:modifyPreflightScript' on resource")`, + ), + }), + ); + }); + }); + }); +}); diff --git a/package.json b/package.json index 57ab25fa80..f569191ea2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "seed": "tsx scripts/seed-local-env.ts", "start": "pnpm run local:setup", "test": "vitest", - "test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run", + "test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run --browser chrome", "test:e2e:open": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress open", "test:integration": "cd integration-tests && pnpm test:integration", "typecheck": "pnpm run -r --filter '!hive' typecheck", @@ -119,9 +119,11 @@ "slonik@30.4.4": "patches/slonik@30.4.4.patch", "@oclif/core@3.26.6": "patches/@oclif__core@3.26.6.patch", "oclif@4.13.6": "patches/oclif@4.13.6.patch", - "@graphiql/react@1.0.0-alpha.3": "patches/@graphiql__react@1.0.0-alpha.3.patch", + "graphiql": "patches/graphiql.patch", + "@graphiql/react": "patches/@graphiql__react.patch", "countup.js": "patches/countup.js.patch", - "@oclif/core@4.0.6": "patches/@oclif__core@4.0.6.patch" + "@oclif/core@4.0.6": "patches/@oclif__core@4.0.6.patch", + "@fastify/vite": "patches/@fastify__vite.patch" } } } diff --git a/packages/migrations/src/actions/2024.11.20T00.00.00.create-preflight-scripts.ts b/packages/migrations/src/actions/2024.11.20T00.00.00.create-preflight-scripts.ts new file mode 100644 index 0000000000..01ae962cdd --- /dev/null +++ b/packages/migrations/src/actions/2024.11.20T00.00.00.create-preflight-scripts.ts @@ -0,0 +1,23 @@ +import { type MigrationExecutor } from '../pg-migrator'; + +export default { + name: '2024.11.20T00.00.00.create-preflight-scripts.ts', + run: ({ sql }) => sql` +CREATE TABLE "document_preflight_scripts" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "source_code" text NOT NULL, + "target_id" uuid NOT NULL UNIQUE REFERENCES "targets"("id") ON DELETE CASCADE, + "created_by_user_id" uuid REFERENCES "users"("id") ON DELETE SET NULL, + "created_at" timestamptz NOT NULL DEFAULT now(), + "updated_at" timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY ("id") +); + +ALTER TABLE "document_preflight_scripts" +ADD CONSTRAINT "unique_target_id" UNIQUE ("target_id"); + +CREATE INDEX "document_preflight_scripts_target" ON "document_preflight_scripts" ( + "target_id" ASC +); +`, +} satisfies MigrationExecutor; diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index b5399c2f7c..e898e555da 100644 --- a/packages/migrations/src/run-pg-migrations.ts +++ b/packages/migrations/src/run-pg-migrations.ts @@ -147,5 +147,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri await import('./actions/2024.11.12T00-00-00.supertokens-9.1'), await import('./actions/2024.11.12T00-00-00.supertokens-9.2'), await import('./actions/2024.11.12T00-00-00.supertokens-9.3'), + await import('./actions/2024.11.20T00.00.00.create-preflight-scripts'), ], }); diff --git a/packages/services/api/package.json b/packages/services/api/package.json index 742a630d79..93a5767201 100644 --- a/packages/services/api/package.json +++ b/packages/services/api/package.json @@ -25,6 +25,8 @@ "@hive/usage-common": "workspace:*", "@hive/usage-ingestor": "workspace:*", "@hive/webhooks": "workspace:*", + "@nodesecure/i18n": "^4.0.1", + "@nodesecure/js-x-ray": "8.0.0", "@octokit/app": "14.1.0", "@octokit/core": "5.2.0", "@octokit/plugin-retry": "6.0.1", diff --git a/packages/services/api/src/modules/auth/lib/authz.ts b/packages/services/api/src/modules/auth/lib/authz.ts index af58d3b3c8..be49cf0ccb 100644 --- a/packages/services/api/src/modules/auth/lib/authz.ts +++ b/packages/services/api/src/modules/auth/lib/authz.ts @@ -350,6 +350,7 @@ const actionDefinitions = { 'target:modifySettings': defaultTargetIdentity, 'laboratory:describe': defaultTargetIdentity, 'laboratory:modify': defaultTargetIdentity, + 'laboratory:modifyPreflightScript': defaultTargetIdentity, 'appDeployment:describe': defaultTargetIdentity, 'appDeployment:create': defaultAppDeploymentIdentity, 'appDeployment:publish': defaultAppDeploymentIdentity, diff --git a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts index eadbcaaf30..c21ade4248 100644 --- a/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts +++ b/packages/services/api/src/modules/auth/lib/supertokens-strategy.ts @@ -307,7 +307,7 @@ function transformOrganizationMemberLegacyScopes(args: { case TargetAccessScope.SETTINGS: { policies.push({ effect: 'allow', - action: ['target:modifySettings'], + action: ['target:modifySettings', 'laboratory:modifyPreflightScript'], resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`], }); break; diff --git a/packages/services/api/src/modules/lab/index.ts b/packages/services/api/src/modules/lab/index.ts index dfefadd456..22b90f387b 100644 --- a/packages/services/api/src/modules/lab/index.ts +++ b/packages/services/api/src/modules/lab/index.ts @@ -1,4 +1,5 @@ import { createModule } from 'graphql-modules'; +import { PreflightScriptProvider } from './providers/preflight-script.provider'; import { resolvers } from './resolvers.generated'; import typeDefs from './module.graphql'; @@ -7,5 +8,5 @@ export const labModule = createModule({ dirname: __dirname, typeDefs, resolvers, - providers: [], + providers: [PreflightScriptProvider], }); diff --git a/packages/services/api/src/modules/lab/module.graphql.ts b/packages/services/api/src/modules/lab/module.graphql.ts index caed0e5bd3..4463509418 100644 --- a/packages/services/api/src/modules/lab/module.graphql.ts +++ b/packages/services/api/src/modules/lab/module.graphql.ts @@ -8,4 +8,41 @@ export default gql` schema: String! mocks: JSON } + + type PreflightScript { + id: ID! + sourceCode: String! + createdAt: DateTime! + updatedAt: DateTime! + } + + input UpdatePreflightScriptInput { + selector: TargetSelectorInput! + sourceCode: String! + } + + extend type Mutation { + updatePreflightScript(input: UpdatePreflightScriptInput!): PreflightScriptResult! + } + + """ + @oneOf + """ + type PreflightScriptResult { + ok: PreflightScriptOk + error: PreflightScriptError + } + + type PreflightScriptOk { + preflightScript: PreflightScript! + updatedTarget: Target! + } + + type PreflightScriptError implements Error { + message: String! + } + + extend type Target { + preflightScript: PreflightScript + } `; diff --git a/packages/services/api/src/modules/lab/providers/preflight-script.provider.ts b/packages/services/api/src/modules/lab/providers/preflight-script.provider.ts new file mode 100644 index 0000000000..9aaa8f2396 --- /dev/null +++ b/packages/services/api/src/modules/lab/providers/preflight-script.provider.ts @@ -0,0 +1,187 @@ +import { Inject, Injectable, Scope } from 'graphql-modules'; +import { sql, type DatabasePool } from 'slonik'; +import { z } from 'zod'; +import { getLocalLang, getTokenSync } from '@nodesecure/i18n'; +import * as jsxray from '@nodesecure/js-x-ray'; +import type { Target } from '../../../shared/entities'; +import { Session } from '../../auth/lib/authz'; +import { IdTranslator } from '../../shared/providers/id-translator'; +import { Logger } from '../../shared/providers/logger'; +import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; +import { Storage } from '../../shared/providers/storage'; + +const SourceCodeModel = z.string().max(5_000); + +const UpdatePreflightScriptModel = z.strictObject({ + // Use validation only on insertion + sourceCode: SourceCodeModel.superRefine((val, ctx) => { + try { + const { warnings } = scanner.analyse(val); + for (const warning of warnings) { + const message = getTokenSync(jsxray.warnings[warning.kind].i18n); + throw new Error(message); + } + } catch (error) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: error instanceof Error ? error.message : String(error), + }); + } + }), +}); + +const PreflightScriptModel = z.strictObject({ + id: z.string(), + sourceCode: SourceCodeModel, + targetId: z.string(), + createdByUserId: z.union([z.string(), z.null()]), + createdAt: z.string(), + updatedAt: z.string(), +}); + +type PreflightScript = z.TypeOf; + +const scanner = new jsxray.AstAnalyser(); +await getLocalLang(); + +@Injectable({ + global: true, + scope: Scope.Operation, +}) +export class PreflightScriptProvider { + private logger: Logger; + + constructor( + logger: Logger, + private storage: Storage, + private session: Session, + private idTranslator: IdTranslator, + @Inject(PG_POOL_CONFIG) private pool: DatabasePool, + ) { + this.logger = logger.child({ source: 'PreflightScriptProvider' }); + } + + async getPreflightScript(targetId: string) { + const result = await this.pool.maybeOne(sql`/* getPreflightScript */ + SELECT + "id" + , "source_code" AS "sourceCode" + , "target_id" AS "targetId" + , "created_by_user_id" AS "createdByUserId" + , to_json("created_at") AS "createdAt" + , to_json("updated_at") AS "updatedAt" + FROM + "document_preflight_scripts" + WHERE + "target_id" = ${targetId} + `); + + if (!result) { + return null; + } + + return PreflightScriptModel.parse(result); + } + + async updatePreflightScript(args: { + selector: { + organizationSlug: string; + projectSlug: string; + targetSlug: string; + }; + sourceCode: string; + }): Promise< + | { + error: { message: string }; + ok?: never; + } + | { + error?: never; + ok: { + preflightScript: PreflightScript; + updatedTarget: Target; + }; + } + > { + const [organizationId, projectId, targetId] = await Promise.all([ + this.idTranslator.translateOrganizationId(args.selector), + this.idTranslator.translateProjectId(args.selector), + this.idTranslator.translateTargetId(args.selector), + ]); + + await this.session.assertPerformAction({ + action: 'laboratory:modifyPreflightScript', + organizationId, + params: { + organizationId, + projectId, + targetId, + }, + }); + + const validationResult = UpdatePreflightScriptModel.safeParse({ sourceCode: args.sourceCode }); + + if (validationResult.error) { + return { + error: { + message: validationResult.error.errors[0].message, + }, + }; + } + + const currentUser = await this.session.getViewer(); + const result = await this.pool.maybeOne(sql`/* createPreflightScript */ + INSERT INTO "document_preflight_scripts" ( + "source_code" + , "target_id" + , "created_by_user_id") + VALUES ( + ${validationResult.data.sourceCode} + , ${targetId} + , ${currentUser.id} + ) + ON CONFLICT ("target_id") + DO UPDATE + SET + "source_code" = EXCLUDED."source_code" + , "updated_at" = NOW() + RETURNING + "id" + , "source_code" AS "sourceCode" + , "target_id" AS "targetId" + , "created_by_user_id" AS "createdByUserId" + , to_json("created_at") AS "createdAt" + , to_json("updated_at") AS "updatedAt" + `); + + if (!result) { + return { + error: { + message: 'No preflight script found', + }, + }; + } + const { data: preflightScript, error } = PreflightScriptModel.safeParse(result); + + if (error) { + return { + error: { + message: error.errors[0].message, + }, + }; + } + + const updatedTarget = await this.storage.getTarget({ + organizationId, + projectId, + targetId, + }); + + return { + ok: { + preflightScript, + updatedTarget, + }, + }; + } +} diff --git a/packages/services/api/src/modules/lab/resolvers/Mutation/updatePreflightScript.ts b/packages/services/api/src/modules/lab/resolvers/Mutation/updatePreflightScript.ts new file mode 100644 index 0000000000..071c45af38 --- /dev/null +++ b/packages/services/api/src/modules/lab/resolvers/Mutation/updatePreflightScript.ts @@ -0,0 +1,23 @@ +import { MutationResolvers } from '../../../../__generated__/types'; +import { PreflightScriptProvider } from '../../providers/preflight-script.provider'; + +export const updatePreflightScript: NonNullable< + MutationResolvers['updatePreflightScript'] +> = async (_parent, args, { injector }) => { + const result = await injector.get(PreflightScriptProvider).updatePreflightScript({ + selector: args.input.selector, + sourceCode: args.input.sourceCode, + }); + + if (result.error) { + return { + error: result.error, + ok: null, + }; + } + + return { + ok: result.ok, + error: null, + }; +}; diff --git a/packages/services/api/src/modules/lab/resolvers/Target.ts b/packages/services/api/src/modules/lab/resolvers/Target.ts new file mode 100644 index 0000000000..640f6e024e --- /dev/null +++ b/packages/services/api/src/modules/lab/resolvers/Target.ts @@ -0,0 +1,16 @@ +import type { TargetResolvers } from '../../../__generated__/types'; +import { PreflightScriptProvider } from '../providers/preflight-script.provider'; + +/* + * Note: This object type is generated because "TargetMapper" is declared. This is to ensure runtime safety. + * + * When a mapper is used, it is possible to hit runtime errors in some scenarios: + * - given a field name, the schema type's field type does not match mapper's field type + * - or a schema type's field does not exist in the mapper's fields + * + * If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config. + */ +export const Target: Pick = { + preflightScript: (parent, _args, { injector }) => + injector.get(PreflightScriptProvider).getPreflightScript(parent.id), +}; diff --git a/packages/services/api/src/shared/entities.ts b/packages/services/api/src/shared/entities.ts index dbf6e8700a..80ad3c36b3 100644 --- a/packages/services/api/src/shared/entities.ts +++ b/packages/services/api/src/shared/entities.ts @@ -240,6 +240,15 @@ export interface DocumentCollection { updatedAt: string; } +export interface PreflightScript { + id: string; + sourceCode: string; + targetId: string; + createdByUserId: string | null; + createdAt: string; + updatedAt: string; +} + export type PaginatedDocumentCollections = Readonly<{ edges: ReadonlyArray<{ node: DocumentCollection; diff --git a/packages/services/storage/src/db/types.ts b/packages/services/storage/src/db/types.ts index 8fca2bf273..552393d2c9 100644 --- a/packages/services/storage/src/db/types.ts +++ b/packages/services/storage/src/db/types.ts @@ -136,6 +136,15 @@ export interface document_collections { updated_at: Date; } +export interface document_preflight_scripts { + created_at: Date; + created_by_user_id: string | null; + id: string; + source_code: string; + target_id: string; + updated_at: Date; +} + export interface migration { date: Date; hash: string; @@ -417,6 +426,7 @@ export interface DBTables { contracts: contracts; document_collection_documents: document_collection_documents; document_collections: document_collections; + document_preflight_scripts: document_preflight_scripts; migration: migration; oidc_integrations: oidc_integrations; organization_invitations: organization_invitations; diff --git a/packages/web/app/.env.template b/packages/web/app/.env.template index 7e56e568f4..5f6c19c1b8 100644 --- a/packages/web/app/.env.template +++ b/packages/web/app/.env.template @@ -34,3 +34,6 @@ INTEGRATION_GITHUB_APP_NAME="" STRIPE_PUBLIC_KEY="" LOG_LEVEL=debug + +# Laboratory Worker URL +LABORATORY_PREFLIGHT_WORKER_URL= \ No newline at end of file diff --git a/packages/web/app/README.md b/packages/web/app/README.md index 1a9efdaaf6..44eb02c414 100644 --- a/packages/web/app/README.md +++ b/packages/web/app/README.md @@ -28,6 +28,7 @@ The following environment variables configure the application. | `NODE_ENV` | No | The `NODE_ENV` value. | `production` | | `GA_TRACKING_ID` | No | The token for Google Analytics in order to track user actions. | `g6aff8102efda5e1d12e` | | `GRAPHQL_PERSISTED_OPERATIONS` | No | Send persisted operation hashes instead of documents to the server. | `1` (enabled) or `0` (disabled) | +| `LABORATORY_PREFLIGHT_WORKER_URL` | No | Where to serve the laboratory worker from. | Default `""` | ## Hive Hosted Configuration diff --git a/packages/web/app/package.json b/packages/web/app/package.json index ef0a316b31..b4b1555bdc 100644 --- a/packages/web/app/package.json +++ b/packages/web/app/package.json @@ -15,9 +15,9 @@ "@date-fns/utc": "2.1.0", "@fastify/cors": "9.0.1", "@fastify/static": "7.0.4", - "@fastify/vite": "6.0.7", + "@fastify/vite": "6.0.6", "@graphiql/plugin-explorer": "4.0.0-alpha.2", - "@graphiql/react": "1.0.0-alpha.3", + "@graphiql/react": "1.0.0-alpha.4", "@graphiql/toolkit": "0.9.1", "@graphql-codegen/client-preset-swc-plugin": "0.2.0", "@graphql-tools/mock": "9.0.6", @@ -66,6 +66,7 @@ "@theguild/editor": "1.2.5", "@trpc/client": "10.45.2", "@trpc/server": "10.45.2", + "@types/crypto-js": "^4.2.2", "@types/dompurify": "3.0.5", "@types/js-cookie": "3.0.6", "@types/react": "18.3.12", @@ -81,6 +82,7 @@ "class-variance-authority": "0.7.0", "clsx": "2.1.1", "cmdk": "0.2.1", + "crypto-js": "^4.2.0", "date-fns": "4.1.0", "dompurify": "3.2.0", "dotenv": "16.4.5", @@ -89,7 +91,7 @@ "fastify": "4.28.1", "formik": "2.4.6", "framer-motion": "11.11.17", - "graphiql": "4.0.0-alpha.4", + "graphiql": "4.0.0-alpha.5", "graphql": "16.9.0", "graphql-sse": "2.5.3", "immer": "10.1.1", @@ -131,7 +133,8 @@ "vite-tsconfig-paths": "5.1.2", "wonka": "6.3.4", "yup": "1.4.0", - "zod": "3.23.8" + "zod": "3.23.8", + "zustand": "5.0.1" }, "buildOptions": { "external": [ diff --git a/packages/web/app/preflight-worker-embed.html b/packages/web/app/preflight-worker-embed.html new file mode 100644 index 0000000000..2c1cebb85a --- /dev/null +++ b/packages/web/app/preflight-worker-embed.html @@ -0,0 +1,28 @@ + + + + + + + + + + Hive Preflight Worker + + + + +
+ + + diff --git a/packages/web/app/src/env/backend.ts b/packages/web/app/src/env/backend.ts index 42367fca60..16c7641b94 100644 --- a/packages/web/app/src/env/backend.ts +++ b/packages/web/app/src/env/backend.ts @@ -64,6 +64,7 @@ const BaseSchema = zod.object({ GRAPHQL_PERSISTED_OPERATIONS: emptyString( zod.union([zod.literal('1'), zod.literal('0')]).optional(), ), + LABORATORY_PREFLIGHT_WORKER_URL: emptyString(zod.string().url().optional()), ZENDESK_SUPPORT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), }); diff --git a/packages/web/app/src/env/frontend-public-variables.ts b/packages/web/app/src/env/frontend-public-variables.ts index 12215830ba..f2a3c7b8da 100644 --- a/packages/web/app/src/env/frontend-public-variables.ts +++ b/packages/web/app/src/env/frontend-public-variables.ts @@ -24,6 +24,7 @@ export const ALLOWED_ENVIRONMENT_VARIABLES = [ 'SENTRY', 'SENTRY_DSN', 'MEMBER_ROLES_DEADLINE', + 'LABORATORY_PREFLIGHT_WORKER_URL', ] as const; export type AllowedEnvironmentVariables = (typeof ALLOWED_ENVIRONMENT_VARIABLES)[number]; diff --git a/packages/web/app/src/env/frontend.ts b/packages/web/app/src/env/frontend.ts index 941ff48b8a..5419e7d710 100644 --- a/packages/web/app/src/env/frontend.ts +++ b/packages/web/app/src/env/frontend.ts @@ -54,6 +54,7 @@ const BaseSchema = protectedObject({ zod.union([zod.literal('1'), zod.literal('0')]).optional(), ), ZENDESK_SUPPORT: enabledOrDisabled, + LABORATORY_PREFLIGHT_WORKER_URL: emptyString(zod.string().url().optional()), }); const IntegrationSlackSchema = protectedObject({ @@ -173,6 +174,9 @@ function buildConfig() { migrations: { member_roles_deadline: migrations.MEMBER_ROLES_DEADLINE ?? null, }, + laboratory: { + preflightWorkerUrl: base.LABORATORY_PREFLIGHT_WORKER_URL ?? null, + }, } as const; } diff --git a/packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx b/packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx index 6d8d77a609..17ddbc6bb1 100644 --- a/packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx +++ b/packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx @@ -104,6 +104,7 @@ export const TargetLaboratoryPageQuery = graphql(` } viewerCanViewLaboratory viewerCanModifyLaboratory + ...PreflightScript_TargetFragment } ...Laboratory_IsCDNEnabledFragment } @@ -123,11 +124,9 @@ export const operationCollectionsPlugin: GraphiQLPlugin = { }; export function Content() { - const { organizationSlug, projectSlug, targetSlug } = useParams({ strict: false }) as { - organizationSlug: string; - projectSlug: string; - targetSlug: string; - }; + const { organizationSlug, projectSlug, targetSlug } = useParams({ + from: '/authenticated/$organizationSlug/$projectSlug/$targetSlug', + }); const [query] = useQuery({ query: TargetLaboratoryPageQuery, variables: { diff --git a/packages/web/app/src/lib/preflight-sandbox/allowed-globals.ts b/packages/web/app/src/lib/preflight-sandbox/allowed-globals.ts new file mode 100644 index 0000000000..2962653ab4 --- /dev/null +++ b/packages/web/app/src/lib/preflight-sandbox/allowed-globals.ts @@ -0,0 +1,55 @@ +/** + * List all variables that we want to allow users to use inside their scripts + * + * initial list comes from https://github.com/postmanlabs/uniscope/blob/develop/lib/allowed-globals.js + */ +export const ALLOWED_GLOBALS = new Set([ + 'Array', + 'Atomics', + 'BigInt', + 'Boolean', + 'DataView', + 'Date', + 'Error', + 'EvalError', + 'Infinity', + 'JSON', + 'Map', + 'Math', + 'NaN', + 'Number', + 'Object', + 'Promise', + 'Proxy', + 'RangeError', + 'ReferenceError', + 'Reflect', + 'RegExp', + 'Set', + 'String', + 'Symbol', + 'SyntaxError', + 'TypeError', + 'URIError', + 'WeakMap', + 'WeakSet', + 'decodeURI', + 'decodeURIComponent', + 'encodeURI', + 'encodeURIComponent', + 'escape', + 'isFinite', + 'isNaN', + 'parseFloat', + 'parseInt', + 'undefined', + 'unescape', + // More global variables + 'btoa', + 'atob', + 'fetch', + 'setTimeout', + // We aren't allowing access to window.console, but we need to "allow" it + // here so a second argument isn't added for it below. + 'console', +]); diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx new file mode 100644 index 0000000000..04cd3222f2 --- /dev/null +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -0,0 +1,667 @@ +import { + ComponentPropsWithoutRef, + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { clsx } from 'clsx'; +import type { editor } from 'monaco-editor'; +import { useMutation } from 'urql'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Subtitle, Title } from '@/components/ui/page'; +import { Switch } from '@/components/ui/switch'; +import { useToast } from '@/components/ui/use-toast'; +import { env } from '@/env/frontend'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { useLocalStorage, useToggle } from '@/lib/hooks'; +import { GraphiQLPlugin } from '@graphiql/react'; +import { Editor as MonacoEditor, OnMount } from '@monaco-editor/react'; +import { + Cross2Icon, + CrossCircledIcon, + ExclamationTriangleIcon, + InfoCircledIcon, + Pencil1Icon, + TriangleRightIcon, +} from '@radix-ui/react-icons'; +import { useParams } from '@tanstack/react-router'; +import type { LogMessage } from './preflight-script-worker'; +import workerUrl from './preflight-script-worker?worker&url'; + +const PreflightWorker = Worker.bind(null, env.laboratory.preflightWorkerUrl || workerUrl, { + type: import.meta.env.DEV ? 'module' : undefined, +}); + +export const preflightScriptPlugin: GraphiQLPlugin = { + icon: () => ( + + + + + + ), + title: 'Preflight Script', + content: PreflightScriptContent, +}; + +const classes = { + monaco: clsx('*:bg-[#10151f]'), + monacoMini: clsx('h-32 *:rounded-md *:bg-[#10151f]'), + icon: clsx('absolute -left-5 top-px'), +}; + +const sharedMonacoProps = { + theme: 'vs-dark', + className: classes.monaco, + options: { + minimap: { enabled: false }, + padding: { + top: 10, + }, + scrollbar: { + horizontalScrollbarSize: 6, + verticalScrollbarSize: 6, + }, + }, +} satisfies ComponentPropsWithoutRef; + +const monacoProps = { + env: { + ...sharedMonacoProps, + defaultLanguage: 'json', + options: { + ...sharedMonacoProps.options, + lineNumbers: 'off', + tabSize: 2, + }, + }, + script: { + ...sharedMonacoProps, + theme: 'vs-dark', + defaultLanguage: 'javascript', + options: { + ...sharedMonacoProps.options, + }, + }, +} satisfies Record<'script' | 'env', ComponentPropsWithoutRef>; + +type PayloadLog = { type: 'log'; message: string }; +type PayloadError = { type: 'error'; error: Error }; +type PayloadResult = { type: 'result'; environmentVariables: Record }; +type PayloadReady = { type: 'ready' }; + +type WorkerMessagePayload = PayloadResult | PayloadLog | PayloadError | PayloadReady; + +const PREFLIGHT_TIMEOUT = 30_000; + +const UpdatePreflightScriptMutation = graphql(` + mutation UpdatePreflightScript($input: UpdatePreflightScriptInput!) { + updatePreflightScript(input: $input) { + ok { + updatedTarget { + id + preflightScript { + id + sourceCode + } + } + } + error { + message + } + } + } +`); + +const PreflightScript_TargetFragment = graphql(` + fragment PreflightScript_TargetFragment on Target { + id + preflightScript { + id + sourceCode + } + } +`); + +type LogRecord = LogMessage | { type: 'separator' }; + +function safeParseJSON(str: string): Record | null { + try { + return JSON.parse(str); + } catch { + return null; + } +} + +const enum PreflightWorkerState { + running, + ready, +} + +export function usePreflightScript(args: { + target: FragmentType | null; +}) { + const target = useFragment(PreflightScript_TargetFragment, args.target); + const [isPreflightScriptEnabled, setIsPreflightScriptEnabled] = useLocalStorage( + 'hive:laboratory:isPreflightScriptEnabled', + false, + ); + const [environmentVariables, setEnvironmentVariables] = useLocalStorage( + 'hive:laboratory:environment', + '', + ); + const latestEnvironmentVariablesRef = useRef(environmentVariables); + useEffect(() => { + latestEnvironmentVariablesRef.current = environmentVariables; + }); + + const [state, setState] = useState(PreflightWorkerState.ready); + const [logs, setLogs] = useState([]); + + const currentRun = useRef(null); + + async function execute(script = target?.preflightScript?.sourceCode ?? '', isPreview = false) { + if (isPreview === false && !isPreflightScriptEnabled) { + return safeParseJSON(latestEnvironmentVariablesRef.current); + } + try { + setState(PreflightWorkerState.running); + + const now = Date.now(); + setLogs(prev => [...prev, '> Start running script']); + + const worker = new PreflightWorker(); + + const isReadyD = Promise.withResolvers(); + const isFinishedD = Promise.withResolvers(); + + const timeout = setTimeout(() => { + setLogs(logs => [ + ...logs, + new Error( + `Preflight script execution timed out after ${PREFLIGHT_TIMEOUT / 1000} seconds`, + ), + ]); + isFinishedD.resolve(); + }, PREFLIGHT_TIMEOUT); + + currentRun.current = () => { + clearTimeout(timeout); + setLogs(logs => [ + ...logs, + '> Preflight script interrupted by user', + { + type: 'separator' as const, + }, + ]); + isFinishedD.resolve(); + }; + + worker.onmessage = (ev: MessageEvent) => { + if (ev.data.type === 'ready') { + isReadyD.resolve(); + return; + } + + if (ev.data.type === 'result') { + const mergedEnvironmentVariables = { + ...safeParseJSON(latestEnvironmentVariablesRef.current), + ...ev.data.environmentVariables, + }; + setEnvironmentVariables(JSON.stringify(mergedEnvironmentVariables, null, 2)); + setLogs(logs => [ + ...logs, + `> End running script. Done in ${(Date.now() - now) / 1000}s`, + { + type: 'separator' as const, + }, + ]); + isFinishedD.resolve(); + clearTimeout(timeout); + return; + } + if (ev.data.type === 'log') { + const message = ev.data.message; + setLogs(logs => [...logs, message]); + return; + } + if (ev.data.type === 'error') { + const error = ev.data.error; + setLogs(logs => [...logs, error]); + isFinishedD.resolve(); + clearTimeout(timeout); + return; + } + + throw new Error('Received unexpected response from worker.'); + }; + + await isReadyD.promise; + + worker.postMessage({ + script, + environmentVariables: (environmentVariables && safeParseJSON(environmentVariables)) || {}, + }); + + await isFinishedD.promise; + setState(PreflightWorkerState.ready); + + worker.onmessage = () => {}; + worker.terminate(); + } catch (err) { + if (err instanceof Error) { + setLogs(prev => [ + ...prev, + err, + '> Preflight script failed', + { + type: 'separator' as const, + }, + ]); + setState(PreflightWorkerState.ready); + return safeParseJSON(latestEnvironmentVariablesRef.current); + } + throw err; + } + + return safeParseJSON(latestEnvironmentVariablesRef.current); + } + + function abort() { + currentRun.current?.(); + } + + // terminate worker when leaving laboratory + useEffect( + () => () => { + currentRun.current?.(); + }, + [], + ); + + return { + execute, + abort, + isPreflightScriptEnabled, + setIsPreflightScriptEnabled, + script: target?.preflightScript?.sourceCode ?? '', + environmentVariables, + setEnvironmentVariables, + state, + logs, + clearLogs: () => setLogs([]), + } as const; +} + +type PreflightScriptObject = ReturnType; + +const PreflightScriptContext = createContext(null); +export const PreflightScriptProvider = PreflightScriptContext.Provider; + +function PreflightScriptContent() { + const preflightScript = useContext(PreflightScriptContext); + if (preflightScript === null) { + throw new Error('PreflightScriptContent used outside PreflightScriptContext.Provider'); + } + + const [showModal, toggleShowModal] = useToggle(); + const params = useParams({ + from: '/authenticated/$organizationSlug/$projectSlug/$targetSlug', + }); + + const [, mutate] = useMutation(UpdatePreflightScriptMutation); + + const { toast } = useToast(); + + const handleScriptChange = useCallback(async (newValue = '') => { + const { data, error } = await mutate({ + input: { + selector: params, + sourceCode: newValue, + }, + }); + const err = error || data?.updatePreflightScript?.error; + + if (err) { + toast({ + title: 'Error', + description: err.message, + variant: 'destructive', + }); + return; + } + + toast({ + title: 'Update', + description: 'Preflight script has been updated successfully', + variant: 'default', + }); + }, []); + + return ( + <> + + preflightScript.execute(value, true).catch(err => { + console.error(err); + }) + } + state={preflightScript.state} + abortScriptRun={preflightScript.abort} + logs={preflightScript.logs} + clearLogs={preflightScript.clearLogs} + onScriptValueChange={handleScriptChange} + envValue={preflightScript.environmentVariables} + onEnvValueChange={preflightScript.setEnvironmentVariables} + /> +
+ Preflight Script + +
+ + This script is run before each operation submitted, e.g. for automated authentication. + + +
+ preflightScript.setIsPreflightScriptEnabled(v)} + className="my-4" + data-cy="toggle-preflight-script" + /> + {preflightScript.isPreflightScriptEnabled ? 'ON' : 'OFF'} +
+ + {preflightScript.isPreflightScriptEnabled && ( + + )} + + + Environment variables{' '} + <Badge className="text-xs" variant="outline"> + JSON + </Badge> + + Define variables to use in your Headers + preflightScript.setEnvironmentVariables(value ?? '')} + {...monacoProps.env} + className={classes.monacoMini} + wrapperProps={{ + ['data-cy']: 'env-editor-mini', + }} + /> + + ); +} + +function PreflightScriptModal({ + isOpen, + toggle, + scriptValue, + executeScript, + state, + abortScriptRun, + logs, + clearLogs, + onScriptValueChange, + envValue, + onEnvValueChange, +}: { + isOpen: boolean; + toggle: () => void; + scriptValue?: string; + executeScript: (script: string) => void; + state: PreflightWorkerState; + abortScriptRun: () => void; + logs: Array; + clearLogs: () => void; + onScriptValueChange: (value: string) => void; + envValue: string; + onEnvValueChange: (value: string) => void; +}) { + const scriptEditorRef = useRef(null); + const envEditorRef = useRef(null); + const consoleRef = useRef(null); + + const handleScriptEditorDidMount: OnMount = useCallback(editor => { + scriptEditorRef.current = editor; + }, []); + + const handleEnvEditorDidMount: OnMount = useCallback(editor => { + envEditorRef.current = editor; + }, []); + const handleSubmit = useCallback(() => { + onScriptValueChange(scriptEditorRef.current?.getValue() ?? ''); + onEnvValueChange(envEditorRef.current?.getValue() ?? ''); + toggle(); + }, []); + + useEffect(() => { + const consoleEl = consoleRef.current; + consoleEl?.scroll({ top: consoleEl.scrollHeight, behavior: 'smooth' }); + }, [logs]); + + return ( + { + if (!open) { + abortScriptRun(); + } + toggle(); + }} + > + { + // prevent pressing escape in monaco to close the modal + if (ev.target instanceof HTMLTextAreaElement) { + ev.preventDefault(); + } + }} + > + + Edit your Preflight Script + + This script will run in each user's browser and be stored in plain text on our servers. + Don't share any secrets here 🤫. +
+ All team members can view the script and toggle it off when they need to. +
+
+
+
+
+ + Script Editor + <Badge className="text-xs" variant="outline"> + JavaScript + </Badge> + + +
+ +
+
+
+ Console Output + +
+
+ {logs.map((log, index) => { + let type = ''; + if (log instanceof Error) { + type = 'error'; + log = `${log.name}: ${log.message}`; + } + if (typeof log === 'string') { + type ||= log.split(':')[0].toLowerCase(); + + const ComponentToUse = { + error: CrossCircledIcon, + warn: ExclamationTriangleIcon, + info: InfoCircledIcon, + }[type]; + + return ( +
+ {ComponentToUse && } + {log} +
+ ); + } + return
; + })} +
+ + Environment Variables + <Badge className="text-xs" variant="outline"> + JSON + </Badge> + + onEnvValueChange(value ?? '')} + onMount={handleEnvEditorDidMount} + {...monacoProps.env} + options={{ + ...monacoProps.env.options, + wordWrap: 'wordWrapColumn', + }} + wrapperProps={{ + ['data-cy']: 'env-editor', + }} + /> +
+
+ +

+ + Changes made to this Preflight Script will apply to all users on your team using this + target. +

+ + +
+
+
+ ); +} diff --git a/packages/web/app/src/lib/preflight-sandbox/json.ts b/packages/web/app/src/lib/preflight-sandbox/json.ts new file mode 100644 index 0000000000..0e472f8e02 --- /dev/null +++ b/packages/web/app/src/lib/preflight-sandbox/json.ts @@ -0,0 +1,29 @@ +export type JSONPrimitive = boolean | null | string | number; +export type JSONObject = { [key: string]: JSONValue }; +export type JSONValue = JSONPrimitive | JSONValue[] | JSONObject; + +function isJSONValue(value: unknown): value is JSONValue { + return ( + (Array.isArray(value) && value.every(isJSONValue)) || + isJSONObject(value) || + isJSONPrimitive(value) + ); +} + +export function isJSONObject(value: unknown): value is JSONObject { + return ( + typeof value === 'object' && + !!value && + !Array.isArray(value) && + Object.values(value).every(isJSONValue) + ); +} + +export function isJSONPrimitive(value: unknown): value is JSONPrimitive { + return ( + typeof value === 'boolean' || + typeof value === 'number' || + typeof value === 'string' || + value === null + ); +} diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts new file mode 100644 index 0000000000..d68fac3fbe --- /dev/null +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts @@ -0,0 +1,144 @@ +import CryptoJS from 'crypto-js'; +import CryptoJSPackageJson from 'crypto-js/package.json'; +import { ALLOWED_GLOBALS } from './allowed-globals'; +import { isJSONPrimitive, JSONPrimitive } from './json'; + +export type LogMessage = string | Error; + +self.onmessage = async event => { + await execute(event.data); +}; + +self.addEventListener('unhandledrejection', event => { + const error = 'reason' in event ? new Error(event.reason) : event; + postMessage({ type: 'error', error }); +}); + +async function execute(args: { + environmentVariables: Record; + script: string; +}): Promise { + const { environmentVariables, script } = args; + const inWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope; + // Confirm the build pipeline worked and this is running inside a worker and not the main thread + if (!inWorker) { + throw new Error( + 'Preflight script must always be run in web workers, this is a problem with laboratory not user input', + ); + } + + // When running in worker `environmentVariables` will not be a reference to the main thread value + // but sometimes this will be tested outside the worker, so we don't want to mutate the input in that case + const workingEnvironmentVariables = { ...environmentVariables }; + + // generate list of all in scope variables, we do getOwnPropertyNames and `for in` because each contain slightly different sets of keys + const allGlobalKeys = Object.getOwnPropertyNames(globalThis); + for (const key in globalThis) { + allGlobalKeys.push(key); + } + + // filter out allowed global variables and keys that will cause problems + const blockedGlobals = allGlobalKeys.filter( + key => + // When testing in the main thread this exists on window and is not a valid argument name. + // because global is blocked, even if this was in the worker it's still wouldn't be available because it's not a valid variable name + !key.includes('-') && + !ALLOWED_GLOBALS.has(key) && + // window has references as indexes on the globalThis such as `globalThis[0]`, numbers are not valid arguments, so we need to filter these out + Number.isNaN(Number(key)) && + // @ is not a valid argument name beginning character, so we don't need to block it and including it will cause a syntax error + // only example currently is @wry/context which is a dep of @apollo/client and adds @wry/context:Slot + key.charAt(0) !== '@', + ); + // restrict window variable + blockedGlobals.push('window'); + + const log = + (level: 'log' | 'warn' | 'error' | 'info') => + (...args: unknown[]) => { + console[level](...args); + let message = `${level.charAt(0).toUpperCase()}${level.slice(1)}: ${args.map(String).join(' ')}`; + message += appendLineAndColumn(new Error(), { + columnOffset: 'console.'.length, + }); + // The messages should be streamed to the main thread as they occur not gathered and send to + // the main thread at the end of the execution of the preflight script + postMessage({ type: 'log', message }); + }; + + function getValidEnvVariable(value: unknown) { + if (isJSONPrimitive(value)) { + return value; + } + consoleApi.warn( + 'You tried to set a non primitive type in env variables, only string, boolean, number and null are allowed in env variables. The value has been filtered out.', + ); + } + + const consoleApi = Object.freeze({ + log: log('log'), + info: log('info'), + warn: log('warn'), + error: log('error'), + }); + + let hasLoggedCryptoJSVersion = false; + + const labApi = Object.freeze({ + get CryptoJS() { + if (!hasLoggedCryptoJSVersion) { + hasLoggedCryptoJSVersion = true; + consoleApi.info(`Using crypto-js version: ${CryptoJSPackageJson.version}`); + } + return CryptoJS; + }, + environment: { + get(key: string) { + return Object.freeze(workingEnvironmentVariables[key]); + }, + set(key: string, value: unknown) { + const validValue = getValidEnvVariable(value); + if (validValue === undefined) { + delete workingEnvironmentVariables[key]; + } else { + workingEnvironmentVariables[key] = validValue; + } + }, + }, + }); + + // Wrap the users script in an async IIFE to allow the use of top level await + const rawJs = `return(async()=>{'use strict'; +${script}})()`; + + try { + await Function( + 'lab', + 'console', + // spreading all the variables we want to block creates an argument that shadows their names, any attempt to access them will result in `undefined` + ...blockedGlobals, + rawJs, + // Bind the function to a null constructor object to prevent `this` leaking scope in + ).bind( + // When `this` is `undefined` or `null`, we get [object DedicatedWorkerGlobalScope] in console output + // instead we set as string `'undefined'` so in console, we'll see undefined as well + 'undefined', + )(labApi, consoleApi); + } catch (error) { + if (error instanceof Error) { + error.message += appendLineAndColumn(error); + } + postMessage({ type: 'error', error }); + return; + } + postMessage({ type: 'result', environmentVariables: workingEnvironmentVariables }); +} + +function appendLineAndColumn(error: Error, { columnOffset = 0 } = {}): string { + const regex = /:(?\d+):(?\d+)/; // Regex to match the line and column numbers + + const { line, column } = error.stack?.match(regex)?.groups || {}; + return ` (Line: ${Number(line) - 3}, Column: ${Number(column) - columnOffset})`; +} + +postMessage({ type: 'ready' }); diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index bd109c3aba..3bcedccac8 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -33,6 +33,11 @@ import { import { useSyncOperationState } from '@/lib/hooks/laboratory/use-sync-operation-state'; import { useOperationFromQueryString } from '@/lib/hooks/laboratory/useOperationFromQueryString'; import { useResetState } from '@/lib/hooks/use-reset-state'; +import { + preflightScriptPlugin, + PreflightScriptProvider, + usePreflightScript, +} from '@/lib/preflight-sandbox/graphiql-plugin'; import { cn } from '@/lib/utils'; import { explorerPlugin } from '@graphiql/plugin-explorer'; import { @@ -52,7 +57,7 @@ import { useRedirect } from '@/lib/access/common'; const explorer = explorerPlugin(); // Declare outside components, otherwise while clicking on field in explorer operationCollectionsPlugin will be open -const plugins = [explorer, operationCollectionsPlugin]; +const plugins = [explorer, operationCollectionsPlugin, preflightScriptPlugin]; function Share(): ReactElement | null { const label = 'Share query'; @@ -243,11 +248,30 @@ function Save(props: { ); } +function substituteVariablesInHeader( + headers: Record, + environmentVariables: Record, +) { + return Object.fromEntries( + Object.entries(headers).map(([key, value]) => { + if (typeof value === 'string') { + // Replace all occurrences of `{{keyName}}` strings only if key exists in `environmentVariables` + value = value.replaceAll(/{{(?.*?)}}/g, (originalString, envKey) => { + return Object.hasOwn(environmentVariables, envKey) + ? (environmentVariables[envKey] as string) + : originalString; + }); + } + return [key, value]; + }), + ); +} + function LaboratoryPageContent(props: { organizationSlug: string; projectSlug: string; targetSlug: string; - selectedOperationId: string | undefined; + selectedOperationId?: string; }) { const [query] = useQuery({ query: TargetLaboratoryPageQuery, @@ -280,21 +304,58 @@ function LaboratoryPageContent(props: { ); const mockEndpoint = `${location.origin}/api/lab/${props.organizationSlug}/${props.projectSlug}/${props.targetSlug}`; + const target = query.data?.target; + + const preflightScript = usePreflightScript({ target: target ?? null }); const fetcher = useMemo(() => { return async (params, opts) => { + let headers = opts?.headers; const url = - (actualSelectedApiEndpoint === 'linkedApi' - ? query.data?.target?.graphqlEndpointUrl - : undefined) ?? mockEndpoint; - - const _fetcher = createGraphiQLFetcher({ url, fetch }); + (actualSelectedApiEndpoint === 'linkedApi' ? target?.graphqlEndpointUrl : undefined) ?? + mockEndpoint; + + return new Repeater(async (push, stop) => { + let hasFinishedPreflightScript = false; + // eslint-disable-next-line @typescript-eslint/no-floating-promises + stop.then(() => { + if (!hasFinishedPreflightScript) { + preflightScript.abort(); + } + }); + try { + const result = await preflightScript.execute(); + if (result && headers) { + headers = substituteVariablesInHeader(headers, result); + } + } catch (err: unknown) { + if (err instanceof Error === false) { + throw err; + } + const formatError = JSON.stringify( + { + name: err.name, + message: err.message, + }, + null, + 2, + ); + const error = new Error(`Error during preflight script execution:\n\n${formatError}`); + // We only want to expose the error message, not the whole stack trace. + delete error.stack; + stop(error); + return; + } finally { + hasFinishedPreflightScript = true; + } - const result = await _fetcher(params, opts); + const graphiqlFetcher = createGraphiQLFetcher({ url, fetch }); + const result = await graphiqlFetcher(params, { + ...opts, + headers, + }); - // We only want to expose the error message, not the whole stack trace. - if (isAsyncIterable(result)) { - return new Repeater(async (push, stop) => { + if (isAsyncIterable(result)) { // eslint-disable-next-line @typescript-eslint/no-floating-promises stop.then( () => 'return' in result && result.return instanceof Function && result.return(), @@ -306,15 +367,22 @@ function LaboratoryPageContent(props: { stop(); } catch (err) { const error = new Error(err instanceof Error ? err.message : 'Unexpected error.'); + // We only want to expose the error message, not the whole stack trace. delete error.stack; stop(error); + return; } - }); - } + } - return result; + return result; + }); }; - }, [query.data?.target?.graphqlEndpointUrl, actualSelectedApiEndpoint]); + }, [ + target?.graphqlEndpointUrl, + actualSelectedApiEndpoint, + preflightScript.execute, + preflightScript.isPreflightScriptEnabled, + ]); const FullScreenIcon = isFullScreen ? ExitFullScreenIcon : EnterFullScreenIcon; @@ -335,8 +403,6 @@ function LaboratoryPageContent(props: { [userOperations], ); - const target = query.data?.target; - useRedirect({ canAccess: target?.viewerCanViewLaboratory === true, redirectTo: router => { @@ -448,7 +514,9 @@ function LaboratoryPageContent(props: { .graphiql-dialog a { --color-primary: 40, 89%, 60% !important; } - + .graphiql-container { + overflow: unset; /* remove default overflow */ + } .graphiql-container, .graphiql-dialog, .CodeMirror-info { @@ -466,47 +534,48 @@ function LaboratoryPageContent(props: { {!query.fetching && !query.stale && ( - - - - - - {({ prettify }) => ( - <> - {query.data?.target?.viewerCanModifyLaboratory && ( - - )} - - {/* if people have no modify access they should still be able to format their own queries. */} - {(query.data?.target?.viewerCanModifyLaboratory === true || - !props.selectedOperationId) && - prettify} - - )} - - + + + + + + + {({ prettify }) => ( + <> + {query.data?.target?.viewerCanModifyLaboratory && ( + + )} + + {/* if people have no modify access they should still be able to format their own queries. */} + {(query.data?.target?.viewerCanModifyLaboratory === true || + !props.selectedOperationId) && + prettify} + + )} + + + )} { + if (isDev) { + // If in development mode, return the Vite preflight-worker-embed.html. + return reply.html(); + } + + // If in production mode, return the static html file. + return reply.sendFile(preflightWorkerEmbed.htmlFile, { + cacheControl: false, + }); + }); + server.get('*', (_req, reply) => { if (isDev) { // If in development mode, return the Vite index.html. diff --git a/packages/web/app/tsconfig.json b/packages/web/app/tsconfig.json index 925606c456..2dd129c3fe 100644 --- a/packages/web/app/tsconfig.json +++ b/packages/web/app/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../../tsconfig.json", "compilerOptions": { "target": "esnext", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": ["dom", "dom.iterable", "esnext", "webworker"], "useDefineForClassFields": true, "allowJs": true, "strict": true, diff --git a/packages/web/app/vite.config.ts b/packages/web/app/vite.config.ts index eee97bf33a..5d21eb6255 100644 --- a/packages/web/app/vite.config.ts +++ b/packages/web/app/vite.config.ts @@ -1,4 +1,5 @@ import type { UserConfig } from 'vite'; +import { resolve } from 'node:path'; import tsconfigPaths from 'vite-tsconfig-paths'; import react from '@vitejs/plugin-react'; @@ -7,4 +8,19 @@ const __dirname = new URL('.', import.meta.url).pathname; export default { root: __dirname, plugins: [tsconfigPaths(), react()], + worker: { + rollupOptions: { + output: { + file: 'preflight-script-worker.js', + }, + }, + }, + build: { + rollupOptions: { + input: { + index: resolve(__dirname, 'index.html'), + ['preflight-worker-embed']: resolve(__dirname, 'preflight-worker-embed.html'), + }, + }, + }, } satisfies UserConfig; diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory/command-line-icon.png b/packages/web/docs/src/pages/docs/dashboard/laboratory/command-line-icon.png new file mode 100644 index 0000000000..b18e07fba5 Binary files /dev/null and b/packages/web/docs/src/pages/docs/dashboard/laboratory/command-line-icon.png differ diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory/crypto-js.png b/packages/web/docs/src/pages/docs/dashboard/laboratory/crypto-js.png new file mode 100644 index 0000000000..85ceaf6dea Binary files /dev/null and b/packages/web/docs/src/pages/docs/dashboard/laboratory/crypto-js.png differ diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory/get-and-set-env-vars.png b/packages/web/docs/src/pages/docs/dashboard/laboratory/get-and-set-env-vars.png new file mode 100644 index 0000000000..78d41be964 Binary files /dev/null and b/packages/web/docs/src/pages/docs/dashboard/laboratory/get-and-set-env-vars.png differ diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory.mdx b/packages/web/docs/src/pages/docs/dashboard/laboratory/index.mdx similarity index 86% rename from packages/web/docs/src/pages/docs/dashboard/laboratory.mdx rename to packages/web/docs/src/pages/docs/dashboard/laboratory/index.mdx index ee00cdbd6e..e7ce09af53 100644 --- a/packages/web/docs/src/pages/docs/dashboard/laboratory.mdx +++ b/packages/web/docs/src/pages/docs/dashboard/laboratory/index.mdx @@ -1,20 +1,22 @@ -import NextImage from 'next/image' +--- +title: Overview +--- + import { Callout } from '@theguild/components' -import labFormImage from '../../../../public/docs/pages/features/lab-form.png' -import labImage from '../../../../public/docs/pages/features/lab.png' +import { Screenshot } from '../../../../components/screenshot' # Laboratory Under your target page, you'll find the **Laboratory** page. The Laboratory allows you to explore your GraphQL schema and run queries against a mocked version of your GraphQL service. -## Explore your GraphQL schema +## Explore your GraphQL Schema You can use the full power of [GraphiQL](https://github.com/graphql/graphiql) directly within Hive: compose your GraphQL operations, explore with different field and variations, and access your GraphQL schema full documentation. - +![Laboratory](/docs/pages/features/lab.png) ## Link a Laboratory Endpoint @@ -49,7 +51,7 @@ To get started with using the Laboratory mock schema externally, create a Now, click on the **Use Schema Externally** button on the Laboratory page, and follow the instructions on the form: - +![Laboratory Form](/docs/pages/features/lab-form.png) To test access to your setup, try running a `curl` command to run a simple GraphQL query against your mocked schema: @@ -60,7 +62,7 @@ curl -X POST -H "X-Hive-Key: HIVE_TOKEN_HERE" -H "Content-Type: application/json --data-raw '{"query": "{ __typename }"}' ``` -### With GraphQL-Code-Generator +### With GraphQL Code Generator We recommend using the CDN for consuming the GraphQL schema in your project. [See GraphQL Code diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-script-plugin.png b/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-script-plugin.png new file mode 100644 index 0000000000..7c09af9499 Binary files /dev/null and b/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-script-plugin.png differ diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-scripts.mdx b/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-scripts.mdx new file mode 100644 index 0000000000..8587b88d12 --- /dev/null +++ b/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-scripts.mdx @@ -0,0 +1,99 @@ +--- +description: + Useful for handling authentication flows like OAuth, where you may need to refresh an access token +--- + +# Preflight Scripts + +import { Callout } from '@theguild/components' +import { Screenshot } from '../../../../components/screenshot' + +export const figcaptionClass = 'text-center text-sm mt-2' + +These scripts allow you to automatically run custom authentication processes before executing your +GraphQL operations. They're especially useful for handling authentication flows like OAuth, where +you may need to refresh an access token. Let's explore how it works. + +## Configuring Preflight Script + +To create a script click on the command line icon (right after Operation Collections plugin icon) in +GraphiQL sidebar section. + +
+ ![Command line icon](./command-line-icon.png) + {/* prettier-ignore */} +
The preflight script is accessible by clicking on the Command line icon in the GraphiQL sidebar
+
+ +You will see Script editor (JavaScript language) which is read-only and present for a quick view of +your saved script and Environment variables editor (JSON language) which is persistent in +localStorage. + +
+ ![](./preflight-script-plugin.png) +
Preflight script plugin view
+
+ +## Editing Preflight Script + +Clicking on the `Edit` button will open Modal where you can edit, test and save your script in +database. + + + **Note**: Your script will stored as plain text in our database, don't put any secrets there, use + Environment variables editor for it! The preflight script is accessible to all members of your + organization, but only users with access to target Settings can edit the script code. + + +You can use any JavaScript syntax (including top-level `await`) in the Script editor. Getting and +Setting environment variables is done by accessing the `environment` property on the `lab` global +variable. + +```js +// get myKey variable from the Environment variables editor +lab.environment.get('myKey') +// set myKey variable to the Environment variables editor (persistent in localStorage) +lab.environment.set('myKey', myValue) +``` + +
+ ![](./get-and-set-env-vars.png) +
Demo how to get and set environment variables
+
+ +## CryptoJS + +Additionally, you can access [the CryptoJS library](https://github.com/brix/crypto-js) by accessing +the `CryptoJS` property on the `lab` global variable. + +
+ ![](./crypto-js.png) +
CryptoJS
+
+ +## Global Variables and Errors + +Access to global variables such as `this`, `window` or `globalThis` is restricted. Errors thrown by +the script will be displayed in Console Output. + +
+ ![](./unable-to-access-global-variables.png) +
Demo restricted access to global variables
+
+ +## Using Environment Variables + +To use your environment variables in GraphiQL headers editor wraps environment keys with +double-curly braces, e.g.: + +```json filename="Headers" /{{myEnvVar}}/ +{ + "Authorization": "Bearer {{myEnvVar}}" +} +``` + +
+ ![Replace syntax in action](./replace-syntax.png) + {/* prettier-ignore */} +
Replace syntax is done via double open/closed curly braces, e.g. `{{ myEnvVar }}`
+
diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory/replace-syntax.png b/packages/web/docs/src/pages/docs/dashboard/laboratory/replace-syntax.png new file mode 100644 index 0000000000..a3ee22c051 Binary files /dev/null and b/packages/web/docs/src/pages/docs/dashboard/laboratory/replace-syntax.png differ diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory/unable-to-access-global-variables.png b/packages/web/docs/src/pages/docs/dashboard/laboratory/unable-to-access-global-variables.png new file mode 100644 index 0000000000..2a2cd0b8ca Binary files /dev/null and b/packages/web/docs/src/pages/docs/dashboard/laboratory/unable-to-access-global-variables.png differ diff --git a/packages/web/docs/src/pages/product-updates/2024-12-04-preflight-script/index.mdx b/packages/web/docs/src/pages/product-updates/2024-12-04-preflight-script/index.mdx new file mode 100644 index 0000000000..3741521949 --- /dev/null +++ b/packages/web/docs/src/pages/product-updates/2024-12-04-preflight-script/index.mdx @@ -0,0 +1,28 @@ +--- +title: Automatically run custom authentication processes in the Laboratory +description: + Useful for handling authentication flows like OAuth, where you may need to refresh an access token +date: 2024-12-04 +authors: [dimitri] +--- + +import { Callout } from '@theguild/components' + +export const figcaptionClass = 'text-center text-sm mt-2' + +We've added Preflight Scripts to Laboratory! These scripts allow you to automatically run custom +authentication processes before executing your GraphQL operations. They're especially useful for +handling authentication flows like OAuth, where you may need to refresh an access token. + +
+ <>![](private-next-pages/docs/dashboard/laboratory/get-and-set-env-vars.png) +
Demo of Preflight Scripts
+
+ + + **Note**: + +Check out [the documentation on Preflight Scripts](/docs/dashboard/laboratory/preflight-scripts) for +information on how to configure, edit, and use them. + + diff --git a/patches/@fastify__vite.patch b/patches/@fastify__vite.patch new file mode 100644 index 0000000000..a8a4039706 --- /dev/null +++ b/patches/@fastify__vite.patch @@ -0,0 +1,17 @@ +diff --git a/mode/development.js b/mode/development.js +index af9de9d75a3689cd4f4b5d2876f2e38bd2674ae4..94ecb29a8e0d2615b1ecd0114dba7f3979dc2b11 100644 +--- a/mode/development.js ++++ b/mode/development.js +@@ -79,7 +79,11 @@ async function setup(config) { + } + } + } +- const indexHtmlPath = join(config.vite.root, 'index.html') ++ ++ // Request is decorated with viteHtmlFile in: packages/web/app/src/server/index.ts ++ // It is used to render more than one html file ++ const htmlFileName = req.viteHtmlFile ?? 'index.html'; ++ const indexHtmlPath = join(config.vite.root,htmlFileName) + const indexHtml = await read(indexHtmlPath, 'utf8') + const transformedHtml = await this.devServer.transformIndexHtml( + req.url, diff --git a/patches/@graphiql__react.patch b/patches/@graphiql__react.patch new file mode 100644 index 0000000000..eeac86f2b1 --- /dev/null +++ b/patches/@graphiql__react.patch @@ -0,0 +1,423 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +index 8ca339a2ba2031f0c1e22f1d099fa9a571492107..1cf3e8c620dc2c3ad4cfc42e2feeb4ca4682163c 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -1,6 +1,6 @@ + import { jsx, jsxs, Fragment } from "react/jsx-runtime"; + import * as React from "react"; +-import { createContext, useContext, useRef, useState, useEffect, forwardRef, useCallback, useMemo, useLayoutEffect } from "react"; ++import { createContext, useContext, useRef, useState, useEffect, forwardRef, useMemo, useCallback, useLayoutEffect } from "react"; + import { clsx } from "clsx"; + import { print, astFromValue, isSchema, buildClientSchema, validateSchema, getIntrospectionQuery, isNamedType, isObjectType, isInputObjectType, isScalarType, isEnumType, isInterfaceType, isUnionType, isNonNullType, isListType, isAbstractType, isType, parse, visit } from "graphql"; + import { StorageAPI, HistoryStore, formatResult, isObservable, formatError, isAsyncIterable, fetcherReturnToPromise, isPromise, mergeAst, fillLeafs, getSelectedOperationName } from "@graphiql/toolkit"; +@@ -40,7 +40,7 @@ function createContextHook(context) { + const StorageContext = createNullableContext("StorageContext"); + function StorageContextProvider(props) { + const isInitialRender = useRef(true); +- const [storage, setStorage] = useState(new StorageAPI(props.storage)); ++ const [storage, setStorage] = useState(() => new StorageAPI(props.storage)); + useEffect(() => { + if (isInitialRender.current) { + isInitialRender.current = false; +@@ -465,68 +465,42 @@ const Tooltip = Object.assign(TooltipRoot, { + Provider: T.Provider + }); + const HistoryContext = createNullableContext("HistoryContext"); +-function HistoryContextProvider(props) { +- var _a; ++function HistoryContextProvider({ ++ maxHistoryLength = DEFAULT_HISTORY_LENGTH, ++ children ++}) { + const storage = useStorageContext(); +- const historyStore = useRef( +- new HistoryStore( ++ const [historyStore] = useState( ++ () => ( + // Fall back to a noop storage when the StorageContext is empty +- storage || new StorageAPI(null), +- props.maxHistoryLength || DEFAULT_HISTORY_LENGTH ++ new HistoryStore(storage || new StorageAPI(null), maxHistoryLength) + ) + ); +- const [items, setItems] = useState(((_a = historyStore.current) == null ? void 0 : _a.queries) || []); +- const addToHistory = useCallback( +- (operation) => { +- var _a2; +- (_a2 = historyStore.current) == null ? void 0 : _a2.updateHistory(operation); +- setItems(historyStore.current.queries); +- }, +- [] +- ); +- const editLabel = useCallback( +- (operation, index) => { +- historyStore.current.editLabel(operation, index); +- setItems(historyStore.current.queries); +- }, +- [] +- ); +- const toggleFavorite = useCallback( +- (operation) => { +- historyStore.current.toggleFavorite(operation); +- setItems(historyStore.current.queries); +- }, +- [] +- ); +- const setActive = useCallback( +- (item) => { +- return item; +- }, +- [] +- ); +- const deleteFromHistory = useCallback((item, clearFavorites = false) => { +- historyStore.current.deleteHistory(item, clearFavorites); +- setItems(historyStore.current.queries); +- }, []); ++ const [items, setItems] = useState(() => historyStore.queries || []); + const value = useMemo( + () => ({ +- addToHistory, +- editLabel, ++ addToHistory(operation) { ++ historyStore.updateHistory(operation); ++ setItems(historyStore.queries); ++ }, ++ editLabel(operation, index) { ++ historyStore.editLabel(operation, index); ++ setItems(historyStore.queries); ++ }, + items, +- toggleFavorite, +- setActive, +- deleteFromHistory ++ toggleFavorite(operation) { ++ historyStore.toggleFavorite(operation); ++ setItems(historyStore.queries); ++ }, ++ setActive: (item) => item, ++ deleteFromHistory(item, clearFavorites) { ++ historyStore.deleteHistory(item, clearFavorites); ++ setItems(historyStore.queries); ++ } + }), +- [ +- addToHistory, +- editLabel, +- items, +- toggleFavorite, +- setActive, +- deleteFromHistory +- ] ++ [items, historyStore] + ); +- return /* @__PURE__ */ jsx(HistoryContext.Provider, { value, children: props.children }); ++ return /* @__PURE__ */ jsx(HistoryContext.Provider, { value, children }); + } + const useHistoryContext = createContextHook(HistoryContext); + const DEFAULT_HISTORY_LENGTH = 20; +@@ -714,7 +688,8 @@ function ExecutionContextProvider({ + fetcher, + getDefaultFieldNames, + children, +- operationName ++ operationName, ++ onModifyHeaders + }) { + if (!fetcher) { + throw new TypeError( +@@ -792,6 +767,9 @@ function ExecutionContextProvider({ + } + setResponse(""); + setIsFetching(true); ++ if (onModifyHeaders) { ++ headers = await onModifyHeaders(headers); ++ } + const opName = operationName ?? queryEditor.operationName ?? void 0; + history == null ? void 0 : history.addToHistory({ + query, +@@ -999,9 +977,9 @@ function mergeIncrementalResult(executionResult, incrementalResult) { + } + } + } ++const isMacOs = typeof navigator !== "undefined" && navigator.userAgent.includes("Mac"); + const DEFAULT_EDITOR_THEME = "graphiql"; + const DEFAULT_KEY_MAP = "sublime"; +-const isMacOs = typeof navigator !== "undefined" && navigator.platform.toLowerCase().indexOf("mac") === 0; + const commonKeys = { + // Persistent search box in Query Editor + [isMacOs ? "Cmd-F" : "Ctrl-F"]: "findPersistent", +@@ -1599,7 +1577,7 @@ function Search() { + onFocus: handleFocus, + onBlur: handleFocus, + onChange: (event) => setSearchValue(event.target.value), +- placeholder: "⌘ K", ++ placeholder: `${isMacOs ? "⌘" : "Ctrl"} K`, + ref: inputRef, + value: searchValue, + "data-cy": "doc-explorer-input" +@@ -3063,14 +3041,16 @@ function useSetEditorValues({ + ); + } + function createTab({ ++ id, ++ title, + query = null, + variables = null, + headers = null +-} = {}) { ++}) { + return { +- id: guid(), ++ id: id || guid(), + hash: hashFromTabContents({ query, variables, headers }), +- title: query && fuzzyExtractOperationName(query) || DEFAULT_TITLE, ++ title: title || query && fuzzyExtractOperationName(query) || DEFAULT_TITLE, + query, + variables, + headers, +@@ -3088,8 +3068,7 @@ function setPropertiesInActiveTab(state, partialTab) { + const newTab = { ...tab, ...partialTab }; + return { + ...newTab, +- hash: hashFromTabContents(newTab), +- title: newTab.operationName || (newTab.query ? fuzzyExtractOperationName(newTab.query) : void 0) || DEFAULT_TITLE ++ hash: hashFromTabContents(newTab) + }; + }) + }; +@@ -3311,32 +3290,36 @@ function EditorContextProvider(props) { + responseEditor, + defaultHeaders + }); +- const addTab = useCallback(() => { +- setTabState((current) => { +- const updatedValues = synchronizeActiveTabValues(current); +- const updated = { +- tabs: [ +- ...updatedValues.tabs, +- createTab({ +- headers: defaultHeaders, +- query: defaultQuery ?? DEFAULT_QUERY +- }) +- ], +- activeTabIndex: updatedValues.tabs.length +- }; +- storeTabs(updated); +- setEditorValues(updated.tabs[updated.activeTabIndex]); +- onTabChange == null ? void 0 : onTabChange(updated); +- return updated; +- }); +- }, [ +- defaultHeaders, +- defaultQuery, +- onTabChange, +- setEditorValues, +- storeTabs, +- synchronizeActiveTabValues +- ]); ++ const addTab = useCallback( ++ (_tabState) => { ++ setTabState((current) => { ++ const updatedValues = synchronizeActiveTabValues(current); ++ const updated = { ++ tabs: [ ++ ...updatedValues.tabs, ++ createTab({ ++ ..._tabState, ++ headers: defaultHeaders, ++ query: defaultQuery ?? DEFAULT_QUERY ++ }) ++ ], ++ activeTabIndex: updatedValues.tabs.length ++ }; ++ storeTabs(updated); ++ setEditorValues(updated.tabs[updated.activeTabIndex]); ++ onTabChange == null ? void 0 : onTabChange(updated); ++ return updated; ++ }); ++ }, ++ [ ++ defaultHeaders, ++ defaultQuery, ++ onTabChange, ++ setEditorValues, ++ storeTabs, ++ synchronizeActiveTabValues ++ ] ++ ); + const changeTab = useCallback( + (index) => { + setTabState((current) => { +@@ -3432,6 +3415,7 @@ function EditorContextProvider(props) { + const value = useMemo( + () => ({ + ...tabState, ++ setTabState, + addTab, + changeTab, + moveTab, +@@ -3743,9 +3727,10 @@ function GraphiQLProvider({ + storage, + validationRules, + variables, +- visiblePlugin ++ visiblePlugin, ++ onModifyHeaders + }) { +- return /* @__PURE__ */ jsx(StorageContextProvider, { storage, children: /* @__PURE__ */ jsx(HistoryContextProvider, { maxHistoryLength, children: /* @__PURE__ */ jsx( ++ return /* @__PURE__ */ jsx(StorageContextProvider, { storage, children: /* @__PURE__ */ jsx( + EditorContextProvider, + { + defaultQuery, +@@ -3776,6 +3761,7 @@ function GraphiQLProvider({ + getDefaultFieldNames, + fetcher, + operationName, ++ onModifyHeaders, + children: /* @__PURE__ */ jsx(ExplorerContextProvider, { children: /* @__PURE__ */ jsx( + PluginContextProvider, + { +@@ -3790,7 +3776,7 @@ function GraphiQLProvider({ + } + ) + } +- ) }) }); ++ ) }); + } + function useTheme(defaultTheme = null) { + const storageContext = useStorageContext(); +@@ -4200,6 +4186,7 @@ export { + TypeLink, + UnStyledButton, + VariableEditor, ++ isMacOs, + useAutoCompleteLeafs, + useCopyQuery, + useDragResize, +diff --git a/dist/types/editor/context.d.ts b/dist/types/editor/context.d.ts +index 199db8a294f8132d46470498870adbdf9fdc83af..d8901fe0d50db17db36a502dcf69d5f69efb84a1 100644 +--- a/dist/types/editor/context.d.ts ++++ b/dist/types/editor/context.d.ts +@@ -1,6 +1,6 @@ + import { DocumentNode, FragmentDefinitionNode, OperationDefinitionNode, ValidationRule } from 'graphql'; + import { VariableToType } from 'graphql-language-service'; +-import { ReactNode } from 'react'; ++import { Dispatch, ReactNode, SetStateAction } from 'react'; + import { TabDefinition, TabsState, TabState } from './tabs'; + import { CodeMirrorEditor } from './types'; + export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { +@@ -10,10 +10,11 @@ export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { + variableToType: VariableToType | null; + }; + export declare type EditorContextType = TabsState & { ++ setTabState: Dispatch>; + /** + * Add a new tab. + */ +- addTab(): void; ++ addTab(tabState?: Pick): void; + /** + * Switch to a different tab. + * @param index The index of the tab that should be switched to. +@@ -38,7 +39,7 @@ export declare type EditorContextType = TabsState & { + * @param partialTab A partial tab state object that will override the + * current values. The properties `id`, `hash` and `title` cannot be changed. + */ +- updateActiveTabValues(partialTab: Partial>): void; ++ updateActiveTabValues(partialTab: Partial>): void; + /** + * The CodeMirror editor instance for the headers editor. + */ +diff --git a/dist/types/editor/tabs.d.ts b/dist/types/editor/tabs.d.ts +index 28704a9c1c6e22fa75986de8591759e13035c8c5..5204d2b25198f89da9bba70804656f02799c7df6 100644 +--- a/dist/types/editor/tabs.d.ts ++++ b/dist/types/editor/tabs.d.ts +@@ -90,7 +90,7 @@ export declare function useSetEditorValues({ queryEditor, variableEditor, header + headers?: string | null | undefined; + response: string | null; + }) => void; +-export declare function createTab({ query, variables, headers, }?: Partial): TabState; ++export declare function createTab({ id, title, query, variables, headers, }: Partial>): TabState; + export declare function setPropertiesInActiveTab(state: TabsState, partialTab: Partial>): TabsState; + export declare function fuzzyExtractOperationName(str: string): string | null; + export declare function clearHeadersFromTabs(storage: StorageAPI | null): void; +diff --git a/dist/types/execution.d.ts b/dist/types/execution.d.ts +index 2d458001265d925ed0323a10aecbefdb7e6d0b4e..eb024cf197f13bfaa67423f5751c7cad7d0664bc 100644 +--- a/dist/types/execution.d.ts ++++ b/dist/types/execution.d.ts +@@ -1,4 +1,4 @@ +-import { Fetcher } from '@graphiql/toolkit'; ++import { Fetcher, MaybePromise } from '@graphiql/toolkit'; + import { ReactNode } from 'react'; + import { UseAutoCompleteLeafsArgs } from './editor/hooks'; + export declare type ExecutionContextType = { +@@ -45,8 +45,13 @@ export declare type ExecutionContextProviderProps = Pick) => MaybePromise>; + }; +-export declare function ExecutionContextProvider({ fetcher, getDefaultFieldNames, children, operationName, }: ExecutionContextProviderProps): import("react/jsx-runtime").JSX.Element; ++export declare function ExecutionContextProvider({ fetcher, getDefaultFieldNames, children, operationName, onModifyHeaders, }: ExecutionContextProviderProps): import("react/jsx-runtime").JSX.Element; + export declare const useExecutionContext: { + (options: { + nonNull: true; +diff --git a/dist/types/history/context.d.ts b/dist/types/history/context.d.ts +index f2699b344d27806094c0e5d62d914e5618dcf4db..9e6e3c6cdfded41af49c4c15c8d0be100e896bb0 100644 +--- a/dist/types/history/context.d.ts ++++ b/dist/types/history/context.d.ts +@@ -76,7 +76,7 @@ export declare type HistoryContextProviderProps = { + * any additional props they added for their needs (i.e., build their own functions that may save + * to a backend instead of localStorage and might need an id property added to the QueryStoreItem) + */ +-export declare function HistoryContextProvider(props: HistoryContextProviderProps): import("react/jsx-runtime").JSX.Element; ++export declare function HistoryContextProvider({ maxHistoryLength, children, }: HistoryContextProviderProps): import("react/jsx-runtime").JSX.Element; + export declare const useHistoryContext: { + (options: { + nonNull: true; +diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts +index 26ef2a2a07dcdf29f868067d32a0f5ff7981d8e6..28d9620636bab2221239ab8b87505425a0468b5f 100644 +--- a/dist/types/index.d.ts ++++ b/dist/types/index.d.ts +@@ -8,6 +8,7 @@ export { SchemaContext, SchemaContextProvider, useSchemaContext, } from './schem + export { StorageContext, StorageContextProvider, useStorageContext, } from './storage'; + export { useTheme } from './theme'; + export { useDragResize } from './utility/resize'; ++export { isMacOs } from './utility/is-macos'; + export * from './icons'; + export * from './ui'; + export * from './toolbar'; +diff --git a/dist/types/provider.d.ts b/dist/types/provider.d.ts +index e95c73f0b8c7cdfaece528e5f411ffd29862d490..d0d1e80a13da5d22abbcb4d6e052e91323fcc86f 100644 +--- a/dist/types/provider.d.ts ++++ b/dist/types/provider.d.ts +@@ -6,4 +6,4 @@ import { PluginContextProviderProps } from './plugin'; + import { SchemaContextProviderProps } from './schema'; + import { StorageContextProviderProps } from './storage'; + export declare type GraphiQLProviderProps = EditorContextProviderProps & ExecutionContextProviderProps & ExplorerContextProviderProps & HistoryContextProviderProps & PluginContextProviderProps & SchemaContextProviderProps & StorageContextProviderProps; +-export declare function GraphiQLProvider({ children, dangerouslyAssumeSchemaIsValid, defaultQuery, defaultHeaders, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, }: GraphiQLProviderProps): import("react/jsx-runtime").JSX.Element; ++export declare function GraphiQLProvider({ children, dangerouslyAssumeSchemaIsValid, defaultQuery, defaultHeaders, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, onModifyHeaders, }: GraphiQLProviderProps): import("react/jsx-runtime").JSX.Element; +diff --git a/dist/types/storage.d.ts b/dist/types/storage.d.ts +index c4c98ab5c3cd32837109d9d20d4808ad6793fd3f..0a1257b6e041d42068bffb5f332855372b89ea88 100644 +--- a/dist/types/storage.d.ts ++++ b/dist/types/storage.d.ts +@@ -6,7 +6,7 @@ export declare type StorageContextProviderProps = { + children: ReactNode; + /** + * Provide a custom storage API. +- * @default `localStorage`` ++ * @default `localStorage` + * @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#storage-2|API docs} + * for details on the required interface. + */ +diff --git a/dist/types/utility/is-macos.d.ts b/dist/types/utility/is-macos.d.ts +new file mode 100644 +index 0000000000000000000000000000000000000000..5f05699dde4723cbd446e914900dd9e7ff41ae70 +--- /dev/null ++++ b/dist/types/utility/is-macos.d.ts +@@ -0,0 +1 @@ ++export declare const isMacOs: boolean; diff --git a/patches/@graphiql__react@1.0.0-alpha.3.patch b/patches/@graphiql__react@1.0.0-alpha.3.patch deleted file mode 100644 index 8e94659e10..0000000000 --- a/patches/@graphiql__react@1.0.0-alpha.3.patch +++ /dev/null @@ -1,140 +0,0 @@ -diff --git a/dist/index.mjs b/dist/index.mjs -index 567480ba5ccc30619db2eb8e6da8d618ddb5e891..c49c573893a26897062cda2af9896bc69881db2a 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -3056,14 +3056,16 @@ function useSetEditorValues({ - ); - } - function createTab({ -+ id, -+ title, - query = null, - variables = null, - headers = null --} = {}) { -+}) { - return { -- id: guid(), -+ id: id || guid(), - hash: hashFromTabContents({ query, variables, headers }), -- title: query && fuzzyExtractOperationName(query) || DEFAULT_TITLE, -+ title: title || query && fuzzyExtractOperationName(query) || DEFAULT_TITLE, - query, - variables, - headers, -@@ -3081,8 +3083,7 @@ function setPropertiesInActiveTab(state, partialTab) { - const newTab = { ...tab, ...partialTab }; - return { - ...newTab, -- hash: hashFromTabContents(newTab), -- title: newTab.operationName || (newTab.query ? fuzzyExtractOperationName(newTab.query) : void 0) || DEFAULT_TITLE -+ hash: hashFromTabContents(newTab) - }; - }) - }; -@@ -3304,25 +3305,31 @@ function EditorContextProvider(props) { - responseEditor, - defaultHeaders - }); -- const addTab = useCallback(() => { -- setTabState((current) => { -- const updatedValues = synchronizeActiveTabValues(current); -- const updated = { -- tabs: [...updatedValues.tabs, createTab({ headers: defaultHeaders })], -- activeTabIndex: updatedValues.tabs.length -- }; -- storeTabs(updated); -- setEditorValues(updated.tabs[updated.activeTabIndex]); -- onTabChange == null ? void 0 : onTabChange(updated); -- return updated; -- }); -- }, [ -- defaultHeaders, -- onTabChange, -- setEditorValues, -- storeTabs, -- synchronizeActiveTabValues -- ]); -+ const addTab = useCallback( -+ (_tabState) => { -+ setTabState((current) => { -+ const updatedValues = synchronizeActiveTabValues(current); -+ const updated = { -+ tabs: [ -+ ...updatedValues.tabs, -+ createTab({ ..._tabState, headers: defaultHeaders }) -+ ], -+ activeTabIndex: updatedValues.tabs.length -+ }; -+ storeTabs(updated); -+ setEditorValues(updated.tabs[updated.activeTabIndex]); -+ onTabChange == null ? void 0 : onTabChange(updated); -+ return updated; -+ }); -+ }, -+ [ -+ defaultHeaders, -+ onTabChange, -+ setEditorValues, -+ storeTabs, -+ synchronizeActiveTabValues -+ ] -+ ); - const changeTab = useCallback( - (index) => { - setTabState((current) => { -@@ -3418,6 +3425,7 @@ function EditorContextProvider(props) { - const value = useMemo( - () => ({ - ...tabState, -+ setTabState, - addTab, - changeTab, - moveTab, -diff --git a/dist/types/editor/context.d.ts b/dist/types/editor/context.d.ts -index 199db8a294f8132d46470498870adbdf9fdc83af..d8901fe0d50db17db36a502dcf69d5f69efb84a1 100644 ---- a/dist/types/editor/context.d.ts -+++ b/dist/types/editor/context.d.ts -@@ -1,6 +1,6 @@ - import { DocumentNode, FragmentDefinitionNode, OperationDefinitionNode, ValidationRule } from 'graphql'; - import { VariableToType } from 'graphql-language-service'; --import { ReactNode } from 'react'; -+import { Dispatch, ReactNode, SetStateAction } from 'react'; - import { TabDefinition, TabsState, TabState } from './tabs'; - import { CodeMirrorEditor } from './types'; - export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { -@@ -10,10 +10,11 @@ export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { - variableToType: VariableToType | null; - }; - export declare type EditorContextType = TabsState & { -+ setTabState: Dispatch>; - /** - * Add a new tab. - */ -- addTab(): void; -+ addTab(tabState?: Pick): void; - /** - * Switch to a different tab. - * @param index The index of the tab that should be switched to. -@@ -38,7 +39,7 @@ export declare type EditorContextType = TabsState & { - * @param partialTab A partial tab state object that will override the - * current values. The properties `id`, `hash` and `title` cannot be changed. - */ -- updateActiveTabValues(partialTab: Partial>): void; -+ updateActiveTabValues(partialTab: Partial>): void; - /** - * The CodeMirror editor instance for the headers editor. - */ -diff --git a/dist/types/editor/tabs.d.ts b/dist/types/editor/tabs.d.ts -index 28704a9c1c6e22fa75986de8591759e13035c8c5..5204d2b25198f89da9bba70804656f02799c7df6 100644 ---- a/dist/types/editor/tabs.d.ts -+++ b/dist/types/editor/tabs.d.ts -@@ -90,7 +90,7 @@ export declare function useSetEditorValues({ queryEditor, variableEditor, header - headers?: string | null | undefined; - response: string | null; - }) => void; --export declare function createTab({ query, variables, headers, }?: Partial): TabState; -+export declare function createTab({ id, title, query, variables, headers, }: Partial>): TabState; - export declare function setPropertiesInActiveTab(state: TabsState, partialTab: Partial>): TabsState; - export declare function fuzzyExtractOperationName(str: string): string | null; - export declare function clearHeadersFromTabs(storage: StorageAPI | null): void; diff --git a/patches/graphiql.patch b/patches/graphiql.patch new file mode 100644 index 0000000000..f145ae175d --- /dev/null +++ b/patches/graphiql.patch @@ -0,0 +1,59 @@ +diff --git a/dist/index.d.ts b/dist/index.d.ts +index d0d893ea0caffb6c1c70c5f95aed8ca49bc74701..ba3c02801e958c66fde9b813821e5a608f49b1cf 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -14,7 +14,7 @@ declare type AddSuffix, Suffix extends string> = + [Key in keyof Obj as `${string & Key}${Suffix}`]: Obj[Key]; + }; + +-export declare function GraphiQL({ dangerouslyAssumeSchemaIsValid, confirmCloseTab, defaultQuery, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, defaultHeaders, ...props }: GraphiQLProps): JSX_2.Element; ++export declare function GraphiQL({ dangerouslyAssumeSchemaIsValid, confirmCloseTab, defaultQuery, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, defaultHeaders, onModifyHeaders, ...props }: GraphiQLProps): JSX_2.Element; + + export declare namespace GraphiQL { + var Logo: typeof GraphiQLLogo; +diff --git a/dist/index.mjs b/dist/index.mjs +index cf1a9036b4b35b7918da09ead6977e1e77724b8a..896dadb9c22b36ceee99776b87684f9e3899023d 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -1,4 +1,4 @@ +-import { GraphiQLProvider, useEditorContext, useExecutionContext, useSchemaContext, useStorageContext, usePluginContext, useTheme, useDragResize, Tooltip, UnStyledButton, ReloadIcon, KeyboardShortcutIcon, SettingsIcon, Tabs, Tab, PlusIcon, QueryEditor, ExecuteButton, ChevronUpIcon, ChevronDownIcon, VariableEditor, HeaderEditor, Spinner, ResponseEditor, Dialog, ButtonGroup, Button, useCopyQuery, useMergeQuery, usePrettifyEditors, ToolbarButton, PrettifyIcon, MergeIcon, CopyIcon } from "@graphiql/react"; ++import { GraphiQLProvider, useEditorContext, useExecutionContext, useSchemaContext, useStorageContext, usePluginContext, useTheme, useDragResize, Tooltip, UnStyledButton, ReloadIcon, KeyboardShortcutIcon, SettingsIcon, Tabs, Tab, PlusIcon, QueryEditor, ExecuteButton, ChevronUpIcon, ChevronDownIcon, VariableEditor, HeaderEditor, Spinner, ResponseEditor, Dialog, ButtonGroup, Button, useCopyQuery, useMergeQuery, usePrettifyEditors, ToolbarButton, PrettifyIcon, MergeIcon, CopyIcon, isMacOs } from "@graphiql/react"; + import { GraphiQLProvider as GraphiQLProvider2 } from "@graphiql/react"; + import React, { version, useMemo, useEffect, useState, Children, cloneElement, useCallback, Fragment } from "react"; + const majorVersion = parseInt(version.slice(0, 2), 10); +@@ -39,6 +39,7 @@ function GraphiQL({ + variables, + visiblePlugin, + defaultHeaders, ++ onModifyHeaders, + ...props + }) { + var _a, _b; +@@ -85,7 +86,8 @@ function GraphiQL({ + shouldPersistHeaders, + storage, + validationRules, +- variables ++ variables, ++ onModifyHeaders + }, + /* @__PURE__ */ React.createElement( + GraphiQLInterface, +@@ -398,7 +400,7 @@ function GraphiQLInterface(props) { + { + type: "button", + className: "graphiql-tab-add", +- onClick: handleAddTab, ++ onClick: () => handleAddTab(), + "aria-label": "New tab" + }, + /* @__PURE__ */ React.createElement(PlusIcon, { "aria-hidden": "true" }) +@@ -602,7 +604,7 @@ function GraphiQLInterface(props) { + )) : null + ))); + } +-const modifier = typeof navigator !== "undefined" && navigator.platform.toLowerCase().indexOf("mac") === 0 ? "Cmd" : "Ctrl"; ++const modifier = isMacOs ? "⌘" : "Ctrl"; + const SHORT_KEYS = Object.entries({ + "Search in editor": [modifier, "F"], + "Search in documentation": [modifier, "K"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3501d23123..75b9ad78ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,9 +16,12 @@ patchedDependencies: '@apollo/federation@0.38.1': hash: rjgakkkphrejw6qrtph4ar24zq path: patches/@apollo__federation@0.38.1.patch - '@graphiql/react@1.0.0-alpha.3': - hash: gnbo5nw2wgehkfq2yrmuhrx4im - path: patches/@graphiql__react@1.0.0-alpha.3.patch + '@fastify/vite': + hash: wz23vdqq6qtsz64wb433afnvou + path: patches/@fastify__vite.patch + '@graphiql/react': + hash: bru5she67j343rpipomank3vn4 + path: patches/@graphiql__react.patch '@graphql-eslint/eslint-plugin@3.20.1': hash: n437g5o7zq7pnxdxldn52uql2q path: patches/@graphql-eslint__eslint-plugin@3.20.1.patch @@ -40,6 +43,9 @@ patchedDependencies: got@14.4.4: hash: b6pwqmrs3qqykctltsasvrfwti path: patches/got@14.4.4.patch + graphiql: + hash: yjzkcog7ut7wshk4npre67txki + path: patches/graphiql.patch mjml-core@4.14.0: hash: zxxsxbqejjmcwuzpigutzzq6wa path: patches/mjml-core@4.14.0.patch @@ -689,6 +695,12 @@ importers: '@hive/webhooks': specifier: workspace:* version: link:../webhooks + '@nodesecure/i18n': + specifier: ^4.0.1 + version: 4.0.1 + '@nodesecure/js-x-ray': + specifier: 8.0.0 + version: 8.0.0 '@octokit/app': specifier: 14.1.0 version: 14.1.0 @@ -1655,14 +1667,14 @@ importers: specifier: 7.0.4 version: 7.0.4 '@fastify/vite': - specifier: 6.0.7 - version: 6.0.7(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0) + specifier: 6.0.6 + version: 6.0.6(patch_hash=wz23vdqq6qtsz64wb433afnvou)(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0) '@graphiql/plugin-explorer': specifier: 4.0.0-alpha.2 - version: 4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@graphiql/react': - specifier: 1.0.0-alpha.3 - version: 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 1.0.0-alpha.4 + version: 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@graphiql/toolkit': specifier: 0.9.1 version: 0.9.1(@types/node@22.9.3)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0) @@ -1807,6 +1819,9 @@ importers: '@trpc/server': specifier: 10.45.2 version: 10.45.2 + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 '@types/dompurify': specifier: 3.0.5 version: 3.0.5 @@ -1852,6 +1867,9 @@ importers: cmdk: specifier: 0.2.1 version: 0.2.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 date-fns: specifier: 4.1.0 version: 4.1.0 @@ -1877,8 +1895,8 @@ importers: specifier: 11.11.17 version: 11.11.17(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphiql: - specifier: 4.0.0-alpha.4 - version: 4.0.0-alpha.4(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 4.0.0-alpha.5 + version: 4.0.0-alpha.5(patch_hash=yjzkcog7ut7wshk4npre67txki)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphql: specifier: 16.9.0 version: 16.9.0 @@ -2005,6 +2023,9 @@ importers: zod: specifier: 3.23.8 version: 3.23.8 + zustand: + specifier: 5.0.1 + version: 5.0.1(@types/react@18.3.12)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1)) packages/web/docs: dependencies: @@ -3865,9 +3886,8 @@ packages: '@fastify/static@7.0.4': resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} - '@fastify/vite@6.0.7': - resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} - bundledDependencies: [] + '@fastify/vite@6.0.6': + resolution: {integrity: sha512-FsWJC92murm5tjeTezTTvMLyZido/ZWy0wYWpVkh/bDe1gAUAabYLB7Vp8hokXGsRE/mOpqYVsRDAKENY2qPUQ==} '@floating-ui/core@1.2.6': resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} @@ -3913,8 +3933,8 @@ packages: react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 - '@graphiql/react@1.0.0-alpha.3': - resolution: {integrity: sha512-9WPfC7I9xO1qC/dKaYwVe3UbPJvdjU+fxoUW2Id0mIljkD7LXnVUnzBQMB1SY4JrRJX3I0nQPbHXzKvAYSpnjw==} + '@graphiql/react@1.0.0-alpha.4': + resolution: {integrity: sha512-psie5qQNVlklXAhNPD8sIRtpNDzJfNzzZ5EH0ofrJ8AeVcj+DYmIqxRWw5zvjDAtNTKyOAJSCpQdCNDCCi2PEQ==} peerDependencies: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2 react: ^16.8.0 || ^17 || ^18 @@ -4965,6 +4985,20 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@nodesecure/estree-ast-utils@1.5.0': + resolution: {integrity: sha512-uRdPpBQOSvn+iuLWlbtP6NZTI54ALhFZNRc5K+ZaYT8FeYFu0g6HwlNwwwPyyTB1kXyysRRi9j9fkW9bAYSXtw==} + + '@nodesecure/i18n@4.0.1': + resolution: {integrity: sha512-i/A8citn5N1i7VBL0PbryAx3zM3sgpFsfVVzIrl5tSTih3cMbQ3QYKCo294U2DMaoncWmI6wO5S71XwctvTHeg==} + engines: {node: '>=18'} + + '@nodesecure/js-x-ray@8.0.0': + resolution: {integrity: sha512-RTDrJfYuLIZ1pnIz+KJOCfH+kHaoxkO0Nyr2xo/6eiuFKzO1F0gSnrE1uQfhUGfeBiMiwx2qzOdyeKFgYn5Baw==} + engines: {node: '>=18.0.0'} + + '@nodesecure/sec-literal@1.2.0': + resolution: {integrity: sha512-LGgJmBtnIVHwjZ1QA62YyDvPysdYvGcGn6/JADjY23snTNZS+D9JrkxnChggoNDYj3/GtjutbY/cSlLXEcUJRw==} + '@npmcli/agent@2.2.1': resolution: {integrity: sha512-H4FrOVtNyWC8MUwL3UfjOsAihHvT1Pe8POj3JvjXhSTJipsZMtgUALCT4mGyYZNxymkUfOw3PUj6dE4QPp6osQ==} engines: {node: ^16.14.0 || >=18.0.0} @@ -7617,6 +7651,9 @@ packages: '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/debug@4.1.7': resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} @@ -9373,6 +9410,10 @@ packages: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + default-browser-id@3.0.0: resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} engines: {node: '>=12'} @@ -9491,6 +9532,10 @@ packages: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} + digraph-js@2.2.3: + resolution: {integrity: sha512-btynrARSW6pBmDz9+cwCxkBJ91CGBxIaNQo7V+ul9/rCRr3HddwehpEMnL6Ru2OeC2pKdRteB1v5TgZRrAAYKQ==} + engines: {node: '>=16.0.0'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -10314,6 +10359,9 @@ packages: react-dom: optional: true + frequency-set@1.0.2: + resolution: {integrity: sha512-Qip6vS0fY/et08sZXumws05weoYvj2ZLkBq3xIwFDFLg8v5IMQiRa+P30tXL0CU6DiYUPLuN3HyRcwW6yWPdeA==} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -10578,8 +10626,8 @@ packages: react: ^15.6.0 || ^16.0.0 react-dom: ^15.6.0 || ^16.0.0 - graphiql@4.0.0-alpha.4: - resolution: {integrity: sha512-mMjUJKqNSuHGDBC6JE3CprtBniB3iaGN7Ifisrd6ucmpH7biqA1uIbz1LgzCeQdtndCTHADLJu6dCGOWAcgP9w==} + graphiql@4.0.0-alpha.5: + resolution: {integrity: sha512-LAxuJ8kwPlT7YbgM2VMr6bn9xWHRgUL4SRZtvA2VTDDkMgsnTLKd9Vro/EZNKJCTaC7SGOBRImAGCOPlpfTTjw==} peerDependencies: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2 react: ^16.8.0 || ^17 || ^18 @@ -11104,6 +11152,10 @@ packages: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} + is-base64@1.1.0: + resolution: {integrity: sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==} + hasBin: true + is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -11210,6 +11262,9 @@ packages: is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + is-minified-code@2.0.0: + resolution: {integrity: sha512-I1BHmOxm7owypunUWnYx2Ggdhg3lzdyJXLepi8NuR/IsvgVgkwjLj+12iYAGUklu0Xvy3nXGcDSKGbE0Q0Nkag==} + is-negative-zero@2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} @@ -11309,6 +11364,10 @@ packages: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} + is-svg@4.4.0: + resolution: {integrity: sha512-v+AgVwiK5DsGtT9ng+m4mClp6zDAmwrW8nZi6Gg15qzvBnRWWdfWA1TGaXyCDnWq5g5asofIgMVl3PjKxvk1ug==} + engines: {node: '>=6'} + is-symbol@1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} @@ -11847,6 +11906,9 @@ packages: lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} @@ -11871,6 +11933,9 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash.uniqwith@4.5.0: + resolution: {integrity: sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==} + lodash.xorby@4.7.0: resolution: {integrity: sha512-gYiD6nvuQy0AEkMoUju+t4f4Rn18fjsLB/7x7YZFqtFT9kmegRLrj/uGEQVyVDy7otTmSrIMXNOk2wwuLcfHCQ==} @@ -12099,6 +12164,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meriyah@5.0.0: + resolution: {integrity: sha512-tNlPDP4AzkH/7cROw7PKJ7mCLe/ZLpa2ja23uqB35vt63+8dgZi2NKLJMrkjxLcxArnLJVvd3Y/7pRl3OLR7yg==} + engines: {node: '>=10.4.0'} + mermaid@11.2.1: resolution: {integrity: sha512-F8TEaLVVyxTUmvKswVFyOkjPrlJA5h5vNR1f7ZnSWSpqxgEZG1hggtn/QCa7znC28bhlcrNh10qYaIiill7q4A==} @@ -14160,6 +14229,9 @@ packages: safe-regex2@3.1.0: resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + safe-stable-stringify@2.4.2: resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==} engines: {node: '>=10'} @@ -14971,6 +15043,9 @@ packages: '@swc/wasm': optional: true + ts-pattern@5.5.0: + resolution: {integrity: sha512-jqbIpTsa/KKTJYWgPNsFNbLVpwCgzXfFJ1ukNn4I8hMwyQzHMJnk/BqWzggB0xpkILuKzaO/aMYhS0SkaJyKXg==} + tsconfck@3.0.3: resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==} engines: {node: ^18 || >=20} @@ -15850,6 +15925,24 @@ packages: zrender@5.6.0: resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} + zustand@5.0.1: + resolution: {integrity: sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -18322,7 +18415,7 @@ snapshots: fastq: 1.17.1 glob: 10.3.12 - '@fastify/vite@6.0.7(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)': + '@fastify/vite@6.0.6(patch_hash=wz23vdqq6qtsz64wb433afnvou)(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)': dependencies: '@fastify/middie': 8.3.1 '@fastify/static': 6.12.0 @@ -18378,15 +18471,15 @@ snapshots: graphql: 16.9.0 typescript: 5.6.3 - '@graphiql/plugin-explorer@4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/plugin-explorer@4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/react': 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/react': 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphiql-explorer: 0.9.0(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphql: 16.9.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@graphiql/toolkit': 0.10.0(@types/node@22.9.3)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0) '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -20011,6 +20104,35 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@nodesecure/estree-ast-utils@1.5.0': + dependencies: + '@nodesecure/sec-literal': 1.2.0 + + '@nodesecure/i18n@4.0.1': + dependencies: + cacache: 18.0.2 + deepmerge: 4.3.1 + lodash.get: 4.4.2 + + '@nodesecure/js-x-ray@8.0.0': + dependencies: + '@nodesecure/estree-ast-utils': 1.5.0 + '@nodesecure/sec-literal': 1.2.0 + digraph-js: 2.2.3 + estree-walker: 3.0.3 + frequency-set: 1.0.2 + is-minified-code: 2.0.0 + meriyah: 5.0.0 + safe-regex: 2.1.1 + ts-pattern: 5.5.0 + + '@nodesecure/sec-literal@1.2.0': + dependencies: + frequency-set: 1.0.2 + is-base64: 1.1.0 + is-svg: 4.4.0 + string-width: 5.1.2 + '@npmcli/agent@2.2.1': dependencies: agent-base: 7.1.0 @@ -23391,8 +23513,8 @@ snapshots: '@typescript-eslint/parser': 7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3) eslint: 8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) eslint-config-prettier: 9.1.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) eslint-plugin-jsonc: 2.11.1(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) eslint-plugin-mdx: 3.0.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) @@ -23552,6 +23674,8 @@ snapshots: dependencies: '@types/node': 22.9.3 + '@types/crypto-js@4.2.2': {} + '@types/debug@4.1.7': dependencies: '@types/ms': 0.7.34 @@ -25587,6 +25711,8 @@ snapshots: deepmerge@4.2.2: {} + deepmerge@4.3.1: {} + default-browser-id@3.0.0: dependencies: bplist-parser: 0.2.0 @@ -25674,6 +25800,11 @@ snapshots: diff@5.2.0: {} + digraph-js@2.2.3: + dependencies: + lodash.isequal: 4.5.0 + lodash.uniqwith: 4.5.0 + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -26100,13 +26231,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): dependencies: debug: 4.3.7(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -26137,14 +26268,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): + eslint-module-utils@2.8.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3) eslint: 8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) transitivePeerDependencies: - supports-color @@ -26160,7 +26291,7 @@ snapshots: eslint: 8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) eslint-compat-utils: 0.1.2(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)): dependencies: array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 @@ -26170,7 +26301,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)))(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1(patch_hash=fjbpfrtrjd6idngyeqxnwopfva)) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -26853,6 +26984,8 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + frequency-set@1.0.2: {} + fresh@0.5.2: {} fromentries@1.3.2: {} @@ -27175,9 +27308,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - graphiql@4.0.0-alpha.4(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + graphiql@4.0.0-alpha.5(patch_hash=yjzkcog7ut7wshk4npre67txki)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@graphiql/react': 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/react': 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.1)(@types/react@18.3.12)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphql: 16.9.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -27904,6 +28037,8 @@ snapshots: dependencies: has-tostringtag: 1.0.0 + is-base64@1.1.0: {} + is-bigint@1.0.4: dependencies: has-bigints: 1.0.2 @@ -27988,6 +28123,8 @@ snapshots: is-map@2.0.2: {} + is-minified-code@2.0.0: {} + is-negative-zero@2.0.2: {} is-network-error@1.0.0: {} @@ -28059,6 +28196,10 @@ snapshots: dependencies: better-path-resolve: 1.0.0 + is-svg@4.4.0: + dependencies: + fast-xml-parser: 4.4.1 + is-symbol@1.0.4: dependencies: has-symbols: 1.0.3 @@ -28578,6 +28719,8 @@ snapshots: lodash.isarguments@3.1.0: {} + lodash.isequal@4.5.0: {} + lodash.isplainobject@4.0.6: {} lodash.lowercase@4.3.0: {} @@ -28594,6 +28737,8 @@ snapshots: lodash.startcase@4.4.0: {} + lodash.uniqwith@4.5.0: {} + lodash.xorby@4.7.0: {} lodash@4.17.21: {} @@ -28975,6 +29120,8 @@ snapshots: merge2@1.4.1: {} + meriyah@5.0.0: {} + mermaid@11.2.1: dependencies: '@braintree/sanitize-url': 7.1.0 @@ -31615,6 +31762,10 @@ snapshots: dependencies: ret: 0.4.3 + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + safe-stable-stringify@2.4.2: {} safer-buffer@2.1.2: {} @@ -32522,6 +32673,8 @@ snapshots: optionalDependencies: '@swc/core': 1.9.2(@swc/helpers@0.5.11) + ts-pattern@5.5.0: {} + tsconfck@3.0.3(typescript@5.6.3): optionalDependencies: typescript: 5.6.3 @@ -33546,4 +33699,11 @@ snapshots: dependencies: tslib: 2.3.0 + zustand@5.0.1(@types/react@18.3.12)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.12 + immer: 10.1.1 + react: 18.3.1 + use-sync-external-store: 1.2.0(react@18.3.1) + zwitch@2.0.4: {}