Compare commits

...

2 Commits

Author SHA1 Message Date
Jiang Bohan
65b66fed77 fix(slugs): rename prod conflicts in migration 056 (home → home-1, dashboard → dashboard-1)
Per db-boy's prod audit in the MUL-961 thread, two §3 slugs had live prod
workspaces at reservation time. Decision on MUL-961: force-rename both in
the audit migration (scheme 1), same playbook as MUL-972 for admin/multica/
new/www.

- `home` → `home-1`  (68a982da, zzlye, 2026-04-14)
- `dashboard` → `dashboard-1`  (ea5a332f, 王争, 2026-04-22)

Targeted UPDATEs land first, followed by a generic `<slug>-N` fallback that
handles any row that slips in between the audit snapshot and deploy. A
post-condition block re-queries the reserved set and fails loud if anything
slipped through.

Down migration reverts the two targeted renames deterministically (they're
keyed by workspace_id, so rollback is safe).

Owner outreach (email zzlye@ + 王争@ about the URL change) is tracked as a
follow-up outside this PR.
2026-04-22 15:04:27 +08:00
Jiang Bohan
81a6554c24 feat(slugs): reserve homepage + expand reserved slug list (MUL-961)
- Fix: `homepage` was a live `/homepage` landing route in apps/web but not
  in the reserved list, so a user could register a workspace slug that
  shadowed the landing page. Now reserved on both backend and frontend.
- Add likely-future global routes (home, dashboard, profile, account,
  billing, notifications, search, members) so we don't have to do another
  audit/rename pass when these get wired up.
- Add API/ops prefixes (v1, v2, graphql, webhooks, sdk, tokens, cli,
  health, ws, metrics, ping) as defense-in-depth against collision with
  API aliases and ops endpoints.
- Clarify in both source files that the dotted/underscored entries in the
  "Next.js / web standards" section are currently unreachable under the
  slug regex `^[a-z0-9]+(?:-[a-z0-9]+)*$` and are kept as defense-in-depth
  in case the regex is ever relaxed.
- Add audit migration 056 following the 047/049 pattern to fail loud if
  any production workspace slug collides with the newly reserved set.
2026-04-22 15:01:19 +08:00
4 changed files with 175 additions and 2 deletions

View File

@@ -31,6 +31,9 @@ export const RESERVED_SLUGS = new Set([
"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",
@@ -48,6 +51,14 @@ export const RESERVED_SLUGS = new Set([
"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).
@@ -63,6 +74,25 @@ export const RESERVED_SLUGS = new Set([
"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` 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",
"ws",
"metrics",
"ping",
// RFC 2142 — privileged email mailboxes. Allowing user workspaces with
// these slugs would let attackers spoof system messaging.
"postmaster",
@@ -82,7 +112,12 @@ export const RESERVED_SLUGS = new Set([
"files",
"uploads",
// Next.js / web standards (framework-mandated)
// 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",

View File

@@ -32,6 +32,9 @@ var reservedSlugs = map[string]bool{
"multica": true, // brand name — prevent impersonation workspaces
"www": true, // hostname confusable; never a legitimate workspace slug
"new": true, // ambiguous verb-as-slug; reserved for future global create routes
"home": true, // likely-future marketing/landing entry
"homepage": true, // existing /homepage landing variant in apps/web
"dashboard": true, // standard SaaS entry; likely-future global route
"help": true,
"about": true,
"pricing": true,
@@ -49,6 +52,14 @@ var reservedSlugs = map[string]bool{
"press": true,
"download": true,
// Account / billing (likely-future global routes in the avatar menu)
"profile": true,
"account": true,
"billing": true,
"notifications": true,
"search": true,
"members": true,
// Dashboard / workspace route segments
"issues": true,
"projects": true,
@@ -62,6 +73,25 @@ var reservedSlugs = map[string]bool{
"workspaces": true, // global /workspaces/new workspace creation page
"teams": true, // 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": true,
"v2": true,
"graphql": true,
"webhooks": true,
"sdk": true,
"tokens": true,
"cli": true,
// Backend ops / observability. `/health` 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": true,
"ws": true,
"metrics": true,
"ping": true,
// RFC 2142 — privileged email mailboxes
"postmaster": true,
"abuse": true,
@@ -79,7 +109,12 @@ var reservedSlugs = map[string]bool{
"files": true,
"uploads": true,
// Next.js / web standards
// 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": true,
"favicon.ico": true,
"robots.txt": true,

View File

@@ -0,0 +1,8 @@
-- Rollback the two targeted renames from 056.up. The DO-block fallback
-- renames are not reversible in general (we don't record the prior slug),
-- but in practice only the two audited rows were touched in prod, and both
-- are identified by workspace_id so the down migration is deterministic.
UPDATE workspace SET slug = 'home'
WHERE id = '68a982da-68a7-4e2e-ac8e-45a0323507f3' AND slug = 'home-1';
UPDATE workspace SET slug = 'dashboard'
WHERE id = 'ea5a332f-06f9-480d-ab81-8f2324c92d80' AND slug = 'dashboard-1';

View File

@@ -0,0 +1,95 @@
-- Audit + rename existing workspace slugs against the newly-added reserved
-- set from MUL-961 (slug review follow-up).
--
-- This PR expands the reserved list in three directions:
-- * §1 Real conflict: `homepage` — `/homepage` is an active Next.js route
-- (`apps/web/app/(landing)/homepage/page.tsx`) that was missing from the
-- reserved list. Audit confirms zero prod workspaces with this slug.
-- * §3 Likely-future routes: home, dashboard, profile, account, billing,
-- notifications, search, members.
-- * API / ops prefixes: v1, v2, graphql, webhooks, sdk, tokens, cli,
-- health, ws, metrics, ping.
--
-- Per db-boy's prod audit (MUL-961 thread, 2026-04-22), two slugs in the §3
-- set already had live prod workspaces:
--
-- * `home` (68a982da-68a7-4e2e-ac8e-45a0323507f3) — zzlye, 2026-04-14
-- * `dashboard` (ea5a332f-06f9-480d-ab81-8f2324c92d80) — 王争, 2026-04-22
--
-- Decision on MUL-961: force-rename both via this migration (scheme 1), same
-- playbook as MUL-972 for admin/multica/new/www. Rename targets `home-1`
-- and `dashboard-1` were verified unoccupied at audit time. The subsequent
-- DO block is a generic fallback that picks `<slug>-N` for any other row
-- that slips in between audit and deploy (defensive against a race with new
-- workspace creation — the reserved-slug check in app code lands in the
-- same deploy, but the migration runs first).
--
-- Owner outreach: zzlye@ and 王争@ should be notified that their
-- workspace URL prefix changed (/home → /home-1, /dashboard → /dashboard-1).
--
-- Keep this slug list aligned with:
-- - server/internal/handler/workspace_reserved_slugs.go
-- - packages/core/paths/reserved-slugs.ts
-- 1. Targeted renames for the two known conflicts at audit time.
UPDATE workspace SET slug = 'home-1'
WHERE id = '68a982da-68a7-4e2e-ac8e-45a0323507f3' AND slug = 'home';
UPDATE workspace SET slug = 'dashboard-1'
WHERE id = 'ea5a332f-06f9-480d-ab81-8f2324c92d80' AND slug = 'dashboard';
-- 2. Generic fallback: any other row whose slug lands in the newly
-- reserved set (race or new data between audit and deploy) is renamed to
-- `<slug>-N` with the lowest N that is free. Same pattern as the existing
-- audit migrations, hardened against collisions.
DO $$
DECLARE
r RECORD;
n INT;
BEGIN
FOR r IN
SELECT id, slug FROM workspace
WHERE slug IN (
-- Real conflict fix
'homepage',
-- Platform / marketing (newly added)
'home', 'dashboard',
-- Account / billing (newly added)
'profile', 'account', 'billing', 'notifications', 'search', 'members',
-- API / integration prefixes (newly added)
'v1', 'v2', 'graphql', 'webhooks', 'sdk', 'tokens', 'cli',
-- Backend ops / observability (newly added)
'health', 'ws', 'metrics', 'ping'
)
LOOP
n := 1;
WHILE EXISTS (SELECT 1 FROM workspace WHERE slug = r.slug || '-' || n) LOOP
n := n + 1;
END LOOP;
UPDATE workspace SET slug = r.slug || '-' || n WHERE id = r.id;
RAISE NOTICE 'Renamed workspace % slug from % to %', r.id, r.slug, r.slug || '-' || n;
END LOOP;
END $$;
-- 3. Post-condition audit: no workspace should remain on a reserved slug.
DO $$
DECLARE
conflict_count INT;
conflict_list TEXT;
BEGIN
SELECT
COUNT(*),
string_agg(slug, ', ' ORDER BY slug)
INTO conflict_count, conflict_list
FROM workspace
WHERE slug IN (
'homepage',
'home', 'dashboard',
'profile', 'account', 'billing', 'notifications', 'search', 'members',
'v1', 'v2', 'graphql', 'webhooks', 'sdk', 'tokens', 'cli',
'health', 'ws', 'metrics', 'ping'
);
IF conflict_count > 0 THEN
RAISE EXCEPTION 'After rename pass, % workspace(s) still on reserved slugs: %. This should be impossible — investigate.', conflict_count, conflict_list;
END IF;
END $$;