@@ -108,11 +108,143 @@ describe('createCredential', () => {
108108 expect ( parsed . challenge . method ) . toBe ( 'tempo' )
109109 } )
110110
111+ test ( 'behavior: createCredential emits client events and supports runtime handlers' , async ( ) => {
112+ const events : string [ ] = [ ]
113+ const createCredential = vi . fn ( async ( { challenge } ) =>
114+ Credential . serialize ( {
115+ challenge,
116+ payload : { signature : '0xsignature' , type : 'transaction' } ,
117+ } ) ,
118+ )
119+ const method = Method . toClient ( Methods . charge , { createCredential } )
120+ const mppx = Mppx . create ( {
121+ polyfill : false ,
122+ methods : [ method ] ,
123+ } )
124+ mppx . onCredentialCreated ( ( payload ) => {
125+ events . push ( `credential:${ payload . credential . startsWith ( 'Payment ' ) } ` )
126+ } )
127+ const offCredential = mppx . onCredentialCreated ( ( ) => {
128+ events . push ( 'removed' )
129+ } )
130+ const offChallenge = mppx . onChallengeReceived ( ( payload ) => {
131+ events . push ( `runtime:${ payload . method . intent } ` )
132+ return payload . createCredential ( )
133+ } )
134+ mppx . on ( '*' , ( event ) => {
135+ events . push ( `*:${ event . name } ` )
136+ } )
137+ offCredential ( )
138+
139+ const challenge = Challenge . fromMethod ( Methods . charge , {
140+ realm,
141+ secretKey,
142+ expires : new Date ( Date . now ( ) + 60_000 ) . toISOString ( ) ,
143+ request : {
144+ amount : '1000' ,
145+ currency : '0x1234567890123456789012345678901234567890' ,
146+ decimals : 6 ,
147+ recipient : '0x1234567890123456789012345678901234567890' ,
148+ } ,
149+ } )
150+ const response = new Response ( null , {
151+ status : 402 ,
152+ headers : {
153+ 'WWW-Authenticate' : Challenge . serialize ( challenge ) ,
154+ } ,
155+ } )
156+
157+ const credential = await mppx . createCredential ( response )
158+ offChallenge ( )
159+
160+ expect ( credential ) . toMatch ( / ^ P a y m e n t / )
161+ expect ( createCredential ) . toHaveBeenCalledTimes ( 1 )
162+ expect ( events ) . toEqual ( [
163+ 'runtime:charge' ,
164+ '*:challenge.received' ,
165+ 'credential:true' ,
166+ '*:credential.created' ,
167+ ] )
168+ } )
169+
170+ test ( 'behavior: createCredential memoizes event helper calls' , async ( ) => {
171+ const createCredential = vi . fn ( async ( { challenge } ) =>
172+ Credential . serialize ( {
173+ challenge,
174+ payload : { signature : '0xsignature' , type : 'transaction' } ,
175+ } ) ,
176+ )
177+ const method = Method . toClient ( Methods . charge , { createCredential } )
178+ const mppx = Mppx . create ( {
179+ polyfill : false ,
180+ methods : [ method ] ,
181+ } )
182+ mppx . on ( '*' , async ( event ) => {
183+ if ( event . name === 'challenge.received' ) await event . payload . createCredential ( )
184+ } )
185+
186+ const challenge = Challenge . fromMethod ( Methods . charge , {
187+ realm,
188+ secretKey,
189+ expires : new Date ( Date . now ( ) + 60_000 ) . toISOString ( ) ,
190+ request : {
191+ amount : '1000' ,
192+ currency : '0x1234567890123456789012345678901234567890' ,
193+ decimals : 6 ,
194+ recipient : '0x1234567890123456789012345678901234567890' ,
195+ } ,
196+ } )
197+ const response = new Response ( null , {
198+ status : 402 ,
199+ headers : {
200+ 'WWW-Authenticate' : Challenge . serialize ( challenge ) ,
201+ } ,
202+ } )
203+
204+ await mppx . createCredential ( response )
205+
206+ expect ( createCredential ) . toHaveBeenCalledTimes ( 1 )
207+ } )
208+
209+ test ( 'behavior: createCredential validates event credentials' , async ( ) => {
210+ const mppx = Mppx . create ( {
211+ polyfill : false ,
212+ methods : [ tempo ( { account : accounts [ 1 ] , getClient : ( ) => client } ) ] ,
213+ } )
214+ mppx . onChallengeReceived ( ( ) => 'Payment invalid\r\nX-Injected: true' )
215+
216+ const challenge = Challenge . fromMethod ( Methods . charge , {
217+ realm,
218+ secretKey,
219+ expires : new Date ( Date . now ( ) + 60_000 ) . toISOString ( ) ,
220+ request : {
221+ amount : '1000' ,
222+ currency : '0x1234567890123456789012345678901234567890' ,
223+ decimals : 6 ,
224+ recipient : '0x1234567890123456789012345678901234567890' ,
225+ } ,
226+ } )
227+ const response = new Response ( null , {
228+ status : 402 ,
229+ headers : {
230+ 'WWW-Authenticate' : Challenge . serialize ( challenge ) ,
231+ } ,
232+ } )
233+
234+ await expect ( mppx . createCredential ( response ) ) . rejects . toThrow ( 'illegal newline' )
235+ } )
236+
111237 test ( 'behavior: throws when method not found' , async ( ) => {
238+ const events : string [ ] = [ ]
112239 const mppx = Mppx . create ( {
113240 polyfill : false ,
114241 methods : [ tempo ( { account : accounts [ 1 ] , getClient : ( ) => client } ) ] ,
115242 } )
243+ mppx . onPaymentFailed ( ( payload ) => {
244+ events . push (
245+ `failed:${ payload . challenge === undefined } :${ payload . challenges ?. length } :${ payload . error instanceof Error } ` ,
246+ )
247+ } )
116248
117249 const challenge = Challenge . from ( {
118250 id : 'test-id' ,
@@ -132,6 +264,7 @@ describe('createCredential', () => {
132264 await expect ( mppx . createCredential ( response ) ) . rejects . toThrow (
133265 'No method found for challenges: unknown.charge. Available: tempo.charge, tempo.session' ,
134266 )
267+ expect ( events ) . toEqual ( [ 'failed:true:1:true' ] )
135268 } )
136269
137270 test ( 'behavior: rejects expired challenges before creating credential' , async ( ) => {
@@ -451,6 +584,54 @@ describe('createCredential', () => {
451584 expect ( ( parsed . payload as { type : string } ) . type ) . toBe ( 'transaction' )
452585 expect ( parsed . challenge . method ) . toBe ( 'tempo' )
453586 } )
587+
588+ test ( 'behavior: mcp transport event responses are not cast to DOM Response' , async ( ) => {
589+ const method = Method . toClient ( Methods . charge , {
590+ async createCredential ( { challenge } ) {
591+ return Credential . serialize ( {
592+ challenge,
593+ payload : { signature : '0xsignature' , type : 'transaction' } ,
594+ } )
595+ } ,
596+ } )
597+ const mppx = Mppx . create ( {
598+ polyfill : false ,
599+ methods : [ method ] ,
600+ transport : Transport . mcp ( ) ,
601+ } )
602+
603+ const challenge = Challenge . fromMethod ( Methods . charge , {
604+ realm,
605+ secretKey,
606+ expires : new Date ( Date . now ( ) + 60_000 ) . toISOString ( ) ,
607+ request : {
608+ amount : '1000' ,
609+ currency : '0x1234567890123456789012345678901234567890' ,
610+ decimals : 6 ,
611+ recipient : '0x1234567890123456789012345678901234567890' ,
612+ } ,
613+ } )
614+ const mcpResponse : Mcp . Response = {
615+ jsonrpc : '2.0' ,
616+ id : 1 ,
617+ error : {
618+ code : Mcp . paymentRequiredCode ,
619+ message : 'Payment Required' ,
620+ data : {
621+ httpStatus : 402 ,
622+ challenges : [ challenge ] ,
623+ } ,
624+ } ,
625+ }
626+ const seen : unknown [ ] = [ ]
627+ mppx . onChallengeReceived ( ( event ) => {
628+ seen . push ( event . response )
629+ } )
630+
631+ await mppx . createCredential ( mcpResponse )
632+
633+ expect ( seen ) . toEqual ( [ mcpResponse ] )
634+ } )
454635} )
455636
456637const server = Mppx_server . create ( {
0 commit comments