11// SubDomain.ts
22
3- import baseX from "base-x" ;
4-
53/**
64 * SubDomain Class
75 *
86 * Encapsulates the logic for encoding and decoding a combination of
97 * chain and storeId into a DNS-friendly identifier using Base62 encoding.
108 */
119class SubDomain {
12- // Define the Base62 character set
13- private static readonly BASE62_CHARSET =
14- "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ;
15-
16- // Initialize the Base62 encoder/decoder
17- private static base62 = baseX ( SubDomain . BASE62_CHARSET ) ;
18-
19- // Define expected byte length for storeId
20- private static readonly STORE_ID_LENGTH = 32 ; // bytes
21-
22- // Properties
23- public readonly chain : string ;
24- public readonly storeId : string ;
25- public readonly encodedId : string ;
26-
27- /**
28- * Constructor for SubDomain
29- *
30- * @param chain - The chain name (e.g., "CHAIN1234").
31- * @param storeId - The store ID as a 64-character hexadecimal string.
32- * @throws Will throw an error if inputs are invalid or encoding exceeds DNS limits.
33- */
34- constructor ( chain : string , storeId : string ) {
35- this . chain = chain ;
36- this . storeId = storeId ;
37- this . encodedId = this . encode ( ) ;
38- }
39-
40- /**
41- * Encodes the provided chain and storeId into a DNS-friendly identifier.
42- *
43- * @returns The Base62-encoded identifier.
44- * @throws Will throw an error if encoding fails.
45- */
46- private encode ( ) : string {
47- // Validate inputs
48- if ( ! this . chain || typeof this . chain !== "string" ) {
49- throw new Error ( "Invalid chain: Chain must be a non-empty string." ) ;
50- }
51-
52- if (
53- ! this . storeId ||
54- typeof this . storeId !== "string" ||
55- ! / ^ [ 0 - 9 a - f A - F ] { 64 } $ / . test ( this . storeId )
56- ) {
57- throw new Error (
58- "Invalid storeId: StoreId must be a 64-character hexadecimal string."
59- ) ;
60- }
61-
62- // Ensure the chain length is within 1-255 characters to fit in one byte
63- const chainLength = this . chain . length ;
64- if ( chainLength < 1 || chainLength > 255 ) {
65- throw new Error (
66- "Invalid chain: Length must be between 1 and 255 characters."
67- ) ;
68- }
69-
70- // Convert chain length to a single byte Buffer
71- const chainLengthBuffer = Buffer . from ( [ chainLength ] ) ;
72-
73- // Convert chain to a Buffer (UTF-8)
74- const chainBuffer = Buffer . from ( this . chain , "utf8" ) ;
75-
76- // Convert storeId from hex string to Buffer
77- const storeIdBuffer = Buffer . from ( this . storeId , "hex" ) ;
78-
79- // Validate storeId byte length
80- if ( storeIdBuffer . length !== SubDomain . STORE_ID_LENGTH ) {
81- throw new Error (
82- `Invalid storeId length: Expected ${ SubDomain . STORE_ID_LENGTH } bytes, got ${ storeIdBuffer . length } bytes.`
83- ) ;
10+ // Define the Base62 character set
11+ private static readonly BASE62_CHARSET =
12+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ;
13+
14+ // Define expected byte length for storeId
15+ private static readonly STORE_ID_LENGTH = 32 ; // bytes
16+
17+ // Properties
18+ public readonly chain : string ;
19+ public readonly storeId : string ;
20+ public readonly encodedId : string ;
21+
22+ /**
23+ * Constructor for SubDomain
24+ *
25+ * @param chain - The chain name (e.g., "CHAIN1234").
26+ * @param storeId - The store ID as a 64-character hexadecimal string.
27+ * @throws Will throw an error if inputs are invalid or encoding exceeds DNS limits.
28+ */
29+ constructor ( chain : string , storeId : string ) {
30+ this . chain = chain ;
31+ this . storeId = storeId ;
32+ this . encodedId = this . encode ( ) ;
8433 }
85-
86- // Concatenate chain_length, chain, and storeId buffers
87- const dataBuffer = Buffer . concat ( [
88- chainLengthBuffer ,
89- chainBuffer ,
90- storeIdBuffer ,
91- ] ) ;
92-
93- // Encode the data buffer using Base62
94- const encodedId = SubDomain . base62 . encode ( dataBuffer ) ;
95-
96- // Ensure DNS label length does not exceed 63 characters
97- if ( encodedId . length > 63 ) {
98- throw new Error (
99- `Encoded identifier length (${ encodedId . length } ) exceeds DNS label limit of 63 characters.`
100- ) ;
34+
35+ /**
36+ * Encodes the provided chain and storeId into a DNS-friendly identifier.
37+ *
38+ * @returns The Base62-encoded identifier.
39+ * @throws Will throw an error if encoding fails.
40+ */
41+ private encode ( ) : string {
42+ // Validate inputs
43+ if ( ! this . chain || typeof this . chain !== "string" ) {
44+ throw new Error ( "Invalid chain: Chain must be a non-empty string." ) ;
45+ }
46+
47+ if (
48+ ! this . storeId ||
49+ typeof this . storeId !== "string" ||
50+ ! / ^ [ 0 - 9 a - f A - F ] { 64 } $ / . test ( this . storeId )
51+ ) {
52+ throw new Error (
53+ "Invalid storeId: StoreId must be a 64-character hexadecimal string."
54+ ) ;
55+ }
56+
57+ // Ensure the chain length is within 1-255 characters to fit in one byte
58+ const chainLength = this . chain . length ;
59+ if ( chainLength < 1 || chainLength > 255 ) {
60+ throw new Error (
61+ "Invalid chain: Length must be between 1 and 255 characters."
62+ ) ;
63+ }
64+
65+ // Convert chain length to a single byte
66+ const chainLengthBuffer = Buffer . from ( [ chainLength ] ) ;
67+
68+ // Convert chain to a Buffer (UTF-8)
69+ const chainBuffer = Buffer . from ( this . chain , "utf8" ) ;
70+
71+ // Convert storeId from hex string to Buffer
72+ const storeIdBuffer = Buffer . from ( this . storeId , "hex" ) ;
73+
74+ // Validate storeId byte length
75+ if ( storeIdBuffer . length !== SubDomain . STORE_ID_LENGTH ) {
76+ throw new Error (
77+ `Invalid storeId length: Expected ${ SubDomain . STORE_ID_LENGTH } bytes, got ${ storeIdBuffer . length } bytes.`
78+ ) ;
79+ }
80+
81+ // Concatenate chain_length, chain, and storeId buffers
82+ const dataBuffer = Buffer . concat ( [
83+ chainLengthBuffer ,
84+ chainBuffer ,
85+ storeIdBuffer ,
86+ ] ) ;
87+
88+ // Encode the data buffer using Base62
89+ const encodedId = SubDomain . encodeBase62 ( dataBuffer ) ;
90+
91+ // Ensure DNS label length does not exceed 63 characters
92+ if ( encodedId . length > 63 ) {
93+ throw new Error (
94+ `Encoded identifier length (${ encodedId . length } ) exceeds DNS label limit of 63 characters.`
95+ ) ;
96+ }
97+
98+ return encodedId ;
10199 }
102-
103- return encodedId ;
104- }
105-
106- /**
107- * Decodes the provided identifier back into the original chain and storeId.
108- *
109- * @param encodedId - The Base62-encoded identifier.
110- * @returns An object containing the original chain and storeId.
111- * @throws Will throw an error if decoding fails or data lengths mismatch.
112- */
113- public static decode ( encodedId : string ) : { chain : string ; storeId : string } {
114- // Validate input
115- if ( ! encodedId || typeof encodedId !== "string" ) {
116- throw new Error (
117- "Invalid encodedId: encodedId must be a non-empty string."
100+
101+ /**
102+ * Decodes the provided identifier back into the original chain and storeId.
103+ *
104+ * @param encodedId - The Base62-encoded identifier.
105+ * @returns An object containing the original chain and storeId.
106+ * @throws Will throw an error if decoding fails or data lengths mismatch.
107+ */
108+ public static decode ( encodedId : string ) : { chain : string ; storeId : string } {
109+ // Validate input
110+ if ( ! encodedId || typeof encodedId !== "string" ) {
111+ throw new Error (
112+ "Invalid encodedId: encodedId must be a non-empty string."
113+ ) ;
114+ }
115+
116+ // Decode the Base62 string back to a Buffer
117+ const decodedBuffer = SubDomain . decodeBase62 ( encodedId ) ;
118+
119+ if ( ! decodedBuffer ) {
120+ throw new Error ( "Failed to decode Base62 string." ) ;
121+ }
122+
123+ // Ensure there's at least 1 byte for chain_length and STORE_ID_LENGTH bytes for storeId
124+ if ( decodedBuffer . length < 1 + SubDomain . STORE_ID_LENGTH ) {
125+ throw new Error ( "Decoded data is too short to contain required fields." ) ;
126+ }
127+
128+ // Extract chain_length (1 byte)
129+ const chain_length = decodedBuffer . readUInt8 ( 0 ) ;
130+
131+ // Extract chain
132+ const chain = decodedBuffer . slice ( 1 , 1 + chain_length ) . toString ( "utf8" ) ;
133+
134+ // Extract storeId
135+ const storeIdBuffer = decodedBuffer . slice (
136+ 1 + chain_length ,
137+ 1 + chain_length + SubDomain . STORE_ID_LENGTH
118138 ) ;
139+
140+ // Convert storeId buffer to hex string
141+ const storeId = storeIdBuffer . toString ( "hex" ) ;
142+
143+ return { chain, storeId } ;
119144 }
120-
121- // Decode the Base62 string back to a Buffer
122- const decodedBuffer = SubDomain . base62 . decode ( encodedId ) ;
123-
124- if ( ! decodedBuffer ) {
125- throw new Error ( "Failed to decode Base62 string." ) ;
145+
146+ /**
147+ * Encodes a Buffer into a Base62 string.
148+ *
149+ * @param buffer - The Buffer to encode.
150+ * @returns The Base62-encoded string.
151+ */
152+ private static encodeBase62 ( buffer : Buffer ) : string {
153+ if ( buffer . length === 0 ) return "" ;
154+
155+ // Convert Buffer to BigInt
156+ let num = BigInt ( 0 ) ;
157+ for ( let i = 0 ; i < buffer . length ; i ++ ) {
158+ num = ( num << BigInt ( 8 ) ) + BigInt ( buffer [ i ] ) ;
159+ }
160+
161+ // Base62 encoding
162+ let encoded = "" ;
163+ const base = BigInt ( 62 ) ;
164+
165+ if ( num === BigInt ( 0 ) ) {
166+ encoded = SubDomain . BASE62_CHARSET [ 0 ] ;
167+ } else {
168+ while ( num > 0 ) {
169+ const remainder = num % base ;
170+ encoded = SubDomain . BASE62_CHARSET [ Number ( remainder ) ] + encoded ;
171+ num = num / base ;
172+ }
173+ }
174+
175+ // Handle leading zero bytes
176+ let leadingZeros = 0 ;
177+ for ( let i = 0 ; i < buffer . length ; i ++ ) {
178+ if ( buffer [ i ] === 0 ) {
179+ leadingZeros ++ ;
180+ } else {
181+ break ;
182+ }
183+ }
184+ for ( let i = 0 ; i < leadingZeros ; i ++ ) {
185+ encoded = SubDomain . BASE62_CHARSET [ 0 ] + encoded ;
186+ }
187+
188+ return encoded ;
126189 }
127-
128- // Ensure there's at least 1 byte for chain_length and STORE_ID_LENGTH bytes for storeId
129- if ( decodedBuffer . length < 1 + SubDomain . STORE_ID_LENGTH ) {
130- throw new Error ( "Decoded data is too short to contain required fields." ) ;
190+
191+ /**
192+ * Decodes a Base62 string into a Buffer.
193+ *
194+ * @param str - The Base62 string to decode.
195+ * @returns The decoded Buffer.
196+ */
197+ private static decodeBase62 ( str : string ) : Buffer {
198+ if ( str . length === 0 ) return Buffer . alloc ( 0 ) ;
199+
200+ // Handle leading '0's
201+ let leadingZeros = 0 ;
202+ while ( leadingZeros < str . length && str [ leadingZeros ] === SubDomain . BASE62_CHARSET [ 0 ] ) {
203+ leadingZeros ++ ;
204+ }
205+
206+ // Decode the Base62 string to BigInt
207+ let num = BigInt ( 0 ) ;
208+ const base = BigInt ( 62 ) ;
209+ for ( let i = leadingZeros ; i < str . length ; i ++ ) {
210+ const char = str [ i ] ;
211+ const value = SubDomain . BASE62_CHARSET . indexOf ( char ) ;
212+ if ( value === - 1 ) {
213+ throw new Error ( `Invalid character in Base62 string: ${ char } ` ) ;
214+ }
215+ num = num * base + BigInt ( value ) ;
216+ }
217+
218+ // Convert BigInt to Buffer
219+ let hex = num . toString ( 16 ) ;
220+ if ( hex . length % 2 !== 0 ) {
221+ hex = "0" + hex ;
222+ }
223+ let decoded = Buffer . from ( hex , "hex" ) ;
224+
225+ // Prepend leading zero bytes
226+ if ( leadingZeros > 0 ) {
227+ const zeroBuffer = Buffer . alloc ( leadingZeros , 0 ) ;
228+ decoded = Buffer . concat ( [ zeroBuffer , decoded ] ) ;
229+ }
230+
231+ return decoded ;
131232 }
132-
133- // Extract chain_length (1 byte)
134- const chain_length = Buffer . from ( decodedBuffer ) . readUInt8 ( 0 ) ;
135-
136- // Define the expected total length
137- const expected_length = 1 + chain_length + SubDomain . STORE_ID_LENGTH ;
138-
139- if ( decodedBuffer . length !== expected_length ) {
140- throw new Error (
141- `Decoded data length mismatch: expected ${ expected_length } bytes, got ${ decodedBuffer . length } bytes.`
142- ) ;
143- }
144-
145- // Extract chain and storeId from the buffer
146- const chain = Buffer . from ( decodedBuffer . slice ( 1 , 1 + chain_length ) ) . toString ( "utf8" ) ;
147- const storeIdBuffer = decodedBuffer . slice (
148- 1 + chain_length ,
149- 1 + chain_length + SubDomain . STORE_ID_LENGTH
150- ) ;
151-
152- // Convert storeId buffer to hex string
153- const storeId = Buffer . from ( storeIdBuffer ) . toString ( "hex" ) ;
154-
155- return { chain, storeId } ;
156233 }
157- }
158-
159- export { SubDomain } ;
234+
235+ export { SubDomain } ;
236+
0 commit comments