| … | … | @@ -17,7 +17,9 @@ import crypto from 'hypercore-crypto' |
| 17 | 17 | import { |
| 18 | 18 | loadHyperidentMaster, |
| 19 | 19 | attestLocalDevice, |
| 20 | | verifyChainAgainstLocalMaster |
| 20 | verifyChainAgainstLocalMaster, |
| 21 | exportMasterBundle, |
| 22 | importMasterBundle |
| 21 | 23 | } from '../lib/hyperident-vault.js' |
| 22 | 24 | |
| 23 | 25 | function mktemp (prefix = 'hyperident-vault') { |
| … | … | @@ -138,6 +140,80 @@ test('verifyChainAgainstLocalMaster — empty/invalid chainB64 reports chain-inv |
| 138 | 140 | }) |
| 139 | 141 | }) |
| 140 | 142 | |
| 143 | test('exportMasterBundle returns a bundle containing the mnemonic + master pubkey', async (t) => { |
| 144 | await withVault(t, async () => { |
| 145 | await initVault() |
| 146 | const master = await loadHyperidentMaster() |
| 147 | const bundle = await exportMasterBundle() |
| 148 | |
| 149 | t.ok(bundle, 'bundle returned') |
| 150 | t.ok(bundle.identity, 'bundle has identity field') |
| 151 | t.is(typeof bundle.identity.mnemonic, 'string', 'mnemonic is a string') |
| 152 | t.is(bundle.identity.publicKey, master.publicKeyHex, |
| 153 | 'bundle.identity.publicKey === local master root') |
| 154 | t.ok(bundle.deviceChain, 'bundle has deviceChain field') |
| 155 | t.ok(bundle.deviceChain.keyPair, 'deviceChain has keyPair') |
| 156 | t.ok(bundle.deviceChain.proof, 'deviceChain has proof') |
| 157 | }) |
| 158 | }) |
| 159 | |
| 160 | test('exportMasterBundle → importMasterBundle reproduces the same master root on a fresh vault', async (t) => { |
| 161 | // Two completely independent vaults. Vault A initializes, exports its |
| 162 | // bundle, vault B imports it. After import, both vaults must hold the |
| 163 | // same master root. |
| 164 | const vaultA = mktemp('hyperident-vault-A') |
| 165 | const vaultB = mktemp('hyperident-vault-B') |
| 166 | const { FileManager } = await import('hyperident/lib/file-ops.js') |
| 167 | const prev = FileManager._customPath || null |
| 168 | t.teardown(() => { |
| 169 | if (prev) FileManager.setBasePath(prev) |
| 170 | else FileManager._customPath = null |
| 171 | fs.rmSync(vaultA, { recursive: true, force: true }) |
| 172 | fs.rmSync(vaultB, { recursive: true, force: true }) |
| 173 | }) |
| 174 | |
| 175 | // Init A and capture its master + bundle |
| 176 | FileManager.setBasePath(vaultA) |
| 177 | await initVault() |
| 178 | const masterA = await loadHyperidentMaster() |
| 179 | const bundle = await exportMasterBundle() |
| 180 | |
| 181 | // Switch to B (no init — empty vault) and import the bundle |
| 182 | FileManager.setBasePath(vaultB) |
| 183 | await importMasterBundle({ bundle, deviceName: 'machine-B' }) |
| 184 | const masterB = await loadHyperidentMaster() |
| 185 | |
| 186 | t.is(masterB.publicKeyHex, masterA.publicKeyHex, |
| 187 | 'after import, vault B holds the same master root as vault A') |
| 188 | t.is(masterB.mnemonic, masterA.mnemonic, |
| 189 | 'after import, vault B holds the same mnemonic as vault A') |
| 190 | |
| 191 | // The receiver should also be able to attest a fresh device under the |
| 192 | // shared master — confirming the import wired up everything keet-id-key |
| 193 | // needs to chain new devices. |
| 194 | const attested = await attestLocalDevice({ label: 'machine-B-writer' }) |
| 195 | t.is(attested.rootPubkeyHex, masterA.publicKeyHex, |
| 196 | 'B can attest a fresh device under the shared master') |
| 197 | |
| 198 | // And the chain it produces must verify against the shared master |
| 199 | const verified = await verifyChainAgainstLocalMaster({ chainB64: attested.chainB64 }) |
| 200 | t.ok(verified.ok, 'B-attested chain verifies against B\'s (now shared) local master') |
| 201 | }) |
| 202 | |
| 203 | test('importMasterBundle rejects malformed input', async (t) => { |
| 204 | await withVault(t, async () => { |
| 205 | await initVault() |
| 206 | await t.exception( |
| 207 | () => importMasterBundle({ bundle: null, deviceName: 'x' }), |
| 208 | /requires a bundle object/ |
| 209 | ) |
| 210 | await t.exception( |
| 211 | () => importMasterBundle({ bundle: { identity: {} }, deviceName: '' }), |
| 212 | /requires a deviceName/ |
| 213 | ) |
| 214 | }) |
| 215 | }) |
| 216 | |
| 141 | 217 | test('verifyChainAgainstLocalMaster — no master vault reports no-master', async (t) => { |
| 142 | 218 | // Don't init the vault — verify without any master present. |
| 143 | 219 | const vault = mktemp('hyperident-vault-empty') |