feat: anthropic-style mesh + invite redesign (wave 1 checkpoint)
Ships the user-visible friction fixes and the foundation for the v2
invite protocol. API wiring + CLI client + email UI ship in wave 2.
Meshes — shipped
- Drop global UNIQUE on mesh.slug; mesh.id is canonical everywhere
- Server derives slug from name; create form has no slug field
- Two users can freely name their mesh "platform"; no collision errors
- Migration 0017
Invites v1 — shipped (URL shortener, backward compatible)
- New invite.code column (base62, 8 chars, nullable unique index)
- createMyInvite mints both token + short code; returns shortUrl
- GET /api/public/invite-code/:code resolves short code to token
- New route /i/[code] server-redirects to /join/[token]
- Invite generator UI shows short URL; QR encodes short URL
- Advanced fields (role/maxUses/expiresInDays) collapsed under disclosure
- Migration 0018
Invites v2 — foundation (broker + DB only; API+CLI+Web wiring in wave 2)
- Broker: canonicalInviteV2, verifyInviteV2, sealRootKeyToRecipient
- Broker: POST /invites/:code/claim endpoint (atomic single-use accounting)
- Broker tests: invite-v2.test.ts (signature, expiry, revocation, exhaustion)
- DB: mesh.invite gains version/capabilityV2/claimedByPubkey columns
- DB: new mesh.pending_invite table for email invites
- Migration 0019
- Contract locked in docs/protocol.md §v2 + SPEC.md §14b
Consent landing — shipped
- /join/[token] redesigned: explicit role, inviter, mesh stats, consent
- New server components: invite-card, role-badge, inviter-line, consent-summary
- "Join [mesh] as [Role]" primary action (not just "Join")
Error surfacing — shipped
- handle() now parses {error} responses from hono route catch blocks
- onError fallback includes timestamp so handle() can match apiErrorSchema
- Real error messages reach the UI instead of "Something went wrong"
Docs
- SPEC.md §14b: v2 invite protocol
- docs/protocol.md: v2 claim wire format
- docs/roadmap.md: status
- .artifacts/specs/2026-04-10-anthropic-vision-meshes-invites.md
Deferred to wave 2/3
- API claim route wiring (packages/api)
- createMyInvite v2 capability generation
- Email invite mutation + Postmark delivery
- CLI v2 join flow (x25519 keypair + unseal)
- Web invite-generator email field + v2 display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
110
apps/web/src/modules/join/role-badge.tsx
Normal file
110
apps/web/src/modules/join/role-badge.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
type Role = "admin" | "member";
|
||||
|
||||
const ROLE_CONFIG: Record<
|
||||
Role,
|
||||
{
|
||||
label: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
accent: string;
|
||||
dot: string;
|
||||
}
|
||||
> = {
|
||||
admin: {
|
||||
label: "Admin",
|
||||
description:
|
||||
"Full control: invite and remove peers, manage settings, send and receive messages.",
|
||||
// subtle warning treatment — fig (pinkish) accent, not alarming
|
||||
accent: "#c46686",
|
||||
dot: "#c46686",
|
||||
icon: (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 2l3 6 6 1-4.5 4.5L18 20l-6-3-6 3 1.5-6.5L3 9l6-1 3-6z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
member: {
|
||||
label: "Member",
|
||||
description:
|
||||
"Send and receive messages, read the shared audit log, participate in mesh traffic.",
|
||||
accent: "var(--cm-clay)",
|
||||
dot: "var(--cm-clay)",
|
||||
icon: (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="8" r="4" stroke="currentColor" strokeWidth="1.6" />
|
||||
<path
|
||||
d="M4 20c0-4 4-6 8-6s8 2 8 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
interface RoleBadgeProps {
|
||||
role: Role;
|
||||
}
|
||||
|
||||
export function RoleBadge({ role }: RoleBadgeProps) {
|
||||
const cfg = ROLE_CONFIG[role];
|
||||
return (
|
||||
<div
|
||||
className="flex items-start gap-3 rounded-[var(--cm-radius-md)] border p-4"
|
||||
style={{
|
||||
borderColor: cfg.accent,
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--cm-bg-elevated) 70%, transparent)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full"
|
||||
style={{
|
||||
color: cfg.accent,
|
||||
backgroundColor: "color-mix(in srgb, var(--cm-bg) 60%, transparent)",
|
||||
border: `1px solid ${cfg.accent}`,
|
||||
}}
|
||||
>
|
||||
{cfg.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="flex items-center gap-2 text-[13px] font-medium"
|
||||
style={{ color: cfg.accent, fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
<span className="uppercase tracking-[0.14em]">
|
||||
You'll join as {cfg.label}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className="mt-1 text-[13.5px] leading-[1.55] text-[var(--cm-fg-secondary)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
{cfg.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function roleLabel(role: Role) {
|
||||
return ROLE_CONFIG[role].label;
|
||||
}
|
||||
Reference in New Issue
Block a user