/**
* GatewayIdentity - Integrates with hyperident for service identity management
*
* Responsibilities:
* - Derive deterministic Hyperswarm topics from hyperident profiles
* - Assign virtual IPs via hashing
* - Manage service registry
*/
import crypto from 'hypercore-crypto'
import fs from 'bare-fs/promises'
import os from 'bare-os'
import path from 'bare-path'
export class GatewayIdentity {
constructor(config = {}) {
this.hyperidentPath = config.hyperidentPath || path.join(os.homedir(), '.hyperident')
this.subnet = config.subnet || '10.22.0.0/16'
this.ipAssignmentStrategy = config.ipAssignmentStrategy || 'hash'
this.staticMappings = config.staticMappings || {}
this.gatewayIP = config.gatewayIP || null // Store gateway's own IP
// Parse subnet
const [network, cidr] = this.subnet.split('/')
this.networkPrefix = network.split('.').slice(0, 2).join('.') // e.g., '10.22'
this.cidr = parseInt(cidr)
// Service registry: virtualIP → service info
this.services = new Map()
// Topic cache: profileName → topic
this.topicCache = new Map()
}
/**
* Ensure ~/.hyperident/ exists with a valid identity.
* Auto-generates one if missing using hypercore-crypto.
*/
async ensureIdentity () {
const identityPubPath = path.join(this.hyperidentPath, 'identity.pub')
try {
await fs.access(identityPubPath)
return // Identity exists
} catch {}
console.log('[Gateway] No hyperident identity found — generating...')
// Generate identity keypair (root identity for this device)
const identityKp = crypto.keyPair()
// Generate device keypair (unique to this machine)
const deviceKp = crypto.keyPair()
// Create directory with restricted permissions
await fs.mkdir(this.hyperidentPath, { recursive: true, mode: 0o700 })
// Write identity files
await fs.writeFile(
identityPubPath,
identityKp.publicKey.toString('hex'),
{ mode: 0o644 }
)
await fs.writeFile(
path.join(this.hyperidentPath, 'identity'),
identityKp.secretKey.toString('hex'),
{ mode: 0o600 }
)
await fs.writeFile(
path.join(this.hyperidentPath, 'device.pub'),
deviceKp.publicKey.toString('hex'),
{ mode: 0o644 }
)
await fs.writeFile(
path.join(this.hyperidentPath, 'device.key'),
deviceKp.secretKey.toString('hex'),
{ mode: 0o600 }
)
// Device metadata
const hostname = os.hostname()
await fs.writeFile(
path.join(this.hyperidentPath, 'device.json'),
JSON.stringify({
hostname,
created: new Date().toISOString(),
devicePublicKey: deviceKp.publicKey.toString('hex'),
identityPublicKey: identityKp.publicKey.toString('hex')
}, null, 2),
{ mode: 0o644 }
)
// Default profile based on hostname
const profileAddress = crypto.hash(
Buffer.concat([identityKp.publicKey, Buffer.from(hostname)])
).toString('hex')
await fs.writeFile(
path.join(this.hyperidentPath, 'profiles.json'),
JSON.stringify({
version: '1.0.0',
profiles: {
[hostname]: {
name: hostname,
description: 'Auto-generated by hyperident-gateway',
created: new Date().toISOString(),
lastModified: new Date().toISOString(),
address: profileAddress
}
}
}, null, 2),
{ mode: 0o644 }
)
console.log(`[Gateway] Identity created:`)
console.log(` Identity: ${identityKp.publicKey.toString('hex').substring(0, 16)}...`)
console.log(` Device: ${deviceKp.publicKey.toString('hex').substring(0, 16)}...`)
console.log(` Profile: ${hostname}`)
console.log(` Path: ${this.hyperidentPath}`)
}
/**
* Load hyperident identity (similar to IdentityManager.loadIdentity)
* For now, we'll use a simplified version that just reads profiles
*/
async loadProfiles() {
// Ensure identity exists before trying to read
await this.ensureIdentity()
try {
const profilesPath = path.join(this.hyperidentPath, 'profiles.json')
const data = await fs.readFile(profilesPath, 'utf8')
const profilesData = JSON.parse(data)
return profilesData.profiles || {}
} catch (error) {
throw new Error(`Failed to load hyperident profiles: ${error.message}`)
}
}
/**
* Derive Hyperswarm topic from hyperident profile
* This mimics what hyperident's IdentityManager.deriveTopicFromProfile() does
*
* For now, we'll use a simplified approach:
* - Hash the profile name with a consistent algorithm
* - Return 32-byte topic
*
* TODO: Integrate directly with hyperident's IdentityManager for true derivation
*/
async deriveServiceTopic(profileName) {
// Check cache first
if (this.topicCache.has(profileName)) {
return this.topicCache.get(profileName)
}
// For MVP: Simple hash-based topic derivation
// This won't match hyperident's keet-identity-key derivation exactly
// but will be deterministic and unique
const profileHash = crypto.hash(Buffer.from(profileName, 'utf8'))
const topic = profileHash.subarray(0, 32) // 32-byte topic
this.topicCache.set(profileName, topic)
return topic
}
/**
* Assign virtual IP from topic
* Uses hash-based assignment for determinism
*/
assignIP(topic) {
// Check static mappings first
// (Will implement after we have service name)
const hash = crypto.hash(topic)
if (this.cidr === 16) {
// /16 subnet: 10.22.X.X
const octet3 = hash[0]
const octet4 = hash[1]
return `${this.networkPrefix}.${octet3}.${octet4}`
} else if (this.cidr === 24) {
// /24 subnet: 10.22.0.X
const octet4 = hash[0] % 254 + 1 // Avoid .0 and .255
return `${this.networkPrefix}.0.${octet4}`
} else {
throw new Error(`Unsupported subnet CIDR: /${this.cidr}. Use /16 or /24`)
}
}
/**
* Check for IP collision
*/
hasCollision(ip) {
return this.services.has(ip)
}
/**
* Register a service
* @param {Object} serviceConfig - { name, profile, port, transport }
* @returns {Object} - { name, ip, topic, port, transport }
*/
async registerService(serviceConfig) {
const { name, profile, port, transport = 'tcp' } = serviceConfig
if (!name || !profile || !port) {
throw new Error('Service registration requires: name, profile, port')
}
// Check if hyperident profile exists
const profiles = await this.loadProfiles()
if (!profiles[profile]) {
throw new Error(`Hyperident profile '${profile}' not found. Create it with: hyperident create-profile ${profile}`)
}
// Derive topic from profile
const topic = await this.deriveServiceTopic(profile)
// Assign IP (with static override support)
let ip
if (this.staticMappings[name]) {
ip = this.staticMappings[name]
console.log(`📌 Using static IP mapping for ${name}: ${ip}`)
} else {
ip = this.assignIP(topic)
}
// Check for collision
if (this.hasCollision(ip)) {
const existing = this.services.get(ip)
throw new Error(`IP collision: ${ip} already assigned to service '${existing.name}'. Use static mapping or different profile.`)
}
// Store service
const service = {
name,
profile,
ip,
topic: topic.toString('hex'),
topicBuffer: topic,
port,
transport,
connections: [], // Will be populated by HyperswarmRouter
registered: new Date().toISOString()
}
this.services.set(ip, service)
console.log(`✅ Registered service: ${name}`)
console.log(` Profile: ${profile}`)
console.log(` Virtual IP: ${ip}:${port}`)
console.log(` Topic: ${topic.toString('hex').substring(0, 16)}...`)
return service
}
/**
* Lookup service by virtual IP
*/
lookup(virtualIP) {
return this.services.get(virtualIP)
}
/**
* List all registered services
*/
listServices() {
return Array.from(this.services.values())
}
/**
* Get all IPs owned by this gateway
* Used for announcement protocol in P2P VPN
* @returns {Array<string>} - Array of IP addresses
*/
getOwnedIPs() {
const ips = []
// Only announce gateway's own IP
// Service IPs are handled locally and not routed via P2P
if (this.gatewayIP) {
ips.push(this.gatewayIP)
}
return ips
}
/**
* Get service by name
*/
getServiceByName(name) {
for (const service of this.services.values()) {
if (service.name === name) {
return service
}
}
return null
}
/**
* Update service connection
*/
updateConnection(virtualIP, connection) {
const service = this.services.get(virtualIP)
if (service) {
service.connections.push(connection)
}
}
/**
* Remove service connection
*/
removeConnection(virtualIP, connection) {
const service = this.services.get(virtualIP)
if (service) {
service.connections = service.connections.filter(c => c !== connection)
}
}
/**
* Export service mappings for debugging/monitoring
*/
exportMappings() {
const mappings = {}
for (const [ip, service] of this.services.entries()) {
mappings[ip] = {
name: service.name,
profile: service.profile,
topic: service.topic,
port: service.port,
activeConnections: service.connections.length
}
}
return mappings
}
}