1
- import * as urns from 'urns' ;
2
- import { createHash } from 'crypto' ;
3
- import { encode as base32Encode , decode as base32Decode } from 'hi-base32' ;
4
-
5
- //
6
- // This class encapsulates the concept of a Universal Data Identifier (UDI), which is a
7
- // standardized way to identify resources across the distributed DIG mesh network.
8
- // The UDI format: urn:dig:chainName:storeId:rootHash/resourceKey
9
- // This allows unique resource identification across the DIG network.
10
- //
1
+ import * as urns from "urns" ;
2
+ import { createHash } from "crypto" ;
3
+ import { encode as base32Encode , decode as base32Decode } from "hi-base32" ;
4
+
11
5
class Udi {
12
6
readonly chainName : string ;
13
- private readonly _storeId : Buffer ;
14
- private readonly _rootHash : Buffer | null ;
7
+ private readonly _storeIdHex : string ;
8
+ private readonly _rootHashHex : string | null ;
15
9
readonly resourceKey : string | null ;
16
10
static readonly nid : string = "dig" ;
17
11
static readonly namespace : string = `urn:${ Udi . nid } ` ;
18
12
19
13
constructor (
20
14
chainName : string ,
21
- storeId : string | Buffer ,
22
- rootHash : string | Buffer | null = null ,
15
+ storeId : string ,
16
+ rootHash : string | null = null ,
23
17
resourceKey : string | null = null
24
18
) {
25
19
if ( ! storeId ) {
26
20
throw new Error ( "storeId cannot be empty" ) ;
27
21
}
28
22
this . chainName = chainName || "chia" ;
29
- this . _storeId = Udi . convertToBuffer ( storeId ) ;
30
- this . _rootHash = rootHash ? Udi . convertToBuffer ( rootHash ) : null ;
23
+ this . _storeIdHex = Udi . verifyAndFormatHex ( storeId ) ;
24
+ this . _rootHashHex = rootHash ? Udi . verifyAndFormatHex ( rootHash ) : null ;
31
25
this . resourceKey = resourceKey ;
32
26
}
33
27
34
- static convertToBuffer ( input : string | Buffer ) : Buffer {
35
- if ( Buffer . isBuffer ( input ) ) {
36
- return input ;
37
- }
38
-
39
- if ( Udi . isHex ( input ) ) {
40
- return Buffer . from ( input , 'hex' ) ;
41
- }
42
-
43
- if ( Udi . isBase32 ( input ) ) {
44
- return Buffer . from ( base32Decode ( input . toUpperCase ( ) , false ) ) ; // Decode with uppercase
28
+ static verifyAndFormatHex ( input : string ) : string {
29
+ if ( ! / ^ [ a - f A - F 0 - 9 ] { 64 } $ / . test ( input ) ) {
30
+ throw new Error ( "Input must be a 64-character hex string." ) ;
45
31
}
46
-
47
- throw new Error ( "Invalid input encoding. Must be 32-byte hex or Base32 string." ) ;
48
- }
49
-
50
- static isHex ( input : string ) : boolean {
51
- return / ^ [ a - f A - F 0 - 9 ] { 64 } $ / . test ( input ) ;
52
- }
53
-
54
- static isBase32 ( input : string ) : boolean {
55
- return / ^ [ A - Z 2 - 7 ] { 52 } $ / . test ( input . toUpperCase ( ) ) ;
56
- }
57
-
58
- withRootHash ( rootHash : string | Buffer | null ) : Udi {
59
- return new Udi ( this . chainName , this . _storeId , rootHash , this . resourceKey ) ;
60
- }
61
-
62
- withResourceKey ( resourceKey : string | null ) : Udi {
63
- return new Udi ( this . chainName , this . _storeId , this . _rootHash , resourceKey ) ;
32
+ return input ;
64
33
}
65
34
66
35
static fromUrn ( urn : string ) : Udi {
67
36
const parsedUrn = urns . parseURN ( urn ) ;
68
- if ( parsedUrn . nid . toLowerCase ( ) !== Udi . nid ) {
37
+ if ( parsedUrn . nid !== Udi . nid ) {
69
38
throw new Error ( `Invalid nid: ${ parsedUrn . nid } ` ) ;
70
39
}
71
40
72
- const parts = parsedUrn . nss . split ( ':' ) ;
41
+ const parts = parsedUrn . nss . split ( ":" ) ;
73
42
if ( parts . length < 2 ) {
74
43
throw new Error ( `Invalid UDI format: ${ parsedUrn . nss } ` ) ;
75
44
}
76
45
77
46
const chainName = parts [ 0 ] ;
78
- const storeId = parts [ 1 ] . split ( '/' ) [ 0 ] ;
47
+ const storeIdHex = Udi . convertToHex ( parts [ 1 ] . split ( "/" ) [ 0 ] ) ;
79
48
80
- let rootHash : string | null = null ;
49
+ let rootHashHex : string | null = null ;
81
50
if ( parts . length > 2 ) {
82
- rootHash = parts [ 2 ] . split ( '/' ) [ 0 ] ;
51
+ rootHashHex = Udi . convertToHex ( parts [ 2 ] . split ( "/" ) [ 0 ] ) ;
83
52
}
84
53
85
- const pathParts = parsedUrn . nss . split ( '/' ) ;
54
+ const pathParts = parsedUrn . nss . split ( "/" ) ;
86
55
let resourceKey : string | null = null ;
87
56
if ( pathParts . length > 1 ) {
88
- resourceKey = pathParts . slice ( 1 ) . join ( '/' ) ;
57
+ resourceKey = pathParts . slice ( 1 ) . join ( "/" ) ;
89
58
}
90
59
91
- return new Udi ( chainName , storeId , rootHash , resourceKey ) ;
60
+ return new Udi ( chainName , storeIdHex , rootHashHex , resourceKey ) ;
92
61
}
93
62
94
- toUrn ( encoding : 'hex' | 'base32' | 'base64' = 'hex' ) : string {
95
- const storeIdStr = this . bufferToString ( this . _storeId , encoding ) ;
63
+ static convertToHex ( input : string ) : string {
64
+ // Attempt hex conversion first
65
+ if ( / ^ [ a - f A - F 0 - 9 ] { 64 } $ / . test ( input ) ) return input ;
66
+
67
+ // Convert from Base32
68
+ try {
69
+ const paddedInput = Udi . addBase32Padding ( input . toUpperCase ( ) ) ;
70
+ const buffer = Buffer . from ( base32Decode ( paddedInput , false ) ) ;
71
+ return buffer . toString ( "hex" ) ;
72
+ } catch ( e ) {
73
+ console . warn ( "Base32 decoding failed, trying Base64 encoding..." ) ;
74
+ }
75
+
76
+ // Convert from Base64
77
+ try {
78
+ const standardBase64 = Udi . addBase64Padding ( Udi . toStandardBase64 ( input ) ) ;
79
+ const buffer = Buffer . from ( standardBase64 , "base64" ) ;
80
+ return buffer . toString ( "hex" ) ;
81
+ } catch ( e ) {
82
+ throw new Error ( "Invalid input encoding. Must be 32-byte hex, Base32, or Base64 string." ) ;
83
+ }
84
+ }
85
+
86
+ static addBase32Padding ( input : string ) : string {
87
+ const paddingNeeded = ( 8 - ( input . length % 8 ) ) % 8 ;
88
+ return input + "=" . repeat ( paddingNeeded ) ;
89
+ }
90
+
91
+ static toStandardBase64 ( base64UrlSafe : string ) : string {
92
+ return base64UrlSafe . replace ( / - / g, "+" ) . replace ( / _ / g, "/" ) ;
93
+ }
94
+
95
+ static addBase64Padding ( base64 : string ) : string {
96
+ const paddingNeeded = ( 4 - ( base64 . length % 4 ) ) % 4 ;
97
+ return base64 + "=" . repeat ( paddingNeeded ) ;
98
+ }
99
+
100
+ toUrn ( encoding : "hex" | "base32" | "base64" = "hex" ) : string {
101
+ const storeIdStr = this . formatBufferAsEncoding ( this . _storeIdHex , encoding ) ;
96
102
let urn = `${ Udi . namespace } :${ this . chainName } :${ storeIdStr } ` ;
97
103
98
- if ( this . _rootHash ) {
99
- const rootHashStr = this . bufferToString ( this . _rootHash , encoding ) ;
104
+ if ( this . _rootHashHex ) {
105
+ const rootHashStr = this . formatBufferAsEncoding ( this . _rootHashHex , encoding ) ;
100
106
urn += `:${ rootHashStr } ` ;
101
107
}
102
108
@@ -107,24 +113,29 @@ class Udi {
107
113
return urn ;
108
114
}
109
115
110
- bufferToString ( buffer : Buffer , encoding : 'hex' | 'base32' | 'base64' ) : string {
111
- switch ( encoding ) {
112
- case 'hex' :
113
- return buffer . toString ( 'hex' ) ;
114
- case 'base32' :
115
- return base32Encode ( buffer ) . toUpperCase ( ) . replace ( / = + $ / , '' ) ; // Convert to uppercase and remove padding
116
- case 'base64' :
117
- return buffer . toString ( 'base64' ) . toLowerCase ( ) ; // Convert to lowercase
118
- default :
119
- throw new Error ( "Unsupported encoding" ) ;
116
+ private formatBufferAsEncoding ( hexString : string , encoding : "hex" | "base32" | "base64" ) : string {
117
+ const buffer = Buffer . from ( hexString , "hex" ) ;
118
+ if ( encoding === "hex" ) {
119
+ return hexString ;
120
+ } else if ( encoding === "base32" ) {
121
+ return base32Encode ( buffer ) . replace ( / = + $ / , "" ) ; // Strip padding for Base32
122
+ } else if ( encoding === "base64" ) {
123
+ return Udi . toBase64UrlSafe ( buffer . toString ( "base64" ) ) ; // Convert to URL-safe Base64
120
124
}
125
+ throw new Error ( "Unsupported encoding type" ) ;
126
+ }
127
+
128
+ static toBase64UrlSafe ( base64Standard : string ) : string {
129
+ return base64Standard . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = + $ / , "" ) ;
121
130
}
122
131
123
132
equals ( other : Udi ) : boolean {
124
133
return (
125
- this . _storeId . equals ( other . _storeId ) &&
134
+ this . _storeIdHex === other . _storeIdHex &&
126
135
this . chainName === other . chainName &&
127
- ( this . _rootHash && other . _rootHash ? this . _rootHash . equals ( other . _rootHash ) : this . _rootHash === other . _rootHash ) &&
136
+ ( this . _rootHashHex && other . _rootHashHex
137
+ ? this . _rootHashHex === other . _rootHashHex
138
+ : this . _rootHashHex === other . _rootHashHex ) &&
128
139
this . resourceKey === other . resourceKey
129
140
) ;
130
141
}
@@ -134,23 +145,37 @@ class Udi {
134
145
}
135
146
136
147
clone ( ) : Udi {
137
- return new Udi ( this . chainName , this . _storeId , this . _rootHash , this . resourceKey ) ;
148
+ return new Udi ( this . chainName , this . _storeIdHex , this . _rootHashHex , this . resourceKey ) ;
138
149
}
139
150
140
151
hashCode ( ) : string {
141
- const hash = createHash ( ' sha256' ) ;
152
+ const hash = createHash ( " sha256" ) ;
142
153
hash . update ( this . toUrn ( ) ) ;
143
- return hash . digest ( ' hex' ) ;
154
+ return hash . digest ( " hex" ) ;
144
155
}
145
156
146
- // Getter for storeId as a hex string
147
157
get storeId ( ) : string {
148
- return this . _storeId . toString ( 'hex' ) ;
158
+ return this . _storeIdHex ;
149
159
}
150
160
151
- // Getter for rootHash as a hex string
152
161
get rootHash ( ) : string | null {
153
- return this . _rootHash ? this . _rootHash . toString ( 'hex' ) : null ;
162
+ return this . _rootHashHex ;
163
+ }
164
+
165
+ get storeIdBase32 ( ) : string {
166
+ return this . formatBufferAsEncoding ( this . _storeIdHex , "base32" ) ;
167
+ }
168
+
169
+ get rootHashBase32 ( ) : string | null {
170
+ return this . _rootHashHex ? this . formatBufferAsEncoding ( this . _rootHashHex , "base32" ) : null ;
171
+ }
172
+
173
+ get storeIdBase64 ( ) : string {
174
+ return this . formatBufferAsEncoding ( this . _storeIdHex , "base64" ) ;
175
+ }
176
+
177
+ get rootHashBase64 ( ) : string | null {
178
+ return this . _rootHashHex ? this . formatBufferAsEncoding ( this . _rootHashHex , "base64" ) : null ;
154
179
}
155
180
}
156
181
0 commit comments