OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-registry commits / ee453c7

Initial commit: hyper-registry P2P name registry
Decentralized naming using Autobase + proof-of-work. Solves Zooko's Triangle for the Holepunch ecosystem. Design phase — architecture complete, lib/ sketches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ee453c79378e48ebd50a0da0b6e5aa4fa2b30258
by plankatron <fleeky@gmail.com> · 2026-02-22T16:13:32.000Z

Changed files (8) +1462 -0

.gitignore +1 -0
……@@ -0,0 +1 @@
1node_modules
ARCHITECTURE.md +246 -0
……@@ -0,0 +1,246 @@
1# Hyper-Registry Architecture
2
3## Overview
4
5Hyper-Registry is a decentralized name registry using Autobase for consensus and proof-of-work for Sybil resistance.
6
7## Zooko's Triangle Resolution
8
9| Property | Solution |
10|----------|----------|
11| Human-meaningful | Namespaces like "tabertron" |
12| Secure | Topics derived from owner's pubkey |
13| Decentralized | Autobase + proof-of-work |
14
15The key insight: **proof-of-work is the cost of getting a memorable name**. Without it, you'd need a central authority to prevent squatting.
16
17## Data Model
18
19### Namespace Claim
20
21```javascript
22{
23 type: 'claim-namespace',
24 name: 'tabertron',
25 rootKey: 'pubkey-hex...', // Mnemonic-derived root identity
26 proof: {
27 nonce: 312847,
28 difficulty: 18,
29 hash: '0000abc...'
30 },
31 initialDevice: {
32 key: 'device-pubkey...',
33 name: 'laptop',
34 canAuthorize: true,
35 canRevoke: true
36 },
37 rootSignature: 'sig-hex...' // Root key signs device auth
38}
39```
40
41### Resource Registration
42
43```javascript
44{
45 type: 'register-resource',
46 namespace: 'tabertron',
47 resource: 'wirecore',
48 metadata: {
49 type: 'git-repo',
50 description: 'P2P social network'
51 }
52}
53```
54
55### Device Authorization
56
57```javascript
58{
59 type: 'authorize-device',
60 namespace: 'tabertron',
61 device: {
62 key: 'new-device-pubkey...',
63 name: 'desktop',
64 canAuthorize: true,
65 canRevoke: false,
66 resources: null // null = all, or ['wirecore']
67 },
68 authorizedBy: 'existing-device-key...',
69 signature: 'sig-hex...'
70}
71```
72
73### Device Revocation
74
75```javascript
76{
77 type: 'revoke-device',
78 namespace: 'tabertron',
79 deviceKey: 'device-to-revoke...',
80 reason: 'lost device',
81 revokedBy: 'revoking-device-key...',
82 signature: 'sig-hex...'
83}
84```
85
86## Topic Derivation
87
88Topics are derived from the **root key** (not device key) to ensure consistency:
89
90```javascript
91function deriveTopic(rootPubkey, resourceName) {
92 return crypto.hash(Buffer.concat([
93 Buffer.from(rootPubkey, 'hex'),
94 Buffer.from(resourceName, 'utf-8')
95 ])).subarray(0, 32)
96}
97```
98
99This ensures:
1001. All devices derive the same topic for a resource
1012. Only the namespace owner can serve that topic
1023. Topics are unforgeable without the root key
103
104## Proof-of-Work
105
106### Difficulty Scaling
107
108| Name Length | Difficulty (bits) | ~Hashes Required |
109|-------------|-------------------|------------------|
110| 1-2 chars | 26 | 67 million |
111| 3-4 chars | 22 | 4 million |
112| 5-8 chars | 18 | 262,000 |
113| 9-16 chars | 14 | 16,000 |
114| 17+ chars | 10 | 1,000 |
115
116### Mining
117
118```javascript
119function mineProof(name, pubkey, difficulty) {
120 let nonce = 0
121 while (true) {
122 const hash = crypto.hash(Buffer.from(`hyper-registry:${name}:${pubkey}:${nonce}`))
123 if (countLeadingZeroBits(hash) >= difficulty) {
124 return { nonce, difficulty, hash: hash.toString('hex') }
125 }
126 nonce++
127 }
128}
129```
130
131### Validation
132
133```javascript
134function validateProof(name, pubkey, proof, requiredDifficulty) {
135 const hash = crypto.hash(Buffer.from(`hyper-registry:${name}:${pubkey}:${proof.nonce}`))
136 return countLeadingZeroBits(hash) >= requiredDifficulty &&
137 hash.toString('hex') === proof.hash
138}
139```
140
141## Autobase Integration
142
143### Writer Model
144
145Each device is a writer in the Autobase:
146
147```javascript
148const base = new Autobase(store, {
149 localInput: deviceWriter,
150 apply: applyFunction,
151 valueEncoding: 'json',
152 open: (store) => new Hyperbee(store.get('view'), {
153 keyEncoding: 'utf-8',
154 valueEncoding: 'json'
155 })
156})
157```
158
159### Apply Function
160
161The apply function enforces all rules:
162
163```javascript
164async function apply(batch, view, base) {
165 for (const node of batch) {
166 const op = node.value
167 const writer = node.from.key.toString('hex')
168
169 switch (op.type) {
170 case 'claim-namespace':
171 await applyClaim(op, writer, view)
172 break
173 case 'register-resource':
174 await applyRegister(op, writer, view)
175 break
176 case 'authorize-device':
177 await applyAuthorize(op, writer, view)
178 break
179 case 'revoke-device':
180 await applyRevoke(op, writer, view)
181 break
182 }
183 }
184}
185```
186
187### Conflict Resolution
188
189- **Namespace conflicts**: First valid claim wins (Autobase sequence order)
190- **Device conflicts**: Signature verification determines validity
191- **Resource conflicts**: Only authorized devices can write
192
193## Security Properties
194
1951. **Sybil Resistance**: PoW makes bulk claiming expensive
1962. **Unforgeable Topics**: Derived from cryptographic pubkey
1973. **Device Isolation**: Compromise one device, revoke it, others unaffected
1984. **Recovery**: Mnemonic restores root key
1995. **Audit Trail**: All operations recorded in Autobase
200
201## Bootstrap
202
203### Genesis Problem
204
205How does the registry start?
206
207**Solution**: Open membership with PoW
208
2091. Anyone can claim a namespace (if they do the work)
2102. Claiming a namespace makes you a writer
2113. No initial trusted set required
212
213### Finding the Registry
214
215Options:
2161. Well-known DHT topic: `hash("hyper-registry-v1")`
2172. Bootstrap peers in config
2183. DNS TXT records (for hybrid approach)
219
220## URL Format
221
222```
223pear://tabertron/wirecore
224 └── namespace/resource
225
226# Resolves to:
227{
228 namespace: 'tabertron',
229 resource: 'wirecore',
230 owner: 'root-pubkey...',
231 topic: 'derived-topic...'
232}
233
234# Client joins topic via Hyperswarm
235swarm.join(Buffer.from(topic, 'hex'), { client: true })
236```
237
238## Comparison
239
240| System | Human-Meaningful | Secure | Decentralized |
241|--------|------------------|--------|---------------|
242| DNS | ✅ | ✅ (with DNSSEC) | ❌ (ICANN) |
243| ENS | ✅ | ✅ | ⚠️ (Ethereum) |
244| Namecoin | ✅ | ✅ | ⚠️ (Blockchain) |
245| IPNS | ❌ (pubkey) | ✅ | ✅ |
246| **Hyper-Registry** | ✅ | ✅ | ✅ |
IDENTITY.md +270 -0
……@@ -0,0 +1,270 @@
1# Multi-Device Identity Model
2
3## Overview
4
5Hyper-Registry uses a multi-device identity model where:
6- One **root key** (mnemonic-derived) owns the namespace
7- Multiple **device keys** are authorized to write
8- Devices can be individually managed (add/revoke)
9
10## Identity Hierarchy
11
12```
13MNEMONIC (24 words)
14 │
15 └── Store offline, use for recovery only
16 │
17 ▼
18ROOT KEY (deterministic from mnemonic)
19 │
20 └── Used for:
21 - Initial namespace claim
22 - Emergency device revocation
23 - Recovery if all devices lost
24 │
25 ▼
26NAMESPACE: "tabertron"
27 │
28 ├── DEVICE: laptop (abc123...)
29 │ └── Permissions: auth, revoke, write all
30 │
31 ├── DEVICE: desktop (def456...)
32 │ └── Permissions: auth, write all
33 │
34 ├── DEVICE: ci-server (789xyz...)
35 │ └── Permissions: write [wirecore] only
36 │
37 └── REVOKED: old-phone (bad000...)
38 └── Can no longer write
39```
40
41## Device Permissions
42
43| Permission | Description |
44|------------|-------------|
45| `canAuthorize` | Can add new devices |
46| `canRevoke` | Can remove other devices |
47| `resources` | Which resources can write (null = all) |
48
49### Permission Levels
50
51```javascript
52// Full admin (first device, recovery)
53{
54 canAuthorize: true,
55 canRevoke: true,
56 resources: null
57}
58
59// Regular device
60{
61 canAuthorize: true,
62 canRevoke: false,
63 resources: null
64}
65
66// CI/automation
67{
68 canAuthorize: false,
69 canRevoke: false,
70 resources: ['wirecore', 'pear-git']
71}
72```
73
74## Device Pairing Flow
75
76### Adding a New Device
77
78```
79EXISTING DEVICE (laptop) NEW DEVICE (desktop)
80
81$ hyper-registry devices add
82 $ hyper-registry devices request
83Ready to pair. Enter code: _
84 Your code: A7F3-B2C1
85 Waiting for approval...
86Enter code: A7F3-B2C1
87
88Device found: def456...
89Name: desktop
90Permissions: [auth] [no-revoke] [all-resources]
91
92Authorize? [Y/n]: Y
93
94✅ Device authorized!
95 ✅ Access granted!
96 Namespace: tabertron
97```
98
99### Code Exchange Protocol
100
101```javascript
102// New device generates short-lived code
103const code = {
104 deviceKey: newDevice.publicKey.toString('hex'),
105 timestamp: Date.now(),
106 shortCode: hash(deviceKey + timestamp).slice(0, 8)
107}
108
109// Existing device verifies and authorizes
110if (verifyCode(enteredCode, receivedDeviceKey)) {
111 await registry.authorizeDevice(receivedDeviceKey, {
112 name: 'desktop',
113 canAuthorize: true
114 })
115}
116```
117
118## Recovery Scenarios
119
120### Lost One Device
121
122```bash
123# From any other authorized device
124$ hyper-registry devices revoke abc123 --reason "lost laptop"
125
126✅ Device abc123 (laptop) revoked
127 It can no longer write to tabertron
128```
129
130### Lost All Devices
131
132```bash
133$ hyper-registry recover
134
135Enter your 24-word recovery phrase:
136> witch collapse practice feed shame open despair creek road...
137
138✅ Root key recovered
139 Found namespace: tabertron
140
141This device will become the new primary.
142Revoke all previous devices? [Y/n]: Y
143
144Revoking: laptop (abc123...)
145Revoking: desktop (def456...)
146Revoking: ci-server (789xyz...)
147
148✅ Recovery complete!
149 You are now the only authorized device.
150```
151
152### Compromised Device
153
154If you suspect a device is compromised:
155
156```bash
157# Immediately revoke from another device
158$ hyper-registry devices revoke abc123 --reason "compromised"
159
160# Or with root key if no other devices available
161$ hyper-registry recover --revoke-only abc123
162```
163
164## Security Considerations
165
166### Root Key Protection
167
168The root key (mnemonic) is the ultimate recovery mechanism:
169- Store offline (paper, hardware wallet)
170- Never store digitally in plain text
171- Consider splitting (Shamir's Secret Sharing)
172
173### Device Key Isolation
174
175Each device generates its own keypair:
176- Stored in device-local keychain/secure storage
177- Never transmitted over network
178- Compromise of one doesn't affect others
179
180### Revocation
181
182Revocations are permanent and recorded:
183- Revoked devices cannot be re-authorized
184- Must use a new keypair to rejoin
185- Full audit trail in Autobase
186
187### Trust Model
188
189```
190Root Key (mnemonic holder)
191 │
192 ├── Can do anything
193 │
194 ▼
195Admin Devices (canRevoke: true)
196 │
197 ├── Can add/remove devices
198 │
199 ▼
200Regular Devices (canRevoke: false)
201 │
202 ├── Can add devices, write resources
203 │
204 ▼
205Limited Devices (resources: [...])
206 │
207 └── Can only write specific resources
208```
209
210## Storage Schema
211
212### Namespace Entry
213
214```javascript
215// Key: name:tabertron
216{
217 owner: 'root-pubkey-hex...',
218 claimedAt: 1706000000000,
219 proofNonce: 312847,
220
221 devices: {
222 'abc123...': {
223 name: 'laptop',
224 authorizedAt: 1706000000000,
225 authorizedBy: 'root',
226 canAuthorize: true,
227 canRevoke: true,
228 resources: null
229 },
230 'def456...': {
231 name: 'desktop',
232 authorizedAt: 1706100000000,
233 authorizedBy: 'abc123...',
234 canAuthorize: true,
235 canRevoke: false,
236 resources: null
237 }
238 },
239
240 revoked: {
241 'bad000...': {
242 name: 'old-phone',
243 revokedAt: 1706200000000,
244 revokedBy: 'abc123...',
245 reason: 'lost device',
246 originalAuth: { ... }
247 }
248 }
249}
250```
251
252## Integration with keet-identity-key
253
254```javascript
255import IdentityKey from 'keet-identity-key'
256
257// Generate new identity
258const identity = IdentityKey.generate()
259console.log('Mnemonic:', identity.mnemonic) // Save this!
260
261// Recover from mnemonic
262const recovered = new IdentityKey(savedMnemonic)
263// recovered.publicKey === identity.publicKey (deterministic)
264
265// Use for namespace claim
266await registry.claimNamespace('tabertron', {
267 mnemonic: savedMnemonic,
268 deviceName: 'laptop'
269})
270```
README.md +168 -0
……@@ -0,0 +1,168 @@
1# Hyper-Registry
2
3Decentralized P2P name registry built on Autobase. Solves Zooko's Triangle — human-meaningful, secure, and decentralized naming for the Holepunch ecosystem.
4
5## Status
6
7**Design phase** — architecture complete, implementation sketches in `lib/`, not yet functional.
8
9## Problem
10
11Zooko's Triangle says naming systems can only have 2 of 3 properties: human-meaningful, secure, decentralized. Existing solutions compromise:
12
13| System | Human-Meaningful | Secure | Decentralized |
14|--------|:---:|:---:|:---:|
15| DNS | Yes | Yes (DNSSEC) | No (ICANN) |
16| ENS | Yes | Yes | Partial (Ethereum) |
17| IPNS | No (pubkey hash) | Yes | Yes |
18| **Hyper-Registry** | **Yes** | **Yes** | **Yes** |
19
20## How It Works
21
22**Proof-of-work** is the cost of getting a memorable name. No central authority needed.
23
24```
25MNEMONIC (24 words) ─── Recovery
26 │
27 ▼
28ROOT KEY ─── Namespace ownership
29 │
30 ▼
31NAMESPACE: "tabertron" ─── Human-memorable (PoW protected)
32 │
33 ├── DEVICE: laptop ─── Write access
34 ├── DEVICE: desktop ─── Write access
35 │
36 └── RESOURCES
37 ├── tabertron/wirecore → topic (secure, unforgeable)
38 └── tabertron/pear-git → topic (secure, unforgeable)
39```
40
41### URL Resolution
42
43```
44pear://tabertron/wirecore
45 └── namespace/resource
46
47→ Resolves to DHT topic derived from root pubkey + resource name
48→ Client joins topic via Hyperswarm
49→ Only the namespace owner can serve that topic
50```
51
52### PoW Difficulty Scaling
53
54Short names are more valuable, so they require more work:
55
56| Name Length | Difficulty (bits) | ~Hashes | ~Time |
57|:-----------:|:-----------------:|:-------:|:-----:|
58| 1-2 chars | 26 | 67M | ~2 min |
59| 3-4 chars | 22 | 4M | ~8 sec |
60| 5-8 chars | 18 | 262K | ~0.5 sec |
61| 9-16 chars | 14 | 16K | instant |
62| 17+ chars | 10 | 1K | instant |
63
64### Autobase Consensus
65
66All nodes compute the same state. First valid claim wins (Autobase sequence order).
67
68Operations:
69- **claim-namespace** — mine PoW, register name with root key
70- **register-resource** — bind a resource to your namespace
71- **authorize-device** — add a device to your namespace (with permissions)
72- **revoke-device** — remove a compromised or lost device
73
74## Architecture
75
76```
77┌─────────────────────────────────────────────────┐
78│ HyperRegistry │
79├─────────────────────────────────────────────────┤
80│ Autobase (multi-writer consensus) │
81│ └── Hyperbee view (name:*, resource:*) │
82│ │
83│ Proof-of-Work (Sybil resistance) │
84│ └── Leading zero bits on hash(name:key:nonce)│
85│ │
86│ Identity (multi-device) │
87│ ├── RootIdentity (mnemonic-derived) │
88│ ├── DeviceIdentity (per-machine keypair) │
89│ └── DevicePairing (short-code exchange) │
90│ │
91│ Topic Derivation │
92│ └── hash(rootPubkey + resourceName)[0:32] │
93└─────────────────────────────────────────────────┘
94```
95
96## Files
97
98```
99lib/
100├── registry.js Main HyperRegistry class (Autobase apply function, public API)
101├── proof-of-work.js PoW mining, validation, difficulty scaling
102└── identity.js DeviceIdentity, RootIdentity, DevicePairing protocol
103```
104
105### Key Classes
106
107- **HyperRegistry** — Autobase-backed registry with claim/register/resolve API
108- **DeviceIdentity** — Per-device Ed25519 keypair management
109- **RootIdentity** — Mnemonic-derived root key (keet-identity-key integration planned)
110- **DevicePairing** — Short-code device authorization protocol (XXXX-XXXX format, 5 min expiry)
111
112## Security Properties
113
1141. **Sybil Resistance** — PoW makes bulk claiming expensive
1152. **Unforgeable Topics** — Derived from cryptographic pubkey, not the name string
1163. **Device Isolation** — Compromise one device, revoke it, others unaffected
1174. **Recovery** — Mnemonic restores root key and all namespace ownership
1185. **Audit Trail** — All operations recorded in Autobase (append-only)
119
120## Multi-Device Identity
121
122Each namespace supports multiple authorized devices with granular permissions:
123
124| Permission | Description |
125|---|---|
126| `canAuthorize` | Can add new devices to the namespace |
127| `canRevoke` | Can remove other devices |
128| `resources` | Which resources the device can write (`null` = all) |
129
130Recovery scenarios:
131- **Lost one device** — revoke from any other authorized device
132- **Lost all devices** — recover with 24-word mnemonic, revoke all previous devices
133- **Compromised device** — immediate revocation from another device or root key
134
135See [IDENTITY.md](./IDENTITY.md) for the full multi-device model and pairing protocol.
136
137## Dependencies
138
139- [autobase](https://github.com/holepunchto/autobase) — Multi-writer consensus
140- [hyperbee](https://github.com/holepunchto/hyperbee) — B-tree view for lookups
141- [hypercore-crypto](https://github.com/holepunchto/hypercore-crypto) — Ed25519 + hashing
142- [corestore](https://github.com/holepunchto/corestore) — Hypercore factory
143- [keet-identity-key](https://github.com/AudiusProject/keet-identity-key) — BIP39 mnemonic identity (planned)
144
145## Use Cases
146
147- **pear-git** — `pear://tabertron/pear-git` instead of raw keys
148- **hyperident** — Human-readable identity discovery
149- **Any P2P service** — Memorable names for Hyperswarm topics
150
151## Related Documents
152
153- [ARCHITECTURE.md](./ARCHITECTURE.md) — Detailed system design, data model, conflict resolution
154- [IDENTITY.md](./IDENTITY.md) — Multi-device identity model, pairing protocol, recovery
155
156## TODO
157
158- [ ] Implement Ed25519 signature verification (currently stubbed)
159- [ ] Integrate keet-identity-key for mnemonic-derived root keys
160- [ ] Implement device authorization and revocation
161- [ ] Build CLI tools (`hyper-registry claim`, `hyper-registry resolve`)
162- [ ] Integration with pear-git (resolve `pear://namespace/repo`)
163- [ ] Bootstrap mechanism (well-known DHT topic or DNS TXT records)
164- [ ] Tests with brittle
165
166## License
167
168MIT
lib/identity.js +230 -0
……@@ -0,0 +1,230 @@
1/**
2 * Multi-Device Identity Management for Hyper-Registry
3 *
4 * STATUS: Implementation sketch - not yet functional
5 */
6
7import crypto from 'hypercore-crypto'
8// import IdentityKey from 'keet-identity-key'
9
10/**
11 * Device identity management
12 */
13export class DeviceIdentity {
14 constructor(options = {}) {
15 this.storagePath = options.storagePath
16 this.deviceKey = null
17 this.deviceSecret = null
18 this.deviceName = options.deviceName || null
19 }
20
21 /**
22 * Initialize device identity
23 * Creates or loads device keypair
24 */
25 async init() {
26 // TODO: Load from secure storage if exists
27 // TODO: Generate new if not exists
28
29 const keypair = crypto.keyPair()
30 this.deviceKey = keypair.publicKey
31 this.deviceSecret = keypair.secretKey
32 }
33
34 /**
35 * Get device public key
36 */
37 getPublicKey() {
38 return this.deviceKey.toString('hex')
39 }
40
41 /**
42 * Sign data with device key
43 */
44 sign(data) {
45 const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data
46 return crypto.sign(dataBuffer, this.deviceSecret)
47 }
48
49 /**
50 * Verify signature from another device
51 */
52 static verify(data, signature, publicKey) {
53 const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data
54 const sigBuffer = typeof signature === 'string' ? Buffer.from(signature, 'hex') : signature
55 const keyBuffer = typeof publicKey === 'string' ? Buffer.from(publicKey, 'hex') : publicKey
56
57 return crypto.verify(dataBuffer, sigBuffer, keyBuffer)
58 }
59}
60
61/**
62 * Root identity (mnemonic-derived)
63 */
64export class RootIdentity {
65 constructor(mnemonic = null) {
66 this.mnemonic = mnemonic
67 this.publicKey = null
68 this.secretKey = null
69 }
70
71 /**
72 * Generate new root identity with mnemonic
73 */
74 static generate() {
75 // TODO: Use keet-identity-key
76 // const identity = IdentityKey.generate()
77 // return new RootIdentity(identity.mnemonic)
78
79 // Placeholder: just generate a keypair
80 const keypair = crypto.keyPair()
81 const instance = new RootIdentity('placeholder-mnemonic-not-real')
82 instance.publicKey = keypair.publicKey
83 instance.secretKey = keypair.secretKey
84 return instance
85 }
86
87 /**
88 * Recover root identity from mnemonic
89 */
90 static fromMnemonic(mnemonic) {
91 // TODO: Use keet-identity-key
92 // const identity = new IdentityKey(mnemonic)
93 // const instance = new RootIdentity(mnemonic)
94 // instance.publicKey = identity.publicKey
95 // instance.secretKey = identity.secretKey
96 // return instance
97
98 throw new Error('Not implemented - requires keet-identity-key')
99 }
100
101 /**
102 * Get root public key
103 */
104 getPublicKey() {
105 return this.publicKey.toString('hex')
106 }
107
108 /**
109 * Sign device authorization
110 */
111 signDeviceAuth(device) {
112 const data = JSON.stringify({
113 key: device.key,
114 name: device.name,
115 canAuthorize: device.canAuthorize,
116 canRevoke: device.canRevoke,
117 resources: device.resources
118 })
119
120 return crypto.sign(Buffer.from(data), this.secretKey).toString('hex')
121 }
122
123 /**
124 * Verify device was authorized by root
125 */
126 static verifyDeviceAuth(device, signature, rootPublicKey) {
127 const data = JSON.stringify({
128 key: device.key,
129 name: device.name,
130 canAuthorize: device.canAuthorize,
131 canRevoke: device.canRevoke,
132 resources: device.resources
133 })
134
135 return crypto.verify(
136 Buffer.from(data),
137 Buffer.from(signature, 'hex'),
138 Buffer.from(rootPublicKey, 'hex')
139 )
140 }
141}
142
143/**
144 * Device pairing protocol
145 */
146export class DevicePairing {
147 constructor(registry) {
148 this.registry = registry
149 this.pendingRequests = new Map()
150 }
151
152 /**
153 * Generate pairing code for new device
154 */
155 generatePairingCode(deviceKey) {
156 const timestamp = Date.now()
157 const data = `${deviceKey}:${timestamp}`
158 const hash = crypto.hash(Buffer.from(data))
159 const shortCode = hash.toString('hex').slice(0, 8).toUpperCase()
160
161 // Format: XXXX-XXXX
162 const formatted = shortCode.slice(0, 4) + '-' + shortCode.slice(4, 8)
163
164 this.pendingRequests.set(shortCode, {
165 deviceKey,
166 timestamp,
167 expiresAt: timestamp + 300000 // 5 minutes
168 })
169
170 return {
171 code: formatted,
172 deviceKey,
173 expiresAt: timestamp + 300000
174 }
175 }
176
177 /**
178 * Verify pairing code and get device key
179 */
180 verifyPairingCode(code) {
181 const normalized = code.replace('-', '').toUpperCase()
182
183 const request = this.pendingRequests.get(normalized)
184 if (!request) {
185 return null
186 }
187
188 if (Date.now() > request.expiresAt) {
189 this.pendingRequests.delete(normalized)
190 return null
191 }
192
193 return request.deviceKey
194 }
195
196 /**
197 * Complete pairing (authorize the device)
198 */
199 async completePairing(code, options = {}) {
200 const deviceKey = this.verifyPairingCode(code)
201 if (!deviceKey) {
202 throw new Error('Invalid or expired pairing code')
203 }
204
205 // Authorize the device
206 await this.registry.authorizeDevice(deviceKey, {
207 name: options.name || 'new-device',
208 canAuthorize: options.canAuthorize || false,
209 canRevoke: options.canRevoke || false,
210 resources: options.resources || null
211 })
212
213 // Clean up
214 this.pendingRequests.delete(code.replace('-', '').toUpperCase())
215
216 return { deviceKey, authorized: true }
217 }
218
219 /**
220 * Clean up expired requests
221 */
222 cleanup() {
223 const now = Date.now()
224 for (const [code, request] of this.pendingRequests) {
225 if (now > request.expiresAt) {
226 this.pendingRequests.delete(code)
227 }
228 }
229 }
230}
lib/proof-of-work.js +143 -0
……@@ -0,0 +1,143 @@
1/**
2 * Proof-of-Work for Hyper-Registry namespace claims
3 *
4 * STATUS: Implementation sketch - not yet functional
5 */
6
7import crypto from 'hypercore-crypto'
8
9/**
10 * Get required difficulty for a name
11 * Shorter names require more work (Sybil resistance)
12 */
13export function getDifficulty(name) {
14 const len = name.length
15 if (len <= 2) return 26 // ~67M hashes
16 if (len <= 4) return 22 // ~4M hashes
17 if (len <= 8) return 18 // ~262K hashes
18 if (len <= 16) return 14 // ~16K hashes
19 return 10 // ~1K hashes
20}
21
22/**
23 * Mine a proof-of-work for a namespace claim
24 *
25 * @param {string} name - Namespace name
26 * @param {string} pubkey - Root public key (hex)
27 * @param {number} difficulty - Required leading zero bits
28 * @returns {Promise<Object>} - { nonce, difficulty, hash, attempts, timeMs }
29 */
30export async function mineProof(name, pubkey, difficulty) {
31 const startTime = Date.now()
32 let nonce = 0
33 let hash
34
35 while (true) {
36 hash = hashProof(name, pubkey, nonce)
37
38 if (countLeadingZeroBits(hash) >= difficulty) {
39 return {
40 nonce,
41 difficulty,
42 hash: hash.toString('hex'),
43 attempts: nonce + 1,
44 timeMs: Date.now() - startTime
45 }
46 }
47
48 nonce++
49
50 // Yield to event loop periodically
51 if (nonce % 10000 === 0) {
52 await new Promise(resolve => setImmediate(resolve))
53 }
54
55 // Progress logging
56 if (nonce % 500000 === 0) {
57 const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
58 const rate = (nonce / 1000 / parseFloat(elapsed)).toFixed(0)
59 console.log(` ...${(nonce / 1000000).toFixed(1)}M hashes (${elapsed}s, ${rate}k/s)`)
60 }
61 }
62}
63
64/**
65 * Validate a proof-of-work
66 *
67 * @param {string} name - Namespace name
68 * @param {string} pubkey - Root public key (hex)
69 * @param {Object} proof - { nonce, difficulty, hash }
70 * @param {number} requiredDifficulty - Minimum required difficulty
71 * @returns {boolean}
72 */
73export function validateProof(name, pubkey, proof, requiredDifficulty) {
74 // Check difficulty meets requirement
75 if (proof.difficulty < requiredDifficulty) {
76 return false
77 }
78
79 // Recompute hash
80 const hash = hashProof(name, pubkey, proof.nonce)
81
82 // Verify hash matches
83 if (hash.toString('hex') !== proof.hash) {
84 return false
85 }
86
87 // Verify leading zeros
88 if (countLeadingZeroBits(hash) < requiredDifficulty) {
89 return false
90 }
91
92 return true
93}
94
95/**
96 * Hash a proof attempt
97 */
98function hashProof(name, pubkey, nonce) {
99 const data = Buffer.from(`hyper-registry:${name}:${pubkey}:${nonce}`)
100 return crypto.hash(data)
101}
102
103/**
104 * Count leading zero bits in a buffer
105 */
106function countLeadingZeroBits(buffer) {
107 let zeros = 0
108
109 for (const byte of buffer) {
110 if (byte === 0) {
111 zeros += 8
112 } else {
113 // Count leading zeros in this byte
114 for (let i = 7; i >= 0; i--) {
115 if ((byte & (1 << i)) === 0) {
116 zeros++
117 } else {
118 return zeros
119 }
120 }
121 }
122 }
123
124 return zeros
125}
126
127/**
128 * Estimate time to mine at given difficulty
129 * Assumes ~500k hashes/second on typical hardware
130 */
131export function estimateMiningTime(difficulty) {
132 const expectedHashes = Math.pow(2, difficulty)
133 const hashesPerSecond = 500000
134 const seconds = expectedHashes / hashesPerSecond
135
136 if (seconds < 60) {
137 return `~${seconds.toFixed(0)} seconds`
138 } else if (seconds < 3600) {
139 return `~${(seconds / 60).toFixed(0)} minutes`
140 } else {
141 return `~${(seconds / 3600).toFixed(1)} hours`
142 }
143}
lib/registry.js +371 -0
……@@ -0,0 +1,371 @@
1/**
2 * Hyper-Registry - P2P Name Registry using Autobase
3 *
4 * STATUS: Implementation sketch - not yet functional
5 */
6
7import Autobase from 'autobase'
8import Hyperbee from 'hyperbee'
9import crypto from 'hypercore-crypto'
10import { mineProof, validateProof, getDifficulty } from './proof-of-work.js'
11
12export class HyperRegistry {
13 constructor(corestore, options = {}) {
14 this.store = corestore
15 this.bootstrapKeys = options.bootstrapKeys || []
16 this.namespace = null
17 }
18
19 async ready() {
20 // Local writer for this device
21 this.localWriter = this.store.get({ name: 'hyper-registry-writer' })
22 await this.localWriter.ready()
23
24 // Create Autobase
25 this.base = new Autobase(this.store, {
26 localInput: this.localWriter,
27 autostart: true,
28 apply: this._apply.bind(this),
29 valueEncoding: 'json',
30 open: (store) => new Hyperbee(store.get('registry-view'), {
31 keyEncoding: 'utf-8',
32 valueEncoding: 'json'
33 })
34 })
35
36 await this.base.ready()
37
38 // Add bootstrap writers
39 for (const key of this.bootstrapKeys) {
40 try {
41 await this.base.addInput(Buffer.from(key, 'hex'))
42 } catch (err) {
43 console.warn(`Failed to add bootstrap: ${key.slice(0, 8)}...`)
44 }
45 }
46
47 // Load our namespace
48 await this._loadLocalNamespace()
49 }
50
51 async _loadLocalNamespace() {
52 const pubkey = this.localWriter.key.toString('hex')
53
54 for await (const entry of this.base.view.createReadStream({
55 gte: 'name:',
56 lt: 'name:\xff'
57 })) {
58 const ns = entry.value
59 if (ns.devices && ns.devices[pubkey]) {
60 this.namespace = entry.key.replace('name:', '')
61 console.log(`Loaded namespace: ${this.namespace}`)
62 return
63 }
64 }
65 }
66
67 // === Autobase Apply Function ===
68
69 async _apply(batch, view, base) {
70 for (const node of batch) {
71 const op = node.value
72 const writer = node.from.key.toString('hex')
73 const seq = base.linearizer.indexedLength
74
75 try {
76 switch (op.type) {
77 case 'claim-namespace':
78 await this._applyClaim(op, writer, seq, view)
79 break
80 case 'register-resource':
81 await this._applyRegister(op, writer, view)
82 break
83 case 'authorize-device':
84 await this._applyAuthorize(op, writer, view)
85 break
86 case 'revoke-device':
87 await this._applyRevoke(op, writer, view)
88 break
89 }
90 } catch (err) {
91 console.error(`Apply error: ${err.message}`)
92 }
93 }
94 }
95
96 async _applyClaim(op, writer, seq, view) {
97 const { name, rootKey, proof, initialDevice, rootSignature } = op
98
99 // Validate name format
100 if (!this._isValidName(name)) return
101
102 // Check not already claimed
103 const existing = await view.get(`name:${name}`)
104 if (existing) return
105
106 // Validate proof-of-work against ROOT key
107 const difficulty = getDifficulty(name)
108 if (!validateProof(name, rootKey, proof, difficulty)) return
109
110 // Validate initial device is signed by root
111 if (!this._verifySignature(initialDevice, rootKey, rootSignature)) return
112
113 // Accept claim
114 await view.put(`name:${name}`, {
115 owner: rootKey,
116 claimedAt: Date.now(),
117 seq,
118 proofNonce: proof.nonce,
119 devices: {
120 [initialDevice.key]: {
121 name: initialDevice.name,
122 authorizedAt: Date.now(),
123 authorizedBy: 'root',
124 canAuthorize: initialDevice.canAuthorize !== false,
125 canRevoke: initialDevice.canRevoke !== false,
126 resources: initialDevice.resources || null
127 }
128 },
129 revoked: {}
130 })
131
132 console.log(`[Registry] Namespace claimed: ${name}`)
133 }
134
135 async _applyRegister(op, writer, view) {
136 const { namespace, resource, metadata } = op
137
138 const ns = await view.get(`name:${namespace}`)
139 if (!ns) return
140
141 // Verify writer is authorized device
142 const device = ns.value.devices[writer]
143 if (!device) return
144
145 // Check device can write this resource
146 if (device.resources && !device.resources.includes(resource)) return
147
148 // Derive topic from ROOT key for consistency
149 const topic = this._deriveTopic(ns.value.owner, resource)
150
151 await view.put(`resource:${namespace}/${resource}`, {
152 namespace,
153 resource,
154 owner: ns.value.owner,
155 registeredBy: writer,
156 topic: topic.toString('hex'),
157 metadata: metadata || {},
158 registeredAt: Date.now()
159 })
160
161 console.log(`[Registry] Resource registered: ${namespace}/${resource}`)
162 }
163
164 async _applyAuthorize(op, writer, view) {
165 const { namespace, device, authorizedBy, signature } = op
166
167 const ns = await view.get(`name:${namespace}`)
168 if (!ns) return
169
170 // Verify authorizer has permission
171 const authorizer = ns.value.devices[authorizedBy]
172 const isRoot = authorizedBy === ns.value.owner
173
174 if (!isRoot && (!authorizer || !authorizer.canAuthorize)) return
175
176 // Verify signature
177 if (!this._verifyDeviceAuth(device, authorizedBy, signature)) return
178
179 // Check not already present
180 if (ns.value.devices[device.key]) return
181 if (ns.value.revoked[device.key]) return
182
183 // Add device
184 ns.value.devices[device.key] = {
185 name: device.name,
186 authorizedAt: Date.now(),
187 authorizedBy,
188 canAuthorize: device.canAuthorize || false,
189 canRevoke: device.canRevoke || false,
190 resources: device.resources || null
191 }
192
193 await view.put(`name:${namespace}`, ns.value)
194 console.log(`[Registry] Device authorized: ${device.name}`)
195 }
196
197 async _applyRevoke(op, writer, view) {
198 const { namespace, deviceKey, reason, revokedBy, signature } = op
199
200 const ns = await view.get(`name:${namespace}`)
201 if (!ns) return
202
203 // Verify revoker has permission
204 const revoker = ns.value.devices[revokedBy]
205 const isRoot = revokedBy === ns.value.owner
206
207 if (!isRoot && (!revoker || !revoker.canRevoke)) return
208
209 // Can't revoke self (unless root)
210 if (deviceKey === revokedBy && !isRoot) return
211
212 // Can't revoke root
213 if (deviceKey === ns.value.owner) return
214
215 // Verify signature
216 if (!this._verifyRevocation(deviceKey, revokedBy, signature)) return
217
218 // Move to revoked
219 const device = ns.value.devices[deviceKey]
220 if (!device) return
221
222 delete ns.value.devices[deviceKey]
223 ns.value.revoked[deviceKey] = {
224 ...device,
225 revokedAt: Date.now(),
226 revokedBy,
227 reason
228 }
229
230 await view.put(`name:${namespace}`, ns.value)
231 console.log(`[Registry] Device revoked: ${device.name}`)
232 }
233
234 // === Helper Methods ===
235
236 _deriveTopic(rootPubkey, resourceName) {
237 return crypto.hash(Buffer.concat([
238 Buffer.from(rootPubkey, 'hex'),
239 Buffer.from(resourceName, 'utf-8')
240 ])).subarray(0, 32)
241 }
242
243 _isValidName(name) {
244 return /^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$/.test(name) ||
245 /^[a-z0-9]$/.test(name)
246 }
247
248 _verifySignature(data, pubkey, signature) {
249 // TODO: Implement ed25519 signature verification
250 return true
251 }
252
253 _verifyDeviceAuth(device, authorizer, signature) {
254 // TODO: Implement
255 return true
256 }
257
258 _verifyRevocation(deviceKey, revoker, signature) {
259 // TODO: Implement
260 return true
261 }
262
263 // === Public API ===
264
265 async claimNamespace(name, options = {}) {
266 if (!this._isValidName(name)) {
267 throw new Error('Invalid namespace name')
268 }
269
270 const existing = await this.base.view.get(`name:${name}`)
271 if (existing) {
272 throw new Error(`Namespace "${name}" already claimed`)
273 }
274
275 // TODO: Handle mnemonic-derived root key
276 const rootKey = options.rootKey || this.localWriter.key.toString('hex')
277 const difficulty = getDifficulty(name)
278
279 console.log(`Mining proof for "${name}" (difficulty: ${difficulty})...`)
280 const proof = await mineProof(name, rootKey, difficulty)
281
282 const deviceKey = this.localWriter.key.toString('hex')
283 const initialDevice = {
284 key: deviceKey,
285 name: options.deviceName || 'primary',
286 canAuthorize: true,
287 canRevoke: true
288 }
289
290 await this.base.append({
291 type: 'claim-namespace',
292 name,
293 rootKey,
294 proof,
295 initialDevice,
296 rootSignature: 'TODO'
297 })
298
299 await this.base.update()
300 this.namespace = name
301
302 return { namespace: name, rootKey, deviceKey }
303 }
304
305 async registerResource(resource, metadata = {}) {
306 if (!this.namespace) {
307 throw new Error('No namespace claimed')
308 }
309
310 await this.base.append({
311 type: 'register-resource',
312 namespace: this.namespace,
313 resource,
314 metadata
315 })
316
317 await this.base.update()
318
319 const entry = await this.base.view.get(`resource:${this.namespace}/${resource}`)
320 return {
321 name: `${this.namespace}/${resource}`,
322 topic: entry.value.topic
323 }
324 }
325
326 async resolve(path) {
327 const entry = await this.base.view.get(`resource:${path}`)
328 if (!entry) return null
329
330 return {
331 ...entry.value,
332 topicBuffer: Buffer.from(entry.value.topic, 'hex')
333 }
334 }
335
336 async resolveNamespace(name) {
337 const entry = await this.base.view.get(`name:${name}`)
338 return entry ? entry.value : null
339 }
340
341 async authorizeDevice(devicePubkey, options = {}) {
342 // TODO: Implement
343 throw new Error('Not implemented')
344 }
345
346 async revokeDevice(devicePubkey, reason = '') {
347 // TODO: Implement
348 throw new Error('Not implemented')
349 }
350
351 async listDevices() {
352 if (!this.namespace) return []
353
354 const ns = await this.resolveNamespace(this.namespace)
355 if (!ns) return []
356
357 return Object.entries(ns.devices).map(([key, info]) => ({
358 key,
359 ...info,
360 isCurrentDevice: key === this.localWriter.key.toString('hex')
361 }))
362 }
363
364 getPublicKey() {
365 return this.localWriter.key.toString('hex')
366 }
367
368 getNamespace() {
369 return this.namespace
370 }
371}
package.json +33 -0
……@@ -0,0 +1,33 @@
1{
2 "name": "hyper-registry",
3 "version": "0.0.1",
4 "description": "Decentralized P2P name registry using Autobase",
5 "type": "module",
6 "main": "lib/registry.js",
7 "scripts": {
8 "test": "brittle test/*.js"
9 },
10 "dependencies": {
11 "autobase": "^6.0.0",
12 "hyperbee": "^2.0.0",
13 "hypercore-crypto": "^3.0.0",
14 "corestore": "^6.0.0",
15 "keet-identity-key": "^1.0.0"
16 },
17 "devDependencies": {
18 "brittle": "^3.0.0"
19 },
20 "keywords": [
21 "p2p",
22 "hypercore",
23 "autobase",
24 "naming",
25 "registry",
26 "decentralized"
27 ],
28 "license": "MIT",
29 "repository": {
30 "type": "git",
31 "url": "git://tabertron.local/hyper-registry.git"
32 }
33}