10 Commits

Author SHA1 Message Date
Alejandro Gutiérrez
d8bafe3144 fix(web): fully remove payload runtime from production build
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Remove ALL Payload imports, withPayload wrapper, and (payload)
routes. Blog index + changelog are now static data arrays.
Blog post at /blog/peer-messaging-claude-code is static TSX.

Payload CMS stays as a dev dependency for future local admin
but has zero presence in the production build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 09:25:02 +01:00
Alejandro Gutiérrez
f4bcad91b0 refactor(deploy): trim docker images via pnpm deploy --legacy
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Use pnpm deploy to flatten each package's runtime subset into /deploy,
then copy ONLY that into the runtime stage. Catalog + workspace:*
specifiers previously forced full-workspace resolution into every
image's node_modules — unnecessary for either runtime.

Results (arm64, same smoke tests pass):
- broker:   3.26GB → 341MB  (-90%, drops all devDeps incl. drizzle-kit)
- migrate:  3.27GB → 653MB  (-80%, keeps drizzle-kit which IS runtime)

Broker /health confirms GIT_SHA build-arg still propagates (gitSha:
"30bc24f" in smoke test). Migrate still reads drizzle.config.ts and
attempts the connection correctly.

--legacy flag needed because pnpm 10 defaults to inject-workspace-
packages mode which the monorepo doesn't opt into; legacy is safe here.
--ignore-scripts on deploy skips the root postinstall (sherif lint:ws)
which has nothing to do with runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:37:21 +01:00
Alejandro Gutiérrez
a4cd068ef5 feat(deploy): pre-start drizzle-kit migrate init container
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
One-shot migrate container runs drizzle-kit migrate against DATABASE_URL
and exits 0 before web boots. web service depends_on with condition
service_completed_successfully, so failed migrations block web startup
instead of serving 500s against a stale schema. Broker deliberately does
NOT depend on migrate - it tolerates DB-down gracefully per DEPLOY_SPEC
and should keep serving WS peers even during migration failures.

Also excludes apps/cli from docker build context (CLI ships to npm, not
containers) to sidestep zod spec drift in its package.json vs lockfile.

Known followup: migrate image is 3.27GB due to pnpm catalog: specifiers
forcing full-workspace resolution. pnpm deploy bundle trim is a P2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:55:36 +01:00
Alejandro Gutiérrez
1c773be577 feat(db): owner_secret_key + root_key columns on mesh for server-side signing
Completes the server-side invite-signing story. The web UI's
create-invite flow needs the mesh owner's ed25519 SECRET key to sign
each invite payload; these columns let the backend hold + use them
per mesh.

- mesh.mesh.owner_secret_key (text, nullable): ed25519 secret key
  (hex, 64 bytes) paired with owner_pubkey. Stored PLAINTEXT AT REST
  for v0.1.0. Acceptable trade-off for a managed-broker SaaS launch —
  the operator controls the key anyway. v0.2.0 will either encrypt
  with a column-level KEK or migrate to client-held keys.
- mesh.mesh.root_key (text, nullable): 32-byte shared key
  (base64url, no padding) used by channel/broadcast encryption in
  later steps. Embedded in every invite so joiners receive it at
  join time.

migrations/0002_vengeful_enchantress.sql — two ALTER TABLE ADD
COLUMN. Nullable so existing rows don't need backfill to migrate;
the backfill script populates them idempotently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:11:46 +01:00
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
Alejandro Gutiérrez
84e14ff410 feat(web): marketing landing page with Anthropic design system
Landing page at / matching claude.com/product/claude-code structure:
hero, surfaces, pricing, laptop-to-laptop, features, meets-you, faq, cta,
+ floating "Latest news" toaster. Motion-based scroll reveals.

Design system extracted from claude.com via playwriter reverse-engineering:
- Self-hosted Anthropic Sans/Serif/Mono fonts (6 woff2 files)
- --cm-* tokens in globals.css (clay #d97757, gray-050..900, fluid clamps)
- Serif display, Sans UI, Mono terminals & section markers
- Italic clay phrases for emphasis

Header rewritten for design consistency: claudemesh wordmark (mesh glyph +
serif), dark bg, nav (Docs · Pricing · Changelog · GitHub), "Start free" CTA.

Free-first messaging: hero subhead "Free and open-source. Forever.", primary
CTA "Start free", pricing defaults to Solo=Free.

Fixes:
- packages/api: comment out aiRouter (module removed in 1f094c4)
- packages/db/schema/mesh.ts: rename memberRelations → meshMemberRelations
  (missed in beeaa3b rename pass, caught via web build — ack'd by BotMou)
- credits/{api,server,index}: stub out @turbostarter/ai/credits/utils
- remove (marketing)/legal/[slug] route and common/mdx.tsx (cms-backed)
- sitemap: drop blog/legal enumeration (cms removed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:09:38 +01:00
Alejandro Gutiérrez
8ce8b04e75 fix(db): rename pgSchema exports to prevent barrel collision
chat/image/mesh modules all exported a generic `const schema`
binding. When packages/db/src/schema/index.ts did `export * from
"./chat"` + `export * from "./image"` + `export * from "./mesh"`,
TypeScript's ambiguous-re-export rule silently dropped the colliding
bindings — drizzle-kit's introspection could not find the pgSchema
instances, so CREATE SCHEMA statements were never emitted. The
migration worked on the prior dev DB only because chat/image already
existed from an earlier turbostarter run; a fresh clone would fail.

pdf.ts already used `pdfSchema` (unique name). Applied the same
pattern everywhere:
- chat.ts:  `export const chatSchema = pgSchema("chat")`
- image.ts: `export const imageSchema = pgSchema("image")`
- mesh.ts:  `export const meshSchema = pgSchema("mesh")`

Also added `CREATE EXTENSION IF NOT EXISTS vector` at the top of the
migration (pgvector is used by pdf.embedding — the generated
migration assumed it was pre-enabled).

Verified end-to-end against a fresh pgvector/pgvector:pg17 container:
`pnpm drizzle-kit migrate` applies cleanly from scratch, all 7 mesh.*
tables + chat/image/pdf/mesh schemas created correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:02:09 +01:00
Alejandro Gutiérrez
beeaa3b3c6 fix(db): rename mesh.member export to meshMember to avoid collision with auth.member
The schema/index.ts barrel does `export * from "./mesh"` + `export *
from "./auth"`. Both modules exported a symbol named `member`, which
caused TypeScript to silently exclude the ambiguous re-export and
drizzle-kit's introspection couldn't see mesh.member — its generated
migration was missing that table entirely.

Fix: rename the TypeScript binding only. The DB table name stays
"member" inside pgSchema "mesh" (still mesh.member in SQL):
- `export const member = schema.table("member", ...)` →
  `export const meshMember = schema.table("member", ...)`
- Internal references in mesh.ts updated (FK lambdas, relations,
  Zod schemas, inferred TS types)
- apps/broker/src/broker.ts import updated to meshMember as memberTable
- migrations/0000_sloppy_stryfe.sql regenerated — now includes all 7
  mesh.* tables (audit_log, invite, member, mesh, message_queue,
  pending_status, presence)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:47:02 +01:00
Alejandro Gutiérrez
e758205eb8 feat(db): runtime presence + message queue + pending_status tables for broker
- mesh.presence: live WS connection tracking (memberId, sessionId, pid,
  cwd, status, statusSource, statusUpdatedAt, connectedAt, lastPingAt,
  disconnectedAt). Persisted so broker can resume after restart.
- mesh.message_queue: E2E-encrypted envelopes awaiting delivery (meshId,
  senderMemberId, targetSpec, priority, nonce, ciphertext, expiresAt).
  Broker routes ciphertext only — crypto happens client-side.
- mesh.pending_status: first-turn race catcher keyed by (pid, cwd). No
  FK to member (member doesn't exist yet when hook fires pre-register).
- Enums: presence_status, presence_status_source, message_priority.
- Relations: mesh→messageQueue, member→presences+sentMessages,
  presence→member, messageQueue→mesh+sender.
- Cascade chain: mesh → member → presence + messageQueue (no orphans).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:28:24 +01:00
Alejandro Gutiérrez
d3163a5bff feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

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