Files
multica/packages/core/paths/reserved-slugs.ts
Naiyuan Qing 51bc5a818f fix(onboarding): decouple from workspace state and route invitees correctly (#1936)
PR #1868 conflated "has workspace" with "completed onboarding" —
restore `onboarded_at` as the single signal, and route invited users
through a dedicated /invitations page before they ever see onboarding.

- Backend: CreateWorkspace + AcceptInvitation atomically set
  onboarded_at alongside the member insert, establishing the
  invariant "member row exists ↔ onboarded_at != null" at the DB
  layer.
- Migration 065: one-shot backfill closes the dirty rows produced
  by PR #1868 (users with a workspace but onboarded_at == null).
- Entry points (web callback, login, desktop App): if onboarded_at
  is null, look up pending invitations by email and route to the
  new batch /invitations page; otherwise the resolver picks
  workspace / new-workspace as before.
- OnboardingPage: stops bouncing on hasWorkspaces; only
  hasOnboarded bounces. Unblocks the user from completing
  Step 3 (workspace creation) → Steps 4 / 5.
- StarterContentPrompt: only shows when the user is the solo
  member of the workspace, so invited users never get prompted to
  import starter content into someone else's workspace.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:05:53 +08:00

136 lines
3.6 KiB
TypeScript

/**
* Slugs reserved because they collide with frontend top-level routes,
* platform features, or web standards.
*
* Keep in sync with server/internal/handler/workspace_reserved_slugs.go.
*
* Convention for new global routes (CLAUDE.md): use a single word
* (`/login`, `/inbox`) or `/{noun}/{verb}` (`/workspaces/new`). Hyphenated
* root-level word groups (`/new-workspace`, `/create-team`) collide with
* common user workspace names — see PR for full discussion.
*/
export const RESERVED_SLUGS = new Set([
// Auth flow
"login",
"logout",
"signin",
"signout",
"signup",
"auth",
"oauth",
"callback",
"invite",
"invitations",
"verify",
"reset",
"password",
"onboarding", // historical, kept reserved post-removal
// Platform / marketing routes (current + likely-future)
"api",
"admin",
"multica", // brand name — prevent impersonation workspaces
"www", // hostname confusable; never a legitimate workspace slug
"new", // ambiguous verb-as-slug; reserved for future global create routes
"home", // likely-future marketing/landing entry
"homepage", // existing /homepage landing variant in apps/web
"dashboard", // standard SaaS entry; likely-future global route
"help",
"about",
"pricing",
"changelog",
"docs",
"support",
"status",
"legal",
"privacy",
"terms",
"security",
"contact",
"blog",
"careers",
"press",
"download",
// Account / billing (likely-future global routes in the avatar menu).
"profile",
"account",
"billing",
"notifications",
"search",
"members",
// Dashboard / workspace route segments. Reserving the segment name
// prevents `/{slug}/{view}` from being visually ambiguous (e.g. a
// workspace named "issues" makes `/issues/abc` mean two things).
"issues",
"projects",
"autopilots",
"agents",
"inbox",
"my-issues",
"runtimes",
"skills",
"settings",
"workspaces", // global `/workspaces/new` workspace creation page
"teams", // reserved for future team management routes
// API / integration prefixes. `api` above already covers /api/*; these
// guard against future top-level API alias routes (e.g. /v1, /graphql)
// and against accidental workspace slugs that read like API identifiers.
"v1",
"v2",
"graphql",
"webhooks",
"sdk",
"tokens",
"cli",
// Backend ops / observability. `/health`, `/readyz`, `/healthz`, and `/ws`
// exist on the backend
// host; reserving them on the workspace slug space prevents naming
// confusion if/when these paths are ever proxied through the web origin.
"health",
"readyz",
"healthz",
"ws",
"metrics",
"ping",
// RFC 2142 — privileged email mailboxes. Allowing user workspaces with
// these slugs would let attackers spoof system messaging.
"postmaster",
"abuse",
"noreply",
"webmaster",
"hostmaster",
// Hostname / subdomain confusables. Even on path-based routing these
// names attract phishing and subdomain-takeover attempts.
"mail",
"ftp",
"static",
"cdn",
"assets",
"public",
"files",
"uploads",
// Next.js / web standards. These entries contain characters (dots,
// underscores) that today's slug regex `^[a-z0-9]+(?:-[a-z0-9]+)*$`
// already rejects at the format-validation step — so `isReservedSlug`
// never actually matches them. They are kept as defense-in-depth so
// that if the slug regex is ever relaxed (e.g. to support dotted
// corporate slugs like `acme.io`), these system paths stay protected.
"_next",
"favicon.ico",
"robots.txt",
"sitemap.xml",
"manifest.json",
".well-known",
]);
export function isReservedSlug(slug: string): boolean {
return RESERVED_SLUGS.has(slug);
}