webplayer.js

const { fetchApi } = require('./util/fetch')

/**
 * @typedef {object} WebPlayerFeatureSet
 * @property {boolean} files
 * @property {boolean} apps
 * @property {boolean} network
 * @property {boolean} coretrace
 * @property {boolean} messaging
 * @property {boolean} settings
 * @property {boolean} frida
 * @property {boolean} console
 * @property {boolean} portForwarding
 * @property {boolean} sensors
 * @property {boolean} snapshots
 */

/**
 * @typedef {object} WebPlayerSession
 * @property {string} projectId.required - The identifier of the project this session is tied to
 * @property {string} identifier - The identifier of this Web Player session
 * @property {string} instanceId - The identifier of the instance this session is tied to
 * @property {WebPlayerFeatureSet} features - Frontend feature set
 * @property {object} permissions - Endpoint permissions (optional)
 * @property {string?} token - The session's JWT
 * @property {string?} expiration - Session expiration in simplified extended ISO format ([ISO 8601]{@link https://en.wikipedia.org/wiki/ISO_8601})
 */

/**
 * @typedef {object} Response
 * @property {number} statusCode - [HTTP Status Code]{@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status}
 * @property {object} result - JSON encoded error or empty object if successful
 */

/**
 * Instances of this class
 * are returned from {@link Instance#webplayer}. They should not be created using the constructor.
 * @hideconstructor
 */
class WebPlayer {
  constructor (project, instanceId, features, permissions) {
    this._onDestroy = () => {}
    this._project = project
    this._session = {
      features,
      permissions,
      projectId: project.id,
      instanceId,
      token: null,
      expiration: null,
      identifier: null
    }
  }

  static async _fetch (args) {
    const { project, sessionId = undefined, options = {} } = args
    const endpoint = sessionId ? `/webplayer/${sessionId}` : '/webplayer'
    return fetchApi(project, endpoint, options)
  }

  /**
   * Returns information about the Web Player session
   * @returns {WebPlayerSession}
   *
   * @example
   * let sessionInfo = webPlayerInst.info
   */
  get info () {
    return this._session
  }

  /**
   * Lists all active Web Player sessions
   * @param {object} project
   * @returns {Promise<Array<WebPlayerSession>>}
   *
   * @example
   * const sessions = webPlayerInst.sessions()
   * session.forEach(session => console.log(`${session.userId} session expires at ${session.expiration}`))
   */
  static async sessions (project) {
    return await WebPlayer._fetch({
      project,
      options: { method: 'GET' }
    })
  }

  /**
   * Updates and returns information about the Web Player session
   * @returns {Promise<WebPlayerSession>}
   *
   * @example
   * let sessionInfo = webPlayerInst.refreshSession()
   */
  async refreshSession () {
    const sessionId = this._session.identifier
    if (sessionId) {
      // TODO: What happens if the record is gone? Auto destroy self?
      const result = await WebPlayer._fetch({
        project: this._project,
        sessionId,
        options: { method: 'GET' }
      })
      if (Array.isArray(result) && result[0]) {
        // Update local data
        this._session = Object.assign(this._session, result[0])
      } else {
        console.warn(`WebPlayer session ${sessionId} not found`)
      }
    }
    return this._session
  }

  /*
   * Create a Web Player session
   * @param {number} expiresIn - Number of seconds until the token expires
   * @param {function} onDestroy - Callback when destroyed
   * @returns {Promise<WebPlayerSession>}
   *
   * @example
   * // Create a session token with a 10-minute expiration
   * let wpSession = await webPlayerInst.sessionToken(600)
   */
  async _createSession (expiresIn, onDestroy) {
    const newSession = await WebPlayer._fetch({
      project: this._project,
      options: {
        method: 'POST',
        json: {
          projectId: this._session.projectId,
          instanceId: this._session.instanceId,
          features: this._session.features,
          permissions: this._session.permissions ? this._session.permissions : null,
          expiresIn
        }
      }
    })
    this._onDestroy = onDestroy
    this._session = Object.assign(this._session, newSession)
    return this._session
  }

  /**
   * Destroy the Web Player session
   * @param {string} session
   * @returns {Promise<Response>}
   *
   * @example
   * await webPlayerInst.destroySession()
   */
  async destroy (session) {
    const onDestroy = this._onDestroy
    const sessionId = session || this._session.identifier
    this._session.identifier = null
    this._session.token = null
    this._session.expiration = null
    this._onDestroy = null
    const result = await WebPlayer._fetch({
      project: this._project,
      sessionId,
      options: { method: 'DELETE' }
    })
    if (typeof onDestroy === 'function') {
      onDestroy()
    }
    return result
  }
}

module.exports = WebPlayer