Home > Documentation > Security > Message Validation
Message Validation
Complete guide to validating message payloads in ParleyJS for secure cross-window communication.
Table of Contents
- Overview
- Why Validate Messages
- JSON Schema Validation
- Type Validation
- Content Sanitization
- Validation Libraries
- Error Handling
- Security Best Practices
- Complete Examples
Overview
Message validation ensures that incoming payloads match expected structures and types before processing. ParleyJS provides built-in validation support through JSON Schema and custom validators.
Validation prevents:
- Type errors from malformed data
- Security vulnerabilities from unexpected content
- Application crashes from invalid payloads
- Data corruption from incorrect types
Why Validate Messages
Cross-window communication involves untrusted data sources. Always validate messages to protect your application.
Security Risks Without Validation
XSS via Messages:
// DANGEROUS - No validation
parley.on('display-message', (payload, respond) => {
document.getElementById('output').innerHTML = payload.html; // XSS!
});2
3
4
Type Confusion:
// DANGEROUS - Assumes number
parley.on('set-quantity', (payload, respond) => {
const total = payload.quantity * price; // What if quantity is a string?
});2
3
4
Prototype Pollution:
// DANGEROUS - No validation
parley.on('update-settings', (payload, respond) => {
Object.assign(settings, payload); // Could pollute Object.prototype!
});2
3
4
Benefits of Validation
- Catches errors early before data processing
- Provides clear error messages for debugging
- Documents expected message structure
- Prevents security vulnerabilities
- Enables type-safe communication
JSON Schema Validation
ParleyJS supports JSON Schema for declarative validation.
Basic Schema Validation
import { Parley } from 'parley-js';
const parley = Parley.create({
allowedOrigins: [window.location.origin],
});
// Define JSON Schema for user data
const userSchema = {
type: 'object',
properties: {
userId: { type: 'number' },
username: { type: 'string', minLength: 3, maxLength: 20 },
email: { type: 'string', format: 'email' },
role: { type: 'string', enum: ['admin', 'user', 'guest'] },
},
required: ['userId', 'username', 'email'],
additionalProperties: false,
};
// Register handler with schema validation
parley.on(
'create-user',
(payload, respond, metadata) => {
// Schema validated automatically by ParleyJS
const { userId, username, email, role } = payload;
// Process validated data safely
const user = createUser({ userId, username, email, role });
respond({ success: true, user });
},
{
schema: userSchema,
}
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Schema Validation with SchemaValidator
import { SchemaValidator } from 'parley-js';
const validator = new SchemaValidator();
// Define schema
const messageSchema = {
type: 'object',
properties: {
action: { type: 'string' },
payload: {
type: 'object',
properties: {
id: { type: 'number' },
data: { type: 'string' },
},
required: ['id'],
},
},
required: ['action', 'payload'],
};
// Validate manually
parley.on('process-action', (payload, respond) => {
const result = validator.validate(payload, messageSchema);
if (!result.valid) {
throw new ValidationError('Invalid payload', result.errors);
}
// Process validated payload
handleAction(payload);
respond({ success: true });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Common JSON Schema Patterns
Enum Values:
const schema = {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['pending', 'active', 'completed', 'cancelled'],
},
},
};2
3
4
5
6
7
8
9
Array Validation:
const schema = {
type: 'object',
properties: {
items: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
},
required: ['id', 'name'],
},
minItems: 1,
maxItems: 100,
},
},
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Nested Objects:
const schema = {
type: 'object',
properties: {
user: {
type: 'object',
properties: {
profile: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number', minimum: 0, maximum: 150 },
},
},
},
},
},
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Type Validation
TypeScript provides compile-time type checking. Combine with runtime validation for complete safety.
TypeScript Interface Validation
interface UpdateRequest {
userId: number;
updates: {
email?: string;
name?: string;
};
}
interface UpdateResponse {
success: boolean;
user?: {
userId: number;
email: string;
name: string;
};
error?: string;
}
// TypeScript ensures type safety
parley.on<UpdateRequest>(
'update-user',
async (payload, respond: ResponseFunction<UpdateResponse>) => {
// Runtime validation
if (typeof payload.userId !== 'number') {
respond({ success: false, error: 'Invalid userId type' });
return;
}
if (payload.userId <= 0) {
respond({ success: false, error: 'userId must be positive' });
return;
}
// Process validated payload
const user = await updateUser(payload.userId, payload.updates);
respond({ success: true, user });
}
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Custom Type Guards
interface UserPayload {
userId: number;
username: string;
}
function isUserPayload(payload: unknown): payload is UserPayload {
return (
typeof payload === 'object' &&
payload !== null &&
'userId' in payload &&
'username' in payload &&
typeof (payload as UserPayload).userId === 'number' &&
typeof (payload as UserPayload).username === 'string' &&
(payload as UserPayload).username.length > 0
);
}
parley.on('process-user', (payload, respond) => {
if (!isUserPayload(payload)) {
throw new ValidationError('Invalid user payload');
}
// TypeScript knows payload is UserPayload here
const { userId, username } = payload;
processUser(userId, username);
respond({ success: true });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Content Sanitization
Sanitize HTML and dangerous content before processing or displaying messages.
HTML Sanitization
import DOMPurify from 'dompurify'; // External library
parley.on('display-content', (payload, respond) => {
// NEVER trust HTML from messages
const sanitized = DOMPurify.sanitize(payload.html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
ALLOWED_ATTR: [],
});
document.getElementById('content').innerHTML = sanitized;
respond({ success: true });
});2
3
4
5
6
7
8
9
10
11
12
String Sanitization
function sanitizeString(input: unknown): string {
if (typeof input !== 'string') {
throw new ValidationError('Input must be a string');
}
// Remove null bytes
let sanitized = input.replace(/\0/g, '');
// Limit length
if (sanitized.length > 1000) {
sanitized = sanitized.substring(0, 1000);
}
// Remove control characters except newline/tab
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
return sanitized;
}
parley.on('submit-comment', (payload, respond) => {
const comment = sanitizeString(payload.text);
saveComment(comment);
respond({ success: true });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
URL Validation
function isValidUrl(url: unknown): url is string {
if (typeof url !== 'string') return false;
try {
const parsed = new URL(url);
// Only allow http/https protocols
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
}
parley.on('open-link', (payload, respond) => {
if (!isValidUrl(payload.url)) {
respond({ success: false, error: 'Invalid URL' });
return;
}
window.open(payload.url, '_blank', 'noopener,noreferrer');
respond({ success: true });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Validation Libraries
Integrate popular validation libraries with ParleyJS.
Zod
import { z } from 'zod';
const userSchema = z.object({
userId: z.number().int().positive(),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['admin', 'user', 'guest']),
});
type User = z.infer<typeof userSchema>;
parley.on<unknown>('create-user', (payload, respond) => {
try {
// Zod validates and transforms
const user = userSchema.parse(payload);
// user is now typed as User
createUser(user);
respond({ success: true, user });
} catch (error) {
if (error instanceof z.ZodError) {
respond({
success: false,
errors: error.errors.map((e) => ({
path: e.path.join('.'),
message: e.message,
})),
});
}
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Yup
import * as yup from 'yup';
const messageSchema = yup.object({
type: yup.string().required().oneOf(['info', 'warning', 'error']),
message: yup.string().required().min(1).max(500),
timestamp: yup.number().required().positive(),
});
parley.on('log-message', async (payload, respond) => {
try {
const validated = await messageSchema.validate(payload, {
abortEarly: false,
stripUnknown: true,
});
logMessage(validated);
respond({ success: true });
} catch (error) {
if (error instanceof yup.ValidationError) {
respond({ success: false, errors: error.errors });
}
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Joi
import Joi from 'joi';
const configSchema = Joi.object({
apiKey: Joi.string().alphanum().length(32).required(),
endpoint: Joi.string().uri().required(),
timeout: Joi.number().integer().min(1000).max(30000).default(5000),
retries: Joi.number().integer().min(0).max(5).default(3),
});
parley.on('update-config', (payload, respond) => {
const { error, value } = configSchema.validate(payload, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
respond({
success: false,
errors: error.details.map((d) => d.message),
});
return;
}
updateConfig(value);
respond({ success: true, config: value });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Error Handling
Handle validation errors gracefully and provide helpful feedback.
ValidationError Handling
import { ValidationError } from 'parley-js';
parley.on('process-data', (payload, respond) => {
try {
validatePayload(payload);
processData(payload);
respond({ success: true });
} catch (error) {
if (error instanceof ValidationError) {
// Validation error with details
respond({
success: false,
error: 'Validation failed',
details: error.validationErrors,
});
} else {
// Unexpected error
respond({
success: false,
error: 'Processing failed',
});
}
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Detailed Error Reporting
function validateAndRespond(
payload: unknown,
schema: object,
respond: ResponseFunction
) {
const result = validator.validate(payload, schema);
if (!result.valid) {
const errors = result.errors.map((err) => ({
field: err.field,
message: err.message,
expected: err.expected,
received: err.received,
}));
respond({
success: false,
validationErrors: errors,
});
return false;
}
return true;
}
parley.on('submit-form', (payload, respond) => {
if (!validateAndRespond(payload, formSchema, respond)) {
return; // Validation failed, error already sent
}
// Process validated payload
submitForm(payload);
respond({ success: true });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Security Best Practices
Validate Everything
// ALWAYS validate before processing
parley.on('update-settings', (payload, respond) => {
// 1. Type validation
if (typeof payload !== 'object' || payload === null) {
throw new ValidationError('Payload must be an object');
}
// 2. Schema validation
const result = validator.validate(payload, settingsSchema);
if (!result.valid) {
throw new ValidationError('Schema validation failed', result.errors);
}
// 3. Business logic validation
if (payload.maxUsers < payload.currentUsers) {
respond({
success: false,
error: 'maxUsers cannot be less than currentUsers',
});
return;
}
// 4. Sanitization
const sanitized = sanitizeSettings(payload);
// 5. Process
updateSettings(sanitized);
respond({ success: true });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Whitelist, Not Blacklist
// GOOD - Whitelist allowed fields
function extractAllowedFields(payload: unknown): Settings {
if (typeof payload !== 'object' || payload === null) {
throw new ValidationError('Invalid payload');
}
const allowed: Settings = {
theme: isValidTheme(payload.theme) ? payload.theme : 'light',
language: isValidLanguage(payload.language) ? payload.language : 'en',
notifications:
typeof payload.notifications === 'boolean'
? payload.notifications
: true,
};
return allowed;
}
// BAD - Trying to blacklist dangerous fields (incomplete)
function removeProtoFields(payload: object) {
delete payload.__proto__;
delete payload.constructor;
return payload; // Still vulnerable!
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Size Limits
const MAX_PAYLOAD_SIZE = 1024 * 100; // 100 KB
parley.on('upload-data', (payload, respond) => {
const payloadSize = JSON.stringify(payload).length;
if (payloadSize > MAX_PAYLOAD_SIZE) {
respond({
success: false,
error: `Payload too large: ${payloadSize} bytes (max: ${MAX_PAYLOAD_SIZE})`,
});
return;
}
processData(payload);
respond({ success: true });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Reject Unknown Properties
const strictSchema = {
type: 'object',
properties: {
action: { type: 'string' },
data: { type: 'object' },
},
required: ['action'],
additionalProperties: false, // Reject unknown properties
};2
3
4
5
6
7
8
9
Complete Examples
Secure User Registration
import { Parley, ValidationError } from 'parley-js';
import { z } from 'zod';
const registerSchema = z.object({
username: z
.string()
.min(3)
.max(20)
.regex(/^[a-zA-Z0-9_]+$/),
email: z.string().email(),
password: z.string().min(8).max(100),
agreeToTerms: z.boolean().refine((val) => val === true, {
message: 'Must agree to terms',
}),
});
const parley = Parley.create({
allowedOrigins: ['https://app.example.com'],
});
parley.on('register-user', async (payload, respond) => {
try {
// Validate with Zod
const validated = registerSchema.parse(payload);
// Additional business logic validation
const userExists = await checkUserExists(validated.username);
if (userExists) {
respond({
success: false,
error: 'Username already taken',
});
return;
}
// Hash password before storing
const hashedPassword = await hashPassword(validated.password);
// Create user
const user = await createUser({
username: validated.username,
email: validated.email,
password: hashedPassword,
});
respond({ success: true, userId: user.id });
} catch (error) {
if (error instanceof z.ZodError) {
respond({
success: false,
validationErrors: error.errors.map((e) => ({
field: e.path.join('.'),
message: e.message,
})),
});
} else {
respond({
success: false,
error: 'Registration failed',
});
}
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Safe Data Processing
const dataSchema = {
type: 'object',
properties: {
items: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string', pattern: '^[a-zA-Z0-9-]+$' },
value: { type: 'number', minimum: 0 },
label: { type: 'string', maxLength: 100 },
},
required: ['id', 'value'],
additionalProperties: false,
},
maxItems: 1000,
},
},
required: ['items'],
additionalProperties: false,
};
parley.on('process-batch', (payload, respond) => {
// Schema validation
const result = validator.validate(payload, dataSchema);
if (!result.valid) {
respond({
success: false,
errors: result.errors,
});
return;
}
// Additional validation
const uniqueIds = new Set(payload.items.map((item) => item.id));
if (uniqueIds.size !== payload.items.length) {
respond({
success: false,
error: 'Duplicate IDs found',
});
return;
}
// Process validated data
const results = payload.items.map((item) => processItem(item));
respond({ success: true, results });
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Related Documentation
- Origin Validation - Validate message origins
- Error Handling Pattern - Handle validation errors
- API Reference: Methods - Schema validation in handlers
- Security Guide - Complete security overview
Navigation
Previous: Origin Validation Next: Security Best Practices Related: Error Handling
Back to: Security | Documentation Home
