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>
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
/**
|
|
* `claudemesh join <invite-link>` — full join flow.
|
|
*
|
|
* 1. Parse + validate the ic://join/... link
|
|
* 2. Generate a fresh ed25519 keypair (libsodium)
|
|
* 3. POST /join to the broker → get member_id
|
|
* 4. Persist the mesh + keypair to ~/.claudemesh/config.json (0600)
|
|
* 5. Print success
|
|
*
|
|
* Signature verification + invite-token one-time-use land in Step 18.
|
|
*/
|
|
|
|
import { parseInviteLink } from "../invite/parse";
|
|
import { enrollWithBroker } from "../invite/enroll";
|
|
import { generateKeypair } from "../crypto/keypair";
|
|
import { loadConfig, saveConfig, getConfigPath } from "../state/config";
|
|
import { hostname } from "node:os";
|
|
|
|
export async function runJoin(args: string[]): Promise<void> {
|
|
const link = args[0];
|
|
if (!link) {
|
|
console.error("Usage: claudemesh join <invite-link>");
|
|
console.error("");
|
|
console.error("Example: claudemesh join ic://join/eyJ2IjoxLC4uLn0");
|
|
process.exit(1);
|
|
}
|
|
|
|
// 1. Parse.
|
|
let invite;
|
|
try {
|
|
invite = parseInviteLink(link);
|
|
} catch (e) {
|
|
console.error(
|
|
`claudemesh: ${e instanceof Error ? e.message : String(e)}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const { payload } = invite;
|
|
console.log(`Joining mesh "${payload.mesh_slug}" (${payload.mesh_id})…`);
|
|
|
|
// 2. Generate keypair.
|
|
const keypair = await generateKeypair();
|
|
|
|
// 3. Enroll with broker.
|
|
const displayName = `${hostname()}-${process.pid}`;
|
|
let enroll;
|
|
try {
|
|
enroll = await enrollWithBroker({
|
|
brokerWsUrl: payload.broker_url,
|
|
meshId: payload.mesh_id,
|
|
peerPubkey: keypair.publicKey,
|
|
displayName,
|
|
role: payload.role,
|
|
});
|
|
} catch (e) {
|
|
console.error(
|
|
`claudemesh: broker enrollment failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// 4. Persist.
|
|
const config = loadConfig();
|
|
config.meshes = config.meshes.filter(
|
|
(m) => m.slug !== payload.mesh_slug,
|
|
);
|
|
config.meshes.push({
|
|
meshId: payload.mesh_id,
|
|
memberId: enroll.memberId,
|
|
slug: payload.mesh_slug,
|
|
name: payload.mesh_slug,
|
|
pubkey: keypair.publicKey,
|
|
secretKey: keypair.secretKey,
|
|
brokerUrl: payload.broker_url,
|
|
joinedAt: new Date().toISOString(),
|
|
});
|
|
saveConfig(config);
|
|
|
|
// 5. Report.
|
|
console.log("");
|
|
console.log(
|
|
`✓ Joined "${payload.mesh_slug}" as ${displayName}${enroll.alreadyMember ? " (already a member — re-enrolled with same pubkey)" : ""}`,
|
|
);
|
|
console.log(` member id: ${enroll.memberId}`);
|
|
console.log(` pubkey: ${keypair.publicKey.slice(0, 16)}…`);
|
|
console.log(` broker: ${payload.broker_url}`);
|
|
console.log(` config: ${getConfigPath()}`);
|
|
console.log("");
|
|
console.log("Restart Claude Code to pick up the new mesh.");
|
|
}
|