feat: validate shortlink aliases

This commit is contained in:
Hermes Agent
2026-06-20 10:16:13 +10:00
parent e6053b8114
commit 0e3e80539e
2 changed files with 90 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest';
import { ALIAS_MAX_LENGTH, normalizeAlias, validateAlias } from '../worker/lib/aliases';
describe('alias normalization', () => {
it('lowercases aliases', () => {
expect(normalizeAlias('Claude')).toBe('claude');
});
it('trims aliases', () => {
expect(normalizeAlias(' claude ')).toBe('claude');
});
it('keeps hyphen and underscore', () => {
expect(normalizeAlias('my_link-1')).toBe('my_link-1');
});
});
describe('alias validation', () => {
it('rejects empty string with exact error', () => {
expect(validateAlias('')).toEqual({ ok: false, error: 'Alias is required' });
expect(validateAlias(' ')).toEqual({ ok: false, error: 'Alias is required' });
});
it('rejects path separators with exact error', () => {
expect(validateAlias('a/b')).toEqual({
ok: false,
error: 'Alias may contain only letters, numbers, underscore, and hyphen',
});
});
it('rejects aliases starting with hyphen or underscore', () => {
expect(validateAlias('-alias')).toEqual({
ok: false,
error: 'Alias may contain only letters, numbers, underscore, and hyphen',
});
expect(validateAlias('_alias')).toEqual({
ok: false,
error: 'Alias may contain only letters, numbers, underscore, and hyphen',
});
});
it('accepts length 100 and rejects length 101', () => {
const validAlias = 'a'.repeat(ALIAS_MAX_LENGTH);
const tooLongAlias = 'a'.repeat(ALIAS_MAX_LENGTH + 1);
expect(validateAlias(validAlias)).toEqual({ ok: true, value: validAlias });
expect(validateAlias(tooLongAlias)).toEqual({
ok: false,
error: 'Alias must be 100 characters or fewer',
});
});
});
+38
View File
@@ -0,0 +1,38 @@
export const ALIAS_MAX_LENGTH = 100;
const ALIAS_INVALID_ERROR = 'Alias may contain only letters, numbers, underscore, and hyphen';
const ALIAS_PATTERN = /^[a-z0-9][a-z0-9_-]{0,99}$/;
type AliasValidationSuccess = {
ok: true;
value: string;
};
type AliasValidationFailure = {
ok: false;
error: string;
};
export type ValidationResult = AliasValidationSuccess | AliasValidationFailure;
export function normalizeAlias(input: string): string {
return input.trim().replace(/[A-Z]/g, (character) => character.toLowerCase());
}
export function validateAlias(input: string): ValidationResult {
const value = normalizeAlias(input);
if (value.length === 0) {
return { ok: false, error: 'Alias is required' };
}
if (value.length > ALIAS_MAX_LENGTH) {
return { ok: false, error: `Alias must be ${ALIAS_MAX_LENGTH} characters or fewer` };
}
if (!ALIAS_PATTERN.test(value)) {
return { ok: false, error: ALIAS_INVALID_ERROR };
}
return { ok: true, value };
}