From f0fbd01a79c895eb17c280b4623338ba2be08b63 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sat, 2 May 2026 18:15:35 +0800 Subject: [PATCH] feat(plan): wire hash-encoded API paths into B1 plan --- .../plans/2026-05-02-b1-nextjs-scaffold.md | 115 +++++++++++++++--- 1 file changed, 98 insertions(+), 17 deletions(-) diff --git a/docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md b/docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md index 4b458b6..a8aa25a 100644 --- a/docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md +++ b/docs/superpowers/plans/2026-05-02-b1-nextjs-scaffold.md @@ -27,7 +27,8 @@ | `web/app/layout.tsx` | Create (via frontend-design) | Root layout. | | `web/app/page.tsx` | Create (via frontend-design) | Scaffold-confirmation placeholder. | | `web/app/globals.css` | Create | `@import "tailwindcss";` | -| `web/app/api/[...path]/route.ts` | Create | Catch-all GET/POST proxy to `api-server:3000`. | +| `web/app/api/[...path]/route.ts` | Create | Catch-all GET/POST proxy that maps the public hash to the upstream path and forwards to `api-server:3000`. | +| `web/lib/api-paths.ts` | Create | Hash → upstream-name mapping (single source of truth for both the Route Handler and future client code). | | `docker/web-next/Dockerfile` | Create | Multi-stage Node 22 alpine, standalone output. | | `docker-compose.yml` | Modify | Add `web-next` service. | | `docker-compose.override.yml` | Modify | Add `web-next` build directive. | @@ -245,7 +246,8 @@ Generate two files for a Next.js 15 App Router project that uses Tailwind v4 - Product name "CM Bot V2" - Literal text "cm-web-next scaffold" (operators grep for this) - One-line note that the real dashboard lands in B2 - - An obvious link to /api/acc/ for smoke-testing the proxy + - An obvious link to /api/414322309db5c06d/ for smoke-testing the proxy + (this is the SHA-256[:16] of "acc"; see web/lib/api-paths.ts) Constraints: - Tailwind v4 utility classes only — no external font, image, or icon deps. @@ -294,12 +296,60 @@ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ --- -## Task 3: Catch-all Route Handler proxy to `api-server:3000` +## Task 3: API path mapping + catch-all Route Handler proxy **Files:** +- Create: `web/lib/api-paths.ts` - Create: `web/app/api/[...path]/route.ts` -- [ ] **Step 1: Create the route handler directory** +- [ ] **Step 1: Create `web/lib/api-paths.ts`** + +```bash +mkdir -p /home/yiekheng/projects/cm_bot_v2/web/lib +``` + +Create `web/lib/api-paths.ts` with the precomputed hashes: + +```typescript +// SHA-256(endpoint).hex[:16]. Deterministic; no salt. Public-boundary only: +// the upstream api-server still uses the readable names. See B1 spec. +export const API_PATHS = { + acc: "414322309db5c06d", // upstream: /acc/ + user: "04f8996da763b7a9", // upstream: /user/ + updateAcc: "982830e2982d95de", // upstream: /update-acc-data + updateUser: "f1a25b37d8db494c", // upstream: /update-user-data +} as const; + +// Reverse map for the Route Handler. Keys are the public path segment, +// values are { upstream: string; trailingSlash: boolean }. +export const PUBLIC_TO_UPSTREAM: Record = { + [API_PATHS.acc]: { upstream: "acc", trailingSlash: true }, + [API_PATHS.user]: { upstream: "user", trailingSlash: true }, + [API_PATHS.updateAcc]: { upstream: "update-acc-data", trailingSlash: false }, + [API_PATHS.updateUser]: { upstream: "update-user-data", trailingSlash: false }, +}; +``` + +Verify the hashes (so future readers can double-check): + +```bash +.venv/bin/python3 -c " +import hashlib +for endpoint in ['acc', 'user', 'update-acc-data', 'update-user-data']: + print(f'{endpoint:24s} -> {hashlib.sha256(endpoint.encode()).hexdigest()[:16]}') +" +``` + +Expected output (matches the constants above): + +``` +acc -> 414322309db5c06d +user -> 04f8996da763b7a9 +update-acc-data -> 982830e2982d95de +update-user-data -> f1a25b37d8db494c +``` + +- [ ] **Step 2: Create the route handler directory** ```bash mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app/api/\[...path\] @@ -307,19 +357,26 @@ mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app/api/\[...path\] (The square brackets are Next.js's catch-all dynamic segment syntax. Shell-escape with backslashes.) -- [ ] **Step 2: Write the route handler** +- [ ] **Step 3: Write the route handler** Create `web/app/api/[...path]/route.ts`: ```typescript import { NextRequest, NextResponse } from "next/server"; +import { PUBLIC_TO_UPSTREAM } from "@/lib/api-paths"; const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000"; -async function forward(request: NextRequest, path: string[]): Promise { - const targetPath = "/" + path.join("/"); - const trailingSlash = request.nextUrl.pathname.endsWith("/") ? "/" : ""; - const target = `${API_BASE_URL}${targetPath}${trailingSlash}`; +async function forward( + request: NextRequest, + path: string[], +): Promise { + const hash = path[0]; + const route = PUBLIC_TO_UPSTREAM[hash]; + if (!route) { + return NextResponse.json({ error: "Not Found" }, { status: 404 }); + } + const target = `${API_BASE_URL}/${route.upstream}${route.trailingSlash ? "/" : ""}`; const init: RequestInit = { method: request.method, @@ -362,7 +419,7 @@ export async function POST( } ``` -The error shape `{"error": }` with HTTP 500 matches `cm_web_view.py`'s Flask proxy on exception. +Mapped requests (`GET /api/414322309db5c06d/` etc.) resolve through `PUBLIC_TO_UPSTREAM` and forward to `api-server:3000/acc/` etc. Unmapped hashes return 404 — they're literally not routes we expose. Network/upstream errors return `{"error": }` with HTTP 500, matching `cm_web_view.py`'s shape. - [ ] **Step 3: Build to verify the route compiles** @@ -373,13 +430,22 @@ npm run build 2>&1 | tail -25 Expected: route summary now includes `/api/[...path]` (or similar). No TS errors. -- [ ] **Step 4: Commit** +- [ ] **Step 4: Build and verify the route compiles** + +```bash +cd /home/yiekheng/projects/cm_bot_v2/web && \ +npm run build 2>&1 | tail -25 +``` + +Expected: route summary now includes `/api/[...path]`. No TS errors. The `@/lib/api-paths` import resolves via the `paths` mapping in `tsconfig.json`. + +- [ ] **Step 5: Commit** ```bash cd /home/yiekheng/projects/cm_bot_v2 && \ -git add web/app/api/\[...path\]/route.ts && \ +git add web/lib/api-paths.ts web/app/api/\[...path\]/route.ts && \ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \ - commit -m "feat(web): add catch-all Route Handler proxy to api-server:3000" + commit -m "feat(web): hash-encoded API paths + catch-all Route Handler proxy" ``` --- @@ -779,10 +845,14 @@ Expected: hits in the response. Open `http://localhost:8010/` in a browser — p - [ ] **Step 3: API proxy parity (GET)** ```bash -diff <(curl -s http://localhost:8000/api/acc/) <(curl -s http://localhost:8010/api/acc/) && echo "GET parity OK" +# Old Flask: /api/acc/. New Next.js: /api//. +diff \ + <(curl -s http://localhost:8000/api/acc/) \ + <(curl -s http://localhost:8010/api/414322309db5c06d/) \ + && echo "GET parity OK" ``` -Expected: `GET parity OK`. Both go through to `api-server:3000/acc/` and return the same JSON. +Expected: `GET parity OK`. Both go through to `api-server:3000/acc/` and return the same JSON. The hash mapping is the only thing in front of the upstream — same body, same status. - [ ] **Step 4: API proxy parity (POST)** @@ -793,12 +863,20 @@ diff \ http://localhost:8000/api/update-acc-data) \ <(curl -s -X POST -H 'Content-Type: application/json' \ -d '{"username":"13c1000","password":"x","status":"","link":""}' \ - http://localhost:8010/api/update-acc-data) \ + http://localhost:8010/api/982830e2982d95de) \ && echo "POST parity OK" ``` Expected: `POST parity OK`. Both POSTs round-trip through to `api-server:3000/update-acc-data`. +- [ ] **Step 4b: Unmapped hash returns 404** + +```bash +curl -s -o /dev/null -w "code=%{http_code}\n" http://localhost:8010/api/deadbeefdeadbeef/ +``` + +Expected: `code=404`. Confirms the proxy is allowlist-based, not pass-through. + - [ ] **Step 5: Old `cm-web` still serves** ```bash @@ -842,7 +920,10 @@ Expected: clean shutdown (no orphan complaints — `--remove-orphans` from the C | Next.js 15 + App Router + TypeScript + Tailwind v4 | Task 1 (configs) + Task 2 (app shell) | | `output: "standalone"`, `trailingSlash: true` in `next.config.ts` | Task 1 step 3 | | `frontend-design`-generated `layout.tsx` and `page.tsx` | Task 2 step 2 | -| Catch-all proxy at `web/app/api/[...path]/route.ts` | Task 3 | +| Hash-encoded API paths at the public boundary | Task 3 | +| `web/lib/api-paths.ts` shared mapping for server + client | Task 3 step 1 | +| Catch-all proxy at `web/app/api/[...path]/route.ts` (allowlist-based) | Task 3 step 3 | +| Unmapped hash returns 404 (not pass-through) | Task 9 step 4b | | Multi-stage Dockerfile (Node 22 alpine, standalone output) | Task 4 | | `web-next` service in base compose, `${CM_WEB_NEXT_HOST_PORT:-8010}:3000` | Task 5 step 1 | | Build directive in override | Task 5 step 2 |