fix(01-foundation): resolve plan checker blockers — 3 fixes across 01-02, 01-03, 01-04

- 01-02: wave corrected from 1 to 2 (has depends_on: ["01-01"])
- 01-03: middleware rewritten to Edge-compatible fetch pattern; internal API route
  app/api/internal/validate-token/route.ts handles DB query in Node.js runtime;
  tasks/deliverables queries scoped with inArray(); accepted_total null-coalesced
- 01-04: Task 1 and Task 6 merged → 5 tasks total (was 6, exceeded threshold)
- STATE.md: updated to reflect Phase 1 planning verified, ready for execution

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone Cavalli
2026-05-13 15:20:50 +02:00
parent 81c667838f
commit 2123dc9d00
4 changed files with 267 additions and 148 deletions
@@ -8,6 +8,7 @@ depends_on:
- "01-02"
files_modified:
- src/middleware.ts
- app/api/internal/validate-token/route.ts
- src/lib/client-view.ts
- app/c/[token]/page.tsx
- app/c/[token]/layout.tsx
@@ -28,8 +29,12 @@ must_haves:
- "TypeScript types are exported for downstream UI rendering"
artifacts:
- path: "src/middleware.ts"
provides: "Token validation at Next.js edge middleware"
provides: "Token validation using fetch to internal API route (Edge-compatible)"
contains: "function middleware"
- path: "app/api/internal/validate-token/route.ts"
provides: "Node.js API route that queries DB and returns 200/404 for token validation"
min_lines: 20
contains: "clients.token"
- path: "src/lib/client-view.ts"
provides: "Client-safe type definitions and query functions"
contains: "ClientView"
@@ -42,6 +47,10 @@ must_haves:
min_lines: 10
key_links:
- from: "src/middleware.ts"
to: "app/api/internal/validate-token/route.ts"
via: "fetch('/api/internal/validate-token?token=X')"
pattern: "validate-token"
- from: "app/api/internal/validate-token/route.ts"
to: "Database query for token validation"
via: "db.select().from(clients).where(eq(clients.token, token))"
pattern: "clients\\.token"
@@ -79,82 +88,111 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
<tasks>
<task type="auto">
<name>Task 1: Create src/middleware.ts to validate client tokens at the edge</name>
<name>Task 1: Create src/middleware.ts (Edge-compatible fetch pattern) + internal validate-token API route</name>
<files>
src/middleware.ts
app/api/internal/validate-token/route.ts
</files>
<read_first>
src/db/schema.ts (clients table definition)
package.json (verify Next.js version)
</read_first>
<action>
Create `src/middleware.ts` at project root (NOT in src/app):
**Why two files:** Next.js middleware runs in the Edge runtime by default. The postgres-js driver (used by Drizzle) requires Node.js `net`/`tls` APIs unavailable at the Edge. The solution is a two-layer pattern: middleware uses `fetch()` to call an internal API route that runs in the Node.js runtime and does the actual DB query.
Create `app/api/internal/validate-token/route.ts` (Node.js runtime, does DB query):
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { eq } from 'drizzle-orm';
import { db } from '@/db';
import { clients } from '@/db/schema';
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Only validate client portal routes /c/[token]/*
if (!pathname.startsWith('/c/')) {
return NextResponse.next();
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('token');
if (!token) {
return NextResponse.json({ valid: false }, { status: 400 });
}
// Extract token from path: /c/[token]/...
const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/);
if (!tokenMatch) {
return NextResponse.rewrite(new URL('/404', request.url), { status: 404 });
}
const token = tokenMatch[1];
try {
// Check if token exists in database
const client = await db
const rows = await db
.select({ id: clients.id })
.from(clients)
.where(eq(clients.token, token))
.limit(1);
if (client.length === 0) {
return NextResponse.rewrite(new URL('/404', request.url), { status: 404 });
if (rows.length === 0) {
return NextResponse.json({ valid: false }, { status: 404 });
}
// Token is valid, proceed
return NextResponse.next();
} catch (error) {
console.error('Middleware error validating token:', error);
return NextResponse.rewrite(new URL('/500', request.url), { status: 500 });
return NextResponse.json({ valid: true }, { status: 200 });
} catch {
return NextResponse.json({ valid: false }, { status: 500 });
}
}
```
Create `src/middleware.ts` (Edge-compatible, uses fetch):
```typescript
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Extract token from path: /c/[token]/...
const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/);
if (!tokenMatch) {
return NextResponse.rewrite(new URL('/not-found', request.url));
}
const token = tokenMatch[1];
try {
// Call internal Node.js API route — Edge middleware cannot use postgres-js directly
const validateUrl = new URL(
`/api/internal/validate-token?token=${encodeURIComponent(token)}`,
request.url
);
const res = await fetch(validateUrl.toString());
if (!res.ok) {
return NextResponse.rewrite(new URL('/not-found', request.url));
}
return NextResponse.next();
} catch {
return NextResponse.rewrite(new URL('/not-found', request.url));
}
}
export const config = {
matcher: ['/c/:path*'],
};
```
Key points:
- Middleware runs at the edge before any page renders
- Token is extracted from URL: /c/[token]
- Database query is a simple SELECT to check token existence
- Returns 404 if token not found (no enumeration hints)
- All errors return 500 (generic error handling)
- Middleware is Edge-compatible: no Node.js imports, only `fetch()`
- DB query lives in the API route (Node.js runtime) where postgres-js works correctly
- Token is URL-encoded before being passed as query param
- Non-existent or invalid tokens resolve to `/not-found` (Next.js built-in 404 page)
- Internal API route should not be called directly by clients (no auth secret needed — it only returns boolean valid/invalid)
</action>
<verify>
<automated>test -f src/middleware.ts && echo "middleware.ts exists"</automated>
<automated>grep -q "export.*function middleware" src/middleware.ts && echo "middleware function exported"</automated>
<automated>grep -q "matcher.*c/" src/middleware.ts && echo "matcher configured for /c/ routes"</automated>
<automated>grep -q "clients.token" src/middleware.ts && echo "Token validation query present"</automated>
<automated>! grep -q "from '@/db'" src/middleware.ts && echo "middleware does not import drizzle/db (good — Edge safe)"</automated>
<automated>test -f app/api/internal/validate-token/route.ts && echo "internal validate-token route exists"</automated>
<automated>grep -q "clients.token" app/api/internal/validate-token/route.ts && echo "Token DB query in API route"</automated>
</verify>
<acceptance_criteria>
- `src/middleware.ts` exists and exports middleware function
- Matcher is configured for `/c/:path*`
- Token validation query checks `clients.token`
- Non-existent tokens return 404
- `src/middleware.ts` does NOT import Drizzle/postgres-js (Edge-safe)
- `src/middleware.ts` fetches `/api/internal/validate-token?token=X`
- `app/api/internal/validate-token/route.ts` queries `clients.token` via Drizzle
- Non-existent tokens return `/not-found` (404)
- Matcher configured for `/c/:path*`
- TypeScript compiles without errors
</acceptance_criteria>
</task>
@@ -171,7 +209,7 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
Create `src/lib/client-view.ts`:
```typescript
import { eq } from 'drizzle-orm';
import { eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { clients, phases, tasks, deliverables, payments, documents, notes } from '@/db/schema';
@@ -252,16 +290,24 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
.where(eq(phases.client_id, client.id))
.orderBy(phases.sort_order);
// Fetch all tasks
const tasksRows = await db
.select()
.from(tasks)
.orderBy(tasks.sort_order);
// Fetch all deliverables
const deliverables_rows = await db
.select()
.from(deliverables);
// Fetch tasks scoped to this client's phases only
const phaseIds = phasesRows.map((p) => p.id);
const tasksRows = phaseIds.length === 0
? []
: await db
.select()
.from(tasks)
.where(inArray(tasks.phase_id, phaseIds))
.orderBy(tasks.sort_order);
// Fetch deliverables scoped to this client's tasks only
const taskIds = tasksRows.map((t) => t.id);
const deliverables_rows = taskIds.length === 0
? []
: await db
.select()
.from(deliverables)
.where(inArray(deliverables.task_id, taskIds));
// Fetch payments
const paymentsRows = await db
@@ -356,7 +402,7 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
name: client.name,
brand_name: client.brand_name,
brief: client.brief,
accepted_total: client.accepted_total,
accepted_total: client.accepted_total ?? '0',
},
phases: phasesList,
payments: paymentsList,
@@ -379,6 +425,8 @@ Output: Fully functional `/c/[token]` route that fetches real client data and pr
<automated>grep -q "interface ClientView" src/lib/client-view.ts && echo "ClientView interface defined"</automated>
<automated>grep -q "export async function getClientView" src/lib/client-view.ts && echo "getClientView function exported"</automated>
<automated>! grep -q "quote_items\|service_catalog" src/lib/client-view.ts && echo "quote_items not referenced (good)"</automated>
<automated>grep -q "inArray" src/lib/client-view.ts && echo "inArray scoping present"</automated>
<automated>grep -q "accepted_total.*?? '0'" src/lib/client-view.ts && echo "null coalescing on accepted_total"</automated>
<automated>npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors" || echo "TypeScript OK"</automated>
</verify>
<acceptance_criteria>