/**
* Hyper-Registry - P2P Name Registry using Autobase
*
* STATUS: Implementation sketch - not yet functional
*/
import Autobase from 'autobase'
import Hyperbee from 'hyperbee'
import crypto from 'hypercore-crypto'
import { mineProof, validateProof, getDifficulty } from './proof-of-work.js'
export class HyperRegistry {
constructor(corestore, options = {}) {
this.store = corestore
this.bootstrapKeys = options.bootstrapKeys || []
this.namespace = null
}
async ready() {
// Local writer for this device
this.localWriter = this.store.get({ name: 'hyper-registry-writer' })
await this.localWriter.ready()
// Create Autobase
this.base = new Autobase(this.store, {
localInput: this.localWriter,
autostart: true,
apply: this._apply.bind(this),
valueEncoding: 'json',
open: (store) => new Hyperbee(store.get('registry-view'), {
keyEncoding: 'utf-8',
valueEncoding: 'json'
})
})
await this.base.ready()
// Add bootstrap writers
for (const key of this.bootstrapKeys) {
try {
await this.base.addInput(Buffer.from(key, 'hex'))
} catch (err) {
console.warn(`Failed to add bootstrap: ${key.slice(0, 8)}...`)
}
}
// Load our namespace
await this._loadLocalNamespace()
}
async _loadLocalNamespace() {
const pubkey = this.localWriter.key.toString('hex')
for await (const entry of this.base.view.createReadStream({
gte: 'name:',
lt: 'name:\xff'
})) {
const ns = entry.value
if (ns.devices && ns.devices[pubkey]) {
this.namespace = entry.key.replace('name:', '')
console.log(`Loaded namespace: ${this.namespace}`)
return
}
}
}
// === Autobase Apply Function ===
async _apply(batch, view, base) {
for (const node of batch) {
const op = node.value
const writer = node.from.key.toString('hex')
const seq = base.linearizer.indexedLength
try {
switch (op.type) {
case 'claim-namespace':
await this._applyClaim(op, writer, seq, view)
break
case 'register-resource':
await this._applyRegister(op, writer, view)
break
case 'authorize-device':
await this._applyAuthorize(op, writer, view)
break
case 'revoke-device':
await this._applyRevoke(op, writer, view)
break
}
} catch (err) {
console.error(`Apply error: ${err.message}`)
}
}
}
async _applyClaim(op, writer, seq, view) {
const { name, rootKey, proof, initialDevice, rootSignature } = op
// Validate name format
if (!this._isValidName(name)) return
// Check not already claimed
const existing = await view.get(`name:${name}`)
if (existing) return
// Validate proof-of-work against ROOT key
const difficulty = getDifficulty(name)
if (!validateProof(name, rootKey, proof, difficulty)) return
// Validate initial device is signed by root
if (!this._verifySignature(initialDevice, rootKey, rootSignature)) return
// Accept claim
await view.put(`name:${name}`, {
owner: rootKey,
claimedAt: Date.now(),
seq,
proofNonce: proof.nonce,
devices: {
[initialDevice.key]: {
name: initialDevice.name,
authorizedAt: Date.now(),
authorizedBy: 'root',
canAuthorize: initialDevice.canAuthorize !== false,
canRevoke: initialDevice.canRevoke !== false,
resources: initialDevice.resources || null
}
},
revoked: {}
})
console.log(`[Registry] Namespace claimed: ${name}`)
}
async _applyRegister(op, writer, view) {
const { namespace, resource, metadata } = op
const ns = await view.get(`name:${namespace}`)
if (!ns) return
// Verify writer is authorized device
const device = ns.value.devices[writer]
if (!device) return
// Check device can write this resource
if (device.resources && !device.resources.includes(resource)) return
// Derive topic from ROOT key for consistency
const topic = this._deriveTopic(ns.value.owner, resource)
await view.put(`resource:${namespace}/${resource}`, {
namespace,
resource,
owner: ns.value.owner,
registeredBy: writer,
topic: topic.toString('hex'),
metadata: metadata || {},
registeredAt: Date.now()
})
console.log(`[Registry] Resource registered: ${namespace}/${resource}`)
}
async _applyAuthorize(op, writer, view) {
const { namespace, device, authorizedBy, signature } = op
const ns = await view.get(`name:${namespace}`)
if (!ns) return
// Verify authorizer has permission
const authorizer = ns.value.devices[authorizedBy]
const isRoot = authorizedBy === ns.value.owner
if (!isRoot && (!authorizer || !authorizer.canAuthorize)) return
// Verify signature
if (!this._verifyDeviceAuth(device, authorizedBy, signature)) return
// Check not already present
if (ns.value.devices[device.key]) return
if (ns.value.revoked[device.key]) return
// Add device
ns.value.devices[device.key] = {
name: device.name,
authorizedAt: Date.now(),
authorizedBy,
canAuthorize: device.canAuthorize || false,
canRevoke: device.canRevoke || false,
resources: device.resources || null
}
await view.put(`name:${namespace}`, ns.value)
console.log(`[Registry] Device authorized: ${device.name}`)
}
async _applyRevoke(op, writer, view) {
const { namespace, deviceKey, reason, revokedBy, signature } = op
const ns = await view.get(`name:${namespace}`)
if (!ns) return
// Verify revoker has permission
const revoker = ns.value.devices[revokedBy]
const isRoot = revokedBy === ns.value.owner
if (!isRoot && (!revoker || !revoker.canRevoke)) return
// Can't revoke self (unless root)
if (deviceKey === revokedBy && !isRoot) return
// Can't revoke root
if (deviceKey === ns.value.owner) return
// Verify signature
if (!this._verifyRevocation(deviceKey, revokedBy, signature)) return
// Move to revoked
const device = ns.value.devices[deviceKey]
if (!device) return
delete ns.value.devices[deviceKey]
ns.value.revoked[deviceKey] = {
...device,
revokedAt: Date.now(),
revokedBy,
reason
}
await view.put(`name:${namespace}`, ns.value)
console.log(`[Registry] Device revoked: ${device.name}`)
}
// === Helper Methods ===
_deriveTopic(rootPubkey, resourceName) {
return crypto.hash(Buffer.concat([
Buffer.from(rootPubkey, 'hex'),
Buffer.from(resourceName, 'utf-8')
])).subarray(0, 32)
}
_isValidName(name) {
return /^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$/.test(name) ||
/^[a-z0-9]$/.test(name)
}
_verifySignature(data, pubkey, signature) {
// TODO: Implement ed25519 signature verification
return true
}
_verifyDeviceAuth(device, authorizer, signature) {
// TODO: Implement
return true
}
_verifyRevocation(deviceKey, revoker, signature) {
// TODO: Implement
return true
}
// === Public API ===
async claimNamespace(name, options = {}) {
if (!this._isValidName(name)) {
throw new Error('Invalid namespace name')
}
const existing = await this.base.view.get(`name:${name}`)
if (existing) {
throw new Error(`Namespace "${name}" already claimed`)
}
// TODO: Handle mnemonic-derived root key
const rootKey = options.rootKey || this.localWriter.key.toString('hex')
const difficulty = getDifficulty(name)
console.log(`Mining proof for "${name}" (difficulty: ${difficulty})...`)
const proof = await mineProof(name, rootKey, difficulty)
const deviceKey = this.localWriter.key.toString('hex')
const initialDevice = {
key: deviceKey,
name: options.deviceName || 'primary',
canAuthorize: true,
canRevoke: true
}
await this.base.append({
type: 'claim-namespace',
name,
rootKey,
proof,
initialDevice,
rootSignature: 'TODO'
})
await this.base.update()
this.namespace = name
return { namespace: name, rootKey, deviceKey }
}
async registerResource(resource, metadata = {}) {
if (!this.namespace) {
throw new Error('No namespace claimed')
}
await this.base.append({
type: 'register-resource',
namespace: this.namespace,
resource,
metadata
})
await this.base.update()
const entry = await this.base.view.get(`resource:${this.namespace}/${resource}`)
return {
name: `${this.namespace}/${resource}`,
topic: entry.value.topic
}
}
async resolve(path) {
const entry = await this.base.view.get(`resource:${path}`)
if (!entry) return null
return {
...entry.value,
topicBuffer: Buffer.from(entry.value.topic, 'hex')
}
}
async resolveNamespace(name) {
const entry = await this.base.view.get(`name:${name}`)
return entry ? entry.value : null
}
async authorizeDevice(devicePubkey, options = {}) {
// TODO: Implement
throw new Error('Not implemented')
}
async revokeDevice(devicePubkey, reason = '') {
// TODO: Implement
throw new Error('Not implemented')
}
async listDevices() {
if (!this.namespace) return []
const ns = await this.resolveNamespace(this.namespace)
if (!ns) return []
return Object.entries(ns.devices).map(([key, info]) => ({
key,
...info,
isCurrentDevice: key === this.localWriter.key.toString('hex')
}))
}
getPublicKey() {
return this.localWriter.key.toString('hex')
}
getNamespace() {
return this.namespace
}
}