text: /**
   * saltyrtc-task-webrtc v0.14.1
   * A SaltyRTC WebRTC task v1 implementation.
   * https://github.com/saltyrtc/saltyrtc-task-webrtc-js#readme
   *
   * Copyright (C) 2016-2019 Threema GmbH
   *
   * This software may be modified and distributed under the terms
   * of the MIT license:
   * 
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to deal
   * in the Software without restriction, including without limitation the rights
   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   * copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   * 
   * The above copyright notice and this permission notice shall be included in all
   * copies or substantial portions of the Software.
   * 
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   * SOFTWARE.
   */
  'use strict';
  
  class DataChannelNonce {
      constructor(cookie, channelId, overflow, sequenceNumber) {
          this.cookie = cookie;
          this.overflow = overflow;
          this.sequenceNumber = sequenceNumber;
          this.channelId = channelId;
      }
      get combinedSequenceNumber() {
          return (this.overflow * (Math.pow(2, 32))) + this.sequenceNumber;
      }
      static fromUint8Array(data) {
          if (data.byteLength !== this.TOTAL_LENGTH) {
              throw new saltyrtcClient.exceptions.ValidationError('Bad packet length');
          }
          const view = new DataView(data.buffer, data.byteOffset, this.TOTAL_LENGTH);
          const slice = new Uint8Array(data.buffer, data.byteOffset, saltyrtcClient.Cookie.COOKIE_LENGTH);
          const cookie = new saltyrtcClient.Cookie(slice);
          const channelId = view.getUint16(16);
          const overflow = view.getUint16(18);
          const sequenceNumber = view.getUint32(20);
          return new DataChannelNonce(cookie, channelId, overflow, sequenceNumber);
      }
      toUint8Array() {
          const buffer = new Uint8Array(DataChannelNonce.TOTAL_LENGTH);
          buffer.set(this.cookie.bytes);
          const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
          view.setUint16(16, this.channelId);
          view.setUint16(18, this.overflow);
          view.setUint32(20, this.sequenceNumber);
          return buffer;
      }
  }
  DataChannelNonce.TOTAL_LENGTH = 24;
  
  class DataChannelCryptoContext {
      constructor(channelId, signaling) {
          this.lastIncomingCsn = null;
          this.channelId = channelId;
          this.signaling = signaling;
          this.cookiePair = new saltyrtcClient.CookiePair();
          this.csnPair = new saltyrtcClient.CombinedSequencePair();
      }
      encrypt(data) {
          const csn = this.csnPair.ours.next();
          const nonce = new DataChannelNonce(this.cookiePair.ours, this.channelId, csn.overflow, csn.sequenceNumber);
          return this.signaling.encryptForPeer(data, nonce.toUint8Array());
      }
      decrypt(box) {
          let nonce;
          try {
              nonce = DataChannelNonce.fromUint8Array(box.nonce);
          }
          catch (error) {
              throw new saltyrtcClient.exceptions.ValidationError(`Unable to create nonce, reason: ${error}`);
          }
          if (nonce.cookie.equals(this.cookiePair.ours)) {
              throw new saltyrtcClient.exceptions.ValidationError('Local and remote cookie are equal');
          }
          if (this.cookiePair.theirs === null || this.cookiePair.theirs === undefined) {
              this.cookiePair.theirs = nonce.cookie;
          }
          else if (!nonce.cookie.equals(this.cookiePair.theirs)) {
              throw new saltyrtcClient.exceptions.ValidationError('Remote cookie changed');
          }
          if (this.lastIncomingCsn !== null &&
              nonce.combinedSequenceNumber === this.lastIncomingCsn) {
              throw new saltyrtcClient.exceptions.ValidationError('CSN reuse detected');
          }
          if (nonce.channelId !== this.channelId) {
              const error = 'Data channel id in nonce does not match';
              throw new saltyrtcClient.exceptions.ValidationError(error);
          }
          this.lastIncomingCsn = nonce.combinedSequenceNumber;
          return this.signaling.decryptFromPeer(box);
      }
  }
  DataChannelCryptoContext.OVERHEAD_LENGTH = 40;
  DataChannelCryptoContext.NONCE_LENGTH = DataChannelNonce.TOTAL_LENGTH;
  
  class SignalingTransportLink {
      constructor(id, protocol) {
          this.label = 'saltyrtc-signaling';
          this.id = id;
          this.protocol = protocol;
          this.untie();
      }
      untie() {
          this.closed = () => { throw new Error('closed: Not tied to a SignalingTransport'); };
          this.receive = () => { throw new Error('receive: Not tied to a SignalingTransport'); };
      }
      tie(transport) {
          this.closed = transport.closed.bind(transport);
          this.receive = transport.receiveChunk.bind(transport);
      }
  }
  class SignalingTransport {
      constructor(link, handler, task, signaling, crypto, logLevel, maxChunkLength) {
          this.logTag = '[SaltyRTC.WebRTC.SignalingTransport]';
          this.messageId = 0;
          this.log = new saltyrtcClient.Log(logLevel);
          this.link = link;
          this.handler = handler;
          this.task = task;
          this.signaling = signaling;
          this.crypto = crypto;
          this.chunkLength = Math.min(this.handler.maxMessageSize, maxChunkLength);
          this.chunkBuffer = new ArrayBuffer(this.chunkLength);
          this.messageQueue = this.signaling.handoverState.peer ? null : [];
          this.unchunker = new chunkedDc.UnreliableUnorderedUnchunker();
          this.unchunker.onMessage = this.receiveMessage.bind(this);
          this.link.tie(this);
          this.log.info(this.logTag, 'Signaling transport created');
      }
      closed() {
          this.log.info('Closed (remote)');
          this.unbind();
          if (this.signaling.handoverState.any) {
              this.signaling.setState('closed');
          }
      }
      receiveChunk(chunk) {
          this.log.debug(this.logTag, 'Received chunk');
          try {
              this.unchunker.add(chunk);
          }
          catch (error) {
              this.log.error(this.logTag, 'Invalid chunk:', error);
              return this.die();
          }
      }
      receiveMessage(message) {
          this.log.debug(this.logTag, 'Received message');
          const box = saltyrtcClient.Box.fromUint8Array(message, DataChannelCryptoContext.NONCE_LENGTH);
          try {
              message = this.crypto.decrypt(box);
          }
          catch (error) {
              this.log.error(this.logTag, 'Invalid nonce:', error);
              return this.die();
          }
          if (!this.signaling.handoverState.peer) {
              this.messageQueue.push(message);
              return;
          }
          this.signaling.onSignalingPeerMessage(message);
      }
      flushMessageQueue() {
          if (!this.signaling.handoverState.peer) {
              throw new Error('Remote did not request handover');
          }
          for (const message of this.messageQueue) {
              this.signaling.onSignalingPeerMessage(message);
          }
          this.messageQueue = null;
      }
      send(message) {
          this.log.debug(this.logTag, 'Sending message');
          const box = this.crypto.encrypt(message);
          message = box.toUint8Array();
          const chunker = new chunkedDc.UnreliableUnorderedChunker(this.messageId++, message, this.chunkLength, this.chunkBuffer);
          for (let chunk of chunker) {
              this.log.debug(this.logTag, 'Sending chunk');
              try {
                  this.handler.send(chunk);
              }
              catch (error) {
                  this.log.error(this.logTag, 'Unable to send chunk:', error);
                  return this.die();
              }
          }
      }
      close() {
          try {
              this.handler.close();
          }
          catch (error) {
              this.log.error(this.logTag, 'Unable to close data channel:', error);
          }
          this.log.info('Closed (local)');
          this.unbind();
      }
      die() {
          this.log.warn(this.logTag, 'Closing task due to an error');
          this.task.close(saltyrtcClient.CloseCode.ProtocolError);
      }
      unbind() {
          this.link.untie();
          this.unchunker.onMessage = undefined;
      }
  }
  
  class WebRTCTaskBuilder {
      constructor() {
          this.version = 'v1';
          this.logLevel = 'none';
          this.handover = true;
          this.maxChunkLength = 262144;
      }
      withLoggingLevel(level) {
          this.logLevel = level;
          return this;
      }
      withVersion(version) {
          this.version = version;
          return this;
      }
      withHandover(on) {
          this.handover = on;
          return this;
      }
      withMaxChunkLength(length) {
          if (length <= chunkedDc.UNRELIABLE_UNORDERED_HEADER_LENGTH) {
              throw new Error('Maximum chunk length must be greater than chunking overhead');
          }
          this.maxChunkLength = length;
          return this;
      }
      build() {
          return new WebRTCTask(this.version, this.logLevel, this.handover, this.maxChunkLength);
      }
  }
  class WebRTCTask {
      constructor(version, logLevel, handover, maxChunkLength) {
          this.logTag = '[SaltyRTC.WebRTC]';
          this.initialized = false;
          this.exclude = new Set();
          this.link = null;
          this.transport = null;
          this.eventRegistry = new saltyrtcClient.EventRegistry();
          this.candidates = [];
          this.sendCandidatesTimeout = null;
          this.version = version;
          this.log = new saltyrtcClient.Log(logLevel);
          this.doHandover = handover;
          this.maxChunkLength = maxChunkLength;
      }
      set signaling(signaling) {
          this._signaling = signaling;
          this.logTag = '[SaltyRTC.WebRTC.' + signaling.role + ']';
      }
      get signaling() {
          return this._signaling;
      }
      init(signaling, data) {
          this.processExcludeList(data[WebRTCTask.FIELD_EXCLUDE]);
          this.processHandover(data[WebRTCTask.FIELD_HANDOVER]);
          if (this.version === 'v0') {
              this.processMaxPacketSize(data[WebRTCTask.FIELD_MAX_PACKET_SIZE]);
          }
          this.signaling = signaling;
          this.initialized = true;
      }
      processExcludeList(ids) {
          for (const id of ids) {
              this.exclude.add(id);
          }
          for (let i = 0; i < 65535; i++) {
              if (!this.exclude.has(i)) {
                  this.channelId = i;
                  break;
              }
          }
          if (this.channelId === undefined && this.doHandover) {
              throw new Error('No free data channel id can be found');
          }
      }
      processHandover(handover) {
          if (handover === false) {
              this.doHandover = false;
          }
      }
      processMaxPacketSize(remoteMaxPacketSize) {
          const localMaxPacketSize = this.maxChunkLength;
          if (!Number.isInteger(remoteMaxPacketSize)) {
              throw new RangeError(WebRTCTask.FIELD_MAX_PACKET_SIZE + ' field must be an integer');
          }
          if (remoteMaxPacketSize < 0) {
              throw new RangeError(WebRTCTask.FIELD_MAX_PACKET_SIZE + ' field must be positive');
          }
          else if (remoteMaxPacketSize > 0) {
              this.maxChunkLength = Math.min(localMaxPacketSize, remoteMaxPacketSize);
          }
          this.log.debug(this.logTag, `Max packet size: Local requested ${localMaxPacketSize}` +
              ` bytes, remote requested ${remoteMaxPacketSize} bytes. Using ${this.maxChunkLength}.`);
      }
      onPeerHandshakeDone() {
      }
      onDisconnected(id) {
          this.emit({ type: 'disconnected', data: id });
      }
      onTaskMessage(message) {
          this.log.debug(this.logTag, 'New task message arrived: ' + message.type);
          switch (message.type) {
              case 'offer':
                  if (this.validateOffer(message) !== true)
                      return;
                  this.emit({ type: 'offer', data: message['offer'] });
                  break;
              case 'answer':
                  if (this.validateAnswer(message) !== true)
                      return;
                  this.emit({ type: 'answer', data: message['answer'] });
                  break;
              case 'candidates':
                  if (this.validateCandidates(message) !== true)
                      return;
                  this.emit({ type: 'candidates', data: message['candidates'] });
                  break;
              case 'handover':
                  if (!this.doHandover) {
                      this.log.error(this.logTag, 'Received unexpected handover message from peer');
                      this.signaling.resetConnection(saltyrtcClient.CloseCode.ProtocolError);
                      break;
                  }
                  if (this.signaling.handoverState.peer) {
                      this.log.warn(this.logTag, 'Handover already received');
                      break;
                  }
                  this.signaling.handoverState.peer = true;
                  if (this.transport !== null) {
                      this.transport.flushMessageQueue();
                  }
                  if (this.signaling.handoverState.both) {
                      this.log.info(this.logTag, 'Handover to data channel finished');
                  }
                  break;
              default:
                  this.log.error(this.logTag, 'Received message with unknown type:', message.type);
          }
      }
      validateOffer(message) {
          if (message['offer'] === undefined) {
              this.log.warn(this.logTag, 'Offer message does not contain offer');
              return false;
          }
          if (message['offer']['sdp'] === undefined) {
              this.log.warn(this.logTag, 'Offer message does not contain offer sdp');
              return false;
          }
          return true;
      }
      validateAnswer(message) {
          if (message['answer'] === undefined) {
              this.log.warn(this.logTag, 'Answer message does not contain answer');
              return false;
          }
          if (message['answer']['sdp'] === undefined) {
              this.log.warn(this.logTag, 'Answer message does not contain answer sdp');
              return false;
          }
          return true;
      }
      validateCandidates(message) {
          if (message['candidates'] === undefined) {
              this.log.warn(this.logTag, 'Candidates message does not contain candidates');
              return false;
          }
          if (message['candidates'].length < 1) {
              this.log.warn(this.logTag, 'Candidates message contains empty candidate list');
              return false;
          }
          for (let candidate of message['candidates']) {
              if (candidate !== null) {
                  if (typeof candidate['candidate'] !== 'string'
                      && !(candidate['candidate'] instanceof String)) {
                      this.log.warn(this.logTag, 'Candidates message contains invalid candidate (candidate field)');
                      return false;
                  }
                  if (typeof candidate['sdpMid'] !== 'string'
                      && !(candidate['sdpMid'] instanceof String) && candidate['sdpMid'] !== null) {
                      this.log.warn(this.logTag, 'Candidates message contains invalid candidate (sdpMid field)');
                      return false;
                  }
                  if (candidate['sdpMLineIndex'] !== null
                      && !Number.isInteger(candidate['sdpMLineIndex'])) {
                      this.log.warn(this.logTag, 'Candidates message contains invalid candidate (sdpMLineIndex field)');
                      return false;
                  }
              }
          }
          return true;
      }
      sendSignalingMessage(payload) {
          if (this.signaling.getState() != 'task') {
              throw new saltyrtcClient.SignalingError(saltyrtcClient.CloseCode.ProtocolError, "Could not send signaling message: Signaling state is not 'task'.");
          }
          if (!this.signaling.handoverState.local) {
              throw new saltyrtcClient.SignalingError(saltyrtcClient.CloseCode.ProtocolError, "Could not send signaling message: Handover hasn't happened yet.");
          }
          if (this.transport === null) {
              throw new saltyrtcClient.SignalingError(saltyrtcClient.CloseCode.ProtocolError, 'Could not send signaling message: Data channel is not established, yet.');
          }
          this.transport.send(payload);
      }
      getName() {
          return `${this.version}.webrtc.tasks.saltyrtc.org`;
      }
      getSupportedMessageTypes() {
          return ['offer', 'answer', 'candidates', 'handover'];
      }
      getData() {
          const data = {};
          data[WebRTCTask.FIELD_EXCLUDE] = Array.from(this.exclude.values());
          data[WebRTCTask.FIELD_HANDOVER] = this.doHandover;
          if (this.version === 'v0') {
              data[WebRTCTask.FIELD_MAX_PACKET_SIZE] = this.maxChunkLength;
          }
          return data;
      }
      sendOffer(offer) {
          this.log.debug(this.logTag, 'Sending offer');
          try {
              this.signaling.sendTaskMessage({
                  'type': 'offer',
                  'offer': {
                      'type': offer.type,
                      'sdp': offer.sdp,
                  }
              });
          }
          catch (e) {
              if (e.name === 'SignalingError') {
                  this.log.error(this.logTag, 'Could not send offer:', e.message);
                  this.signaling.resetConnection(e.closeCode);
              }
          }
      }
      sendAnswer(answer) {
          this.log.debug(this.logTag, 'Sending answer');
          try {
              this.signaling.sendTaskMessage({
                  'type': 'answer',
                  'answer': {
                      'type': answer.type,
                      'sdp': answer.sdp,
                  }
              });
          }
          catch (e) {
              if (e.name === 'SignalingError') {
                  this.log.error(this.logTag, 'Could not send answer:', e.message);
                  this.signaling.resetConnection(e.closeCode);
              }
          }
      }
      sendCandidate(candidate) {
          this.sendCandidates([candidate]);
      }
      sendCandidates(candidates) {
          this.log.debug(this.logTag, 'Buffering', candidates.length, 'candidate(s)');
          this.candidates.push(...candidates);
          const sendFunc = () => {
              try {
                  this.log.debug(this.logTag, 'Sending', this.candidates.length, 'candidate(s)');
                  this.signaling.sendTaskMessage({
                      'type': 'candidates',
                      'candidates': this.candidates
                  });
              }
              catch (e) {
                  if (e.name === 'SignalingError') {
                      this.log.error(this.logTag, 'Could not send candidates:', e.message);
                      this.signaling.resetConnection(e.closeCode);
                  }
              }
              finally {
                  this.candidates = [];
                  this.sendCandidatesTimeout = null;
              }
          };
          if (this.sendCandidatesTimeout === null) {
              this.sendCandidatesTimeout = self.setTimeout(sendFunc, WebRTCTask.CANDIDATE_BUFFERING_MS);
          }
      }
      getTransportLink() {
          this.log.debug(this.logTag, 'Create signalling transport link');
          if (!this.doHandover) {
              throw new Error('Handover has not been negotiated');
          }
          if (this.channelId === undefined) {
              const error = 'Data channel id not set';
              throw new Error(error);
          }
          if (this.link === null) {
              this.link = new SignalingTransportLink(this.channelId, this.getName());
          }
          return this.link;
      }
      handover(handler) {
          this.log.debug(this.logTag, 'Initiate handover');
          if (!this.doHandover) {
              throw new Error('Handover has not been negotiated');
          }
          if (this.signaling.handoverState.local || this.transport !== null) {
              throw new Error('Handover already requested');
          }
          const crypto = this.createCryptoContext(this.channelId);
          this.transport = new SignalingTransport(this.link, handler, this, this.signaling, crypto, this.log.level, this.maxChunkLength);
          this.sendHandover();
      }
      sendHandover() {
          this.log.debug(this.logTag, 'Sending handover');
          try {
              this.signaling.sendTaskMessage({ 'type': 'handover' });
          }
          catch (e) {
              if (e.name === 'SignalingError') {
                  this.log.error(this.logTag, 'Could not send handover message', e.message);
                  this.signaling.resetConnection(e.closeCode);
              }
          }
          this.signaling.handoverState.local = true;
          if (this.signaling.handoverState.both) {
              this.log.info(this.logTag, 'Handover to data channel finished');
          }
      }
      createCryptoContext(channelId) {
          return new DataChannelCryptoContext(channelId, this.signaling);
      }
      close(reason) {
          this.log.debug(this.logTag, 'Closing signaling data channel:', saltyrtcClient.explainCloseCode(reason));
          if (this.transport !== null) {
              this.transport.close();
          }
          this.transport = null;
      }
      on(event, handler) {
          this.eventRegistry.register(event, handler);
      }
      once(event, handler) {
          const onceHandler = (ev) => {
              try {
                  handler(ev);
              }
              catch (e) {
                  this.off(ev.type, onceHandler);
                  throw e;
              }
              this.off(ev.type, onceHandler);
          };
          this.eventRegistry.register(event, onceHandler);
      }
      off(event, handler) {
          if (event === undefined) {
              this.eventRegistry.unregisterAll();
          }
          else {
              this.eventRegistry.unregister(event, handler);
          }
      }
      emit(event) {
          this.log.debug(this.logTag, 'New event:', event.type);
          const handlers = this.eventRegistry.get(event.type);
          for (let handler of handlers) {
              try {
                  this.callHandler(handler, event);
              }
              catch (e) {
                  this.log.error(this.logTag, 'Unhandled exception in', event.type, 'handler:', e);
              }
          }
      }
      callHandler(handler, event) {
          const response = handler(event);
          if (response === false) {
              this.eventRegistry.unregister(event.type, handler);
          }
      }
  }
  WebRTCTask.FIELD_EXCLUDE = 'exclude';
  WebRTCTask.FIELD_HANDOVER = 'handover';
  WebRTCTask.FIELD_MAX_PACKET_SIZE = 'max_packet_size';
  WebRTCTask.CANDIDATE_BUFFERING_MS = 5;
  
  export { DataChannelCryptoContext, WebRTCTaskBuilder };
  