/**
* DeviceMeshManager - Identity-based device mesh networking
* Enables automatic device discovery, status monitoring, and real-time sync
*/
import { randomBytes, createHash } from 'node:crypto'
import { IdentityManager } from './identity-manager.js'
let Hyperswarm, b4a, idEncoding
// Lazy load hyperswarm dependencies with graceful fallback
async function loadNetworkDependencies() {
if (Hyperswarm && b4a && idEncoding) return true
try {
const hyperswarmModule = await import('hyperswarm')
const b4aModule = await import('b4a')
const idEncodingModule = await import('hypercore-id-encoding')
Hyperswarm = hyperswarmModule.default
b4a = b4aModule.default
idEncoding = idEncodingModule.default
return true
} catch (error) {
return false
}
}
/**
* DeviceMeshManager handles identity-based device mesh networking
*/
export class DeviceMeshManager {
constructor() {
this.swarm = null
this.isJoined = false
this.meshDiscoveryKey = null
this.connectedDevices = new Map()
this.syncInterval = null
this.statusCheckInterval = null
this.eventHandlers = new Map()
}
/**
* Derive mesh discovery key from identity
* All devices with the same identity will use the same discovery key
* @param {string} identityPublicKey - Hex-encoded identity public key
* @returns {Buffer} 32-byte mesh discovery key
*/
deriveMeshDiscoveryKey(identityPublicKey) {
const seed = `hyperident-mesh-${identityPublicKey}`
return createHash('sha256').update(seed).digest()
}
/**
* Join the identity mesh network
* @param {string} passphrase - Passphrase if identity is encrypted
* @returns {Promise<boolean>} Success status
*/
async joinMesh(passphrase = null) {
const networkAvailable = await loadNetworkDependencies()
if (!networkAvailable) {
throw new Error('Network dependencies not available. Install with: npm install hyperswarm hypercore-id-encoding b4a')
}
if (this.isJoined) {
throw new Error('Already joined mesh. Leave current mesh first.')
}
// Check if identity exists
if (!IdentityManager.identityExists()) {
throw new Error('Identity not found. Run "hyperident init" first.')
}
// Load identity to get public key
const identity = await IdentityManager.loadIdentity(passphrase)
const identityPublicKey = identity.publicKey
// Derive mesh discovery key from identity
this.meshDiscoveryKey = this.deriveMeshDiscoveryKey(identityPublicKey)
// Create hyperswarm instance
this.swarm = new Hyperswarm()
this.isJoined = true
// Handle connections
this.swarm.on('connection', (connection, info) => {
this.handleMeshConnection(connection, info)
})
// Join the mesh discovery key
const discovery = this.swarm.join(this.meshDiscoveryKey, { server: true, client: true })
await discovery.flushed()
// Start periodic sync and status checks
this.startPeriodicSync()
this.startStatusMonitoring()
return true
}
/**
* Leave the identity mesh network
* @returns {Promise<void>}
*/
async leaveMesh() {
if (!this.isJoined) return
this.isJoined = false
// Stop periodic tasks
this.stopPeriodicSync()
this.stopStatusMonitoring()
// Clean up connections
await this.cleanup()
}
/**
* Handle incoming mesh connection
* @param {Object} connection - Hyperswarm connection
* @param {Object} info - Connection info
*/
async handleMeshConnection(connection, info) {
const deviceId = connection.remotePublicKey.toString('hex')
// Add to connected devices
this.connectedDevices.set(deviceId, {
connection,
lastSeen: Date.now(),
status: 'online',
info
})
console.log(`📱 Device connected: ${deviceId.slice(0, 8)}...`)
// Set up connection handlers
connection.on('data', (data) => {
this.handleMeshMessage(deviceId, data)
})
connection.on('close', () => {
this.connectedDevices.delete(deviceId)
console.log(`📱 Device disconnected: ${deviceId.slice(0, 8)}...`)
this.emit('device-disconnected', deviceId)
})
connection.on('error', (error) => {
console.error(`Connection error with ${deviceId.slice(0, 8)}...:`, error.message)
this.connectedDevices.delete(deviceId)
})
// Send device info handshake
await this.sendDeviceInfo(connection)
this.emit('device-connected', deviceId)
}
/**
* Handle mesh message from another device
* @param {string} deviceId - Device ID
* @param {Buffer} data - Message data
*/
async handleMeshMessage(deviceId, data) {
try {
const message = JSON.parse(data.toString('utf8'))
switch (message.type) {
case 'device-info':
await this.handleDeviceInfo(deviceId, message.data)
break
case 'profile-sync':
await this.handleProfileSync(deviceId, message.data)
break
case 'status-ping':
await this.handleStatusPing(deviceId, message.data)
break
case 'status-pong':
await this.handleStatusPong(deviceId, message.data)
break
default:
console.warn(`Unknown mesh message type: ${message.type}`)
}
} catch (error) {
console.error(`Error handling mesh message from ${deviceId.slice(0, 8)}...:`, error.message)
}
}
/**
* Send device info to connected device
* @param {Object} connection - Connection to send to
*/
async sendDeviceInfo(connection) {
try {
const deviceChain = await IdentityManager.loadDeviceChain()
const deviceInfo = {
name: deviceChain.metadata.name,
publicKey: deviceChain.metadata.publicKey,
created: deviceChain.metadata.created,
version: deviceChain.metadata.version
}
const message = {
type: 'device-info',
data: deviceInfo,
timestamp: Date.now()
}
connection.write(Buffer.from(JSON.stringify(message), 'utf8'))
} catch (error) {
console.error('Error sending device info:', error.message)
}
}
/**
* Handle device info from another device
* @param {string} deviceId - Device ID
* @param {Object} deviceInfo - Device information
*/
async handleDeviceInfo(deviceId, deviceInfo) {
const device = this.connectedDevices.get(deviceId)
if (device) {
device.info = { ...device.info, ...deviceInfo }
device.lastSeen = Date.now()
this.emit('device-info-updated', deviceId, deviceInfo)
}
}
/**
* Sync profiles with all connected devices
* @returns {Promise<void>}
*/
async syncProfiles() {
if (!this.isJoined || this.connectedDevices.size === 0) return
try {
const profilesData = await IdentityManager.listProfiles()
const syncMessage = {
type: 'profile-sync',
data: {
profiles: profilesData.profiles,
version: profilesData.version,
lastModified: profilesData.lastModified
},
timestamp: Date.now()
}
const messageBuffer = Buffer.from(JSON.stringify(syncMessage), 'utf8')
// Send to all connected devices
for (const [deviceId, device] of this.connectedDevices) {
try {
device.connection.write(messageBuffer)
} catch (error) {
console.error(`Error syncing profiles to ${deviceId.slice(0, 8)}...:`, error.message)
}
}
console.log(`📋 Synced profiles to ${this.connectedDevices.size} device(s)`)
} catch (error) {
console.error('Error syncing profiles:', error.message)
}
}
/**
* Handle profile sync from another device
* @param {string} deviceId - Device ID
* @param {Object} syncData - Profile sync data
*/
async handleProfileSync(deviceId, syncData) {
try {
// Simple conflict resolution: use most recent timestamp
const localProfiles = await IdentityManager.listProfiles()
if (syncData.lastModified > localProfiles.lastModified) {
// Remote profiles are newer, merge them
const mergedProfiles = { ...localProfiles.profiles, ...syncData.profiles }
// Update local profiles
const updatedProfilesData = {
version: Math.max(localProfiles.version, syncData.version),
profiles: mergedProfiles,
lastModified: Math.max(localProfiles.lastModified, syncData.lastModified)
}
await IdentityManager.writeProfiles(updatedProfilesData)
console.log(`📋 Merged profiles from device ${deviceId.slice(0, 8)}...`)
this.emit('profiles-synced', deviceId, syncData)
}
} catch (error) {
console.error(`Error handling profile sync from ${deviceId.slice(0, 8)}...:`, error.message)
}
}
/**
* Send status ping to all connected devices
*/
async sendStatusPing() {
if (!this.isJoined || this.connectedDevices.size === 0) return
const pingMessage = {
type: 'status-ping',
data: { timestamp: Date.now() },
timestamp: Date.now()
}
const messageBuffer = Buffer.from(JSON.stringify(pingMessage), 'utf8')
for (const [deviceId, device] of this.connectedDevices) {
try {
device.connection.write(messageBuffer)
} catch (error) {
console.error(`Error sending ping to ${deviceId.slice(0, 8)}...:`, error.message)
}
}
}
/**
* Handle status ping from another device
* @param {string} deviceId - Device ID
* @param {Object} pingData - Ping data
*/
async handleStatusPing(deviceId, pingData) {
const device = this.connectedDevices.get(deviceId)
if (device) {
device.lastSeen = Date.now()
// Send pong response
const pongMessage = {
type: 'status-pong',
data: {
timestamp: Date.now(),
pingTimestamp: pingData.timestamp
},
timestamp: Date.now()
}
try {
device.connection.write(Buffer.from(JSON.stringify(pongMessage), 'utf8'))
} catch (error) {
console.error(`Error sending pong to ${deviceId.slice(0, 8)}...:`, error.message)
}
}
}
/**
* Handle status pong from another device
* @param {string} deviceId - Device ID
* @param {Object} pongData - Pong data
*/
async handleStatusPong(deviceId, pongData) {
const device = this.connectedDevices.get(deviceId)
if (device) {
device.lastSeen = Date.now()
device.latency = Date.now() - pongData.pingTimestamp
this.emit('device-status-updated', deviceId, device)
}
}
/**
* Get status of all devices in the mesh
* @returns {Object} Device status information
*/
getDeviceStatus() {
const devices = []
const now = Date.now()
for (const [deviceId, device] of this.connectedDevices) {
devices.push({
id: deviceId,
name: device.info?.name || 'Unknown',
publicKey: device.info?.publicKey || 'Unknown',
status: device.status,
lastSeen: device.lastSeen,
timeSinceLastSeen: now - device.lastSeen,
latency: device.latency || null,
connected: device.connection && !device.connection.destroyed
})
}
return {
meshJoined: this.isJoined,
totalDevices: devices.length,
onlineDevices: devices.filter(d => d.connected).length,
devices
}
}
/**
* Get mesh status information (alias for getDeviceStatus for CLI compatibility)
* @returns {Promise<Object>} Mesh status information
*/
async getMeshStatus() {
if (!IdentityManager.identityExists()) {
throw new Error('Identity not found. Run "hyperident init" first.')
}
// Load identity and device info
const identity = await IdentityManager.loadIdentity()
const deviceChain = await IdentityManager.loadDeviceChain()
const deviceStatus = this.getDeviceStatus()
return {
connected: this.isJoined,
connectedDevices: deviceStatus.onlineDevices,
totalDevices: deviceStatus.totalDevices,
identityPublicKey: identity.publicKey,
thisDevicePublicKey: deviceChain.metadata.publicKey,
devices: deviceStatus.devices.map(device => ({
name: device.name,
publicKey: device.publicKey,
status: device.connected ? 'online' : 'offline',
lastSeen: device.lastSeen
}))
}
}
/**
* Start periodic profile synchronization
*/
startPeriodicSync() {
if (this.syncInterval) return
// Sync every 30 seconds
this.syncInterval = setInterval(() => {
this.syncProfiles()
}, 30000)
}
/**
* Stop periodic profile synchronization
*/
stopPeriodicSync() {
if (this.syncInterval) {
clearInterval(this.syncInterval)
this.syncInterval = null
}
}
/**
* Start device status monitoring
*/
startStatusMonitoring() {
if (this.statusCheckInterval) return
// Send status ping every 10 seconds
this.statusCheckInterval = setInterval(() => {
this.sendStatusPing()
}, 10000)
}
/**
* Stop device status monitoring
*/
stopStatusMonitoring() {
if (this.statusCheckInterval) {
clearInterval(this.statusCheckInterval)
this.statusCheckInterval = null
}
}
/**
* Clean up resources
*/
async cleanup() {
// Close all connections
for (const [deviceId, device] of this.connectedDevices) {
try {
device.connection.end()
} catch (error) {
// Ignore cleanup errors
}
}
this.connectedDevices.clear()
// Close swarm
if (this.swarm) {
try {
await this.swarm.destroy()
} catch (error) {
// Ignore cleanup errors
}
this.swarm = null
}
this.meshDiscoveryKey = null
}
/**
* Add event listener
* @param {string} event - Event name
* @param {Function} handler - Event handler
*/
on(event, handler) {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, [])
}
this.eventHandlers.get(event).push(handler)
}
/**
* Remove event listener
* @param {string} event - Event name
* @param {Function} handler - Event handler
*/
off(event, handler) {
const handlers = this.eventHandlers.get(event)
if (handlers) {
const index = handlers.indexOf(handler)
if (index !== -1) {
handlers.splice(index, 1)
}
}
}
/**
* Emit event
* @param {string} event - Event name
* @param {...any} args - Event arguments
*/
emit(event, ...args) {
const handlers = this.eventHandlers.get(event)
if (handlers) {
handlers.forEach(handler => {
try {
handler(...args)
} catch (error) {
console.error(`Error in event handler for ${event}:`, error.message)
}
})
}
}
/**
* Check if network dependencies are available
* @returns {Promise<boolean>} Availability status
*/
static async isNetworkAvailable() {
return await loadNetworkDependencies()
}
}