// lib/pairing-protocol.cjs - Device pairing protocol using Protomux
const Protomux = require('protomux')
const b4a = require('b4a')
const c = require('compact-encoding')
// Protocol identifier
const PAIRING_PROTOCOL = 'hyper-wallet-pairing'
/**
* Create pairing channel for device that is adding another device (server side)
* @param {Connection} conn - Hyperswarm connection
* @param {string} identityMnemonic - Identity mnemonic (24 words) to send to joining device
* @param {string} walletMnemonic - Wallet mnemonic (12 words) to send to joining device
* @param {string} deviceName - Name of this device
* @returns {Promise<Object>} { remoteDeviceName, remoteWriterKey }
*/
async function createPairingServer(conn, identityMnemonic, walletMnemonic, deviceName) {
return new Promise((resolve, reject) => {
const mux = Protomux.from(conn)
const channel = mux.createChannel({
protocol: PAIRING_PROTOCOL,
id: null
})
// Message types
const messages = {
// 0: Request pairing (client → server)
request: channel.addMessage({
encoding: {
preencode(state, m) {
c.string.preencode(state, m.deviceName)
c.buffer.preencode(state, m.writerKey)
},
encode(state, m) {
c.string.encode(state, m.deviceName)
c.buffer.encode(state, m.writerKey)
},
decode(state) {
return {
deviceName: c.string.decode(state),
writerKey: c.buffer.decode(state)
}
}
},
onmessage(msg) {
console.log(`📨 Received pairing request from: ${msg.deviceName}`)
// Send BOTH mnemonics to joining device
messages.response.send({
identityMnemonic,
walletMnemonic,
serverDeviceName: deviceName,
approved: true
})
// Resolve with remote device info
resolve({
remoteDeviceName: msg.deviceName,
remoteWriterKey: msg.writerKey
})
}
}),
// 1: Send mnemonics (server → client)
response: channel.addMessage({
encoding: {
preencode(state, m) {
c.string.preencode(state, m.identityMnemonic)
c.string.preencode(state, m.walletMnemonic)
c.string.preencode(state, m.serverDeviceName)
c.bool.preencode(state, m.approved)
},
encode(state, m) {
c.string.encode(state, m.identityMnemonic)
c.string.encode(state, m.walletMnemonic)
c.string.encode(state, m.serverDeviceName)
c.bool.encode(state, m.approved)
},
decode(state) {
return {
identityMnemonic: c.string.decode(state),
walletMnemonic: c.string.decode(state),
serverDeviceName: c.string.decode(state),
approved: c.bool.decode(state)
}
}
}
})
}
channel.open()
// Timeout after 60 seconds
const timeout = setTimeout(() => {
reject(new Error('Pairing timeout - no response from joining device'))
}, 60000)
// Clear timeout when resolved
const originalResolve = resolve
resolve = (value) => {
clearTimeout(timeout)
originalResolve(value)
}
})
}
/**
* Create pairing channel for device that is joining (client side)
* @param {Connection} conn - Hyperswarm connection
* @param {Buffer} writerKey - Writer key of this device
* @param {string} deviceName - Name of this device
* @returns {Promise<Object>} { identityMnemonic, walletMnemonic } received from server
*/
async function createPairingClient(conn, writerKey, deviceName) {
return new Promise((resolve, reject) => {
const mux = Protomux.from(conn)
const channel = mux.createChannel({
protocol: PAIRING_PROTOCOL,
id: null
})
// Message types
const messages = {
// 0: Request pairing (client → server)
request: channel.addMessage({
encoding: {
preencode(state, m) {
c.string.preencode(state, m.deviceName)
c.buffer.preencode(state, m.writerKey)
},
encode(state, m) {
c.string.encode(state, m.deviceName)
c.buffer.encode(state, m.writerKey)
},
decode(state) {
return {
deviceName: c.string.decode(state),
writerKey: c.buffer.decode(state)
}
}
}
}),
// 1: Receive mnemonics (server → client)
response: channel.addMessage({
encoding: {
preencode(state, m) {
c.string.preencode(state, m.identityMnemonic)
c.string.preencode(state, m.walletMnemonic)
c.string.preencode(state, m.serverDeviceName)
c.bool.preencode(state, m.approved)
},
encode(state, m) {
c.string.encode(state, m.identityMnemonic)
c.string.encode(state, m.walletMnemonic)
c.string.encode(state, m.serverDeviceName)
c.bool.encode(state, m.approved)
},
decode(state) {
return {
identityMnemonic: c.string.decode(state),
walletMnemonic: c.string.decode(state),
serverDeviceName: c.string.decode(state),
approved: c.bool.decode(state)
}
}
},
onmessage(msg) {
if (!msg.approved) {
reject(new Error('Pairing rejected by server'))
return
}
console.log(`✅ Received mnemonics from: ${msg.serverDeviceName}`)
resolve({
identityMnemonic: msg.identityMnemonic,
walletMnemonic: msg.walletMnemonic
})
}
})
}
channel.open()
// Send pairing request immediately (channel is ready after open())
console.log(`📤 Sending pairing request as: ${deviceName}`)
messages.request.send({
deviceName,
writerKey
})
// Timeout after 60 seconds
const timeout = setTimeout(() => {
reject(new Error('Pairing timeout - no response from server'))
}, 60000)
// Clear timeout when resolved
const originalResolve = resolve
resolve = (value) => {
clearTimeout(timeout)
originalResolve(value)
}
})
}
module.exports = {
createPairingServer,
createPairingClient
}