import { createMessageListener , FilteringEventListener , isPermittedOrigin , RpcMessageListener , WindowLike } from './comms' ; import { OpenYoloError , OpenYoloInternalError } from './errors' ; import { ackMessage , channelConnectMessage , channelReadyMessage , PostMessageType , readyForConnectMessage } from './post_messages' ; import { RpcMessage , RpcMessageData , RpcMessageType } from './rpc_messages' ; import { PromiseResolver , sha256 , timeoutPromise } from './utils' ; /**
 * The timeout for ack message.
 */ const ACK_TIMEOUT_MS = 500 ; export type UnknownMessageEventListener = ( ev : MessageEvent ) => void ; interface ListenerPair { portListener : FilteringEventListener ; wrappedListener : RpcMessageListener < any > ; } export class SecureChannel { private mainListener : ( ev : MessageEvent ) => void ; private listeners : Array < ListenerPair | null > = [ ] ; private fallbackListeners : UnknownMessageEventListener [ ] = [ ] ; static async clientConnect ( clientWindow , providerWindow , connectionNonce , connectionNonceHash ) { await SecureChannel . providerReadyToConnect ( clientWindow , connectionNonceHash ) ; const channel = new MessageChannel ( ) ; const readyPromiseResolver = new PromiseResolver ( ) ; const readyListener = createMessageListener ( PostMessageType . channelReady , ( ) => { readyPromiseResolver . resolve ( ) ; } ) ; const errorListener = createMessageListener ( PostMessageType . channelError , ( data ) => { readyPromiseResolver . reject ( OpenYoloError . fromData ( data ) ) ; } ) ; clientWindow . addEventListener ( 'message' , readyListener ) ; clientWindow . addEventListener ( 'message' , errorListener ) ; providerWindow . postMessage ( channelConnectMessage ( connectionNonce ) , '*' , [ channel . port2 ] ) ; try { await readyPromiseResolver . promise ; return new SecureChannel ( channel . port1 ) ; } catch ( err ) { channel . port1 . close ( ) ; throw err ; } finally { clientWindow . removeEventListener ( 'message' , readyListener ) ; clientWindow . removeEventListener ( 'message' , errorListener ) ; } } static providerReadyToConnect ( clientWindow , expectedNonceHash ) { let promiseResolver = new PromiseResolver < void > ( ) ; let listener = createMessageListener ( PostMessageType . readyForConnect , ( nonce ) => { if ( expectedNonceHash === nonce ) { promiseResolver . resolve ( ) ; } } ) ; clientWindow . addEventListener ( 'message' , listener ) ; promiseResolver . promise . then ( ( ) => { clientWindow . removeEventListener ( 'message' , listener ) ; } , ( ) => { clientWindow . removeEventListener ( 'message' , listener ) ; } ) ; return promiseResolver . promise ; } /**
     * Connect method that the OpenYOLO provider calls to establish contact
     * with the client.
     */ static async providerConnect ( providerWindow , permittedOrigins , connectionNonce ) { let port = null ; let promiseResolver = new PromiseResolver < SecureChannel > ( ) ; let listener = createMessageListener ( PostMessageType . channelConnect , async function ( nonce , type , ev ) { if ( typeof nonce !== 'string' ) { return ; } const nonceHash = await sha256 ( nonce ) ; if ( nonceHash !== connectionNonce ) { return ; } if ( ! isPermittedOrigin ( ev . origin , permittedOrigins ) ) { promiseResolver . reject ( OpenYoloInternalError . untrustedOrigin ( ev . origin ) ) ; return ; } if ( ! ev . ports ) { promiseResolver . reject ( OpenYoloInternalError . illegalStateError ( 'channel initialization message does not contain ports' ) ) ; return ; } port = ev . ports [ 0 ] as MessagePort ; providerWindow . parent . postMessage ( channelReadyMessage ( connectionNonce ) , ev . origin ) ; promiseResolver . resolve ( new SecureChannel ( port ) ) ; } ) ; providerWindow . addEventListener ( 'message' , listener ) ; promiseResolver . promise . then ( ( ) => { providerWindow . removeEventListener ( 'message' , listener ) ; } , ( err ) => { providerWindow . removeEventListener ( 'message' , listener ) ; } ) ; providerWindow . parent . postMessage ( readyForConnectMessage ( connectionNonce ) , '*' ) ; return promiseResolver . promise ; } constructor ( private port ) { this . mainListener = ( ev ) => { let anyMatched = false ; for ( let i = 0 ; i < this . listeners . length ; i ++ ) { let listener = this . listeners [ i ] ; if ( ! listener ) continue ; anyMatched = ! ! listener . portListener ( ev ) || anyMatched ; } if ( anyMatched ) { return ; } for ( let i = 0 ; i < this . fallbackListeners . length ; i ++ ) { this . fallbackListeners [ i ] ( ev ) ; } } ; this . port . addEventListener ( 'message' , this . mainListener ) ; this . port . start ( ) ; } send < T extends RpcMessageType > ( message ) { this . port . postMessage ( message ) ; } /**
     * Sends a message and waits for acknowledgment of the recipient.
     */ sendAndWaitAck < T extends RpcMessageType > ( message ) { const promiseResolver = new PromiseResolver < void > ( ) ; message . data . ack = true ; const ackListner = createMessageListener ( PostMessageType . ack , ( id ) => { if ( id === message . data . id ) { this . port . removeEventListener ( 'message' , ackListner ) ; promiseResolver . resolve ( ) ; } } ) ; const timeout = timeoutPromise < SecureChannel > ( OpenYoloInternalError . ackTimeout ( ) , ACK_TIMEOUT_MS ) ; timeout . catch ( ( err ) => { this . port . removeEventListener ( 'message' , ackListner ) ; } ) ; this . port . addEventListener ( 'message' , ackListner ) ; this . send ( message ) ; return Promise . race ( [ timeout , promiseResolver . promise ] ) as Promise < void > ; } listen < T extends RpcMessageType > ( messageType , listener ) { if ( ! messageType || ! listener ) { throw OpenYoloInternalError . illegalStateError ( 'invalid type or listener' ) ; } let portListener = createMessageListener ( messageType , ( data , type , event ) => { const anyData = data as any ; if ( anyData . ack ) { this . port . postMessage ( ackMessage ( anyData . id ) ) ; } listener ( data , type , event ) ; } ) ; let listenerPair = { portListener , wrappedListener : listener } ; this . listeners . push ( listenerPair ) ; return this . listeners . length - 1 ; } addFallbackListener ( fallbackListener ) { this . fallbackListeners . push ( fallbackListener ) ; } unlisten ( key ) { let listenerPair = this . listeners [ key ] ; if ( ! listenerPair ) { return null ; } this . listeners [ key ] = null ; return listenerPair . wrappedListener ; } dispose ( ) { this . port . removeEventListener ( 'message' , this . mainListener ) ; this . listeners = [ ] ; this . fallbackListeners = [ ] ; this . port . close ( ) ; } }