Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/shared-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"./constants/*": "./src/constants/*"
},
"dependencies": {
"lucide-react": "^0.540.0"
"lucide-react": "^0.540.0",
"@cfworker/json-schema": "^4.1.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
138 changes: 138 additions & 0 deletions packages/shared-ui/src/components/canvas/util/csp-safe-validator.ts
Original file line number Diff line number Diff line change
@@ -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 || '<root>'} ${unit.error}`.trim(),
};
}

function buildErrorSchema<T>(errors: RJSFValidationError[]): ErrorSchema<T> {
const root: ErrorSchema<T> = {} as ErrorSchema<T>;
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<T>(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<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
implements ValidatorType<T, S, F>
{
validateFormData(
formData: T | undefined,
schema: S,
customValidate?: CustomValidator<T, S, F>,
transformErrors?: ErrorTransformer<T, S, F>,
uiSchema?: UiSchema<T, S, F>
): ValidationData<T> {
const { errors: rawErrors, validationError } = rawValidate<T>(schema, formData);
const errors = transformErrors ? transformErrors(rawErrors, uiSchema) : rawErrors;
let errorSchema = buildErrorSchema<T>(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<T>;
}

toErrorList(errorSchema?: ErrorSchema<T>, fieldPath: string[] = []): RJSFValidationError[] {
return rjsfToErrorList<T>(errorSchema, fieldPath);
}

isValid(schema: S, formData: T | undefined, _rootSchema: S): boolean {
const { errors, validationError } = rawValidate<T>(schema, formData);
return !validationError && errors.length === 0;
}

rawValidation<Result = any>(schema: S, formData?: T): { errors?: Result[]; validationError?: Error } {
const { errors, validationError } = rawValidate<T>(schema, formData);
return { errors: errors as unknown as Result[], validationError };
}
}

const cspSafeValidator = new CspSafeValidator();

export default cspSafeValidator;
export { CspSafeValidator };
2 changes: 1 addition & 1 deletion packages/shared-ui/src/components/canvas/util/rjsf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading