feat: v2 invite API + CLI claim flow + CLI friction reducer (wave 2)
Wires the v2 invite protocol end-to-end from a CLI user's perspective.
Broker foundation landed in c1fa3bc; this commit is the glue between
it and the human.
API (packages/api)
- createMyInvite now mints BOTH v1 token (legacy) AND v2 capability.
Two-phase insert: row first (to get invite.id), then UPDATE with
signed canonical bytes stored as JSON {canonical, signature} in the
capabilityV2 column. Broker's claim handler parses the same shape.
- canonicalInviteV2 locked to `v=2|mesh_id|invite_id|expires_at|role|
owner_pubkey_hex` — byte-identical to apps/broker/src/crypto.ts.
- brokerHttpBase() helper rewrites wss://host/ws → https://host for
server-to-server calls.
- POST /api/public/invites/:code/claim — thin proxy to broker;
passes status + body through, 502 broker_unreachable on fetch fail,
cache-control: no-store.
- POST /api/my/meshes/:id/invites/email — mints a normal v2 invite
via createMyInvite, records a pending_invite row, calls stubbed
sendEmailInvite (logs TODO for Postmark wiring in a later PR).
- New schemas: claimInviteInput/ResponseSchema,
createEmailInviteInput/ResponseSchema, v2 fields on
createMyInviteResponseSchema.
- v1 paths untouched — legacy /join/[token] and /api/public/invite/:token
continue to work throughout v0.1.x.
CLI (apps/cli)
- New `claudemesh join <code-or-url>` subcommand.
- Accepts bare code (abc12345), short URL (claudemesh.com/i/abc12345),
or legacy ic://join/<token>. Detects v2 vs v1 and dispatches.
- v2 path: generates fresh ephemeral x25519 keypair (separate from
the ed25519 identity) → POST /api/public/invites/:code/claim →
unseals sealed_root_key via crypto_box_seal_open → persists mesh
with inviteVersion: 2 and base64url rootKey to local config.
- Signature verification skipped with TODO — v0.1.x trusts broker;
seal-open is already authenticated.
- apps/cli/src/lib/invite-v2.ts: generateX25519Keypair, claimInviteV2,
parseV2InviteInput.
- state/config.ts: additive rootKey?/inviteVersion? fields.
CLI friction reducer
- apps/cli/src/index.ts: flag-first invocations
(`claudemesh --resume xxx`, `claudemesh -c`, `claudemesh -- --model
opus`) now route through `launch` automatically. Bare `claudemesh`
still shows welcome; known subcommands dispatch normally.
- Removes one word of cognitive load: users never type `launch`.
No schema changes. No new deps. v1 fully backward compatible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -130,9 +130,56 @@ export const createMyInviteResponseSchema = z.object({
|
||||
joinUrl: z.string(),
|
||||
shortUrl: z.string().nullable(),
|
||||
expiresAt: z.coerce.date(),
|
||||
// v2 fields — present on every new invite. v1-only rows will return
|
||||
// these as undefined on the legacy list endpoint; new rows always set
|
||||
// them because createMyInvite now mints v2 capabilities by default.
|
||||
version: z.literal(2).optional(),
|
||||
canonicalV2: z.string().optional(),
|
||||
ownerPubkey: z.string().optional(),
|
||||
});
|
||||
export type CreateMyInviteResponse = z.infer<typeof createMyInviteResponseSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Email invites
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
export const createEmailInviteInputSchema = z.object({
|
||||
email: z.string().email(),
|
||||
role: meshRoleEnum.default("member"),
|
||||
maxUses: z.number().int().min(1).max(1000).default(1),
|
||||
expiresInDays: z.number().int().min(1).max(365).default(7),
|
||||
});
|
||||
export type CreateEmailInviteInput = z.infer<typeof createEmailInviteInputSchema>;
|
||||
|
||||
export const createEmailInviteResponseSchema = z.object({
|
||||
pendingInviteId: z.string(),
|
||||
code: z.string(),
|
||||
email: z.string(),
|
||||
shortUrl: z.string(),
|
||||
expiresAt: z.coerce.date(),
|
||||
});
|
||||
export type CreateEmailInviteResponse = z.infer<
|
||||
typeof createEmailInviteResponseSchema
|
||||
>;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// v2 invite claim (public, proxies to broker)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
export const claimInviteInputSchema = z.object({
|
||||
recipient_x25519_pubkey: z.string().min(32),
|
||||
});
|
||||
export type ClaimInviteInput = z.infer<typeof claimInviteInputSchema>;
|
||||
|
||||
export const claimInviteResponseSchema = z.object({
|
||||
sealed_root_key: z.string(),
|
||||
mesh_id: z.string(),
|
||||
member_id: z.string(),
|
||||
owner_pubkey: z.string(),
|
||||
canonical_v2: z.string(),
|
||||
});
|
||||
export type ClaimInviteResponse = z.infer<typeof claimInviteResponseSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// List my invites (pending + sent)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user