/** * Persistent HyperDHT keypair management. * SERVER_SEED (32-byte hex) is the secret seed. * Clients connect using the derived public key. */ import fs from 'fs' import path from 'path' import DHT from 'hyperdht' import b4a from 'b4a' import crypto from 'hypercore-crypto' import dotenv from 'dotenv' import logger from '../utils/logger.js' dotenv.config() const log = logger.child('keys') /** * @param {string} [envPath] * @returns {{ seed: Uint8Array, keyPair: { publicKey: Uint8Array, secretKey: Uint8Array }, publicKeyHex: string, seedHex: string }} */ export function loadOrCreateKeyPair(envPath = '.env') { let seedHex = process.env.SERVER_SEED || process.env.SERVER_KEY // SERVER_KEY historically was a 32-byte topic seed; reuse as DHT seed if present if (!seedHex) { const seed = crypto.randomBytes(32) seedHex = b4a.toString(seed, 'hex') const publicKeyHex = b4a.toString(DHT.keyPair(seed).publicKey, 'hex') const line = `\nSERVER_SEED=${seedHex}\nSERVER_PUBLIC_KEY=${publicKeyHex}\n` fs.appendFileSync(envPath, line, { flag: 'a' }) log.info('Generated new SERVER_SEED and SERVER_PUBLIC_KEY', { path: path.resolve(envPath), }) } if (!/^[0-9a-fA-F]{64}$/.test(seedHex)) { throw new Error('SERVER_SEED / SERVER_KEY must be 64 hex characters (32 bytes)') } const seed = b4a.from(seedHex, 'hex') const keyPair = DHT.keyPair(seed) const publicKeyHex = b4a.toString(keyPair.publicKey, 'hex') // Keep PUBLIC_KEY in env for operator convenience if (process.env.SERVER_PUBLIC_KEY !== publicKeyHex) { try { let env = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '' if (env.includes('SERVER_PUBLIC_KEY=')) { env = env.replace(/SERVER_PUBLIC_KEY=.*/g, `SERVER_PUBLIC_KEY=${publicKeyHex}`) } else { env += `\nSERVER_PUBLIC_KEY=${publicKeyHex}\n` } if (!env.includes('SERVER_SEED=') && !process.env.SERVER_SEED) { // migrate old SERVER_KEY-only files if (!env.includes('SERVER_SEED=')) { env += `SERVER_SEED=${seedHex}\n` } } fs.writeFileSync(envPath, env) } catch (err) { log.warn('Could not update .env with SERVER_PUBLIC_KEY', { error: err.message }) } } return { seed, keyPair, publicKeyHex, seedHex } }