Files
claudemesh/apps/cli/src/crypto/keypair.ts
Alejandro Gutiérrez 758ea0e42c feat(cli): invite-link parsing + join flow + keypair generation
End-to-end join: user runs `claudemesh join ic://join/<base64>` and
walks away with a signed member record + persistent keypair.

new modules:
- src/crypto/keypair.ts: libsodium ed25519 keypair generation. Format
  is crypto_sign_keypair raw bytes, hex-encoded (32-byte pub, 64-byte
  secret = seed || pub). Same format libsodium will need in Step 18
  for sign/verify.
- src/invite/parse.ts: ic://join/<base64url(JSON)> parser with Zod
  shape validation + expiry check. encodeInviteLink helper for tests.
- src/invite/enroll.ts: POST /join to broker, converts ws:// to http://
  transparently.

rewritten join command wires them together:
  1. parse invite → 2. generate keypair → 3. POST /join → 4. persist
  config → 5. print success.

state/config.ts: saveConfig now chmods the file to 0600 after write,
since it holds ed25519 secret keys. No-op on Windows.

signature verification (step 18) + invite-token one-time-use tracking
are deferred. For now the invite link is a plain bearer token; any
client with the link can join.

verified end-to-end via apps/cli/scripts/join-roundtrip.ts:
  build invite → run join subprocess → load new config → connect as
  new member → send A→B → receive push. Flow passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:36:32 +01:00

37 lines
898 B
TypeScript

/**
* Ed25519 keypair generation using libsodium.
*
* We use libsodium-wrappers even in Step 17 (pre-crypto) so the key
* format matches what Step 18's signing/encryption code will expect —
* no migration needed later.
*/
import sodium from "libsodium-wrappers";
let ready = false;
export async function ensureSodium(): Promise<typeof sodium> {
if (!ready) {
await sodium.ready;
ready = true;
}
return sodium;
}
export interface Ed25519Keypair {
/** 32-byte public key, hex-encoded. */
publicKey: string;
/** 64-byte secret key (seed || publicKey), hex-encoded. */
secretKey: string;
}
/** Generate a fresh ed25519 keypair. */
export async function generateKeypair(): Promise<Ed25519Keypair> {
const s = await ensureSodium();
const kp = s.crypto_sign_keypair();
return {
publicKey: s.to_hex(kp.publicKey),
secretKey: s.to_hex(kp.privateKey),
};
}