A social knowledge tool for researchers built on ATProto
1import { ValueObject } from '../../../shared/domain/ValueObject';
2import { Result, ok, err } from '../../../shared/core/Result';
3
4export class InvalidHandleError extends Error {
5 constructor(message: string) {
6 super(message);
7 this.name = 'InvalidHandleError';
8 }
9}
10
11interface HandleProps {
12 value: string;
13}
14
15export class Handle extends ValueObject<HandleProps> {
16 get value(): string {
17 return this.props.value;
18 }
19
20 private constructor(props: HandleProps) {
21 super(props);
22 }
23
24 public static create(handle: string): Result<Handle, InvalidHandleError> {
25 if (!handle || handle.trim().length === 0) {
26 return err(new InvalidHandleError('Handle cannot be empty'));
27 }
28
29 const trimmedHandle = handle.trim();
30
31 // Basic domain validation - must contain at least one dot and valid characters
32 if (!Handle.isValidDomain(trimmedHandle)) {
33 return err(new InvalidHandleError('Handle must be a valid domain'));
34 }
35
36 return ok(new Handle({ value: trimmedHandle }));
37 }
38
39 private static isValidDomain(domain: string): boolean {
40 // Basic domain validation
41 // Must contain at least one dot
42 if (!domain.includes('.')) {
43 return false;
44 }
45
46 // Must not start or end with dot or hyphen
47 if (
48 domain.startsWith('.') ||
49 domain.endsWith('.') ||
50 domain.startsWith('-') ||
51 domain.endsWith('-')
52 ) {
53 return false;
54 }
55
56 // Split by dots and validate each part
57 const parts = domain.split('.');
58 if (parts.length < 2) {
59 return false;
60 }
61
62 for (const part of parts) {
63 if (!Handle.isValidDomainPart(part)) {
64 return false;
65 }
66 }
67
68 return true;
69 }
70
71 private static isValidDomainPart(part: string): boolean {
72 // Each part must be 1-63 characters
73 if (part.length === 0 || part.length > 63) {
74 return false;
75 }
76
77 // Must not start or end with hyphen
78 if (part.startsWith('-') || part.endsWith('-')) {
79 return false;
80 }
81
82 // Must contain only alphanumeric characters and hyphens
83 const validChars = /^[a-zA-Z0-9-]+$/;
84 return validChars.test(part);
85 }
86
87 public toString(): string {
88 return this.props.value;
89 }
90
91 public equals(other: Handle): boolean {
92 return this.props.value === other.props.value;
93 }
94}