@@ -16,27 +16,38 @@ describe('Attribution', () => {
1616
1717 describe ( 'encode' , ( ) => {
1818 test ( 'returns a 32-byte hex string' , ( ) => {
19- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
19+ const memo = Attribution . encode ( {
20+ challengeId : 'test-challenge-1' ,
21+ serverId : 'api.example.com' ,
22+ } )
2023 // 0x prefix + 64 hex chars = 32 bytes
2124 expect ( memo ) . toMatch ( / ^ 0 x [ 0 - 9 a - f ] { 64 } $ / i)
2225 } )
2326
2427 test ( 'starts with TAG + version byte' , ( ) => {
25- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
28+ const memo = Attribution . encode ( {
29+ challengeId : 'test-challenge-1' ,
30+ serverId : 'api.example.com' ,
31+ } )
2632 const tag = memo . slice ( 0 , 10 ) // 0x + 8 hex chars
2733 expect ( tag . toLowerCase ( ) ) . toBe ( Attribution . tag . toLowerCase ( ) )
2834 const version = memo . slice ( 10 , 12 )
2935 expect ( version ) . toBe ( '01' )
3036 } )
3137
32- test ( 'generates unique memos (random nonce)' , ( ) => {
33- const a = Attribution . encode ( { serverId : 'api.example.com' } )
34- const b = Attribution . encode ( { serverId : 'api.example.com' } )
38+ test ( 'produces deterministic memos (challenge-bound nonce)' , ( ) => {
39+ const a = Attribution . encode ( { challengeId : 'challenge-a' , serverId : 'api.example.com' } )
40+ const b = Attribution . encode ( { challengeId : 'challenge-b' , serverId : 'api.example.com' } )
3541 expect ( a ) . not . toBe ( b )
42+ const c = Attribution . encode ( { challengeId : 'challenge-a' , serverId : 'api.example.com' } )
43+ expect ( a ) . toBe ( c )
3644 } )
3745
3846 test ( 'encodes server fingerprint from serverId' , ( ) => {
39- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
47+ const memo = Attribution . encode ( {
48+ challengeId : 'test-challenge-1' ,
49+ serverId : 'api.example.com' ,
50+ } )
4051 const expectedFingerprint = Hex . slice (
4152 Hash . keccak256 ( Bytes . fromString ( 'api.example.com' ) , { as : 'Hex' } ) ,
4253 0 ,
@@ -47,7 +58,11 @@ describe('Attribution', () => {
4758 } )
4859
4960 test ( 'encodes client fingerprint from clientId' , ( ) => {
50- const memo = Attribution . encode ( { serverId : 'api.example.com' , clientId : 'my-app' } )
61+ const memo = Attribution . encode ( {
62+ challengeId : 'test-challenge-1' ,
63+ clientId : 'my-app' ,
64+ serverId : 'api.example.com' ,
65+ } )
5166 const expectedFingerprint = Hex . slice (
5267 Hash . keccak256 ( Bytes . fromString ( 'my-app' ) , { as : 'Hex' } ) ,
5368 0 ,
@@ -58,13 +73,20 @@ describe('Attribution', () => {
5873 } )
5974
6075 test ( 'encodes zero client bytes when no clientId' , ( ) => {
61- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
76+ const memo = Attribution . encode ( {
77+ challengeId : 'test-challenge-1' ,
78+ serverId : 'api.example.com' ,
79+ } )
6280 const clientHex = `0x${ memo . slice ( 32 , 52 ) } ` as `0x${string } `
6381 expect ( clientHex ) . toBe ( Attribution . anonymous )
6482 } )
6583
6684 test ( 'treats empty string clientId as anonymous' , ( ) => {
67- const memo = Attribution . encode ( { serverId : 'api.example.com' , clientId : '' } )
85+ const memo = Attribution . encode ( {
86+ challengeId : 'test-challenge-1' ,
87+ clientId : '' ,
88+ serverId : 'api.example.com' ,
89+ } )
6890 const clientHex = `0x${ memo . slice ( 32 , 52 ) } ` as `0x${string } `
6991 expect ( clientHex ) . toBe ( Attribution . anonymous )
7092 const decoded = Attribution . decode ( memo )
@@ -75,12 +97,19 @@ describe('Attribution', () => {
7597
7698 describe ( 'isMppMemo' , ( ) => {
7799 test ( 'returns true for encoded memos' , ( ) => {
78- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
100+ const memo = Attribution . encode ( {
101+ challengeId : 'test-challenge-1' ,
102+ serverId : 'api.example.com' ,
103+ } )
79104 expect ( Attribution . isMppMemo ( memo ) ) . toBe ( true )
80105 } )
81106
82107 test ( 'returns true for encoded memos with clientId' , ( ) => {
83- const memo = Attribution . encode ( { serverId : 'api.example.com' , clientId : 'my-app' } )
108+ const memo = Attribution . encode ( {
109+ challengeId : 'test-challenge-1' ,
110+ clientId : 'my-app' ,
111+ serverId : 'api.example.com' ,
112+ } )
84113 expect ( Attribution . isMppMemo ( memo ) ) . toBe ( true )
85114 } )
86115
@@ -101,13 +130,19 @@ describe('Attribution', () => {
101130 } )
102131
103132 test ( 'returns false for wrong version' , ( ) => {
104- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
133+ const memo = Attribution . encode ( {
134+ challengeId : 'test-challenge-1' ,
135+ serverId : 'api.example.com' ,
136+ } )
105137 const wrongVersion = `${ memo . slice ( 0 , 10 ) } ff${ memo . slice ( 12 ) } ` as `0x${string } `
106138 expect ( Attribution . isMppMemo ( wrongVersion ) ) . toBe ( false )
107139 } )
108140
109141 test ( 'handles mixed case hex' , ( ) => {
110- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
142+ const memo = Attribution . encode ( {
143+ challengeId : 'test-challenge-1' ,
144+ serverId : 'api.example.com' ,
145+ } )
111146 const tagUpper = memo . slice ( 0 , 10 ) . toUpperCase ( )
112147 const mixed = `0x${ tagUpper . slice ( 2 ) } ${ memo . slice ( 10 ) } ` as `0x${string } `
113148 expect ( Attribution . isMppMemo ( mixed ) ) . toBe ( true )
@@ -116,17 +151,27 @@ describe('Attribution', () => {
116151
117152 describe ( 'verifyServer' , ( ) => {
118153 test ( 'returns true for matching serverId' , ( ) => {
119- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
154+ const memo = Attribution . encode ( {
155+ challengeId : 'test-challenge-1' ,
156+ serverId : 'api.example.com' ,
157+ } )
120158 expect ( Attribution . verifyServer ( memo , 'api.example.com' ) ) . toBe ( true )
121159 } )
122160
123161 test ( 'returns true for matching serverId with clientId' , ( ) => {
124- const memo = Attribution . encode ( { serverId : 'api.example.com' , clientId : 'my-app' } )
162+ const memo = Attribution . encode ( {
163+ challengeId : 'test-challenge-1' ,
164+ clientId : 'my-app' ,
165+ serverId : 'api.example.com' ,
166+ } )
125167 expect ( Attribution . verifyServer ( memo , 'api.example.com' ) ) . toBe ( true )
126168 } )
127169
128170 test ( 'returns false for wrong serverId' , ( ) => {
129- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
171+ const memo = Attribution . encode ( {
172+ challengeId : 'test-challenge-1' ,
173+ serverId : 'api.example.com' ,
174+ } )
130175 expect ( Attribution . verifyServer ( memo , 'other.example.com' ) ) . toBe ( false )
131176 } )
132177
@@ -139,7 +184,11 @@ describe('Attribution', () => {
139184
140185 describe ( 'decode' , ( ) => {
141186 test ( 'decodes an encoded memo with serverId and clientId' , ( ) => {
142- const memo = Attribution . encode ( { serverId : 'api.example.com' , clientId : 'my-app' } )
187+ const memo = Attribution . encode ( {
188+ challengeId : 'test-challenge-1' ,
189+ clientId : 'my-app' ,
190+ serverId : 'api.example.com' ,
191+ } )
143192 const result = Attribution . decode ( memo )
144193 expect ( result ) . not . toBeNull ( )
145194 expect ( result ! . version ) . toBe ( 1 )
@@ -150,7 +199,10 @@ describe('Attribution', () => {
150199 } )
151200
152201 test ( 'decodes anonymous client as null' , ( ) => {
153- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
202+ const memo = Attribution . encode ( {
203+ challengeId : 'test-challenge-1' ,
204+ serverId : 'api.example.com' ,
205+ } )
154206 const result = Attribution . decode ( memo )
155207 expect ( result ) . not . toBeNull ( )
156208 expect ( result ! . clientFingerprint ) . toBeNull ( )
@@ -162,14 +214,22 @@ describe('Attribution', () => {
162214 expect ( Attribution . decode ( arbitrary ) ) . toBeNull ( )
163215 } )
164216
165- test ( 'different encodes produce different nonces' , ( ) => {
166- const a = Attribution . decode ( Attribution . encode ( { serverId : 'api.example.com' } ) )
167- const b = Attribution . decode ( Attribution . encode ( { serverId : 'api.example.com' } ) )
217+ test ( 'different challengeIds produce different nonces' , ( ) => {
218+ const a = Attribution . decode (
219+ Attribution . encode ( { challengeId : 'challenge-a' , serverId : 'api.example.com' } ) ,
220+ )
221+ const b = Attribution . decode (
222+ Attribution . encode ( { challengeId : 'challenge-b' , serverId : 'api.example.com' } ) ,
223+ )
168224 expect ( a ! . nonce ) . not . toBe ( b ! . nonce )
169225 } )
170226
171227 test ( 'serverId fingerprint matches expected keccak hash' , ( ) => {
172- const memo = Attribution . encode ( { serverId : 'api.example.com' , clientId : 'my-app' } )
228+ const memo = Attribution . encode ( {
229+ challengeId : 'test-challenge-1' ,
230+ clientId : 'my-app' ,
231+ serverId : 'api.example.com' ,
232+ } )
173233 const result = Attribution . decode ( memo ) !
174234 const expectedServer = Hex . slice (
175235 Hash . keccak256 ( Bytes . fromString ( 'api.example.com' ) , { as : 'Hex' } ) ,
@@ -180,9 +240,55 @@ describe('Attribution', () => {
180240 } )
181241
182242 test ( 'returns null for wrong version via decode' , ( ) => {
183- const memo = Attribution . encode ( { serverId : 'api.example.com' } )
243+ const memo = Attribution . encode ( {
244+ challengeId : 'test-challenge-1' ,
245+ serverId : 'api.example.com' ,
246+ } )
184247 const corrupted = `${ memo . slice ( 0 , 10 ) } ff${ memo . slice ( 12 ) } ` as `0x${string } `
185248 expect ( Attribution . decode ( corrupted ) ) . toBeNull ( )
186249 } )
187250 } )
251+
252+ describe ( 'challengeNonce' , ( ) => {
253+ test ( 'returns 7 bytes' , ( ) => {
254+ const nonce = Attribution . challengeNonce ( 'challenge-123' )
255+ expect ( nonce . length ) . toBe ( 7 )
256+ } )
257+
258+ test ( 'is deterministic' , ( ) => {
259+ const a = Attribution . challengeNonce ( 'challenge-123' )
260+ const b = Attribution . challengeNonce ( 'challenge-123' )
261+ expect ( Hex . fromBytes ( a ) ) . toBe ( Hex . fromBytes ( b ) )
262+ } )
263+
264+ test ( 'differs for different challengeIds' , ( ) => {
265+ const a = Attribution . challengeNonce ( 'challenge-123' )
266+ const b = Attribution . challengeNonce ( 'challenge-456' )
267+ expect ( Hex . fromBytes ( a ) ) . not . toBe ( Hex . fromBytes ( b ) )
268+ } )
269+ } )
270+
271+ describe ( 'verifyChallengeBinding' , ( ) => {
272+ test ( 'returns true for matching challengeId' , ( ) => {
273+ const memo = Attribution . encode ( {
274+ challengeId : 'challenge-123' ,
275+ serverId : 'api.example.com' ,
276+ } )
277+ expect ( Attribution . verifyChallengeBinding ( memo , 'challenge-123' ) ) . toBe ( true )
278+ } )
279+
280+ test ( 'returns false for wrong challengeId' , ( ) => {
281+ const memo = Attribution . encode ( {
282+ challengeId : 'challenge-123' ,
283+ serverId : 'api.example.com' ,
284+ } )
285+ expect ( Attribution . verifyChallengeBinding ( memo , 'challenge-456' ) ) . toBe ( false )
286+ } )
287+
288+ test ( 'returns false for non-MPP memo' , ( ) => {
289+ const arbitrary =
290+ '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as `0x${string } `
291+ expect ( Attribution . verifyChallengeBinding ( arbitrary , 'challenge-123' ) ) . toBe ( false )
292+ } )
293+ } )
188294} )
0 commit comments