';
+const verified = (e) => verifyEvent(e) ? ' · ✓ signature verifies' : ' · signature fails';
+function vecChip(node) {
+ const r = vecOk(node);
+ if (r === null) return '';
+ const name = node === 'org' ? 'NIP-06' : node === 'btc' ? 'BIP-84' : 'known';
+ return r ? '✓ ' + name + ' test vector' : '✗ test vector mismatch';
+}
+const deriv = (ikm, info) => 'HKDF-SHA256(' + ikm + ', salt "lineage/v0", info "' + info + '")';
+
+function inspect(id) {
+ const dm = S.domain;
+ if (id === 'seed') {
+ const n = S.mnemonic.split(' ').length;
+ return ['seed', head('Root · BIP-39 + BIP-32', 'org seed',
+ 'Every key in the tree comes from these ' + n + ' words' + (S.passphrase ? ' and the passphrase' : '') + '. Whoever holds them is the organization, so for real money or legal weight the root should be a threshold key (FROST or multisig), not one mnemonic.') +
+ body(fields(field('master fingerprint (BIP-32)', D.fp), fieldS('passphrase', S.passphrase ? 'set' : 'none'), field('URN', urn())),
+ secret(() => fields(field('mnemonic', S.mnemonic), field('BIP-39 seed (hex)', hex(S.seed)))))];
+ }
+ if (id === 'eth') {
+ return ["m/44'/60'", head('Legal control · Ethereum', "m/44'/60'/0'/0/0",
+ 'This address holds the Otoco NFT, and the NFT is the LLC. Moving the NFT moves control of the company, so this key should sign almost nothing and sit behind the threshold root.', vecChip('eth')) +
+ body(fields(field('address (EIP-55)', D.eth.addr), field('public key (compressed)', hex(D.eth.pub)), field('URN', urn('legal'))),
+ secret(() => fields(field('private key', '0x' + hex(D.eth.priv)))))];
+ }
+ if (id === 'btc') {
+ return ["m/84'/0'", head('Treasury · Bitcoin', "m/84'/0'/0'/0/0",
+ "Native segwit (BIP-84), the branch supertestnet's vanilla-js-browser-wallet uses. Give the account zpub to a watch-only wallet to see balances; spending still needs the seed.", vecChip('btc')) +
+ body(fields(field('first receive address', D.btc.addr), field("account zpub (m/84'/0'/0')", D.btc.zpub), field('public key', hex(D.btc.pub)), field('URN', urn('treasury'))),
+ secret(() => fields(field('private key (WIF)', wif(D.btc.priv)))))];
+ }
+ if (id === 'org') {
+ const o = D.org;
+ return ['org identity', head('Identity · Nostr (NIP-06 account 0)', '_@' + dm,
+ "The organization's public face, on the path supertestnet's vanilla-js-nostr uses. Its NIP-05 name is _, which clients show as just " + esc(dm) + '. It signs every agent\'s birth certificate.', vecChip('org')) +
+ body(fields(field('npub', o.npub), field('public key (hex)', o.hex), field('path', o.path), field('NIP-05', '_@' + dm), field('URN', urn())),
+ secret(() => fields(field('nsec', o.nsec), field('private key (hex)', hex(o.sk)))))];
+ }
+ if (/^a\d+$/.test(id)) {
+ const a = agentOf(+id.slice(1)), d = D.agents.get(a.i);
+ const birth = birthOf(a), ring = keyringOf(a);
+ const chips = a.revoked ? 'Revoked' : 'Vouched for by the org';
+ return [a.name, head('Agent i=' + a.i + ' · Nostr (NIP-06 account ' + a.i + ')', handle(a),
+ "A hardened child: its key reveals nothing about the org or its siblings, and its public key alone can't show whose child it is, so the org signs a birth certificate. The agent's machine holds this key and derives its own leaves; the org seed never goes there.", chips) +
+ body(fields(
+ '
',
+ field('npub', d.npub), field('public key (hex)', d.hex), field('path', d.path), field('NIP-05 · mailr address', handle(a)), field('URN', urn(a.name))),
+ code('Birth certificate · kind 30078 · signed by the org', fmtEvent(birth), verified(birth)),
+ code('Keyring · kind 30078 · signed by the agent', fmtEvent(ring), verified(ring)),
+ a.revoked ? code('Revocation · kind 5 (NIP-09) · signed by the org', fmtEvent(revocationOf(a)), verified(revocationOf(a))) : '',
+ '
' +
+ (a.revoked ? "Derivation can't be undone, so the keys still exist. Revoking means the org stops vouching: the name leaves nostr.json and no certificate gets renewed." : 'Revoking removes the name from nostr.json and signs a deletion request for the birth certificate. Index ' + a.i + ' is never reused.') + '
',
+ secret(() => fields(field('nsec', d.nsec), field('private key (hex)', hex(d.sk)))))];
+ }
+ const m = /^a(\d+)\.(ssh|git|dkim|wg)$/.exec(id);
+ if (m) {
+ const a = agentOf(+m[1]), d = D.agents.get(a.i), lf = m[2], lv = d.leaves[lf];
+ const title = a.name + ' / ' + lf, src = deriv(a.name + "'s nostr key", lf);
+ if (lf === 'ssh' || lf === 'git') {
+ const line = sshLine(lv.pub, handle(a));
+ const isSsh = lf === 'ssh';
+ return [title, head(isSsh ? 'Leaf · SSH (Ed25519)' : 'Leaf · Git signing (Ed25519)', title,
+ isSsh ? 'Shell access. Servers trust the lineage CA, so giving an agent a rank means signing this key into a certificate; nothing gets pasted into authorized_keys.'
+ : "Commit signing with an SSH key, which Git supports from 2.34. Load the private key into ssh-agent and Git signs with it; a forge or a reviewer checks commits against the allowed_signers line.") +
+ body(fields(fieldS('derived as', src), field('fingerprint', sshFp(lv.pub)), field('public key', line), field('URN', urn(a.name, lf))),
+ isSsh ? code('Certify it on the CA host', 'ssh-keygen -s lineage_ca -I ' + handle(a) + ' -n very -V +52w ' + a.name + '_ssh.pub')
+ : code('git config', 'git config gpg.format ssh\ngit config user.signingkey "key::' + line + '"\ngit config commit.gpgsign true') + code('allowed_signers', handle(a) + ' namespaces="git" ' + line),
+ secret(() => code('OpenSSH private key', opensshPriv(lv.seed, lv.pub, handle(a)))))];
+ }
+ if (lf === 'dkim') {
+ const rec = 'v=DKIM1; k=ed25519; p=' + b64(lv.pub);
+ return [title, head('Leaf · DKIM (Ed25519, RFC 8463)', title,
+ 'Signs mail sent as ' + esc(handle(a)) + '. RFC 8463 recommends signing with an RSA key as well, since not every receiver checks Ed25519 yet.') +
+ body(fields(fieldS('derived as', src), field('selector', a.name + '._domainkey.' + dm), field('TXT value', rec), field('URN', urn(a.name, lf))),
+ code('DNS record', a.name + '._domainkey.' + dm + '. IN TXT "' + rec + '"'),
+ secret(() => fields(field('Ed25519 private key (base64)', b64(lv.seed)))))];
+ }
+ return [title, head('Leaf · WireGuard (X25519)', title,
+ "The agent's key for plain WireGuard peers. Tailscale clients make their own node keys, so on a Headscale tailnet the agent joins by signing in with its npub instead, and its rank picks its ACL group.") +
+ body(fields(fieldS('derived as', src + ', clamped'), field('public key', b64(lv.pub)), field('URN', urn(a.name, lf))),
+ code('Peer entry', '[Peer]\n# ' + handle(a) + '\nPublicKey = ' + b64(lv.pub)),
+ secret(() => code('Interface entry', '[Interface]\nPrivateKey = ' + b64(lv.priv))))];
+ }
+ if (id === 'node') {
+ const n = D.node;
+ return ['node', head('Node · HKDF from the seed', 'the node itself',
+ 'Keys for the machine that runs the directory, relays, mail and git. Derive them during the key ceremony and install them on the node; the node never sees the seed.') +
+ body(fields(field('ssh-ca fingerprint', sshFp(n['ssh-ca'].pub)), field('mtls public key (base64)', b64(n.mtls.pub)), field('noise public key', 'mkey:' + hex(n.noise.pub))))];
+ }
+ const nm = /^node\.(ssh-ca|mtls|noise)$/.exec(id);
+ if (nm) {
+ const lf = nm[1], n = D.node[lf], title = 'node / ' + lf, src = deriv('BIP-39 seed', 'node/' + lf);
+ if (lf === 'ssh-ca') {
+ const line = sshLine(n.pub, 'lineage-ca@' + dm);
+ return [title, head('Node leaf · SSH certificate authority', title,
+ "Servers trust this one key. Rank becomes a certificate principal: sign an agent's ssh key with -n very to promote it, and stop renewing the certificate to demote it.") +
+ body(fields(fieldS('derived as', src), field('fingerprint', sshFp(n.pub)), field('CA public key', line), field('URN', urn('node', lf))),
+ code('sshd_config', 'TrustedUserCAKeys /etc/ssh/lineage_ca.pub\nAuthorizedPrincipalsFile /etc/ssh/principals/%u'),
+ code('/etc/ssh/principals/agent', 'very'),
+ secret(() => code('OpenSSH private key', opensshPriv(n.seed, n.pub, 'lineage-ca@' + dm))))];
+ }
+ if (lf === 'mtls') {
+ return [title, head('Node leaf · TLS between nodes (Ed25519)', title,
+ "Identity for traffic between your own nodes, where both ends can pin the key. Public HTTPS stays with Caddy and its ACME certificates, because public certificate authorities don't issue Ed25519 certificates.") +
+ body(fields(fieldS('derived as', src), field('public key (base64)', b64(n.pub)), field('URN', urn('node', lf))),
+ code('Public key (PEM)', pem('PUBLIC KEY', L.concatBytes(SPKI_ED, n.pub))),
+ secret(() => code('Private key (PKCS#8 PEM)', pem('PRIVATE KEY', L.concatBytes(PKCS8_ED, n.seed)))))];
+ }
+ return [title, head('Node leaf · Headscale noise key (X25519)', title,
+ "Headscale's control-plane key for the Noise protocol. Deriving it means a rebuilt control server comes back with the same identity.") +
+ body(fields(fieldS('derived as', src + ', clamped'), field('public key', 'mkey:' + hex(n.pub)), field('URN', urn('node', lf))),
+ code('config.yaml', 'noise:\n private_key_path: /var/lib/headscale/noise_private.key'),
+ secret(() => code('noise_private.key', 'privkey:' + hex(n.priv))))];
+ }
+ return ['', ''];
+}
+function renderInspector() {
+ clearKeys('k');
+ const [sub, html] = inspect(S.sel);
+ $('#insp').innerHTML = html;
+ $('#insp-sub').textContent = sub;
+}
+
+/* publish */
+function renderExports() {
+ clearKeys('x');
+ ['nostr', 'lineage', 'spec'].forEach((t) => $('#ex-' + t).setAttribute('aria-selected', String(S.ex === t)));
+ $('#ex').setAttribute('aria-labelledby', 'ex-' + S.ex);
+ const live = S.agents.filter((a) => !a.revoked), gone = S.agents.filter((a) => a.revoked);
+ let html = '';
+ if (S.ex === 'nostr') {
+ const names = { _: D.org.hex };
+ live.forEach((a) => { names[a.name] = D.agents.get(a.i).hex; });
+ html = '
Serve this at https://' + esc(S.domain) + '/.well-known/nostr.json (NIP-05). Revoked agents drop out.
Proposed /.well-known/lineage.json: the public half of the tree. Enough to resolve names, trust certificates and verify events, and nothing that can spend or sign.