Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ jobs:
run: npm ci
- name: Build
run: npm run build
- name: Lint
- name: Lint
run: npm run lint
- name: Test (format-vector conformance)
run: npm test
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
109 changes: 109 additions & 0 deletions scripts/check-format-vectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* 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';
Comment thread
chandrajeet-singh marked this conversation as resolved.

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',
'http://localhost:3000',
'https://1.2.3.4/p?q=1',
'https://x.io/a?b=c#d',
],
rejects: [
'example.com',
'ftp://host',
'',
'https://api.example.com extra text',
'https://foo bar/baz',
'https://',
'https://?q=1',
'https:///path',
'https://#frag',
'https://a b',
],
},
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',
'2026-07-06T10:30:00z',
'2026-07-06T10:30:00+0100',
'2026-07-06T10:30:00+01',
],
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 thread
chandrajeet-singh marked this conversation as resolved.
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}"`);
}
}
}

// Reverse coverage: a format added to the registry (mirroring a new core format) with no
// vectors here would otherwise pass silently — the exact drift D3 exists to catch (a field
// with an unrecognised format renders unvalidated). Fail if any registry key lacks vectors.
for (const format of Object.keys(FORMAT_PATTERNS)) {
if (!FORMAT_PATTERN_TEST_VECTORS[format]) {
failures++;
console.error(`FAIL: no vectors for registry format "${format}"`);
}
}

if (failures > 0) {
console.error(`${failures} format vector failure(s)`);
process.exit(1);
}

console.log('All format vectors pass');
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ interface IntegrationFieldsProps {
onChange: (data: Record<string, string>) => void;
onValidationChange?: (isValid: boolean) => void;
integrationName: string;
connectorKey?: string;
editingSecrets?: Set<string>;
setEditingSecrets?: (updater: (prev: Set<string>) => Set<string>) => void;
}
Expand Down Expand Up @@ -277,6 +278,7 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
error,
onValidationChange,
integrationName,
connectorKey,
editingSecrets,
setEditingSecrets,
}) => {
Expand All @@ -285,7 +287,7 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
typeof f.key === 'object' ? JSON.stringify(f.key) : String(f.key),
);
const { noticesBefore, noticesAfter } = partitionNotices(notices, fieldKeys);
const schema = useMemo(() => createFormSchema(fields), [fields]);
const schema = useMemo(() => createFormSchema(fields, connectorKey), [fields, connectorKey]);

const defaultValues = useMemo(() => {
const initialData: Record<string, string> = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const IntegrationPickerContent: React.FC<IntegrationPickerContentProps> =
onChange={onChange}
onValidationChange={onValidationChange}
integrationName={connectorData.name}
connectorKey={connectorData.key}
editingSecrets={editingSecrets}
setEditingSecrets={setEditingSecrets}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface IntegrationFormViewProps {
onChange: (data: Record<string, string>) => void;
onValidationChange?: (isValid: boolean) => void;
integrationName: string;
connectorKey?: string;
editingSecrets?: Set<string>;
setEditingSecrets?: (updater: (prev: Set<string>) => Set<string>) => void;
}
Expand All @@ -23,6 +24,7 @@ export const IntegrationFormView: React.FC<IntegrationFormViewProps> = ({
onChange,
onValidationChange,
integrationName,
connectorKey,
editingSecrets,
setEditingSecrets,
}) => {
Expand All @@ -34,6 +36,7 @@ export const IntegrationFormView: React.FC<IntegrationFormViewProps> = ({
onChange={onChange}
onValidationChange={onValidationChange}
integrationName={integrationName}
connectorKey={connectorKey}
editingSecrets={editingSecrets}
setEditingSecrets={setEditingSecrets}
/>
Expand Down
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;
Comment thread
chandrajeet-singh marked this conversation as resolved.
display?: boolean;
}

Expand Down
Loading
Loading