Alejandro Gutiérrez 0c4a9591fa feat(broker): invite signature verification + atomic one-time-use
Completes the v0.1.0 security model. Every /join is now gated by a
signed invite that the broker re-verifies against the mesh owner's
ed25519 pubkey, plus an atomic single-use counter.

schema (migrations/0001_demonic_karnak.sql):
- mesh.mesh.owner_pubkey: ed25519 hex of the invite signer
- mesh.invite.token_bytes: canonical signed bytes (for re-verification)
Both nullable; required for new meshes going forward.

canonical invite format (signed bytes):
  `${v}|${mesh_id}|${mesh_slug}|${broker_url}|${expires_at}|
   ${mesh_root_key}|${role}|${owner_pubkey}`

wire format — invite payload in ic://join/<base64url(JSON)> now has:
  owner_pubkey: "<64 hex>"
  signature:    "<128 hex>"

broker joinMesh() (apps/broker/src/broker.ts):
1. verify ed25519 signature over canonical bytes using payload's
   owner_pubkey → else invite_bad_signature
2. load mesh, ensure mesh.owner_pubkey matches payload's owner_pubkey
   → else invite_owner_mismatch (prevents a malicious admin from
   substituting their own owner key)
3. load invite row by token, verify mesh_id matches → else
   invite_mesh_mismatch
4. expiry check → else invite_expired
5. revoked check → else invite_revoked
6. idempotency: if pubkey is already a member, return existing id
   WITHOUT burning an invite use
7. atomic CAS: UPDATE used_count = used_count + 1 WHERE used_count <
   max_uses → if 0 rows affected, return invite_exhausted
8. insert member with role from payload

cli side:
- apps/cli/src/invite/parse.ts: zod-validated owner_pubkey + signature
  fields; client verifies signature immediately and rejects tampered
  links (fail-fast before even touching the broker)
- buildSignedInvite() helper: owners sign invites client-side
- enrollWithBroker sends {invite_token, invite_payload, peer_pubkey,
  display_name} (was: {mesh_id, peer_pubkey, display_name, role})
- parseInviteLink is now async (libsodium ready + verify)

seed-test-mesh.ts generates an owner keypair, sets mesh.owner_pubkey,
builds + signs an invite, stores the invite row, emits ownerPubkey +
ownerSecretKey + inviteToken + inviteLink in the output JSON.

tests — invite-signature.test.ts (9 new):
- valid signed invite → join succeeds
- tampered payload → invite_bad_signature
- signer not the mesh owner → invite_owner_mismatch
- expired invite → invite_expired
- revoked invite → invite_revoked
- exhausted (maxUses=2, 3rd join) → invite_exhausted
- idempotent re-join doesn't burn a use
- atomic single-use: 5 concurrent joins → exactly 1 success, 4 exhausted
- mesh_id payload vs DB row mismatch → invite_mesh_mismatch

verified live: tampered link blocked client-side with a clear error.
Unmodified link joins cleanly end-to-end (roundtrip.ts + join-roundtrip.ts
both pass). 64/64 tests green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:02:12 +01:00

TurboStarter Kit

Full-stack monorepo built with Next.js, Expo, Turborepo, and pnpm workspaces.

Prerequisites

Project Structure

apps/
  web/       # Next.js web application (port 3000)
  mobile/    # Expo React Native app
packages/
  ai/        # AI provider integrations
  analytics/ # Analytics providers
  api/       # tRPC API layer
  auth/      # Authentication (BetterAuth)
  billing/   # Payment providers (Stripe, Lemon Squeezy, Polar)
  cms/       # Content management
  db/        # Database (Drizzle ORM + PostgreSQL)
  email/     # Email providers (Resend, Sendgrid, etc.)
  i18n/      # Internationalization
  monitoring/# Monitoring (Sentry, PostHog)
  shared/    # Shared utilities and config
  storage/   # File storage (S3/MinIO)
  ui/        # Shared UI components

Quick Start

1. Install dependencies

pnpm install

2. Configure environment variables

Copy the example env files:

# Root env (database, product name, URL)
cp .env.example .env

# Web app env (auth, billing, email, storage, AI, etc.)
cp apps/web/.env.example apps/web/.env.local

Root .env — minimum required variables:

DATABASE_URL="postgresql://turbostarter:turbostarter@localhost:5440/core"
PRODUCT_NAME="TurboStarter"
URL="http://localhost:3000"
DEFAULT_LOCALE="en"

Note: The database port is 5440 (mapped from Docker), not the default 5432.

apps/web/.env.local — key variables to configure:

Variable Description Required
BETTER_AUTH_SECRET Auth token signing secret Yes
NEXT_PUBLIC_AUTH_PASSWORD Enable password auth (true/false) Yes
NEXT_PUBLIC_URL Public URL of the web app Yes
STRIPE_SECRET_KEY Stripe key (if using Stripe billing) Optional
RESEND_API_KEY Resend key (if using Resend email) Optional
S3_* S3/MinIO storage credentials Optional
OPENAI_API_KEY OpenAI key (if using AI features) Optional

For local MinIO storage, use these S3 settings in apps/web/.env.local:

S3_REGION="us-east-1"
S3_BUCKET="uploads"
S3_ENDPOINT="http://localhost:9000"
S3_ACCESS_KEY_ID="minioadmin"
S3_SECRET_ACCESS_KEY="minioadmin"

See apps/web/.env.example for the full list of available variables.

3. Start infrastructure (Docker Compose)

Start PostgreSQL and MinIO:

docker compose up -d

Wait for services to be healthy:

docker compose up -d --wait

Or use the built-in shortcut:

pnpm services:start

4. Set up the database

Run migrations and seed data:

pnpm services:setup

This runs docker compose up -d --wait, then applies database migrations and seeds initial data.

5. Start development

pnpm dev

The web app will be available at http://localhost:3000.

Docker Commands

Infrastructure Services

Command Description
docker compose up -d Start all services (PostgreSQL + MinIO)
docker compose down Stop all services
docker compose logs -f Follow service logs
docker compose ps Show service status

Or use the pnpm shortcuts:

Command Description
pnpm services:start Start Docker services and wait for healthy
pnpm services:stop Stop Docker services
pnpm services:logs Follow Docker service logs
pnpm services:status Show Docker service status
pnpm services:setup Start services + run DB migrations + seed

Service URLs

Service URL Credentials
Web App http://localhost:3000
PostgreSQL localhost:5440 turbostarter / turbostarter
MinIO API http://localhost:9000 minioadmin / minioadmin
MinIO Console http://localhost:9001 minioadmin / minioadmin

Production Build (Docker)

Build and run the web app as a production Docker image:

docker build -t turbostarter-web .
docker run -p 3000:3000 --env-file apps/web/.env.local turbostarter-web

Development Commands

Command Description
pnpm dev Start all apps in development mode
pnpm build Build all packages and apps
pnpm lint Run ESLint across the monorepo
pnpm format Check formatting with Prettier
pnpm format:fix Fix formatting
pnpm typecheck Run TypeScript type checking
pnpm test Run tests
pnpm auth:seed Seed auth dev accounts

Database Commands

Run from the root (or within packages/db):

Command Description
pnpm --filter @turbostarter/db db:migrate Run database migrations
pnpm --filter @turbostarter/db db:push Push schema changes
pnpm --filter @turbostarter/db db:generate Generate new migration
pnpm --filter @turbostarter/db db:studio Open Drizzle Studio
pnpm --filter @turbostarter/db db:reset Reset database
pnpm --filter @turbostarter/db db:seed Seed database

Dev Login Credentials

After running pnpm services:setup or pnpm auth:seed:

Role Email Password
User me+user@turbostarter.dev Pa$$w0rd
Admin me+admin@turbostarter.dev Pa$$w0rd
Description
ClaudeMesh broker - WebSocket peer broker for Claude Code instances
Readme 40 MiB
Languages
TypeScript 94.7%
JavaScript 2.4%
HTML 1.2%
CSS 1%
Python 0.3%
Other 0.3%