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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
85 changes: 85 additions & 0 deletions scripts/check-format-vectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* 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/specs/formatPatterns.vectors.ts`).
*
* 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';

const FORMAT_PATTERN_TEST_VECTORS: Record<string, { accepts: string[]; rejects: string[] }> = {
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',
'',
'https://api.example.com extra text',
'https://foo bar/baz',
],
},
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',
'2026-07-06T10:30:00.123Z',
],
rejects: [
'2026-07-06',
'10:30:00',
'',
'2026-07-06T10:30:00banana',
'2026-07-06T10:30:00Zzz',
],
},
};

let failures = 0;

for (const [format, vectors] of Object.entries(FORMAT_PATTERN_TEST_VECTORS)) {
const pattern = FORMAT_PATTERNS[format as keyof typeof FORMAT_PATTERNS];
if (!pattern) {
failures++;
console.error(`FAIL: registry is missing format "${format}"`);
continue;
}
Comment on lines +59 to +65
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');
31 changes: 26 additions & 5 deletions src/modules/integration-picker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,31 @@ 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;
}

// Local copy of the format names from `@stackone/core`'s `InputFormat` (connect repo,
// `packages/core/src/connector/types.ts`) — the hub deliberately carries no @stackone
// package dependencies for this feature; keep in sync when a format is added. The
// FORMAT_PATTERNS copy in utils/zodSchema.ts and the vector check in
// scripts/check-format-vectors.ts guard the regexes themselves.
export type FormatName = 'email' | 'url' | 'uuid' | 'date' | 'datetime' | 'uri';

// Falcon connectors — no `type`; exactly one of pattern/format is set (XOR), message
// field is `errorMessage`. Local copy of `AuthenticationFieldValidation` from
// `@stackone/core` (connect repo) — keep in sync if the authoring contract changes.
export type FalconFieldValidation =
| { type?: never; error?: never; pattern: string; format?: never; errorMessage?: string }
| { type?: never; error?: never; format: FormatName; pattern?: never; errorMessage?: string };

export type FieldValidation = LegacyFieldValidation | FalconFieldValidation;

export interface ConnectorConfigField {
type?: 'text' | 'password' | 'number' | 'select' | 'text_area';
label: string;
Expand All @@ -37,11 +62,7 @@ export interface ConnectorConfigField {
};
value?: string | number;
condition?: string;
validation?: {
type: 'html-pattern' | 'domain';
pattern: string;
error?: string;
};
validation?: FieldValidation;
display?: boolean;
}

Expand Down
113 changes: 89 additions & 24 deletions src/modules/integration-picker/utils/zodSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,85 @@
import { z } from 'zod';
import { ConnectorConfigField } from '../types';
import {
ConnectorConfigField,
FalconFieldValidation,
FieldValidation,
FormatName,
LegacyFieldValidation,
} from '../types';

// 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<FormatName, RegExp> = {
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
url: /^https?:\/\/\S+$/,
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})?$/,
};

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}`,
};
}
Comment on lines +36 to +42

if (validation.type === 'domain') {
return {
pattern: new RegExp(`.*${validation.pattern}\\.com.*`),
errorMessage:
validation.error || `Please enter a valid ${validation.pattern}.com domain`,
};
}
Comment on lines +44 to +50

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`,
};
}
Comment on lines +66 to +71

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();
Expand All @@ -18,29 +98,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);
}
}

Expand Down
Loading