OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity commits / e0bbbb3

hyperident-vault: add shareMasterVault / joinSharedDevice (network device-link)
Wraps hyperident's DeviceLinkingManager so callers (pgh) can invoke device share/join through hyper-identity instead of importing hyperident directly. - shareMasterVault({ timeoutMinutes }) starts a Hyperswarm sharing session. Returns the local master pubkey (operator gives it to the receiver) and a stop() callback to tear down the swarm before the timeout fires. - joinSharedDevice({ identityPublicKey, deviceName, passphrase }) connects to the sharing peer, receives the bundle, imports it into the local ~/.hyperident/. Refuses malformed input. Same trust model as exportMasterBundle/importMasterBundle: the bundle is sent over the wire, the receiver ends up with the mnemonic. Network transport is the only difference. For fleeky's two-machine private deployment this is the right model; a stricter attestation-only flow (receiver generates own keypair, sender attests but never sends mnemonic) would be a separate effort. Tests now 12/12, 46/46 asserts. The shareMasterVault test exercises a real Hyperswarm session (5s) and verifies stop() cleans up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
e0bbbb3dd04617b22743bc6f1ab23b1b708c6dcd
by plankatron <fleeky@gmail.com> · 2026-04-30T01:31:11.000Z

Changed files (3) +88 -4

index.js +5 -3
……@@ -37,12 +37,14 @@ export { RateLimiter } from './lib/auth/rate-limiter.js'
3737export { SSHKeyParser } from './lib/auth/ssh-parser.js'
3838
3939// Hyperident master-vault bridge — load master, attest local writer
40// devices, verify chains against the local master, export/import the
41// master vault between machines.
40// devices, verify chains against the local master, export/import or
41// network-share the master vault between machines.
4242export {
4343 loadHyperidentMaster,
4444 attestLocalDevice,
4545 verifyChainAgainstLocalMaster,
4646 exportMasterBundle,
47 importMasterBundle
47 importMasterBundle,
48 shareMasterVault,
49 joinSharedDevice
4850} from './lib/hyperident-vault.js'
lib/hyperident-vault.js +55 -0
……@@ -24,6 +24,7 @@
2424
2525import IdentityKey from 'keet-identity-key'
2626import { IdentityManager as HyperidentVault } from 'hyperident/lib/identity-manager.js'
27import { DeviceLinkingManager } from 'hyperident/lib/device-linking-manager.js'
2728import crypto from 'hypercore-crypto'
2829import b4a from 'b4a'
2930
……@@ -147,6 +148,60 @@ export async function importMasterBundle ({ bundle, deviceName, passphrase = nul
147148 return HyperidentVault.importDeviceBundle(bundle, deviceName, passphrase)
148149}
149150
151/**
152 * Start sharing the local master vault over the network so another
153 * machine can join via `joinSharedDevice` below.
154 *
155 * Returns the identity public key (hex) the joining device needs.
156 * Uses hyperident's DeviceLinkingManager — network transport over
157 * Hyperswarm, same trust model as exportMasterBundle (the bundle is
158 * sent over the wire; the joining peer ends up with the mnemonic).
159 *
160 * Caller is responsible for stopping the share via the returned
161 * `stop()` callback (or letting the timeout expire).
162 *
163 * @param {object} [opts]
164 * @param {number} [opts.timeoutMinutes=5]
165 * @returns {Promise<{
166 * identityPublicKey: string,
167 * stop: () => Promise<void>
168 * }>}
169 */
170export async function shareMasterVault ({ timeoutMinutes = 5 } = {}) {
171 const linker = new DeviceLinkingManager()
172 const identityPublicKey = await linker.startSharing(timeoutMinutes)
173 return {
174 identityPublicKey,
175 stop: async () => { await linker.stopSharing() }
176 }
177}
178
179/**
180 * Join a master vault that another machine is currently sharing.
181 *
182 * Connects to the sharing peer over Hyperswarm, receives the bundle,
183 * imports it into the local ~/.hyperident/. Same trust model as
184 * importMasterBundle — receiver ends up with the mnemonic.
185 *
186 * Refuses to run if a local vault already exists. Caller (pgh) is
187 * responsible for backing up + clearing the existing vault before
188 * invoking if --force is set.
189 *
190 * @param {object} opts
191 * @param {string} opts.identityPublicKey - hex pubkey returned by
192 * shareMasterVault on the source
193 * @param {string} opts.deviceName - name for the local device
194 * @param {string|null} [opts.passphrase] - if the local vault should be
195 * encrypted after import
196 * @returns {Promise<boolean>} success
197 */
198export async function joinSharedDevice ({ identityPublicKey, deviceName, passphrase = null }) {
199 if (!identityPublicKey) throw new Error('joinSharedDevice requires identityPublicKey')
200 if (!deviceName) throw new Error('joinSharedDevice requires deviceName')
201 const linker = new DeviceLinkingManager()
202 return linker.joinDevice(identityPublicKey, deviceName, passphrase)
203}
204
150205/**
151206 * Verify a chain proof resolves to the local hyperident master root.
152207 *
test/hyperident-vault.test.js +28 -1
……@@ -19,7 +19,9 @@ import {
1919 attestLocalDevice,
2020 verifyChainAgainstLocalMaster,
2121 exportMasterBundle,
22 importMasterBundle
22 importMasterBundle,
23 shareMasterVault,
24 joinSharedDevice
2325} from '../lib/hyperident-vault.js'
2426
2527function mktemp (prefix = 'hyperident-vault') {
……@@ -200,6 +202,31 @@ test('exportMasterBundle → importMasterBundle reproduces the same master root
200202 t.ok(verified.ok, 'B-attested chain verifies against B\'s (now shared) local master')
201203})
202204
205test('joinSharedDevice rejects malformed input', async (t) => {
206 await t.exception(
207 () => joinSharedDevice({ identityPublicKey: '', deviceName: 'x' }),
208 /requires identityPublicKey/
209 )
210 await t.exception(
211 () => joinSharedDevice({ identityPublicKey: 'aa'.repeat(32), deviceName: '' }),
212 /requires deviceName/
213 )
214})
215
216test('shareMasterVault returns the local master pubkey + a stop() callback', async (t) => {
217 await withVault(t, async () => {
218 const generated = await initVault()
219
220 const session = await shareMasterVault({ timeoutMinutes: 1 })
221 t.is(session.identityPublicKey, generated.publicKey,
222 'identityPublicKey === local master root')
223 t.is(typeof session.stop, 'function', 'stop is callable')
224
225 // Tear down the swarm immediately — we don't want a real connection
226 await session.stop()
227 })
228})
229
203230test('importMasterBundle rejects malformed input', async (t) => {
204231 await withVault(t, async () => {
205232 await initVault()