From e31bfc24a3d730b824f71e3d22bef4f4ce216f48 Mon Sep 17 00:00:00 2001 From: Chandrajeet Singh Date: Wed, 8 Jul 2026 15:34:53 +0530 Subject: [PATCH 1/3] feat(eng-821): setup and config field validation --- package.json | 2 +- scripts/check-format-vectors.ts | 68 +++++++++++ src/modules/integration-picker/types.ts | 26 ++++- .../integration-picker/utils/zodSchema.ts | 106 ++++++++++++++---- 4 files changed, 172 insertions(+), 30 deletions(-) create mode 100644 scripts/check-format-vectors.ts diff --git a/package.json b/package.json index 4a88ee0..f353acc 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "./dist/webcomponent.js" ], "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "npx -y tsx scripts/check-format-vectors.ts", "build": "rollup -c", "dev": "cd dev/vite && npm run dev", "dev:setup": "npm run build && cd dev/vite && npm install", diff --git a/scripts/check-format-vectors.ts b/scripts/check-format-vectors.ts new file mode 100644 index 0000000..98e9222 --- /dev/null +++ b/scripts/check-format-vectors.ts @@ -0,0 +1,68 @@ +/** + * Asserts the local FORMAT_PATTERNS mirror against the canonical format accept/reject + * vectors — copied from `@stackone/core` `FORMAT_PATTERN_TEST_VECTORS` (connect repo, + * `packages/core/src/connector/formatPatterns.ts`). + * + * The build-time registry and every runtime mirror (DynamicForm, the embedded widget, + * this hub) must pass exactly these vectors, so `stackone validate` and the browser can + * never disagree about what a format accepts. Keep in sync when adding a format + * (field-validation RFC, Q3). Run via `npm test`. + */ +import { FORMAT_PATTERNS } from '../src/modules/integration-picker/utils/zodSchema'; + +const FORMAT_PATTERN_TEST_VECTORS: Record = { + email: { + accepts: ['john@example.com', 'a.b+tag@sub.domain.co'], + rejects: ['not-an-email', 'a b@example.com', '@example.com', 'john@'], + }, + url: { + accepts: ['https://api.example.com', 'http://x.io/path?q=1'], + rejects: ['example.com', 'ftp://host'], + }, + uri: { + accepts: ['https://api.example.com', 'mailto:x@y.z', 'urn:isbn:0451450523'], + rejects: ['no-scheme-here', '://missing'], + }, + uuid: { + accepts: ['123e4567-e89b-12d3-a456-426614174000', '123E4567-E89B-12D3-A456-426614174000'], + rejects: ['123e4567', 'zzze4567-e89b-12d3-a456-426614174000'], + }, + date: { + accepts: ['2026-07-06', '1999-12-31'], + rejects: ['06-07-2026', '2026/07/06', '2026-7-6'], + }, + datetime: { + accepts: ['2026-07-06T10:30:00', '2026-07-06T10:30:00Z', '2026-07-06T10:30:00+01:00'], + rejects: ['2026-07-06', '10:30:00'], + }, +}; + +let failures = 0; + +for (const [format, vectors] of Object.entries(FORMAT_PATTERN_TEST_VECTORS)) { + const pattern = FORMAT_PATTERNS[format]; + if (!pattern) { + failures++; + console.error(`FAIL: registry is missing format "${format}"`); + continue; + } + for (const value of vectors.accepts) { + if (!pattern.test(value)) { + failures++; + console.error(`FAIL: ${format} should accept "${value}"`); + } + } + for (const value of vectors.rejects) { + if (pattern.test(value)) { + failures++; + console.error(`FAIL: ${format} should reject "${value}"`); + } + } +} + +if (failures > 0) { + console.error(`${failures} format vector failure(s)`); + process.exit(1); +} + +console.log('All format vectors pass'); diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index bf7543c..dcd1a08 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -17,6 +17,26 @@ export interface HubData { events_encoded_context?: string; } +// V2/legacy TS connectors — always discriminated by the required `type` on the wire; message field is `error` +export interface LegacyFieldValidation { + type: 'html-pattern' | 'domain'; + pattern: string; + error?: string; + format?: never; + errorMessage?: never; +} + +// Falcon connectors — no `type`; pattern and format are mutually exclusive (enforced in connect-sdk); message field is `errorMessage` +export interface FalconFieldValidation { + type?: never; + error?: never; + pattern?: string; + format?: 'email' | 'url' | 'uuid' | 'date' | 'datetime' | 'uri'; + errorMessage?: string; +} + +export type FieldValidation = LegacyFieldValidation | FalconFieldValidation; + export interface ConnectorConfigField { type?: 'text' | 'password' | 'number' | 'select' | 'text_area'; label: string; @@ -37,11 +57,7 @@ export interface ConnectorConfigField { }; value?: string | number; condition?: string; - validation?: { - type: 'html-pattern' | 'domain'; - pattern: string; - error?: string; - }; + validation?: FieldValidation; display?: boolean; } diff --git a/src/modules/integration-picker/utils/zodSchema.ts b/src/modules/integration-picker/utils/zodSchema.ts index 6e399da..492bc8d 100644 --- a/src/modules/integration-picker/utils/zodSchema.ts +++ b/src/modules/integration-picker/utils/zodSchema.ts @@ -1,5 +1,78 @@ import { z } from 'zod'; -import { ConnectorConfigField } from '../types'; +import { + ConnectorConfigField, + FalconFieldValidation, + FieldValidation, + LegacyFieldValidation, +} from '../types'; + +// Mirrored from @stackone/core — centralising this registry is tracked in the +// field-validation RFC. Exported so scripts/check-format-vectors.ts can assert it +// against the canonical format test vectors (run via `npm test`). +export const FORMAT_PATTERNS: Record = { + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, + url: /^https?:\/\/\S+$/, + uri: /^[a-zA-Z][a-zA-Z\d+.-]*:\S*$/, + uuid: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, + date: /^\d{4}-\d{2}-\d{2}$/, + datetime: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$/, +}; + +interface ValidationRule { + pattern: RegExp; + errorMessage: string; +} + +function isLegacyValidation(validation: FieldValidation): validation is LegacyFieldValidation { + return validation.type !== undefined; +} + +// V2/legacy TS connectors — behaviour preserved as-is, delete wholesale when V2 retires +function resolveLegacyRule(validation: LegacyFieldValidation): ValidationRule | null { + if (validation.type === 'html-pattern') { + return { + pattern: new RegExp(validation.pattern), + errorMessage: + validation.error || `Please match the required format: ${validation.pattern}`, + }; + } + + if (validation.type === 'domain') { + return { + pattern: new RegExp(`.*${validation.pattern}\\.com.*`), + errorMessage: + validation.error || `Please enter a valid ${validation.pattern}.com domain`, + }; + } + + return null; +} + +function resolveFalconRule(validation: FalconFieldValidation, label: string): ValidationRule | null { + if (validation.format && FORMAT_PATTERNS[validation.format]) { + return { + pattern: FORMAT_PATTERNS[validation.format], + errorMessage: validation.errorMessage || `Must be a valid ${validation.format}`, + }; + } + + if (validation.pattern) { + return { + pattern: new RegExp(validation.pattern), + errorMessage: validation.errorMessage || `${label} format is invalid`, + }; + } + + return null; +} + +function resolveValidationRule(field: ConnectorConfigField): ValidationRule | null { + if (!field.validation) return null; + + return isLegacyValidation(field.validation) + ? resolveLegacyRule(field.validation) + : resolveFalconRule(field.validation, field.label); +} function createFieldSchema(field: ConnectorConfigField): z.ZodTypeAny { let schema: z.ZodString = z.string(); @@ -18,29 +91,14 @@ function createFieldSchema(field: ConnectorConfigField): z.ZodTypeAny { } } - if (field.validation) { - if (field.validation.type === 'html-pattern') { - const pattern = new RegExp(field.validation.pattern); - const errorMessage = - field.validation.error || - `Please match the required format: ${field.validation.pattern}`; - - if (field.required) { - schema = schema.regex(pattern, errorMessage); - } else { - return z.string().refine((val) => val === '' || pattern.test(val), errorMessage); - } - } else if (field.validation.type === 'domain') { - const pattern = new RegExp(`.*${field.validation.pattern}\\.com.*`); - const errorMessage = - field.validation.error || - `Please enter a valid ${field.validation.pattern}.com domain`; - - if (field.required) { - schema = schema.regex(pattern, errorMessage); - } else { - return z.string().refine((val) => val === '' || pattern.test(val), errorMessage); - } + const rule = resolveValidationRule(field); + if (rule) { + if (field.required) { + schema = schema.regex(rule.pattern, rule.errorMessage); + } else { + return z + .string() + .refine((val) => val === '' || rule.pattern.test(val), rule.errorMessage); } } From 5fdd895b800c597ea5aaa6656323f44d2b191aa1 Mon Sep 17 00:00:00 2001 From: Chandrajeet Singh Date: Wed, 8 Jul 2026 15:48:51 +0530 Subject: [PATCH 2/3] lint fix --- src/modules/integration-picker/utils/zodSchema.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/integration-picker/utils/zodSchema.ts b/src/modules/integration-picker/utils/zodSchema.ts index 492bc8d..85296d4 100644 --- a/src/modules/integration-picker/utils/zodSchema.ts +++ b/src/modules/integration-picker/utils/zodSchema.ts @@ -48,7 +48,10 @@ function resolveLegacyRule(validation: LegacyFieldValidation): ValidationRule | return null; } -function resolveFalconRule(validation: FalconFieldValidation, label: string): ValidationRule | null { +function resolveFalconRule( + validation: FalconFieldValidation, + label: string, +): ValidationRule | null { if (validation.format && FORMAT_PATTERNS[validation.format]) { return { pattern: FORMAT_PATTERNS[validation.format], From d9209721c6d80f9799ff60d3c50e276e79b6afd6 Mon Sep 17 00:00:00 2001 From: Chandrajeet Singh Date: Fri, 10 Jul 2026 14:45:57 +0530 Subject: [PATCH 3/3] feat(eng-821): align format patterns copy with canonical registry - Anchor the local FORMAT_PATTERNS copy (url/uri; datetime requires seconds, optional fraction/offset) to match @stackone/core - Add local FormatName copy and tighten FalconFieldValidation to the XOR shape matching the authoring contract - Sync canonical accept/reject vectors in check-format-vectors (npm test) Co-Authored-By: Claude Fable 5 --- scripts/check-format-vectors.ts | 43 +++++++++++++------ src/modules/integration-picker/types.ts | 21 +++++---- .../integration-picker/utils/zodSchema.ts | 18 +++++--- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/scripts/check-format-vectors.ts b/scripts/check-format-vectors.ts index 98e9222..237eb3f 100644 --- a/scripts/check-format-vectors.ts +++ b/scripts/check-format-vectors.ts @@ -1,12 +1,12 @@ /** - * Asserts the local FORMAT_PATTERNS mirror against the canonical format accept/reject + * Asserts the local FORMAT_PATTERNS copy against the canonical format accept/reject * vectors — copied from `@stackone/core` `FORMAT_PATTERN_TEST_VECTORS` (connect repo, - * `packages/core/src/connector/formatPatterns.ts`). + * `packages/core/src/connector/specs/formatPatterns.vectors.ts`). * - * The build-time registry and every runtime mirror (DynamicForm, the embedded widget, - * this hub) must pass exactly these vectors, so `stackone validate` and the browser can - * never disagree about what a format accepts. Keep in sync when adding a format - * (field-validation RFC, Q3). Run via `npm test`. + * The canonical registry and this local copy must pass exactly these vectors, so + * `stackone validate` and this hub can never disagree about what a format accepts. + * Keep both the registry copy (utils/zodSchema.ts) and these vectors in sync when a + * format changes. Run via `npm test`. */ import { FORMAT_PATTERNS } from '../src/modules/integration-picker/utils/zodSchema'; @@ -17,30 +17,47 @@ const FORMAT_PATTERN_TEST_VECTORS: Record = { +// Local copy of the canonical `FORMAT_PATTERNS` registry from `@stackone/core` +// (connect repo, `packages/core/src/connector/formatPatterns.ts`) — the hub +// deliberately carries no @stackone package dependencies for this feature. Keep in +// sync when a format changes; `scripts/check-format-vectors.ts` (run via `npm test`) +// asserts this copy against the canonical accept/reject vectors so a drifted copy +// fails CI. Exported for that script. +export const FORMAT_PATTERNS: Record = { email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, url: /^https?:\/\/\S+$/, - uri: /^[a-zA-Z][a-zA-Z\d+.-]*:\S*$/, - uuid: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/, + uri: /^[a-zA-Z][a-zA-Z0-9+.-]*:.+$/, + uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, date: /^\d{4}-\d{2}-\d{2}$/, - datetime: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?$/, + datetime: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/, }; interface ValidationRule {