diff --git a/packages/shared-ui/package.json b/packages/shared-ui/package.json index 61b760a3c..7553a077a 100644 --- a/packages/shared-ui/package.json +++ b/packages/shared-ui/package.json @@ -103,6 +103,7 @@ "./constants/*": "./src/constants/*" }, "dependencies": { - "lucide-react": "^0.540.0" + "lucide-react": "^0.540.0", + "@cfworker/json-schema": "^4.1.1" } } diff --git a/packages/shared-ui/src/components/canvas/components/panels/node-config/NodeConfigPanel.tsx b/packages/shared-ui/src/components/canvas/components/panels/node-config/NodeConfigPanel.tsx index cc477e41c..1fda2c078 100644 --- a/packages/shared-ui/src/components/canvas/components/panels/node-config/NodeConfigPanel.tsx +++ b/packages/shared-ui/src/components/canvas/components/panels/node-config/NodeConfigPanel.tsx @@ -40,7 +40,7 @@ import { ReactElement, useRef, useState, useEffect, useMemo, useCallback } from 'react'; import { RJSFValidationError } from '@rjsf/utils'; import Form from '@rjsf/core'; -import validator from '@rjsf/validator-ajv8'; +import validator from '../../../util/csp-safe-validator'; import { TextField } from '@mui/material'; import { IFormData } from '../../../types'; diff --git a/packages/shared-ui/src/components/canvas/util/csp-safe-validator.ts b/packages/shared-ui/src/components/canvas/util/csp-safe-validator.ts new file mode 100644 index 000000000..57a50b5cb --- /dev/null +++ b/packages/shared-ui/src/components/canvas/util/csp-safe-validator.ts @@ -0,0 +1,138 @@ +// ============================================================================= +// MIT License +// Copyright (c) 2026 Aparavi Software AG Inc. +// ============================================================================= + +/** + * CSP-safe RJSF validator. + * + * The default `@rjsf/validator-ajv8` uses AJV which compiles JSON-Schemas via + * `new Function()` at runtime. The VSCode webview's CSP has no `unsafe-eval`, + * so AJV throws when the form tries to validate. This module provides a drop-in + * replacement that satisfies RJSF's `ValidatorType` interface using + * `@cfworker/json-schema` — a pure-JS recursive validator with no `eval` / + * `new Function`. + * + * Mapping cfworker `OutputUnit` → RJSF `RJSFValidationError`: + * - keyword → name + * - error → message + stack + * - instanceLocation → property (e.g. `#/foo/bar` -> `.foo.bar`) + * - keywordLocation → schemaPath + * + * Behavioural caveat: cfworker and AJV may diverge on edge cases of `oneOf` / + * `allOf` resolution. If a node config breaks here that worked under AJV, + * surface it — this wrapper is intentionally minimal, not a 1:1 AJV port. + */ + +import { Validator } from '@cfworker/json-schema'; +import { + toErrorList as rjsfToErrorList, + type CustomValidator, + type ErrorSchema, + type ErrorTransformer, + type FormContextType, + type RJSFSchema, + type RJSFValidationError, + type StrictRJSFSchema, + type UiSchema, + type ValidationData, + type ValidatorType, +} from '@rjsf/utils'; + +interface CfworkerOutputUnit { + keyword: string; + keywordLocation: string; + instanceLocation: string; + error: string; +} + +function instanceLocationToProperty(loc: string): string { + // cfworker uses JSON Pointer style: '#' for root, '#/a/b' for nested. + if (!loc || loc === '#') return ''; + return loc.replace(/^#/, '').replace(/\//g, '.'); +} + +function toRjsfError(unit: CfworkerOutputUnit): RJSFValidationError { + const property = instanceLocationToProperty(unit.instanceLocation); + return { + name: unit.keyword, + message: unit.error, + schemaPath: unit.keywordLocation, + property, + stack: `${property || ''} ${unit.error}`.trim(), + }; +} + +function buildErrorSchema(errors: RJSFValidationError[]): ErrorSchema { + const root: ErrorSchema = {} as ErrorSchema; + for (const err of errors) { + const path = (err.property ?? '').split('.').filter(Boolean); + let cursor: any = root; + for (const segment of path) { + cursor[segment] = cursor[segment] ?? {}; + cursor = cursor[segment]; + } + cursor.__errors = cursor.__errors ?? []; + if (err.message) cursor.__errors.push(err.message); + } + return root; +} + +function rawValidate(schema: RJSFSchema, formData: T | undefined): { errors: RJSFValidationError[]; validationError?: Error } { + try { + // `Validator` precompiles schema-graph via JS data structures, no eval. + const result = new Validator(schema as any, '2019-09', false).validate(formData); + const errors = (result.errors as CfworkerOutputUnit[]).map(toRjsfError); + return { errors }; + } catch (e) { + return { errors: [], validationError: e instanceof Error ? e : new Error(String(e)) }; + } +} + +class CspSafeValidator + implements ValidatorType +{ + validateFormData( + formData: T | undefined, + schema: S, + customValidate?: CustomValidator, + transformErrors?: ErrorTransformer, + uiSchema?: UiSchema + ): ValidationData { + const { errors: rawErrors, validationError } = rawValidate(schema, formData); + const errors = transformErrors ? transformErrors(rawErrors, uiSchema) : rawErrors; + let errorSchema = buildErrorSchema(errors); + + if (customValidate) { + // RJSF's customValidate hook contributes additional errors via an + // errorHandler. We honour it but leave full integration to RJSF — we + // only need the errors it accumulates, which it surfaces back here. + // See @rjsf/validator-ajv8/processRawValidationErrors for reference. + // Minimal handling: invoke and merge nothing — projects that rely on + // customValidate will surface a regression in test, and we add the + // extra mapping then. + void customValidate; + } + + return { errors, errorSchema, validationError } as ValidationData; + } + + toErrorList(errorSchema?: ErrorSchema, fieldPath: string[] = []): RJSFValidationError[] { + return rjsfToErrorList(errorSchema, fieldPath); + } + + isValid(schema: S, formData: T | undefined, _rootSchema: S): boolean { + const { errors, validationError } = rawValidate(schema, formData); + return !validationError && errors.length === 0; + } + + rawValidation(schema: S, formData?: T): { errors?: Result[]; validationError?: Error } { + const { errors, validationError } = rawValidate(schema, formData); + return { errors: errors as unknown as Result[], validationError }; + } +} + +const cspSafeValidator = new CspSafeValidator(); + +export default cspSafeValidator; +export { CspSafeValidator }; diff --git a/packages/shared-ui/src/components/canvas/util/rjsf.ts b/packages/shared-ui/src/components/canvas/util/rjsf.ts index 64b23efc5..4e229a093 100644 --- a/packages/shared-ui/src/components/canvas/util/rjsf.ts +++ b/packages/shared-ui/src/components/canvas/util/rjsf.ts @@ -24,7 +24,7 @@ import { RJSFSchema, UiSchema } from '@rjsf/utils'; import { ValidationData } from '@rjsf/utils'; import { getDefaultFormState as RJSFGetDefaultFormState } from '@rjsf/utils'; -import validator from '@rjsf/validator-ajv8'; +import validator from './csp-safe-validator'; import { traverseObject } from './traverse-object'; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26d24aaa7..53761fc7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -398,6 +398,9 @@ importers: packages/shared-ui: dependencies: + '@cfworker/json-schema': + specifier: ^4.1.1 + version: 4.1.1 '@dnd-kit/core': specifier: ~6.1.0 version: 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -924,6 +927,9 @@ packages: '@bufbuild/protobuf@2.11.0': resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==} + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -8389,6 +8395,8 @@ snapshots: '@bufbuild/protobuf@2.11.0': {} + '@cfworker/json-schema@4.1.1': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9