feat(broker): add cli-sync, member-api, jwt modules + DB schema updates
Some checks failed
CI / Typecheck (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

New broker endpoints for CLI auth sync flow (POST /cli-sync),
member profile management, and mesh settings. Includes JWT
verification for dashboard-issued sync tokens. DB schema adds
member profile fields and mesh policy columns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-09 01:54:50 +01:00
parent d263fe0f26
commit a7d9ecab15
6 changed files with 605 additions and 1 deletions

133
apps/broker/src/cli-sync.ts Normal file
View File

@@ -0,0 +1,133 @@
/**
* POST /cli-sync handler.
*
* Accepts a sync JWT from the dashboard, creates or finds member rows
* for each mesh in the token, and returns mesh details + member IDs.
*/
import { and, eq, isNull } from "drizzle-orm";
import { db } from "./db";
import { verifySyncToken, type SyncTokenPayload } from "./jwt";
// Import schema tables
import {
mesh as meshTable,
meshMember as memberTable,
} from "@turbostarter/db/schema/mesh";
import { generateId } from "@turbostarter/shared/utils";
export interface CliSyncRequest {
sync_token: string;
peer_pubkey: string; // ed25519 hex (64 chars)
display_name: string;
}
export interface CliSyncResponse {
ok: true;
account_id: string;
meshes: Array<{
mesh_id: string;
slug: string;
broker_url: string;
member_id: string;
role: "admin" | "member";
}>;
}
export interface CliSyncError {
ok: false;
error: string;
}
export async function handleCliSync(
body: CliSyncRequest,
): Promise<CliSyncResponse | CliSyncError> {
// 1. Validate inputs
if (!body.sync_token || !body.peer_pubkey || !body.display_name) {
return { ok: false, error: "sync_token, peer_pubkey, display_name required" };
}
if (!/^[0-9a-f]{64}$/i.test(body.peer_pubkey)) {
return { ok: false, error: "peer_pubkey must be 64 hex chars (32 bytes)" };
}
// 2. Verify JWT
const tokenResult = await verifySyncToken(body.sync_token);
if (!tokenResult.ok) {
return { ok: false, error: `sync token invalid: ${tokenResult.error}` };
}
const payload = tokenResult.payload;
// 3. For each mesh in the token, create or find a member row
const resultMeshes: CliSyncResponse["meshes"] = [];
for (const tokenMesh of payload.meshes) {
// Verify mesh exists and is not archived
const [m] = await db
.select({ id: meshTable.id, slug: meshTable.slug })
.from(meshTable)
.where(and(eq(meshTable.id, tokenMesh.id), isNull(meshTable.archivedAt)));
if (!m) {
// Skip meshes that don't exist (could have been deleted)
continue;
}
// Check if this pubkey is already a member of this mesh
const [existing] = await db
.select({ id: memberTable.id, role: memberTable.role })
.from(memberTable)
.where(
and(
eq(memberTable.meshId, tokenMesh.id),
eq(memberTable.peerPubkey, body.peer_pubkey),
isNull(memberTable.revokedAt),
),
);
let memberId: string;
let role: "admin" | "member";
if (existing) {
// Already a member — update dashboard link + display name
memberId = existing.id;
role = existing.role;
await db
.update(memberTable)
.set({
dashboardUserId: payload.sub,
displayName: body.display_name,
})
.where(eq(memberTable.id, existing.id));
} else {
// Create new member row
memberId = generateId();
role = tokenMesh.role;
await db.insert(memberTable).values({
id: memberId,
meshId: tokenMesh.id,
peerPubkey: body.peer_pubkey,
displayName: body.display_name,
role: tokenMesh.role,
dashboardUserId: payload.sub,
});
}
resultMeshes.push({
mesh_id: tokenMesh.id,
slug: m.slug,
broker_url: process.env.BROKER_PUBLIC_URL ?? "wss://ic.claudemesh.com/ws",
member_id: memberId,
role,
});
}
if (resultMeshes.length === 0) {
return { ok: false, error: "no valid meshes found in sync token" };
}
return {
ok: true,
account_id: payload.sub,
meshes: resultMeshes,
};
}