Skip to content
Merged
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
201 changes: 198 additions & 3 deletions src/components/SiweValidator/__tests__/lineBreakValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { LineBreakValidator } from '../lineBreakValidator';
import { ValidationEngine } from '../validationEngine';
import { SiweMessageParser } from '../parser';
import { FieldReplacer } from '../fieldReplacer';

describe('LineBreakValidator', () => {
const validMessage = `example.com wants you to sign in with your Ethereum account:
Expand All @@ -19,6 +21,7 @@ Expiration Time: 2025-08-19T05:16:33.555Z`;
describe('User Reported Scenario', () => {
test('detects extra line break between last two lines', () => {
// Add extra line break between Issued At and Expiration Time
// Note: Expiration Time is an optional field, so this should use EXTRA_LINE_BREAKS_BEFORE_OPTIONAL_FIELD
const messageWithExtraLineBreak = `example.com wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890

Expand All @@ -34,14 +37,14 @@ Expiration Time: 2025-08-19T05:16:33.555Z`;

const errors = LineBreakValidator.validateLineBreaks(messageWithExtraLineBreak);

// Should detect extra line break between fields
// Should detect extra line break before optional field (Expiration Time)
const extraLineBreakError = errors.find(e =>
e.code === 'EXTRA_LINE_BREAKS_BETWEEN_FIELDS'
e.code === 'EXTRA_LINE_BREAKS_BEFORE_OPTIONAL_FIELD'
);

expect(extraLineBreakError).toBeDefined();
expect(extraLineBreakError?.fixable).toBe(true);
expect(extraLineBreakError?.message).toContain('Extra empty lines between required fields');
expect(extraLineBreakError?.message).toContain('Extra empty lines before Expiration Time field');
});

test('full validation engine prioritizes line break errors over parsing errors', () => {
Expand Down Expand Up @@ -362,6 +365,198 @@ Not Before: 2025-08-19T05:06:33.555Z`;
});
});

describe('No Statement Line Break Handling (EIP-4361)', () => {
test('valid message with no statement and 2 empty lines should pass', () => {
const validNoStatement = `example.com wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890


URI: https://example.com
Version: 1
Chain ID: 1
Nonce: abc123defg4567
Issued At: 2025-08-19T05:06:33.555Z`;

const errors = LineBreakValidator.validateLineBreaks(validNoStatement);
const lineBreakErrors = errors.filter(e =>
e.code.includes('LINE_BREAK') || e.code.includes('EXTRA')
);
expect(lineBreakErrors).toHaveLength(0);
});

test('invalid message with no statement and only 1 empty line should fail', () => {
const invalidNoStatement = `example.com wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890

URI: https://example.com
Version: 1
Chain ID: 1
Nonce: abc123defg4567
Issued At: 2025-08-19T05:06:33.555Z`;

const errors = LineBreakValidator.validateLineBreaks(invalidNoStatement);
const missingLineError = errors.find(e => e.code === 'MISSING_LINE_BREAK_NO_STATEMENT');
expect(missingLineError).toBeDefined();
expect(missingLineError?.message).toContain('expected 2');
});

test('message with no statement and 3 empty lines should report extra', () => {
const tooManyLines = `example.com wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890



URI: https://example.com
Version: 1
Chain ID: 1
Nonce: abc123defg4567
Issued At: 2025-08-19T05:06:33.555Z`;

const errors = LineBreakValidator.validateLineBreaks(tooManyLines);
const extraLinesError = errors.find(e => e.code === 'EXTRA_LINE_BREAKS_BEFORE_URI');
expect(extraLinesError).toBeDefined();
expect(extraLinesError?.message).toContain('expected 2');
});

test('fixLineBreaks adds 2 empty lines when no statement', () => {
// Message with only 1 empty line (incorrect)
const invalidNoStatement = `example.com wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890

URI: https://example.com
Version: 1
Chain ID: 1
Nonce: abc123defg4567
Issued At: 2025-08-19T05:06:33.555Z`;

const fixed = LineBreakValidator.fixLineBreaks(invalidNoStatement);

// Should have 2 empty lines between address and URI
expect(fixed).toContain('0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890\n\n\nURI:');

// Validate the fixed message has no line break errors
const errors = LineBreakValidator.validateLineBreaks(fixed);
const lineBreakErrors = errors.filter(e =>
e.code.includes('LINE_BREAK') || e.code.includes('EXTRA')
);
expect(lineBreakErrors).toHaveLength(0);
});

test('SiweMessageParser.generateMessage outputs 2 empty lines when no statement', () => {
const fields = {
domain: 'example.com',
address: '0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890',
uri: 'https://example.com',
version: '1',
chainId: '1',
nonce: 'abc123defg4567',
issuedAt: '2025-08-19T05:06:33.555Z'
// No statement
};

const generated = SiweMessageParser.generateMessage(fields);

// Should have 2 empty lines between address and URI
expect(generated).toContain('0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890\n\n\nURI:');

// Validate the generated message has no line break errors
const errors = LineBreakValidator.validateLineBreaks(generated);
const lineBreakErrors = errors.filter(e =>
e.code.includes('LINE_BREAK') || e.code.includes('EXTRA')
);
expect(lineBreakErrors).toHaveLength(0);
});

test('SiweMessageParser.generateMessage outputs 1 empty line when statement exists', () => {
const fields = {
domain: 'example.com',
address: '0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890',
statement: 'Sign in to our Web3 application.',
uri: 'https://example.com',
version: '1',
chainId: '1',
nonce: 'abc123defg4567',
issuedAt: '2025-08-19T05:06:33.555Z'
};

const generated = SiweMessageParser.generateMessage(fields);

// Should have 1 empty line before statement, then statement, then 1 empty line before URI
expect(generated).toContain('0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890\n\nSign in');
expect(generated).toContain('application.\n\nURI:');

// Validate the generated message has no line break errors
const errors = LineBreakValidator.validateLineBreaks(generated);
const lineBreakErrors = errors.filter(e =>
e.code.includes('LINE_BREAK') || e.code.includes('EXTRA')
);
expect(lineBreakErrors).toHaveLength(0);
});

test('FieldReplacer.applyFieldFix handles MISSING_LINE_BREAK_NO_STATEMENT error', () => {
// Message with only 1 empty line (incorrect per EIP-4361)
const invalidNoStatement = `example.com wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890

URI: https://example.com
Version: 1
Chain ID: 1
Nonce: abc123defg4567
Issued At: 2025-08-19T05:06:33.555Z`;

const error = {
type: 'format' as const,
field: 'structure',
line: 3,
column: 1,
message: 'Missing empty line between address and URI field',
severity: 'error' as const,
fixable: true,
code: 'MISSING_LINE_BREAK_NO_STATEMENT'
};

const fixed = FieldReplacer.applyFieldFix(invalidNoStatement, error);

// Should have 2 empty lines between address and URI
expect(fixed).toContain('0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890\n\n\nURI:');

// Validate the fixed message has no line break errors
const errors = LineBreakValidator.validateLineBreaks(fixed);
const lineBreakErrors = errors.filter(e =>
e.code.includes('LINE_BREAK') || e.code.includes('EXTRA')
);
expect(lineBreakErrors).toHaveLength(0);
});

test('SiweMessageParser.parse correctly handles 2 empty lines when no statement', () => {
const validNoStatement = `example.com wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890


URI: https://example.com
Version: 1
Chain ID: 1
Nonce: abc123defg4567
Issued At: 2025-08-19T05:06:33.555Z`;

const parsed = SiweMessageParser.parse(validNoStatement);

// Should parse all fields correctly
expect(parsed.fields.domain).toBe('example.com');
expect(parsed.fields.address).toBe('0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890');
expect(parsed.fields.statement).toBeUndefined();
expect(parsed.fields.uri).toBe('https://example.com');
expect(parsed.fields.version).toBe('1');
expect(parsed.fields.chainId).toBe('1');
expect(parsed.fields.nonce).toBe('abc123defg4567');
expect(parsed.fields.issuedAt).toBe('2025-08-19T05:06:33.555Z');

// Should have no parse errors
expect(parsed.parseErrors).toHaveLength(0);
expect(parsed.isValid).toBe(true);
});
});

describe('Message Structure Analysis', () => {
test('correctly identifies all field positions', () => {
const structure = (LineBreakValidator as any).analyzeMessageStructure(validMessage.split('\n'));
Expand Down
1 change: 1 addition & 0 deletions src/components/SiweValidator/fieldReplacer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export class FieldReplacer {
case 'EXTRA_LINE_BREAKS_BEFORE_OPTIONAL_FIELD':
case 'MISSING_LINE_BREAK_ADDRESS_STATEMENT':
case 'MISSING_LINE_BREAK_STATEMENT_URI':
case 'MISSING_LINE_BREAK_NO_STATEMENT':
case 'TRAILING_WHITESPACE':
case 'TOO_MANY_CONSECUTIVE_EMPTY_LINES':
return LineBreakValidator.fixLineBreaks(originalMessage);
Expand Down
31 changes: 27 additions & 4 deletions src/components/SiweValidator/lineBreakValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,22 @@ export class LineBreakValidator {
}
}

// Check for extra empty lines before URI (should be exactly one after statement/address)
// Check for extra empty lines before URI
// Per EIP-4361: expect 1 empty line if statement exists, 2 empty lines if no statement
if (structure.uriIndex !== -1) {
const expectedEmptyLineIndex = this.getExpectedEmptyLineBeforeUri(lines, structure);
if (expectedEmptyLineIndex !== -1) {
// Count consecutive empty lines before URI
const emptyLinesBefore = this.countConsecutiveEmptyLinesBefore(lines, structure.uriIndex);
if (emptyLinesBefore > 1) {
const expectedEmptyLines = structure.statementIndex === -1 ? 2 : 1;
if (emptyLinesBefore > expectedEmptyLines) {
const extraLinesStart = structure.uriIndex - emptyLinesBefore + 1;
errors.push({
type: 'format',
field: 'structure',
line: extraLinesStart + 1,
column: 1,
message: `Extra empty lines before URI field (found ${emptyLinesBefore}, expected 1)`,
message: `Extra empty lines before URI field (found ${emptyLinesBefore}, expected ${expectedEmptyLines})`,
severity: 'error',
fixable: true,
suggestion: 'Remove extra empty lines before URI field',
Expand Down Expand Up @@ -190,6 +192,24 @@ export class LineBreakValidator {
}
}

// Check if there should be 2 empty lines when no statement (per EIP-4361)
if (structure.statementIndex === -1 && structure.addressIndex !== -1 && structure.uriIndex !== -1) {
const emptyLinesBefore = this.countConsecutiveEmptyLinesBefore(lines, structure.uriIndex);
if (emptyLinesBefore < 2) {
errors.push({
type: 'format',
field: 'structure',
line: structure.addressIndex + 2,
column: 1,
message: `Missing empty line between address and URI field (found ${emptyLinesBefore}, expected 2 when no statement)`,
severity: 'error',
fixable: true,
suggestion: 'Add a second empty line between the address and URI field when there is no statement',
code: 'MISSING_LINE_BREAK_NO_STATEMENT'
});
}
}

return errors;
}

Expand Down Expand Up @@ -394,9 +414,12 @@ export class LineBreakValidator {
fixedLines.push(line);
i++;

// Add empty line after address if there's a statement
// Per EIP-4361: add 1 empty line if there's a statement, 2 empty lines if no statement
if (structure.statementIndex !== -1) {
fixedLines.push('');
} else {
fixedLines.push('');
fixedLines.push('');
}
}
// Statement line
Expand Down
24 changes: 13 additions & 11 deletions src/components/SiweValidator/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,21 @@ export class SiweMessageParser {
));
}

// Skip empty line
// Skip empty line after address
if (lineIndex < lines.length && lines[lineIndex] === '') {
lineIndex++;
}

// Parse optional statement
// Parse optional statement (non-empty line that doesn't start with URI:)
if (lineIndex < lines.length && lines[lineIndex] && !lines[lineIndex].startsWith('URI:')) {
fields.statement = lines[lineIndex];
lineIndex++;

// Skip empty line after statement
if (lineIndex < lines.length && lines[lineIndex] === '') {
lineIndex++;
}
}

// Skip empty line(s) before required fields
// Per EIP-4361: 1 empty line after statement, or 2 empty lines total when no statement
while (lineIndex < lines.length && lines[lineIndex] === '') {
lineIndex++;
}

// Parse required fields
Expand Down Expand Up @@ -197,13 +198,14 @@ export class SiweMessageParser {
lines.push(fields.address);
}

// Empty line
lines.push('');

// Statement (optional)
// Per EIP-4361: 1 empty line + statement + 1 empty line, or 2 empty lines if no statement
if (fields.statement) {
lines.push('');
lines.push(fields.statement);
lines.push('');
} else {
lines.push('');
lines.push('');
}

// Required fields
Expand Down
2 changes: 2 additions & 0 deletions src/components/SiweValidator/validationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,11 @@ export class ValidationEngine {
// Generate sample messages for testing
public static generateSamples(): { [key: string]: string } {
return {
// Per EIP-4361: 2 empty lines when no statement
minimal: `example.com wants you to sign in with your Ethereum account:
0x742d35Cc6C4C1Ca5d428d9eE0e9B1E1234567890


URI: https://example.com
Version: 1
Chain ID: 1
Expand Down
Loading