OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note commits / 8b360b0

multiwriter: SharedRegistry accepts a stable writer keypair (admission groundwork)
Foundation for moving writer admission off the unauthenticated HTTP callback token onto the authenticated blind-pairing handshake. Today localWriterKey = base.local.key, derived from a corestore namespace keyed by the bootstrap — so a joining instance only learns its final writer key AFTER re-initialising against the shared bootstrap, which is exactly why it has to HTTP-callback the source with a bearer token (the inviter can't bind the token to a key it doesn't yet know). Pass an optional Autobase `keyPair` (v7 handler, verified) so the local writer key becomes DETERMINISTIC and bootstrap-independent — knowable before pairing, hence sendable as signed userData during the handshake. Additive: without opts.keyPair behaviour is unchanged. Test: same keyPair across different store+bootstrap yields the identical writer key; unkeyed registries keep distinct default keys. 2/2 pass. Next steps (this series): persist a per-instance keypair in the manager and thread it through every SharedRegistry construction; have the joiner send that stable key in pairing userData; admit it in the linker onadd bound to the paired identity; demote the HTTP callback to admin-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8b360b0c0f51d852a8974ec2426df84ed2eb88ce
by plankatron <fleeky@gmail.com> · 2026-06-23T22:40:13.000Z

Changed files (2) +68 -0

lib/multiwriter/shared-registry.js +7 -0
……@@ -24,6 +24,13 @@ export class SharedRegistry {
2424 apply: registryApply,
2525 valueEncoding: 'binary'
2626 }
27 // Stable per-instance writer keypair (optional). Fixing the Autobase local
28 // writer keypair makes localWriterKey DETERMINISTIC and bootstrap-INDEPENDENT,
29 // so a joining instance knows its final writer key BEFORE re-initialising
30 // against a shared bootstrap. That lets writer admission ride the
31 // authenticated blind-pairing handshake (key sent as signed userData, bound
32 // to the paired identity) instead of an unauthenticated HTTP callback token.
33 if (opts.keyPair) handlers.keyPair = opts.keyPair
2734 // Autobase v7: bootstrap key is the second positional arg, NOT a handler property
2835 if (opts.bootstrapKey) {
2936 this.base = new Autobase(this.store, opts.bootstrapKey, handlers)
test/unit/shared-registry-keypair.test.js +61 -0
……@@ -0,0 +1,61 @@
1import test from 'brittle'
2import fs from 'bare-fs'
3import path from 'bare-path'
4import os from 'bare-os'
5import b4a from 'b4a'
6import Corestore from 'corestore'
7import crypto from 'hypercore-crypto'
8import { SharedRegistry } from '../../lib/multiwriter/shared-registry.js'
9
10function tmpStore () {
11 const dir = path.join(os.tmpdir(), 'hn-srkp-' + Date.now() + '-' + Math.random().toString(36).slice(2))
12 fs.mkdirSync(dir, { recursive: true })
13 return { store: new Corestore(dir), dir }
14}
15
16function rmrf (dir) {
17 try {
18 for (const f of fs.readdirSync(dir)) {
19 const fp = path.join(dir, f)
20 if (fs.statSync(fp).isDirectory()) rmrf(fp)
21 else fs.unlinkSync(fp)
22 }
23 fs.rmdirSync(dir)
24 } catch {}
25}
26
27test('SharedRegistry - stable keyPair makes localWriterKey deterministic + bootstrap-independent', async (t) => {
28 const keyPair = crypto.keyPair()
29
30 // Same keyPair, DIFFERENT store, DIFFERENT bootstrap → SAME writer key.
31 const a = tmpStore()
32 const reg1 = new SharedRegistry(a.store, { keyPair })
33 await reg1.ready()
34 const k1 = reg1.localWriterKey
35
36 const b = tmpStore()
37 const reg2 = new SharedRegistry(b.store, { keyPair, bootstrapKey: crypto.keyPair().publicKey })
38 await reg2.ready()
39 const k2 = reg2.localWriterKey
40
41 t.ok(b4a.equals(k1, k2), 'writer key is identical across store + bootstrap — knowable before pairing')
42
43 await reg1.close(); await reg2.close()
44 rmrf(a.dir); rmrf(b.dir)
45})
46
47test('SharedRegistry - no keyPair preserves the old random-per-namespace behaviour', async (t) => {
48 const keyPair = crypto.keyPair()
49 const c = tmpStore()
50 const stable = new SharedRegistry(c.store, { keyPair })
51 await stable.ready()
52
53 const d = tmpStore()
54 const random = new SharedRegistry(d.store, {})
55 await random.ready()
56
57 t.absent(b4a.equals(stable.localWriterKey, random.localWriterKey), 'unkeyed registry keeps a distinct (default) writer key')
58
59 await stable.close(); await random.close()
60 rmrf(c.dir); rmrf(d.dir)
61})