#!/usr/bin/env bun /** * Seed a minimal "smoke-test" mesh with two members. * * Idempotent: safe to run repeatedly. Re-creates members by * deleting any prior "smoke-test" mesh and its cascaded rows first. * * Outputs the meshId + both memberIds + both pubkeys as JSON (stdout) * so peer-a.ts and peer-b.ts can read them before connecting. */ import { eq } from "drizzle-orm"; import sodium from "libsodium-wrappers"; import { db } from "../src/db"; import { invite, mesh, meshMember } from "@turbostarter/db/schema/mesh"; import { user } from "@turbostarter/db/schema/auth"; import { canonicalInvite } from "../src/crypto"; const USER_ID = "test-user-smoke"; const MESH_SLUG = "smoke-test"; const BROKER_URL = process.env.BROKER_WS_URL ?? "ws://localhost:7900/ws"; async function main() { // Generate real ed25519 keypairs so crypto_box (via ed25519→X25519 // conversion) works in Step 18+ round-trip tests. await sodium.ready; const kpOwner = sodium.crypto_sign_keypair(); const kpA = sodium.crypto_sign_keypair(); const kpB = sodium.crypto_sign_keypair(); const OWNER_PUBKEY = sodium.to_hex(kpOwner.publicKey); const OWNER_SECRET = sodium.to_hex(kpOwner.privateKey); const PEER_A_PUBKEY = sodium.to_hex(kpA.publicKey); const PEER_A_SECRET = sodium.to_hex(kpA.privateKey); const PEER_B_PUBKEY = sodium.to_hex(kpB.publicKey); const PEER_B_SECRET = sodium.to_hex(kpB.privateKey); // Ensure the test user exists (re-usable across runs). const [existingUser] = await db .select({ id: user.id }) .from(user) .where(eq(user.id, USER_ID)); if (!existingUser) { await db.insert(user).values({ id: USER_ID, name: "Smoke Test User", email: "smoke@claudemesh.test", emailVerified: true, }); } // Drop any prior mesh with this slug (cascades to members). await db.delete(mesh).where(eq(mesh.slug, MESH_SLUG)); // Fresh mesh + 2 members. const [m] = await db .insert(mesh) .values({ name: "Smoke Test", slug: MESH_SLUG, ownerUserId: USER_ID, ownerPubkey: OWNER_PUBKEY, visibility: "private", transport: "managed", tier: "free", }) .returning({ id: mesh.id }); if (!m) throw new Error("mesh insert failed"); // Build + sign an invite, store it so /join can verify. const expiresAtSec = Math.floor(Date.now() / 1000) + 3600; const invitePayload = { v: 1 as const, mesh_id: m.id, mesh_slug: MESH_SLUG, broker_url: BROKER_URL, expires_at: expiresAtSec, mesh_root_key: "c21va2UtdGVzdC1tZXNoLXJvb3Qta2V5LWRldg", role: "member" as const, owner_pubkey: OWNER_PUBKEY, }; const canonical = canonicalInvite(invitePayload); const signature = sodium.to_hex( sodium.crypto_sign_detached( sodium.from_string(canonical), kpOwner.privateKey, ), ); const fullPayload = { ...invitePayload, signature }; const token = Buffer.from(JSON.stringify(fullPayload), "utf-8").toString( "base64url", ); await db.insert(invite).values({ meshId: m.id, token, tokenBytes: canonical, maxUses: 5, usedCount: 0, role: "member", expiresAt: new Date(expiresAtSec * 1000), createdBy: USER_ID, }); const [peerA] = await db .insert(meshMember) .values({ meshId: m.id, userId: USER_ID, peerPubkey: PEER_A_PUBKEY, displayName: "peer-a", role: "admin", }) .returning({ id: meshMember.id }); const [peerB] = await db .insert(meshMember) .values({ meshId: m.id, userId: USER_ID, peerPubkey: PEER_B_PUBKEY, displayName: "peer-b", role: "member", }) .returning({ id: meshMember.id }); if (!peerA || !peerB) throw new Error("member insert failed"); const seed = { meshId: m.id, ownerPubkey: OWNER_PUBKEY, ownerSecretKey: OWNER_SECRET, inviteToken: token, inviteLink: `ic://join/${token}`, peerA: { memberId: peerA.id, pubkey: PEER_A_PUBKEY, secretKey: PEER_A_SECRET, }, peerB: { memberId: peerB.id, pubkey: PEER_B_PUBKEY, secretKey: PEER_B_SECRET, }, }; console.log(JSON.stringify(seed, null, 2)); process.exit(0); } main().catch((e) => { console.error("[seed] error:", e instanceof Error ? e.message : e); process.exit(1); });