| … | … | @@ -0,0 +1 @@ |
| 1 | node_modules |
| … | … | @@ -0,0 +1,246 @@ |
| 1 | # Hyper-Registry Architecture | |
| 2 | ||
| 3 | ## Overview | |
| 4 | ||
| 5 | Hyper-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 | ||
| 15 | The 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 | ||
| 88 | Topics are derived from the **root key** (not device key) to ensure consistency: | |
| 89 | ||
| 90 | ```javascript | |
| 91 | function 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 | ||
| 99 | This ensures: | |
| 100 | 1. All devices derive the same topic for a resource | |
| 101 | 2. Only the namespace owner can serve that topic | |
| 102 | 3. 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 | |
| 119 | function 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 | |
| 134 | function 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 | ||
| 145 | Each device is a writer in the Autobase: | |
| 146 | ||
| 147 | ```javascript | |
| 148 | const 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 | ||
| 161 | The apply function enforces all rules: | |
| 162 | ||
| 163 | ```javascript | |
| 164 | async 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 | ||
| 195 | 1. **Sybil Resistance**: PoW makes bulk claiming expensive | |
| 196 | 2. **Unforgeable Topics**: Derived from cryptographic pubkey | |
| 197 | 3. **Device Isolation**: Compromise one device, revoke it, others unaffected | |
| 198 | 4. **Recovery**: Mnemonic restores root key | |
| 199 | 5. **Audit Trail**: All operations recorded in Autobase | |
| 200 | ||
| 201 | ## Bootstrap | |
| 202 | ||
| 203 | ### Genesis Problem | |
| 204 | ||
| 205 | How does the registry start? | |
| 206 | ||
| 207 | **Solution**: Open membership with PoW | |
| 208 | ||
| 209 | 1. Anyone can claim a namespace (if they do the work) | |
| 210 | 2. Claiming a namespace makes you a writer | |
| 211 | 3. No initial trusted set required | |
| 212 | ||
| 213 | ### Finding the Registry | |
| 214 | ||
| 215 | Options: | |
| 216 | 1. Well-known DHT topic: `hash("hyper-registry-v1")` | |
| 217 | 2. Bootstrap peers in config | |
| 218 | 3. DNS TXT records (for hybrid approach) | |
| 219 | ||
| 220 | ## URL Format | |
| 221 | ||
| 222 | ``` | |
| 223 | pear://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 | |
| 235 | swarm.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** | ✅ | ✅ | ✅ | |
| … | … | @@ -0,0 +1,270 @@ |
| 1 | # Multi-Device Identity Model | |
| 2 | ||
| 3 | ## Overview | |
| 4 | ||
| 5 | Hyper-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 | ``` | |
| 13 | MNEMONIC (24 words) | |
| 14 | │ | |
| 15 | └── Store offline, use for recovery only | |
| 16 | │ | |
| 17 | ▼ | |
| 18 | ROOT KEY (deterministic from mnemonic) | |
| 19 | │ | |
| 20 | └── Used for: | |
| 21 | - Initial namespace claim | |
| 22 | - Emergency device revocation | |
| 23 | - Recovery if all devices lost | |
| 24 | │ | |
| 25 | ▼ | |
| 26 | NAMESPACE: "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 | ``` | |
| 79 | EXISTING DEVICE (laptop) NEW DEVICE (desktop) | |
| 80 | ||
| 81 | $ hyper-registry devices add | |
| 82 | $ hyper-registry devices request | |
| 83 | Ready to pair. Enter code: _ | |
| 84 | Your code: A7F3-B2C1 | |
| 85 | Waiting for approval... | |
| 86 | Enter code: A7F3-B2C1 | |
| 87 | ||
| 88 | Device found: def456... | |
| 89 | Name: desktop | |
| 90 | Permissions: [auth] [no-revoke] [all-resources] | |
| 91 | ||
| 92 | Authorize? [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 | |
| 103 | const 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 | |
| 110 | if (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 | ||
| 135 | Enter 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 | ||
| 141 | This device will become the new primary. | |
| 142 | Revoke all previous devices? [Y/n]: Y | |
| 143 | ||
| 144 | Revoking: laptop (abc123...) | |
| 145 | Revoking: desktop (def456...) | |
| 146 | Revoking: ci-server (789xyz...) | |
| 147 | ||
| 148 | ✅ Recovery complete! | |
| 149 | You are now the only authorized device. | |
| 150 | ``` | |
| 151 | ||
| 152 | ### Compromised Device | |
| 153 | ||
| 154 | If 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 | ||
| 168 | The 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 | ||
| 175 | Each 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 | ||
| 182 | Revocations 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 | ``` | |
| 190 | Root Key (mnemonic holder) | |
| 191 | │ | |
| 192 | ├── Can do anything | |
| 193 | │ | |
| 194 | ▼ | |
| 195 | Admin Devices (canRevoke: true) | |
| 196 | │ | |
| 197 | ├── Can add/remove devices | |
| 198 | │ | |
| 199 | ▼ | |
| 200 | Regular Devices (canRevoke: false) | |
| 201 | │ | |
| 202 | ├── Can add devices, write resources | |
| 203 | │ | |
| 204 | ▼ | |
| 205 | Limited 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 | |
| 255 | import IdentityKey from 'keet-identity-key' | |
| 256 | ||
| 257 | // Generate new identity | |
| 258 | const identity = IdentityKey.generate() | |
| 259 | console.log('Mnemonic:', identity.mnemonic) // Save this! | |
| 260 | ||
| 261 | // Recover from mnemonic | |
| 262 | const recovered = new IdentityKey(savedMnemonic) | |
| 263 | // recovered.publicKey === identity.publicKey (deterministic) | |
| 264 | ||
| 265 | // Use for namespace claim | |
| 266 | await registry.claimNamespace('tabertron', { | |
| 267 | mnemonic: savedMnemonic, | |
| 268 | deviceName: 'laptop' | |
| 269 | }) | |
| 270 | ``` |
| … | … | @@ -0,0 +1,168 @@ |
| 1 | # Hyper-Registry | |
| 2 | ||
| 3 | Decentralized 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 | ||
| 11 | Zooko'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 | ``` | |
| 25 | MNEMONIC (24 words) ─── Recovery | |
| 26 | │ | |
| 27 | ▼ | |
| 28 | ROOT KEY ─── Namespace ownership | |
| 29 | │ | |
| 30 | ▼ | |
| 31 | NAMESPACE: "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 | ``` | |
| 44 | pear://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 | ||
| 54 | Short 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 | ||
| 66 | All nodes compute the same state. First valid claim wins (Autobase sequence order). | |
| 67 | ||
| 68 | Operations: | |
| 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 | ``` | |
| 99 | lib/ | |
| 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 | ||
| 114 | 1. **Sybil Resistance** — PoW makes bulk claiming expensive | |
| 115 | 2. **Unforgeable Topics** — Derived from cryptographic pubkey, not the name string | |
| 116 | 3. **Device Isolation** — Compromise one device, revoke it, others unaffected | |
| 117 | 4. **Recovery** — Mnemonic restores root key and all namespace ownership | |
| 118 | 5. **Audit Trail** — All operations recorded in Autobase (append-only) | |
| 119 | ||
| 120 | ## Multi-Device Identity | |
| 121 | ||
| 122 | Each 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 | ||
| 130 | Recovery 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 | ||
| 135 | See [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 | ||
| 168 | MIT |
| … | … | @@ -0,0 +1,230 @@ |
| 1 | /** | |
| 2 | * Multi-Device Identity Management for Hyper-Registry | |
| 3 | * | |
| 4 | * STATUS: Implementation sketch - not yet functional | |
| 5 | */ | |
| 6 | ||
| 7 | import crypto from 'hypercore-crypto' | |
| 8 | // import IdentityKey from 'keet-identity-key' | |
| 9 | ||
| 10 | /** | |
| 11 | * Device identity management | |
| 12 | */ | |
| 13 | export 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 | */ | |
| 64 | export 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 | */ | |
| 146 | export 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 | } |
| … | … | @@ -0,0 +1,143 @@ |
| 1 | /** | |
| 2 | * Proof-of-Work for Hyper-Registry namespace claims | |
| 3 | * | |
| 4 | * STATUS: Implementation sketch - not yet functional | |
| 5 | */ | |
| 6 | ||
| 7 | import crypto from 'hypercore-crypto' | |
| 8 | ||
| 9 | /** | |
| 10 | * Get required difficulty for a name | |
| 11 | * Shorter names require more work (Sybil resistance) | |
| 12 | */ | |
| 13 | export 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 | */ | |
| 30 | export 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 | */ | |
| 73 | export 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 | */ | |
| 98 | function 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 | */ | |
| 106 | function 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 | */ | |
| 131 | export 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 | } |
| … | … | @@ -0,0 +1,371 @@ |
| 1 | /** | |
| 2 | * Hyper-Registry - P2P Name Registry using Autobase | |
| 3 | * | |
| 4 | * STATUS: Implementation sketch - not yet functional | |
| 5 | */ | |
| 6 | ||
| 7 | import Autobase from 'autobase' | |
| 8 | import Hyperbee from 'hyperbee' | |
| 9 | import crypto from 'hypercore-crypto' | |
| 10 | import { mineProof, validateProof, getDifficulty } from './proof-of-work.js' | |
| 11 | ||
| 12 | export 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 | } |
| … | … | @@ -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 | } |