Files
claudemesh/apps/cli/scripts/make-invite.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

25 lines
641 B
TypeScript

#!/usr/bin/env bun
/**
* Build a test invite link from a seeded mesh (reads /tmp/cli-seed.json).
* Writes the link to stdout.
*/
import { readFileSync } from "node:fs";
import { encodeInviteLink } from "../src/invite/parse";
const seed = JSON.parse(readFileSync("/tmp/cli-seed.json", "utf-8")) as {
meshId: string;
};
const link = encodeInviteLink({
v: 1,
mesh_id: seed.meshId,
mesh_slug: "rt-join",
broker_url: process.env.BROKER_WS_URL ?? "ws://localhost:7900/ws",
expires_at: Math.floor(Date.now() / 1000) + 3600,
mesh_root_key: "Y2xhdWRlbWVzaC10ZXN0LW1lc2gta2V5LWRldm9ubHk",
role: "member",
});
console.log(link);