OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity / lib / device-linker.js

87 lines · javascript ref:
import BlindPairing from 'blind-pairing'
import b4a from 'b4a'

/**
 * DeviceLinker — Blind-pairing device linking for mnemonic exchange
 *
 * Uses blind-pairing over Hyperswarm for secure mnemonic transfer
 * between devices owned by the same user.
 */
export class DeviceLinker {
  constructor (swarm, opts = {}) {
    this._swarm = swarm
    this._identityManager = opts.identityManager || null
    this._onLinked = opts.onLinked || null // callback(mnemonic, remoteInfo)
  }

  /**
   * Create a link invite (source device)
   * Generates invite hex that the target device uses to pair.
   * @returns {Promise<{ invite: string, promise: Promise<Object> }>}
   */
  async createInvite () {
    return new Promise((resolve, reject) => {
      const identityManager = this._identityManager
      const mnemonic = identityManager ? identityManager.getMnemonic() : null
      if (!mnemonic) {
        reject(new Error('No mnemonic available to share'))
        return
      }

      const member = BlindPairing.member(this._swarm, {
        onadd: (candidate) => {
          candidate.open(b4a.from(JSON.stringify({
            mnemonic,
            instanceKey: identityManager ? identityManager.getPublicKeyHex() : null
          })))
        }
      })

      member.invite().then(invite => {
        resolve({
          invite: b4a.toString(invite, 'hex'),
          member
        })
      }).catch(reject)
    })
  }

  /**
   * Join with invite (target device)
   * Uses invite hex from source device to receive mnemonic.
   * @param {string} inviteHex - Hex-encoded invite from source
   * @returns {Promise<Object>} { mnemonic, instanceKey }
   */
  async joinWithInvite (inviteHex) {
    const identityManager = this._identityManager
    const onLinked = this._onLinked

    return new Promise((resolve, reject) => {
      const invite = b4a.from(inviteHex, 'hex')

      const candidate = BlindPairing.candidate(this._swarm, invite, {
        onopen: (data) => {
          try {
            const payload = JSON.parse(b4a.toString(data))
            if (identityManager && payload.mnemonic) {
              identityManager.setMnemonic(payload.mnemonic).catch(() => {})
            }
            if (onLinked) onLinked(payload.mnemonic, payload)
            resolve(payload)
          } catch (err) {
            reject(err)
          }
        }
      })

      candidate.on('error', reject)
    })
  }

  destroy () {
    // Cleanup if needed
  }
}

export default DeviceLinker