# Invite v2 — CLI migration (server-side already shipped) ## Current state **Server-side (broker) — DEPLOYED** - `canonicalInviteV2` bytes format (crypto.ts) - `verifyInviteV2` signature check - `claimInviteV2Core` at `POST /invites/:code/claim` - `sealRootKeyToRecipient` using crypto_box_seal - Every v1 invite also stores `capability_v2` for cross-compat - Web route `/api/public/invites/:code/claim` proxies to broker **Client-side (CLI) — NOT MIGRATED** The CLI still uses the v1 flow (`enrollWithBroker`) which reads `mesh_root_key` from the invite token's base64 payload. This means: - Long URL `/join/` contains the root key - Short URL `/i/` resolves to the long URL → still contains root key - Anyone who can read the URL (history, screenshot, mail archive) has the key ## The v2 CLI flow ``` parseInviteLinkV2(url) → short URL /i/? GET /api/public/invite-code/:code → returns `{ found, code, mesh_slug, broker_url, owner_pubkey, canonical_v2, expires_at, role }` (NO root_key) → generate local x25519 keypair (curve25519) → POST /invites//claim { recipient_x25519_pubkey, display_name } → broker verifies capability_v2 signature → broker seals mesh.root_key with crypto_box_seal(root_key, our_pubkey) → returns { sealed_root_key, mesh_id, member_id, owner_pubkey, canonical_v2 } → open sealed_root_key with our x25519 secret key → store root_key in ~/.claudemesh/config.json.meshes[].rootKey (NOT in the invite link — it was never transmitted unsealed) → upgrade enroll to use claim response instead of the /join endpoint ``` ## What needs to change in the CLI 1. **New file** `apps/cli/src/services/invite/parse-v2.ts` - Detect short URL, resolve via `/api/public/invite-code/:code` - Expect the API returns v2 shape (server already has this route; verify field names) - Generate x25519 keypair via libsodium - POST to claim endpoint - Unseal root_key 2. **Conditional in `parseInviteLink`** - If URL is short-form and broker supports v2, use the new path - Fall back to v1 for legacy long-form URLs in transit 3. **Config schema** already has `rootKey` per mesh — just write from unsealed bytes instead of from the token payload. 4. **Spec test** `tests/golden/invite-v2.test.ts` - Broker already has `claimInviteV2Core` tests; add a CLI-side end-to-end that hits a local broker and verifies the sealed key round-trips. ## Why it wasn't rushed in this session Crypto code deserves review. The server-side v2 shipped weeks ago with its own testing and audit; the CLI migration needs the same rigor — at minimum, a test that proves the sealed key we unseal matches the root_key the broker had in its DB, verified against `canonical_v2` signature. The current v1 flow is a known quantity (the root_key-in-URL risk is documented in the spec). Broker is already v2-ready so when the CLI migration lands, emails / links can immediately start using the claim-only short URL without a server deploy. ## Rollout plan 1. Ship CLI v2 path behind `CLAUDEMESH_INVITE_V2=1` env. 2. Dogfood: new invites generated by `claudemesh share` use `/api/public/invite-code/:code` with v2-shape response that omits token; CLI resolves via claim. 3. Verify with `claudemesh verify` safety numbers cross-check. 4. After 2 weeks uneventful, flip default to v2. 5. After 60 days, stop embedding root_key in long URLs entirely. 6. v3 (future): short URL becomes the only form. ## Effort ~1 day of focused crypto + testing. Broker work is done; API work is done; CLI work is a new parse path + a new enroll path + a few tests.