Blame

9c448e Claude (MCP) 2026-03-14 02:24:41
[mcp] Document P2-2/3 username-based URL implementation
1
---
2
category: reference
3
tags:
4
- phase-2
5
- username
6
- routing
7
last_updated: 2026-03-14
8
---
9
10
# P2-2/3: Username-Based URLs for Multi-Tenant Routing
11
12
## Summary
13
14
Added username field to the Users table and updated the resolver to route by `{username}.wikibot.io` instead of `{user_id}.wikibot.io`. Internal storage paths (EFS, wiki_id, ACL keys) remain UUID-based.
15
16
## Changes
17
18
### Infrastructure (`infra/components/dynamodb.py`)
19
- Added `username` attribute (type S) to Users table
20
- Added `username-index` GSI (hash_key: `username`, projection: ALL)
21
22
### User Model (`app/models/user.py`)
23
- `RESERVED_USERNAMES` — 18 reserved names (admin, www, api, dev, mcp, wiki, etc.)
24
- `validate_username(username)` — lowercase alphanumeric + hyphens, 3-30 chars, no leading/trailing hyphens; returns `(valid, error)`
25
- `get_by_username(username)` — GSI query on `username-index`
26
- `set_username(user_id, username)` — validates format, checks reserved list, checks uniqueness via GSI, then updates
27
28
### Auth Middleware (`app/auth/middleware.py`)
29
- `exchange_auth_code()` return dict now includes `needs_username: bool` (true when user has no username set)
30
31
### Management API (`app/management/routes.py`)
32
- New endpoint: `POST /admin/username` with body `{"username": "..."}`
33
- Returns 200 on success, 400 for invalid/reserved, 409 for taken
34
- Normalizes input to lowercase before validation
35
36
### CLI (`app/cli/main.py`)
37
- After `wikibot login`, if `needs_username` is true, prompts user for username and calls `POST /admin/username`
38
- `_set_username()` helper handles the API call and output
39
40
### Resolver (`app/otterwiki/resolver.py`)
41
- `_parse_host()` now extracts username (not user_id) from subdomain — docstrings updated
42
- `__call__()` uses `get_by_username(username)` instead of `get(user_id)`
43
- Extracts `user_id` from the looked-up user record for downstream UUID-based paths
44
- 404 if username not found
45
46
## Test Coverage
47
48
- **Username validation**: valid formats, too short/long, leading/trailing hyphens, uppercase, special chars, reserved names, empty
49
- **GSI lookup**: `get_by_username` found and not found
50
- **set_username**: success, invalid format, reserved, duplicate, idempotent
51
- **POST /admin/username**: success, invalid, reserved, taken, unauthenticated, empty, case normalization
52
- **needs_username**: new user gets `true`, existing user with username gets `false`
53
- **Resolver**: all existing tests updated to use username subdomain; new test verifying UUID-based internal paths
54
55
All 230 runnable tests pass. 15 pre-existing `fastmcp` import failures unrelated.
56
57
## Branch
58
59
Committed on `worktree-agent-a245f93f` (commit `8d37f26`), based on `phase-2`.