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
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';

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;
}
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