// Test helpers — minting attested device credentials.
//
// Every feed is owned by an identity now, so a test that wants a feed needs a device credential:
// `{ keyPair, proof }`, where the proof chains the keypair to an identity root. That is the same
// shape production uses — the daemon holds exactly this and never the mnemonic.
//
// ⚠ These mint a THROWAWAY identity per call. Two feeds built from separate `identity()` calls are
// owned by different people, which is usually what a test means; two devices from ONE identity are
// two devices of one owner, which is what a roster test means. The distinction matters and is easy
// to get backwards.
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import IdentityKey from 'keet-identity-key'
const hex = (k) => b4a.toString(k, 'hex')
/**
* A whole identity, and the devices under it.
*
* ⛔ A DEVICE is a real Ed25519 keypair — one that can sign, so it can attest the next device with
* no mnemonic. That is the shape pear-git-v3's DeviceLinker ships over blind-pairing
* ({ deviceSecretKey, deviceProof }, never the root), and it is why linking a second machine does
* not need the vault.
*
* A hypercore WRITER key is a different thing entirely: manifest-derived, no secret half, cannot
* sign. It is bound to a device by `bind()`, as attested DATA.
*/
export async function identity () {
const idk = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
const root = idk.identityKeyPair
const mk = (keyPair, proof) => ({
keyPair,
hex: hex(keyPair.publicKey),
proof: b4a.toString(proof, 'base64'),
/** Bind a writer core key to this device — what an `add-device` op actually carries. */
bind (writerKeyHex) {
return b4a.toString(
IdentityKey.attestData(b4a.from(writerKeyHex, 'hex'), keyPair, b4a.from(proof)), 'base64')
},
/** Attest a NEW device from this one. No mnemonic — the blind-pairing path. */
async child () {
const kp = crypto.keyPair()
return mk(kp, IdentityKey.attestDevice(kp.publicKey, keyPair, b4a.from(proof)))
}
})
return {
rootHex: hex(root.publicKey),
/** A device attested directly by the root — what the vault mints, once. */
async device () {
const kp = crypto.keyPair()
return mk(kp, await IdentityKey.bootstrap({ identity: root }, kp.publicKey))
}
}
}
/** Found a feed under a throwaway identity: a device, bound to that feed's writer key. */
export async function attest (feed) {
const d = await (await identity()).device()
return d.bind(feed.deviceKey)
}
/**
* Bound proofs for a fixed set of WRITER keys, all under ONE device of ONE identity.
*
* For the unit tests that drive `applyOp` with constant keys like `'d1'.repeat(32)`. Those stand in
* for writer core keys, which is exactly right: they are opaque, they never sign, and a proof binds
* one as attested data.
*
* ⚠ Keys minted here share an owner AND a device. A key meant to be a STRANGER must come from a
* separate `identity()`, or a test named "a stranger cannot X" is quietly testing a co-owner.
*/
export async function proofsFor (keys) {
const id = await identity()
const d = await id.device()
const out = { __root: id.rootHex, __device: d.hex }
for (const k of keys) out[k] = d.bind(k)
return out
}
export default { identity, attest, proofsFor }