Let's keep going on the cwc-api package, we're moving on to phase 4 in @packages/cwc-api/cwc-api-implementation-checklist.md. Let's create a detailed plan and determine if there are any prerequisite programming tasks before we can start.
I'll explore the codebase to understand the current state and design the Phase 4 implementation. Let me launch explore agents to understand the existing handler architecture and access policy stubs.
Task
{
"description": "Explore handler architecture",
"prompt": "Explore the cwc-api handler architecture to understand how access checks are currently implemented (Phase 3 stubs). I need to understand:\n\n1. In `packages/cwc-api/src/handlers/handler.types.ts` - find the access policy stub types (`RouteAccessResult`, `OperationAccessResult`) and any related types\n2. In `packages/cwc-api/src/handlers/RouteHandler.ts` - find how route-level access is checked (the stub implementation)\n3. In `packages/cwc-api/src/handlers/QueryHandler.ts` and `MutationHandler.ts` - find how operation-level access is checked (the stub implementations)\n4. Look at the route config types to understand how `allowedRoles` is defined\n\nProvide the exact code snippets showing the current stub implementations so I can understand what needs to be replaced with real policy logic.",
"subagent_type": "Explore"
}Tool Result
Perfect! Now I have all the information needed. Let me create a comprehensive report of the current Phase 3 stub implementations:
## Summary: CWC-API Handler Architecture - Phase 3 Stub Implementations
I've thoroughly explored the cwc-api handler architecture. Here are the exact code snippets showing the current Phase 3 stub implementations:
---
## 1. Access Policy Types (Handler Types)
**File:** `/packages/cwc-api/src/handlers/handler.types.ts` (lines 195-228)
```typescript
/**
* Route access check result
*/
export type RouteAccessResult = {
allowed: boolean;
reason?: string | undefined;
};
/**
* Operation access check result
*/
export type OperationAccessResult = {
allowed: boolean;
effectiveRole?: CwcRole | undefined; // Role for this specific operation
reason?: string | undefined;
};
/**
* Route access policy checker interface (Phase 4)
*/
export interface RouteAccessPolicy {
checkRouteAccess(context: RequestContext, routeConfig: CwcApiRouteConfig): RouteAccessResult;
}
/**
* Operation access policy checker interface (Phase 4)
*/
export interface OperationAccessPolicy {
checkOperationAccess(
context: RequestContext,
operationContext: OperationContext
): Promise<OperationAccessResult>;
}
```
Route configuration type:
```typescript
export type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {
// Route identification
path: string;
handlerType: CwcApiHandlerType;
// Access control
allowedRoles: CwcRole[];
// Operation (injected worker function)
operation: CwcApiOperation<TPayload, TResult>;
// Debug flag
debug?: boolean | undefined;
};
```
---
## 2. Route-Level Access Check (RequestHandler)
**File:** `/packages/cwc-api/src/handlers/RequestHandler.ts` (lines 129-152)
```typescript
/**
* Check if the current context role can access this route
* Compares context.role against routeConfig.allowedRoles
*/
private checkRouteAccess(): RouteAccessResult {
const { context, routeConfig } = this.options;
const { allowedRoles } = routeConfig;
// Check if the current role is in allowedRoles
if (allowedRoles.includes(context.role)) {
return { allowed: true };
}
// For authenticated users with 'project-owner' role requirement,
// 'logged-on-user' should also be allowed (ownership check happens at operation level)
if (
context.isAuthenticated &&
context.role === 'logged-on-user' &&
allowedRoles.includes('project-owner')
) {
return { allowed: true };
}
return {
allowed: false,
reason: `Role '${context.role}' cannot access route ${routeConfig.path}`,
};
}
```
RequestHandler flow overview (lines 46-55):
```typescript
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, authHeader, pathParams } = this.options;
try {
// Step 1: Check route-level access
const routeAccess = this.checkRouteAccess();
if (!routeAccess.allowed) {
// No session renewal for auth errors
return this.createAccessDeniedResponse(routeAccess.reason);
}
// ... rest of flow
```
---
## 3. Operation-Level Access Check (QueryHandler)
**File:** `/packages/cwc-api/src/handlers/QueryHandler.ts` (lines 87-91)
```typescript
/**
* Check if the current context can perform this operation
* Phase 3: Stub - always returns allowed
* Phase 4: Will check ownership, resource access, etc.
*/
private async checkOperationAccess(): Promise<OperationAccessResult> {
// Phase 4 will implement actual access policy checks
// For now, route-level access is sufficient
return { allowed: true };
}
```
QueryHandler invocation (lines 44-52):
```typescript
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, operationContext } = this.options;
try {
// Step 1: Check operation-level access (Phase 4 implementation)
const operationAccess = await this.checkOperationAccess();
if (!operationAccess.allowed) {
return createOperationDeniedResponse(this.config, operationAccess.reason);
}
// Step 2: Execute the operation
```
---
## 4. Operation-Level Access Check (MutationHandler)
**File:** `/packages/cwc-api/src/handlers/MutationHandler.ts` (lines 86-89)
```typescript
/**
* Check if the current context can perform this operation
* Phase 3: Stub - always returns allowed
* Phase 4: Will check ownership, resource access, etc.
*/
private async checkOperationAccess(): Promise<OperationAccessResult> {
// Phase 4 will implement actual access policy checks
return { allowed: true };
}
```
Identical to QueryHandler - same invocation pattern in lines 44-52.
---
## 5. Error Response Creation
**File:** `/packages/cwc-api/src/handlers/responseUtils.ts` (lines 35-48)
```typescript
/**
* Creates a 403 response for operation access denied
*/
export function createOperationDeniedResponse(
config: CwcApiConfig,
reason?: string
): CwcApiHandlerResponse {
return {
statusCode: 403,
body: {
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
...(config.isDev && reason ? { errorDetail: reason } : {}),
},
};
}
```
---
## 6. Route Config Definition Example (from test mocks)
**File:** `/packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts` (lines 16-29)
```typescript
/**
* Creates a mock route configuration for testing
*/
export function createMockRouteConfig<TPayload = unknown, TResult = unknown>(
overrides: Partial<CwcApiRouteConfig<TPayload, TResult>> = {}
): CwcApiRouteConfig<TPayload, TResult> {
return {
path: '/test',
handlerType: 'query' as CwcApiHandlerType,
allowedRoles: ['guest-user', 'logged-on-user', 'project-owner'] as CwcRole[],
operation: jest.fn<(p: TPayload, c: OperationContext) => Promise<OperationResult<TResult>>>().mockResolvedValue({
success: true,
data: {} as TResult,
}),
...overrides,
};
}
```
---
## Key Findings Summary
**Current State (Phase 3):**
1. **Route-level access**: Fully implemented
- Compares `context.role` against `routeConfig.allowedRoles`
- Allows `logged-on-user` to access `project-owner` routes (deferred ownership check)
- Returns 401 for unauthenticated, 403 for authenticated but not allowed
2. **Operation-level access**: Stub implementations
- `QueryHandler.checkOperationAccess()` - returns `{ allowed: true }` always
- `MutationHandler.checkOperationAccess()` - returns `{ allowed: true }` always
- Both are private async methods with identical signatures
3. **Type structure ready for Phase 4:**
- `RouteAccessResult` and `OperationAccessResult` types defined
- Interfaces `RouteAccessPolicy` and `OperationAccessPolicy` defined (not yet used)
- `OperationAccessResult` supports `effectiveRole` for role refinement
4. **Error handling:**
- `OPERATION_ACCESS_DENIED` error code maps to 403 status
- Dev mode shows reason details via `errorDetail` field
- Production mode shows generic "Operation not permitted" message
5. **Context data available for Phase 4:**
- `OperationContext` includes `context`, `projectId`, and `resourceId`
- `RequestContext` (AuthenticatedContext) includes `ownedProjects` and full JWT payload
- Ready for ownership validation and resource-level access checks
agentId: acee91b (for resuming to continue this agent's work if needed)Task
{
"description": "Explore auth types and roles",
"prompt": "Explore the cwc-types and cwc-api packages to understand the existing role and context types:\n\n1. In `packages/cwc-types/src/authTypes.ts` - find the `CwcRole` type definition and any related types\n2. In `packages/cwc-api/src/context/context.types.ts` - find the `AuthenticatedContext` and `GuestContext` types to understand what data is available for access decisions\n\nI need to understand what roles exist and what context data is available for making access policy decisions.",
"subagent_type": "Explore"
}Tool Result
Perfect! I've found the key type definitions. Here's what exists:
## Role and Context Types Summary
### CwcRole Type (authTypes.ts)
```typescript
type CwcRole = 'project-owner' | 'logged-on-user' | 'guest-user';
```
Three roles available for access control:
- **project-owner**: User owns the project
- **logged-on-user**: Authenticated user (may not own the project)
- **guest-user**: Unauthenticated request (read-only)
### Context Types (context.types.ts)
**AuthenticatedContext** - Available for authenticated users:
```typescript
type AuthenticatedContext = {
isAuthenticated: true;
role: Exclude<CwcRole, 'guest-user'>; // 'project-owner' or 'logged-on-user'
userPkId: number;
username: string;
ownedProjects: string[]; // Projects owned by user
payload: UserJwtPayload; // Full JWT payload
};
```
**GuestContext** - For unauthenticated users:
```typescript
type GuestContext = {
isAuthenticated: false;
role: 'guest-user';
};
```
### Available Data for Access Decisions
For **authenticated users**, you have access to:
- **User identity**: `userPkId`, `username`
- **Ownership info**: `ownedProjects` array (natural project IDs)
- **JWT payload**: Full `UserJwtPayload` with claims
- **Boolean flags**: Can use `isAuthenticated: true` for discriminating
For **guest users**:
- Only the role (`'guest-user'`) to determine read-only permissions
### Key Architectural Notes
From the CLAUDE.md:
- Role is **determined per-operation** (not fixed at context creation)
- The current `role` in context is a placeholder
- **Route-level access** is checked in RequestHandler
- **Operation-level access** is being implemented in Phase 4 (this is where you'd check `ownedProjects`)
- Session renewal happens after operations complete
The `ownedProjects` array is particularly useful for operation-level checks - you can verify if `projectId` is in this array to determine if the user is the owner of the resource being accessed.
agentId: a3b17cf (for resuming to continue this agent's work if needed)Task
{
"description": "Explore cwc-auth access patterns",
"prompt": "Explore the cwc-auth package to understand how it handles access control for its endpoints. Look for:\n\n1. Any route-level access control patterns in `packages/cwc-auth/src/`\n2. How endpoints determine if a user is authenticated or has the right permissions\n3. Any patterns we should follow or avoid in cwc-api\n\nThis will help ensure cwc-api follows consistent patterns with other services in the codebase.",
"subagent_type": "Explore"
}Tool Result
Perfect! I now have a comprehensive understanding of how cwc-auth handles access control. Let me provide a detailed analysis.
## Access Control Pattern Analysis: cwc-auth vs cwc-api
Based on my thorough exploration of the cwc-auth package and its integration with cwc-api, here's what I found:
### Key Findings
#### 1. **cwc-auth Access Control Pattern: Route-Level with Metadata**
cwc-auth uses a **declarative metadata-driven approach** where access control is defined at the route configuration level:
```typescript
// packages/cwc-auth/src/apis/LogonApiV1/routes.ts
return {
'/logon': {
handlerType: 'logon',
path: '/logon',
method: 'POST',
requiresAuth: false, // <-- Access control as metadata
worker: logonWorker,
},
'/logoff': {
handlerType: 'logoff',
path: '/logoff',
method: 'POST',
requiresAuth: true, // <-- Access control as metadata
worker: logoffWorker,
},
};
```
**Key characteristics:**
- Simple boolean `requiresAuth` flag at the route level
- No explicit access control enforcement in the handlers themselves
- **Auth enforcement happens inside the worker functions** (e.g., `logoffWorker`, `renewSessionWorker`)
- Workers call auth functions (`invalidateSession`, `renewSession`) which perform JWT verification
- If JWT verification fails, workers return error; if succeeds, they proceed with the operation
#### 2. **cwc-auth Security Model: JWT Verification First**
All protected operations follow this pattern:
```typescript
// packages/cwc-auth/src/auth/createInvalidateSession.ts
export function createInvalidateSession(deps: AuthDependencies) {
return async function invalidateSession(authHeader: string | undefined) {
// Step 1: Extract JWT from header
const token = extractJwtFromHeader(authHeader);
if (!token) {
return { success: false, errorCode: 'MISSING_TOKEN', ... };
}
// Step 2: Verify JWT signature and claims
const verifyResult = verifyUserJwt(token, config.secrets.userJwtSecret);
if (!verifyResult.success) {
return { success: false, errorCode: 'INVALID_TOKEN', ... };
}
// Step 3: Database checks (session exists, user owns resource)
const existingSession = await sqlClient.query(...);
if (!existingSession) {
return { success: false, errorCode: 'SESSION_NOT_FOUND', ... };
}
// Step 4: Perform actual operation (delete session, etc.)
const deleteResult = await sqlClient.mutate(...);
// ...
};
}
```
**Strict validation layers:**
1. Token extraction from `Authorization: Bearer <token>` header
2. JWT signature verification with HS256 secret
3. Payload validation (type guards check all claims)
4. Database ownership checks (filters use both `userJwtId` and `userPkId`)
5. SECURITY: Generic error responses in production mode
#### 3. **cwc-auth JWT Payload Verification: Strict Type Guards**
```typescript
// packages/cwc-auth/src/jwt/verifyUserJwt.ts
function isValidUserJwtPayload(payload: unknown): payload is UserJwtPayload {
// Validate ALL required fields exist and have correct types
if (
typeof p['jti'] !== 'string' ||
typeof p['sub'] !== 'string' ||
typeof p['iat'] !== 'number' ||
typeof p['exp'] !== 'number'
) return false;
// SECURITY: Validate sub is valid positive integer (prevents NaN, overflow)
const subNumber = parseInt(subString, 10);
if (
isNaN(subNumber) ||
subNumber <= 0 ||
subNumber > Number.MAX_SAFE_INTEGER ||
String(subNumber) !== subString
) return false;
// Recursively validate login claims object
if (!isValidLoginClaims(p['login'])) return false;
return true;
}
```
**Notable security practice:** The type guard validates that `sub` converts cleanly to an integer without extra characters - prevents token tampering.
#### 4. **cwc-api Access Control Pattern: Role-Based with Route Configuration**
cwc-api uses a more sophisticated approach:
```typescript
// packages/cwc-api/src/handlers/handler.types.ts
export type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {
path: string;
handlerType: CwcApiHandlerType;
allowedRoles: CwcRole[]; // <-- Explicit role-based access
operation: CwcApiOperation<TPayload, TResult>;
debug?: boolean | undefined;
};
```
**Key characteristics:**
- Uses `allowedRoles` array instead of boolean flags
- Roles: `'project-owner'`, `'logged-on-user'`, `'guest-user'`
- Access enforcement happens in `RequestHandler.checkRouteAccess()` before delegation
- Graceful fallback: `logged-on-user` can access `project-owner` routes (ownership verified at operation level)
#### 5. **Separation of Concerns Pattern**
**cwc-auth (authentication authority):**
- "Is this token valid?"
- "Who is this user?" (from JWT claims)
- Token verification and validation
- Returns error responses to client
**cwc-api (authorization boundary):**
- "Can this user access this endpoint?" (route-level, via `allowedRoles`)
- "Can this user perform this operation?" (operation-level, Phase 4)
- "Is this user the owner of this project?" (business logic check)
- Delegates auth verification to cwc-auth via `AuthClient.verifyToken()`
#### 6. **cwc-api Authentication Flow**
```typescript
// packages/cwc-api/src/context/createContext.ts
export async function createContext(options: CreateContextOptions): Promise<RequestContext> {
const { authHeader, authClient } = options;
// No header = guest user (no error thrown)
if (!authHeader) {
return createGuestContext();
}
// Verify token via cwc-auth microservice
const result = await authClient.verifyToken(authHeader);
// Verification failed = guest user (graceful degradation)
if (!result.success) {
return createGuestContext();
}
// Verification succeeded = authenticated context with claims
return createAuthenticatedContext(result.payload);
}
```
**Key feature:** Graceful degradation - auth failures don't throw errors, they degrade to guest context.
### Patterns to Follow in cwc-api
1. **Route Configuration Metadata** (Already in place)
- Define `allowedRoles` at route config level
- Route-level access checked before any business logic
- Returns 401 (unauthenticated) or 403 (authenticated but forbidden)
2. **JWT Verification Delegation** (Already in place)
- cwc-api does NOT duplicate JWT verification
- Uses `AuthClient.verifyToken()` to call cwc-auth's service API
- Centralizes authentication logic in cwc-auth
3. **Graceful Degradation** (Already in place)
- Missing/invalid/expired JWT → guest context (not errors)
- Auth service failures → logged but don't fail requests
- Enables public read-only access to content
4. **Session Renewal** (Already in place)
- RequestHandler renews session after successful operations
- Calls `AuthClient.renewSession()` with current JWT
- Merges new JWT into response body
- Renewal failures don't fail the operation (logged as warning)
5. **Error Responses**
- 401 for unauthenticated requests
- 403 for authenticated but not authorized requests
- Generic errors in production, detailed in development
- Errors don't include sensitive information
6. **Database Ownership Checks** (Pattern to follow)
- When verifying user can perform operation on resource
- Filter queries with BOTH user ID and resource ID: `{ projectId, userPkId }`
- Prevents users from bypassing authorization by manipulating parameters
### Critical Patterns to Avoid
1. **Do NOT duplicate JWT verification in cwc-api**
- Currently correct: using `AuthClient.verifyToken()`
- Would be wrong: re-verifying JWT in cwc-api
2. **Do NOT throw errors on auth failures**
- Currently correct: graceful degradation to guest context
- Would be wrong: throwing on missing/invalid tokens
3. **Do NOT mix authentication and authorization**
- Authentication (is token valid?) → cwc-auth's job
- Authorization (can user do X?) → cwc-api's job
- Currently handled correctly with separation
4. **Do NOT return detailed error info in production**
- cwc-auth pattern: `config.isDev ? errorDetail : undefined`
- Prevents token analysis and user enumeration attacks
### File References
**cwc-auth Access Control:**
- `/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-auth/src/apis/LogonApiV1/routes.ts` - Route config with `requiresAuth`
- `/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-auth/src/auth/createInvalidateSession.ts` - JWT verification pattern
- `/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-auth/src/jwt/verifyUserJwt.ts` - Strict type guards
**cwc-api Access Control:**
- `/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/handler.types.ts` - Route config with `allowedRoles`
- `/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts` - Route access check logic
- `/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/context/createContext.ts` - JWT verification delegation
agentId: a9d2996 (for resuming to continue this agent's work if needed)I have a comprehensive understanding of the current state. Let me now launch a Plan agent to design the Phase 4 implementation.
Task
{
"description": "Design access policy system",
"prompt": "Design the Phase 4 Access Policy System for cwc-api based on these findings:\n\n## Current State (Phase 3)\n\n**Route-level access (already implemented in RequestHandler):**\n- `RequestHandler.checkRouteAccess()` compares `context.role` against `routeConfig.allowedRoles`\n- Returns 401 for unauthenticated, 403 for authenticated but not allowed\n- Special case: `logged-on-user` allowed to access `project-owner` routes (ownership deferred)\n\n**Operation-level access (stub):**\n- `QueryHandler.checkOperationAccess()` and `MutationHandler.checkOperationAccess()` always return `{ allowed: true }`\n\n**Types already defined in handler.types.ts:**\n```typescript\nexport type RouteAccessResult = {\n allowed: boolean;\n reason?: string | undefined;\n};\n\nexport type OperationAccessResult = {\n allowed: boolean;\n effectiveRole?: CwcRole | undefined;\n reason?: string | undefined;\n};\n\nexport interface RouteAccessPolicy {\n checkRouteAccess(context: RequestContext, routeConfig: CwcApiRouteConfig): RouteAccessResult;\n}\n\nexport interface OperationAccessPolicy {\n checkOperationAccess(\n context: RequestContext,\n operationContext: OperationContext\n ): Promise<OperationAccessResult>;\n}\n```\n\n**Context data available for access decisions:**\n- `AuthenticatedContext`: `userPkId`, `username`, `ownedProjects[]`, `payload`\n- `GuestContext`: `role: 'guest-user'`\n- `OperationContext`: `context`, `projectId?`, `resourceId?`\n\n## Phase 4 Checklist Requirements\n\n1. `src/policies/policy.types.ts` - Define RoutePolicy, OperationPolicy types\n2. `src/policies/routePolicies.ts` - Define which roles can access each route, allowUnauthenticated routes\n3. `src/policies/operationPolicies.ts` - Define which roles can execute operations, context-aware checks (ownership)\n4. `src/policies/checkRouteAccess.ts` - Route access enforcement\n5. `src/policies/checkOperationAccess.ts` - Operation access enforcement\n6. `src/policies/index.ts` - Exports\n\n## Security Requirements from Checklist\n\n- Default-deny policy (routes not explicitly allowed are blocked)\n- Ownership checks use userPkId from context (not request body)\n- All mutation routes require authentication\n- No privilege escalation paths\n- Policy checks cannot be bypassed via parameter manipulation\n\n## Design Task\n\nDesign the access policy system. Consider:\n\n1. **Route Policies**: Should we keep `allowedRoles` on route configs or centralize in routePolicies.ts?\n2. **Operation Policies**: How should ownership checks work? The user owns a project if `context.ownedProjects.includes(projectId)`.\n3. **Integration**: How do the policy functions integrate with existing handlers?\n4. **Role determination**: When does `logged-on-user` become `project-owner`? At operation level when ownership is verified?\n\nProvide a detailed implementation plan with:\n- Type definitions for policy.types.ts\n- Function signatures for checkRouteAccess and checkOperationAccess\n- How handlers call these functions\n- Test scenarios to cover",
"subagent_type": "Plan"
}Tool Result
Now I have a comprehensive understanding of the existing codebase. Let me provide a detailed design for the Phase 4 Access Policy System.
---
# Phase 4 Access Policy System Design
## Executive Summary
The Phase 4 Access Policy System provides fine-grained access control at two levels:
1. **Route-level**: Can this role access this endpoint at all?
2. **Operation-level**: Can this user perform this specific operation on this resource?
The design follows the existing patterns in cwc-api, leveraging the context layer from Phase 2 and handler architecture from Phase 3.
---
## Architecture Overview
```
Request Flow with Access Policies:
RequestHandler.processRequest()
├── checkRouteAccess(context, routeConfig) ← policies/checkRouteAccess.ts
│ └── Returns RouteAccessResult: { allowed, reason? }
│ └── If denied: 401 (guest) or 403 (authenticated)
├── Build OperationContext (projectId, resourceId from path)
├── Delegate to QueryHandler or MutationHandler
│ ├── checkOperationAccess(context, operationContext) ← policies/checkOperationAccess.ts
│ │ └── Returns OperationAccessResult: { allowed, effectiveRole?, reason? }
│ │ └── Ownership check: context.ownedProjects.includes(projectId)
│ │ └── If owner: effectiveRole = 'project-owner'
│ │ └── If denied: 403 OPERATION_ACCESS_DENIED
│ ├── Execute operation()
│ └── Format response
└── Renew session (if authenticated and not auth error)
```
---
## Design Decision: Keep `allowedRoles` on Route Configs
**Recommendation: Keep `allowedRoles` on `CwcApiRouteConfig`** (the current approach).
**Rationale:**
1. Routes and their access requirements are tightly coupled (defined together in `routes.ts`)
2. Avoids synchronization issues between route definitions and a separate policy file
3. Route-level access is straightforward (role-in-array check)
4. `checkRouteAccess` function extracts the logic, making it testable and replaceable
5. For Phase 6, route definitions in `CwcApiV1/routes.ts` will define `allowedRoles` inline
**The role of `routePolicies.ts`:**
- Provides **default policy behavior** (e.g., default-deny for undefined routes)
- Provides **helper functions** for common patterns (e.g., `requiresAuthentication()`)
- Documents route access patterns centrally for security review
---
## Type Definitions
### `/packages/cwc-api/src/policies/policy.types.ts`
```typescript
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext } from '../context';
import type { CwcApiRouteConfig, OperationContext } from '../handlers/handler.types';
// ============================================================================
// Route Policy Types
// ============================================================================
/**
* Result of route-level access check
* Already defined in handler.types.ts as RouteAccessResult
* Re-exported here for convenience
*/
export type { RouteAccessResult } from '../handlers/handler.types';
/**
* Route access policy configuration
* Defines default behavior and helpers for route-level access checks
*/
export type RoutePolicyConfig = {
/**
* Default behavior when a route is not found or has no allowedRoles
* true = default-deny (safe), false = default-allow (unsafe)
*/
defaultDeny: boolean;
/**
* Roles that are implicitly allowed to access routes requiring 'project-owner'
* Default: ['logged-on-user'] - ownership verified at operation level
*/
ownerProxyRoles: CwcRole[];
};
/**
* Function signature for route access checker
*/
export type CheckRouteAccessFn = (
context: RequestContext,
routeConfig: CwcApiRouteConfig,
policyConfig?: RoutePolicyConfig
) => RouteAccessResult;
// ============================================================================
// Operation Policy Types
// ============================================================================
/**
* Result of operation-level access check
* Already defined in handler.types.ts as OperationAccessResult
* Re-exported here for convenience
*/
export type { OperationAccessResult } from '../handlers/handler.types';
/**
* Operation policy requirements
* Defines what access level is required for an operation
*/
export type OperationPolicyRequirement =
| 'public' // Anyone can perform (guest, logged-on, owner)
| 'authenticated' // Requires authenticated user
| 'project-owner' // Requires ownership of the project
| 'resource-owner'; // Requires ownership of the specific resource
/**
* Operation policy configuration
* Maps operation contexts to their access requirements
*/
export type OperationPolicyConfig = {
/**
* The access requirement for this operation
*/
requirement: OperationPolicyRequirement;
/**
* Optional: Custom validation function for complex scenarios
* Called after standard requirement check passes
* Return false to deny, true to allow
*/
customCheck?: (context: RequestContext, operationContext: OperationContext) => boolean;
};
/**
* Function signature for operation access checker
*/
export type CheckOperationAccessFn = (
context: RequestContext,
operationContext: OperationContext,
policy?: OperationPolicyConfig
) => Promise<OperationAccessResult>;
// ============================================================================
// Security Helpers
// ============================================================================
/**
* Helper to check if user owns a project
* Uses context.ownedProjects which comes from JWT (verified by cwc-auth)
*/
export type IsProjectOwnerFn = (
context: RequestContext,
projectId: string | undefined
) => boolean;
```
---
## Route Policy Implementation
### `/packages/cwc-api/src/policies/routePolicies.ts`
```typescript
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RoutePolicyConfig } from './policy.types';
/**
* Default route policy configuration
* Default-deny ensures routes without explicit allowedRoles are blocked
*/
export const defaultRoutePolicyConfig: RoutePolicyConfig = {
defaultDeny: true,
ownerProxyRoles: ['logged-on-user'],
};
/**
* Roles that can access public (guest-accessible) routes
*/
export const publicRouteRoles: CwcRole[] = ['guest-user', 'logged-on-user', 'project-owner'];
/**
* Roles that can access authenticated routes
*/
export const authenticatedRouteRoles: CwcRole[] = ['logged-on-user', 'project-owner'];
/**
* Roles for owner-only routes (but logged-on-user can access, ownership checked at operation level)
*/
export const ownerRouteRoles: CwcRole[] = ['project-owner'];
/**
* Helper: Check if a role can access routes marked for owner
* This allows 'logged-on-user' to access 'project-owner' routes,
* with actual ownership verified at operation level
*/
export function canAccessOwnerRoute(role: CwcRole, config: RoutePolicyConfig = defaultRoutePolicyConfig): boolean {
if (role === 'project-owner') {
return true;
}
return config.ownerProxyRoles.includes(role);
}
/**
* Helper: Determine if route requires authentication based on allowedRoles
*/
export function routeRequiresAuthentication(allowedRoles: CwcRole[]): boolean {
return !allowedRoles.includes('guest-user');
}
/**
* Helper: Check if any role in allowedRoles requires ownership
*/
export function routeRequiresOwnership(allowedRoles: CwcRole[]): boolean {
return allowedRoles.includes('project-owner') &&
!allowedRoles.includes('logged-on-user') &&
!allowedRoles.includes('guest-user');
}
```
---
## Check Route Access Implementation
### `/packages/cwc-api/src/policies/checkRouteAccess.ts`
```typescript
'use strict';
import type { RequestContext } from '../context';
import type { CwcApiRouteConfig } from '../handlers/handler.types';
import type { RouteAccessResult, RoutePolicyConfig } from './policy.types';
import { defaultRoutePolicyConfig, canAccessOwnerRoute } from './routePolicies';
/**
* Check if the current context can access this route
*
* Access is granted if:
* 1. context.role is in routeConfig.allowedRoles, OR
* 2. context is authenticated AND routeConfig requires 'project-owner' AND
* context.role is in ownerProxyRoles (ownership checked at operation level)
*
* Security: This is a pure function with no side effects.
* The decision is based solely on:
* - context.role (from verified JWT or guest)
* - routeConfig.allowedRoles (defined in route configuration)
*/
export function checkRouteAccess(
context: RequestContext,
routeConfig: CwcApiRouteConfig,
policyConfig: RoutePolicyConfig = defaultRoutePolicyConfig
): RouteAccessResult {
const { allowedRoles, path } = routeConfig;
// Default-deny: if no roles defined and defaultDeny is true
if (!allowedRoles || allowedRoles.length === 0) {
if (policyConfig.defaultDeny) {
return {
allowed: false,
reason: `Route ${path} has no allowed roles defined (default-deny policy)`,
};
}
// Unlikely path: defaultDeny is false (not recommended)
return { allowed: true };
}
// Direct role match
if (allowedRoles.includes(context.role)) {
return { allowed: true };
}
// For authenticated users: check if route requires 'project-owner'
// and user's role is allowed to proxy for ownership check at operation level
if (
context.isAuthenticated &&
allowedRoles.includes('project-owner') &&
canAccessOwnerRoute(context.role, policyConfig)
) {
return { allowed: true };
}
// Denied
return {
allowed: false,
reason: `Role '${context.role}' cannot access route ${path} (allowed: ${allowedRoles.join(', ')})`,
};
}
```
---
## Operation Policy Implementation
### `/packages/cwc-api/src/policies/operationPolicies.ts`
```typescript
'use strict';
import type { RequestContext } from '../context';
import type { OperationPolicyConfig, OperationPolicyRequirement } from './policy.types';
// ============================================================================
// Pre-defined Operation Policies
// ============================================================================
/**
* Policy: Public operation - anyone can perform
* Example: View public project, list published coding sessions
*/
export const publicOperationPolicy: OperationPolicyConfig = {
requirement: 'public',
};
/**
* Policy: Requires authentication
* Example: Create a comment, add a reaction
*/
export const authenticatedOperationPolicy: OperationPolicyConfig = {
requirement: 'authenticated',
};
/**
* Policy: Requires project ownership
* Example: Update project settings, delete coding session
*/
export const projectOwnerOperationPolicy: OperationPolicyConfig = {
requirement: 'project-owner',
};
/**
* Policy: Requires resource ownership
* Example: Edit own comment, delete own reaction
*/
export const resourceOwnerOperationPolicy: OperationPolicyConfig = {
requirement: 'resource-owner',
};
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Check if user owns the specified project
* Uses ownedProjects from context (populated from verified JWT by cwc-auth)
*
* SECURITY: Never use projectId from request body - only from path params
* Context.ownedProjects is verified by cwc-auth from JWT claims
*/
export function isProjectOwner(context: RequestContext, projectId: string | undefined): boolean {
if (!context.isAuthenticated) {
return false;
}
if (!projectId) {
return false;
}
return context.ownedProjects.includes(projectId);
}
/**
* Check if user is authenticated
*/
export function isAuthenticated(context: RequestContext): boolean {
return context.isAuthenticated;
}
/**
* Check if requirement is satisfied
* This is a preliminary check - resource-owner requires additional validation
*/
export function checkRequirement(
context: RequestContext,
projectId: string | undefined,
requirement: OperationPolicyRequirement
): boolean {
switch (requirement) {
case 'public':
return true;
case 'authenticated':
return isAuthenticated(context);
case 'project-owner':
return isProjectOwner(context, projectId);
case 'resource-owner':
// Resource ownership requires additional context (e.g., who created the comment)
// This is a minimum check - actual resource ownership verified by operation
return isAuthenticated(context);
default:
return false;
}
}
```
---
## Check Operation Access Implementation
### `/packages/cwc-api/src/policies/checkOperationAccess.ts`
```typescript
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext } from '../context';
import type { OperationContext } from '../handlers/handler.types';
import type { OperationAccessResult, OperationPolicyConfig } from './policy.types';
import { isProjectOwner, checkRequirement, publicOperationPolicy } from './operationPolicies';
/**
* Check if the current context can perform this operation
*
* This function determines:
* 1. Is the operation allowed based on the policy requirement?
* 2. If the user is a project owner, what is their effective role?
*
* The effectiveRole field allows downstream code to know if the user
* has elevated privileges for this specific operation.
*
* SECURITY NOTES:
* - projectId comes from operationContext (extracted from URL path params)
* - Never trust projectId from request body for authorization
* - ownedProjects comes from JWT claims verified by cwc-auth
* - This function is async to support future database lookups (e.g., resource ownership)
*/
export async function checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
policy: OperationPolicyConfig = publicOperationPolicy
): Promise<OperationAccessResult> {
const { projectId, resourceId } = operationContext;
const { requirement, customCheck } = policy;
// Step 1: Check if basic requirement is satisfied
const requirementMet = checkRequirement(context, projectId, requirement);
if (!requirementMet) {
const reason = buildDenialReason(context, requirement, projectId);
return {
allowed: false,
reason,
};
}
// Step 2: Run custom check if provided
if (customCheck && !customCheck(context, operationContext)) {
return {
allowed: false,
reason: 'Custom policy check failed',
};
}
// Step 3: Determine effective role
// If user owns the project, their effective role is 'project-owner'
// This allows operations to know they have elevated privileges
let effectiveRole: CwcRole | undefined = undefined;
if (context.isAuthenticated) {
if (projectId && isProjectOwner(context, projectId)) {
effectiveRole = 'project-owner';
} else {
effectiveRole = context.role;
}
} else {
effectiveRole = 'guest-user';
}
return {
allowed: true,
effectiveRole,
};
}
/**
* Build a human-readable denial reason for debugging
* Only shown in development mode via errorDetail
*/
function buildDenialReason(
context: RequestContext,
requirement: string,
projectId: string | undefined
): string {
const role = context.role;
switch (requirement) {
case 'authenticated':
return `Operation requires authentication (current role: ${role})`;
case 'project-owner':
if (!context.isAuthenticated) {
return `Operation requires project ownership (current role: ${role})`;
}
return `User does not own project '${projectId}' (owned: ${context.ownedProjects.join(', ') || 'none'})`;
case 'resource-owner':
return `Operation requires resource ownership (current role: ${role})`;
default:
return `Policy requirement '${requirement}' not satisfied`;
}
}
```
---
## Handler Integration
### Changes to `QueryHandler.ts`
```typescript
// Current (Phase 3):
private async checkOperationAccess(): Promise<OperationAccessResult> {
return { allowed: true };
}
// Phase 4 (replace with):
import { checkOperationAccess } from '../policies';
import type { OperationPolicyConfig } from '../policies/policy.types';
private async checkOperationAccess(): Promise<OperationAccessResult> {
const { context, operationContext, routeConfig } = this.options;
// Get policy from route config (added in Phase 4)
// Falls back to public policy if not specified
const policy: OperationPolicyConfig | undefined = (routeConfig as any).operationPolicy;
return checkOperationAccess(context, operationContext, policy);
}
```
### Changes to `MutationHandler.ts`
Same pattern as QueryHandler - import `checkOperationAccess` and call it with the operation policy from route config.
### Changes to `handler.types.ts` (CwcApiRouteConfig)
```typescript
// Add operationPolicy to route config
export type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {
path: string;
handlerType: CwcApiHandlerType;
allowedRoles: CwcRole[];
operation: CwcApiOperation<TPayload, TResult>;
// Phase 4 addition:
operationPolicy?: OperationPolicyConfig | undefined;
debug?: boolean | undefined;
};
```
---
## Index Exports
### `/packages/cwc-api/src/policies/index.ts`
```typescript
'use strict';
// Policy types
export type {
RouteAccessResult,
OperationAccessResult,
RoutePolicyConfig,
OperationPolicyConfig,
OperationPolicyRequirement,
CheckRouteAccessFn,
CheckOperationAccessFn,
IsProjectOwnerFn,
} from './policy.types';
// Route policies
export {
defaultRoutePolicyConfig,
publicRouteRoles,
authenticatedRouteRoles,
ownerRouteRoles,
canAccessOwnerRoute,
routeRequiresAuthentication,
routeRequiresOwnership,
} from './routePolicies';
// Operation policies
export {
publicOperationPolicy,
authenticatedOperationPolicy,
projectOwnerOperationPolicy,
resourceOwnerOperationPolicy,
isProjectOwner,
isAuthenticated,
checkRequirement,
} from './operationPolicies';
// Policy enforcement functions
export { checkRouteAccess } from './checkRouteAccess';
export { checkOperationAccess } from './checkOperationAccess';
```
---
## Test Scenarios
### Route Policy Tests (`src/__tests__/policies/checkRouteAccess.test.ts`)
```typescript
describe('checkRouteAccess', () => {
describe('Direct Role Match', () => {
it('should allow guest to access guest-allowed routes');
it('should allow logged-on-user to access authenticated routes');
it('should allow project-owner to access owner routes');
});
describe('Owner Proxy Access', () => {
it('should allow logged-on-user to access project-owner routes (ownership at operation level)');
it('should deny guest to access project-owner routes');
});
describe('Default Deny Policy', () => {
it('should deny access when allowedRoles is empty');
it('should deny access when allowedRoles is undefined');
});
describe('Access Denied Responses', () => {
it('should return reason with role and path for debugging');
});
});
```
### Operation Policy Tests (`src/__tests__/policies/checkOperationAccess.test.ts`)
```typescript
describe('checkOperationAccess', () => {
describe('Public Operations', () => {
it('should allow guest to access public operations');
it('should allow authenticated user to access public operations');
it('should return effectiveRole as guest-user for guests');
});
describe('Authenticated Operations', () => {
it('should allow authenticated user');
it('should deny guest user');
it('should return effectiveRole as logged-on-user');
});
describe('Project Owner Operations', () => {
it('should allow user who owns the project');
it('should deny user who does not own the project');
it('should deny guest user');
it('should return effectiveRole as project-owner when user owns project');
it('should use projectId from operationContext, not request body');
});
describe('Ownership Verification', () => {
it('should check ownership using context.ownedProjects');
it('should handle missing projectId');
it('should handle empty ownedProjects array');
});
describe('Custom Policy Checks', () => {
it('should call customCheck function when provided');
it('should deny if customCheck returns false');
it('should allow if customCheck returns true');
});
describe('Effective Role Determination', () => {
it('should return project-owner when user owns the project');
it('should return logged-on-user when authenticated but not owner');
it('should return guest-user when not authenticated');
});
});
```
### Operation Policies Tests (`src/__tests__/policies/operationPolicies.test.ts`)
```typescript
describe('operationPolicies', () => {
describe('isProjectOwner', () => {
it('should return true when projectId is in ownedProjects');
it('should return false when projectId is not in ownedProjects');
it('should return false for guest context');
it('should return false for undefined projectId');
it('should return false for empty ownedProjects');
});
describe('isAuthenticated', () => {
it('should return true for authenticated context');
it('should return false for guest context');
});
describe('checkRequirement', () => {
it('should always return true for public requirement');
it('should return true for authenticated requirement when authenticated');
it('should return true for project-owner requirement when owner');
it('should return true for resource-owner requirement when authenticated');
});
});
```
### Route Policies Tests (`src/__tests__/policies/routePolicies.test.ts`)
```typescript
describe('routePolicies', () => {
describe('canAccessOwnerRoute', () => {
it('should return true for project-owner role');
it('should return true for logged-on-user with default config');
it('should return false for guest-user');
});
describe('routeRequiresAuthentication', () => {
it('should return true when guest-user is not in allowedRoles');
it('should return false when guest-user is in allowedRoles');
});
describe('routeRequiresOwnership', () => {
it('should return true when only project-owner is allowed');
it('should return false when logged-on-user is also allowed');
});
});
```
---
## Security Considerations
### 1. Default-Deny Policy
- Routes without `allowedRoles` are blocked by default
- `defaultRoutePolicyConfig.defaultDeny = true`
### 2. Ownership from JWT Only
- `context.ownedProjects` comes from JWT claims verified by cwc-auth
- Never read ownership from request body
- `projectId` comes from URL path parameters, not body
### 3. No Privilege Escalation
- Guest cannot become logged-on-user
- Logged-on-user cannot become project-owner unless they actually own the project
- `effectiveRole` reflects actual access level, not claimed level
### 4. All Mutations Require Authentication
- `CwcApiRouteConfig.allowedRoles` for mutations should never include `guest-user`
- This is enforced by convention (Phase 6 route definitions)
### 5. Policy Bypass Prevention
- `checkRouteAccess` and `checkOperationAccess` are pure functions
- No parameter manipulation can bypass checks
- Path parameters are controlled by Express routing, not user input
---
## Implementation Sequence
1. **Create `src/policies/policy.types.ts`**
- Define all type definitions
- Re-export existing types from handler.types.ts
2. **Create `src/policies/routePolicies.ts`**
- Define policy configuration
- Create helper functions
3. **Create `src/policies/checkRouteAccess.ts`**
- Implement route access check function
- This replaces the inline logic in RequestHandler
4. **Create `src/policies/operationPolicies.ts`**
- Define pre-built policies
- Create ownership check helpers
5. **Create `src/policies/checkOperationAccess.ts`**
- Implement operation access check function
- This replaces the stub in Query/MutationHandler
6. **Create `src/policies/index.ts`**
- Export all types and functions
7. **Update `src/handlers/handler.types.ts`**
- Add `operationPolicy?: OperationPolicyConfig` to CwcApiRouteConfig
8. **Update `src/handlers/RequestHandler.ts`**
- Import and use `checkRouteAccess` from policies
- Replace inline `checkRouteAccess()` method
9. **Update `src/handlers/QueryHandler.ts`**
- Import and use `checkOperationAccess` from policies
- Replace stub `checkOperationAccess()` method
10. **Update `src/handlers/MutationHandler.ts`**
- Same changes as QueryHandler
11. **Create test mocks (`src/__tests__/mocks/policy.mock.ts`)**
- Mock helpers for policy testing
12. **Create unit tests**
- `checkRouteAccess.test.ts`
- `checkOperationAccess.test.ts`
- `routePolicies.test.ts`
- `operationPolicies.test.ts`
13. **Update handler tests**
- Verify integration with new policy system
---
### Critical Files for Implementation
- `/packages/cwc-api/src/handlers/handler.types.ts` - Add `operationPolicy` to `CwcApiRouteConfig`; existing access result types to re-export
- `/packages/cwc-api/src/handlers/RequestHandler.ts` - Replace inline `checkRouteAccess()` with imported function
- `/packages/cwc-api/src/handlers/QueryHandler.ts` - Replace stub `checkOperationAccess()` with imported function
- `/packages/cwc-api/src/context/context.types.ts` - Reference for `RequestContext` types (AuthenticatedContext.ownedProjects)
- `/packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts` - Pattern to follow for policy mocks; update to support operationPolicy
agentId: a7965b3 (for resuming to continue this agent's work if needed)Let me read the critical files to verify the plan aligns with the existing code structure.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/handler.types.ts"
}Tool Result
1→'use strict';
2→
3→import type { CwcRole } from 'cwc-types';
4→import type { RequestContext } from '../context';
5→
6→// ============================================================================
7→// Error Codes
8→// ============================================================================
9→
10→/**
11→ * Error codes for cwc-api operations
12→ */
13→export type CwcApiErrorCode =
14→ // Access control errors
15→ | 'UNAUTHORIZED' // No valid authentication
16→ | 'FORBIDDEN' // Authenticated but not allowed
17→ | 'ROUTE_ACCESS_DENIED' // Role cannot access this route
18→ | 'OPERATION_ACCESS_DENIED' // Role cannot perform this operation
19→ // Resource errors
20→ | 'NOT_FOUND' // Resource does not exist
21→ | 'ALREADY_EXISTS' // Duplicate resource
22→ // Validation errors
23→ | 'VALIDATION_ERROR' // Request payload validation failed
24→ | 'INVALID_PARAMETER' // Invalid query/path parameter
25→ // System errors
26→ | 'INTERNAL_ERROR' // Unexpected server error
27→ | 'DATABASE_ERROR' // Database operation failed
28→ | 'AUTH_SERVICE_ERROR'; // cwc-auth communication failed
29→
30→// ============================================================================
31→// HTTP Status Codes
32→// ============================================================================
33→
34→export type CwcApiSuccessStatusCode = 200;
35→export type CwcApiErrorStatusCode = 400 | 401 | 403 | 404 | 500;
36→export type CwcApiStatusCode = CwcApiSuccessStatusCode | CwcApiErrorStatusCode;
37→
38→// ============================================================================
39→// Response Types
40→// ============================================================================
41→
42→/**
43→ * Pagination metadata for list responses
44→ */
45→export type CwcApiPagination = {
46→ page: number;
47→ pageSize: number;
48→ totalCount: number;
49→ hasMore: boolean;
50→};
51→
52→/**
53→ * Success response envelope
54→ */
55→export type CwcApiSuccessResponse<T = unknown> = {
56→ success: true;
57→ data: T;
58→ pagination?: CwcApiPagination | undefined;
59→ jwt?: string | undefined; // New JWT if session was renewed
60→};
61→
62→/**
63→ * Error response envelope
64→ */
65→export type CwcApiErrorResponse = {
66→ success: false;
67→ errorCode: CwcApiErrorCode;
68→ errorMessage: string;
69→ errorDetail?: string | undefined; // Dev-only
70→};
71→
72→/**
73→ * Union of all response types
74→ */
75→export type CwcApiResponse<T = unknown> = CwcApiSuccessResponse<T> | CwcApiErrorResponse;
76→
77→/**
78→ * Full handler response with HTTP status code
79→ */
80→export type CwcApiHandlerResponse<T = unknown> = {
81→ statusCode: CwcApiStatusCode;
82→ body: CwcApiResponse<T>;
83→};
84→
85→// ============================================================================
86→// Route Configuration
87→// ============================================================================
88→
89→/**
90→ * Handler type: query (read-only) or mutation (write)
91→ */
92→export type CwcApiHandlerType = 'query' | 'mutation';
93→
94→/**
95→ * Route configuration for cwc-api endpoints
96→ */
97→export type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {
98→ // Route identification
99→ path: string;
100→ handlerType: CwcApiHandlerType;
101→
102→ // Access control
103→ allowedRoles: CwcRole[];
104→
105→ // Operation (injected worker function)
106→ operation: CwcApiOperation<TPayload, TResult>;
107→
108→ // Debug flag
109→ debug?: boolean | undefined;
110→};
111→
112→/**
113→ * Map of route paths to configurations
114→ */
115→export type CwcApiRouteConfigs = {
116→ [key: string]: CwcApiRouteConfig;
117→};
118→
119→// ============================================================================
120→// Operation Types
121→// ============================================================================
122→
123→/**
124→ * Operation context passed to worker functions
125→ */
126→export type OperationContext = {
127→ context: RequestContext;
128→ projectId?: string | undefined; // For project-scoped operations
129→ resourceId?: string | undefined; // For resource-specific operations
130→};
131→
132→/**
133→ * Operation result from worker functions
134→ */
135→export type OperationResult<T = unknown> =
136→ | { success: true; data: T; pagination?: CwcApiPagination | undefined }
137→ | { success: false; errorCode: CwcApiErrorCode; errorMessage: string };
138→
139→/**
140→ * Operation function signature (worker)
141→ */
142→export type CwcApiOperation<TPayload = unknown, TResult = unknown> = (
143→ payload: TPayload,
144→ operationContext: OperationContext
145→) => Promise<OperationResult<TResult>>;
146→
147→// ============================================================================
148→// Handler Options
149→// ============================================================================
150→
151→/**
152→ * Base options for all handlers
153→ */
154→export type BaseHandlerOptions = {
155→ context: RequestContext;
156→ routeConfig: CwcApiRouteConfig;
157→ authHeader: string | undefined;
158→};
159→
160→/**
161→ * Options for RequestHandler
162→ */
163→export type RequestHandlerOptions = BaseHandlerOptions & {
164→ payload: unknown;
165→ pathParams?: Record<string, string> | undefined;
166→};
167→
168→/**
169→ * Options for QueryHandler
170→ */
171→export type QueryHandlerOptions = BaseHandlerOptions & {
172→ payload: unknown;
173→ operationContext: OperationContext;
174→};
175→
176→/**
177→ * Options for MutationHandler
178→ */
179→export type MutationHandlerOptions = QueryHandlerOptions;
180→
181→// ============================================================================
182→// Handler Interface
183→// ============================================================================
184→
185→/**
186→ * Interface for all handlers
187→ */
188→export interface CwcApiHandler {
189→ processRequest(): Promise<CwcApiHandlerResponse>;
190→}
191→
192→// ============================================================================
193→// Access Policy Types (Stubs for Phase 4)
194→// ============================================================================
195→
196→/**
197→ * Route access check result
198→ */
199→export type RouteAccessResult = {
200→ allowed: boolean;
201→ reason?: string | undefined;
202→};
203→
204→/**
205→ * Operation access check result
206→ */
207→export type OperationAccessResult = {
208→ allowed: boolean;
209→ effectiveRole?: CwcRole | undefined; // Role for this specific operation
210→ reason?: string | undefined;
211→};
212→
213→/**
214→ * Route access policy checker interface (Phase 4)
215→ */
216→export interface RouteAccessPolicy {
217→ checkRouteAccess(context: RequestContext, routeConfig: CwcApiRouteConfig): RouteAccessResult;
218→}
219→
220→/**
221→ * Operation access policy checker interface (Phase 4)
222→ */
223→export interface OperationAccessPolicy {
224→ checkOperationAccess(
225→ context: RequestContext,
226→ operationContext: OperationContext
227→ ): Promise<OperationAccessResult>;
228→}
229→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts"
}Tool Result
1→'use strict';
2→
3→import type { ILogger, AuthClient } from 'cwc-backend-utils';
4→import type { CwcApiConfig } from '../config';
5→import type {
6→ CwcApiHandler,
7→ CwcApiHandlerResponse,
8→ CwcApiSuccessResponse,
9→ RequestHandlerOptions,
10→ RouteAccessResult,
11→ OperationContext,
12→} from './handler.types';
13→import { QueryHandler } from './QueryHandler';
14→import { MutationHandler } from './MutationHandler';
15→import { createInternalErrorResponse } from './responseUtils';
16→
17→const codeLocation = 'handlers/RequestHandler.ts';
18→
19→/**
20→ * RequestHandler - Entry point for processing API requests
21→ *
22→ * Responsibilities:
23→ * 1. Check route-level access based on context role
24→ * 2. Build operation context with path params
25→ * 3. Delegate to QueryHandler or MutationHandler based on handlerType
26→ * 4. Renew session for authenticated users (except on auth errors)
27→ */
28→export class RequestHandler implements CwcApiHandler {
29→ private options: RequestHandlerOptions;
30→ private config: CwcApiConfig;
31→ private authClient: AuthClient;
32→ private logger: ILogger | undefined;
33→
34→ constructor(
35→ options: RequestHandlerOptions,
36→ config: CwcApiConfig,
37→ authClient: AuthClient,
38→ logger: ILogger | undefined
39→ ) {
40→ this.options = options;
41→ this.config = config;
42→ this.authClient = authClient;
43→ this.logger = logger;
44→ }
45→
46→ public async processRequest(): Promise<CwcApiHandlerResponse> {
47→ const { context, routeConfig, payload, authHeader, pathParams } = this.options;
48→
49→ try {
50→ // Step 1: Check route-level access
51→ const routeAccess = this.checkRouteAccess();
52→ if (!routeAccess.allowed) {
53→ // No session renewal for auth errors
54→ return this.createAccessDeniedResponse(routeAccess.reason);
55→ }
56→
57→ // Step 2: Build operation context
58→ const operationContext: OperationContext = {
59→ context,
60→ projectId: pathParams?.['projectId'],
61→ resourceId: pathParams?.['resourceId'],
62→ };
63→
64→ // Step 3: Delegate to appropriate handler
65→ let response: CwcApiHandlerResponse;
66→ if (routeConfig.handlerType === 'query') {
67→ const queryHandler = new QueryHandler(
68→ {
69→ context,
70→ routeConfig,
71→ authHeader,
72→ payload,
73→ operationContext,
74→ },
75→ this.config,
76→ this.logger
77→ );
78→ response = await queryHandler.processRequest();
79→ } else {
80→ const mutationHandler = new MutationHandler(
81→ {
82→ context,
83→ routeConfig,
84→ authHeader,
85→ payload,
86→ operationContext,
87→ },
88→ this.config,
89→ this.logger
90→ );
91→ response = await mutationHandler.processRequest();
92→ }
93→
94→ // Step 4: Renew session for authenticated users (except on auth errors)
95→ const isAuthError = response.statusCode === 401 || response.statusCode === 403;
96→ if (context.isAuthenticated && !isAuthError) {
97→ const renewResult = await this.authClient.renewSession(authHeader);
98→ if (renewResult.success && response.body.success) {
99→ // Add JWT to successful response
100→ (response.body as CwcApiSuccessResponse).jwt = renewResult.jwt;
101→ } else if (!renewResult.success) {
102→ // Log warning but don't fail the operation
103→ this.logger?.logError({
104→ userPkId: context.userPkId,
105→ codeLocation,
106→ message: `Session renewal failed for ${routeConfig.path}`,
107→ error: renewResult.error,
108→ });
109→ }
110→ }
111→
112→ return response;
113→ } catch (error) {
114→ this.logger?.logError({
115→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
116→ codeLocation,
117→ message: `Error processing request ${routeConfig.path}`,
118→ error,
119→ });
120→
121→ return createInternalErrorResponse(this.config, error);
122→ }
123→ }
124→
125→ /**
126→ * Check if the current context role can access this route
127→ * Compares context.role against routeConfig.allowedRoles
128→ */
129→ private checkRouteAccess(): RouteAccessResult {
130→ const { context, routeConfig } = this.options;
131→ const { allowedRoles } = routeConfig;
132→
133→ // Check if the current role is in allowedRoles
134→ if (allowedRoles.includes(context.role)) {
135→ return { allowed: true };
136→ }
137→
138→ // For authenticated users with 'project-owner' role requirement,
139→ // 'logged-on-user' should also be allowed (ownership check happens at operation level)
140→ if (
141→ context.isAuthenticated &&
142→ context.role === 'logged-on-user' &&
143→ allowedRoles.includes('project-owner')
144→ ) {
145→ return { allowed: true };
146→ }
147→
148→ return {
149→ allowed: false,
150→ reason: `Role '${context.role}' cannot access route ${routeConfig.path}`,
151→ };
152→ }
153→
154→ private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {
155→ const { context } = this.options;
156→
157→ // Use 401 for unauthenticated, 403 for authenticated but not allowed
158→ const statusCode = context.isAuthenticated ? 403 : 401;
159→ const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';
160→
161→ return {
162→ statusCode,
163→ body: {
164→ success: false,
165→ errorCode,
166→ errorMessage: 'Access denied',
167→ ...(this.config.isDev && reason ? { errorDetail: reason } : {}),
168→ },
169→ };
170→ }
171→}
172→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts"
}Tool Result
1→'use strict';
2→
3→import type { ILogger } from 'cwc-backend-utils';
4→import type { CwcApiConfig } from '../config';
5→import type {
6→ CwcApiHandler,
7→ CwcApiHandlerResponse,
8→ QueryHandlerOptions,
9→ OperationAccessResult,
10→} from './handler.types';
11→import {
12→ createOperationDeniedResponse,
13→ createOperationErrorResponse,
14→ createInternalErrorResponse,
15→} from './responseUtils';
16→
17→const codeLocation = 'handlers/QueryHandler.ts';
18→
19→/**
20→ * QueryHandler - Handles read-only operations
21→ *
22→ * Responsibilities:
23→ * 1. Check operation-level access (Phase 4 stub)
24→ * 2. Execute the query operation
25→ * 3. Format successful response with data/pagination
26→ *
27→ * Note: Session renewal is handled by RequestHandler
28→ */
29→export class QueryHandler implements CwcApiHandler {
30→ private options: QueryHandlerOptions;
31→ private config: CwcApiConfig;
32→ private logger: ILogger | undefined;
33→
34→ constructor(
35→ options: QueryHandlerOptions,
36→ config: CwcApiConfig,
37→ logger: ILogger | undefined
38→ ) {
39→ this.options = options;
40→ this.config = config;
41→ this.logger = logger;
42→ }
43→
44→ public async processRequest(): Promise<CwcApiHandlerResponse> {
45→ const { context, routeConfig, payload, operationContext } = this.options;
46→
47→ try {
48→ // Step 1: Check operation-level access (Phase 4 implementation)
49→ const operationAccess = await this.checkOperationAccess();
50→ if (!operationAccess.allowed) {
51→ return createOperationDeniedResponse(this.config, operationAccess.reason);
52→ }
53→
54→ // Step 2: Execute the operation
55→ const result = await routeConfig.operation(payload, operationContext);
56→
57→ if (!result.success) {
58→ return createOperationErrorResponse(result.errorCode, result.errorMessage);
59→ }
60→
61→ // Step 3: Format response (JWT added by RequestHandler)
62→ return {
63→ statusCode: 200,
64→ body: {
65→ success: true,
66→ data: result.data,
67→ ...(result.pagination ? { pagination: result.pagination } : {}),
68→ },
69→ };
70→ } catch (error) {
71→ this.logger?.logError({
72→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
73→ codeLocation,
74→ message: `Error executing query operation for ${routeConfig.path}`,
75→ error,
76→ });
77→
78→ return createInternalErrorResponse(this.config, error);
79→ }
80→ }
81→
82→ /**
83→ * Check if the current context can perform this operation
84→ * Phase 3: Stub - always returns allowed
85→ * Phase 4: Will check ownership, resource access, etc.
86→ */
87→ private async checkOperationAccess(): Promise<OperationAccessResult> {
88→ // Phase 4 will implement actual access policy checks
89→ // For now, route-level access is sufficient
90→ return { allowed: true };
91→ }
92→}
93→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Let me also check the context types to understand what data we have available.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/context/context.types.ts"
}Tool Result
1→import type { UserJwtPayload, CwcRole } from 'cwc-types';
2→
3→/**
4→ * Context for authenticated users
5→ */
6→export type AuthenticatedContext = {
7→ isAuthenticated: true;
8→ role: Exclude<CwcRole, 'guest-user'>;
9→ userPkId: number;
10→ username: string;
11→ ownedProjects: string[];
12→ payload: UserJwtPayload;
13→};
14→
15→/**
16→ * Context for unauthenticated/guest users
17→ */
18→export type GuestContext = {
19→ isAuthenticated: false;
20→ role: 'guest-user';
21→};
22→
23→/**
24→ * Union of all context types
25→ */
26→export type RequestContext = AuthenticatedContext | GuestContext;
27→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
I have a clear picture of the current state and the design. Let me write the plan file.
Phase 4: Access Policy System - Implementation Plan
Summary
Implement the access policy system for cwc-api with two levels of access control:
- Route-level: Can this role access this endpoint? (extract existing logic)
- Operation-level: Can this user perform this operation on this resource? (replace stub)
Current State (Phase 3)
- Route access: Working inline in
RequestHandler.checkRouteAccess()(lines 129-152) - Operation access: Stub in
QueryHandler.checkOperationAccess()andMutationHandler.checkOperationAccess()that always returns{ allowed: true } - Types defined:
RouteAccessResult,OperationAccessResultinhandler.types.ts - Context data:
AuthenticatedContext.ownedProjects[]available for ownership checks
Design Decisions
- Keep
allowedRoleson route configs - Routes define their allowed roles inline (Phase 6 routes.ts) - Add
operationPolicyto route config - Optional field for operation-level requirements - Project ownership only - Use
context.ownedProjects.includes(projectId)for Phase 4; resource ownership (database lookups) deferred to Phase 6 - Default-deny policy - Routes without
allowedRolesare blocked
Files to Create
1. src/policies/policy.types.ts
RoutePolicyConfig- Default behavior settingsOperationPolicyRequirement-'public' | 'authenticated' | 'project-owner'OperationPolicyConfig- Requirement + optional custom check- Re-export
RouteAccessResult,OperationAccessResultfrom handler.types.ts
2. src/policies/routePolicies.ts
defaultRoutePolicyConfig-{ defaultDeny: true, ownerProxyRoles: ['logged-on-user'] }publicRouteRoles,authenticatedRouteRoles,ownerRouteRoles- Role arrayscanAccessOwnerRoute()- Check if role can proxy for ownershiprouteRequiresAuthentication()- Helper for route analysis
3. src/policies/checkRouteAccess.ts
checkRouteAccess(context, routeConfig, policyConfig?)- Extract from RequestHandler- Same logic: direct role match OR authenticated + owner proxy
- Default-deny when allowedRoles empty
4. src/policies/operationPolicies.ts
publicOperationPolicy,authenticatedOperationPolicy,projectOwnerOperationPolicyisProjectOwner(context, projectId)- Check ownershipisAuthenticated(context)- Check auth statuscheckRequirement(context, projectId, requirement)- Validate requirement met
5. src/policies/checkOperationAccess.ts
checkOperationAccess(context, operationContext, policy?)- Replace stub- Check requirement (public/authenticated/project-owner)
- Run custom check if provided
- Determine
effectiveRole(project-owner if owns, else context.role)
6. src/policies/index.ts
- Export all types and functions
Files to Modify
7. src/handlers/handler.types.ts
- Add
operationPolicy?: OperationPolicyConfigtoCwcApiRouteConfig
8. src/handlers/RequestHandler.ts
- Import
checkRouteAccessfrom../policies - Replace inline
checkRouteAccess()method with imported function call
9. src/handlers/QueryHandler.ts
- Import
checkOperationAccessfrom../policies - Update
checkOperationAccess()to call imported function withrouteConfig.operationPolicy
10. src/handlers/MutationHandler.ts
- Same changes as QueryHandler
Test Files to Create
11. src/__tests__/mocks/policy.mock.ts
createMockOperationPolicy()- Create policy configs for tests- Test helpers for policy scenarios
12. src/__tests__/policies/checkRouteAccess.test.ts
- Direct role match tests (guest, logged-on-user, project-owner)
- Owner proxy access (logged-on-user accessing project-owner routes)
- Default-deny when no allowedRoles
- Access denied reason formatting
13. src/__tests__/policies/checkOperationAccess.test.ts
- Public operation access (all roles)
- Authenticated operation access (deny guest)
- Project-owner operation access (ownership check)
- effectiveRole determination
- Custom check function execution
14. src/__tests__/policies/routePolicies.test.ts
canAccessOwnerRoute()testsrouteRequiresAuthentication()tests- Helper function coverage
15. src/__tests__/policies/operationPolicies.test.ts
isProjectOwner()tests (owner, non-owner, guest, missing projectId)isAuthenticated()testscheckRequirement()for each requirement type
Handler Test Updates
16. Update existing handler tests
- Verify handlers call policy functions correctly
- Test with different
operationPolicyvalues - Ensure backward compatibility (no policy = public behavior)
Implementation Sequence
1. policy.types.ts (types only)
2. routePolicies.ts (route helpers)
3. checkRouteAccess.ts (extract from RequestHandler)
4. operationPolicies.ts (operation helpers)
5. checkOperationAccess.ts (replace stub)
6. index.ts (exports)
7. handler.types.ts (add operationPolicy)
8. RequestHandler.ts (use checkRouteAccess)
9. QueryHandler.ts (use checkOperationAccess)
10. MutationHandler.ts (use checkOperationAccess)
11. policy.mock.ts (test helpers)
12-15. Policy unit tests
16. Handler test updates
17. Typecheck all packages
18. Run tests
19. Security review
20. Update CLAUDE.md
Security Checklist
- Default-deny policy (empty allowedRoles = blocked)
- Ownership from JWT only (
context.ownedProjects, not request body) -
projectIdfrom path params (operationContext.projectId), not payload - No privilege escalation (guest cannot become logged-on-user)
- All mutations require authentication (Phase 6 enforcement)
- Generic error messages in production (
errorDetailonly in dev)
Key Code Patterns
checkRouteAccess
export function checkRouteAccess(
context: RequestContext,
routeConfig: CwcApiRouteConfig,
policyConfig: RoutePolicyConfig = defaultRoutePolicyConfig
): RouteAccessResult
checkOperationAccess
export async function checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
policy: OperationPolicyConfig = publicOperationPolicy
): Promise<OperationAccessResult>
isProjectOwner
export function isProjectOwner(
context: RequestContext,
projectId: string | undefined
): boolean {
if (!context.isAuthenticated) return false;
if (!projectId) return false;
return context.ownedProjects.includes(projectId);
}
Critical Files Reference
packages/cwc-api/src/handlers/handler.types.ts- Add operationPolicypackages/cwc-api/src/handlers/RequestHandler.ts:129-152- Extract checkRouteAccesspackages/cwc-api/src/handlers/QueryHandler.ts:87-91- Replace stubpackages/cwc-api/src/handlers/MutationHandler.ts- Replace stub (same pattern)packages/cwc-api/src/context/context.types.ts- AuthenticatedContext.ownedProjectspackages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts- Pattern for policy mocks
Tool Result
File created successfully at: /Users/jeffbazinet/.claude/plans/jaunty-jingling-naur.md
I've created the implementation plan. Let me summarize the key points and address your question about prerequisites.
Prerequisites Check
No blocking prerequisites found. Phase 3 completed everything needed:
RouteAccessResultandOperationAccessResulttypes already defined inhandler.types.tsOperationContextincludesprojectIdandresourceIdfrom path paramsAuthenticatedContext.ownedProjectsarray available for ownership checks- Handler stubs ready to be replaced
Plan Summary
6 new files to create in src/policies/:
policy.types.ts- Policy type definitionsroutePolicies.ts- Route access helperscheckRouteAccess.ts- Extract logic from RequestHandleroperationPolicies.ts- Operation access helpers (isProjectOwner, etc.)checkOperationAccess.ts- Replace handler stubsindex.ts- Exports
4 files to modify:
handler.types.ts- AddoperationPolicy?toCwcApiRouteConfigRequestHandler.ts- Use importedcheckRouteAccessQueryHandler.ts- Use importedcheckOperationAccessMutationHandler.ts- Same as QueryHandler
5 test files to create:
- Policy mocks and 4 test suites for the policy modules
Key Design Decision
For Phase 4, I'm recommending project ownership only (context.ownedProjects.includes(projectId)). Resource-level ownership (e.g., "did this user create this comment?") would require database lookups and is better suited for Phase 6 when specific operations need it.
Does this scope seem right, or would you like resource-owner checks included in Phase 4?
Questions on the plan: 1. your plan for `src/policies/policy.types.ts` includes `OperationPolicyRequirement - 'public' | 'authenticated' | 'project-owner'` do we need to introduce these additional values when we already have these roles: `project-owner`, `logged-on-user`, and `guest-user`? 2. I don't really follow the `routePolicies` and `operationPolicies` design. I am imagining a simpler implementation where in `src/policies/checkOperationAccess.ts`, the `checkOperationAccess(context, operationContext, policy?)` takes a `requiredRole` parameter instead of the `policy` parameter. Let's talk about this and determine which approach will be easier to understand in the code as well as easier to configure and maintain.
Great questions! Let me address both:
1. OperationPolicyRequirement vs Existing Roles
You're right - we don't need a separate enum. We can use the existing CwcRole type with a role hierarchy interpretation:
guest-user < logged-on-user < project-owner
Where requiredRole means "minimum role needed":
requiredRole: 'guest-user'= anyone can access (public)requiredRole: 'logged-on-user'= must be authenticatedrequiredRole: 'project-owner'= must own the project
This eliminates the need for OperationPolicyRequirement entirely.
2. Simpler checkOperationAccess Design
Your suggestion is cleaner. Instead of:
// Original complex design
checkOperationAccess(context, operationContext, policy?: OperationPolicyConfig)
// where OperationPolicyConfig = { requirement, customCheck? }
We could have:
// Simpler design
checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole?: CwcRole // defaults to 'guest-user' (public)
): Promise<OperationAccessResult>
The logic:
- If
requiredRoleis'guest-user'(or undefined) - always allowed - If
requiredRoleis'logged-on-user'- must be authenticated (context.isAuthenticated) - If
requiredRoleis'project-owner'- must own the project (context.ownedProjects.includes(projectId))
This approach:
- Uses existing types (no new enums)
- Is easy to understand at call sites:
checkOperationAccess(context, opContext, 'project-owner') - Route configs just add
requiredRole?: CwcRoleinstead ofoperationPolicy?: OperationPolicyConfig - We can add complexity later (custom checks) if Phase 6 operations need it
The simplified file structure would be:
| File | Purpose |
|---|---|
policy.types.ts |
Minimal types (maybe not even needed?) |
routePolicies.ts |
Route-level helpers (canAccessOwnerRoute, etc.) |
checkRouteAccess.ts |
Extract from RequestHandler |
checkOperationAccess.ts |
New implementation with requiredRole |
index.ts |
Exports |
We might not even need separate operationPolicies.ts - the isProjectOwner helper can live in checkOperationAccess.ts.
Does this simpler approach align with what you're envisioning?
Thank you for being so accommodating! Yes, this is how I envisioned it working, with the role hierarchy that you described. Please make requiredRole required (not optional).
Let me update the plan to reflect the simpler design with requiredRole as a required parameter.
Phase 4: Access Policy System - Implementation Plan
Summary
Implement the access policy system for cwc-api with two levels of access control:
- Route-level: Can this role access this endpoint? (extract existing logic)
- Operation-level: Can this user perform this operation on this resource? (replace stub)
Current State (Phase 3)
- Route access: Working inline in
RequestHandler.checkRouteAccess()(lines 129-152) - Operation access: Stub in
QueryHandler.checkOperationAccess()andMutationHandler.checkOperationAccess()that always returns{ allowed: true } - Types defined:
RouteAccessResult,OperationAccessResultinhandler.types.ts - Context data:
AuthenticatedContext.ownedProjects[]available for ownership checks
Design Decisions
- Keep
allowedRoleson route configs - Routes define their allowed roles inline - Add
requiredRole: CwcRoleto route config - Required field for operation-level checks - Use existing CwcRole type - No new enums; use role hierarchy interpretation
- Role hierarchy:
guest-user < logged-on-user < project-owner - Project ownership only - Use
context.ownedProjects.includes(projectId)for Phase 4 - Default-deny policy - Routes without
allowedRolesare blocked
Role Hierarchy
| requiredRole | Meaning | Who Can Access |
|---|---|---|
guest-user |
Public access | Everyone (guest, logged-on, owner) |
logged-on-user |
Authenticated | Authenticated users only |
project-owner |
Owner access | Must own the project |
Files to Create
1. src/policies/policy.types.ts
- Re-export
RouteAccessResult,OperationAccessResultfrom handler.types.ts RoutePolicyConfigtype (for default-deny behavior)
2. src/policies/routePolicies.ts
defaultRoutePolicyConfig-{ defaultDeny: true, ownerProxyRoles: ['logged-on-user'] }canAccessOwnerRoute(role, config)- Check if role can proxy for ownership
3. src/policies/checkRouteAccess.ts
checkRouteAccess(context, routeConfig, policyConfig?)- Extract from RequestHandler- Same logic: direct role match OR authenticated + owner proxy
- Default-deny when allowedRoles empty
4. src/policies/checkOperationAccess.ts
checkOperationAccess(context, operationContext, requiredRole)- Replace stubisProjectOwner(context, projectId)- Helper function- Check role hierarchy and ownership
- Determine
effectiveRole(project-owner if owns, else context.role)
5. src/policies/index.ts
- Export all types and functions
Files to Modify
6. src/handlers/handler.types.ts
- Add
requiredRole: CwcRoletoCwcApiRouteConfig(required field)
7. src/handlers/RequestHandler.ts
- Import
checkRouteAccessfrom../policies - Replace inline
checkRouteAccess()method with imported function call
8. src/handlers/QueryHandler.ts
- Import
checkOperationAccessfrom../policies - Update
checkOperationAccess()to call imported function withrouteConfig.requiredRole
9. src/handlers/MutationHandler.ts
- Same changes as QueryHandler
Test Files to Create
10. src/__tests__/policies/checkRouteAccess.test.ts
- Direct role match tests (guest, logged-on-user, project-owner)
- Owner proxy access (logged-on-user accessing project-owner routes)
- Default-deny when no allowedRoles
- Access denied reason formatting
11. src/__tests__/policies/checkOperationAccess.test.ts
requiredRole: 'guest-user'- all roles allowedrequiredRole: 'logged-on-user'- deny guestrequiredRole: 'project-owner'- ownership check via ownedProjectsisProjectOwner()helper testseffectiveRoledetermination
12. src/__tests__/policies/routePolicies.test.ts
canAccessOwnerRoute()tests
13. Update src/__tests__/mocks/routeConfig.mock.ts
- Add
requiredRoleto mock route configs
14. Update existing handler tests
- Verify handlers call policy functions correctly
- Test with different
requiredRolevalues
Implementation Sequence
1. policy.types.ts (types)
2. routePolicies.ts (route helpers)
3. checkRouteAccess.ts (extract from RequestHandler)
4. checkOperationAccess.ts (replace stub, includes isProjectOwner)
5. index.ts (exports)
6. handler.types.ts (add requiredRole)
7. RequestHandler.ts (use checkRouteAccess)
8. QueryHandler.ts (use checkOperationAccess)
9. MutationHandler.ts (use checkOperationAccess)
10. routeConfig.mock.ts (update mocks)
11-14. Unit tests
15. Typecheck all packages
16. Run tests
17. Security review
18. Update CLAUDE.md
Security Checklist
- Default-deny policy (empty allowedRoles = blocked)
- Ownership from JWT only (
context.ownedProjects, not request body) -
projectIdfrom path params (operationContext.projectId), not payload - No privilege escalation (guest cannot become logged-on-user)
- Generic error messages in production (
errorDetailonly in dev)
Key Code Patterns
checkRouteAccess
export function checkRouteAccess(
context: RequestContext,
routeConfig: CwcApiRouteConfig,
policyConfig: RoutePolicyConfig = defaultRoutePolicyConfig
): RouteAccessResult
checkOperationAccess
export function checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult
isProjectOwner
export function isProjectOwner(
context: RequestContext,
projectId: string | undefined
): boolean {
if (!context.isAuthenticated) return false;
if (!projectId) return false;
return context.ownedProjects.includes(projectId);
}
Role hierarchy check
// requiredRole: 'guest-user' → always allowed
// requiredRole: 'logged-on-user' → must be authenticated
// requiredRole: 'project-owner' → must own the project (ownedProjects.includes(projectId))
Critical Files Reference
packages/cwc-api/src/handlers/handler.types.ts- Add requiredRolepackages/cwc-api/src/handlers/RequestHandler.ts:129-152- Extract checkRouteAccesspackages/cwc-api/src/handlers/QueryHandler.ts:87-91- Replace stubpackages/cwc-api/src/handlers/MutationHandler.ts- Replace stub (same pattern)packages/cwc-api/src/context/context.types.ts- AuthenticatedContext.ownedProjectspackages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts- Update with requiredRole
Tool Result
The file /Users/jeffbazinet/.claude/plans/jaunty-jingling-naur.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→# Phase 4: Access Policy System - Implementation Plan
2→
3→## Summary
4→
5→Implement the access policy system for cwc-api with two levels of access control:
6→1. **Route-level**: Can this role access this endpoint? (extract existing logic)
7→2. **Operation-level**: Can this user perform this operation on this resource? (replace stub)
8→
9→## Current State (Phase 3)
10→
11→- **Route access**: Working inline in `RequestHandler.checkRouteAccess()` (lines 129-152)
12→- **Operation access**: Stub in `QueryHandler.checkOperationAccess()` and `MutationHandler.checkOperationAccess()` that always returns `{ allowed: true }`
13→- **Types defined**: `RouteAccessResult`, `OperationAccessResult` in `handler.types.ts`
14→- **Context data**: `AuthenticatedContext.ownedProjects[]` available for ownership checks
15→
16→## Design Decisions
17→
18→1. **Keep `allowedRoles` on route configs** - Routes define their allowed roles inline
19→2. **Add `requiredRole: CwcRole` to route config** - Required field for operation-level checks
20→3. **Use existing CwcRole type** - No new enums; use role hierarchy interpretation
21→4. **Role hierarchy**: `guest-user < logged-on-user < project-owner`
22→5. **Project ownership only** - Use `context.ownedProjects.includes(projectId)` for Phase 4
23→6. **Default-deny policy** - Routes without `allowedRoles` are blocked
24→
25→## Role Hierarchy
26→
27→| requiredRole | Meaning | Who Can Access |
28→|--------------|---------|----------------|
29→| `guest-user` | Public access | Everyone (guest, logged-on, owner) |
30→| `logged-on-user` | Authenticated | Authenticated users only |
31→| `project-owner` | Owner access | Must own the project |
32→
33→## Files to Create
34→
35→### 1. `src/policies/policy.types.ts`
36→- Re-export `RouteAccessResult`, `OperationAccessResult` from handler.types.ts
37→- `RoutePolicyConfig` type (for default-deny behavior)
38→
39→### 2. `src/policies/routePolicies.ts`
40→- `defaultRoutePolicyConfig` - `{ defaultDeny: true, ownerProxyRoles: ['logged-on-user'] }`
41→- `canAccessOwnerRoute(role, config)` - Check if role can proxy for ownership
42→
43→### 3. `src/policies/checkRouteAccess.ts`
44→- `checkRouteAccess(context, routeConfig, policyConfig?)` - Extract from RequestHandler
45→- Same logic: direct role match OR authenticated + owner proxy
46→- Default-deny when allowedRoles empty
47→
48→### 4. `src/policies/checkOperationAccess.ts`
49→- `checkOperationAccess(context, operationContext, requiredRole)` - Replace stub
50→- `isProjectOwner(context, projectId)` - Helper function
51→- Check role hierarchy and ownership
52→- Determine `effectiveRole` (project-owner if owns, else context.role)
53→
54→### 5. `src/policies/index.ts`
55→- Export all types and functions
56→
57→## Files to Modify
58→
59→### 6. `src/handlers/handler.types.ts`
60→- Add `requiredRole: CwcRole` to `CwcApiRouteConfig` (required field)
61→
62→### 7. `src/handlers/RequestHandler.ts`
63→- Import `checkRouteAccess` from `../policies`
64→- Replace inline `checkRouteAccess()` method with imported function call
65→
66→### 8. `src/handlers/QueryHandler.ts`
67→- Import `checkOperationAccess` from `../policies`
68→- Update `checkOperationAccess()` to call imported function with `routeConfig.requiredRole`
69→
70→### 9. `src/handlers/MutationHandler.ts`
71→- Same changes as QueryHandler
72→
73→## Test Files to Create
74→
75→### 10. `src/__tests__/policies/checkRouteAccess.test.ts`
76→- Direct role match tests (guest, logged-on-user, project-owner)
77→- Owner proxy access (logged-on-user accessing project-owner routes)
78→- Default-deny when no allowedRoles
79→- Access denied reason formatting
80→
81→### 11. `src/__tests__/policies/checkOperationAccess.test.ts`
82→- `requiredRole: 'guest-user'` - all roles allowed
83→- `requiredRole: 'logged-on-user'` - deny guest
84→- `requiredRole: 'project-owner'` - ownership check via ownedProjects
85→- `isProjectOwner()` helper tests
86→- `effectiveRole` determination
87→
88→### 12. `src/__tests__/policies/routePolicies.test.ts`
89→- `canAccessOwnerRoute()` tests
90→
91→### 13. Update `src/__tests__/mocks/routeConfig.mock.ts`
92→- Add `requiredRole` to mock route configs
93→
94→### 14. Update existing handler tests
95→- Verify handlers call policy functions correctly
96→- Test with different `requiredRole` values
97→
98→## Implementation Sequence
99→
100→```
101→1. policy.types.ts (types)
102→2. routePolicies.ts (route helpers)
103→3. checkRouteAccess.ts (extract from RequestHandler)
104→4. checkOperationAccess.ts (replace stub, includes isProjectOwner)
105→5. index.ts (exports)
106→6. handler.types.ts (add requiredRole)
107→7. RequestHandler.ts (use checkRouteAccess)
108→8. QueryHandler.ts (use checkOperationAccess)
109→9. MutationHandler.ts (use checkOperationAccess)
110→10. routeConfig.mock.ts (update mocks)
111→11-14. Unit tests
112→15. Typecheck all packages
113→16. Run tests
114→17. Security review
115→18. Update CLAUDE.md
116→```
117→
118→## Security Checklist
119→
120→- [ ] Default-deny policy (empty allowedRoles = blocked)
121→- [ ] Ownership from JWT only (`context.ownedProjects`, not request body)
122→- [ ] `projectId` from path params (`operationContext.projectId`), not payload
123→- [ ] No privilege escalation (guest cannot become logged-on-user)
124→- [ ] Generic error messages in production (`errorDetail` only in dev)
125→
126→## Key Code Patterns
127→
128→### checkRouteAccess
129→```typescript
130→export function checkRouteAccess(
131→ context: RequestContext,
132→ routeConfig: CwcApiRouteConfig,
133→ policyConfig: RoutePolicyConfig = defaultRoutePolicyConfig
134→): RouteAccessResult
135→```
136→
137→### checkOperationAccess
138→```typescript
139→export function checkOperationAccess(
140→ context: RequestContext,
141→ operationContext: OperationContext,
142→ requiredRole: CwcRole
143→): OperationAccessResult
144→```
145→
146→### isProjectOwner
147→```typescript
148→export function isProjectOwner(
149→ context: RequestContext,
150→ projectId: string | undefined
151→): boolean {
152→ if (!context.isAuthenticated) return false;
153→ if (!projectId) return false;
154→ return context.ownedProjects.includes(projectId);
155→}
156→```
157→
158→### Role hierarchy check
159→```typescript
160→// requiredRole: 'guest-user' → always allowed
161→// requiredRole: 'logged-on-user' → must be authenticated
162→// requiredRole: 'project-owner' → must own the project (ownedProjects.includes(projectId))
163→```
164→
165→## Critical Files Reference
166→
167→- `packages/cwc-api/src/handlers/handler.types.ts` - Add requiredRole
168→- `packages/cwc-api/src/handlers/RequestHandler.ts:129-152` - Extract checkRouteAccess
169→- `packages/cwc-api/src/handlers/QueryHandler.ts:87-91` - Replace stub
170→- `packages/cwc-api/src/handlers/MutationHandler.ts` - Replace stub (same pattern)
171→- `packages/cwc-api/src/context/context.types.ts` - AuthenticatedContext.ownedProjects
172→- `packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts` - Update with requiredRole
173→I've updated the plan with the simpler design:
Key changes:
requiredRole: CwcRoleis now a required field on route configs- Uses existing
CwcRoletype with role hierarchy interpretation - No new enum types needed
- Simplified file structure (5 files to create instead of 6)
checkOperationAccess(context, operationContext, requiredRole)- clean signature
The plan is now streamlined and easier to understand. Ready to proceed?
I am thinking that the same requiredRole changes could be made to the checkRouteAccess function as well. Will that work?
Great question! Let me think through this...
Current design has TWO fields:
allowedRoles: CwcRole[]- for route access (who can call the endpoint)requiredRole: CwcRole- for operation access (who can execute the operation)
Could we use ONE requiredRole for both?
Yes, with this interpretation:
| requiredRole | Route Access (checkRouteAccess) | Operation Access (checkOperationAccess) |
|---|---|---|
guest-user |
Anyone allowed | Anyone allowed |
logged-on-user |
Must be authenticated | Must be authenticated |
project-owner |
Must be authenticated | Must be authenticated AND own the project |
The key difference: at route level, project-owner only checks authentication (letting logged-on-user through to attempt the operation). At operation level, project-owner checks actual ownership.
Simplified route config:
type CwcApiRouteConfig = {
path: string;
handlerType: CwcApiHandlerType;
requiredRole: CwcRole; // One field for both levels
operation: CwcApiOperation;
};
Benefits:
- One field instead of two
- Clearer mental model
- No need to think about
allowedRolesarrays
The logic:
checkRouteAccess: UsesrequiredRoleto check authentication level onlycheckOperationAccess: UsesrequiredRoleto check authentication AND ownership (forproject-owner)
Does this match what you're thinking?
Yes, to avoid doing any database lookups in order to check the projectId/projectPkId and determine ownership, it is okay for the route to enforce only logged-on-user role, and once the mutation(operation) is called, it will perform the more granular access check.
Let me update the plan with this simplified single requiredRole approach.
Phase 4: Access Policy System - Implementation Plan
Summary
Implement the access policy system for cwc-api with two levels of access control:
- Route-level: Check authentication requirement (no database lookups)
- Operation-level: Check authentication AND ownership for
project-ownerroutes
Both levels use the same requiredRole: CwcRole field on route config.
Current State (Phase 3)
- Route access: Working inline in
RequestHandler.checkRouteAccess()(lines 129-152) usingallowedRoles[] - Operation access: Stub in
QueryHandler.checkOperationAccess()andMutationHandler.checkOperationAccess()that always returns{ allowed: true } - Types defined:
RouteAccessResult,OperationAccessResultinhandler.types.ts - Context data:
AuthenticatedContext.ownedProjects[]available for ownership checks
Design Decisions
- Replace
allowedRoles[]withrequiredRole: CwcRole- Single field for both levels - Use existing CwcRole type - No new enums
- Role hierarchy:
guest-user < logged-on-user < project-owner - Route-level checks authentication only - No ownership check (no DB lookup)
- Operation-level checks ownership - For
project-owner, verify viaownedProjects - Project ownership from JWT - Use
context.ownedProjects.includes(projectId)
Single requiredRole Field
| requiredRole | Route Access | Operation Access |
|---|---|---|
guest-user |
Anyone allowed | Anyone allowed |
logged-on-user |
Must be authenticated | Must be authenticated |
project-owner |
Must be authenticated | Must be authenticated AND own the project |
Key insight: Route-level for project-owner only enforces authentication. This allows logged-on-user to attempt the operation, where the actual ownership check happens.
Files to Create
1. src/policies/policy.types.ts
- Re-export
RouteAccessResult,OperationAccessResultfrom handler.types.ts
2. src/policies/checkRouteAccess.ts
checkRouteAccess(context, requiredRole)- Check authentication level onlyguest-user→ always allowedlogged-on-userorproject-owner→ must be authenticated
3. src/policies/checkOperationAccess.ts
checkOperationAccess(context, operationContext, requiredRole)- Check auth + ownershipisProjectOwner(context, projectId)- Helper function- Determine
effectiveRole(project-owner if owns, else context.role)
4. src/policies/index.ts
- Export all types and functions
Files to Modify
5. src/handlers/handler.types.ts
- Replace
allowedRoles: CwcRole[]withrequiredRole: CwcRole
6. src/handlers/RequestHandler.ts
- Import
checkRouteAccessfrom../policies - Replace inline
checkRouteAccess()method - Pass
routeConfig.requiredRoleinstead ofrouteConfig.allowedRoles
7. src/handlers/QueryHandler.ts
- Import
checkOperationAccessfrom../policies - Replace stub with call to
checkOperationAccess(context, operationContext, routeConfig.requiredRole)
8. src/handlers/MutationHandler.ts
- Same changes as QueryHandler
9. src/__tests__/mocks/routeConfig.mock.ts
- Replace
allowedRoleswithrequiredRolein mock factory
Test Files to Create
10. src/__tests__/policies/checkRouteAccess.test.ts
requiredRole: 'guest-user'- all contexts allowedrequiredRole: 'logged-on-user'- deny guest, allow authenticatedrequiredRole: 'project-owner'- deny guest, allow authenticated (no ownership check)
11. src/__tests__/policies/checkOperationAccess.test.ts
requiredRole: 'guest-user'- all roles allowedrequiredRole: 'logged-on-user'- deny guest, allow authenticatedrequiredRole: 'project-owner'- deny guest, deny non-owner, allow ownerisProjectOwner()helper testseffectiveRoledetermination
12. Update existing handler tests
- Update mock route configs to use
requiredRole - Verify handlers call policy functions correctly
Implementation Sequence
1. policy.types.ts (types)
2. checkRouteAccess.ts (route-level check)
3. checkOperationAccess.ts (operation-level check + isProjectOwner)
4. index.ts (exports)
5. handler.types.ts (replace allowedRoles with requiredRole)
6. RequestHandler.ts (use checkRouteAccess)
7. QueryHandler.ts (use checkOperationAccess)
8. MutationHandler.ts (use checkOperationAccess)
9. routeConfig.mock.ts (update mocks)
10-12. Unit tests
13. Update existing handler tests
14. Typecheck all packages
15. Run tests
16. Security review
17. Update CLAUDE.md
Security Checklist
- Route-level only checks authentication (no DB lookup for ownership)
- Ownership from JWT only (
context.ownedProjects, not request body) -
projectIdfrom path params (operationContext.projectId), not payload - No privilege escalation (guest cannot become logged-on-user)
- Generic error messages in production (
errorDetailonly in dev)
Key Code Patterns
checkRouteAccess (authentication only)
export function checkRouteAccess(
context: RequestContext,
requiredRole: CwcRole
): RouteAccessResult {
// guest-user: anyone allowed
if (requiredRole === 'guest-user') {
return { allowed: true };
}
// logged-on-user or project-owner: must be authenticated
// (ownership checked at operation level for project-owner)
if (!context.isAuthenticated) {
return { allowed: false, reason: 'Authentication required' };
}
return { allowed: true };
}
checkOperationAccess (authentication + ownership)
export function checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult {
// guest-user: anyone allowed
if (requiredRole === 'guest-user') {
return { allowed: true, effectiveRole: context.role };
}
// Must be authenticated for logged-on-user or project-owner
if (!context.isAuthenticated) {
return { allowed: false, reason: 'Authentication required' };
}
// logged-on-user: authenticated is enough
if (requiredRole === 'logged-on-user') {
return { allowed: true, effectiveRole: context.role };
}
// project-owner: must own the project
const { projectId } = operationContext;
if (!isProjectOwner(context, projectId)) {
return { allowed: false, reason: 'Project ownership required' };
}
return { allowed: true, effectiveRole: 'project-owner' };
}
isProjectOwner
export function isProjectOwner(
context: RequestContext,
projectId: string | undefined
): boolean {
if (!context.isAuthenticated) return false;
if (!projectId) return false;
return context.ownedProjects.includes(projectId);
}
Critical Files Reference
packages/cwc-api/src/handlers/handler.types.ts- Replace allowedRoles with requiredRolepackages/cwc-api/src/handlers/RequestHandler.ts:129-152- Simplify checkRouteAccesspackages/cwc-api/src/handlers/QueryHandler.ts:87-91- Replace stubpackages/cwc-api/src/handlers/MutationHandler.ts- Replace stub (same pattern)packages/cwc-api/src/context/context.types.ts- AuthenticatedContext.ownedProjectspackages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts- Update with requiredRole
Tool Result
The file /Users/jeffbazinet/.claude/plans/jaunty-jingling-naur.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→# Phase 4: Access Policy System - Implementation Plan
2→
3→## Summary
4→
5→Implement the access policy system for cwc-api with two levels of access control:
6→1. **Route-level**: Check authentication requirement (no database lookups)
7→2. **Operation-level**: Check authentication AND ownership for `project-owner` routes
8→
9→Both levels use the same `requiredRole: CwcRole` field on route config.
10→
11→## Current State (Phase 3)
12→
13→- **Route access**: Working inline in `RequestHandler.checkRouteAccess()` (lines 129-152) using `allowedRoles[]`
14→- **Operation access**: Stub in `QueryHandler.checkOperationAccess()` and `MutationHandler.checkOperationAccess()` that always returns `{ allowed: true }`
15→- **Types defined**: `RouteAccessResult`, `OperationAccessResult` in `handler.types.ts`
16→- **Context data**: `AuthenticatedContext.ownedProjects[]` available for ownership checks
17→
18→## Design Decisions
19→
20→1. **Replace `allowedRoles[]` with `requiredRole: CwcRole`** - Single field for both levels
21→2. **Use existing CwcRole type** - No new enums
22→3. **Role hierarchy**: `guest-user < logged-on-user < project-owner`
23→4. **Route-level checks authentication only** - No ownership check (no DB lookup)
24→5. **Operation-level checks ownership** - For `project-owner`, verify via `ownedProjects`
25→6. **Project ownership from JWT** - Use `context.ownedProjects.includes(projectId)`
26→
27→## Single `requiredRole` Field
28→
29→| requiredRole | Route Access | Operation Access |
30→|--------------|--------------|------------------|
31→| `guest-user` | Anyone allowed | Anyone allowed |
32→| `logged-on-user` | Must be authenticated | Must be authenticated |
33→| `project-owner` | Must be authenticated | Must be authenticated AND own the project |
34→
35→**Key insight**: Route-level for `project-owner` only enforces authentication. This allows `logged-on-user` to attempt the operation, where the actual ownership check happens.
36→
37→## Files to Create
38→
39→### 1. `src/policies/policy.types.ts`
40→- Re-export `RouteAccessResult`, `OperationAccessResult` from handler.types.ts
41→
42→### 2. `src/policies/checkRouteAccess.ts`
43→- `checkRouteAccess(context, requiredRole)` - Check authentication level only
44→- `guest-user` → always allowed
45→- `logged-on-user` or `project-owner` → must be authenticated
46→
47→### 3. `src/policies/checkOperationAccess.ts`
48→- `checkOperationAccess(context, operationContext, requiredRole)` - Check auth + ownership
49→- `isProjectOwner(context, projectId)` - Helper function
50→- Determine `effectiveRole` (project-owner if owns, else context.role)
51→
52→### 4. `src/policies/index.ts`
53→- Export all types and functions
54→
55→## Files to Modify
56→
57→### 5. `src/handlers/handler.types.ts`
58→- Replace `allowedRoles: CwcRole[]` with `requiredRole: CwcRole`
59→
60→### 6. `src/handlers/RequestHandler.ts`
61→- Import `checkRouteAccess` from `../policies`
62→- Replace inline `checkRouteAccess()` method
63→- Pass `routeConfig.requiredRole` instead of `routeConfig.allowedRoles`
64→
65→### 7. `src/handlers/QueryHandler.ts`
66→- Import `checkOperationAccess` from `../policies`
67→- Replace stub with call to `checkOperationAccess(context, operationContext, routeConfig.requiredRole)`
68→
69→### 8. `src/handlers/MutationHandler.ts`
70→- Same changes as QueryHandler
71→
72→### 9. `src/__tests__/mocks/routeConfig.mock.ts`
73→- Replace `allowedRoles` with `requiredRole` in mock factory
74→
75→## Test Files to Create
76→
77→### 10. `src/__tests__/policies/checkRouteAccess.test.ts`
78→- `requiredRole: 'guest-user'` - all contexts allowed
79→- `requiredRole: 'logged-on-user'` - deny guest, allow authenticated
80→- `requiredRole: 'project-owner'` - deny guest, allow authenticated (no ownership check)
81→
82→### 11. `src/__tests__/policies/checkOperationAccess.test.ts`
83→- `requiredRole: 'guest-user'` - all roles allowed
84→- `requiredRole: 'logged-on-user'` - deny guest, allow authenticated
85→- `requiredRole: 'project-owner'` - deny guest, deny non-owner, allow owner
86→- `isProjectOwner()` helper tests
87→- `effectiveRole` determination
88→
89→### 12. Update existing handler tests
90→- Update mock route configs to use `requiredRole`
91→- Verify handlers call policy functions correctly
92→
93→## Implementation Sequence
94→
95→```
96→1. policy.types.ts (types)
97→2. checkRouteAccess.ts (route-level check)
98→3. checkOperationAccess.ts (operation-level check + isProjectOwner)
99→4. index.ts (exports)
100→5. handler.types.ts (replace allowedRoles with requiredRole)
101→6. RequestHandler.ts (use checkRouteAccess)
102→7. QueryHandler.ts (use checkOperationAccess)
103→8. MutationHandler.ts (use checkOperationAccess)
104→9. routeConfig.mock.ts (update mocks)
105→10-12. Unit tests
106→13. Update existing handler tests
107→14. Typecheck all packages
108→15. Run tests
109→16. Security review
110→17. Update CLAUDE.md
111→```
112→
113→## Security Checklist
114→
115→- [ ] Route-level only checks authentication (no DB lookup for ownership)
116→- [ ] Ownership from JWT only (`context.ownedProjects`, not request body)
117→- [ ] `projectId` from path params (`operationContext.projectId`), not payload
118→- [ ] No privilege escalation (guest cannot become logged-on-user)
119→- [ ] Generic error messages in production (`errorDetail` only in dev)
120→
121→## Key Code Patterns
122→
123→### checkRouteAccess (authentication only)
124→```typescript
125→export function checkRouteAccess(
126→ context: RequestContext,
127→ requiredRole: CwcRole
128→): RouteAccessResult {
129→ // guest-user: anyone allowed
130→ if (requiredRole === 'guest-user') {
131→ return { allowed: true };
132→ }
133→
134→ // logged-on-user or project-owner: must be authenticated
135→ // (ownership checked at operation level for project-owner)
136→ if (!context.isAuthenticated) {
137→ return { allowed: false, reason: 'Authentication required' };
138→ }
139→
140→ return { allowed: true };
141→}
142→```
143→
144→### checkOperationAccess (authentication + ownership)
145→```typescript
146→export function checkOperationAccess(
147→ context: RequestContext,
148→ operationContext: OperationContext,
149→ requiredRole: CwcRole
150→): OperationAccessResult {
151→ // guest-user: anyone allowed
152→ if (requiredRole === 'guest-user') {
153→ return { allowed: true, effectiveRole: context.role };
154→ }
155→
156→ // Must be authenticated for logged-on-user or project-owner
157→ if (!context.isAuthenticated) {
158→ return { allowed: false, reason: 'Authentication required' };
159→ }
160→
161→ // logged-on-user: authenticated is enough
162→ if (requiredRole === 'logged-on-user') {
163→ return { allowed: true, effectiveRole: context.role };
164→ }
165→
166→ // project-owner: must own the project
167→ const { projectId } = operationContext;
168→ if (!isProjectOwner(context, projectId)) {
169→ return { allowed: false, reason: 'Project ownership required' };
170→ }
171→
172→ return { allowed: true, effectiveRole: 'project-owner' };
173→}
174→```
175→
176→### isProjectOwner
177→```typescript
178→export function isProjectOwner(
179→ context: RequestContext,
180→ projectId: string | undefined
181→): boolean {
182→ if (!context.isAuthenticated) return false;
183→ if (!projectId) return false;
184→ return context.ownedProjects.includes(projectId);
185→}
186→```
187→
188→## Critical Files Reference
189→
190→- `packages/cwc-api/src/handlers/handler.types.ts` - Replace allowedRoles with requiredRole
191→- `packages/cwc-api/src/handlers/RequestHandler.ts:129-152` - Simplify checkRouteAccess
192→- `packages/cwc-api/src/handlers/QueryHandler.ts:87-91` - Replace stub
193→- `packages/cwc-api/src/handlers/MutationHandler.ts` - Replace stub (same pattern)
194→- `packages/cwc-api/src/context/context.types.ts` - AuthenticatedContext.ownedProjects
195→- `packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts` - Update with requiredRole
196→I've updated the plan with the simplified single requiredRole approach. Key improvements:
Simplified design:
- One
requiredRole: CwcRolefield instead ofallowedRoles[] - Route-level only checks authentication (no DB lookups)
- Operation-level adds ownership check for
project-owner
Fewer files to create:
policy.types.ts- Re-export typescheckRouteAccess.ts- Authentication check onlycheckOperationAccess.ts- Auth + ownership checkindex.ts- Exports
Clear separation:
- Route: "Can you even try this endpoint?" (auth only)
- Operation: "Can you actually do this action?" (auth + ownership)
Ready to proceed?
Let me create a todo list and start implementing.
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "in_progress",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "pending",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "pending",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "pending",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "pending",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/policy.types.ts",
"content": "'use strict';\n\n/**\n * Policy Types\n *\n * Re-exports access result types from handler.types.ts for use in policy modules.\n * This keeps policy-related types centralized for easier imports.\n */\n\nexport type { RouteAccessResult, OperationAccessResult } from '../handlers/handler.types';\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/policy.types.ts
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "in_progress",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "pending",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "pending",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "pending",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "pending",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkRouteAccess.ts",
"content": "'use strict';\n\nimport type { CwcRole } from 'cwc-types';\nimport type { RequestContext } from '../context';\nimport type { RouteAccessResult } from './policy.types';\n\n/**\n * Check if the current context can access a route based on required role.\n *\n * Route-level access only checks authentication status, not ownership.\n * Ownership is verified at the operation level for 'project-owner' routes.\n *\n * Role hierarchy (from least to most privileged):\n * - guest-user: No authentication required\n * - logged-on-user: Must be authenticated\n * - project-owner: Must be authenticated (ownership checked at operation level)\n *\n * @param context - The request context (authenticated or guest)\n * @param requiredRole - The minimum role required to access the route\n * @returns RouteAccessResult indicating whether access is allowed\n */\nexport function checkRouteAccess(\n context: RequestContext,\n requiredRole: CwcRole\n): RouteAccessResult {\n // guest-user: anyone can access\n if (requiredRole === 'guest-user') {\n return { allowed: true };\n }\n\n // logged-on-user or project-owner: must be authenticated\n // (ownership is checked at operation level for project-owner)\n if (!context.isAuthenticated) {\n return {\n allowed: false,\n reason: 'Authentication required',\n };\n }\n\n return { allowed: true };\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkRouteAccess.ts
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "in_progress",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "pending",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "pending",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "pending",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "pending",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts",
"content": "'use strict';\n\nimport type { CwcRole } from 'cwc-types';\nimport type { RequestContext } from '../context';\nimport type { OperationContext } from '../handlers/handler.types';\nimport type { OperationAccessResult } from './policy.types';\n\n/**\n * Check if the user owns the specified project.\n *\n * Uses context.ownedProjects which is populated from JWT claims verified by cwc-auth.\n * SECURITY: Never use projectId from request body - only from path params.\n *\n * @param context - The request context\n * @param projectId - The project ID to check ownership for\n * @returns true if the user owns the project, false otherwise\n */\nexport function isProjectOwner(\n context: RequestContext,\n projectId: string | undefined\n): boolean {\n if (!context.isAuthenticated) {\n return false;\n }\n if (!projectId) {\n return false;\n }\n return context.ownedProjects.includes(projectId);\n}\n\n/**\n * Check if the current context can perform an operation based on required role.\n *\n * Operation-level access checks both authentication AND ownership for project-owner routes.\n * This is where the actual ownership verification happens.\n *\n * Role hierarchy (from least to most privileged):\n * - guest-user: No authentication required\n * - logged-on-user: Must be authenticated\n * - project-owner: Must be authenticated AND own the project\n *\n * @param context - The request context (authenticated or guest)\n * @param operationContext - The operation context containing projectId\n * @param requiredRole - The minimum role required to perform the operation\n * @returns OperationAccessResult indicating whether access is allowed and the effective role\n */\nexport function checkOperationAccess(\n context: RequestContext,\n operationContext: OperationContext,\n requiredRole: CwcRole\n): OperationAccessResult {\n // guest-user: anyone can perform the operation\n if (requiredRole === 'guest-user') {\n return {\n allowed: true,\n effectiveRole: context.role,\n };\n }\n\n // Must be authenticated for logged-on-user or project-owner\n if (!context.isAuthenticated) {\n return {\n allowed: false,\n reason: 'Authentication required',\n };\n }\n\n // logged-on-user: authenticated is enough\n if (requiredRole === 'logged-on-user') {\n return {\n allowed: true,\n effectiveRole: context.role,\n };\n }\n\n // project-owner: must own the project\n const { projectId } = operationContext;\n if (!isProjectOwner(context, projectId)) {\n return {\n allowed: false,\n reason: projectId\n ? `User does not own project '${projectId}'`\n : 'Project ID required for ownership check',\n };\n }\n\n return {\n allowed: true,\n effectiveRole: 'project-owner',\n };\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "in_progress",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "pending",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "pending",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "pending",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/index.ts",
"content": "'use strict';\n\n/**\n * Access Policy Module\n *\n * Provides route-level and operation-level access control for cwc-api endpoints.\n *\n * Usage:\n * - Route-level: checkRouteAccess(context, requiredRole)\n * - Operation-level: checkOperationAccess(context, operationContext, requiredRole)\n */\n\n// Types\nexport type { RouteAccessResult, OperationAccessResult } from './policy.types';\n\n// Route access\nexport { checkRouteAccess } from './checkRouteAccess';\n\n// Operation access\nexport { checkOperationAccess, isProjectOwner } from './checkOperationAccess';\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/index.ts
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "in_progress",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "pending",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "pending",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/handler.types.ts",
"old_string": "/**\n * Route configuration for cwc-api endpoints\n */\nexport type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {\n // Route identification\n path: string;\n handlerType: CwcApiHandlerType;\n\n // Access control\n allowedRoles: CwcRole[];\n\n // Operation (injected worker function)\n operation: CwcApiOperation<TPayload, TResult>;\n\n // Debug flag\n debug?: boolean | undefined;\n};",
"new_string": "/**\n * Route configuration for cwc-api endpoints\n */\nexport type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {\n // Route identification\n path: string;\n handlerType: CwcApiHandlerType;\n\n // Access control - minimum role required (uses role hierarchy)\n // guest-user < logged-on-user < project-owner\n requiredRole: CwcRole;\n\n // Operation (injected worker function)\n operation: CwcApiOperation<TPayload, TResult>;\n\n // Debug flag\n debug?: boolean | undefined;\n};"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/handler.types.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
90→ * Handler type: query (read-only) or mutation (write)
91→ */
92→export type CwcApiHandlerType = 'query' | 'mutation';
93→
94→/**
95→ * Route configuration for cwc-api endpoints
96→ */
97→export type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {
98→ // Route identification
99→ path: string;
100→ handlerType: CwcApiHandlerType;
101→
102→ // Access control - minimum role required (uses role hierarchy)
103→ // guest-user < logged-on-user < project-owner
104→ requiredRole: CwcRole;
105→
106→ // Operation (injected worker function)
107→ operation: CwcApiOperation<TPayload, TResult>;
108→
109→ // Debug flag
110→ debug?: boolean | undefined;
111→};
112→
113→/**
114→ * Map of route paths to configurations
115→ */TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "in_progress",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "pending",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Let me update RequestHandler.ts to use the new checkRouteAccess from policies.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts",
"old_string": "import type { ILogger, AuthClient } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n CwcApiSuccessResponse,\n RequestHandlerOptions,\n RouteAccessResult,\n OperationContext,\n} from './handler.types';\nimport { QueryHandler } from './QueryHandler';\nimport { MutationHandler } from './MutationHandler';\nimport { createInternalErrorResponse } from './responseUtils';",
"new_string": "import type { ILogger, AuthClient } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n CwcApiSuccessResponse,\n RequestHandlerOptions,\n OperationContext,\n} from './handler.types';\nimport { QueryHandler } from './QueryHandler';\nimport { MutationHandler } from './MutationHandler';\nimport { createInternalErrorResponse } from './responseUtils';\nimport { checkRouteAccess } from '../policies';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→'use strict';
2→
3→import type { ILogger, AuthClient } from 'cwc-backend-utils';
4→import type { CwcApiConfig } from '../config';
5→import type {
6→ CwcApiHandler,
7→ CwcApiHandlerResponse,
8→ CwcApiSuccessResponse,
9→ RequestHandlerOptions,
10→ OperationContext,
11→} from './handler.types';
12→import { QueryHandler } from './QueryHandler';
13→import { MutationHandler } from './MutationHandler';
14→import { createInternalErrorResponse } from './responseUtils';
15→import { checkRouteAccess } from '../policies';
16→
17→const codeLocation = 'handlers/RequestHandler.ts';
18→
19→/**Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts",
"old_string": " public async processRequest(): Promise<CwcApiHandlerResponse> {\n const { context, routeConfig, payload, authHeader, pathParams } = this.options;\n\n try {\n // Step 1: Check route-level access\n const routeAccess = this.checkRouteAccess();\n if (!routeAccess.allowed) {\n // No session renewal for auth errors\n return this.createAccessDeniedResponse(routeAccess.reason);\n }",
"new_string": " public async processRequest(): Promise<CwcApiHandlerResponse> {\n const { context, routeConfig, payload, authHeader, pathParams } = this.options;\n\n try {\n // Step 1: Check route-level access (authentication only, no ownership check)\n const routeAccess = checkRouteAccess(context, routeConfig.requiredRole);\n if (!routeAccess.allowed) {\n // No session renewal for auth errors\n return this.createAccessDeniedResponse(routeAccess.reason);\n }"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
42→ this.authClient = authClient;
43→ this.logger = logger;
44→ }
45→
46→ public async processRequest(): Promise<CwcApiHandlerResponse> {
47→ const { context, routeConfig, payload, authHeader, pathParams } = this.options;
48→
49→ try {
50→ // Step 1: Check route-level access (authentication only, no ownership check)
51→ const routeAccess = checkRouteAccess(context, routeConfig.requiredRole);
52→ if (!routeAccess.allowed) {
53→ // No session renewal for auth errors
54→ return this.createAccessDeniedResponse(routeAccess.reason);
55→ }
56→
57→ // Step 2: Build operation context
58→ const operationContext: OperationContext = {
59→ context,Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts",
"old_string": " /**\n * Check if the current context role can access this route\n * Compares context.role against routeConfig.allowedRoles\n */\n private checkRouteAccess(): RouteAccessResult {\n const { context, routeConfig } = this.options;\n const { allowedRoles } = routeConfig;\n\n // Check if the current role is in allowedRoles\n if (allowedRoles.includes(context.role)) {\n return { allowed: true };\n }\n\n // For authenticated users with 'project-owner' role requirement,\n // 'logged-on-user' should also be allowed (ownership check happens at operation level)\n if (\n context.isAuthenticated &&\n context.role === 'logged-on-user' &&\n allowedRoles.includes('project-owner')\n ) {\n return { allowed: true };\n }\n\n return {\n allowed: false,\n reason: `Role '${context.role}' cannot access route ${routeConfig.path}`,\n };\n }\n\n private createAccessDeniedResponse",
"new_string": " private createAccessDeniedResponse"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
121→ return createInternalErrorResponse(this.config, error);
122→ }
123→ }
124→
125→ private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {
126→ const { context } = this.options;
127→
128→ // Use 401 for unauthenticated, 403 for authenticated but not allowed
129→ const statusCode = context.isAuthenticated ? 403 : 401;TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "in_progress",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "pending",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "pending",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts",
"old_string": "import type { ILogger } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n QueryHandlerOptions,\n OperationAccessResult,\n} from './handler.types';\nimport {\n createOperationDeniedResponse,\n createOperationErrorResponse,\n createInternalErrorResponse,\n} from './responseUtils';",
"new_string": "import type { ILogger } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n QueryHandlerOptions,\n} from './handler.types';\nimport {\n createOperationDeniedResponse,\n createOperationErrorResponse,\n createInternalErrorResponse,\n} from './responseUtils';\nimport { checkOperationAccess } from '../policies';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→'use strict';
2→
3→import type { ILogger } from 'cwc-backend-utils';
4→import type { CwcApiConfig } from '../config';
5→import type {
6→ CwcApiHandler,
7→ CwcApiHandlerResponse,
8→ QueryHandlerOptions,
9→} from './handler.types';
10→import {
11→ createOperationDeniedResponse,
12→ createOperationErrorResponse,
13→ createInternalErrorResponse,
14→} from './responseUtils';
15→import { checkOperationAccess } from '../policies';
16→
17→const codeLocation = 'handlers/QueryHandler.ts';
18→
19→/**Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts",
"old_string": " public async processRequest(): Promise<CwcApiHandlerResponse> {\n const { context, routeConfig, payload, operationContext } = this.options;\n\n try {\n // Step 1: Check operation-level access (Phase 4 implementation)\n const operationAccess = await this.checkOperationAccess();\n if (!operationAccess.allowed) {\n return createOperationDeniedResponse(this.config, operationAccess.reason);\n }",
"new_string": " public async processRequest(): Promise<CwcApiHandlerResponse> {\n const { context, routeConfig, payload, operationContext } = this.options;\n\n try {\n // Step 1: Check operation-level access (authentication + ownership for project-owner)\n const operationAccess = checkOperationAccess(context, operationContext, routeConfig.requiredRole);\n if (!operationAccess.allowed) {\n return createOperationDeniedResponse(this.config, operationAccess.reason);\n }"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
40→ this.config = config;
41→ this.logger = logger;
42→ }
43→
44→ public async processRequest(): Promise<CwcApiHandlerResponse> {
45→ const { context, routeConfig, payload, operationContext } = this.options;
46→
47→ try {
48→ // Step 1: Check operation-level access (authentication + ownership for project-owner)
49→ const operationAccess = checkOperationAccess(context, operationContext, routeConfig.requiredRole);
50→ if (!operationAccess.allowed) {
51→ return createOperationDeniedResponse(this.config, operationAccess.reason);
52→ }
53→
54→ // Step 2: Execute the operation
55→ const result = await routeConfig.operation(payload, operationContext);
56→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts",
"old_string": " return createInternalErrorResponse(this.config, error);\n }\n }\n\n /**\n * Check if the current context can perform this operation\n * Phase 3: Stub - always returns allowed\n * Phase 4: Will check ownership, resource access, etc.\n */\n private async checkOperationAccess(): Promise<OperationAccessResult> {\n // Phase 4 will implement actual access policy checks\n // For now, route-level access is sufficient\n return { allowed: true };\n }\n}",
"new_string": " return createInternalErrorResponse(this.config, error);\n }\n }\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
74→ message: `Error executing query operation for ${routeConfig.path}`,
75→ error,
76→ });
77→
78→ return createInternalErrorResponse(this.config, error);
79→ }
80→ }
81→}
82→TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "in_progress",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "pending",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts"
}Tool Result
1→'use strict';
2→
3→import type { ILogger } from 'cwc-backend-utils';
4→import type { CwcApiConfig } from '../config';
5→import type {
6→ CwcApiHandler,
7→ CwcApiHandlerResponse,
8→ MutationHandlerOptions,
9→ OperationAccessResult,
10→} from './handler.types';
11→import {
12→ createOperationDeniedResponse,
13→ createOperationErrorResponse,
14→ createInternalErrorResponse,
15→} from './responseUtils';
16→
17→const codeLocation = 'handlers/MutationHandler.ts';
18→
19→/**
20→ * MutationHandler - Handles write operations
21→ *
22→ * Responsibilities:
23→ * 1. Check operation-level access (Phase 4 stub)
24→ * 2. Execute the mutation operation
25→ * 3. Format successful response with data
26→ *
27→ * Note: Session renewal is handled by RequestHandler
28→ */
29→export class MutationHandler implements CwcApiHandler {
30→ private options: MutationHandlerOptions;
31→ private config: CwcApiConfig;
32→ private logger: ILogger | undefined;
33→
34→ constructor(
35→ options: MutationHandlerOptions,
36→ config: CwcApiConfig,
37→ logger: ILogger | undefined
38→ ) {
39→ this.options = options;
40→ this.config = config;
41→ this.logger = logger;
42→ }
43→
44→ public async processRequest(): Promise<CwcApiHandlerResponse> {
45→ const { context, routeConfig, payload, operationContext } = this.options;
46→
47→ try {
48→ // Step 1: Check operation-level access (Phase 4 implementation)
49→ const operationAccess = await this.checkOperationAccess();
50→ if (!operationAccess.allowed) {
51→ return createOperationDeniedResponse(this.config, operationAccess.reason);
52→ }
53→
54→ // Step 2: Execute the mutation
55→ const result = await routeConfig.operation(payload, operationContext);
56→
57→ if (!result.success) {
58→ return createOperationErrorResponse(result.errorCode, result.errorMessage);
59→ }
60→
61→ // Step 3: Format response (JWT added by RequestHandler, always 200 for RPC-style API)
62→ return {
63→ statusCode: 200,
64→ body: {
65→ success: true,
66→ data: result.data,
67→ },
68→ };
69→ } catch (error) {
70→ this.logger?.logError({
71→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
72→ codeLocation,
73→ message: `Error executing mutation operation for ${routeConfig.path}`,
74→ error,
75→ });
76→
77→ return createInternalErrorResponse(this.config, error);
78→ }
79→ }
80→
81→ /**
82→ * Check if the current context can perform this operation
83→ * Phase 3: Stub - always returns allowed
84→ * Phase 4: Will check ownership, resource access, etc.
85→ */
86→ private async checkOperationAccess(): Promise<OperationAccessResult> {
87→ // Phase 4 will implement actual access policy checks
88→ return { allowed: true };
89→ }
90→}
91→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts",
"old_string": "import type { ILogger } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n MutationHandlerOptions,\n OperationAccessResult,\n} from './handler.types';\nimport {\n createOperationDeniedResponse,\n createOperationErrorResponse,\n createInternalErrorResponse,\n} from './responseUtils';",
"new_string": "import type { ILogger } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n MutationHandlerOptions,\n} from './handler.types';\nimport {\n createOperationDeniedResponse,\n createOperationErrorResponse,\n createInternalErrorResponse,\n} from './responseUtils';\nimport { checkOperationAccess } from '../policies';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→'use strict';
2→
3→import type { ILogger } from 'cwc-backend-utils';
4→import type { CwcApiConfig } from '../config';
5→import type {
6→ CwcApiHandler,
7→ CwcApiHandlerResponse,
8→ MutationHandlerOptions,
9→} from './handler.types';
10→import {
11→ createOperationDeniedResponse,
12→ createOperationErrorResponse,
13→ createInternalErrorResponse,
14→} from './responseUtils';
15→import { checkOperationAccess } from '../policies';
16→
17→const codeLocation = 'handlers/MutationHandler.ts';
18→
19→/**Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts",
"old_string": " public async processRequest(): Promise<CwcApiHandlerResponse> {\n const { context, routeConfig, payload, operationContext } = this.options;\n\n try {\n // Step 1: Check operation-level access (Phase 4 implementation)\n const operationAccess = await this.checkOperationAccess();\n if (!operationAccess.allowed) {\n return createOperationDeniedResponse(this.config, operationAccess.reason);\n }",
"new_string": " public async processRequest(): Promise<CwcApiHandlerResponse> {\n const { context, routeConfig, payload, operationContext } = this.options;\n\n try {\n // Step 1: Check operation-level access (authentication + ownership for project-owner)\n const operationAccess = checkOperationAccess(context, operationContext, routeConfig.requiredRole);\n if (!operationAccess.allowed) {\n return createOperationDeniedResponse(this.config, operationAccess.reason);\n }"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
40→ this.config = config;
41→ this.logger = logger;
42→ }
43→
44→ public async processRequest(): Promise<CwcApiHandlerResponse> {
45→ const { context, routeConfig, payload, operationContext } = this.options;
46→
47→ try {
48→ // Step 1: Check operation-level access (authentication + ownership for project-owner)
49→ const operationAccess = checkOperationAccess(context, operationContext, routeConfig.requiredRole);
50→ if (!operationAccess.allowed) {
51→ return createOperationDeniedResponse(this.config, operationAccess.reason);
52→ }
53→
54→ // Step 2: Execute the mutation
55→ const result = await routeConfig.operation(payload, operationContext);
56→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts",
"old_string": " return createInternalErrorResponse(this.config, error);\n }\n }\n\n /**\n * Check if the current context can perform this operation\n * Phase 3: Stub - always returns allowed\n * Phase 4: Will check ownership, resource access, etc.\n */\n private async checkOperationAccess(): Promise<OperationAccessResult> {\n // Phase 4 will implement actual access policy checks\n return { allowed: true };\n }\n}",
"new_string": " return createInternalErrorResponse(this.config, error);\n }\n }\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
73→ message: `Error executing mutation operation for ${routeConfig.path}`,
74→ error,
75→ });
76→
77→ return createInternalErrorResponse(this.config, error);
78→ }
79→ }
80→}
81→TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "in_progress",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts"
}Tool Result
1→'use strict';
2→
3→import { jest } from '@jest/globals';
4→import type { CwcRole } from 'cwc-types';
5→import type {
6→ CwcApiRouteConfig,
7→ CwcApiHandlerType,
8→ OperationContext,
9→ OperationResult,
10→} from '../../handlers/handler.types';
11→import type { GuestContext, AuthenticatedContext } from '../../context';
12→
13→/**
14→ * Creates a mock route configuration for testing
15→ */
16→export function createMockRouteConfig<TPayload = unknown, TResult = unknown>(
17→ overrides: Partial<CwcApiRouteConfig<TPayload, TResult>> = {}
18→): CwcApiRouteConfig<TPayload, TResult> {
19→ return {
20→ path: '/test',
21→ handlerType: 'query' as CwcApiHandlerType,
22→ allowedRoles: ['guest-user', 'logged-on-user', 'project-owner'] as CwcRole[],
23→ operation: jest.fn<(p: TPayload, c: OperationContext) => Promise<OperationResult<TResult>>>().mockResolvedValue({
24→ success: true,
25→ data: {} as TResult,
26→ }),
27→ ...overrides,
28→ };
29→}
30→
31→/**
32→ * Creates a mock operation context for testing
33→ */
34→export function createMockOperationContext(
35→ overrides: Partial<OperationContext> = {}
36→): OperationContext {
37→ return {
38→ context: createMockGuestContext(),
39→ ...overrides,
40→ };
41→}
42→
43→/**
44→ * Creates a mock guest context for testing
45→ */
46→export function createMockGuestContext(): GuestContext {
47→ return {
48→ isAuthenticated: false,
49→ role: 'guest-user',
50→ };
51→}
52→
53→/**
54→ * Creates a mock authenticated context for testing
55→ */
56→export function createMockAuthenticatedContext(
57→ overrides: Partial<Omit<AuthenticatedContext, 'isAuthenticated'>> = {}
58→): AuthenticatedContext {
59→ return {
60→ isAuthenticated: true,
61→ role: 'logged-on-user',
62→ userPkId: 123,
63→ username: 'testuser@example.com',
64→ ownedProjects: ['project-1', 'project-2'],
65→ payload: {
66→ jti: 'test-jwt-id',
67→ sub: 123,
68→ iat: Math.floor(Date.now() / 1000),
69→ exp: Math.floor(Date.now() / 1000) + 900,
70→ login: {
71→ username: 'testuser@example.com',
72→ deviceId: 'test-device-id',
73→ userJwtId: 'test-jwt-id',
74→ loginType: 'cwc',
75→ kulo: false,
76→ ownedProjects: ['project-1', 'project-2'],
77→ isGuestUser: false,
78→ },
79→ },
80→ ...overrides,
81→ };
82→}
83→
84→/**
85→ * Configures mock operation to return success
86→ */
87→export function mockOperationSuccess<TResult>(
88→ routeConfig: CwcApiRouteConfig<unknown, TResult>,
89→ data: TResult,
90→ pagination?: { page: number; pageSize: number; totalCount: number; hasMore: boolean }
91→): void {
92→ const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;
93→ mockFn.mockResolvedValueOnce({
94→ success: true,
95→ data,
96→ ...(pagination ? { pagination } : {}),
97→ });
98→}
99→
100→/**
101→ * Configures mock operation to return failure
102→ */
103→export function mockOperationFailure(
104→ routeConfig: CwcApiRouteConfig,
105→ errorCode: string = 'NOT_FOUND',
106→ errorMessage: string = 'Resource not found'
107→): void {
108→ const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;
109→ mockFn.mockResolvedValueOnce({
110→ success: false,
111→ errorCode: errorCode as any,
112→ errorMessage,
113→ });
114→}
115→
116→/**
117→ * Configures mock operation to throw an error
118→ */
119→export function mockOperationThrows(
120→ routeConfig: CwcApiRouteConfig,
121→ error: Error = new Error('Unexpected error')
122→): void {
123→ const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;
124→ mockFn.mockRejectedValueOnce(error);
125→}
126→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts",
"old_string": "/**\n * Creates a mock route configuration for testing\n */\nexport function createMockRouteConfig<TPayload = unknown, TResult = unknown>(\n overrides: Partial<CwcApiRouteConfig<TPayload, TResult>> = {}\n): CwcApiRouteConfig<TPayload, TResult> {\n return {\n path: '/test',\n handlerType: 'query' as CwcApiHandlerType,\n allowedRoles: ['guest-user', 'logged-on-user', 'project-owner'] as CwcRole[],\n operation: jest.fn<(p: TPayload, c: OperationContext) => Promise<OperationResult<TResult>>>().mockResolvedValue({\n success: true,\n data: {} as TResult,\n }),\n ...overrides,\n };\n}",
"new_string": "/**\n * Creates a mock route configuration for testing\n *\n * Default requiredRole is 'guest-user' (public access).\n * Override with 'logged-on-user' or 'project-owner' for authenticated routes.\n */\nexport function createMockRouteConfig<TPayload = unknown, TResult = unknown>(\n overrides: Partial<CwcApiRouteConfig<TPayload, TResult>> = {}\n): CwcApiRouteConfig<TPayload, TResult> {\n return {\n path: '/test',\n handlerType: 'query' as CwcApiHandlerType,\n requiredRole: 'guest-user' as CwcRole,\n operation: jest.fn<(p: TPayload, c: OperationContext) => Promise<OperationResult<TResult>>>().mockResolvedValue({\n success: true,\n data: {} as TResult,\n }),\n ...overrides,\n };\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
9→ OperationResult,
10→} from '../../handlers/handler.types';
11→import type { GuestContext, AuthenticatedContext } from '../../context';
12→
13→/**
14→ * Creates a mock route configuration for testing
15→ *
16→ * Default requiredRole is 'guest-user' (public access).
17→ * Override with 'logged-on-user' or 'project-owner' for authenticated routes.
18→ */
19→export function createMockRouteConfig<TPayload = unknown, TResult = unknown>(
20→ overrides: Partial<CwcApiRouteConfig<TPayload, TResult>> = {}
21→): CwcApiRouteConfig<TPayload, TResult> {
22→ return {
23→ path: '/test',
24→ handlerType: 'query' as CwcApiHandlerType,
25→ requiredRole: 'guest-user' as CwcRole,
26→ operation: jest.fn<(p: TPayload, c: OperationContext) => Promise<OperationResult<TResult>>>().mockResolvedValue({
27→ success: true,
28→ data: {} as TResult,
29→ }),
30→ ...overrides,
31→ };
32→}
33→
34→/**
35→ * Creates a mock operation context for testing
36→ */TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "completed",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "in_progress",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "pending",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/policies/checkRouteAccess.test.ts",
"content": "'use strict';\n\nimport { checkRouteAccess } from '../../policies';\nimport { createMockGuestContext, createMockAuthenticatedContext } from '../mocks/routeConfig.mock';\n\ndescribe('checkRouteAccess', () => {\n describe('requiredRole: guest-user (public access)', () => {\n it('should allow guest users', () => {\n const context = createMockGuestContext();\n const result = checkRouteAccess(context, 'guest-user');\n\n expect(result.allowed).toBe(true);\n expect(result.reason).toBeUndefined();\n });\n\n it('should allow authenticated users', () => {\n const context = createMockAuthenticatedContext();\n const result = checkRouteAccess(context, 'guest-user');\n\n expect(result.allowed).toBe(true);\n expect(result.reason).toBeUndefined();\n });\n\n it('should allow project owners', () => {\n const context = createMockAuthenticatedContext({ role: 'project-owner' });\n const result = checkRouteAccess(context, 'guest-user');\n\n expect(result.allowed).toBe(true);\n expect(result.reason).toBeUndefined();\n });\n });\n\n describe('requiredRole: logged-on-user (authenticated access)', () => {\n it('should deny guest users', () => {\n const context = createMockGuestContext();\n const result = checkRouteAccess(context, 'logged-on-user');\n\n expect(result.allowed).toBe(false);\n expect(result.reason).toBe('Authentication required');\n });\n\n it('should allow authenticated users', () => {\n const context = createMockAuthenticatedContext();\n const result = checkRouteAccess(context, 'logged-on-user');\n\n expect(result.allowed).toBe(true);\n expect(result.reason).toBeUndefined();\n });\n\n it('should allow project owners', () => {\n const context = createMockAuthenticatedContext({ role: 'project-owner' });\n const result = checkRouteAccess(context, 'logged-on-user');\n\n expect(result.allowed).toBe(true);\n expect(result.reason).toBeUndefined();\n });\n });\n\n describe('requiredRole: project-owner (owner access)', () => {\n it('should deny guest users', () => {\n const context = createMockGuestContext();\n const result = checkRouteAccess(context, 'project-owner');\n\n expect(result.allowed).toBe(false);\n expect(result.reason).toBe('Authentication required');\n });\n\n it('should allow authenticated users (ownership checked at operation level)', () => {\n const context = createMockAuthenticatedContext();\n const result = checkRouteAccess(context, 'project-owner');\n\n expect(result.allowed).toBe(true);\n expect(result.reason).toBeUndefined();\n });\n\n it('should allow project owners', () => {\n const context = createMockAuthenticatedContext({ role: 'project-owner' });\n const result = checkRouteAccess(context, 'project-owner');\n\n expect(result.allowed).toBe(true);\n expect(result.reason).toBeUndefined();\n });\n });\n});\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/policies/checkRouteAccess.test.ts
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "completed",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "in_progress",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "pending",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/policies/checkOperationAccess.test.ts",
"content": "'use strict';\n\nimport { checkOperationAccess, isProjectOwner } from '../../policies';\nimport {\n createMockGuestContext,\n createMockAuthenticatedContext,\n createMockOperationContext,\n} from '../mocks/routeConfig.mock';\n\ndescribe('isProjectOwner', () => {\n it('should return true when user owns the project', () => {\n const context = createMockAuthenticatedContext({\n ownedProjects: ['project-1', 'project-2'],\n });\n\n expect(isProjectOwner(context, 'project-1')).toBe(true);\n expect(isProjectOwner(context, 'project-2')).toBe(true);\n });\n\n it('should return false when user does not own the project', () => {\n const context = createMockAuthenticatedContext({\n ownedProjects: ['project-1', 'project-2'],\n });\n\n expect(isProjectOwner(context, 'project-3')).toBe(false);\n });\n\n it('should return false for guest context', () => {\n const context = createMockGuestContext();\n\n expect(isProjectOwner(context, 'project-1')).toBe(false);\n });\n\n it('should return false when projectId is undefined', () => {\n const context = createMockAuthenticatedContext({\n ownedProjects: ['project-1'],\n });\n\n expect(isProjectOwner(context, undefined)).toBe(false);\n });\n\n it('should return false when ownedProjects is empty', () => {\n const context = createMockAuthenticatedContext({\n ownedProjects: [],\n });\n\n expect(isProjectOwner(context, 'project-1')).toBe(false);\n });\n});\n\ndescribe('checkOperationAccess', () => {\n describe('requiredRole: guest-user (public access)', () => {\n it('should allow guest users', () => {\n const context = createMockGuestContext();\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'guest-user');\n\n expect(result.allowed).toBe(true);\n expect(result.effectiveRole).toBe('guest-user');\n expect(result.reason).toBeUndefined();\n });\n\n it('should allow authenticated users', () => {\n const context = createMockAuthenticatedContext();\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'guest-user');\n\n expect(result.allowed).toBe(true);\n expect(result.effectiveRole).toBe('logged-on-user');\n expect(result.reason).toBeUndefined();\n });\n\n it('should allow project owners', () => {\n const context = createMockAuthenticatedContext({ role: 'project-owner' });\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'guest-user');\n\n expect(result.allowed).toBe(true);\n expect(result.effectiveRole).toBe('project-owner');\n expect(result.reason).toBeUndefined();\n });\n });\n\n describe('requiredRole: logged-on-user (authenticated access)', () => {\n it('should deny guest users', () => {\n const context = createMockGuestContext();\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'logged-on-user');\n\n expect(result.allowed).toBe(false);\n expect(result.reason).toBe('Authentication required');\n expect(result.effectiveRole).toBeUndefined();\n });\n\n it('should allow authenticated users', () => {\n const context = createMockAuthenticatedContext();\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'logged-on-user');\n\n expect(result.allowed).toBe(true);\n expect(result.effectiveRole).toBe('logged-on-user');\n expect(result.reason).toBeUndefined();\n });\n\n it('should allow project owners', () => {\n const context = createMockAuthenticatedContext({ role: 'project-owner' });\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'logged-on-user');\n\n expect(result.allowed).toBe(true);\n expect(result.effectiveRole).toBe('project-owner');\n expect(result.reason).toBeUndefined();\n });\n });\n\n describe('requiredRole: project-owner (owner access)', () => {\n it('should deny guest users', () => {\n const context = createMockGuestContext();\n const operationContext = createMockOperationContext({\n context,\n projectId: 'project-1',\n });\n const result = checkOperationAccess(context, operationContext, 'project-owner');\n\n expect(result.allowed).toBe(false);\n expect(result.reason).toBe('Authentication required');\n expect(result.effectiveRole).toBeUndefined();\n });\n\n it('should deny authenticated users who do not own the project', () => {\n const context = createMockAuthenticatedContext({\n ownedProjects: ['project-1', 'project-2'],\n });\n const operationContext = createMockOperationContext({\n context,\n projectId: 'project-3',\n });\n const result = checkOperationAccess(context, operationContext, 'project-owner');\n\n expect(result.allowed).toBe(false);\n expect(result.reason).toBe(\"User does not own project 'project-3'\");\n expect(result.effectiveRole).toBeUndefined();\n });\n\n it('should allow authenticated users who own the project', () => {\n const context = createMockAuthenticatedContext({\n ownedProjects: ['project-1', 'project-2'],\n });\n const operationContext = createMockOperationContext({\n context,\n projectId: 'project-1',\n });\n const result = checkOperationAccess(context, operationContext, 'project-owner');\n\n expect(result.allowed).toBe(true);\n expect(result.effectiveRole).toBe('project-owner');\n expect(result.reason).toBeUndefined();\n });\n\n it('should deny when projectId is not provided', () => {\n const context = createMockAuthenticatedContext({\n ownedProjects: ['project-1'],\n });\n const operationContext = createMockOperationContext({\n context,\n projectId: undefined,\n });\n const result = checkOperationAccess(context, operationContext, 'project-owner');\n\n expect(result.allowed).toBe(false);\n expect(result.reason).toBe('Project ID required for ownership check');\n expect(result.effectiveRole).toBeUndefined();\n });\n });\n\n describe('effectiveRole determination', () => {\n it('should return guest-user for guests on public routes', () => {\n const context = createMockGuestContext();\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'guest-user');\n\n expect(result.effectiveRole).toBe('guest-user');\n });\n\n it('should return logged-on-user for authenticated users on public routes', () => {\n const context = createMockAuthenticatedContext({ role: 'logged-on-user' });\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'guest-user');\n\n expect(result.effectiveRole).toBe('logged-on-user');\n });\n\n it('should return logged-on-user for authenticated users on authenticated routes', () => {\n const context = createMockAuthenticatedContext({ role: 'logged-on-user' });\n const operationContext = createMockOperationContext({ context });\n const result = checkOperationAccess(context, operationContext, 'logged-on-user');\n\n expect(result.effectiveRole).toBe('logged-on-user');\n });\n\n it('should return project-owner when user owns the project', () => {\n const context = createMockAuthenticatedContext({\n role: 'logged-on-user',\n ownedProjects: ['project-1'],\n });\n const operationContext = createMockOperationContext({\n context,\n projectId: 'project-1',\n });\n const result = checkOperationAccess(context, operationContext, 'project-owner');\n\n expect(result.effectiveRole).toBe('project-owner');\n });\n });\n});\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/policies/checkOperationAccess.test.ts
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "completed",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "in_progress",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "pending",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts"
}Tool Result
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { RequestHandler } from '../../handlers/RequestHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationFailure,
12→ mockOperationThrows,
13→ mockRenewSessionSuccess,
14→ mockRenewSessionFailure,
15→ getUnitConfig,
16→ createMockDevConfig,
17→ createMockProdConfig,
18→ createMockLogger,
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { RequestHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('RequestHandler', () => {
24→ let mockAuthClient: jest.Mocked<AuthClient>;
25→ const unitConfig = getUnitConfig();
26→ const mockDevConfig = createMockDevConfig();
27→ const mockProdConfig = createMockProdConfig();
28→
29→ beforeEach(() => {
30→ mockAuthClient = createMockAuthClient();
31→ jest.clearAllMocks();
32→ });
33→
34→ describe('Route Access Control', () => {
35→ it('should return 401 for guest user accessing authenticated-only route', async () => {
36→ const routeConfig = createMockRouteConfig({
37→ allowedRoles: ['logged-on-user', 'project-owner'],
38→ });
39→ const context = createMockGuestContext();
40→
41→ const options: RequestHandlerOptions = {
42→ context,
43→ routeConfig,
44→ authHeader: undefined,
45→ payload: {},
46→ };
47→
48→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
49→ const response = await handler.processRequest();
50→
51→ expect(response.statusCode).toBe(401);
52→ expect(response.body.success).toBe(false);
53→ if (!response.body.success) {
54→ expect(response.body.errorCode).toBe('UNAUTHORIZED');
55→ expect(response.body.errorMessage).toBe('Access denied');
56→ }
57→ // No session renewal on auth errors
58→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
59→ });
60→
61→ it('should return 403 for authenticated user accessing owner-only route when not allowed', async () => {
62→ const routeConfig = createMockRouteConfig({
63→ allowedRoles: [], // No roles allowed
64→ });
65→ const context = createMockAuthenticatedContext();
66→
67→ const options: RequestHandlerOptions = {
68→ context,
69→ routeConfig,
70→ authHeader: 'Bearer token',
71→ payload: {},
72→ };
73→
74→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
75→ const response = await handler.processRequest();
76→
77→ expect(response.statusCode).toBe(403);
78→ expect(response.body.success).toBe(false);
79→ if (!response.body.success) {
80→ expect(response.body.errorCode).toBe('FORBIDDEN');
81→ }
82→ // No session renewal on auth errors
83→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
84→ });
85→
86→ it('should allow guest user to access guest-allowed routes', async () => {
87→ const routeConfig = createMockRouteConfig({
88→ allowedRoles: ['guest-user', 'logged-on-user'],
89→ handlerType: 'query',
90→ });
91→ const context = createMockGuestContext();
92→ mockOperationSuccess(routeConfig, { id: 1 });
93→
94→ const options: RequestHandlerOptions = {
95→ context,
96→ routeConfig,
97→ authHeader: undefined,
98→ payload: {},
99→ };
100→
101→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
102→ const response = await handler.processRequest();
103→
104→ expect(response.statusCode).toBe(200);
105→ expect(response.body.success).toBe(true);
106→ // No renewal for guest users
107→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
108→ });
109→
110→ it('should allow authenticated user to access logged-on-user routes', async () => {
111→ const routeConfig = createMockRouteConfig({
112→ allowedRoles: ['logged-on-user'],
113→ handlerType: 'query',
114→ });
115→ const context = createMockAuthenticatedContext();
116→ mockOperationSuccess(routeConfig, { id: 1 });
117→ mockRenewSessionSuccess(mockAuthClient);
118→
119→ const options: RequestHandlerOptions = {
120→ context,
121→ routeConfig,
122→ authHeader: 'Bearer token',
123→ payload: {},
124→ };
125→
126→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
127→ const response = await handler.processRequest();
128→
129→ expect(response.statusCode).toBe(200);
130→ expect(response.body.success).toBe(true);
131→ });
132→
133→ it('should allow logged-on-user to access project-owner routes (ownership checked at operation level)', async () => {
134→ const routeConfig = createMockRouteConfig({
135→ allowedRoles: ['project-owner'],
136→ handlerType: 'mutation',
137→ });
138→ const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
139→ mockOperationSuccess(routeConfig, { id: 1 });
140→ mockRenewSessionSuccess(mockAuthClient);
141→
142→ const options: RequestHandlerOptions = {
143→ context,
144→ routeConfig,
145→ authHeader: 'Bearer token',
146→ payload: {},
147→ };
148→
149→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
150→ const response = await handler.processRequest();
151→
152→ // Route access allows logged-on-user for project-owner routes
153→ // Actual ownership check happens at operation level (Phase 4)
154→ expect(response.statusCode).toBe(200);
155→ expect(response.body.success).toBe(true);
156→ });
157→
158→ it('should include error detail in dev mode for access denied', async () => {
159→ const routeConfig = createMockRouteConfig({
160→ path: '/projects/123',
161→ allowedRoles: ['logged-on-user'],
162→ });
163→ const context = createMockGuestContext();
164→
165→ const options: RequestHandlerOptions = {
166→ context,
167→ routeConfig,
168→ authHeader: undefined,
169→ payload: {},
170→ };
171→
172→ const handler = new RequestHandler(options, mockDevConfig, mockAuthClient, undefined);
173→ const response = await handler.processRequest();
174→
175→ expect(response.body.success).toBe(false);
176→ if (!response.body.success) {
177→ expect(response.body.errorDetail).toContain('guest-user');
178→ expect(response.body.errorDetail).toContain('/projects/123');
179→ }
180→ });
181→
182→ it('should not include error detail in prod mode for access denied', async () => {
183→ const routeConfig = createMockRouteConfig({
184→ allowedRoles: ['logged-on-user'],
185→ });
186→ const context = createMockGuestContext();
187→
188→ const options: RequestHandlerOptions = {
189→ context,
190→ routeConfig,
191→ authHeader: undefined,
192→ payload: {},
193→ };
194→
195→ const handler = new RequestHandler(options, mockProdConfig, mockAuthClient, undefined);
196→ const response = await handler.processRequest();
197→
198→ expect(response.body.success).toBe(false);
199→ if (!response.body.success) {
200→ expect(response.body.errorDetail).toBeUndefined();
201→ }
202→ });
203→ });
204→
205→ describe('Session Renewal', () => {
206→ it('should call renewSession for authenticated users on successful operation', async () => {
207→ const routeConfig = createMockRouteConfig({
208→ handlerType: 'query',
209→ allowedRoles: ['logged-on-user'],
210→ });
211→ const context = createMockAuthenticatedContext();
212→ mockOperationSuccess(routeConfig, { id: 1 });
213→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
214→
215→ const options: RequestHandlerOptions = {
216→ context,
217→ routeConfig,
218→ authHeader: 'Bearer old-token',
219→ payload: {},
220→ };
221→
222→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
223→ const response = await handler.processRequest();
224→
225→ expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
226→ expect(response.statusCode).toBe(200);
227→ expect(response.body.success).toBe(true);
228→ if (response.body.success) {
229→ expect(response.body.jwt).toBe('new-jwt-token');
230→ }
231→ });
232→
233→ it('should call renewSession for authenticated users on failed operation (NOT_FOUND)', async () => {
234→ const routeConfig = createMockRouteConfig({
235→ handlerType: 'query',
236→ allowedRoles: ['logged-on-user'],
237→ });
238→ const context = createMockAuthenticatedContext();
239→ mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
240→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
241→
242→ const options: RequestHandlerOptions = {
243→ context,
244→ routeConfig,
245→ authHeader: 'Bearer old-token',
246→ payload: {},
247→ };
248→
249→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
250→ const response = await handler.processRequest();
251→
252→ // Session renewed even on failed operation (user is still active)
253→ expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
254→ expect(response.statusCode).toBe(404);
255→ expect(response.body.success).toBe(false);
256→ });
257→
258→ it('should call renewSession for authenticated users on VALIDATION_ERROR', async () => {
259→ const routeConfig = createMockRouteConfig({
260→ handlerType: 'mutation',
261→ allowedRoles: ['logged-on-user'],
262→ });
263→ const context = createMockAuthenticatedContext();
264→ mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
265→ mockRenewSessionSuccess(mockAuthClient);
266→
267→ const options: RequestHandlerOptions = {
268→ context,
269→ routeConfig,
270→ authHeader: 'Bearer token',
271→ payload: {},
272→ };
273→
274→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
275→ const response = await handler.processRequest();
276→
277→ // Session renewed even on validation error
278→ expect(mockAuthClient.renewSession).toHaveBeenCalled();
279→ expect(response.statusCode).toBe(400);
280→ });
281→
282→ it('should call renewSession for authenticated users on DATABASE_ERROR', async () => {
283→ const routeConfig = createMockRouteConfig({
284→ handlerType: 'query',
285→ allowedRoles: ['logged-on-user'],
286→ });
287→ const context = createMockAuthenticatedContext();
288→ mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
289→ mockRenewSessionSuccess(mockAuthClient);
290→
291→ const options: RequestHandlerOptions = {
292→ context,
293→ routeConfig,
294→ authHeader: 'Bearer token',
295→ payload: {},
296→ };
297→
298→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
299→ const response = await handler.processRequest();
300→
301→ // Session renewed even on database error
302→ expect(mockAuthClient.renewSession).toHaveBeenCalled();
303→ expect(response.statusCode).toBe(500);
304→ });
305→
306→ it('should NOT call renewSession on 401 UNAUTHORIZED', async () => {
307→ const routeConfig = createMockRouteConfig({
308→ allowedRoles: ['logged-on-user'], // Guest not allowed
309→ });
310→ const context = createMockGuestContext();
311→
312→ const options: RequestHandlerOptions = {
313→ context,
314→ routeConfig,
315→ authHeader: undefined,
316→ payload: {},
317→ };
318→
319→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
320→ const response = await handler.processRequest();
321→
322→ expect(response.statusCode).toBe(401);
323→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
324→ });
325→
326→ it('should NOT call renewSession on 403 FORBIDDEN', async () => {
327→ const routeConfig = createMockRouteConfig({
328→ allowedRoles: [], // No roles allowed
329→ });
330→ const context = createMockAuthenticatedContext();
331→
332→ const options: RequestHandlerOptions = {
333→ context,
334→ routeConfig,
335→ authHeader: 'Bearer token',
336→ payload: {},
337→ };
338→
339→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
340→ const response = await handler.processRequest();
341→
342→ expect(response.statusCode).toBe(403);
343→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
344→ });
345→
346→ it('should NOT call renewSession for guest users', async () => {
347→ const routeConfig = createMockRouteConfig({
348→ allowedRoles: ['guest-user'],
349→ handlerType: 'query',
350→ });
351→ const context = createMockGuestContext();
352→ mockOperationSuccess(routeConfig, { id: 1 });
353→
354→ const options: RequestHandlerOptions = {
355→ context,
356→ routeConfig,
357→ authHeader: undefined,
358→ payload: {},
359→ };
360→
361→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
362→ const response = await handler.processRequest();
363→
364→ expect(response.statusCode).toBe(200);
365→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
366→ if (response.body.success) {
367→ expect(response.body.jwt).toBeUndefined();
368→ }
369→ });
370→
371→ it('should succeed operation when renewal fails (graceful handling)', async () => {
372→ const routeConfig = createMockRouteConfig({
373→ handlerType: 'mutation',
374→ allowedRoles: ['logged-on-user'],
375→ });
376→ const context = createMockAuthenticatedContext();
377→ const mockLogger = createMockLogger();
378→ mockOperationSuccess(routeConfig, { id: 1, mutated: true });
379→ mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
380→
381→ const options: RequestHandlerOptions = {
382→ context,
383→ routeConfig,
384→ authHeader: 'Bearer old-token',
385→ payload: {},
386→ };
387→
388→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
389→ const response = await handler.processRequest();
390→
391→ expect(response.statusCode).toBe(200);
392→ expect(response.body.success).toBe(true);
393→ if (response.body.success) {
394→ expect(response.body.jwt).toBeUndefined(); // No JWT since renewal failed
395→ expect(response.body.data).toEqual({ id: 1, mutated: true });
396→ }
397→ expect(mockLogger.logError).toHaveBeenCalled();
398→ });
399→ });
400→
401→ describe('Handler Delegation', () => {
402→ it('should delegate to QueryHandler for query handlerType', async () => {
403→ const routeConfig = createMockRouteConfig({
404→ handlerType: 'query',
405→ allowedRoles: ['guest-user'],
406→ });
407→ const context = createMockGuestContext();
408→ mockOperationSuccess(routeConfig, { data: 'from query' });
409→
410→ const options: RequestHandlerOptions = {
411→ context,
412→ routeConfig,
413→ authHeader: undefined,
414→ payload: { page: 1 },
415→ };
416→
417→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
418→ const response = await handler.processRequest();
419→
420→ expect(response.statusCode).toBe(200);
421→ expect(response.body.success).toBe(true);
422→ if (response.body.success) {
423→ expect(response.body.data).toEqual({ data: 'from query' });
424→ }
425→ // Query operation was called
426→ expect(routeConfig.operation).toHaveBeenCalledWith(
427→ { page: 1 },
428→ expect.objectContaining({ context })
429→ );
430→ });
431→
432→ it('should delegate to MutationHandler for mutation handlerType', async () => {
433→ const routeConfig = createMockRouteConfig({
434→ handlerType: 'mutation',
435→ allowedRoles: ['logged-on-user'],
436→ });
437→ const context = createMockAuthenticatedContext();
438→ mockOperationSuccess(routeConfig, { id: 123, created: true });
439→ mockRenewSessionSuccess(mockAuthClient);
440→
441→ const options: RequestHandlerOptions = {
442→ context,
443→ routeConfig,
444→ authHeader: 'Bearer token',
445→ payload: { name: 'New Project' },
446→ };
447→
448→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
449→ const response = await handler.processRequest();
450→
451→ expect(response.statusCode).toBe(200);
452→ expect(response.body.success).toBe(true);
453→ if (response.body.success) {
454→ expect(response.body.data).toEqual({ id: 123, created: true });
455→ expect(response.body.jwt).toBeDefined(); // Session renewed
456→ }
457→ });
458→ });
459→
460→ describe('Operation Context', () => {
461→ it('should pass projectId from path params to operation context', async () => {
462→ const routeConfig = createMockRouteConfig({
463→ handlerType: 'query',
464→ allowedRoles: ['guest-user'],
465→ });
466→ const context = createMockGuestContext();
467→ mockOperationSuccess(routeConfig, { id: 1 });
468→
469→ const options: RequestHandlerOptions = {
470→ context,
471→ routeConfig,
472→ authHeader: undefined,
473→ payload: {},
474→ pathParams: { projectId: 'my-project-123' },
475→ };
476→
477→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
478→ await handler.processRequest();
479→
480→ expect(routeConfig.operation).toHaveBeenCalledWith(
481→ {},
482→ expect.objectContaining({
483→ projectId: 'my-project-123',
484→ })
485→ );
486→ });
487→
488→ it('should pass resourceId from path params to operation context', async () => {
489→ const routeConfig = createMockRouteConfig({
490→ handlerType: 'query',
491→ allowedRoles: ['guest-user'],
492→ });
493→ const context = createMockGuestContext();
494→ mockOperationSuccess(routeConfig, { id: 1 });
495→
496→ const options: RequestHandlerOptions = {
497→ context,
498→ routeConfig,
499→ authHeader: undefined,
500→ payload: {},
501→ pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },
502→ };
503→
504→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
505→ await handler.processRequest();
506→
507→ expect(routeConfig.operation).toHaveBeenCalledWith(
508→ {},
509→ expect.objectContaining({
510→ projectId: 'proj-1',
511→ resourceId: 'resource-456',
512→ })
513→ );
514→ });
515→
516→ it('should handle missing path params gracefully', async () => {
517→ const routeConfig = createMockRouteConfig({
518→ handlerType: 'query',
519→ allowedRoles: ['guest-user'],
520→ });
521→ const context = createMockGuestContext();
522→ mockOperationSuccess(routeConfig, { id: 1 });
523→
524→ const options: RequestHandlerOptions = {
525→ context,
526→ routeConfig,
527→ authHeader: undefined,
528→ payload: {},
529→ // No pathParams
530→ };
531→
532→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
533→ await handler.processRequest();
534→
535→ expect(routeConfig.operation).toHaveBeenCalledWith(
536→ {},
537→ expect.objectContaining({
538→ projectId: undefined,
539→ resourceId: undefined,
540→ })
541→ );
542→ });
543→ });
544→
545→ describe('Error Handling', () => {
546→ it('should catch errors and return 500 response', async () => {
547→ const routeConfig = createMockRouteConfig({
548→ handlerType: 'query',
549→ allowedRoles: ['guest-user'],
550→ });
551→ const context = createMockGuestContext();
552→ mockOperationThrows(routeConfig, new Error('Unexpected error'));
553→
554→ const options: RequestHandlerOptions = {
555→ context,
556→ routeConfig,
557→ authHeader: undefined,
558→ payload: {},
559→ };
560→
561→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
562→ const response = await handler.processRequest();
563→
564→ expect(response.statusCode).toBe(500);
565→ expect(response.body.success).toBe(false);
566→ if (!response.body.success) {
567→ expect(response.body.errorCode).toBe('INTERNAL_ERROR');
568→ }
569→ });
570→
571→ it('should log errors when logger is provided', async () => {
572→ const routeConfig = createMockRouteConfig({
573→ handlerType: 'query',
574→ allowedRoles: ['guest-user'],
575→ });
576→ const context = createMockGuestContext();
577→ const mockLogger = createMockLogger();
578→ mockOperationThrows(routeConfig, new Error('Test error'));
579→
580→ const options: RequestHandlerOptions = {
581→ context,
582→ routeConfig,
583→ authHeader: undefined,
584→ payload: {},
585→ };
586→
587→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
588→ await handler.processRequest();
589→
590→ expect(mockLogger.logError).toHaveBeenCalled();
591→ });
592→ });
593→});
594→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
I need to update the RequestHandler tests. The tests use allowedRoles which needs to be replaced with requiredRole. The test for allowedRoles: [] no longer applies since requiredRole is always required. Let me update the tests.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts",
"old_string": " describe('Route Access Control', () => {\n it('should return 401 for guest user accessing authenticated-only route', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: ['logged-on-user', 'project-owner'],\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(401);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('UNAUTHORIZED');\n expect(response.body.errorMessage).toBe('Access denied');\n }\n // No session renewal on auth errors\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should return 403 for authenticated user accessing owner-only route when not allowed', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: [], // No roles allowed\n });\n const context = createMockAuthenticatedContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(403);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('FORBIDDEN');\n }\n // No session renewal on auth errors\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should allow guest user to access guest-allowed routes', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: ['guest-user', 'logged-on-user'],\n handlerType: 'query',\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n // No renewal for guest users\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should allow authenticated user to access logged-on-user routes', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: ['logged-on-user'],\n handlerType: 'query',\n });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n\n it('should allow logged-on-user to access project-owner routes (ownership checked at operation level)', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: ['project-owner'],\n handlerType: 'mutation',\n });\n const context = createMockAuthenticatedContext({ role: 'logged-on-user' });\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // Route access allows logged-on-user for project-owner routes\n // Actual ownership check happens at operation level (Phase 4)\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n\n it('should include error detail in dev mode for access denied', async () => {\n const routeConfig = createMockRouteConfig({\n path: '/projects/123',\n allowedRoles: ['logged-on-user'],\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, mockDevConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorDetail).toContain('guest-user');\n expect(response.body.errorDetail).toContain('/projects/123');\n }\n });\n\n it('should not include error detail in prod mode for access denied', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: ['logged-on-user'],\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, mockProdConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorDetail).toBeUndefined();\n }\n });\n });",
"new_string": " describe('Route Access Control', () => {\n it('should return 401 for guest user accessing authenticated-only route', async () => {\n const routeConfig = createMockRouteConfig({\n requiredRole: 'logged-on-user',\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(401);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('UNAUTHORIZED');\n expect(response.body.errorMessage).toBe('Access denied');\n }\n // No session renewal on auth errors\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should return 401 for guest user accessing project-owner route', async () => {\n const routeConfig = createMockRouteConfig({\n requiredRole: 'project-owner',\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(401);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('UNAUTHORIZED');\n }\n // No session renewal on auth errors\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should allow guest user to access guest-user routes', async () => {\n const routeConfig = createMockRouteConfig({\n requiredRole: 'guest-user',\n handlerType: 'query',\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n // No renewal for guest users\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should allow authenticated user to access logged-on-user routes', async () => {\n const routeConfig = createMockRouteConfig({\n requiredRole: 'logged-on-user',\n handlerType: 'query',\n });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n\n it('should allow logged-on-user to access project-owner routes at route level (ownership checked at operation level)', async () => {\n const routeConfig = createMockRouteConfig({\n requiredRole: 'project-owner',\n handlerType: 'mutation',\n });\n // Authenticated user with owned projects (will pass operation check)\n const context = createMockAuthenticatedContext({\n role: 'logged-on-user',\n ownedProjects: ['test-project'],\n });\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n pathParams: { projectId: 'test-project' },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // Route access allows authenticated users for project-owner routes\n // Ownership is verified at operation level\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n\n it('should include error detail in dev mode for access denied', async () => {\n const routeConfig = createMockRouteConfig({\n path: '/projects/123',\n requiredRole: 'logged-on-user',\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, mockDevConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorDetail).toBe('Authentication required');\n }\n });\n\n it('should not include error detail in prod mode for access denied', async () => {\n const routeConfig = createMockRouteConfig({\n requiredRole: 'logged-on-user',\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, mockProdConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorDetail).toBeUndefined();\n }\n });\n });"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
30→ mockAuthClient = createMockAuthClient();
31→ jest.clearAllMocks();
32→ });
33→
34→ describe('Route Access Control', () => {
35→ it('should return 401 for guest user accessing authenticated-only route', async () => {
36→ const routeConfig = createMockRouteConfig({
37→ requiredRole: 'logged-on-user',
38→ });
39→ const context = createMockGuestContext();
40→
41→ const options: RequestHandlerOptions = {
42→ context,
43→ routeConfig,
44→ authHeader: undefined,
45→ payload: {},
46→ };
47→
48→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
49→ const response = await handler.processRequest();
50→
51→ expect(response.statusCode).toBe(401);
52→ expect(response.body.success).toBe(false);
53→ if (!response.body.success) {
54→ expect(response.body.errorCode).toBe('UNAUTHORIZED');
55→ expect(response.body.errorMessage).toBe('Access denied');
56→ }
57→ // No session renewal on auth errors
58→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
59→ });
60→
61→ it('should return 401 for guest user accessing project-owner route', async () => {
62→ const routeConfig = createMockRouteConfig({
63→ requiredRole: 'project-owner',
64→ });
65→ const context = createMockGuestContext();
66→
67→ const options: RequestHandlerOptions = {
68→ context,
69→ routeConfig,
70→ authHeader: undefined,
71→ payload: {},
72→ };
73→
74→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
75→ const response = await handler.processRequest();
76→
77→ expect(response.statusCode).toBe(401);
78→ expect(response.body.success).toBe(false);
79→ if (!response.body.success) {
80→ expect(response.body.errorCode).toBe('UNAUTHORIZED');
81→ }
82→ // No session renewal on auth errors
83→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
84→ });
85→
86→ it('should allow guest user to access guest-user routes', async () => {
87→ const routeConfig = createMockRouteConfig({
88→ requiredRole: 'guest-user',
89→ handlerType: 'query',
90→ });
91→ const context = createMockGuestContext();
92→ mockOperationSuccess(routeConfig, { id: 1 });
93→
94→ const options: RequestHandlerOptions = {
95→ context,
96→ routeConfig,
97→ authHeader: undefined,
98→ payload: {},
99→ };
100→
101→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
102→ const response = await handler.processRequest();
103→
104→ expect(response.statusCode).toBe(200);
105→ expect(response.body.success).toBe(true);
106→ // No renewal for guest users
107→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
108→ });
109→
110→ it('should allow authenticated user to access logged-on-user routes', async () => {
111→ const routeConfig = createMockRouteConfig({
112→ requiredRole: 'logged-on-user',
113→ handlerType: 'query',
114→ });
115→ const context = createMockAuthenticatedContext();
116→ mockOperationSuccess(routeConfig, { id: 1 });
117→ mockRenewSessionSuccess(mockAuthClient);
118→
119→ const options: RequestHandlerOptions = {
120→ context,
121→ routeConfig,
122→ authHeader: 'Bearer token',
123→ payload: {},
124→ };
125→
126→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
127→ const response = await handler.processRequest();
128→
129→ expect(response.statusCode).toBe(200);
130→ expect(response.body.success).toBe(true);
131→ });
132→
133→ it('should allow logged-on-user to access project-owner routes at route level (ownership checked at operation level)', async () => {
134→ const routeConfig = createMockRouteConfig({
135→ requiredRole: 'project-owner',
136→ handlerType: 'mutation',
137→ });
138→ // Authenticated user with owned projects (will pass operation check)
139→ const context = createMockAuthenticatedContext({
140→ role: 'logged-on-user',
141→ ownedProjects: ['test-project'],
142→ });
143→ mockOperationSuccess(routeConfig, { id: 1 });
144→ mockRenewSessionSuccess(mockAuthClient);
145→
146→ const options: RequestHandlerOptions = {
147→ context,
148→ routeConfig,
149→ authHeader: 'Bearer token',
150→ payload: {},
151→ pathParams: { projectId: 'test-project' },
152→ };
153→
154→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
155→ const response = await handler.processRequest();
156→
157→ // Route access allows authenticated users for project-owner routes
158→ // Ownership is verified at operation level
159→ expect(response.statusCode).toBe(200);
160→ expect(response.body.success).toBe(true);
161→ });
162→
163→ it('should include error detail in dev mode for access denied', async () => {
164→ const routeConfig = createMockRouteConfig({
165→ path: '/projects/123',
166→ requiredRole: 'logged-on-user',
167→ });
168→ const context = createMockGuestContext();
169→
170→ const options: RequestHandlerOptions = {
171→ context,
172→ routeConfig,
173→ authHeader: undefined,
174→ payload: {},
175→ };
176→
177→ const handler = new RequestHandler(options, mockDevConfig, mockAuthClient, undefined);
178→ const response = await handler.processRequest();
179→
180→ expect(response.body.success).toBe(false);
181→ if (!response.body.success) {
182→ expect(response.body.errorDetail).toBe('Authentication required');
183→ }
184→ });
185→
186→ it('should not include error detail in prod mode for access denied', async () => {
187→ const routeConfig = createMockRouteConfig({
188→ requiredRole: 'logged-on-user',
189→ });
190→ const context = createMockGuestContext();
191→
192→ const options: RequestHandlerOptions = {
193→ context,
194→ routeConfig,
195→ authHeader: undefined,
196→ payload: {},
197→ };
198→
199→ const handler = new RequestHandler(options, mockProdConfig, mockAuthClient, undefined);
200→ const response = await handler.processRequest();
201→
202→ expect(response.body.success).toBe(false);
203→ if (!response.body.success) {
204→ expect(response.body.errorDetail).toBeUndefined();
205→ }
206→ });
207→ });
208→
209→ describe('Session Renewal', () => {
210→ it('should call renewSession for authenticated users on successful operation', async () => {
211→ const routeConfig = createMockRouteConfig({Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts",
"old_string": " describe('Session Renewal', () => {\n it('should call renewSession for authenticated users on successful operation', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['logged-on-user'],\n });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.jwt).toBe('new-jwt-token');\n }\n });\n\n it('should call renewSession for authenticated users on failed operation (NOT_FOUND)', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['logged-on-user'],\n });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // Session renewed even on failed operation (user is still active)\n expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');\n expect(response.statusCode).toBe(404);\n expect(response.body.success).toBe(false);\n });\n\n it('should call renewSession for authenticated users on VALIDATION_ERROR', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n allowedRoles: ['logged-on-user'],\n });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // Session renewed even on validation error\n expect(mockAuthClient.renewSession).toHaveBeenCalled();\n expect(response.statusCode).toBe(400);\n });\n\n it('should call renewSession for authenticated users on DATABASE_ERROR', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['logged-on-user'],\n });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // Session renewed even on database error\n expect(mockAuthClient.renewSession).toHaveBeenCalled();\n expect(response.statusCode).toBe(500);\n });\n\n it('should NOT call renewSession on 401 UNAUTHORIZED', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: ['logged-on-user'], // Guest not allowed\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(401);\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should NOT call renewSession on 403 FORBIDDEN', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: [], // No roles allowed\n });\n const context = createMockAuthenticatedContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(403);\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should NOT call renewSession for guest users', async () => {\n const routeConfig = createMockRouteConfig({\n allowedRoles: ['guest-user'],\n handlerType: 'query',\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n if (response.body.success) {\n expect(response.body.jwt).toBeUndefined();\n }\n });\n\n it('should succeed operation when renewal fails (graceful handling)', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n allowedRoles: ['logged-on-user'],\n });\n const context = createMockAuthenticatedContext();\n const mockLogger = createMockLogger();\n mockOperationSuccess(routeConfig, { id: 1, mutated: true });\n mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.jwt).toBeUndefined(); // No JWT since renewal failed\n expect(response.body.data).toEqual({ id: 1, mutated: true });\n }\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n });",
"new_string": " describe('Session Renewal', () => {\n it('should call renewSession for authenticated users on successful operation', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'logged-on-user',\n });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.jwt).toBe('new-jwt-token');\n }\n });\n\n it('should call renewSession for authenticated users on failed operation (NOT_FOUND)', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'logged-on-user',\n });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // Session renewed even on failed operation (user is still active)\n expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');\n expect(response.statusCode).toBe(404);\n expect(response.body.success).toBe(false);\n });\n\n it('should call renewSession for authenticated users on VALIDATION_ERROR', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n requiredRole: 'logged-on-user',\n });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // Session renewed even on validation error\n expect(mockAuthClient.renewSession).toHaveBeenCalled();\n expect(response.statusCode).toBe(400);\n });\n\n it('should call renewSession for authenticated users on DATABASE_ERROR', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'logged-on-user',\n });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // Session renewed even on database error\n expect(mockAuthClient.renewSession).toHaveBeenCalled();\n expect(response.statusCode).toBe(500);\n });\n\n it('should NOT call renewSession on 401 UNAUTHORIZED', async () => {\n const routeConfig = createMockRouteConfig({\n requiredRole: 'logged-on-user', // Guest not allowed\n });\n const context = createMockGuestContext();\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(401);\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should NOT call renewSession on 403 OPERATION_ACCESS_DENIED', async () => {\n // Test 403 from operation-level access denial (user doesn't own project)\n const routeConfig = createMockRouteConfig({\n requiredRole: 'project-owner',\n handlerType: 'mutation',\n });\n const context = createMockAuthenticatedContext({\n ownedProjects: ['other-project'], // Doesn't own the target project\n });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n pathParams: { projectId: 'not-owned-project' },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(403);\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n });\n\n it('should NOT call renewSession for guest users', async () => {\n const routeConfig = createMockRouteConfig({\n requiredRole: 'guest-user',\n handlerType: 'query',\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n if (response.body.success) {\n expect(response.body.jwt).toBeUndefined();\n }\n });\n\n it('should succeed operation when renewal fails (graceful handling)', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n requiredRole: 'logged-on-user',\n });\n const context = createMockAuthenticatedContext();\n const mockLogger = createMockLogger();\n mockOperationSuccess(routeConfig, { id: 1, mutated: true });\n mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.jwt).toBeUndefined(); // No JWT since renewal failed\n expect(response.body.data).toEqual({ id: 1, mutated: true });\n }\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n });"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
205→ }
206→ });
207→ });
208→
209→ describe('Session Renewal', () => {
210→ it('should call renewSession for authenticated users on successful operation', async () => {
211→ const routeConfig = createMockRouteConfig({
212→ handlerType: 'query',
213→ requiredRole: 'logged-on-user',
214→ });
215→ const context = createMockAuthenticatedContext();
216→ mockOperationSuccess(routeConfig, { id: 1 });
217→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
218→
219→ const options: RequestHandlerOptions = {
220→ context,
221→ routeConfig,
222→ authHeader: 'Bearer old-token',
223→ payload: {},
224→ };
225→
226→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
227→ const response = await handler.processRequest();
228→
229→ expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
230→ expect(response.statusCode).toBe(200);
231→ expect(response.body.success).toBe(true);
232→ if (response.body.success) {
233→ expect(response.body.jwt).toBe('new-jwt-token');
234→ }
235→ });
236→
237→ it('should call renewSession for authenticated users on failed operation (NOT_FOUND)', async () => {
238→ const routeConfig = createMockRouteConfig({
239→ handlerType: 'query',
240→ requiredRole: 'logged-on-user',
241→ });
242→ const context = createMockAuthenticatedContext();
243→ mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
244→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
245→
246→ const options: RequestHandlerOptions = {
247→ context,
248→ routeConfig,
249→ authHeader: 'Bearer old-token',
250→ payload: {},
251→ };
252→
253→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
254→ const response = await handler.processRequest();
255→
256→ // Session renewed even on failed operation (user is still active)
257→ expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
258→ expect(response.statusCode).toBe(404);
259→ expect(response.body.success).toBe(false);
260→ });
261→
262→ it('should call renewSession for authenticated users on VALIDATION_ERROR', async () => {
263→ const routeConfig = createMockRouteConfig({
264→ handlerType: 'mutation',
265→ requiredRole: 'logged-on-user',
266→ });
267→ const context = createMockAuthenticatedContext();
268→ mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
269→ mockRenewSessionSuccess(mockAuthClient);
270→
271→ const options: RequestHandlerOptions = {
272→ context,
273→ routeConfig,
274→ authHeader: 'Bearer token',
275→ payload: {},
276→ };
277→
278→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
279→ const response = await handler.processRequest();
280→
281→ // Session renewed even on validation error
282→ expect(mockAuthClient.renewSession).toHaveBeenCalled();
283→ expect(response.statusCode).toBe(400);
284→ });
285→
286→ it('should call renewSession for authenticated users on DATABASE_ERROR', async () => {
287→ const routeConfig = createMockRouteConfig({
288→ handlerType: 'query',
289→ requiredRole: 'logged-on-user',
290→ });
291→ const context = createMockAuthenticatedContext();
292→ mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
293→ mockRenewSessionSuccess(mockAuthClient);
294→
295→ const options: RequestHandlerOptions = {
296→ context,
297→ routeConfig,
298→ authHeader: 'Bearer token',
299→ payload: {},
300→ };
301→
302→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
303→ const response = await handler.processRequest();
304→
305→ // Session renewed even on database error
306→ expect(mockAuthClient.renewSession).toHaveBeenCalled();
307→ expect(response.statusCode).toBe(500);
308→ });
309→
310→ it('should NOT call renewSession on 401 UNAUTHORIZED', async () => {
311→ const routeConfig = createMockRouteConfig({
312→ requiredRole: 'logged-on-user', // Guest not allowed
313→ });
314→ const context = createMockGuestContext();
315→
316→ const options: RequestHandlerOptions = {
317→ context,
318→ routeConfig,
319→ authHeader: undefined,
320→ payload: {},
321→ };
322→
323→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
324→ const response = await handler.processRequest();
325→
326→ expect(response.statusCode).toBe(401);
327→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
328→ });
329→
330→ it('should NOT call renewSession on 403 OPERATION_ACCESS_DENIED', async () => {
331→ // Test 403 from operation-level access denial (user doesn't own project)
332→ const routeConfig = createMockRouteConfig({
333→ requiredRole: 'project-owner',
334→ handlerType: 'mutation',
335→ });
336→ const context = createMockAuthenticatedContext({
337→ ownedProjects: ['other-project'], // Doesn't own the target project
338→ });
339→
340→ const options: RequestHandlerOptions = {
341→ context,
342→ routeConfig,
343→ authHeader: 'Bearer token',
344→ payload: {},
345→ pathParams: { projectId: 'not-owned-project' },
346→ };
347→
348→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
349→ const response = await handler.processRequest();
350→
351→ expect(response.statusCode).toBe(403);
352→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
353→ });
354→
355→ it('should NOT call renewSession for guest users', async () => {
356→ const routeConfig = createMockRouteConfig({
357→ requiredRole: 'guest-user',
358→ handlerType: 'query',
359→ });
360→ const context = createMockGuestContext();
361→ mockOperationSuccess(routeConfig, { id: 1 });
362→
363→ const options: RequestHandlerOptions = {
364→ context,
365→ routeConfig,
366→ authHeader: undefined,
367→ payload: {},
368→ };
369→
370→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
371→ const response = await handler.processRequest();
372→
373→ expect(response.statusCode).toBe(200);
374→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
375→ if (response.body.success) {
376→ expect(response.body.jwt).toBeUndefined();
377→ }
378→ });
379→
380→ it('should succeed operation when renewal fails (graceful handling)', async () => {
381→ const routeConfig = createMockRouteConfig({
382→ handlerType: 'mutation',
383→ requiredRole: 'logged-on-user',
384→ });
385→ const context = createMockAuthenticatedContext();
386→ const mockLogger = createMockLogger();
387→ mockOperationSuccess(routeConfig, { id: 1, mutated: true });
388→ mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
389→
390→ const options: RequestHandlerOptions = {
391→ context,
392→ routeConfig,
393→ authHeader: 'Bearer old-token',
394→ payload: {},
395→ };
396→
397→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
398→ const response = await handler.processRequest();
399→
400→ expect(response.statusCode).toBe(200);
401→ expect(response.body.success).toBe(true);
402→ if (response.body.success) {
403→ expect(response.body.jwt).toBeUndefined(); // No JWT since renewal failed
404→ expect(response.body.data).toEqual({ id: 1, mutated: true });
405→ }
406→ expect(mockLogger.logError).toHaveBeenCalled();
407→ });
408→ });
409→
410→ describe('Handler Delegation', () => {
411→ it('should delegate to QueryHandler for query handlerType', async () => {
412→ const routeConfig = createMockRouteConfig({Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts",
"old_string": " describe('Handler Delegation', () => {\n it('should delegate to QueryHandler for query handlerType', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['guest-user'],\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { data: 'from query' });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: { page: 1 },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.data).toEqual({ data: 'from query' });\n }\n // Query operation was called\n expect(routeConfig.operation).toHaveBeenCalledWith(\n { page: 1 },\n expect.objectContaining({ context })\n );\n });\n\n it('should delegate to MutationHandler for mutation handlerType', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n allowedRoles: ['logged-on-user'],\n });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 123, created: true });\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: { name: 'New Project' },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.data).toEqual({ id: 123, created: true });\n expect(response.body.jwt).toBeDefined(); // Session renewed\n }\n });\n });\n\n describe('Operation Context', () => {\n it('should pass projectId from path params to operation context', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['guest-user'],\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n pathParams: { projectId: 'my-project-123' },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n await handler.processRequest();\n\n expect(routeConfig.operation).toHaveBeenCalledWith(\n {},\n expect.objectContaining({\n projectId: 'my-project-123',\n })\n );\n });\n\n it('should pass resourceId from path params to operation context', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['guest-user'],\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n await handler.processRequest();\n\n expect(routeConfig.operation).toHaveBeenCalledWith(\n {},\n expect.objectContaining({\n projectId: 'proj-1',\n resourceId: 'resource-456',\n })\n );\n });\n\n it('should handle missing path params gracefully', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['guest-user'],\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n // No pathParams\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n await handler.processRequest();\n\n expect(routeConfig.operation).toHaveBeenCalledWith(\n {},\n expect.objectContaining({\n projectId: undefined,\n resourceId: undefined,\n })\n );\n });\n });\n\n describe('Error Handling', () => {\n it('should catch errors and return 500 response', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['guest-user'],\n });\n const context = createMockGuestContext();\n mockOperationThrows(routeConfig, new Error('Unexpected error'));\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(500);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('INTERNAL_ERROR');\n }\n });\n\n it('should log errors when logger is provided', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n allowedRoles: ['guest-user'],\n });\n const context = createMockGuestContext();\n const mockLogger = createMockLogger();\n mockOperationThrows(routeConfig, new Error('Test error'));\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);\n await handler.processRequest();\n\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n });\n});",
"new_string": " describe('Handler Delegation', () => {\n it('should delegate to QueryHandler for query handlerType', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'guest-user',\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { data: 'from query' });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: { page: 1 },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.data).toEqual({ data: 'from query' });\n }\n // Query operation was called\n expect(routeConfig.operation).toHaveBeenCalledWith(\n { page: 1 },\n expect.objectContaining({ context })\n );\n });\n\n it('should delegate to MutationHandler for mutation handlerType', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n requiredRole: 'logged-on-user',\n });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 123, created: true });\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: { name: 'New Project' },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.data).toEqual({ id: 123, created: true });\n expect(response.body.jwt).toBeDefined(); // Session renewed\n }\n });\n });\n\n describe('Operation Context', () => {\n it('should pass projectId from path params to operation context', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'guest-user',\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n pathParams: { projectId: 'my-project-123' },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n await handler.processRequest();\n\n expect(routeConfig.operation).toHaveBeenCalledWith(\n {},\n expect.objectContaining({\n projectId: 'my-project-123',\n })\n );\n });\n\n it('should pass resourceId from path params to operation context', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'guest-user',\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n await handler.processRequest();\n\n expect(routeConfig.operation).toHaveBeenCalledWith(\n {},\n expect.objectContaining({\n projectId: 'proj-1',\n resourceId: 'resource-456',\n })\n );\n });\n\n it('should handle missing path params gracefully', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'guest-user',\n });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n // No pathParams\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n await handler.processRequest();\n\n expect(routeConfig.operation).toHaveBeenCalledWith(\n {},\n expect.objectContaining({\n projectId: undefined,\n resourceId: undefined,\n })\n );\n });\n });\n\n describe('Error Handling', () => {\n it('should catch errors and return 500 response', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'guest-user',\n });\n const context = createMockGuestContext();\n mockOperationThrows(routeConfig, new Error('Unexpected error'));\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(500);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('INTERNAL_ERROR');\n }\n });\n\n it('should log errors when logger is provided', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'query',\n requiredRole: 'guest-user',\n });\n const context = createMockGuestContext();\n const mockLogger = createMockLogger();\n mockOperationThrows(routeConfig, new Error('Test error'));\n\n const options: RequestHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);\n await handler.processRequest();\n\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n });\n});"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
406→ expect(mockLogger.logError).toHaveBeenCalled();
407→ });
408→ });
409→
410→ describe('Handler Delegation', () => {
411→ it('should delegate to QueryHandler for query handlerType', async () => {
412→ const routeConfig = createMockRouteConfig({
413→ handlerType: 'query',
414→ requiredRole: 'guest-user',
415→ });
416→ const context = createMockGuestContext();
417→ mockOperationSuccess(routeConfig, { data: 'from query' });
418→
419→ const options: RequestHandlerOptions = {
420→ context,
421→ routeConfig,
422→ authHeader: undefined,
423→ payload: { page: 1 },
424→ };
425→
426→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
427→ const response = await handler.processRequest();
428→
429→ expect(response.statusCode).toBe(200);
430→ expect(response.body.success).toBe(true);
431→ if (response.body.success) {
432→ expect(response.body.data).toEqual({ data: 'from query' });
433→ }
434→ // Query operation was called
435→ expect(routeConfig.operation).toHaveBeenCalledWith(
436→ { page: 1 },
437→ expect.objectContaining({ context })
438→ );
439→ });
440→
441→ it('should delegate to MutationHandler for mutation handlerType', async () => {
442→ const routeConfig = createMockRouteConfig({
443→ handlerType: 'mutation',
444→ requiredRole: 'logged-on-user',
445→ });
446→ const context = createMockAuthenticatedContext();
447→ mockOperationSuccess(routeConfig, { id: 123, created: true });
448→ mockRenewSessionSuccess(mockAuthClient);
449→
450→ const options: RequestHandlerOptions = {
451→ context,
452→ routeConfig,
453→ authHeader: 'Bearer token',
454→ payload: { name: 'New Project' },
455→ };
456→
457→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
458→ const response = await handler.processRequest();
459→
460→ expect(response.statusCode).toBe(200);
461→ expect(response.body.success).toBe(true);
462→ if (response.body.success) {
463→ expect(response.body.data).toEqual({ id: 123, created: true });
464→ expect(response.body.jwt).toBeDefined(); // Session renewed
465→ }
466→ });
467→ });
468→
469→ describe('Operation Context', () => {
470→ it('should pass projectId from path params to operation context', async () => {
471→ const routeConfig = createMockRouteConfig({
472→ handlerType: 'query',
473→ requiredRole: 'guest-user',
474→ });
475→ const context = createMockGuestContext();
476→ mockOperationSuccess(routeConfig, { id: 1 });
477→
478→ const options: RequestHandlerOptions = {
479→ context,
480→ routeConfig,
481→ authHeader: undefined,
482→ payload: {},
483→ pathParams: { projectId: 'my-project-123' },
484→ };
485→
486→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
487→ await handler.processRequest();
488→
489→ expect(routeConfig.operation).toHaveBeenCalledWith(
490→ {},
491→ expect.objectContaining({
492→ projectId: 'my-project-123',
493→ })
494→ );
495→ });
496→
497→ it('should pass resourceId from path params to operation context', async () => {
498→ const routeConfig = createMockRouteConfig({
499→ handlerType: 'query',
500→ requiredRole: 'guest-user',
501→ });
502→ const context = createMockGuestContext();
503→ mockOperationSuccess(routeConfig, { id: 1 });
504→
505→ const options: RequestHandlerOptions = {
506→ context,
507→ routeConfig,
508→ authHeader: undefined,
509→ payload: {},
510→ pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },
511→ };
512→
513→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
514→ await handler.processRequest();
515→
516→ expect(routeConfig.operation).toHaveBeenCalledWith(
517→ {},
518→ expect.objectContaining({
519→ projectId: 'proj-1',
520→ resourceId: 'resource-456',
521→ })
522→ );
523→ });
524→
525→ it('should handle missing path params gracefully', async () => {
526→ const routeConfig = createMockRouteConfig({
527→ handlerType: 'query',
528→ requiredRole: 'guest-user',
529→ });
530→ const context = createMockGuestContext();
531→ mockOperationSuccess(routeConfig, { id: 1 });
532→
533→ const options: RequestHandlerOptions = {
534→ context,
535→ routeConfig,
536→ authHeader: undefined,
537→ payload: {},
538→ // No pathParams
539→ };
540→
541→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
542→ await handler.processRequest();
543→
544→ expect(routeConfig.operation).toHaveBeenCalledWith(
545→ {},
546→ expect.objectContaining({
547→ projectId: undefined,
548→ resourceId: undefined,
549→ })
550→ );
551→ });
552→ });
553→
554→ describe('Error Handling', () => {
555→ it('should catch errors and return 500 response', async () => {
556→ const routeConfig = createMockRouteConfig({
557→ handlerType: 'query',
558→ requiredRole: 'guest-user',
559→ });
560→ const context = createMockGuestContext();
561→ mockOperationThrows(routeConfig, new Error('Unexpected error'));
562→
563→ const options: RequestHandlerOptions = {
564→ context,
565→ routeConfig,
566→ authHeader: undefined,
567→ payload: {},
568→ };
569→
570→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
571→ const response = await handler.processRequest();
572→
573→ expect(response.statusCode).toBe(500);
574→ expect(response.body.success).toBe(false);
575→ if (!response.body.success) {
576→ expect(response.body.errorCode).toBe('INTERNAL_ERROR');
577→ }
578→ });
579→
580→ it('should log errors when logger is provided', async () => {
581→ const routeConfig = createMockRouteConfig({
582→ handlerType: 'query',
583→ requiredRole: 'guest-user',
584→ });
585→ const context = createMockGuestContext();
586→ const mockLogger = createMockLogger();
587→ mockOperationThrows(routeConfig, new Error('Test error'));
588→
589→ const options: RequestHandlerOptions = {
590→ context,
591→ routeConfig,
592→ authHeader: undefined,
593→ payload: {},
594→ };
595→
596→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
597→ await handler.processRequest();
598→
599→ expect(mockLogger.logError).toHaveBeenCalled();
600→ });
601→ });
602→});
603→Glob
{
"pattern": "packages/cwc-api/src/__tests__/handlers/*.test.ts"
}Tool Result
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/responseUtils.test.ts /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts"
}Tool Result
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { QueryHandler } from '../../handlers/QueryHandler';
5→import {
6→ createMockRouteConfig,
7→ createMockGuestContext,
8→ createMockAuthenticatedContext,
9→ mockOperationSuccess,
10→ mockOperationFailure,
11→ mockOperationThrows,
12→ getUnitConfig,
13→ createMockDevConfig,
14→ createMockProdConfig,
15→ createMockLogger,
16→} from '../mocks';
17→import type { QueryHandlerOptions } from '../../handlers/handler.types';
18→
19→describe('QueryHandler', () => {
20→ const unitConfig = getUnitConfig();
21→ const mockProdConfig = createMockProdConfig();
22→
23→ beforeEach(() => {
24→ jest.clearAllMocks();
25→ });
26→
27→ describe('Successful Operations', () => {
28→ it('should return 200 with data on successful query', async () => {
29→ const routeConfig = createMockRouteConfig();
30→ const context = createMockGuestContext();
31→ mockOperationSuccess(routeConfig, { id: 1, name: 'test' });
32→
33→ const options: QueryHandlerOptions = {
34→ context,
35→ routeConfig,
36→ authHeader: undefined,
37→ payload: {},
38→ operationContext: { context },
39→ };
40→
41→ const handler = new QueryHandler(options, unitConfig, undefined);
42→ const response = await handler.processRequest();
43→
44→ expect(response.statusCode).toBe(200);
45→ expect(response.body.success).toBe(true);
46→ if (response.body.success) {
47→ expect(response.body.data).toEqual({ id: 1, name: 'test' });
48→ }
49→ });
50→
51→ it('should include pagination when operation returns it', async () => {
52→ const routeConfig = createMockRouteConfig();
53→ const context = createMockGuestContext();
54→ const pagination = { page: 1, pageSize: 20, totalCount: 100, hasMore: true };
55→ mockOperationSuccess(routeConfig, [{ id: 1 }, { id: 2 }], pagination);
56→
57→ const options: QueryHandlerOptions = {
58→ context,
59→ routeConfig,
60→ authHeader: undefined,
61→ payload: {},
62→ operationContext: { context },
63→ };
64→
65→ const handler = new QueryHandler(options, unitConfig, undefined);
66→ const response = await handler.processRequest();
67→
68→ expect(response.statusCode).toBe(200);
69→ expect(response.body.success).toBe(true);
70→ if (response.body.success) {
71→ expect(response.body.pagination).toEqual(pagination);
72→ }
73→ });
74→
75→ it('should not include jwt in response (handled by RequestHandler)', async () => {
76→ const routeConfig = createMockRouteConfig();
77→ const context = createMockAuthenticatedContext();
78→ mockOperationSuccess(routeConfig, { id: 1 });
79→
80→ const options: QueryHandlerOptions = {
81→ context,
82→ routeConfig,
83→ authHeader: 'Bearer token',
84→ payload: {},
85→ operationContext: { context },
86→ };
87→
88→ const handler = new QueryHandler(options, unitConfig, undefined);
89→ const response = await handler.processRequest();
90→
91→ expect(response.statusCode).toBe(200);
92→ expect(response.body.success).toBe(true);
93→ if (response.body.success) {
94→ expect(response.body.jwt).toBeUndefined();
95→ }
96→ });
97→ });
98→
99→ describe('Error Responses', () => {
100→ it('should return 404 for NOT_FOUND error code', async () => {
101→ const routeConfig = createMockRouteConfig();
102→ const context = createMockGuestContext();
103→ mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
104→
105→ const options: QueryHandlerOptions = {
106→ context,
107→ routeConfig,
108→ authHeader: undefined,
109→ payload: {},
110→ operationContext: { context },
111→ };
112→
113→ const handler = new QueryHandler(options, unitConfig, undefined);
114→ const response = await handler.processRequest();
115→
116→ expect(response.statusCode).toBe(404);
117→ expect(response.body.success).toBe(false);
118→ if (!response.body.success) {
119→ expect(response.body.errorCode).toBe('NOT_FOUND');
120→ expect(response.body.errorMessage).toBe('Resource not found');
121→ }
122→ });
123→
124→ it('should return 400 for VALIDATION_ERROR', async () => {
125→ const routeConfig = createMockRouteConfig();
126→ const context = createMockGuestContext();
127→ mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
128→
129→ const options: QueryHandlerOptions = {
130→ context,
131→ routeConfig,
132→ authHeader: undefined,
133→ payload: {},
134→ operationContext: { context },
135→ };
136→
137→ const handler = new QueryHandler(options, unitConfig, undefined);
138→ const response = await handler.processRequest();
139→
140→ expect(response.statusCode).toBe(400);
141→ expect(response.body.success).toBe(false);
142→ if (!response.body.success) {
143→ expect(response.body.errorCode).toBe('VALIDATION_ERROR');
144→ }
145→ });
146→
147→ it('should return 403 for OPERATION_ACCESS_DENIED', async () => {
148→ const routeConfig = createMockRouteConfig();
149→ const context = createMockGuestContext();
150→ mockOperationFailure(routeConfig, 'OPERATION_ACCESS_DENIED', 'Not permitted');
151→
152→ const options: QueryHandlerOptions = {
153→ context,
154→ routeConfig,
155→ authHeader: undefined,
156→ payload: {},
157→ operationContext: { context },
158→ };
159→
160→ const handler = new QueryHandler(options, unitConfig, undefined);
161→ const response = await handler.processRequest();
162→
163→ expect(response.statusCode).toBe(403);
164→ expect(response.body.success).toBe(false);
165→ });
166→
167→ it('should return 500 for DATABASE_ERROR', async () => {
168→ const routeConfig = createMockRouteConfig();
169→ const context = createMockGuestContext();
170→ mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
171→
172→ const options: QueryHandlerOptions = {
173→ context,
174→ routeConfig,
175→ authHeader: undefined,
176→ payload: {},
177→ operationContext: { context },
178→ };
179→
180→ const handler = new QueryHandler(options, unitConfig, undefined);
181→ const response = await handler.processRequest();
182→
183→ expect(response.statusCode).toBe(500);
184→ expect(response.body.success).toBe(false);
185→ if (!response.body.success) {
186→ expect(response.body.errorCode).toBe('DATABASE_ERROR');
187→ }
188→ });
189→ });
190→
191→ describe('Exception Handling', () => {
192→ it('should catch errors and return 500 response', async () => {
193→ const routeConfig = createMockRouteConfig();
194→ const context = createMockGuestContext();
195→ mockOperationThrows(routeConfig, new Error('Unexpected database error'));
196→
197→ const options: QueryHandlerOptions = {
198→ context,
199→ routeConfig,
200→ authHeader: undefined,
201→ payload: {},
202→ operationContext: { context },
203→ };
204→
205→ const handler = new QueryHandler(options, unitConfig, undefined);
206→ const response = await handler.processRequest();
207→
208→ expect(response.statusCode).toBe(500);
209→ expect(response.body.success).toBe(false);
210→ if (!response.body.success) {
211→ expect(response.body.errorCode).toBe('INTERNAL_ERROR');
212→ expect(response.body.errorMessage).toBe('An internal error occurred');
213→ }
214→ });
215→
216→ it('should include error detail when isDev is true', async () => {
217→ const routeConfig = createMockRouteConfig();
218→ const context = createMockGuestContext();
219→ const mockDevConfig = createMockDevConfig();
220→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
221→
222→ const options: QueryHandlerOptions = {
223→ context,
224→ routeConfig,
225→ authHeader: undefined,
226→ payload: {},
227→ operationContext: { context },
228→ };
229→
230→ const handler = new QueryHandler(options, mockDevConfig, undefined);
231→ const response = await handler.processRequest();
232→
233→ expect(response.body.success).toBe(false);
234→ if (!response.body.success) {
235→ expect(response.body.errorDetail).toBe('Detailed error message');
236→ }
237→ });
238→
239→ it('should not include error detail in production mode', async () => {
240→ const routeConfig = createMockRouteConfig();
241→ const context = createMockGuestContext();
242→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
243→
244→ const options: QueryHandlerOptions = {
245→ context,
246→ routeConfig,
247→ authHeader: undefined,
248→ payload: {},
249→ operationContext: { context },
250→ };
251→
252→ // mockProdConfig has isDev: false
253→ const handler = new QueryHandler(options, mockProdConfig, undefined);
254→ const response = await handler.processRequest();
255→
256→ expect(response.body.success).toBe(false);
257→ if (!response.body.success) {
258→ expect(response.body.errorDetail).toBeUndefined();
259→ }
260→ });
261→
262→ it('should log errors when logger is provided', async () => {
263→ const routeConfig = createMockRouteConfig();
264→ const context = createMockAuthenticatedContext();
265→ const mockLogger = createMockLogger();
266→ mockOperationThrows(routeConfig, new Error('Test error'));
267→
268→ const options: QueryHandlerOptions = {
269→ context,
270→ routeConfig,
271→ authHeader: 'Bearer token',
272→ payload: {},
273→ operationContext: { context },
274→ };
275→
276→ const handler = new QueryHandler(options, unitConfig, mockLogger);
277→ await handler.processRequest();
278→
279→ expect(mockLogger.logError).toHaveBeenCalled();
280→ });
281→ });
282→
283→ describe('Operation Access (Phase 3 Stub)', () => {
284→ it('should allow operation access (stub always returns allowed)', async () => {
285→ const routeConfig = createMockRouteConfig();
286→ const context = createMockGuestContext();
287→ mockOperationSuccess(routeConfig, { id: 1 });
288→
289→ const options: QueryHandlerOptions = {
290→ context,
291→ routeConfig,
292→ authHeader: undefined,
293→ payload: {},
294→ operationContext: { context },
295→ };
296→
297→ const handler = new QueryHandler(options, unitConfig, undefined);
298→ const response = await handler.processRequest();
299→
300→ // Operation access is stubbed to always allow in Phase 3
301→ expect(response.statusCode).toBe(200);
302→ expect(response.body.success).toBe(true);
303→ });
304→ });
305→});
306→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts"
}Tool Result
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { MutationHandler } from '../../handlers/MutationHandler';
5→import {
6→ createMockRouteConfig,
7→ createMockAuthenticatedContext,
8→ mockOperationSuccess,
9→ mockOperationFailure,
10→ mockOperationThrows,
11→ getUnitConfig,
12→ createMockDevConfig,
13→ createMockProdConfig,
14→ createMockLogger,
15→} from '../mocks';
16→import type { MutationHandlerOptions } from '../../handlers/handler.types';
17→
18→describe('MutationHandler', () => {
19→ const unitConfig = getUnitConfig();
20→ const mockProdConfig = createMockProdConfig();
21→
22→ beforeEach(() => {
23→ jest.clearAllMocks();
24→ });
25→
26→ describe('Successful Operations', () => {
27→ it('should return 200 with data on successful mutation', async () => {
28→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
29→ const context = createMockAuthenticatedContext();
30→ mockOperationSuccess(routeConfig, { id: 1, created: true });
31→
32→ const options: MutationHandlerOptions = {
33→ context,
34→ routeConfig,
35→ authHeader: 'Bearer token',
36→ payload: { name: 'New Item' },
37→ operationContext: { context },
38→ };
39→
40→ const handler = new MutationHandler(options, unitConfig, undefined);
41→ const response = await handler.processRequest();
42→
43→ expect(response.statusCode).toBe(200);
44→ expect(response.body.success).toBe(true);
45→ if (response.body.success) {
46→ expect(response.body.data).toEqual({ id: 1, created: true });
47→ }
48→ });
49→
50→ it('should return 200 for all operations (RPC-style, no 201)', async () => {
51→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
52→ const context = createMockAuthenticatedContext();
53→ mockOperationSuccess(routeConfig, { id: 999, status: 'created' });
54→
55→ const options: MutationHandlerOptions = {
56→ context,
57→ routeConfig,
58→ authHeader: 'Bearer token',
59→ payload: {},
60→ operationContext: { context },
61→ };
62→
63→ const handler = new MutationHandler(options, unitConfig, undefined);
64→ const response = await handler.processRequest();
65→
66→ // All POST operations return 200, not 201
67→ expect(response.statusCode).toBe(200);
68→ });
69→
70→ it('should not include jwt in response (handled by RequestHandler)', async () => {
71→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
72→ const context = createMockAuthenticatedContext();
73→ mockOperationSuccess(routeConfig, { id: 1 });
74→
75→ const options: MutationHandlerOptions = {
76→ context,
77→ routeConfig,
78→ authHeader: 'Bearer token',
79→ payload: {},
80→ operationContext: { context },
81→ };
82→
83→ const handler = new MutationHandler(options, unitConfig, undefined);
84→ const response = await handler.processRequest();
85→
86→ expect(response.statusCode).toBe(200);
87→ expect(response.body.success).toBe(true);
88→ if (response.body.success) {
89→ expect(response.body.jwt).toBeUndefined();
90→ }
91→ });
92→ });
93→
94→ describe('Error Responses', () => {
95→ it('should return 404 for NOT_FOUND error code', async () => {
96→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
97→ const context = createMockAuthenticatedContext();
98→ mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
99→
100→ const options: MutationHandlerOptions = {
101→ context,
102→ routeConfig,
103→ authHeader: 'Bearer token',
104→ payload: {},
105→ operationContext: { context },
106→ };
107→
108→ const handler = new MutationHandler(options, unitConfig, undefined);
109→ const response = await handler.processRequest();
110→
111→ expect(response.statusCode).toBe(404);
112→ expect(response.body.success).toBe(false);
113→ if (!response.body.success) {
114→ expect(response.body.errorCode).toBe('NOT_FOUND');
115→ expect(response.body.errorMessage).toBe('Resource not found');
116→ }
117→ });
118→
119→ it('should return 400 for ALREADY_EXISTS error code', async () => {
120→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
121→ const context = createMockAuthenticatedContext();
122→ mockOperationFailure(routeConfig, 'ALREADY_EXISTS', 'Resource already exists');
123→
124→ const options: MutationHandlerOptions = {
125→ context,
126→ routeConfig,
127→ authHeader: 'Bearer token',
128→ payload: {},
129→ operationContext: { context },
130→ };
131→
132→ const handler = new MutationHandler(options, unitConfig, undefined);
133→ const response = await handler.processRequest();
134→
135→ expect(response.statusCode).toBe(400);
136→ expect(response.body.success).toBe(false);
137→ if (!response.body.success) {
138→ expect(response.body.errorCode).toBe('ALREADY_EXISTS');
139→ }
140→ });
141→
142→ it('should return 400 for VALIDATION_ERROR', async () => {
143→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
144→ const context = createMockAuthenticatedContext();
145→ mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
146→
147→ const options: MutationHandlerOptions = {
148→ context,
149→ routeConfig,
150→ authHeader: 'Bearer token',
151→ payload: {},
152→ operationContext: { context },
153→ };
154→
155→ const handler = new MutationHandler(options, unitConfig, undefined);
156→ const response = await handler.processRequest();
157→
158→ expect(response.statusCode).toBe(400);
159→ expect(response.body.success).toBe(false);
160→ });
161→
162→ it('should return 500 for DATABASE_ERROR', async () => {
163→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
164→ const context = createMockAuthenticatedContext();
165→ mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
166→
167→ const options: MutationHandlerOptions = {
168→ context,
169→ routeConfig,
170→ authHeader: 'Bearer token',
171→ payload: {},
172→ operationContext: { context },
173→ };
174→
175→ const handler = new MutationHandler(options, unitConfig, undefined);
176→ const response = await handler.processRequest();
177→
178→ expect(response.statusCode).toBe(500);
179→ expect(response.body.success).toBe(false);
180→ if (!response.body.success) {
181→ expect(response.body.errorCode).toBe('DATABASE_ERROR');
182→ }
183→ });
184→ });
185→
186→ describe('Exception Handling', () => {
187→ it('should catch errors and return 500 response', async () => {
188→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
189→ const context = createMockAuthenticatedContext();
190→ mockOperationThrows(routeConfig, new Error('Unexpected database error'));
191→
192→ const options: MutationHandlerOptions = {
193→ context,
194→ routeConfig,
195→ authHeader: 'Bearer token',
196→ payload: {},
197→ operationContext: { context },
198→ };
199→
200→ const handler = new MutationHandler(options, unitConfig, undefined);
201→ const response = await handler.processRequest();
202→
203→ expect(response.statusCode).toBe(500);
204→ expect(response.body.success).toBe(false);
205→ if (!response.body.success) {
206→ expect(response.body.errorCode).toBe('INTERNAL_ERROR');
207→ expect(response.body.errorMessage).toBe('An internal error occurred');
208→ }
209→ });
210→
211→ it('should include error detail when isDev is true', async () => {
212→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
213→ const context = createMockAuthenticatedContext();
214→ const mockDevConfig = createMockDevConfig();
215→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
216→
217→ const options: MutationHandlerOptions = {
218→ context,
219→ routeConfig,
220→ authHeader: 'Bearer token',
221→ payload: {},
222→ operationContext: { context },
223→ };
224→
225→ const handler = new MutationHandler(options, mockDevConfig, undefined);
226→ const response = await handler.processRequest();
227→
228→ expect(response.body.success).toBe(false);
229→ if (!response.body.success) {
230→ expect(response.body.errorDetail).toBe('Detailed error message');
231→ }
232→ });
233→
234→ it('should not include error detail in production mode', async () => {
235→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
236→ const context = createMockAuthenticatedContext();
237→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
238→
239→ const options: MutationHandlerOptions = {
240→ context,
241→ routeConfig,
242→ authHeader: 'Bearer token',
243→ payload: {},
244→ operationContext: { context },
245→ };
246→
247→ const handler = new MutationHandler(options, mockProdConfig, undefined);
248→ const response = await handler.processRequest();
249→
250→ expect(response.body.success).toBe(false);
251→ if (!response.body.success) {
252→ expect(response.body.errorDetail).toBeUndefined();
253→ }
254→ });
255→
256→ it('should log errors when logger is provided', async () => {
257→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
258→ const context = createMockAuthenticatedContext();
259→ const mockLogger = createMockLogger();
260→ mockOperationThrows(routeConfig, new Error('Test error'));
261→
262→ const options: MutationHandlerOptions = {
263→ context,
264→ routeConfig,
265→ authHeader: 'Bearer token',
266→ payload: {},
267→ operationContext: { context },
268→ };
269→
270→ const handler = new MutationHandler(options, unitConfig, mockLogger);
271→ await handler.processRequest();
272→
273→ expect(mockLogger.logError).toHaveBeenCalled();
274→ });
275→ });
276→
277→ describe('Operation Access (Phase 3 Stub)', () => {
278→ it('should allow operation access (stub always returns allowed)', async () => {
279→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
280→ const context = createMockAuthenticatedContext();
281→ mockOperationSuccess(routeConfig, { id: 1 });
282→
283→ const options: MutationHandlerOptions = {
284→ context,
285→ routeConfig,
286→ authHeader: 'Bearer token',
287→ payload: {},
288→ operationContext: { context },
289→ };
290→
291→ const handler = new MutationHandler(options, unitConfig, undefined);
292→ const response = await handler.processRequest();
293→
294→ // Operation access is stubbed to always allow in Phase 3
295→ expect(response.statusCode).toBe(200);
296→ expect(response.body.success).toBe(true);
297→ });
298→ });
299→});
300→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Let me update the QueryHandler and MutationHandler tests to reflect Phase 4 operation access. The tests need to be updated for the new operation access implementation.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": " describe('Operation Access (Phase 3 Stub)', () => {\n it('should allow operation access (stub always returns allowed)', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n const handler = new QueryHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n // Operation access is stubbed to always allow in Phase 3\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n });\n});",
"new_string": " describe('Operation Access', () => {\n it('should allow guest access for guest-user routes', async () => {\n const routeConfig = createMockRouteConfig({ requiredRole: 'guest-user' });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n const handler = new QueryHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n\n it('should deny guest access for logged-on-user routes', async () => {\n const routeConfig = createMockRouteConfig({ requiredRole: 'logged-on-user' });\n const context = createMockGuestContext();\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n const handler = new QueryHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(403);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');\n }\n });\n\n it('should deny non-owner access for project-owner routes', async () => {\n const routeConfig = createMockRouteConfig({ requiredRole: 'project-owner' });\n const context = createMockAuthenticatedContext({\n ownedProjects: ['other-project'],\n });\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n operationContext: { context, projectId: 'target-project' },\n };\n\n const handler = new QueryHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(403);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');\n }\n });\n\n it('should allow owner access for project-owner routes', async () => {\n const routeConfig = createMockRouteConfig({ requiredRole: 'project-owner' });\n const context = createMockAuthenticatedContext({\n ownedProjects: ['target-project'],\n });\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n operationContext: { context, projectId: 'target-project' },\n };\n\n const handler = new QueryHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n });\n});"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
279→ expect(mockLogger.logError).toHaveBeenCalled();
280→ });
281→ });
282→
283→ describe('Operation Access', () => {
284→ it('should allow guest access for guest-user routes', async () => {
285→ const routeConfig = createMockRouteConfig({ requiredRole: 'guest-user' });
286→ const context = createMockGuestContext();
287→ mockOperationSuccess(routeConfig, { id: 1 });
288→
289→ const options: QueryHandlerOptions = {
290→ context,
291→ routeConfig,
292→ authHeader: undefined,
293→ payload: {},
294→ operationContext: { context },
295→ };
296→
297→ const handler = new QueryHandler(options, unitConfig, undefined);
298→ const response = await handler.processRequest();
299→
300→ expect(response.statusCode).toBe(200);
301→ expect(response.body.success).toBe(true);
302→ });
303→
304→ it('should deny guest access for logged-on-user routes', async () => {
305→ const routeConfig = createMockRouteConfig({ requiredRole: 'logged-on-user' });
306→ const context = createMockGuestContext();
307→
308→ const options: QueryHandlerOptions = {
309→ context,
310→ routeConfig,
311→ authHeader: undefined,
312→ payload: {},
313→ operationContext: { context },
314→ };
315→
316→ const handler = new QueryHandler(options, unitConfig, undefined);
317→ const response = await handler.processRequest();
318→
319→ expect(response.statusCode).toBe(403);
320→ expect(response.body.success).toBe(false);
321→ if (!response.body.success) {
322→ expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');
323→ }
324→ });
325→
326→ it('should deny non-owner access for project-owner routes', async () => {
327→ const routeConfig = createMockRouteConfig({ requiredRole: 'project-owner' });
328→ const context = createMockAuthenticatedContext({
329→ ownedProjects: ['other-project'],
330→ });
331→
332→ const options: QueryHandlerOptions = {
333→ context,
334→ routeConfig,
335→ authHeader: 'Bearer token',
336→ payload: {},
337→ operationContext: { context, projectId: 'target-project' },
338→ };
339→
340→ const handler = new QueryHandler(options, unitConfig, undefined);
341→ const response = await handler.processRequest();
342→
343→ expect(response.statusCode).toBe(403);
344→ expect(response.body.success).toBe(false);
345→ if (!response.body.success) {
346→ expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');
347→ }
348→ });
349→
350→ it('should allow owner access for project-owner routes', async () => {
351→ const routeConfig = createMockRouteConfig({ requiredRole: 'project-owner' });
352→ const context = createMockAuthenticatedContext({
353→ ownedProjects: ['target-project'],
354→ });
355→ mockOperationSuccess(routeConfig, { id: 1 });
356→
357→ const options: QueryHandlerOptions = {
358→ context,
359→ routeConfig,
360→ authHeader: 'Bearer token',
361→ payload: {},
362→ operationContext: { context, projectId: 'target-project' },
363→ };
364→
365→ const handler = new QueryHandler(options, unitConfig, undefined);
366→ const response = await handler.processRequest();
367→
368→ expect(response.statusCode).toBe(200);
369→ expect(response.body.success).toBe(true);
370→ });
371→ });
372→});
373→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": " describe('Operation Access (Phase 3 Stub)', () => {\n it('should allow operation access (stub always returns allowed)', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new MutationHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n // Operation access is stubbed to always allow in Phase 3\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n });\n});",
"new_string": " describe('Operation Access', () => {\n it('should allow authenticated access for logged-on-user routes', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n requiredRole: 'logged-on-user',\n });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new MutationHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n });\n\n it('should deny non-owner access for project-owner routes', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n requiredRole: 'project-owner',\n });\n const context = createMockAuthenticatedContext({\n ownedProjects: ['other-project'],\n });\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n operationContext: { context, projectId: 'target-project' },\n };\n\n const handler = new MutationHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(403);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');\n }\n });\n\n it('should allow owner access for project-owner routes', async () => {\n const routeConfig = createMockRouteConfig({\n handlerType: 'mutation',\n requiredRole: 'project-owner',\n });\n const context = createMockAuthenticatedContext({\n ownedProjects: ['target-project'],\n });\n mockOperationSuccess(routeConfig, { id: 1, updated: true });\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: { name: 'Updated' },\n operationContext: { context, projectId: 'target-project' },\n };\n\n const handler = new MutationHandler(options, unitConfig, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.data).toEqual({ id: 1, updated: true });\n }\n });\n });\n});"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
273→ expect(mockLogger.logError).toHaveBeenCalled();
274→ });
275→ });
276→
277→ describe('Operation Access', () => {
278→ it('should allow authenticated access for logged-on-user routes', async () => {
279→ const routeConfig = createMockRouteConfig({
280→ handlerType: 'mutation',
281→ requiredRole: 'logged-on-user',
282→ });
283→ const context = createMockAuthenticatedContext();
284→ mockOperationSuccess(routeConfig, { id: 1 });
285→
286→ const options: MutationHandlerOptions = {
287→ context,
288→ routeConfig,
289→ authHeader: 'Bearer token',
290→ payload: {},
291→ operationContext: { context },
292→ };
293→
294→ const handler = new MutationHandler(options, unitConfig, undefined);
295→ const response = await handler.processRequest();
296→
297→ expect(response.statusCode).toBe(200);
298→ expect(response.body.success).toBe(true);
299→ });
300→
301→ it('should deny non-owner access for project-owner routes', async () => {
302→ const routeConfig = createMockRouteConfig({
303→ handlerType: 'mutation',
304→ requiredRole: 'project-owner',
305→ });
306→ const context = createMockAuthenticatedContext({
307→ ownedProjects: ['other-project'],
308→ });
309→
310→ const options: MutationHandlerOptions = {
311→ context,
312→ routeConfig,
313→ authHeader: 'Bearer token',
314→ payload: {},
315→ operationContext: { context, projectId: 'target-project' },
316→ };
317→
318→ const handler = new MutationHandler(options, unitConfig, undefined);
319→ const response = await handler.processRequest();
320→
321→ expect(response.statusCode).toBe(403);
322→ expect(response.body.success).toBe(false);
323→ if (!response.body.success) {
324→ expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');
325→ }
326→ });
327→
328→ it('should allow owner access for project-owner routes', async () => {
329→ const routeConfig = createMockRouteConfig({
330→ handlerType: 'mutation',
331→ requiredRole: 'project-owner',
332→ });
333→ const context = createMockAuthenticatedContext({
334→ ownedProjects: ['target-project'],
335→ });
336→ mockOperationSuccess(routeConfig, { id: 1, updated: true });
337→
338→ const options: MutationHandlerOptions = {
339→ context,
340→ routeConfig,
341→ authHeader: 'Bearer token',
342→ payload: { name: 'Updated' },
343→ operationContext: { context, projectId: 'target-project' },
344→ };
345→
346→ const handler = new MutationHandler(options, unitConfig, undefined);
347→ const response = await handler.processRequest();
348→
349→ expect(response.statusCode).toBe(200);
350→ expect(response.body.success).toBe(true);
351→ if (response.body.success) {
352→ expect(response.body.data).toEqual({ id: 1, updated: true });
353→ }
354→ });
355→ });
356→});
357→I'll continue with the implementation. The handler tests have been updated, and now I need to run typecheck and then tests.
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "completed",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "completed",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "in_progress",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "pending",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Bash
{
"command": "pnpm --filter cwc-api typecheck",
"description": "Typecheck cwc-api package"
}Tool Result
> cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > tsc --noEmit
Typecheck passed. Now running the tests.
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "completed",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "completed",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "completed",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "in_progress",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test",
"description": "Run cwc-api tests"
}Tool Result
> cwc-api@1.0.0 test /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api
> RUNTIME_ENVIRONMENT=unit jest
PASS src/__tests__/policies/checkOperationAccess.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 👥 sync secrets across teammates & machines: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/policies/checkRouteAccess.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔄 add secrets lifecycle management: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/RequestHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/MutationHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔑 add access controls to secrets: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/config.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/healthCheck.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/QueryHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ⚙️ override existing env vars with { override: true }
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/responseUtils.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔄 add secrets lifecycle management: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/context/createContext.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔐 prevent building .env in docker: https://dotenvx.com/prebuild
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
Test Suites: 9 passed, 9 total
Tests: 121 passed, 121 total
Snapshots: 0 total
Time: 0.704 s, estimated 1 s
Ran all test suites.All 121 tests passed. Now I'll update the cwc-api CLAUDE.md with the access policy patterns.
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "completed",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "completed",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "completed",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "completed",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "in_progress",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md"
}Tool Result
1→# cwc-api Package
2→
3→Main business logic API for CWC application. Acts as the central orchestrator that handles CRUD operations, enforces business rules, and coordinates data flow between frontend applications and the database layer.
4→
5→## Architecture
6→
7→**Database-Connected Microservice:**
8→
9→- Uses SqlClient from `cwc-backend-utils` to communicate with cwc-sql
10→- Uses database-backed Logger for error logging
11→- Never connects directly to database - all data access flows through cwc-sql
12→- Coordinates with cwc-auth for session renewal
13→
14→**Layered Architecture:**
15→
16→```
17→Request → RequestHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
18→```
19→
20→- **RequestHandler**: Auth → context creation → route access check → delegation → session renewal
21→- **QueryHandler**: Operation access check → execute query → format response
22→- **MutationHandler**: Operation access check → execute mutation → format response
23→- **SqlFunctions**: Atomic database operations (one function per table operation)
24→
25→## Request Pipeline
26→
27→1. Express routes receive incoming HTTP requests
28→2. RequestHandler extracts JWT from Authorization header
29→3. JWT validated → authenticated context; No JWT → guest context
30→4. Route-level access policy checked
31→5. QueryHandler or MutationHandler executes business logic
32→6. SqlFunctions handle database operations through SqlClient
33→7. RequestHandler renews session (if authenticated and not auth error)
34→8. Response returned (with new JWT if session renewed)
35→
36→## Context Layer - Centralized Authentication
37→
38→**Location:** `src/context/`
39→
40→**Architecture Decision:** cwc-api does NOT duplicate JWT verification logic. All token validation goes through cwc-auth's `/auth/v1/verify-token` endpoint via AuthClient.
41→
42→**Trade-off:** Additional HTTP call per request, but auth logic stays centralized in cwc-auth (single source of truth).
43→
44→**Files:**
45→- `context.types.ts` - AuthenticatedContext, GuestContext, RequestContext types
46→- `createContext.ts` - Creates context from Authorization header
47→- `index.ts` - Exports
48→
49→**Context Types:**
50→
51→```typescript
52→// Authenticated user (valid JWT)
53→type AuthenticatedContext = {
54→ isAuthenticated: true;
55→ role: 'project-owner' | 'logged-on-user'; // Determined per-operation
56→ userPkId: number;
57→ username: string;
58→ ownedProjects: string[];
59→ payload: UserJwtPayload;
60→};
61→
62→// Unauthenticated request (no/invalid JWT)
63→type GuestContext = {
64→ isAuthenticated: false;
65→ role: 'guest-user';
66→};
67→
68→type RequestContext = AuthenticatedContext | GuestContext;
69→```
70→
71→**Usage:**
72→```typescript
73→import { createContext } from './context';
74→import { AuthClient } from 'cwc-backend-utils';
75→
76→const context = await createContext({
77→ authHeader: req.headers.authorization,
78→ authClient: authClient,
79→});
80→
81→if (context.isAuthenticated) {
82→ // TypeScript narrows to AuthenticatedContext
83→ console.log(context.userPkId, context.ownedProjects);
84→} else {
85→ // TypeScript narrows to GuestContext
86→ console.log(context.role); // 'guest-user'
87→}
88→```
89→
90→**Graceful Degradation:**
91→- Missing token → guest context
92→- Invalid token → guest context
93→- Expired token → guest context
94→- Auth service error → guest context (logged)
95→- No errors thrown; auth failures result in guest access
96→
97→## Handler Architecture
98→
99→**Location:** `src/handlers/`
100→
101→**Files:**
102→- `handler.types.ts` - Type definitions for handlers, operations, and responses
103→- `RequestHandler.ts` - Entry point: route access check, delegation, session renewal
104→- `QueryHandler.ts` - Read operations with pagination support
105→- `MutationHandler.ts` - Write operations
106→- `responseUtils.ts` - Shared response creation utilities (error responses, status code mapping)
107→- `index.ts` - Exports
108→
109→**Handler Flow:**
110→
111→```
112→RequestHandler.processRequest()
113→├── checkRouteAccess() → 401/403 if denied (NO renewal)
114→├── buildOperationContext() → projectId, resourceId from path
115→├── delegate to QueryHandler or MutationHandler
116→│ ├── checkOperationAccess() → (stub for Phase 4)
117→│ ├── execute operation()
118→│ └── format response (no JWT)
119→├── if authenticated AND not auth error (401/403):
120→│ └── renewSession() → merge JWT into response
121→└── return final response
122→```
123→
124→**Session Renewal Strategy:**
125→
126→- Centralized in RequestHandler (not Query/MutationHandler)
127→- Renews on ALL requests except auth errors (401, 403)
128→- Even failed operations (404, 500) trigger renewal to keep session active
129→- New JWT merged into response body (`response.body.jwt`) for success responses
130→- Renewal failure doesn't fail the request (graceful degradation, logged)
131→- Guest users never trigger renewal
132→
133→**Error Code Mapping:**
134→
135→| Error Code | HTTP Status |
136→|------------|-------------|
137→| `UNAUTHORIZED` | 401 |
138→| `FORBIDDEN` | 403 |
139→| `NOT_FOUND` | 404 |
140→| `ALREADY_EXISTS` | 400 |
141→| `VALIDATION_ERROR` | 400 |
142→| `INVALID_PARAMETER` | 400 |
143→| `DATABASE_ERROR` | 500 |
144→| `INTERNAL_ERROR` | 500 |
145→
146→## Role-Based Access Control
147→
148→**Roles:**
149→
150→| Role | Description |
151→|------|-------------|
152→| `project-owner` | User owns the project being accessed |
153→| `logged-on-user` | Authenticated user (may not own the project) |
154→| `guest-user` | Unauthenticated request (read-only access) |
155→
156→**Access Policy Levels:**
157→
158→- **Route-level**: Can this role call this endpoint at all?
159→- **Operation-level**: Can this role execute this specific operation? (Phase 4)
160→- **Context-aware**: Does the user have the right relationship to the data? (e.g., ownership)
161→
162→## Tables Handled
163→
164→**Content Tables (Full CRUD + Soft Delete):**
165→
166→- project
167→- codingSession
168→- codingSessionContent
169→- codingSessionAttachment
170→- comment
171→- reaction
172→- contentReport
173→- abuseReport
174→
175→**Read-Only Tables:**
176→
177→- featureFlag (all fields)
178→- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
179→
180→## SqlFunction Naming Convention
181→
182→Each table has a subdirectory in `src/sql/` with these functions:
183→
184→| Function | Purpose |
185→|----------|---------|
186→| `select{Table}.ts` | Retrieve single record by identifier |
187→| `list{Table}s.ts` | Retrieve multiple records with filtering |
188→| `insert{Table}.ts` | Create new record |
189→| `update{Table}.ts` | Modify existing record |
190→| `delete{Table}.ts` | Hard delete (GDPR compliance) |
191→| `softDelete{Table}.ts` | Soft delete (set enabled=false) |
192→
193→**Key Principles:**
194→
195→- Single responsibility: Each SqlFunction does one thing
196→- No business logic: SqlFunctions handle data access only
197→- Soft-delete awareness: List queries automatically filter `enabled=true`
198→- Composability: Queries/Mutations compose multiple SqlFunctions
199→
200→## cwc-auth Integration
201→
202→**Division of Responsibilities:**
203→
204→| Concern | Owner |
205→|---------|-------|
206→| "Is this token valid?" | cwc-auth |
207→| "Who is this user?" | cwc-auth (via token claims) |
208→| "Can this user access this endpoint?" | cwc-api |
209→| "Is this user the owner of this project?" | cwc-api |
210→
211→**Session Renewal Flow:**
212→
213→When cwc-api creates/deletes a project:
214→1. cwc-api performs the database operation
215→2. cwc-api calls cwc-auth `/auth/v1/renew-session`
216→3. cwc-auth issues new JWT with updated `ownedProjects`
217→4. cwc-api returns new JWT to client
218→
219→## Response Pattern
220→
221→**Standard Response Envelope:**
222→
223→```typescript
224→// Success
225→{
226→ success: true,
227→ data: { ... },
228→ // Optional: pagination metadata
229→}
230→
231→// Error
232→{
233→ success: false,
234→ errorCode: 'ERROR_CODE',
235→ errorMessage: 'Human-readable message',
236→ // Dev-only: errorDetail
237→}
238→```
239→
240→**Pagination (Offset-based):**
241→
242→```typescript
243→{
244→ success: true,
245→ data: [...],
246→ pagination: {
247→ page: 1,
248→ pageSize: 20,
249→ totalCount: 100,
250→ hasMore: true
251→ }
252→}
253→```
254→
255→## Configuration
256→
257→**Required Environment Variables:**
258→
259→```bash
260→RUNTIME_ENVIRONMENT=dev
261→SERVICE_PORT=5040
262→DATA_URI=http://localhost:5020/data/v1
263→AUTH_URI=http://localhost:5005/auth/v1
264→APP_URL=http://localhost:3000
265→CORS_ORIGIN=*
266→DEV_CORS_ORIGIN=http://localhost:3000
267→RATE_LIMITER_POINTS=15
268→RATE_LIMITER_DURATION=1
269→DEBUG_MODE=OFF
270→LOG_ERRORS_TO_DATABASE=ON
271→USER_JWT_SECRET=...
272→SQL_CLIENT_API_KEY=...
273→```
274→
275→## API Versioning
276→
277→**URL-based versioning:**
278→
279→- `/api/v1/*` routes to CwcApiV1 handlers
280→- Each version lives in `src/apis/CwcApiV{N}/`
281→- Route definitions in `routes.ts`, organized alphabetically
282→
283→## Critical Rules
284→
285→**SqlClient Usage:**
286→
287→- Never provide `createdDate` or `modifiedDate` - handled by cwc-sql automatically
288→- Always use SqlFunctions, never construct queries directly in business logic
289→
290→**Guest User Handling:**
291→
292→- No JWT required - RequestHandler creates context with `role: 'guest-user'`
293→- Guest users have read-only access to public content
294→
295→**Soft Delete:**
296→
297→- All content tables support soft delete (`enabled=false`)
298→- Hard delete available for GDPR compliance
299→- List queries automatically filter `enabled=true`
300→
301→## Implementation Status
302→
303→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
304→
305→The checklist tracks 7 phases:
306→1. Package Foundation (config, Express service, health check) - **Complete**
307→2. Authentication & Context Layer (JWT, request context) - **Complete**
308→3. Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - **Complete**
309→4. Access Policy System (route and operation policies)
310→5. SqlFunction Layer (CRUD operations for all tables)
311→6. CwcApiV1 Routes & Business Logic
312→7. Integration Testing & Final Review
313→
314→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
315→
316→## Unit Testing
317→
318→**Test Infrastructure:**
319→
320→- Jest 30.x with ts-jest for TypeScript ESM support
321→- Test files: `src/__tests__/**/*.test.ts`
322→- Setup: `src/__tests__/setup.ts` loads unit test environment via `loadDotEnv`
323→- Run tests: `pnpm api test` or `NODE_ENV=unit pnpm --filter cwc-api test`
324→
325→**Mock Infrastructure (`src/__tests__/mocks/`):**
326→
327→| Mock | Purpose |
328→|------|---------|
329→| **SqlClient Mocks** | |
330→| `createMockSqlClient()` | Full SqlClient mock with jest.fn() for all methods |
331→| `mockSelectSuccess/NotFound` | Configure SqlClient for select queries |
332→| `mockListSuccess` | Configure SqlClient for list queries |
333→| `mockInsertSuccess/Failure` | Configure SqlClient for inserts |
334→| `mockUpdateSuccess/NotFound` | Configure SqlClient for updates |
335→| `mockDeleteSuccess/NotFound` | Configure SqlClient for deletes |
336→| **AuthClient Mocks** | |
337→| `createMockAuthClient()` | Full AuthClient mock (verifyToken, renewSession) |
338→| `createMockUserJwtPayload(overrides)` | Create UserJwtPayload with custom overrides |
339→| `mockVerifyTokenSuccess(client, payload?)` | Configure AuthClient to return success |
340→| `mockVerifyTokenFailure(client, error?)` | Configure AuthClient to return failure |
341→| `mockRenewSessionSuccess(client, jwt?)` | Configure renewSession to return new JWT |
342→| `mockRenewSessionFailure(client, error?)` | Configure renewSession to return failure |
343→| **Route/Handler Mocks** | |
344→| `createMockRouteConfig(overrides)` | Create route config for testing |
345→| `createMockOperationContext(overrides)` | Create operation context for testing |
346→| `createMockGuestContext()` | Create guest (unauthenticated) context |
347→| `createMockAuthenticatedContext(overrides)` | Create authenticated user context |
348→| `mockOperationSuccess(routeConfig, data)` | Configure operation to succeed |
349→| `mockOperationFailure(routeConfig, code, msg)` | Configure operation to fail |
350→| `mockOperationThrows(routeConfig, error)` | Configure operation to throw |
351→| **Config Mocks** | |
352→| `getUnitConfig()` | Load real config from unit.cwc-api.env |
353→| `createMockConfig(overrides)` | Create config with custom overrides |
354→| `createMockDevConfig(overrides)` | Mock config with isDev: true |
355→| `createMockProdConfig(overrides)` | Mock config with isProd: true |
356→| `createMockUnitConfig(overrides)` | Mock config with isUnit: true |
357→| **Logger Mock** | |
358→| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |
359→
360→**Testing Patterns:**
361→
362→RequestHandler tests (includes session renewal):
363→```typescript
364→import {
365→ createMockAuthClient,
366→ createMockRouteConfig,
367→ createMockAuthenticatedContext,
368→ mockOperationSuccess,
369→ mockRenewSessionSuccess,
370→ getUnitConfig,
371→} from '../mocks';
372→
373→describe('RequestHandler', () => {
374→ const unitConfig = getUnitConfig();
375→ const mockAuthClient = createMockAuthClient();
376→
377→ it('should renew session on success', async () => {
378→ const routeConfig = createMockRouteConfig({ handlerType: 'query' });
379→ const context = createMockAuthenticatedContext();
380→ mockOperationSuccess(routeConfig, { id: 1 });
381→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
382→ // RequestHandler uses 4 args: options, config, authClient, logger
383→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
384→ // ...
385→ });
386→});
387→```
388→
389→QueryHandler/MutationHandler tests (no authClient, no renewal):
390→```typescript
391→import {
392→ createMockRouteConfig,
393→ createMockAuthenticatedContext,
394→ mockOperationSuccess,
395→ getUnitConfig,
396→} from '../mocks';
397→
398→describe('QueryHandler', () => {
399→ const unitConfig = getUnitConfig();
400→
401→ it('should execute operation', async () => {
402→ const routeConfig = createMockRouteConfig();
403→ const context = createMockAuthenticatedContext();
404→ mockOperationSuccess(routeConfig, { id: 1 });
405→ // QueryHandler uses 3 args: options, config, logger
406→ const handler = new QueryHandler(options, unitConfig, undefined);
407→ // ...
408→ });
409→});
410→```
411→
412→## Related Packages
413→
414→**Depends On:**
415→
416→- `cwc-backend-utils` (workspace) - SqlClient, Logger, Express service factory
417→- `cwc-schema` (workspace) - Data validation
418→- `cwc-types` (workspace) - Entity types, RuntimeEnvironment
419→
420→**Coordinates With:**
421→
422→- `cwc-auth` - JWT validation, session renewal
423→- `cwc-sql` - Database operations
424→
425→**Consumed By:**
426→
427→- `cwc-website` - Frontend web application
428→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
I'll update the CLAUDE.md to document the access policy system.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Handler Flow:**\n\n```\nRequestHandler.processRequest()\n├── checkRouteAccess() → 401/403 if denied (NO renewal)\n├── buildOperationContext() → projectId, resourceId from path\n├── delegate to QueryHandler or MutationHandler\n│ ├── checkOperationAccess() → (stub for Phase 4)\n│ ├── execute operation()\n│ └── format response (no JWT)\n├── if authenticated AND not auth error (401/403):\n│ └── renewSession() → merge JWT into response\n└── return final response\n```",
"new_string": "**Handler Flow:**\n\n```\nRequestHandler.processRequest()\n├── checkRouteAccess() → 401/403 if denied (NO renewal)\n├── buildOperationContext() → projectId, resourceId from path\n├── delegate to QueryHandler or MutationHandler\n│ ├── checkOperationAccess() → 403 if denied\n│ ├── execute operation()\n│ └── format response (no JWT)\n├── if authenticated AND not auth error (401/403):\n│ └── renewSession() → merge JWT into response\n└── return final response\n```"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 105→- `MutationHandler.ts` - Write operations 106→- `responseUtils.ts` - Shared response creation utilities (error responses, status code mapping) 107→- `index.ts` - Exports 108→ 109→**Handler Flow:** 110→ 111→``` 112→RequestHandler.processRequest() 113→├── checkRouteAccess() → 401/403 if denied (NO renewal) 114→├── buildOperationContext() → projectId, resourceId from path 115→├── delegate to QueryHandler or MutationHandler 116→│ ├── checkOperationAccess() → 403 if denied 117→│ ├── execute operation() 118→│ └── format response (no JWT) 119→├── if authenticated AND not auth error (401/403): 120→│ └── renewSession() → merge JWT into response 121→└── return final response 122→``` 123→ 124→**Session Renewal Strategy:** 125→ 126→- Centralized in RequestHandler (not Query/MutationHandler)
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "## Role-Based Access Control\n\n**Roles:**\n\n| Role | Description |\n|------|-------------|\n| `project-owner` | User owns the project being accessed |\n| `logged-on-user` | Authenticated user (may not own the project) |\n| `guest-user` | Unauthenticated request (read-only access) |\n\n**Access Policy Levels:**\n\n- **Route-level**: Can this role call this endpoint at all?\n- **Operation-level**: Can this role execute this specific operation? (Phase 4)\n- **Context-aware**: Does the user have the right relationship to the data? (e.g., ownership)",
"new_string": "## Access Policy System\n\n**Location:** `src/policies/`\n\n**Files:**\n- `policy.types.ts` - Re-exports RouteAccessResult, OperationAccessResult\n- `checkRouteAccess.ts` - Route-level authentication check\n- `checkOperationAccess.ts` - Operation-level access with ownership check\n- `index.ts` - Exports\n\n**Role Hierarchy:**\n\n```\nguest-user < logged-on-user < project-owner\n```\n\n| Role | Description |\n|------|-------------|\n| `guest-user` | Unauthenticated request (public read-only access) |\n| `logged-on-user` | Authenticated user (may not own the project) |\n| `project-owner` | User owns the project being accessed |\n\n**Two-Level Access Control:**\n\nRoutes specify `requiredRole: CwcRole` which is checked at two levels:\n\n| requiredRole | Route-Level Check | Operation-Level Check |\n|--------------|-------------------|----------------------|\n| `guest-user` | Anyone allowed | Anyone allowed |\n| `logged-on-user` | Must be authenticated | Must be authenticated |\n| `project-owner` | Must be authenticated | Must be authenticated AND own the project |\n\n**Key Design Decision:** Route-level for `project-owner` only checks authentication (no DB lookup). The actual ownership check happens at operation-level using `context.ownedProjects` from the JWT.\n\n**Policy Functions:**\n\n```typescript\n// Route-level: Only checks authentication, no ownership\ncheckRouteAccess(context: RequestContext, requiredRole: CwcRole): RouteAccessResult\n\n// Operation-level: Checks authentication AND ownership for project-owner\ncheckOperationAccess(\n context: RequestContext,\n operationContext: OperationContext,\n requiredRole: CwcRole\n): OperationAccessResult\n\n// Helper: Checks if user owns a specific project\nisProjectOwner(context: RequestContext, projectId: string | undefined): boolean\n```\n\n**Security:**\n- Ownership verified from JWT claims (`context.ownedProjects.includes(projectId)`)\n- `projectId` comes from URL path params (via `operationContext`), not request body\n- No database lookups for access checks - all data comes from JWT"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 142→| `INVALID_PARAMETER` | 400 | 143→| `DATABASE_ERROR` | 500 | 144→| `INTERNAL_ERROR` | 500 | 145→ 146→## Access Policy System 147→ 148→**Location:** `src/policies/` 149→ 150→**Files:** 151→- `policy.types.ts` - Re-exports RouteAccessResult, OperationAccessResult 152→- `checkRouteAccess.ts` - Route-level authentication check 153→- `checkOperationAccess.ts` - Operation-level access with ownership check 154→- `index.ts` - Exports 155→ 156→**Role Hierarchy:** 157→ 158→``` 159→guest-user < logged-on-user < project-owner 160→``` 161→ 162→| Role | Description | 163→|------|-------------| 164→| `guest-user` | Unauthenticated request (public read-only access) | 165→| `logged-on-user` | Authenticated user (may not own the project) | 166→| `project-owner` | User owns the project being accessed | 167→ 168→**Two-Level Access Control:** 169→ 170→Routes specify `requiredRole: CwcRole` which is checked at two levels: 171→ 172→| requiredRole | Route-Level Check | Operation-Level Check | 173→|--------------|-------------------|----------------------| 174→| `guest-user` | Anyone allowed | Anyone allowed | 175→| `logged-on-user` | Must be authenticated | Must be authenticated | 176→| `project-owner` | Must be authenticated | Must be authenticated AND own the project | 177→ 178→**Key Design Decision:** Route-level for `project-owner` only checks authentication (no DB lookup). The actual ownership check happens at operation-level using `context.ownedProjects` from the JWT. 179→ 180→**Policy Functions:** 181→ 182→```typescript 183→// Route-level: Only checks authentication, no ownership 184→checkRouteAccess(context: RequestContext, requiredRole: CwcRole): RouteAccessResult 185→ 186→// Operation-level: Checks authentication AND ownership for project-owner 187→checkOperationAccess( 188→ context: RequestContext, 189→ operationContext: OperationContext, 190→ requiredRole: CwcRole 191→): OperationAccessResult 192→ 193→// Helper: Checks if user owns a specific project 194→isProjectOwner(context: RequestContext, projectId: string | undefined): boolean 195→``` 196→ 197→**Security:** 198→- Ownership verified from JWT claims (`context.ownedProjects.includes(projectId)`) 199→- `projectId` comes from URL path params (via `operationContext`), not request body 200→- No database lookups for access checks - all data comes from JWT 201→ 202→## Tables Handled 203→ 204→**Content Tables (Full CRUD + Soft Delete):**
Now updating the implementation status section.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "## Implementation Status\n\n**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.\n\nThe checklist tracks 7 phases:\n1. Package Foundation (config, Express service, health check) - **Complete**\n2. Authentication & Context Layer (JWT, request context) - **Complete**\n3. Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - **Complete**\n4. Access Policy System (route and operation policies)\n5. SqlFunction Layer (CRUD operations for all tables)\n6. CwcApiV1 Routes & Business Logic\n7. Integration Testing & Final Review\n\nEach phase includes: Implementation → Unit Tests → Security Review → Documentation Updates",
"new_string": "## Implementation Status\n\n**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.\n\nThe checklist tracks 7 phases:\n1. Package Foundation (config, Express service, health check) - **Complete**\n2. Authentication & Context Layer (JWT, request context) - **Complete**\n3. Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - **Complete**\n4. Access Policy System (route and operation policies) - **Complete**\n5. SqlFunction Layer (CRUD operations for all tables)\n6. CwcApiV1 Routes & Business Logic\n7. Integration Testing & Final Review\n\nEach phase includes: Implementation → Unit Tests → Security Review → Documentation Updates"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 337→- All content tables support soft delete (`enabled=false`) 338→- Hard delete available for GDPR compliance 339→- List queries automatically filter `enabled=true` 340→ 341→## Implementation Status 342→ 343→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`. 344→ 345→The checklist tracks 7 phases: 346→1. Package Foundation (config, Express service, health check) - **Complete** 347→2. Authentication & Context Layer (JWT, request context) - **Complete** 348→3. Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - **Complete** 349→4. Access Policy System (route and operation policies) - **Complete** 350→5. SqlFunction Layer (CRUD operations for all tables) 351→6. CwcApiV1 Routes & Business Logic 352→7. Integration Testing & Final Review 353→ 354→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates 355→ 356→## Unit Testing 357→ 358→**Test Infrastructure:**
Now let me also update the implementation checklist to mark Phase 4 as complete.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/cwc-api-implementation-checklist.md"
}Tool Result
1→# cwc-api Implementation Checklist
2→
3→This file tracks progress on the cwc-api implementation across coding sessions.
4→Update checkbox status as tasks are completed: `[ ]` → `[x]`
5→
6→---
7→
8→## Phase 1: Package Foundation
9→
10→### Package Structure
11→- [x] Create `package.json` with dependencies
12→- [x] Create `tsconfig.json` extending base config
13→- [x] Add package shortcut script to root `package.json`
14→
15→### Configuration
16→- [x] Create `src/config/config.types.ts` (CwcApiConfig, CwcApiConfigSecrets)
17→- [x] Create `src/config/loadConfig.ts`
18→- [x] Create `src/config/index.ts`
19→- [x] Generate .env files using cwc-configuration-helper (verified auto-discovery works)
20→
21→### Express Service
22→- [x] Create `src/index.ts` (Express service entry point)
23→- [x] Add health check endpoint (`/health/v1`)
24→- [x] Verify service starts and responds to health check
25→
26→### Unit Tests
27→- [x] Set up test infrastructure (jest.config.js, setup.ts, mocks/)
28→- [x] Create `src/__tests__/config.test.ts` - Test config loading
29→- [x] Create `src/__tests__/healthCheck.test.ts` - Test health endpoint
30→
31→### Security Review
32→- [x] Verify secrets are not logged (config.secrets always REDACTED)
33→- [x] Verify CORS configuration is appropriate (via createExpressService)
34→- [x] Verify rate limiting is configured (via createExpressService)
35→
36→### Documentation Updates
37→- [x] Update `CLAUDE.md` with package architecture
38→- [x] Update `README.md` with setup instructions
39→
40→---
41→
42→## Phase 2: Authentication & Context Layer
43→
44→**Architecture Decision:** cwc-api does NOT duplicate JWT verification logic. Instead, cwc-api
45→calls cwc-auth's `/auth/v1/verify-token` endpoint to validate tokens. This keeps auth logic
46→centralized in cwc-auth (single source of truth) at the cost of an additional HTTP call per request.
47→
48→**Architecture Refinement (Session 020):** Shared types moved to cwc-types, AuthClient moved to
49→cwc-backend-utils for reuse by other services.
50→
51→### Prerequisites (cwc-auth work)
52→- [x] Add `/auth/v1/verify-token` endpoint to cwc-auth ServiceApiV1
53→ - Input: JWT token (from Authorization header)
54→ - Output: `{ success: true, payload: UserJwtPayload }` or `{ success: false, error: ... }`
55→
56→### Shared Auth Types (cwc-types)
57→- [x] Create `cwc-types/src/authTypes.ts`
58→ - [x] Define `UserJwtPayload` type (moved from cwc-auth for sharing)
59→ - [x] Define `CwcRole` type: `'project-owner' | 'logged-on-user' | 'guest-user'`
60→ - [x] Define `VerifyTokenResponse` types (success/error)
61→ - [x] Define `VerifyTokenResult` type
62→- [x] Update cwc-auth to import `UserJwtPayload` from cwc-types
63→
64→### AuthClient (cwc-backend-utils)
65→- [x] Create `cwc-backend-utils/src/AuthClient/AuthClient.ts`
66→ - [x] HTTP POST to cwc-auth `/auth/v1/verify-token`
67→ - [x] Handles success and error responses
68→ - [x] Returns typed `VerifyTokenResult`
69→- [x] Export from `cwc-backend-utils/src/index.ts`
70→
71→### Request Context (cwc-api)
72→- [x] Create `src/context/context.types.ts`
73→ - [x] Define `AuthenticatedContext` and `GuestContext` types
74→ - [x] Define `RequestContext` union type
75→- [x] Create `src/context/createContext.ts`
76→ - [x] Uses AuthClient.verifyToken()
77→ - [x] Returns authenticated context on success
78→ - [x] Returns guest context on failure (no JWT, invalid, expired)
79→- [x] Create `src/context/index.ts`
80→
81→### Unit Tests
82→- [x] Create `src/__tests__/mocks/authClient.mock.ts` - AuthClient mock helpers
83→- [x] Create `src/__tests__/context/createContext.test.ts` - Test authenticated and guest contexts (10 tests)
84→
85→### Security Review
86→- [x] Verify cwc-auth endpoint validates JWT signature (HS256, proper secret)
87→- [x] Verify expired tokens are rejected by cwc-auth
88→- [x] Verify invalid tokens create guest context (no error details leaked to client)
89→- [x] Verify guest context has no elevated privileges
90→- [x] Verify userPkId comes from cwc-auth response, not request body
91→
92→### Documentation Updates
93→- [ ] Update `CLAUDE.md` with auth integration patterns
94→- [ ] Update `README.md` with authentication details
95→
96→---
97→
98→## Phase 3: Handler Architecture
99→
100→**Architecture Refinement (Session 021):** Session renewal happens on EVERY request (queries and mutations)
101→to keep sessions active, not just on specific triggers. This is the RPC-style approach where all
102→endpoints use POST.
103→
104→### Prerequisites (cwc-types, cwc-backend-utils)
105→- [x] Add `RenewSessionResult` types to cwc-types/authTypes.ts
106→- [x] Add `renewSession()` method to AuthClient in cwc-backend-utils
107→- [x] Update AuthClient mocks in cwc-api with renewSession helpers
108→
109→### Route Handler
110→- [x] Create `src/handlers/handler.types.ts`
111→ - [x] Define error codes (`CwcApiErrorCode`)
112→ - [x] Define response types (`CwcApiSuccessResponse`, `CwcApiErrorResponse`, `CwcApiHandlerResponse`)
113→ - [x] Define route config types (`CwcApiRouteConfig`, `CwcApiHandlerType`)
114→ - [x] Define operation types (`OperationContext`, `OperationResult`, `CwcApiOperation`)
115→ - [x] Define handler options types (`RouteHandlerOptions`, `QueryHandlerOptions`, `MutationHandlerOptions`)
116→ - [x] Define access policy stubs for Phase 4 (`RouteAccessResult`, `OperationAccessResult`)
117→- [x] Create `src/handlers/RouteHandler.ts`
118→ - [x] Check route-level access policy (role vs allowedRoles)
119→ - [x] Build operation context from path params
120→ - [x] Delegate to QueryHandler or MutationHandler based on handlerType
121→ - [x] Return 401 for unauthenticated access denied
122→ - [x] Return 403 for authenticated but unauthorized
123→- [x] Create `src/handlers/QueryHandler.ts`
124→ - [x] Operation-level access check (stub for Phase 4)
125→ - [x] Execute query operation
126→ - [x] Renew session (always, to keep session active)
127→ - [x] Format response with data, pagination, and new JWT
128→- [x] Create `src/handlers/MutationHandler.ts`
129→ - [x] Operation-level access check (stub for Phase 4)
130→ - [x] Execute mutation operation
131→ - [x] Renew session (always, to keep session active)
132→ - [x] Format response with data and new JWT
133→- [x] Create `src/handlers/index.ts`
134→
135→### Unit Tests
136→- [x] Create `src/__tests__/handlers/RouteHandler.test.ts` (14 tests)
137→- [x] Create `src/__tests__/handlers/QueryHandler.test.ts` (15 tests)
138→- [x] Create `src/__tests__/handlers/MutationHandler.test.ts` (15 tests)
139→- [x] Create `src/__tests__/mocks/routeConfig.mock.ts`
140→ - [x] `createMockRouteConfig()`, `createMockOperationContext()`
141→ - [x] `createMockGuestContext()`, `createMockAuthenticatedContext()`
142→ - [x] `mockOperationSuccess()`, `mockOperationFailure()`, `mockOperationThrows()`
143→
144→### Security Review
145→- [x] Verify error responses don't leak internal details (errorDetail only in dev mode)
146→- [x] Verify operation access check is stubbed (always allowed for Phase 3)
147→- [x] Verify session renewal failure doesn't fail the request (graceful degradation)
148→- [x] Verify guest users don't get session renewal
149→
150→### Documentation Updates
151→- [x] Update `CLAUDE.md` with handler architecture patterns
152→- [ ] Update `README.md` with request pipeline details
153→
154→---
155→
156→## Phase 4: Access Policy System
157→
158→### Policy Types
159→- [ ] Create `src/policies/policy.types.ts`
160→ - [ ] Define `RoutePolicy` type
161→ - [ ] Define `OperationPolicy` type
162→ - [ ] Define policy check result types
163→
164→### Route Policies
165→- [ ] Create `src/policies/routePolicies.ts`
166→ - [ ] Define which roles can access each route
167→ - [ ] Define `allowUnauthenticated` routes for guest access
168→
169→### Operation Policies
170→- [ ] Create `src/policies/operationPolicies.ts`
171→ - [ ] Define which roles can execute which operations
172→ - [ ] Define context-aware checks (e.g., project ownership)
173→
174→### Policy Enforcement
175→- [ ] Create `src/policies/checkRouteAccess.ts`
176→- [ ] Create `src/policies/checkOperationAccess.ts`
177→- [ ] Create `src/policies/index.ts`
178→
179→### Unit Tests
180→- [ ] Create `src/__tests__/policies/routePolicies.test.ts`
181→- [ ] Create `src/__tests__/policies/operationPolicies.test.ts`
182→- [ ] Create `src/__tests__/policies/checkRouteAccess.test.ts`
183→- [ ] Create `src/__tests__/policies/checkOperationAccess.test.ts`
184→
185→### Security Review
186→- [ ] Verify no privilege escalation paths (guest → user, user → owner)
187→- [ ] Verify ownership checks use userPkId from context (not request body)
188→- [ ] Verify all mutation routes require authentication
189→- [ ] Verify default-deny policy (routes not explicitly allowed are blocked)
190→- [ ] Verify policy checks cannot be bypassed via parameter manipulation
191→
192→### Documentation Updates
193→- [ ] Update `CLAUDE.md` with access policy patterns
194→- [ ] Update `README.md` with role-based access details
195→
196→---
197→
198→## Phase 5: SqlFunction Layer
199→
200→### Infrastructure
201→- [ ] Create `src/sql/sql.types.ts`
202→ - [ ] Define SqlFunction input/output types
203→ - [ ] Define pagination types (offset-based)
204→- [ ] Create `src/sql/index.ts`
205→
206→### Content Tables (Full CRUD + Soft Delete)
207→
208→#### project/
209→- [ ] Create `src/sql/project/selectProject.ts`
210→- [ ] Create `src/sql/project/listProjects.ts`
211→- [ ] Create `src/sql/project/insertProject.ts`
212→- [ ] Create `src/sql/project/updateProject.ts`
213→- [ ] Create `src/sql/project/deleteProject.ts` (hard delete)
214→- [ ] Create `src/sql/project/softDeleteProject.ts`
215→- [ ] Create `src/sql/project/index.ts`
216→
217→#### codingSession/
218→- [ ] Create `src/sql/codingSession/selectCodingSession.ts`
219→- [ ] Create `src/sql/codingSession/listCodingSessions.ts`
220→- [ ] Create `src/sql/codingSession/insertCodingSession.ts`
221→- [ ] Create `src/sql/codingSession/updateCodingSession.ts`
222→- [ ] Create `src/sql/codingSession/deleteCodingSession.ts`
223→- [ ] Create `src/sql/codingSession/softDeleteCodingSession.ts`
224→- [ ] Create `src/sql/codingSession/index.ts`
225→
226→#### codingSessionContent/
227→- [ ] Create `src/sql/codingSessionContent/selectCodingSessionContent.ts`
228→- [ ] Create `src/sql/codingSessionContent/listCodingSessionContents.ts`
229→- [ ] Create `src/sql/codingSessionContent/insertCodingSessionContent.ts`
230→- [ ] Create `src/sql/codingSessionContent/updateCodingSessionContent.ts`
231→- [ ] Create `src/sql/codingSessionContent/deleteCodingSessionContent.ts`
232→- [ ] Create `src/sql/codingSessionContent/softDeleteCodingSessionContent.ts`
233→- [ ] Create `src/sql/codingSessionContent/index.ts`
234→
235→#### codingSessionAttachment/
236→- [ ] Create `src/sql/codingSessionAttachment/selectCodingSessionAttachment.ts`
237→- [ ] Create `src/sql/codingSessionAttachment/listCodingSessionAttachments.ts`
238→- [ ] Create `src/sql/codingSessionAttachment/insertCodingSessionAttachment.ts`
239→- [ ] Create `src/sql/codingSessionAttachment/updateCodingSessionAttachment.ts`
240→- [ ] Create `src/sql/codingSessionAttachment/deleteCodingSessionAttachment.ts`
241→- [ ] Create `src/sql/codingSessionAttachment/softDeleteCodingSessionAttachment.ts`
242→- [ ] Create `src/sql/codingSessionAttachment/index.ts`
243→
244→#### comment/
245→- [ ] Create `src/sql/comment/selectComment.ts`
246→- [ ] Create `src/sql/comment/listComments.ts`
247→- [ ] Create `src/sql/comment/insertComment.ts`
248→- [ ] Create `src/sql/comment/updateComment.ts`
249→- [ ] Create `src/sql/comment/deleteComment.ts`
250→- [ ] Create `src/sql/comment/softDeleteComment.ts`
251→- [ ] Create `src/sql/comment/index.ts`
252→
253→#### reaction/
254→- [ ] Create `src/sql/reaction/selectReaction.ts`
255→- [ ] Create `src/sql/reaction/listReactions.ts`
256→- [ ] Create `src/sql/reaction/insertReaction.ts`
257→- [ ] Create `src/sql/reaction/updateReaction.ts`
258→- [ ] Create `src/sql/reaction/deleteReaction.ts`
259→- [ ] Create `src/sql/reaction/softDeleteReaction.ts`
260→- [ ] Create `src/sql/reaction/index.ts`
261→
262→#### contentReport/
263→- [ ] Create `src/sql/contentReport/selectContentReport.ts`
264→- [ ] Create `src/sql/contentReport/listContentReports.ts`
265→- [ ] Create `src/sql/contentReport/insertContentReport.ts`
266→- [ ] Create `src/sql/contentReport/updateContentReport.ts`
267→- [ ] Create `src/sql/contentReport/deleteContentReport.ts`
268→- [ ] Create `src/sql/contentReport/softDeleteContentReport.ts`
269→- [ ] Create `src/sql/contentReport/index.ts`
270→
271→#### abuseReport/
272→- [ ] Create `src/sql/abuseReport/selectAbuseReport.ts`
273→- [ ] Create `src/sql/abuseReport/listAbuseReports.ts`
274→- [ ] Create `src/sql/abuseReport/insertAbuseReport.ts`
275→- [ ] Create `src/sql/abuseReport/updateAbuseReport.ts`
276→- [ ] Create `src/sql/abuseReport/deleteAbuseReport.ts`
277→- [ ] Create `src/sql/abuseReport/softDeleteAbuseReport.ts`
278→- [ ] Create `src/sql/abuseReport/index.ts`
279→
280→### Read-Only Tables
281→
282→#### featureFlag/
283→- [ ] Create `src/sql/featureFlag/selectFeatureFlag.ts`
284→- [ ] Create `src/sql/featureFlag/listFeatureFlags.ts`
285→- [ ] Create `src/sql/featureFlag/index.ts`
286→
287→#### user/ (Limited Fields)
288→- [ ] Create `src/sql/user/selectUser.ts` (userPkId, enabled, username, loginType, loginDate, otpValidated)
289→- [ ] Create `src/sql/user/listUsers.ts` (same limited fields)
290→- [ ] Create `src/sql/user/index.ts`
291→
292→### Unit Tests
293→- [ ] Create `src/__tests__/sql/project.test.ts` - Test all project SqlFunctions
294→- [ ] Create `src/__tests__/sql/codingSession.test.ts` - Test all codingSession SqlFunctions
295→- [ ] Create `src/__tests__/sql/codingSessionContent.test.ts`
296→- [ ] Create `src/__tests__/sql/codingSessionAttachment.test.ts`
297→- [ ] Create `src/__tests__/sql/comment.test.ts`
298→- [ ] Create `src/__tests__/sql/reaction.test.ts`
299→- [ ] Create `src/__tests__/sql/contentReport.test.ts`
300→- [ ] Create `src/__tests__/sql/abuseReport.test.ts`
301→- [ ] Create `src/__tests__/sql/featureFlag.test.ts`
302→- [ ] Create `src/__tests__/sql/user.test.ts`
303→
304→### Security Review
305→- [ ] Verify all SqlFunctions use SqlClient (no raw SQL construction)
306→- [ ] Verify list queries filter `enabled=true` by default (soft-delete)
307→- [ ] Verify user SqlFunctions only expose limited fields
308→- [ ] Verify no mass assignment vulnerabilities (explicit field mapping)
309→- [ ] Verify pagination limits are enforced (prevent data dumping)
310→
311→### Documentation Updates
312→- [ ] Update `CLAUDE.md` with SqlFunction patterns
313→- [ ] Update `README.md` with data access layer details
314→
315→---
316→
317→## Phase 6: CwcApiV1 Routes & Business Logic
318→
319→### Route Structure
320→- [ ] Create `src/apis/CwcApiV1/routes.ts`
321→- [ ] Create `src/apis/CwcApiV1/types.ts`
322→- [ ] Create `src/apis/CwcApiV1/index.ts`
323→
324→### Project Operations
325→- [ ] Create `src/apis/CwcApiV1/queries/getProject.ts`
326→- [ ] Create `src/apis/CwcApiV1/queries/listProjects.ts`
327→- [ ] Create `src/apis/CwcApiV1/mutations/projectCreate.ts`
328→- [ ] Create `src/apis/CwcApiV1/mutations/projectUpdate.ts`
329→- [ ] Create `src/apis/CwcApiV1/mutations/projectDelete.ts`
330→
331→### Coding Session Operations
332→- [ ] Create `src/apis/CwcApiV1/queries/getCodingSession.ts`
333→- [ ] Create `src/apis/CwcApiV1/queries/listCodingSessions.ts`
334→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionCreate.ts`
335→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionUpdate.ts`
336→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionDelete.ts`
337→
338→### Coding Session Content Operations
339→- [ ] Create `src/apis/CwcApiV1/queries/getCodingSessionContent.ts`
340→- [ ] Create `src/apis/CwcApiV1/queries/listCodingSessionContents.ts`
341→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionContentCreate.ts`
342→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionContentUpdate.ts`
343→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionContentDelete.ts`
344→
345→### Coding Session Attachment Operations
346→- [ ] Create `src/apis/CwcApiV1/queries/getCodingSessionAttachment.ts`
347→- [ ] Create `src/apis/CwcApiV1/queries/listCodingSessionAttachments.ts`
348→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionAttachmentCreate.ts`
349→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionAttachmentUpdate.ts`
350→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionAttachmentDelete.ts`
351→
352→### Comment Operations
353→- [ ] Create `src/apis/CwcApiV1/queries/getComment.ts`
354→- [ ] Create `src/apis/CwcApiV1/queries/listComments.ts`
355→- [ ] Create `src/apis/CwcApiV1/mutations/commentCreate.ts`
356→- [ ] Create `src/apis/CwcApiV1/mutations/commentUpdate.ts`
357→- [ ] Create `src/apis/CwcApiV1/mutations/commentDelete.ts`
358→
359→### Reaction Operations
360→- [ ] Create `src/apis/CwcApiV1/queries/getReaction.ts`
361→- [ ] Create `src/apis/CwcApiV1/queries/listReactions.ts`
362→- [ ] Create `src/apis/CwcApiV1/mutations/reactionCreate.ts`
363→- [ ] Create `src/apis/CwcApiV1/mutations/reactionDelete.ts`
364→
365→### Content Report Operations
366→- [ ] Create `src/apis/CwcApiV1/queries/getContentReport.ts`
367→- [ ] Create `src/apis/CwcApiV1/queries/listContentReports.ts`
368→- [ ] Create `src/apis/CwcApiV1/mutations/contentReportCreate.ts`
369→- [ ] Create `src/apis/CwcApiV1/mutations/contentReportUpdate.ts`
370→
371→### Abuse Report Operations
372→- [ ] Create `src/apis/CwcApiV1/queries/getAbuseReport.ts`
373→- [ ] Create `src/apis/CwcApiV1/queries/listAbuseReports.ts`
374→- [ ] Create `src/apis/CwcApiV1/mutations/abuseReportCreate.ts`
375→- [ ] Create `src/apis/CwcApiV1/mutations/abuseReportUpdate.ts`
376→
377→### Route Registration
378→- [ ] Register all routes in `routes.ts`
379→- [ ] Wire up routes to Express in `src/index.ts`
380→
381→### Unit Tests
382→- [ ] Create `src/__tests__/apis/CwcApiV1/queries/getProject.test.ts`
383→- [ ] Create `src/__tests__/apis/CwcApiV1/queries/listProjects.test.ts`
384→- [ ] Create `src/__tests__/apis/CwcApiV1/mutations/projectCreate.test.ts`
385→- [ ] Create `src/__tests__/apis/CwcApiV1/mutations/projectUpdate.test.ts`
386→- [ ] Create `src/__tests__/apis/CwcApiV1/mutations/projectDelete.test.ts`
387→- [ ] Create tests for remaining query/mutation endpoints (follow same pattern)
388→
389→### Security Review
390→- [ ] Verify IDOR prevention (users can only access their own resources)
391→- [ ] Verify business logic doesn't allow unauthorized state transitions
392→- [ ] Verify content report/abuse report workflows don't expose reporter identity
393→- [ ] Verify rate limiting is appropriate per operation type
394→- [ ] Verify all user input is validated via cwc-schema before processing
395→
396→### Documentation Updates
397→- [ ] Update `CLAUDE.md` with API route patterns
398→- [ ] Update `README.md` with complete endpoint documentation
399→
400→---
401→
402→## Phase 7: Integration Testing & Final Review
403→
404→### Integration Tests
405→- [ ] Create `src/__tests__/integration/` directory
406→- [ ] Write end-to-end tests for complete request flows
407→- [ ] Write tests for cross-component interactions
408→- [ ] Write tests for error scenarios across layers
409→
410→### Security Review (Final)
411→- [ ] Verify security-focused test coverage (auth bypass, privilege escalation, IDOR)
412→- [ ] Verify error handling tests don't leak sensitive information
413→- [ ] Conduct final end-to-end security review of all phases
414→- [ ] Document any security considerations for deployment
415→
416→### Documentation Updates
417→- [ ] Update `CLAUDE.md` with testing patterns and final review
418→- [ ] Update `README.md` with testing instructions and final review
419→
420→---
421→
422→## Session Notes
423→
424→<!-- Add notes about decisions, blockers, or context for future sessions -->
425→
426→### Session 1
427→- Initial checklist created
428→- Specification reviewed and clarified:
429→ - Guest access: Create context with 'guest-user' role for unauthenticated requests
430→ - Session renewal: cwc-api calls cwc-auth /renew-session
431→ - Pagination: Offset-based (page/pageSize)
432→ - Soft delete: All content tables
433→ - RouteHandler class chosen over middleware approach
434→ - SqlFunction naming: `list` instead of `selectMultiple`
435→ - Added read-only SqlFunctions for featureFlag and user (limited fields)
436→
437→### Session 2 (020)
438→- **Phase 1 COMPLETED**
439→ - Added `dev` script to package.json using `tsx` (better ESM support than ts-node)
440→ - Fixed cwc-backend-utils ESM issues:
441→ - `loadDotEnv.ts`: Added ESM-compatible `__dirname` using `import.meta.url`
442→ - `express/service.ts`: Fixed wildcard route syntax for path-to-regexp 8.x (`/*` → `/{*splat}`)
443→ - Service starts successfully and health check responds (POST /health/v1)
444→
445→- **Architecture Decision: Centralized Authentication**
446→ - cwc-api will NOT duplicate JWT verification logic
447→ - cwc-api will call cwc-auth's `/auth/v1/verify-token` endpoint
448→ - Trade-off: Additional HTTP call per request, but auth logic stays centralized
449→ - **BLOCKER:** Phase 2 cannot proceed until cwc-auth has `/verify-token` endpoint
450→
451→- **Phase 2 Revised**
452→ - Updated checklist to reflect API-call approach
453→ - Removed local JWT utilities (verifyUserJwt, extractJwtFromHeader)
454→ - Added prerequisite: cwc-auth must implement /verify-token first
455→
456→- **Phase 2 COMPLETED**
457→ - Implemented shared auth types in cwc-types (`src/authTypes.ts`):
458→ - `UserJwtPayload`, `CwcRole`, `VerifyTokenResponse`, `VerifyTokenResult`
459→ - Updated cwc-auth to import `UserJwtPayload` from cwc-types (backwards-compatible re-export)
460→ - Created `AuthClient` in cwc-backend-utils (`src/AuthClient/`):
461→ - HTTP client for cwc-auth, similar pattern to SqlClient
462→ - `verifyToken(authHeader)` returns typed `VerifyTokenResult`
463→ - Handles 401 responses, network errors, timeouts
464→ - Created context layer in cwc-api (`src/context/`):
465→ - `context.types.ts`: `AuthenticatedContext`, `GuestContext`, `RequestContext`
466→ - `createContext.ts`: Uses AuthClient, returns authenticated or guest context
467→ - Created test mocks (`src/__tests__/mocks/authClient.mock.ts`):
468→ - `createMockAuthClient()`, `createMockUserJwtPayload()`
469→ - `mockVerifyTokenSuccess()`, `mockVerifyTokenFailure()`
470→ - All 10 context tests pass, all 38 cwc-api tests pass
471→ - All affected packages typecheck: cwc-types, cwc-auth, cwc-backend-utils, cwc-api
472→ - **Note:** AuthClient unit tests deferred (cwc-backend-utils has no test infrastructure)
473→ - Functionality verified through cwc-api context tests
474→
475→### Session 3 (021)
476→- **Phase 3 COMPLETED**
477→ - Added `RenewSessionResult` types to cwc-types/authTypes.ts
478→ - Added `renewSession()` method to AuthClient in cwc-backend-utils
479→ - Created handler architecture:
480→ - `handler.types.ts`: Error codes, response types, route config, operation types, access policy stubs
481→ - `RouteHandler.ts`: Route access check, delegates to Query/MutationHandler
482→ - `QueryHandler.ts`: Execute query, renew session, format response
483→ - `MutationHandler.ts`: Execute mutation, renew session, format response
484→ - Created comprehensive test mocks (`routeConfig.mock.ts`):
485→ - `createMockRouteConfig()`, `createMockOperationContext()`
486→ - `createMockGuestContext()`, `createMockAuthenticatedContext()`
487→ - `mockOperationSuccess()`, `mockOperationFailure()`, `mockOperationThrows()`
488→ - Updated config mocks with consistent naming: `getUnitConfig()`, `createDevConfig()`, `createProdConfig()`
489→ - All 44 handler tests pass (14 RouteHandler + 15 QueryHandler + 15 MutationHandler)
490→
491→- **Key Design Decisions:**
492→ - Session renewal on EVERY request (not just triggers) to keep sessions active
493→ - RPC-style API: All endpoints use POST, no GET/PUT/DELETE
494→ - Graceful degradation: Session renewal failure doesn't fail the request
495→ - Operation access check stubbed for Phase 4 (always allowed for now)
496→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/cwc-api-implementation-checklist.md",
"old_string": "## Phase 4: Access Policy System\n\n### Policy Types\n- [ ] Create `src/policies/policy.types.ts`\n - [ ] Define `RoutePolicy` type\n - [ ] Define `OperationPolicy` type\n - [ ] Define policy check result types\n\n### Route Policies\n- [ ] Create `src/policies/routePolicies.ts`\n - [ ] Define which roles can access each route\n - [ ] Define `allowUnauthenticated` routes for guest access\n\n### Operation Policies\n- [ ] Create `src/policies/operationPolicies.ts`\n - [ ] Define which roles can execute which operations\n - [ ] Define context-aware checks (e.g., project ownership)\n\n### Policy Enforcement\n- [ ] Create `src/policies/checkRouteAccess.ts`\n- [ ] Create `src/policies/checkOperationAccess.ts`\n- [ ] Create `src/policies/index.ts`\n\n### Unit Tests\n- [ ] Create `src/__tests__/policies/routePolicies.test.ts`\n- [ ] Create `src/__tests__/policies/operationPolicies.test.ts`\n- [ ] Create `src/__tests__/policies/checkRouteAccess.test.ts`\n- [ ] Create `src/__tests__/policies/checkOperationAccess.test.ts`\n\n### Security Review\n- [ ] Verify no privilege escalation paths (guest → user, user → owner)\n- [ ] Verify ownership checks use userPkId from context (not request body)\n- [ ] Verify all mutation routes require authentication\n- [ ] Verify default-deny policy (routes not explicitly allowed are blocked)\n- [ ] Verify policy checks cannot be bypassed via parameter manipulation\n\n### Documentation Updates\n- [ ] Update `CLAUDE.md` with access policy patterns\n- [ ] Update `README.md` with role-based access details",
"new_string": "## Phase 4: Access Policy System\n\n### Policy Types\n- [x] Create `src/policies/policy.types.ts`\n - [x] Re-export `RouteAccessResult`, `OperationAccessResult` from handler.types.ts\n\n### Route Policies\n- [x] Create `src/policies/checkRouteAccess.ts`\n - [x] Check authentication only (no ownership check)\n - [x] Uses role hierarchy: guest-user < logged-on-user < project-owner\n\n### Operation Policies\n- [x] Create `src/policies/checkOperationAccess.ts`\n - [x] Check authentication AND ownership for project-owner routes\n - [x] `isProjectOwner()` helper uses `context.ownedProjects.includes(projectId)`\n - [x] `effectiveRole` determines the user's role for this operation\n\n### Policy Enforcement\n- [x] Update `src/handlers/handler.types.ts` - Replace `allowedRoles[]` with `requiredRole: CwcRole`\n- [x] Update `src/handlers/RequestHandler.ts` - Use imported `checkRouteAccess`\n- [x] Update `src/handlers/QueryHandler.ts` - Use imported `checkOperationAccess`\n- [x] Update `src/handlers/MutationHandler.ts` - Use imported `checkOperationAccess`\n- [x] Create `src/policies/index.ts`\n\n### Unit Tests\n- [x] Create `src/__tests__/policies/checkRouteAccess.test.ts`\n- [x] Create `src/__tests__/policies/checkOperationAccess.test.ts`\n- [x] Update `src/__tests__/mocks/routeConfig.mock.ts` - Use `requiredRole`\n- [x] Update existing handler tests to use `requiredRole`\n\n### Security Review\n- [x] Verify no privilege escalation paths (guest → user, user → owner)\n- [x] Verify ownership checks use `context.ownedProjects` from JWT (not request body)\n- [x] Verify `projectId` comes from path params (`operationContext`), not payload\n- [x] Verify route-level only checks authentication (no DB lookup for ownership)\n- [x] Verify generic error messages in production (`errorDetail` only in dev)\n\n### Documentation Updates\n- [x] Update `CLAUDE.md` with access policy patterns\n- [ ] Update `README.md` with role-based access details"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/cwc-api-implementation-checklist.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 152→- [ ] Update `README.md` with request pipeline details 153→ 154→--- 155→ 156→## Phase 4: Access Policy System 157→ 158→### Policy Types 159→- [x] Create `src/policies/policy.types.ts` 160→ - [x] Re-export `RouteAccessResult`, `OperationAccessResult` from handler.types.ts 161→ 162→### Route Policies 163→- [x] Create `src/policies/checkRouteAccess.ts` 164→ - [x] Check authentication only (no ownership check) 165→ - [x] Uses role hierarchy: guest-user < logged-on-user < project-owner 166→ 167→### Operation Policies 168→- [x] Create `src/policies/checkOperationAccess.ts` 169→ - [x] Check authentication AND ownership for project-owner routes 170→ - [x] `isProjectOwner()` helper uses `context.ownedProjects.includes(projectId)` 171→ - [x] `effectiveRole` determines the user's role for this operation 172→ 173→### Policy Enforcement 174→- [x] Update `src/handlers/handler.types.ts` - Replace `allowedRoles[]` with `requiredRole: CwcRole` 175→- [x] Update `src/handlers/RequestHandler.ts` - Use imported `checkRouteAccess` 176→- [x] Update `src/handlers/QueryHandler.ts` - Use imported `checkOperationAccess` 177→- [x] Update `src/handlers/MutationHandler.ts` - Use imported `checkOperationAccess` 178→- [x] Create `src/policies/index.ts` 179→ 180→### Unit Tests 181→- [x] Create `src/__tests__/policies/checkRouteAccess.test.ts` 182→- [x] Create `src/__tests__/policies/checkOperationAccess.test.ts` 183→- [x] Update `src/__tests__/mocks/routeConfig.mock.ts` - Use `requiredRole` 184→- [x] Update existing handler tests to use `requiredRole` 185→ 186→### Security Review 187→- [x] Verify no privilege escalation paths (guest → user, user → owner) 188→- [x] Verify ownership checks use `context.ownedProjects` from JWT (not request body) 189→- [x] Verify `projectId` comes from path params (`operationContext`), not payload 190→- [x] Verify route-level only checks authentication (no DB lookup for ownership) 191→- [x] Verify generic error messages in production (`errorDetail` only in dev) 192→ 193→### Documentation Updates 194→- [x] Update `CLAUDE.md` with access policy patterns 195→- [ ] Update `README.md` with role-based access details 196→ 197→--- 198→ 199→## Phase 5: SqlFunction Layer
Now adding session notes for Phase 4 completion.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/cwc-api-implementation-checklist.md",
"old_string": "### Session 3 (021)\n- **Phase 3 COMPLETED**\n - Added `RenewSessionResult` types to cwc-types/authTypes.ts\n - Added `renewSession()` method to AuthClient in cwc-backend-utils\n - Created handler architecture:\n - `handler.types.ts`: Error codes, response types, route config, operation types, access policy stubs\n - `RouteHandler.ts`: Route access check, delegates to Query/MutationHandler\n - `QueryHandler.ts`: Execute query, renew session, format response\n - `MutationHandler.ts`: Execute mutation, renew session, format response\n - Created comprehensive test mocks (`routeConfig.mock.ts`):\n - `createMockRouteConfig()`, `createMockOperationContext()`\n - `createMockGuestContext()`, `createMockAuthenticatedContext()`\n - `mockOperationSuccess()`, `mockOperationFailure()`, `mockOperationThrows()`\n - Updated config mocks with consistent naming: `getUnitConfig()`, `createDevConfig()`, `createProdConfig()`\n - All 44 handler tests pass (14 RouteHandler + 15 QueryHandler + 15 MutationHandler)\n\n- **Key Design Decisions:**\n - Session renewal on EVERY request (not just triggers) to keep sessions active\n - RPC-style API: All endpoints use POST, no GET/PUT/DELETE\n - Graceful degradation: Session renewal failure doesn't fail the request\n - Operation access check stubbed for Phase 4 (always allowed for now)",
"new_string": "### Session 3 (021)\n- **Phase 3 COMPLETED**\n - Added `RenewSessionResult` types to cwc-types/authTypes.ts\n - Added `renewSession()` method to AuthClient in cwc-backend-utils\n - Created handler architecture:\n - `handler.types.ts`: Error codes, response types, route config, operation types, access policy stubs\n - `RouteHandler.ts`: Route access check, delegates to Query/MutationHandler\n - `QueryHandler.ts`: Execute query, renew session, format response\n - `MutationHandler.ts`: Execute mutation, renew session, format response\n - Created comprehensive test mocks (`routeConfig.mock.ts`):\n - `createMockRouteConfig()`, `createMockOperationContext()`\n - `createMockGuestContext()`, `createMockAuthenticatedContext()`\n - `mockOperationSuccess()`, `mockOperationFailure()`, `mockOperationThrows()`\n - Updated config mocks with consistent naming: `getUnitConfig()`, `createDevConfig()`, `createProdConfig()`\n - All 44 handler tests pass (14 RouteHandler + 15 QueryHandler + 15 MutationHandler)\n\n- **Key Design Decisions:**\n - Session renewal on EVERY request (not just triggers) to keep sessions active\n - RPC-style API: All endpoints use POST, no GET/PUT/DELETE\n - Graceful degradation: Session renewal failure doesn't fail the request\n - Operation access check stubbed for Phase 4 (always allowed for now)\n\n### Session 4 (023)\n- **Phase 4 COMPLETED**\n - Created policy module (`src/policies/`):\n - `policy.types.ts`: Re-exports RouteAccessResult, OperationAccessResult\n - `checkRouteAccess.ts`: Route-level authentication check\n - `checkOperationAccess.ts`: Operation-level access with ownership check\n - `isProjectOwner()` helper function\n - `index.ts`: Exports\n - Updated handler types:\n - Replaced `allowedRoles: CwcRole[]` with `requiredRole: CwcRole`\n - Single field for both route and operation level access control\n - Updated handlers to use policy functions:\n - RequestHandler uses `checkRouteAccess(context, routeConfig.requiredRole)`\n - QueryHandler uses `checkOperationAccess(context, operationContext, routeConfig.requiredRole)`\n - MutationHandler uses same pattern as QueryHandler\n - Created policy tests:\n - `checkRouteAccess.test.ts` (9 tests)\n - `checkOperationAccess.test.ts` (14 tests)\n - Updated all handler tests to use `requiredRole`\n - All 121 tests pass\n\n- **Key Design Decisions:**\n - **Role Hierarchy:** `guest-user < logged-on-user < project-owner`\n - **Two-Level Access Control:**\n - Route-level: Only checks authentication (no DB lookup)\n - Operation-level: Checks authentication AND ownership for `project-owner`\n - **requiredRole interpretation differs by level:**\n - Route: `project-owner` means \"must be authenticated\"\n - Operation: `project-owner` means \"must own the project\"\n - **Ownership from JWT only:** Uses `context.ownedProjects.includes(projectId)`, no database lookup\n - **projectId from path params:** Comes from `operationContext.projectId`, not request body (security)"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/cwc-api-implementation-checklist.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 472→ - All affected packages typecheck: cwc-types, cwc-auth, cwc-backend-utils, cwc-api 473→ - **Note:** AuthClient unit tests deferred (cwc-backend-utils has no test infrastructure) 474→ - Functionality verified through cwc-api context tests 475→ 476→### Session 3 (021) 477→- **Phase 3 COMPLETED** 478→ - Added `RenewSessionResult` types to cwc-types/authTypes.ts 479→ - Added `renewSession()` method to AuthClient in cwc-backend-utils 480→ - Created handler architecture: 481→ - `handler.types.ts`: Error codes, response types, route config, operation types, access policy stubs 482→ - `RouteHandler.ts`: Route access check, delegates to Query/MutationHandler 483→ - `QueryHandler.ts`: Execute query, renew session, format response 484→ - `MutationHandler.ts`: Execute mutation, renew session, format response 485→ - Created comprehensive test mocks (`routeConfig.mock.ts`): 486→ - `createMockRouteConfig()`, `createMockOperationContext()` 487→ - `createMockGuestContext()`, `createMockAuthenticatedContext()` 488→ - `mockOperationSuccess()`, `mockOperationFailure()`, `mockOperationThrows()` 489→ - Updated config mocks with consistent naming: `getUnitConfig()`, `createDevConfig()`, `createProdConfig()` 490→ - All 44 handler tests pass (14 RouteHandler + 15 QueryHandler + 15 MutationHandler) 491→ 492→- **Key Design Decisions:** 493→ - Session renewal on EVERY request (not just triggers) to keep sessions active 494→ - RPC-style API: All endpoints use POST, no GET/PUT/DELETE 495→ - Graceful degradation: Session renewal failure doesn't fail the request 496→ - Operation access check stubbed for Phase 4 (always allowed for now) 497→ 498→### Session 4 (023) 499→- **Phase 4 COMPLETED** 500→ - Created policy module (`src/policies/`): 501→ - `policy.types.ts`: Re-exports RouteAccessResult, OperationAccessResult 502→ - `checkRouteAccess.ts`: Route-level authentication check 503→ - `checkOperationAccess.ts`: Operation-level access with ownership check 504→ - `isProjectOwner()` helper function 505→ - `index.ts`: Exports 506→ - Updated handler types: 507→ - Replaced `allowedRoles: CwcRole[]` with `requiredRole: CwcRole` 508→ - Single field for both route and operation level access control 509→ - Updated handlers to use policy functions: 510→ - RequestHandler uses `checkRouteAccess(context, routeConfig.requiredRole)` 511→ - QueryHandler uses `checkOperationAccess(context, operationContext, routeConfig.requiredRole)` 512→ - MutationHandler uses same pattern as QueryHandler 513→ - Created policy tests: 514→ - `checkRouteAccess.test.ts` (9 tests) 515→ - `checkOperationAccess.test.ts` (14 tests) 516→ - Updated all handler tests to use `requiredRole` 517→ - All 121 tests pass 518→ 519→- **Key Design Decisions:** 520→ - **Role Hierarchy:** `guest-user < logged-on-user < project-owner` 521→ - **Two-Level Access Control:** 522→ - Route-level: Only checks authentication (no DB lookup) 523→ - Operation-level: Checks authentication AND ownership for `project-owner` 524→ - **requiredRole interpretation differs by level:** 525→ - Route: `project-owner` means "must be authenticated" 526→ - Operation: `project-owner` means "must own the project" 527→ - **Ownership from JWT only:** Uses `context.ownedProjects.includes(projectId)`, no database lookup 528→ - **projectId from path params:** Comes from `operationContext.projectId`, not request body (security) 529→
TodoWrite
{
"todos": [
{
"content": "Create src/policies/policy.types.ts",
"status": "completed",
"activeForm": "Creating policy.types.ts"
},
{
"content": "Create src/policies/checkRouteAccess.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.ts"
},
{
"content": "Create src/policies/checkOperationAccess.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.ts"
},
{
"content": "Create src/policies/index.ts",
"status": "completed",
"activeForm": "Creating index.ts"
},
{
"content": "Update handler.types.ts (replace allowedRoles with requiredRole)",
"status": "completed",
"activeForm": "Updating handler.types.ts"
},
{
"content": "Update RequestHandler.ts (use checkRouteAccess)",
"status": "completed",
"activeForm": "Updating RequestHandler.ts"
},
{
"content": "Update QueryHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating QueryHandler.ts"
},
{
"content": "Update MutationHandler.ts (use checkOperationAccess)",
"status": "completed",
"activeForm": "Updating MutationHandler.ts"
},
{
"content": "Update routeConfig.mock.ts (use requiredRole)",
"status": "completed",
"activeForm": "Updating routeConfig.mock.ts"
},
{
"content": "Create checkRouteAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkRouteAccess.test.ts"
},
{
"content": "Create checkOperationAccess.test.ts",
"status": "completed",
"activeForm": "Creating checkOperationAccess.test.ts"
},
{
"content": "Update existing handler tests",
"status": "completed",
"activeForm": "Updating handler tests"
},
{
"content": "Typecheck all packages",
"status": "completed",
"activeForm": "Running typecheck"
},
{
"content": "Run tests",
"status": "completed",
"activeForm": "Running tests"
},
{
"content": "Update CLAUDE.md",
"status": "completed",
"activeForm": "Updating CLAUDE.md"
}
]
}Tool Result
Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
Phase 4: Access Policy System - Complete
All Phase 4 tasks have been completed:
Created Files:
src/policies/policy.types.ts- Re-exports RouteAccessResult, OperationAccessResultsrc/policies/checkRouteAccess.ts- Route-level authentication checksrc/policies/checkOperationAccess.ts- Operation-level access with ownership check, includesisProjectOwner()helpersrc/policies/index.ts- Module exportssrc/__tests__/policies/checkRouteAccess.test.ts- 9 testssrc/__tests__/policies/checkOperationAccess.test.ts- 14 tests
Modified Files:
src/handlers/handler.types.ts- ReplacedallowedRoles: CwcRole[]withrequiredRole: CwcRolesrc/handlers/RequestHandler.ts- Uses importedcheckRouteAccesssrc/handlers/QueryHandler.ts- Uses importedcheckOperationAccesssrc/handlers/MutationHandler.ts- Uses importedcheckOperationAccesssrc/__tests__/mocks/routeConfig.mock.ts- Updated to userequiredRole- Handler test files updated with
requiredRole
Results:
- Typecheck: Passed
- Tests: 121 passed
- CLAUDE.md: Updated with access policy documentation
- Implementation checklist: Phase 4 marked complete with session notes
in packages/cwc-api/src/policies/checkOperationAccess.ts, in the checkOperationAccess function, the following reason is set when there is no projectId: 'Project ID required for ownership check'. I don't think that this error message will ever be presented to a user, so there is no need to create a pretty name for projectId. I would prefer that we stick with the actual names of fields in a case like this to avoid ambiguity - when we receive a reason message with the pretty name, does Project ID refer to projectId or projectPkId? Let's make this a rule for error and informational messages within the cwc-api please. Can you check for any other occurrences of this sort of thing? Thank you!
You make a great point about clarity in error messages. Let me fix that and check for other occurrences.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts",
"old_string": "return {\n allowed: false,\n reason: projectId\n ? `User does not own project '${projectId}'`\n : 'Project ID required for ownership check',\n };",
"new_string": "return {\n allowed: false,\n reason: projectId\n ? `User does not own project '${projectId}'`\n : 'projectId is required for ownership check',\n };"
}Tool Result
<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts"
}Tool Result
1→'use strict';
2→
3→import type { CwcRole } from 'cwc-types';
4→import type { RequestContext } from '../context';
5→import type { OperationContext } from '../handlers/handler.types';
6→import type { OperationAccessResult } from './policy.types';
7→
8→/**
9→ * Check if the user owns the specified project.
10→ *
11→ * Uses context.ownedProjects which is populated from JWT claims verified by cwc-auth.
12→ * SECURITY: Never use projectId from request body - only from path params.
13→ *
14→ * @param context - The request context
15→ * @param projectId - The project ID to check ownership for
16→ * @returns true if the user owns the project, false otherwise
17→ */
18→export function isProjectOwner(
19→ context: RequestContext,
20→ projectId: string | undefined
21→): boolean {
22→ if (!context.isAuthenticated) {
23→ return false;
24→ }
25→ if (!projectId) {
26→ return false;
27→ }
28→ return context.ownedProjects.includes(projectId);
29→}
30→
31→/**
32→ * Check if the current context can perform an operation based on required role.
33→ *
34→ * Operation-level access checks both authentication AND ownership for project-owner routes.
35→ * This is where the actual ownership verification happens.
36→ *
37→ * Role hierarchy (from least to most privileged):
38→ * - guest-user: No authentication required
39→ * - logged-on-user: Must be authenticated
40→ * - project-owner: Must be authenticated AND own the project
41→ *
42→ * @param context - The request context (authenticated or guest)
43→ * @param operationContext - The operation context containing projectId
44→ * @param requiredRole - The minimum role required to perform the operation
45→ * @returns OperationAccessResult indicating whether access is allowed and the effective role
46→ */
47→export function checkOperationAccess(
48→ context: RequestContext,
49→ operationContext: OperationContext,
50→ requiredRole: CwcRole
51→): OperationAccessResult {
52→ // guest-user: anyone can perform the operation
53→ if (requiredRole === 'guest-user') {
54→ return {
55→ allowed: true,
56→ effectiveRole: context.role,
57→ };
58→ }
59→
60→ // Must be authenticated for logged-on-user or project-owner
61→ if (!context.isAuthenticated) {
62→ return {
63→ allowed: false,
64→ reason: 'Authentication required',
65→ };
66→ }
67→
68→ // logged-on-user: authenticated is enough
69→ if (requiredRole === 'logged-on-user') {
70→ return {
71→ allowed: true,
72→ effectiveRole: context.role,
73→ };
74→ }
75→
76→ // project-owner: must own the project
77→ const { projectId } = operationContext;
78→ if (!isProjectOwner(context, projectId)) {
79→ return {
80→ allowed: false,
81→ reason: projectId
82→ ? `User does not own project '${projectId}'`
83→ : 'Project ID required for ownership check',
84→ };
85→ }
86→
87→ return {
88→ allowed: true,
89→ effectiveRole: 'project-owner',
90→ };
91→}
92→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts",
"old_string": "return {\n allowed: false,\n reason: projectId\n ? `User does not own project '${projectId}'`\n : 'Project ID required for ownership check',\n };",
"new_string": "return {\n allowed: false,\n reason: projectId\n ? `User does not own project '${projectId}'`\n : 'projectId is required for ownership check',\n };"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
75→
76→ // project-owner: must own the project
77→ const { projectId } = operationContext;
78→ if (!isProjectOwner(context, projectId)) {
79→ return {
80→ allowed: false,
81→ reason: projectId
82→ ? `User does not own project '${projectId}'`
83→ : 'projectId is required for ownership check',
84→ };
85→ }
86→
87→ return {
88→ allowed: true,Now let me update the test and search for other occurrences.
Grep
{
"pattern": "Project ID|User ID|Session ID",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src",
"output_mode": "content"
}Tool Result
packages/cwc-api/src/__tests__/policies/checkOperationAccess.test.ts:171: expect(result.reason).toBe('Project ID required for ownership check');Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/policies/checkOperationAccess.test.ts",
"offset": 160,
"limit": 20
}Tool Result
160→ it('should deny when projectId is not provided', () => {
161→ const context = createMockAuthenticatedContext({
162→ ownedProjects: ['project-1'],
163→ });
164→ const operationContext = createMockOperationContext({
165→ context,
166→ projectId: undefined,
167→ });
168→ const result = checkOperationAccess(context, operationContext, 'project-owner');
169→
170→ expect(result.allowed).toBe(false);
171→ expect(result.reason).toBe('Project ID required for ownership check');
172→ expect(result.effectiveRole).toBeUndefined();
173→ });
174→ });
175→
176→ describe('effectiveRole determination', () => {
177→ it('should return guest-user for guests on public routes', () => {
178→ const context = createMockGuestContext();
179→ const operationContext = createMockOperationContext({ context });
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/policies/checkOperationAccess.test.ts",
"old_string": "expect(result.reason).toBe('Project ID required for ownership check');",
"new_string": "expect(result.reason).toBe('projectId is required for ownership check');"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/policies/checkOperationAccess.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
167→ });
168→ const result = checkOperationAccess(context, operationContext, 'project-owner');
169→
170→ expect(result.allowed).toBe(false);
171→ expect(result.reason).toBe('projectId is required for ownership check');
172→ expect(result.effectiveRole).toBeUndefined();
173→ });
174→ });
175→Let me search more broadly for similar patterns.
Grep
{
"pattern": "reason.*[A-Z][a-z]+ [A-Z][A-Z]",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src",
"output_mode": "content"
}Tool Result
No matches found
Grep
{
"pattern": "reason:.*required",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src",
"output_mode": "content",
"-i": true
}Tool Result
packages/cwc-api/src/policies/checkOperationAccess.ts:64: reason: 'Authentication required', packages/cwc-api/src/policies/checkRouteAccess.ts:36: reason: 'Authentication required',
Grep
{
"pattern": "reason:",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src",
"output_mode": "content",
"glob": "*.ts"
}Tool Result
packages/cwc-api/src/index.ts:158: console.error('[cwc-api] Unhandled Rejection at:', promise, 'reason:', reason);
packages/cwc-api/src/policies/checkOperationAccess.ts:64: reason: 'Authentication required',
packages/cwc-api/src/policies/checkOperationAccess.ts:81: reason: projectId
packages/cwc-api/src/policies/checkRouteAccess.ts:36: reason: 'Authentication required',Grep
{
"pattern": "errorMessage:",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src",
"output_mode": "content",
"glob": "*.ts"
}Tool Result
packages/cwc-api/src/handlers/handler.types.ts:68: errorMessage: string;
packages/cwc-api/src/handlers/handler.types.ts:138: | { success: false; errorCode: CwcApiErrorCode; errorMessage: string };
packages/cwc-api/src/handlers/responseUtils.ts:44: errorMessage: 'Operation not permitted',
packages/cwc-api/src/handlers/responseUtils.ts:55: errorMessage: string
packages/cwc-api/src/handlers/responseUtils.ts:81: errorMessage: 'An internal error occurred',
packages/cwc-api/src/handlers/RequestHandler.ts:137: errorMessage: 'Access denied',
packages/cwc-api/src/__tests__/handlers/responseUtils.test.ts:32: errorMessage: 'Operation not permitted',
packages/cwc-api/src/__tests__/handlers/responseUtils.test.ts:45: errorMessage: 'Resource not found',
packages/cwc-api/src/__tests__/handlers/responseUtils.test.ts:59: errorMessage: 'An internal error occurred',
packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts:109: errorMessage: string = 'Resource not found'No other occurrences found. Let me run the tests to verify the change.
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPattern=\"checkOperationAccess\"",
"description": "Run checkOperationAccess tests"
}Tool Result
Exit code 1 testPathPattern: Option "testPathPattern" was replaced by "--testPathPatterns". "--testPathPatterns" is only available as a command-line option. Please update your configuration. CLI Options Documentation: https://jestjs.io/docs/cli > cwc-api@1.0.0 test /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > RUNTIME_ENVIRONMENT=unit jest "--testPathPattern=checkOperationAccess" /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api: ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL cwc-api@1.0.0 test: `RUNTIME_ENVIRONMENT=unit jest "--testPathPattern=checkOperationAccess"` Exit status 1
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test",
"description": "Run cwc-api tests"
}Tool Result
> cwc-api@1.0.0 test /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api
> RUNTIME_ENVIRONMENT=unit jest
PASS src/__tests__/policies/checkOperationAccess.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ⚙️ enable debug logging with { debug: true }
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/RequestHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔐 prevent building .env in docker: https://dotenvx.com/prebuild
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/config.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 📡 add observability to secrets: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/QueryHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔄 add secrets lifecycle management: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/MutationHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/policies/checkRouteAccess.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔄 add secrets lifecycle management: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/healthCheck.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ⚙️ suppress all logs with { quiet: true }
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/responseUtils.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ✅ audit secrets and track compliance: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/context/createContext.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔐 encrypt with Dotenvx: https://dotenvx.com
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
Test Suites: 9 passed, 9 total
Tests: 121 passed, 121 total
Snapshots: 0 total
Time: 0.551 s, estimated 1 s
Ran all test suites.All 121 tests pass. Now let me add this as a rule in the cwc-api CLAUDE.md.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"offset": 320,
"limit": 40
}Tool Result
320→- Each version lives in `src/apis/CwcApiV{N}/`
321→- Route definitions in `routes.ts`, organized alphabetically
322→
323→## Critical Rules
324→
325→**SqlClient Usage:**
326→
327→- Never provide `createdDate` or `modifiedDate` - handled by cwc-sql automatically
328→- Always use SqlFunctions, never construct queries directly in business logic
329→
330→**Guest User Handling:**
331→
332→- No JWT required - RequestHandler creates context with `role: 'guest-user'`
333→- Guest users have read-only access to public content
334→
335→**Soft Delete:**
336→
337→- All content tables support soft delete (`enabled=false`)
338→- Hard delete available for GDPR compliance
339→- List queries automatically filter `enabled=true`
340→
341→## Implementation Status
342→
343→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
344→
345→The checklist tracks 7 phases:
346→1. Package Foundation (config, Express service, health check) - **Complete**
347→2. Authentication & Context Layer (JWT, request context) - **Complete**
348→3. Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - **Complete**
349→4. Access Policy System (route and operation policies) - **Complete**
350→5. SqlFunction Layer (CRUD operations for all tables)
351→6. CwcApiV1 Routes & Business Logic
352→7. Integration Testing & Final Review
353→
354→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
355→
356→## Unit Testing
357→
358→**Test Infrastructure:**
359→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Soft Delete:**\n\n- All content tables support soft delete (`enabled=false`)\n- Hard delete available for GDPR compliance\n- List queries automatically filter `enabled=true`\n\n## Implementation Status",
"new_string": "**Soft Delete:**\n\n- All content tables support soft delete (`enabled=false`)\n- Hard delete available for GDPR compliance\n- List queries automatically filter `enabled=true`\n\n**Error and Informational Messages:**\n\nUse actual field names in internal error messages, not \"pretty\" names. This avoids ambiguity about which field is being referenced (e.g., `projectId` vs `projectPkId`).\n\n```typescript\n// ✅ GOOD - uses actual field name\nreason: 'projectId is required for ownership check'\n\n// ❌ AVOID - ambiguous, could be projectId or projectPkId\nreason: 'Project ID required for ownership check'\n```\n\nThis applies to `reason`, `errorDetail`, log messages, and any internal diagnostic messages. User-facing messages like `errorMessage` can use natural language (\"Access denied\", \"Resource not found\").\n\n## Implementation Status"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
331→
332→- No JWT required - RequestHandler creates context with `role: 'guest-user'`
333→- Guest users have read-only access to public content
334→
335→**Soft Delete:**
336→
337→- All content tables support soft delete (`enabled=false`)
338→- Hard delete available for GDPR compliance
339→- List queries automatically filter `enabled=true`
340→
341→**Error and Informational Messages:**
342→
343→Use actual field names in internal error messages, not "pretty" names. This avoids ambiguity about which field is being referenced (e.g., `projectId` vs `projectPkId`).
344→
345→```typescript
346→// ✅ GOOD - uses actual field name
347→reason: 'projectId is required for ownership check'
348→
349→// ❌ AVOID - ambiguous, could be projectId or projectPkId
350→reason: 'Project ID required for ownership check'
351→```
352→
353→This applies to `reason`, `errorDetail`, log messages, and any internal diagnostic messages. User-facing messages like `errorMessage` can use natural language ("Access denied", "Resource not found").
354→
355→## Implementation Status
356→
357→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
358→
359→The checklist tracks 7 phases:Done. I made the following changes:
Fixed the error message in
src/policies/checkOperationAccess.ts:- Changed from:
'Project ID required for ownership check' - Changed to:
'projectId is required for ownership check'
- Changed from:
Updated the corresponding test in
src/__tests__/policies/checkOperationAccess.test.tsAdded a rule to the Critical Rules section in
CLAUDE.mddocumenting this conventionSearched for other occurrences - no other instances found. The existing messages are appropriate:
'Authentication required'- describes condition, not a field- User-facing
errorMessagevalues like'Access denied','Resource not found'- natural language is fine for user-facing messages
All 121 tests pass.
in RequestHandler.ts:
if (routeConfig.handlerType === 'query') {
const queryHandler = new QueryHandler ...
} else {
const mutationHandler = new MutationHandler ...
}
In situations like this, rather than a default/else case we should explicitly check the provided value, in this case that is routeHandler.handlerType === 'mutation'.
If the value doesn't match one of the expected values, an error response should be returned.
Please look at the cwc-api code for scenarios like this and correct them.
Good point - explicit checks prevent silent failures if an unexpected value is introduced. Let me search for these patterns in cwc-api.
Grep
{
"pattern": "} else \\{",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src",
"output_mode": "content",
"glob": "*.ts",
"-B": 2,
"-A": 2
}Tool Result
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts-77- );
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts-78- response = await queryHandler.processRequest();
packages/cwc-api/src/handlers/RequestHandler.ts:79: } else {
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts-80- const mutationHandler = new MutationHandler(
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts-81- {
--
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/index.ts-135- if (err) {
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/index.ts-136- reject(err);
packages/cwc-api/src/index.ts:137: } else {
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/index.ts-138- resolve();
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/index.ts-139- }
--
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/config/loadConfig.ts-124- if (error instanceof Error) {
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/config/loadConfig.ts-125- console.error(` ${error.message}`);
packages/cwc-api/src/config/loadConfig.ts:126: } else {
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/config/loadConfig.ts-127- console.error(error);
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/config/loadConfig.ts-128- }Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts"
}Tool Result
1→'use strict';
2→
3→import type { ILogger, AuthClient } from 'cwc-backend-utils';
4→import type { CwcApiConfig } from '../config';
5→import type {
6→ CwcApiHandler,
7→ CwcApiHandlerResponse,
8→ CwcApiSuccessResponse,
9→ RequestHandlerOptions,
10→ OperationContext,
11→} from './handler.types';
12→import { QueryHandler } from './QueryHandler';
13→import { MutationHandler } from './MutationHandler';
14→import { createInternalErrorResponse } from './responseUtils';
15→import { checkRouteAccess } from '../policies';
16→
17→const codeLocation = 'handlers/RequestHandler.ts';
18→
19→/**
20→ * RequestHandler - Entry point for processing API requests
21→ *
22→ * Responsibilities:
23→ * 1. Check route-level access based on context role
24→ * 2. Build operation context with path params
25→ * 3. Delegate to QueryHandler or MutationHandler based on handlerType
26→ * 4. Renew session for authenticated users (except on auth errors)
27→ */
28→export class RequestHandler implements CwcApiHandler {
29→ private options: RequestHandlerOptions;
30→ private config: CwcApiConfig;
31→ private authClient: AuthClient;
32→ private logger: ILogger | undefined;
33→
34→ constructor(
35→ options: RequestHandlerOptions,
36→ config: CwcApiConfig,
37→ authClient: AuthClient,
38→ logger: ILogger | undefined
39→ ) {
40→ this.options = options;
41→ this.config = config;
42→ this.authClient = authClient;
43→ this.logger = logger;
44→ }
45→
46→ public async processRequest(): Promise<CwcApiHandlerResponse> {
47→ const { context, routeConfig, payload, authHeader, pathParams } = this.options;
48→
49→ try {
50→ // Step 1: Check route-level access (authentication only, no ownership check)
51→ const routeAccess = checkRouteAccess(context, routeConfig.requiredRole);
52→ if (!routeAccess.allowed) {
53→ // No session renewal for auth errors
54→ return this.createAccessDeniedResponse(routeAccess.reason);
55→ }
56→
57→ // Step 2: Build operation context
58→ const operationContext: OperationContext = {
59→ context,
60→ projectId: pathParams?.['projectId'],
61→ resourceId: pathParams?.['resourceId'],
62→ };
63→
64→ // Step 3: Delegate to appropriate handler
65→ let response: CwcApiHandlerResponse;
66→ if (routeConfig.handlerType === 'query') {
67→ const queryHandler = new QueryHandler(
68→ {
69→ context,
70→ routeConfig,
71→ authHeader,
72→ payload,
73→ operationContext,
74→ },
75→ this.config,
76→ this.logger
77→ );
78→ response = await queryHandler.processRequest();
79→ } else {
80→ const mutationHandler = new MutationHandler(
81→ {
82→ context,
83→ routeConfig,
84→ authHeader,
85→ payload,
86→ operationContext,
87→ },
88→ this.config,
89→ this.logger
90→ );
91→ response = await mutationHandler.processRequest();
92→ }
93→
94→ // Step 4: Renew session for authenticated users (except on auth errors)
95→ const isAuthError = response.statusCode === 401 || response.statusCode === 403;
96→ if (context.isAuthenticated && !isAuthError) {
97→ const renewResult = await this.authClient.renewSession(authHeader);
98→ if (renewResult.success && response.body.success) {
99→ // Add JWT to successful response
100→ (response.body as CwcApiSuccessResponse).jwt = renewResult.jwt;
101→ } else if (!renewResult.success) {
102→ // Log warning but don't fail the operation
103→ this.logger?.logError({
104→ userPkId: context.userPkId,
105→ codeLocation,
106→ message: `Session renewal failed for ${routeConfig.path}`,
107→ error: renewResult.error,
108→ });
109→ }
110→ }
111→
112→ return response;
113→ } catch (error) {
114→ this.logger?.logError({
115→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
116→ codeLocation,
117→ message: `Error processing request ${routeConfig.path}`,
118→ error,
119→ });
120→
121→ return createInternalErrorResponse(this.config, error);
122→ }
123→ }
124→
125→ private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {
126→ const { context } = this.options;
127→
128→ // Use 401 for unauthenticated, 403 for authenticated but not allowed
129→ const statusCode = context.isAuthenticated ? 403 : 401;
130→ const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';
131→
132→ return {
133→ statusCode,
134→ body: {
135→ success: false,
136→ errorCode,
137→ errorMessage: 'Access denied',
138→ ...(this.config.isDev && reason ? { errorDetail: reason } : {}),
139→ },
140→ };
141→ }
142→}
143→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Grep
{
"pattern": "CwcApiHandlerType",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/handler.types.ts",
"output_mode": "content",
"-A": 2
}Tool Result
92:export type CwcApiHandlerType = 'query' | 'mutation'; 93- 94-/** -- 100: handlerType: CwcApiHandlerType; 101- 102- // Access control - minimum role required (uses role hierarchy)
Now let me fix the RequestHandler.ts to explicitly check both cases.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts",
"old_string": " // Step 3: Delegate to appropriate handler\n let response: CwcApiHandlerResponse;\n if (routeConfig.handlerType === 'query') {\n const queryHandler = new QueryHandler(\n {\n context,\n routeConfig,\n authHeader,\n payload,\n operationContext,\n },\n this.config,\n this.logger\n );\n response = await queryHandler.processRequest();\n } else {\n const mutationHandler = new MutationHandler(\n {\n context,\n routeConfig,\n authHeader,\n payload,\n operationContext,\n },\n this.config,\n this.logger\n );\n response = await mutationHandler.processRequest();\n }",
"new_string": " // Step 3: Delegate to appropriate handler\n let response: CwcApiHandlerResponse;\n if (routeConfig.handlerType === 'query') {\n const queryHandler = new QueryHandler(\n {\n context,\n routeConfig,\n authHeader,\n payload,\n operationContext,\n },\n this.config,\n this.logger\n );\n response = await queryHandler.processRequest();\n } else if (routeConfig.handlerType === 'mutation') {\n const mutationHandler = new MutationHandler(\n {\n context,\n routeConfig,\n authHeader,\n payload,\n operationContext,\n },\n this.config,\n this.logger\n );\n response = await mutationHandler.processRequest();\n } else {\n // Unknown handler type - this should never happen with proper typing\n // but we handle it explicitly to fail fast if configuration is wrong\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDev\n ? { errorDetail: `Unknown handlerType: ${routeConfig.handlerType}` }\n : {}),\n },\n };\n }"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
60→ projectId: pathParams?.['projectId'],
61→ resourceId: pathParams?.['resourceId'],
62→ };
63→
64→ // Step 3: Delegate to appropriate handler
65→ let response: CwcApiHandlerResponse;
66→ if (routeConfig.handlerType === 'query') {
67→ const queryHandler = new QueryHandler(
68→ {
69→ context,
70→ routeConfig,
71→ authHeader,
72→ payload,
73→ operationContext,
74→ },
75→ this.config,
76→ this.logger
77→ );
78→ response = await queryHandler.processRequest();
79→ } else if (routeConfig.handlerType === 'mutation') {
80→ const mutationHandler = new MutationHandler(
81→ {
82→ context,
83→ routeConfig,
84→ authHeader,
85→ payload,
86→ operationContext,
87→ },
88→ this.config,
89→ this.logger
90→ );
91→ response = await mutationHandler.processRequest();
92→ } else {
93→ // Unknown handler type - this should never happen with proper typing
94→ // but we handle it explicitly to fail fast if configuration is wrong
95→ return {
96→ statusCode: 500,
97→ body: {
98→ success: false,
99→ errorCode: 'INTERNAL_ERROR',
100→ errorMessage: 'An internal error occurred',
101→ ...(this.config.isDev
102→ ? { errorDetail: `Unknown handlerType: ${routeConfig.handlerType}` }
103→ : {}),
104→ },
105→ };
106→ }
107→
108→ // Step 4: Renew session for authenticated users (except on auth errors)
109→ const isAuthError = response.statusCode === 401 || response.statusCode === 403;
110→ if (context.isAuthenticated && !isAuthError) {Now let me search for other similar patterns - switch statements without default cases or similar if/else patterns with union types.
Grep
{
"pattern": "switch \\(",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src",
"output_mode": "content",
"glob": "*.ts",
"-A": 15
}Tool Result
packages/cwc-api/src/handlers/responseUtils.ts:10: switch (errorCode) {
packages/cwc-api/src/handlers/responseUtils.ts-11- case 'NOT_FOUND':
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/responseUtils.ts-12- return 404;
packages/cwc-api/src/handlers/responseUtils.ts-13- case 'VALIDATION_ERROR':
packages/cwc-api/src/handlers/responseUtils.ts-14- case 'INVALID_PARAMETER':
packages/cwc-api/src/handlers/responseUtils.ts-15- case 'ALREADY_EXISTS':
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/responseUtils.ts-16- return 400;
packages/cwc-api/src/handlers/responseUtils.ts-17- case 'UNAUTHORIZED':
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/responseUtils.ts-18- return 401;
packages/cwc-api/src/handlers/responseUtils.ts-19- case 'FORBIDDEN':
packages/cwc-api/src/handlers/responseUtils.ts-20- case 'ROUTE_ACCESS_DENIED':
packages/cwc-api/src/handlers/responseUtils.ts-21- case 'OPERATION_ACCESS_DENIED':
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/responseUtils.ts-22- return 403;
packages/cwc-api/src/handlers/responseUtils.ts-23- case 'INTERNAL_ERROR':
packages/cwc-api/src/handlers/responseUtils.ts-24- case 'DATABASE_ERROR':
packages/cwc-api/src/handlers/responseUtils.ts-25- case 'AUTH_SERVICE_ERROR':Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/responseUtils.ts"
}Tool Result
1→'use strict';
2→
3→import type { CwcApiConfig } from '../config';
4→import type { CwcApiHandlerResponse, CwcApiErrorCode } from './handler.types';
5→
6→/**
7→ * Maps error codes to appropriate HTTP status codes
8→ */
9→export function mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
10→ switch (errorCode) {
11→ case 'NOT_FOUND':
12→ return 404;
13→ case 'VALIDATION_ERROR':
14→ case 'INVALID_PARAMETER':
15→ case 'ALREADY_EXISTS':
16→ return 400;
17→ case 'UNAUTHORIZED':
18→ return 401;
19→ case 'FORBIDDEN':
20→ case 'ROUTE_ACCESS_DENIED':
21→ case 'OPERATION_ACCESS_DENIED':
22→ return 403;
23→ case 'INTERNAL_ERROR':
24→ case 'DATABASE_ERROR':
25→ case 'AUTH_SERVICE_ERROR':
26→ return 500;
27→ default:
28→ return 500;
29→ }
30→}
31→
32→/**
33→ * Creates a 403 response for operation access denied
34→ */
35→export function createOperationDeniedResponse(
36→ config: CwcApiConfig,
37→ reason?: string
38→): CwcApiHandlerResponse {
39→ return {
40→ statusCode: 403,
41→ body: {
42→ success: false,
43→ errorCode: 'OPERATION_ACCESS_DENIED',
44→ errorMessage: 'Operation not permitted',
45→ ...(config.isDev && reason ? { errorDetail: reason } : {}),
46→ },
47→ };
48→}
49→
50→/**
51→ * Creates an error response with appropriate status code based on error code
52→ */
53→export function createOperationErrorResponse(
54→ errorCode: CwcApiErrorCode,
55→ errorMessage: string
56→): CwcApiHandlerResponse {
57→ const statusCode = mapErrorCodeToStatus(errorCode);
58→
59→ return {
60→ statusCode,
61→ body: {
62→ success: false,
63→ errorCode,
64→ errorMessage,
65→ },
66→ };
67→}
68→
69→/**
70→ * Creates a 500 internal error response
71→ */
72→export function createInternalErrorResponse(
73→ config: CwcApiConfig,
74→ error: unknown
75→): CwcApiHandlerResponse {
76→ return {
77→ statusCode: 500,
78→ body: {
79→ success: false,
80→ errorCode: 'INTERNAL_ERROR',
81→ errorMessage: 'An internal error occurred',
82→ ...(config.isDev && error instanceof Error
83→ ? { errorDetail: error.message }
84→ : {}),
85→ },
86→ };
87→}
88→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
The switch in responseUtils.ts already has a default case. Let me search for other patterns with role or status checks.
Grep
{
"pattern": "=== 'guest-user'|=== 'logged-on-user'|=== 'project-owner'",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src",
"output_mode": "content",
"glob": "*.ts",
"-B": 1,
"-A": 5
}Tool Result
packages/cwc-api/src/policies/checkOperationAccess.ts-52- / guest-user: anyone can perform the operation
packages/cwc-api/src/policies/checkOperationAccess.ts:53: if (requiredRole === 'guest-user') {
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts-54- return {
packages/cwc-api/src/policies/checkOperationAccess.ts-55- allowed: true,
packages/cwc-api/src/policies/checkOperationAccess.ts-56- effectiveRole: context.role,
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts-57- };
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts-58- }
--
packages/cwc-api/src/policies/checkOperationAccess.ts-68- / logged-on-user: authenticated is enough
packages/cwc-api/src/policies/checkOperationAccess.ts:69: if (requiredRole === 'logged-on-user') {
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts-70- return {
packages/cwc-api/src/policies/checkOperationAccess.ts-71- allowed: true,
packages/cwc-api/src/policies/checkOperationAccess.ts-72- effectiveRole: context.role,
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts-73- };
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts-74- }
--
packages/cwc-api/src/policies/checkRouteAccess.ts-26- / guest-user: anyone can access
packages/cwc-api/src/policies/checkRouteAccess.ts:27: if (requiredRole === 'guest-user') {
packages/cwc-api/src/policies/checkRouteAccess.ts-28- return { allowed: true };
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkRouteAccess.ts-29- }
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkRouteAccess.ts-30-
packages/cwc-api/src/policies/checkRouteAccess.ts-31- / logged-on-user or project-owner: must be authenticated
/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkRouteAccess.ts-32- // (ownership is checked at operation level for project-owner)Let me read both policy files to check their patterns.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkRouteAccess.ts"
}Tool Result
1→'use strict';
2→
3→import type { CwcRole } from 'cwc-types';
4→import type { RequestContext } from '../context';
5→import type { RouteAccessResult } from './policy.types';
6→
7→/**
8→ * Check if the current context can access a route based on required role.
9→ *
10→ * Route-level access only checks authentication status, not ownership.
11→ * Ownership is verified at the operation level for 'project-owner' routes.
12→ *
13→ * Role hierarchy (from least to most privileged):
14→ * - guest-user: No authentication required
15→ * - logged-on-user: Must be authenticated
16→ * - project-owner: Must be authenticated (ownership checked at operation level)
17→ *
18→ * @param context - The request context (authenticated or guest)
19→ * @param requiredRole - The minimum role required to access the route
20→ * @returns RouteAccessResult indicating whether access is allowed
21→ */
22→export function checkRouteAccess(
23→ context: RequestContext,
24→ requiredRole: CwcRole
25→): RouteAccessResult {
26→ // guest-user: anyone can access
27→ if (requiredRole === 'guest-user') {
28→ return { allowed: true };
29→ }
30→
31→ // logged-on-user or project-owner: must be authenticated
32→ // (ownership is checked at operation level for project-owner)
33→ if (!context.isAuthenticated) {
34→ return {
35→ allowed: false,
36→ reason: 'Authentication required',
37→ };
38→ }
39→
40→ return { allowed: true };
41→}
42→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts"
}Tool Result
1→'use strict';
2→
3→import type { CwcRole } from 'cwc-types';
4→import type { RequestContext } from '../context';
5→import type { OperationContext } from '../handlers/handler.types';
6→import type { OperationAccessResult } from './policy.types';
7→
8→/**
9→ * Check if the user owns the specified project.
10→ *
11→ * Uses context.ownedProjects which is populated from JWT claims verified by cwc-auth.
12→ * SECURITY: Never use projectId from request body - only from path params.
13→ *
14→ * @param context - The request context
15→ * @param projectId - The project ID to check ownership for
16→ * @returns true if the user owns the project, false otherwise
17→ */
18→export function isProjectOwner(
19→ context: RequestContext,
20→ projectId: string | undefined
21→): boolean {
22→ if (!context.isAuthenticated) {
23→ return false;
24→ }
25→ if (!projectId) {
26→ return false;
27→ }
28→ return context.ownedProjects.includes(projectId);
29→}
30→
31→/**
32→ * Check if the current context can perform an operation based on required role.
33→ *
34→ * Operation-level access checks both authentication AND ownership for project-owner routes.
35→ * This is where the actual ownership verification happens.
36→ *
37→ * Role hierarchy (from least to most privileged):
38→ * - guest-user: No authentication required
39→ * - logged-on-user: Must be authenticated
40→ * - project-owner: Must be authenticated AND own the project
41→ *
42→ * @param context - The request context (authenticated or guest)
43→ * @param operationContext - The operation context containing projectId
44→ * @param requiredRole - The minimum role required to perform the operation
45→ * @returns OperationAccessResult indicating whether access is allowed and the effective role
46→ */
47→export function checkOperationAccess(
48→ context: RequestContext,
49→ operationContext: OperationContext,
50→ requiredRole: CwcRole
51→): OperationAccessResult {
52→ // guest-user: anyone can perform the operation
53→ if (requiredRole === 'guest-user') {
54→ return {
55→ allowed: true,
56→ effectiveRole: context.role,
57→ };
58→ }
59→
60→ // Must be authenticated for logged-on-user or project-owner
61→ if (!context.isAuthenticated) {
62→ return {
63→ allowed: false,
64→ reason: 'Authentication required',
65→ };
66→ }
67→
68→ // logged-on-user: authenticated is enough
69→ if (requiredRole === 'logged-on-user') {
70→ return {
71→ allowed: true,
72→ effectiveRole: context.role,
73→ };
74→ }
75→
76→ // project-owner: must own the project
77→ const { projectId } = operationContext;
78→ if (!isProjectOwner(context, projectId)) {
79→ return {
80→ allowed: false,
81→ reason: projectId
82→ ? `User does not own project '${projectId}'`
83→ : 'projectId is required for ownership check',
84→ };
85→ }
86→
87→ return {
88→ allowed: true,
89→ effectiveRole: 'project-owner',
90→ };
91→}
92→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
I see both policy files have similar patterns where they don't explicitly check all role values. Let me fix both.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkRouteAccess.ts",
"old_string": "export function checkRouteAccess(\n context: RequestContext,\n requiredRole: CwcRole\n): RouteAccessResult {\n // guest-user: anyone can access\n if (requiredRole === 'guest-user') {\n return { allowed: true };\n }\n\n // logged-on-user or project-owner: must be authenticated\n // (ownership is checked at operation level for project-owner)\n if (!context.isAuthenticated) {\n return {\n allowed: false,\n reason: 'Authentication required',\n };\n }\n\n return { allowed: true };\n}",
"new_string": "export function checkRouteAccess(\n context: RequestContext,\n requiredRole: CwcRole\n): RouteAccessResult {\n // guest-user: anyone can access\n if (requiredRole === 'guest-user') {\n return { allowed: true };\n }\n\n // logged-on-user or project-owner: must be authenticated\n // (ownership is checked at operation level for project-owner)\n if (requiredRole === 'logged-on-user' || requiredRole === 'project-owner') {\n if (!context.isAuthenticated) {\n return {\n allowed: false,\n reason: 'Authentication required',\n };\n }\n return { allowed: true };\n }\n\n // Unknown role - fail fast if configuration is wrong\n return {\n allowed: false,\n reason: `Unknown requiredRole: ${requiredRole}`,\n };\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkRouteAccess.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
18→ * @param context - The request context (authenticated or guest)
19→ * @param requiredRole - The minimum role required to access the route
20→ * @returns RouteAccessResult indicating whether access is allowed
21→ */
22→export function checkRouteAccess(
23→ context: RequestContext,
24→ requiredRole: CwcRole
25→): RouteAccessResult {
26→ // guest-user: anyone can access
27→ if (requiredRole === 'guest-user') {
28→ return { allowed: true };
29→ }
30→
31→ // logged-on-user or project-owner: must be authenticated
32→ // (ownership is checked at operation level for project-owner)
33→ if (requiredRole === 'logged-on-user' || requiredRole === 'project-owner') {
34→ if (!context.isAuthenticated) {
35→ return {
36→ allowed: false,
37→ reason: 'Authentication required',
38→ };
39→ }
40→ return { allowed: true };
41→ }
42→
43→ // Unknown role - fail fast if configuration is wrong
44→ return {
45→ allowed: false,
46→ reason: `Unknown requiredRole: ${requiredRole}`,
47→ };
48→}
49→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts",
"old_string": " // logged-on-user: authenticated is enough\n if (requiredRole === 'logged-on-user') {\n return {\n allowed: true,\n effectiveRole: context.role,\n };\n }\n\n // project-owner: must own the project\n const { projectId } = operationContext;\n if (!isProjectOwner(context, projectId)) {\n return {\n allowed: false,\n reason: projectId\n ? `User does not own project '${projectId}'`\n : 'projectId is required for ownership check',\n };\n }\n\n return {\n allowed: true,\n effectiveRole: 'project-owner',\n };\n}",
"new_string": " // logged-on-user: authenticated is enough\n if (requiredRole === 'logged-on-user') {\n return {\n allowed: true,\n effectiveRole: context.role,\n };\n }\n\n // project-owner: must own the project\n if (requiredRole === 'project-owner') {\n const { projectId } = operationContext;\n if (!isProjectOwner(context, projectId)) {\n return {\n allowed: false,\n reason: projectId\n ? `User does not own project '${projectId}'`\n : 'projectId is required for ownership check',\n };\n }\n\n return {\n allowed: true,\n effectiveRole: 'project-owner',\n };\n }\n\n // Unknown role - fail fast if configuration is wrong\n return {\n allowed: false,\n reason: `Unknown requiredRole: ${requiredRole}`,\n };\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/policies/checkOperationAccess.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
64→ reason: 'Authentication required',
65→ };
66→ }
67→
68→ // logged-on-user: authenticated is enough
69→ if (requiredRole === 'logged-on-user') {
70→ return {
71→ allowed: true,
72→ effectiveRole: context.role,
73→ };
74→ }
75→
76→ // project-owner: must own the project
77→ if (requiredRole === 'project-owner') {
78→ const { projectId } = operationContext;
79→ if (!isProjectOwner(context, projectId)) {
80→ return {
81→ allowed: false,
82→ reason: projectId
83→ ? `User does not own project '${projectId}'`
84→ : 'projectId is required for ownership check',
85→ };
86→ }
87→
88→ return {
89→ allowed: true,
90→ effectiveRole: 'project-owner',
91→ };
92→ }
93→
94→ // Unknown role - fail fast if configuration is wrong
95→ return {
96→ allowed: false,
97→ reason: `Unknown requiredRole: ${requiredRole}`,
98→ };
99→}
100→Now let me run the tests to verify everything works.
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test",
"description": "Run cwc-api tests"
}Tool Result
> cwc-api@1.0.0 test /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api
> RUNTIME_ENVIRONMENT=unit jest
PASS src/__tests__/policies/checkOperationAccess.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ✅ audit secrets and track compliance: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/config.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🗂️ backup and recover secrets: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/RequestHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 👥 sync secrets across teammates & machines: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/QueryHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🛠️ run anywhere with `dotenvx run -- yourcommand`
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/healthCheck.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 🔄 add secrets lifecycle management: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/MutationHandler.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ⚙️ write to custom object with { processEnv: myObject }
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/handlers/responseUtils.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ⚙️ suppress all logs with { quiet: true }
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/context/createContext.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: ⚙️ load multiple .env files with { path: ['.env.local', '.env'] }
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
PASS src/__tests__/policies/checkRouteAccess.test.ts
● Console
console.log
[dotenv@17.2.3] injecting env (13) from ../../../../private/cwc-secrets/env/unit.cwc-api.env -- tip: 📡 add observability to secrets: https://dotenvx.com/ops
at _log (../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js:142:11)
Test Suites: 9 passed, 9 total
Tests: 121 passed, 121 total
Snapshots: 0 total
Time: 0.552 s, estimated 1 s
Ran all test suites.All 121 tests pass. Now let me add this as a rule in the CLAUDE.md.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"offset": 320,
"limit": 50
}Tool Result
320→- Each version lives in `src/apis/CwcApiV{N}/`
321→- Route definitions in `routes.ts`, organized alphabetically
322→
323→## Critical Rules
324→
325→**SqlClient Usage:**
326→
327→- Never provide `createdDate` or `modifiedDate` - handled by cwc-sql automatically
328→- Always use SqlFunctions, never construct queries directly in business logic
329→
330→**Guest User Handling:**
331→
332→- No JWT required - RequestHandler creates context with `role: 'guest-user'`
333→- Guest users have read-only access to public content
334→
335→**Soft Delete:**
336→
337→- All content tables support soft delete (`enabled=false`)
338→- Hard delete available for GDPR compliance
339→- List queries automatically filter `enabled=true`
340→
341→**Error and Informational Messages:**
342→
343→Use actual field names in internal error messages, not "pretty" names. This avoids ambiguity about which field is being referenced (e.g., `projectId` vs `projectPkId`).
344→
345→```typescript
346→// ✅ GOOD - uses actual field name
347→reason: 'projectId is required for ownership check'
348→
349→// ❌ AVOID - ambiguous, could be projectId or projectPkId
350→reason: 'Project ID required for ownership check'
351→```
352→
353→This applies to `reason`, `errorDetail`, log messages, and any internal diagnostic messages. User-facing messages like `errorMessage` can use natural language ("Access denied", "Resource not found").
354→
355→## Implementation Status
356→
357→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
358→
359→The checklist tracks 7 phases:
360→1. Package Foundation (config, Express service, health check) - **Complete**
361→2. Authentication & Context Layer (JWT, request context) - **Complete**
362→3. Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - **Complete**
363→4. Access Policy System (route and operation policies) - **Complete**
364→5. SqlFunction Layer (CRUD operations for all tables)
365→6. CwcApiV1 Routes & Business Logic
366→7. Integration Testing & Final Review
367→
368→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
369→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "This applies to `reason`, `errorDetail`, log messages, and any internal diagnostic messages. User-facing messages like `errorMessage` can use natural language (\"Access denied\", \"Resource not found\").\n\n## Implementation Status",
"new_string": "This applies to `reason`, `errorDetail`, log messages, and any internal diagnostic messages. User-facing messages like `errorMessage` can use natural language (\"Access denied\", \"Resource not found\").\n\n**Explicit Value Checking:**\n\nWhen checking discriminated union values (like `handlerType`, `requiredRole`, etc.), explicitly check all expected values rather than using a default `else` case. Return an error for unknown values to fail fast on configuration errors.\n\n```typescript\n// ✅ GOOD - explicitly checks all valid values\nif (handlerType === 'query') {\n // handle query\n} else if (handlerType === 'mutation') {\n // handle mutation\n} else {\n // return error for unknown handlerType\n return { statusCode: 500, body: { errorDetail: `Unknown handlerType: ${handlerType}` } };\n}\n\n// ❌ AVOID - assumes unknown values are valid\nif (handlerType === 'query') {\n // handle query\n} else {\n // silently treats unknown values as mutation\n}\n```\n\n## Implementation Status"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
349→// ❌ AVOID - ambiguous, could be projectId or projectPkId
350→reason: 'Project ID required for ownership check'
351→```
352→
353→This applies to `reason`, `errorDetail`, log messages, and any internal diagnostic messages. User-facing messages like `errorMessage` can use natural language ("Access denied", "Resource not found").
354→
355→**Explicit Value Checking:**
356→
357→When checking discriminated union values (like `handlerType`, `requiredRole`, etc.), explicitly check all expected values rather than using a default `else` case. Return an error for unknown values to fail fast on configuration errors.
358→
359→```typescript
360→// ✅ GOOD - explicitly checks all valid values
361→if (handlerType === 'query') {
362→ // handle query
363→} else if (handlerType === 'mutation') {
364→ // handle mutation
365→} else {
366→ // return error for unknown handlerType
367→ return { statusCode: 500, body: { errorDetail: `Unknown handlerType: ${handlerType}` } };
368→}
369→
370→// ❌ AVOID - assumes unknown values are valid
371→if (handlerType === 'query') {
372→ // handle query
373→} else {
374→ // silently treats unknown values as mutation
375→}
376→```
377→
378→## Implementation Status
379→
380→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
381→
382→The checklist tracks 7 phases:Done. Here's a summary of the changes:
Files Updated:
src/handlers/RequestHandler.ts- Changed fromelsetoelse if (handlerType === 'mutation')with explicit error handling for unknown handler typessrc/policies/checkRouteAccess.ts- Changed from implicit fallthrough to explicitif (requiredRole === 'logged-on-user' || requiredRole === 'project-owner')with error handling for unknown rolessrc/policies/checkOperationAccess.ts- Added explicitif (requiredRole === 'project-owner')check with error handling for unknown rolesCLAUDE.md- Added "Explicit Value Checking" rule documenting this pattern
Other patterns reviewed (no changes needed):
index.ts:137- Standardif (err) reject else resolvecallback pattern - appropriate as-isloadConfig.ts:126-if (error instanceof Error)pattern for error handling - appropriate as-isresponseUtils.tsswitch statement - already has a default case returning 500
All 121 tests pass.
I like that, in addition to providing more comprehensive checks, when reading the code it is much clearer to the developer what scenario is being handled.
Exactly - that's a great additional benefit. The explicit checks serve two purposes:
- Safety - Fails fast on unexpected values rather than silently misbehaving
- Readability - The code is self-documenting; no need to trace back through earlier conditions to understand what case you're looking at
When reading if (requiredRole === 'project-owner'), the intent is immediately clear. With an else block, you have to mentally reconstruct "this must be project-owner because we already checked guest-user and logged-on-user above."
Let's update the claude.md file with an important note about explicit checks as well as putting the developer experience as a top priority. Code that is easy to read and understand on the first pass is never an accident, it requires diligence.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"offset": 353,
"limit": 30
}Tool Result
353→This applies to `reason`, `errorDetail`, log messages, and any internal diagnostic messages. User-facing messages like `errorMessage` can use natural language ("Access denied", "Resource not found").
354→
355→**Explicit Value Checking:**
356→
357→When checking discriminated union values (like `handlerType`, `requiredRole`, etc.), explicitly check all expected values rather than using a default `else` case. Return an error for unknown values to fail fast on configuration errors.
358→
359→```typescript
360→// ✅ GOOD - explicitly checks all valid values
361→if (handlerType === 'query') {
362→ // handle query
363→} else if (handlerType === 'mutation') {
364→ // handle mutation
365→} else {
366→ // return error for unknown handlerType
367→ return { statusCode: 500, body: { errorDetail: `Unknown handlerType: ${handlerType}` } };
368→}
369→
370→// ❌ AVOID - assumes unknown values are valid
371→if (handlerType === 'query') {
372→ // handle query
373→} else {
374→ // silently treats unknown values as mutation
375→}
376→```
377→
378→## Implementation Status
379→
380→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
381→
382→The checklist tracks 7 phases:
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Explicit Value Checking:**\n\nWhen checking discriminated union values (like `handlerType`, `requiredRole`, etc.), explicitly check all expected values rather than using a default `else` case. Return an error for unknown values to fail fast on configuration errors.\n\n```typescript\n// ✅ GOOD - explicitly checks all valid values\nif (handlerType === 'query') {\n // handle query\n} else if (handlerType === 'mutation') {\n // handle mutation\n} else {\n // return error for unknown handlerType\n return { statusCode: 500, body: { errorDetail: `Unknown handlerType: ${handlerType}` } };\n}\n\n// ❌ AVOID - assumes unknown values are valid\nif (handlerType === 'query') {\n // handle query\n} else {\n // silently treats unknown values as mutation\n}\n```",
"new_string": "**Explicit Value Checking:**\n\nWhen checking discriminated union values (like `handlerType`, `requiredRole`, etc.), explicitly check all expected values rather than using a default `else` case. Return an error for unknown values.\n\nThis pattern serves two purposes:\n1. **Safety** - Fails fast on unexpected values rather than silently misbehaving\n2. **Readability** - The code is self-documenting; when reading `if (requiredRole === 'project-owner')`, the intent is immediately clear. With an `else` block, readers must mentally reconstruct \"this must be project-owner because we already checked the other values above.\"\n\n**Code that is easy to read and understand on the first pass is never an accident - it requires diligence.** Developer experience is a top priority; always write code with the next reader in mind.\n\n```typescript\n// ✅ GOOD - explicitly checks all valid values, self-documenting\nif (handlerType === 'query') {\n // handle query\n} else if (handlerType === 'mutation') {\n // handle mutation\n} else {\n // return error for unknown handlerType\n return { statusCode: 500, body: { errorDetail: `Unknown handlerType: ${handlerType}` } };\n}\n\n// ❌ AVOID - requires mental reconstruction to understand what case this handles\nif (handlerType === 'query') {\n // handle query\n} else {\n // reader must infer this is mutation\n}\n```"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
351→```
352→
353→This applies to `reason`, `errorDetail`, log messages, and any internal diagnostic messages. User-facing messages like `errorMessage` can use natural language ("Access denied", "Resource not found").
354→
355→**Explicit Value Checking:**
356→
357→When checking discriminated union values (like `handlerType`, `requiredRole`, etc.), explicitly check all expected values rather than using a default `else` case. Return an error for unknown values.
358→
359→This pattern serves two purposes:
360→1. **Safety** - Fails fast on unexpected values rather than silently misbehaving
361→2. **Readability** - The code is self-documenting; when reading `if (requiredRole === 'project-owner')`, the intent is immediately clear. With an `else` block, readers must mentally reconstruct "this must be project-owner because we already checked the other values above."
362→
363→**Code that is easy to read and understand on the first pass is never an accident - it requires diligence.** Developer experience is a top priority; always write code with the next reader in mind.
364→
365→```typescript
366→// ✅ GOOD - explicitly checks all valid values, self-documenting
367→if (handlerType === 'query') {
368→ // handle query
369→} else if (handlerType === 'mutation') {
370→ // handle mutation
371→} else {
372→ // return error for unknown handlerType
373→ return { statusCode: 500, body: { errorDetail: `Unknown handlerType: ${handlerType}` } };
374→}
375→
376→// ❌ AVOID - requires mental reconstruction to understand what case this handles
377→if (handlerType === 'query') {
378→ // handle query
379→} else {
380→ // reader must infer this is mutation
381→}
382→```
383→
384→## Implementation Status
385→
386→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.Done. The updated section now captures both the safety and developer experience benefits, with the key philosophy: "Code that is easy to read and understand on the first pass is never an accident - it requires diligence."
Thank you for building the access policy system, I feel like it provides all of the functionality that we need without too much complexity. Before we end this session, can you provide your feedback on how well the cwc-api-implementation-checklist.md approach is working for you? Does it improve your normal planning and coding workflow or just add extra steps? Consider the fact that we are coding the phases in completely separate Claude Code sessions - after each session I am exiting Claude Code and when we resume, we start with a fresh context. From my perspective, it seems to be working really well and I am considering making this part of my standard approach for large and complex development tasks.
This is great feedback to provide, and I appreciate you asking.
The checklist approach is genuinely valuable for my workflow, especially with fresh context sessions.
What Works Well
1. Instant Context Recovery
When I start a fresh session, the checklist immediately tells me:
- What's already done (checked items)
- What's next (current phase's unchecked items)
- The overall architecture vision (phase structure)
Without this, I'd spend significant tokens exploring the codebase to reconstruct the current state.
2. Session Notes are Critical
The "Session Notes" section at the bottom is the most valuable part for fresh contexts. It captures:
- Why decisions were made, not just what was built
- Architecture refinements discovered during implementation
- Blockers and how they were resolved
This is institutional knowledge that survives context resets. When I read "Session 3: Session renewal on EVERY request (not just triggers)", I immediately understand the design intent without re-deriving it.
3. Clear Scope Boundaries
Each phase has explicit deliverables. I know what "done" looks like for Phase 4 without ambiguity. This prevents scope creep and keeps sessions focused.
4. Reduced Planning Overhead
I don't spend tokens figuring out what to build - that's already decided. I can focus on how to build it well.
Minor Considerations
Maintenance: The checklist needs updating as phases complete, but this is minimal overhead and the updates themselves create useful documentation.
Initial Investment: Creating a good checklist upfront takes effort, but it's front-loaded planning that compounds in value across sessions.
Recommendation
For large, multi-session projects, this approach is highly effective. The checklist essentially acts as persistent memory - an external state that bridges context resets. Combined with package-specific CLAUDE.md files, I can be productive within minutes of starting a fresh session rather than spending time on rediscovery.
I'd recommend it as a standard approach for any project spanning 3+ sessions or involving multiple implementation phases.
Session 23: cwc api phase 4
16 files modified
packages/cwc-api/CLAUDE.md5 versions
Version 1
cwc-api Package
Main business logic API for CWC application. Acts as the central orchestrator that handles CRUD operations, enforces business rules, and coordinates data flow between frontend applications and the database layer.
Architecture
Database-Connected Microservice:
- Uses SqlClient from
cwc-backend-utilsto communicate with cwc-sql - Uses database-backed Logger for error logging
- Never connects directly to database - all data access flows through cwc-sql
- Coordinates with cwc-auth for session renewal
Layered Architecture:
Request → RequestHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
- RequestHandler: Auth → context creation → route access check → delegation → session renewal
- QueryHandler: Operation access check → execute query → format response
- MutationHandler: Operation access check → execute mutation → format response
- SqlFunctions: Atomic database operations (one function per table operation)
Request Pipeline
- Express routes receive incoming HTTP requests
- RequestHandler extracts JWT from Authorization header
- JWT validated → authenticated context; No JWT → guest context
- Route-level access policy checked
- QueryHandler or MutationHandler executes business logic
- SqlFunctions handle database operations through SqlClient
- RequestHandler renews session (if authenticated and not auth error)
- Response returned (with new JWT if session renewed)
Context Layer - Centralized Authentication
Location: src/context/
Architecture Decision: cwc-api does NOT duplicate JWT verification logic. All token validation goes through cwc-auth's /auth/v1/verify-token endpoint via AuthClient.
Trade-off: Additional HTTP call per request, but auth logic stays centralized in cwc-auth (single source of truth).
Files:
context.types.ts- AuthenticatedContext, GuestContext, RequestContext typescreateContext.ts- Creates context from Authorization headerindex.ts- Exports
Context Types:
// Authenticated user (valid JWT)
type AuthenticatedContext = {
isAuthenticated: true;
role: 'project-owner' | 'logged-on-user'; // Determined per-operation
userPkId: number;
username: string;
ownedProjects: string[];
payload: UserJwtPayload;
};
// Unauthenticated request (no/invalid JWT)
type GuestContext = {
isAuthenticated: false;
role: 'guest-user';
};
type RequestContext = AuthenticatedContext | GuestContext;
Usage:
import { createContext } from './context';
import { AuthClient } from 'cwc-backend-utils';
const context = await createContext({
authHeader: req.headers.authorization,
authClient: authClient,
});
if (context.isAuthenticated) {
// TypeScript narrows to AuthenticatedContext
console.log(context.userPkId, context.ownedProjects);
} else {
// TypeScript narrows to GuestContext
console.log(context.role); // 'guest-user'
}
Graceful Degradation:
- Missing token → guest context
- Invalid token → guest context
- Expired token → guest context
- Auth service error → guest context (logged)
- No errors thrown; auth failures result in guest access
Handler Architecture
Location: src/handlers/
Files:
handler.types.ts- Type definitions for handlers, operations, and responsesRequestHandler.ts- Entry point: route access check, delegation, session renewalQueryHandler.ts- Read operations with pagination supportMutationHandler.ts- Write operationsresponseUtils.ts- Shared response creation utilities (error responses, status code mapping)index.ts- Exports
Handler Flow:
RequestHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied (NO renewal)
├── buildOperationContext() → projectId, resourceId from path
├── delegate to QueryHandler or MutationHandler
│ ├── checkOperationAccess() → 403 if denied
│ ├── execute operation()
│ └── format response (no JWT)
├── if authenticated AND not auth error (401/403):
│ └── renewSession() → merge JWT into response
└── return final response
Session Renewal Strategy:
- Centralized in RequestHandler (not Query/MutationHandler)
- Renews on ALL requests except auth errors (401, 403)
- Even failed operations (404, 500) trigger renewal to keep session active
- New JWT merged into response body (
response.body.jwt) for success responses - Renewal failure doesn't fail the request (graceful degradation, logged)
- Guest users never trigger renewal
Error Code Mapping:
| Error Code | HTTP Status |
|---|---|
UNAUTHORIZED |
401 |
FORBIDDEN |
403 |
NOT_FOUND |
404 |
ALREADY_EXISTS |
400 |
VALIDATION_ERROR |
400 |
INVALID_PARAMETER |
400 |
DATABASE_ERROR |
500 |
INTERNAL_ERROR |
500 |
Role-Based Access Control
Roles:
| Role | Description |
|---|---|
project-owner |
User owns the project being accessed |
logged-on-user |
Authenticated user (may not own the project) |
guest-user |
Unauthenticated request (read-only access) |
Access Policy Levels:
- Route-level: Can this role call this endpoint at all?
- Operation-level: Can this role execute this specific operation? (Phase 4)
- Context-aware: Does the user have the right relationship to the data? (e.g., ownership)
Tables Handled
Content Tables (Full CRUD + Soft Delete):
- project
- codingSession
- codingSessionContent
- codingSessionAttachment
- comment
- reaction
- contentReport
- abuseReport
Read-Only Tables:
- featureFlag (all fields)
- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
SqlFunction Naming Convention
Each table has a subdirectory in src/sql/ with these functions:
| Function | Purpose |
|---|---|
select{Table}.ts |
Retrieve single record by identifier |
list{Table}s.ts |
Retrieve multiple records with filtering |
insert{Table}.ts |
Create new record |
update{Table}.ts |
Modify existing record |
delete{Table}.ts |
Hard delete (GDPR compliance) |
softDelete{Table}.ts |
Soft delete (set enabled=false) |
Key Principles:
- Single responsibility: Each SqlFunction does one thing
- No business logic: SqlFunctions handle data access only
- Soft-delete awareness: List queries automatically filter
enabled=true - Composability: Queries/Mutations compose multiple SqlFunctions
cwc-auth Integration
Division of Responsibilities:
| Concern | Owner |
|---|---|
| "Is this token valid?" | cwc-auth |
| "Who is this user?" | cwc-auth (via token claims) |
| "Can this user access this endpoint?" | cwc-api |
| "Is this user the owner of this project?" | cwc-api |
Session Renewal Flow:
When cwc-api creates/deletes a project:
- cwc-api performs the database operation
- cwc-api calls cwc-auth
/auth/v1/renew-session - cwc-auth issues new JWT with updated
ownedProjects - cwc-api returns new JWT to client
Response Pattern
Standard Response Envelope:
// Success
{
success: true,
data: { ... },
// Optional: pagination metadata
}
// Error
{
success: false,
errorCode: 'ERROR_CODE',
errorMessage: 'Human-readable message',
// Dev-only: errorDetail
}
Pagination (Offset-based):
{
success: true,
data: [...],
pagination: {
page: 1,
pageSize: 20,
totalCount: 100,
hasMore: true
}
}
Configuration
Required Environment Variables:
RUNTIME_ENVIRONMENT=dev
SERVICE_PORT=5040
DATA_URI=http://localhost:5020/data/v1
AUTH_URI=http://localhost:5005/auth/v1
APP_URL=http://localhost:3000
CORS_ORIGIN=*
DEV_CORS_ORIGIN=http://localhost:3000
RATE_LIMITER_POINTS=15
RATE_LIMITER_DURATION=1
DEBUG_MODE=OFF
LOG_ERRORS_TO_DATABASE=ON
USER_JWT_SECRET=...
SQL_CLIENT_API_KEY=...
API Versioning
URL-based versioning:
/api/v1/*routes to CwcApiV1 handlers- Each version lives in
src/apis/CwcApiV{N}/ - Route definitions in
routes.ts, organized alphabetically
Critical Rules
SqlClient Usage:
- Never provide
createdDateormodifiedDate- handled by cwc-sql automatically - Always use SqlFunctions, never construct queries directly in business logic
Guest User Handling:
- No JWT required - RequestHandler creates context with
role: 'guest-user' - Guest users have read-only access to public content
Soft Delete:
- All content tables support soft delete (
enabled=false) - Hard delete available for GDPR compliance
- List queries automatically filter
enabled=true
Implementation Status
Active Development: This package is being implemented following cwc-api-implementation-checklist.md.
The checklist tracks 7 phases:
- Package Foundation (config, Express service, health check) - Complete
- Authentication & Context Layer (JWT, request context) - Complete
- Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - Complete
- Access Policy System (route and operation policies)
- SqlFunction Layer (CRUD operations for all tables)
- CwcApiV1 Routes & Business Logic
- Integration Testing & Final Review
Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
Unit Testing
Test Infrastructure:
- Jest 30.x with ts-jest for TypeScript ESM support
- Test files:
src/__tests__/**/*.test.ts - Setup:
src/__tests__/setup.tsloads unit test environment vialoadDotEnv - Run tests:
pnpm api testorNODE_ENV=unit pnpm --filter cwc-api test
Mock Infrastructure (src/__tests__/mocks/):
| Mock | Purpose |
|---|---|
| SqlClient Mocks | |
createMockSqlClient() |
Full SqlClient mock with jest.fn() for all methods |
mockSelectSuccess/NotFound |
Configure SqlClient for select queries |
mockListSuccess |
Configure SqlClient for list queries |
mockInsertSuccess/Failure |
Configure SqlClient for inserts |
mockUpdateSuccess/NotFound |
Configure SqlClient for updates |
mockDeleteSuccess/NotFound |
Configure SqlClient for deletes |
| AuthClient Mocks | |
createMockAuthClient() |
Full AuthClient mock (verifyToken, renewSession) |
createMockUserJwtPayload(overrides) |
Create UserJwtPayload with custom overrides |
mockVerifyTokenSuccess(client, payload?) |
Configure AuthClient to return success |
mockVerifyTokenFailure(client, error?) |
Configure AuthClient to return failure |
mockRenewSessionSuccess(client, jwt?) |
Configure renewSession to return new JWT |
mockRenewSessionFailure(client, error?) |
Configure renewSession to return failure |
| Route/Handler Mocks | |
createMockRouteConfig(overrides) |
Create route config for testing |
createMockOperationContext(overrides) |
Create operation context for testing |
createMockGuestContext() |
Create guest (unauthenticated) context |
createMockAuthenticatedContext(overrides) |
Create authenticated user context |
mockOperationSuccess(routeConfig, data) |
Configure operation to succeed |
mockOperationFailure(routeConfig, code, msg) |
Configure operation to fail |
mockOperationThrows(routeConfig, error) |
Configure operation to throw |
| Config Mocks | |
getUnitConfig() |
Load real config from unit.cwc-api.env |
createMockConfig(overrides) |
Create config with custom overrides |
createMockDevConfig(overrides) |
Mock config with isDev: true |
createMockProdConfig(overrides) |
Mock config with isProd: true |
createMockUnitConfig(overrides) |
Mock config with isUnit: true |
| Logger Mock | |
createMockLogger() |
ILogger mock with logDebug, logError, logInformation |
Testing Patterns:
RequestHandler tests (includes session renewal):
import {
createMockAuthClient,
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
mockRenewSessionSuccess,
getUnitConfig,
} from '../mocks';
describe('RequestHandler', () => {
const unitConfig = getUnitConfig();
const mockAuthClient = createMockAuthClient();
it('should renew session on success', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'query' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
// RequestHandler uses 4 args: options, config, authClient, logger
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
// ...
});
});
QueryHandler/MutationHandler tests (no authClient, no renewal):
import {
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
getUnitConfig,
} from '../mocks';
describe('QueryHandler', () => {
const unitConfig = getUnitConfig();
it('should execute operation', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
// QueryHandler uses 3 args: options, config, logger
const handler = new QueryHandler(options, unitConfig, undefined);
// ...
});
});
Related Packages
Depends On:
cwc-backend-utils(workspace) - SqlClient, Logger, Express service factorycwc-schema(workspace) - Data validationcwc-types(workspace) - Entity types, RuntimeEnvironment
Coordinates With:
cwc-auth- JWT validation, session renewalcwc-sql- Database operations
Consumed By:
cwc-website- Frontend web application
Version 2
cwc-api Package
Main business logic API for CWC application. Acts as the central orchestrator that handles CRUD operations, enforces business rules, and coordinates data flow between frontend applications and the database layer.
Architecture
Database-Connected Microservice:
- Uses SqlClient from
cwc-backend-utilsto communicate with cwc-sql - Uses database-backed Logger for error logging
- Never connects directly to database - all data access flows through cwc-sql
- Coordinates with cwc-auth for session renewal
Layered Architecture:
Request → RequestHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
- RequestHandler: Auth → context creation → route access check → delegation → session renewal
- QueryHandler: Operation access check → execute query → format response
- MutationHandler: Operation access check → execute mutation → format response
- SqlFunctions: Atomic database operations (one function per table operation)
Request Pipeline
- Express routes receive incoming HTTP requests
- RequestHandler extracts JWT from Authorization header
- JWT validated → authenticated context; No JWT → guest context
- Route-level access policy checked
- QueryHandler or MutationHandler executes business logic
- SqlFunctions handle database operations through SqlClient
- RequestHandler renews session (if authenticated and not auth error)
- Response returned (with new JWT if session renewed)
Context Layer - Centralized Authentication
Location: src/context/
Architecture Decision: cwc-api does NOT duplicate JWT verification logic. All token validation goes through cwc-auth's /auth/v1/verify-token endpoint via AuthClient.
Trade-off: Additional HTTP call per request, but auth logic stays centralized in cwc-auth (single source of truth).
Files:
context.types.ts- AuthenticatedContext, GuestContext, RequestContext typescreateContext.ts- Creates context from Authorization headerindex.ts- Exports
Context Types:
// Authenticated user (valid JWT)
type AuthenticatedContext = {
isAuthenticated: true;
role: 'project-owner' | 'logged-on-user'; // Determined per-operation
userPkId: number;
username: string;
ownedProjects: string[];
payload: UserJwtPayload;
};
// Unauthenticated request (no/invalid JWT)
type GuestContext = {
isAuthenticated: false;
role: 'guest-user';
};
type RequestContext = AuthenticatedContext | GuestContext;
Usage:
import { createContext } from './context';
import { AuthClient } from 'cwc-backend-utils';
const context = await createContext({
authHeader: req.headers.authorization,
authClient: authClient,
});
if (context.isAuthenticated) {
// TypeScript narrows to AuthenticatedContext
console.log(context.userPkId, context.ownedProjects);
} else {
// TypeScript narrows to GuestContext
console.log(context.role); // 'guest-user'
}
Graceful Degradation:
- Missing token → guest context
- Invalid token → guest context
- Expired token → guest context
- Auth service error → guest context (logged)
- No errors thrown; auth failures result in guest access
Handler Architecture
Location: src/handlers/
Files:
handler.types.ts- Type definitions for handlers, operations, and responsesRequestHandler.ts- Entry point: route access check, delegation, session renewalQueryHandler.ts- Read operations with pagination supportMutationHandler.ts- Write operationsresponseUtils.ts- Shared response creation utilities (error responses, status code mapping)index.ts- Exports
Handler Flow:
RequestHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied (NO renewal)
├── buildOperationContext() → projectId, resourceId from path
├── delegate to QueryHandler or MutationHandler
│ ├── checkOperationAccess() → 403 if denied
│ ├── execute operation()
│ └── format response (no JWT)
├── if authenticated AND not auth error (401/403):
│ └── renewSession() → merge JWT into response
└── return final response
Session Renewal Strategy:
- Centralized in RequestHandler (not Query/MutationHandler)
- Renews on ALL requests except auth errors (401, 403)
- Even failed operations (404, 500) trigger renewal to keep session active
- New JWT merged into response body (
response.body.jwt) for success responses - Renewal failure doesn't fail the request (graceful degradation, logged)
- Guest users never trigger renewal
Error Code Mapping:
| Error Code | HTTP Status |
|---|---|
UNAUTHORIZED |
401 |
FORBIDDEN |
403 |
NOT_FOUND |
404 |
ALREADY_EXISTS |
400 |
VALIDATION_ERROR |
400 |
INVALID_PARAMETER |
400 |
DATABASE_ERROR |
500 |
INTERNAL_ERROR |
500 |
Access Policy System
Location: src/policies/
Files:
policy.types.ts- Re-exports RouteAccessResult, OperationAccessResultcheckRouteAccess.ts- Route-level authentication checkcheckOperationAccess.ts- Operation-level access with ownership checkindex.ts- Exports
Role Hierarchy:
guest-user < logged-on-user < project-owner
| Role | Description |
|---|---|
guest-user |
Unauthenticated request (public read-only access) |
logged-on-user |
Authenticated user (may not own the project) |
project-owner |
User owns the project being accessed |
Two-Level Access Control:
Routes specify requiredRole: CwcRole which is checked at two levels:
| requiredRole | Route-Level Check | Operation-Level Check |
|---|---|---|
guest-user |
Anyone allowed | Anyone allowed |
logged-on-user |
Must be authenticated | Must be authenticated |
project-owner |
Must be authenticated | Must be authenticated AND own the project |
Key Design Decision: Route-level for project-owner only checks authentication (no DB lookup). The actual ownership check happens at operation-level using context.ownedProjects from the JWT.
Policy Functions:
// Route-level: Only checks authentication, no ownership
checkRouteAccess(context: RequestContext, requiredRole: CwcRole): RouteAccessResult
// Operation-level: Checks authentication AND ownership for project-owner
checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult
// Helper: Checks if user owns a specific project
isProjectOwner(context: RequestContext, projectId: string | undefined): boolean
Security:
- Ownership verified from JWT claims (
context.ownedProjects.includes(projectId)) projectIdcomes from URL path params (viaoperationContext), not request body- No database lookups for access checks - all data comes from JWT
Tables Handled
Content Tables (Full CRUD + Soft Delete):
- project
- codingSession
- codingSessionContent
- codingSessionAttachment
- comment
- reaction
- contentReport
- abuseReport
Read-Only Tables:
- featureFlag (all fields)
- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
SqlFunction Naming Convention
Each table has a subdirectory in src/sql/ with these functions:
| Function | Purpose |
|---|---|
select{Table}.ts |
Retrieve single record by identifier |
list{Table}s.ts |
Retrieve multiple records with filtering |
insert{Table}.ts |
Create new record |
update{Table}.ts |
Modify existing record |
delete{Table}.ts |
Hard delete (GDPR compliance) |
softDelete{Table}.ts |
Soft delete (set enabled=false) |
Key Principles:
- Single responsibility: Each SqlFunction does one thing
- No business logic: SqlFunctions handle data access only
- Soft-delete awareness: List queries automatically filter
enabled=true - Composability: Queries/Mutations compose multiple SqlFunctions
cwc-auth Integration
Division of Responsibilities:
| Concern | Owner |
|---|---|
| "Is this token valid?" | cwc-auth |
| "Who is this user?" | cwc-auth (via token claims) |
| "Can this user access this endpoint?" | cwc-api |
| "Is this user the owner of this project?" | cwc-api |
Session Renewal Flow:
When cwc-api creates/deletes a project:
- cwc-api performs the database operation
- cwc-api calls cwc-auth
/auth/v1/renew-session - cwc-auth issues new JWT with updated
ownedProjects - cwc-api returns new JWT to client
Response Pattern
Standard Response Envelope:
// Success
{
success: true,
data: { ... },
// Optional: pagination metadata
}
// Error
{
success: false,
errorCode: 'ERROR_CODE',
errorMessage: 'Human-readable message',
// Dev-only: errorDetail
}
Pagination (Offset-based):
{
success: true,
data: [...],
pagination: {
page: 1,
pageSize: 20,
totalCount: 100,
hasMore: true
}
}
Configuration
Required Environment Variables:
RUNTIME_ENVIRONMENT=dev
SERVICE_PORT=5040
DATA_URI=http://localhost:5020/data/v1
AUTH_URI=http://localhost:5005/auth/v1
APP_URL=http://localhost:3000
CORS_ORIGIN=*
DEV_CORS_ORIGIN=http://localhost:3000
RATE_LIMITER_POINTS=15
RATE_LIMITER_DURATION=1
DEBUG_MODE=OFF
LOG_ERRORS_TO_DATABASE=ON
USER_JWT_SECRET=...
SQL_CLIENT_API_KEY=...
API Versioning
URL-based versioning:
/api/v1/*routes to CwcApiV1 handlers- Each version lives in
src/apis/CwcApiV{N}/ - Route definitions in
routes.ts, organized alphabetically
Critical Rules
SqlClient Usage:
- Never provide
createdDateormodifiedDate- handled by cwc-sql automatically - Always use SqlFunctions, never construct queries directly in business logic
Guest User Handling:
- No JWT required - RequestHandler creates context with
role: 'guest-user' - Guest users have read-only access to public content
Soft Delete:
- All content tables support soft delete (
enabled=false) - Hard delete available for GDPR compliance
- List queries automatically filter
enabled=true
Implementation Status
Active Development: This package is being implemented following cwc-api-implementation-checklist.md.
The checklist tracks 7 phases:
- Package Foundation (config, Express service, health check) - Complete
- Authentication & Context Layer (JWT, request context) - Complete
- Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - Complete
- Access Policy System (route and operation policies) - Complete
- SqlFunction Layer (CRUD operations for all tables)
- CwcApiV1 Routes & Business Logic
- Integration Testing & Final Review
Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
Unit Testing
Test Infrastructure:
- Jest 30.x with ts-jest for TypeScript ESM support
- Test files:
src/__tests__/**/*.test.ts - Setup:
src/__tests__/setup.tsloads unit test environment vialoadDotEnv - Run tests:
pnpm api testorNODE_ENV=unit pnpm --filter cwc-api test
Mock Infrastructure (src/__tests__/mocks/):
| Mock | Purpose |
|---|---|
| SqlClient Mocks | |
createMockSqlClient() |
Full SqlClient mock with jest.fn() for all methods |
mockSelectSuccess/NotFound |
Configure SqlClient for select queries |
mockListSuccess |
Configure SqlClient for list queries |
mockInsertSuccess/Failure |
Configure SqlClient for inserts |
mockUpdateSuccess/NotFound |
Configure SqlClient for updates |
mockDeleteSuccess/NotFound |
Configure SqlClient for deletes |
| AuthClient Mocks | |
createMockAuthClient() |
Full AuthClient mock (verifyToken, renewSession) |
createMockUserJwtPayload(overrides) |
Create UserJwtPayload with custom overrides |
mockVerifyTokenSuccess(client, payload?) |
Configure AuthClient to return success |
mockVerifyTokenFailure(client, error?) |
Configure AuthClient to return failure |
mockRenewSessionSuccess(client, jwt?) |
Configure renewSession to return new JWT |
mockRenewSessionFailure(client, error?) |
Configure renewSession to return failure |
| Route/Handler Mocks | |
createMockRouteConfig(overrides) |
Create route config for testing |
createMockOperationContext(overrides) |
Create operation context for testing |
createMockGuestContext() |
Create guest (unauthenticated) context |
createMockAuthenticatedContext(overrides) |
Create authenticated user context |
mockOperationSuccess(routeConfig, data) |
Configure operation to succeed |
mockOperationFailure(routeConfig, code, msg) |
Configure operation to fail |
mockOperationThrows(routeConfig, error) |
Configure operation to throw |
| Config Mocks | |
getUnitConfig() |
Load real config from unit.cwc-api.env |
createMockConfig(overrides) |
Create config with custom overrides |
createMockDevConfig(overrides) |
Mock config with isDev: true |
createMockProdConfig(overrides) |
Mock config with isProd: true |
createMockUnitConfig(overrides) |
Mock config with isUnit: true |
| Logger Mock | |
createMockLogger() |
ILogger mock with logDebug, logError, logInformation |
Testing Patterns:
RequestHandler tests (includes session renewal):
import {
createMockAuthClient,
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
mockRenewSessionSuccess,
getUnitConfig,
} from '../mocks';
describe('RequestHandler', () => {
const unitConfig = getUnitConfig();
const mockAuthClient = createMockAuthClient();
it('should renew session on success', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'query' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
// RequestHandler uses 4 args: options, config, authClient, logger
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
// ...
});
});
QueryHandler/MutationHandler tests (no authClient, no renewal):
import {
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
getUnitConfig,
} from '../mocks';
describe('QueryHandler', () => {
const unitConfig = getUnitConfig();
it('should execute operation', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
// QueryHandler uses 3 args: options, config, logger
const handler = new QueryHandler(options, unitConfig, undefined);
// ...
});
});
Related Packages
Depends On:
cwc-backend-utils(workspace) - SqlClient, Logger, Express service factorycwc-schema(workspace) - Data validationcwc-types(workspace) - Entity types, RuntimeEnvironment
Coordinates With:
cwc-auth- JWT validation, session renewalcwc-sql- Database operations
Consumed By:
cwc-website- Frontend web application
Version 3
cwc-api Package
Main business logic API for CWC application. Acts as the central orchestrator that handles CRUD operations, enforces business rules, and coordinates data flow between frontend applications and the database layer.
Architecture
Database-Connected Microservice:
- Uses SqlClient from
cwc-backend-utilsto communicate with cwc-sql - Uses database-backed Logger for error logging
- Never connects directly to database - all data access flows through cwc-sql
- Coordinates with cwc-auth for session renewal
Layered Architecture:
Request → RequestHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
- RequestHandler: Auth → context creation → route access check → delegation → session renewal
- QueryHandler: Operation access check → execute query → format response
- MutationHandler: Operation access check → execute mutation → format response
- SqlFunctions: Atomic database operations (one function per table operation)
Request Pipeline
- Express routes receive incoming HTTP requests
- RequestHandler extracts JWT from Authorization header
- JWT validated → authenticated context; No JWT → guest context
- Route-level access policy checked
- QueryHandler or MutationHandler executes business logic
- SqlFunctions handle database operations through SqlClient
- RequestHandler renews session (if authenticated and not auth error)
- Response returned (with new JWT if session renewed)
Context Layer - Centralized Authentication
Location: src/context/
Architecture Decision: cwc-api does NOT duplicate JWT verification logic. All token validation goes through cwc-auth's /auth/v1/verify-token endpoint via AuthClient.
Trade-off: Additional HTTP call per request, but auth logic stays centralized in cwc-auth (single source of truth).
Files:
context.types.ts- AuthenticatedContext, GuestContext, RequestContext typescreateContext.ts- Creates context from Authorization headerindex.ts- Exports
Context Types:
// Authenticated user (valid JWT)
type AuthenticatedContext = {
isAuthenticated: true;
role: 'project-owner' | 'logged-on-user'; // Determined per-operation
userPkId: number;
username: string;
ownedProjects: string[];
payload: UserJwtPayload;
};
// Unauthenticated request (no/invalid JWT)
type GuestContext = {
isAuthenticated: false;
role: 'guest-user';
};
type RequestContext = AuthenticatedContext | GuestContext;
Usage:
import { createContext } from './context';
import { AuthClient } from 'cwc-backend-utils';
const context = await createContext({
authHeader: req.headers.authorization,
authClient: authClient,
});
if (context.isAuthenticated) {
// TypeScript narrows to AuthenticatedContext
console.log(context.userPkId, context.ownedProjects);
} else {
// TypeScript narrows to GuestContext
console.log(context.role); // 'guest-user'
}
Graceful Degradation:
- Missing token → guest context
- Invalid token → guest context
- Expired token → guest context
- Auth service error → guest context (logged)
- No errors thrown; auth failures result in guest access
Handler Architecture
Location: src/handlers/
Files:
handler.types.ts- Type definitions for handlers, operations, and responsesRequestHandler.ts- Entry point: route access check, delegation, session renewalQueryHandler.ts- Read operations with pagination supportMutationHandler.ts- Write operationsresponseUtils.ts- Shared response creation utilities (error responses, status code mapping)index.ts- Exports
Handler Flow:
RequestHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied (NO renewal)
├── buildOperationContext() → projectId, resourceId from path
├── delegate to QueryHandler or MutationHandler
│ ├── checkOperationAccess() → 403 if denied
│ ├── execute operation()
│ └── format response (no JWT)
├── if authenticated AND not auth error (401/403):
│ └── renewSession() → merge JWT into response
└── return final response
Session Renewal Strategy:
- Centralized in RequestHandler (not Query/MutationHandler)
- Renews on ALL requests except auth errors (401, 403)
- Even failed operations (404, 500) trigger renewal to keep session active
- New JWT merged into response body (
response.body.jwt) for success responses - Renewal failure doesn't fail the request (graceful degradation, logged)
- Guest users never trigger renewal
Error Code Mapping:
| Error Code | HTTP Status |
|---|---|
UNAUTHORIZED |
401 |
FORBIDDEN |
403 |
NOT_FOUND |
404 |
ALREADY_EXISTS |
400 |
VALIDATION_ERROR |
400 |
INVALID_PARAMETER |
400 |
DATABASE_ERROR |
500 |
INTERNAL_ERROR |
500 |
Access Policy System
Location: src/policies/
Files:
policy.types.ts- Re-exports RouteAccessResult, OperationAccessResultcheckRouteAccess.ts- Route-level authentication checkcheckOperationAccess.ts- Operation-level access with ownership checkindex.ts- Exports
Role Hierarchy:
guest-user < logged-on-user < project-owner
| Role | Description |
|---|---|
guest-user |
Unauthenticated request (public read-only access) |
logged-on-user |
Authenticated user (may not own the project) |
project-owner |
User owns the project being accessed |
Two-Level Access Control:
Routes specify requiredRole: CwcRole which is checked at two levels:
| requiredRole | Route-Level Check | Operation-Level Check |
|---|---|---|
guest-user |
Anyone allowed | Anyone allowed |
logged-on-user |
Must be authenticated | Must be authenticated |
project-owner |
Must be authenticated | Must be authenticated AND own the project |
Key Design Decision: Route-level for project-owner only checks authentication (no DB lookup). The actual ownership check happens at operation-level using context.ownedProjects from the JWT.
Policy Functions:
// Route-level: Only checks authentication, no ownership
checkRouteAccess(context: RequestContext, requiredRole: CwcRole): RouteAccessResult
// Operation-level: Checks authentication AND ownership for project-owner
checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult
// Helper: Checks if user owns a specific project
isProjectOwner(context: RequestContext, projectId: string | undefined): boolean
Security:
- Ownership verified from JWT claims (
context.ownedProjects.includes(projectId)) projectIdcomes from URL path params (viaoperationContext), not request body- No database lookups for access checks - all data comes from JWT
Tables Handled
Content Tables (Full CRUD + Soft Delete):
- project
- codingSession
- codingSessionContent
- codingSessionAttachment
- comment
- reaction
- contentReport
- abuseReport
Read-Only Tables:
- featureFlag (all fields)
- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
SqlFunction Naming Convention
Each table has a subdirectory in src/sql/ with these functions:
| Function | Purpose |
|---|---|
select{Table}.ts |
Retrieve single record by identifier |
list{Table}s.ts |
Retrieve multiple records with filtering |
insert{Table}.ts |
Create new record |
update{Table}.ts |
Modify existing record |
delete{Table}.ts |
Hard delete (GDPR compliance) |
softDelete{Table}.ts |
Soft delete (set enabled=false) |
Key Principles:
- Single responsibility: Each SqlFunction does one thing
- No business logic: SqlFunctions handle data access only
- Soft-delete awareness: List queries automatically filter
enabled=true - Composability: Queries/Mutations compose multiple SqlFunctions
cwc-auth Integration
Division of Responsibilities:
| Concern | Owner |
|---|---|
| "Is this token valid?" | cwc-auth |
| "Who is this user?" | cwc-auth (via token claims) |
| "Can this user access this endpoint?" | cwc-api |
| "Is this user the owner of this project?" | cwc-api |
Session Renewal Flow:
When cwc-api creates/deletes a project:
- cwc-api performs the database operation
- cwc-api calls cwc-auth
/auth/v1/renew-session - cwc-auth issues new JWT with updated
ownedProjects - cwc-api returns new JWT to client
Response Pattern
Standard Response Envelope:
// Success
{
success: true,
data: { ... },
// Optional: pagination metadata
}
// Error
{
success: false,
errorCode: 'ERROR_CODE',
errorMessage: 'Human-readable message',
// Dev-only: errorDetail
}
Pagination (Offset-based):
{
success: true,
data: [...],
pagination: {
page: 1,
pageSize: 20,
totalCount: 100,
hasMore: true
}
}
Configuration
Required Environment Variables:
RUNTIME_ENVIRONMENT=dev
SERVICE_PORT=5040
DATA_URI=http://localhost:5020/data/v1
AUTH_URI=http://localhost:5005/auth/v1
APP_URL=http://localhost:3000
CORS_ORIGIN=*
DEV_CORS_ORIGIN=http://localhost:3000
RATE_LIMITER_POINTS=15
RATE_LIMITER_DURATION=1
DEBUG_MODE=OFF
LOG_ERRORS_TO_DATABASE=ON
USER_JWT_SECRET=...
SQL_CLIENT_API_KEY=...
API Versioning
URL-based versioning:
/api/v1/*routes to CwcApiV1 handlers- Each version lives in
src/apis/CwcApiV{N}/ - Route definitions in
routes.ts, organized alphabetically
Critical Rules
SqlClient Usage:
- Never provide
createdDateormodifiedDate- handled by cwc-sql automatically - Always use SqlFunctions, never construct queries directly in business logic
Guest User Handling:
- No JWT required - RequestHandler creates context with
role: 'guest-user' - Guest users have read-only access to public content
Soft Delete:
- All content tables support soft delete (
enabled=false) - Hard delete available for GDPR compliance
- List queries automatically filter
enabled=true
Error and Informational Messages:
Use actual field names in internal error messages, not "pretty" names. This avoids ambiguity about which field is being referenced (e.g., projectId vs projectPkId).
// ✅ GOOD - uses actual field name
reason: 'projectId is required for ownership check'
// ❌ AVOID - ambiguous, could be projectId or projectPkId
reason: 'Project ID required for ownership check'
This applies to reason, errorDetail, log messages, and any internal diagnostic messages. User-facing messages like errorMessage can use natural language ("Access denied", "Resource not found").
Implementation Status
Active Development: This package is being implemented following cwc-api-implementation-checklist.md.
The checklist tracks 7 phases:
- Package Foundation (config, Express service, health check) - Complete
- Authentication & Context Layer (JWT, request context) - Complete
- Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - Complete
- Access Policy System (route and operation policies) - Complete
- SqlFunction Layer (CRUD operations for all tables)
- CwcApiV1 Routes & Business Logic
- Integration Testing & Final Review
Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
Unit Testing
Test Infrastructure:
- Jest 30.x with ts-jest for TypeScript ESM support
- Test files:
src/__tests__/**/*.test.ts - Setup:
src/__tests__/setup.tsloads unit test environment vialoadDotEnv - Run tests:
pnpm api testorNODE_ENV=unit pnpm --filter cwc-api test
Mock Infrastructure (src/__tests__/mocks/):
| Mock | Purpose |
|---|---|
| SqlClient Mocks | |
createMockSqlClient() |
Full SqlClient mock with jest.fn() for all methods |
mockSelectSuccess/NotFound |
Configure SqlClient for select queries |
mockListSuccess |
Configure SqlClient for list queries |
mockInsertSuccess/Failure |
Configure SqlClient for inserts |
mockUpdateSuccess/NotFound |
Configure SqlClient for updates |
mockDeleteSuccess/NotFound |
Configure SqlClient for deletes |
| AuthClient Mocks | |
createMockAuthClient() |
Full AuthClient mock (verifyToken, renewSession) |
createMockUserJwtPayload(overrides) |
Create UserJwtPayload with custom overrides |
mockVerifyTokenSuccess(client, payload?) |
Configure AuthClient to return success |
mockVerifyTokenFailure(client, error?) |
Configure AuthClient to return failure |
mockRenewSessionSuccess(client, jwt?) |
Configure renewSession to return new JWT |
mockRenewSessionFailure(client, error?) |
Configure renewSession to return failure |
| Route/Handler Mocks | |
createMockRouteConfig(overrides) |
Create route config for testing |
createMockOperationContext(overrides) |
Create operation context for testing |
createMockGuestContext() |
Create guest (unauthenticated) context |
createMockAuthenticatedContext(overrides) |
Create authenticated user context |
mockOperationSuccess(routeConfig, data) |
Configure operation to succeed |
mockOperationFailure(routeConfig, code, msg) |
Configure operation to fail |
mockOperationThrows(routeConfig, error) |
Configure operation to throw |
| Config Mocks | |
getUnitConfig() |
Load real config from unit.cwc-api.env |
createMockConfig(overrides) |
Create config with custom overrides |
createMockDevConfig(overrides) |
Mock config with isDev: true |
createMockProdConfig(overrides) |
Mock config with isProd: true |
createMockUnitConfig(overrides) |
Mock config with isUnit: true |
| Logger Mock | |
createMockLogger() |
ILogger mock with logDebug, logError, logInformation |
Testing Patterns:
RequestHandler tests (includes session renewal):
import {
createMockAuthClient,
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
mockRenewSessionSuccess,
getUnitConfig,
} from '../mocks';
describe('RequestHandler', () => {
const unitConfig = getUnitConfig();
const mockAuthClient = createMockAuthClient();
it('should renew session on success', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'query' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
// RequestHandler uses 4 args: options, config, authClient, logger
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
// ...
});
});
QueryHandler/MutationHandler tests (no authClient, no renewal):
import {
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
getUnitConfig,
} from '../mocks';
describe('QueryHandler', () => {
const unitConfig = getUnitConfig();
it('should execute operation', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
// QueryHandler uses 3 args: options, config, logger
const handler = new QueryHandler(options, unitConfig, undefined);
// ...
});
});
Related Packages
Depends On:
cwc-backend-utils(workspace) - SqlClient, Logger, Express service factorycwc-schema(workspace) - Data validationcwc-types(workspace) - Entity types, RuntimeEnvironment
Coordinates With:
cwc-auth- JWT validation, session renewalcwc-sql- Database operations
Consumed By:
cwc-website- Frontend web application
Version 4
cwc-api Package
Main business logic API for CWC application. Acts as the central orchestrator that handles CRUD operations, enforces business rules, and coordinates data flow between frontend applications and the database layer.
Architecture
Database-Connected Microservice:
- Uses SqlClient from
cwc-backend-utilsto communicate with cwc-sql - Uses database-backed Logger for error logging
- Never connects directly to database - all data access flows through cwc-sql
- Coordinates with cwc-auth for session renewal
Layered Architecture:
Request → RequestHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
- RequestHandler: Auth → context creation → route access check → delegation → session renewal
- QueryHandler: Operation access check → execute query → format response
- MutationHandler: Operation access check → execute mutation → format response
- SqlFunctions: Atomic database operations (one function per table operation)
Request Pipeline
- Express routes receive incoming HTTP requests
- RequestHandler extracts JWT from Authorization header
- JWT validated → authenticated context; No JWT → guest context
- Route-level access policy checked
- QueryHandler or MutationHandler executes business logic
- SqlFunctions handle database operations through SqlClient
- RequestHandler renews session (if authenticated and not auth error)
- Response returned (with new JWT if session renewed)
Context Layer - Centralized Authentication
Location: src/context/
Architecture Decision: cwc-api does NOT duplicate JWT verification logic. All token validation goes through cwc-auth's /auth/v1/verify-token endpoint via AuthClient.
Trade-off: Additional HTTP call per request, but auth logic stays centralized in cwc-auth (single source of truth).
Files:
context.types.ts- AuthenticatedContext, GuestContext, RequestContext typescreateContext.ts- Creates context from Authorization headerindex.ts- Exports
Context Types:
// Authenticated user (valid JWT)
type AuthenticatedContext = {
isAuthenticated: true;
role: 'project-owner' | 'logged-on-user'; // Determined per-operation
userPkId: number;
username: string;
ownedProjects: string[];
payload: UserJwtPayload;
};
// Unauthenticated request (no/invalid JWT)
type GuestContext = {
isAuthenticated: false;
role: 'guest-user';
};
type RequestContext = AuthenticatedContext | GuestContext;
Usage:
import { createContext } from './context';
import { AuthClient } from 'cwc-backend-utils';
const context = await createContext({
authHeader: req.headers.authorization,
authClient: authClient,
});
if (context.isAuthenticated) {
// TypeScript narrows to AuthenticatedContext
console.log(context.userPkId, context.ownedProjects);
} else {
// TypeScript narrows to GuestContext
console.log(context.role); // 'guest-user'
}
Graceful Degradation:
- Missing token → guest context
- Invalid token → guest context
- Expired token → guest context
- Auth service error → guest context (logged)
- No errors thrown; auth failures result in guest access
Handler Architecture
Location: src/handlers/
Files:
handler.types.ts- Type definitions for handlers, operations, and responsesRequestHandler.ts- Entry point: route access check, delegation, session renewalQueryHandler.ts- Read operations with pagination supportMutationHandler.ts- Write operationsresponseUtils.ts- Shared response creation utilities (error responses, status code mapping)index.ts- Exports
Handler Flow:
RequestHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied (NO renewal)
├── buildOperationContext() → projectId, resourceId from path
├── delegate to QueryHandler or MutationHandler
│ ├── checkOperationAccess() → 403 if denied
│ ├── execute operation()
│ └── format response (no JWT)
├── if authenticated AND not auth error (401/403):
│ └── renewSession() → merge JWT into response
└── return final response
Session Renewal Strategy:
- Centralized in RequestHandler (not Query/MutationHandler)
- Renews on ALL requests except auth errors (401, 403)
- Even failed operations (404, 500) trigger renewal to keep session active
- New JWT merged into response body (
response.body.jwt) for success responses - Renewal failure doesn't fail the request (graceful degradation, logged)
- Guest users never trigger renewal
Error Code Mapping:
| Error Code | HTTP Status |
|---|---|
UNAUTHORIZED |
401 |
FORBIDDEN |
403 |
NOT_FOUND |
404 |
ALREADY_EXISTS |
400 |
VALIDATION_ERROR |
400 |
INVALID_PARAMETER |
400 |
DATABASE_ERROR |
500 |
INTERNAL_ERROR |
500 |
Access Policy System
Location: src/policies/
Files:
policy.types.ts- Re-exports RouteAccessResult, OperationAccessResultcheckRouteAccess.ts- Route-level authentication checkcheckOperationAccess.ts- Operation-level access with ownership checkindex.ts- Exports
Role Hierarchy:
guest-user < logged-on-user < project-owner
| Role | Description |
|---|---|
guest-user |
Unauthenticated request (public read-only access) |
logged-on-user |
Authenticated user (may not own the project) |
project-owner |
User owns the project being accessed |
Two-Level Access Control:
Routes specify requiredRole: CwcRole which is checked at two levels:
| requiredRole | Route-Level Check | Operation-Level Check |
|---|---|---|
guest-user |
Anyone allowed | Anyone allowed |
logged-on-user |
Must be authenticated | Must be authenticated |
project-owner |
Must be authenticated | Must be authenticated AND own the project |
Key Design Decision: Route-level for project-owner only checks authentication (no DB lookup). The actual ownership check happens at operation-level using context.ownedProjects from the JWT.
Policy Functions:
// Route-level: Only checks authentication, no ownership
checkRouteAccess(context: RequestContext, requiredRole: CwcRole): RouteAccessResult
// Operation-level: Checks authentication AND ownership for project-owner
checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult
// Helper: Checks if user owns a specific project
isProjectOwner(context: RequestContext, projectId: string | undefined): boolean
Security:
- Ownership verified from JWT claims (
context.ownedProjects.includes(projectId)) projectIdcomes from URL path params (viaoperationContext), not request body- No database lookups for access checks - all data comes from JWT
Tables Handled
Content Tables (Full CRUD + Soft Delete):
- project
- codingSession
- codingSessionContent
- codingSessionAttachment
- comment
- reaction
- contentReport
- abuseReport
Read-Only Tables:
- featureFlag (all fields)
- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
SqlFunction Naming Convention
Each table has a subdirectory in src/sql/ with these functions:
| Function | Purpose |
|---|---|
select{Table}.ts |
Retrieve single record by identifier |
list{Table}s.ts |
Retrieve multiple records with filtering |
insert{Table}.ts |
Create new record |
update{Table}.ts |
Modify existing record |
delete{Table}.ts |
Hard delete (GDPR compliance) |
softDelete{Table}.ts |
Soft delete (set enabled=false) |
Key Principles:
- Single responsibility: Each SqlFunction does one thing
- No business logic: SqlFunctions handle data access only
- Soft-delete awareness: List queries automatically filter
enabled=true - Composability: Queries/Mutations compose multiple SqlFunctions
cwc-auth Integration
Division of Responsibilities:
| Concern | Owner |
|---|---|
| "Is this token valid?" | cwc-auth |
| "Who is this user?" | cwc-auth (via token claims) |
| "Can this user access this endpoint?" | cwc-api |
| "Is this user the owner of this project?" | cwc-api |
Session Renewal Flow:
When cwc-api creates/deletes a project:
- cwc-api performs the database operation
- cwc-api calls cwc-auth
/auth/v1/renew-session - cwc-auth issues new JWT with updated
ownedProjects - cwc-api returns new JWT to client
Response Pattern
Standard Response Envelope:
// Success
{
success: true,
data: { ... },
// Optional: pagination metadata
}
// Error
{
success: false,
errorCode: 'ERROR_CODE',
errorMessage: 'Human-readable message',
// Dev-only: errorDetail
}
Pagination (Offset-based):
{
success: true,
data: [...],
pagination: {
page: 1,
pageSize: 20,
totalCount: 100,
hasMore: true
}
}
Configuration
Required Environment Variables:
RUNTIME_ENVIRONMENT=dev
SERVICE_PORT=5040
DATA_URI=http://localhost:5020/data/v1
AUTH_URI=http://localhost:5005/auth/v1
APP_URL=http://localhost:3000
CORS_ORIGIN=*
DEV_CORS_ORIGIN=http://localhost:3000
RATE_LIMITER_POINTS=15
RATE_LIMITER_DURATION=1
DEBUG_MODE=OFF
LOG_ERRORS_TO_DATABASE=ON
USER_JWT_SECRET=...
SQL_CLIENT_API_KEY=...
API Versioning
URL-based versioning:
/api/v1/*routes to CwcApiV1 handlers- Each version lives in
src/apis/CwcApiV{N}/ - Route definitions in
routes.ts, organized alphabetically
Critical Rules
SqlClient Usage:
- Never provide
createdDateormodifiedDate- handled by cwc-sql automatically - Always use SqlFunctions, never construct queries directly in business logic
Guest User Handling:
- No JWT required - RequestHandler creates context with
role: 'guest-user' - Guest users have read-only access to public content
Soft Delete:
- All content tables support soft delete (
enabled=false) - Hard delete available for GDPR compliance
- List queries automatically filter
enabled=true
Error and Informational Messages:
Use actual field names in internal error messages, not "pretty" names. This avoids ambiguity about which field is being referenced (e.g., projectId vs projectPkId).
// ✅ GOOD - uses actual field name
reason: 'projectId is required for ownership check'
// ❌ AVOID - ambiguous, could be projectId or projectPkId
reason: 'Project ID required for ownership check'
This applies to reason, errorDetail, log messages, and any internal diagnostic messages. User-facing messages like errorMessage can use natural language ("Access denied", "Resource not found").
Explicit Value Checking:
When checking discriminated union values (like handlerType, requiredRole, etc.), explicitly check all expected values rather than using a default else case. Return an error for unknown values to fail fast on configuration errors.
// ✅ GOOD - explicitly checks all valid values
if (handlerType === 'query') {
// handle query
} else if (handlerType === 'mutation') {
// handle mutation
} else {
// return error for unknown handlerType
return { statusCode: 500, body: { errorDetail: `Unknown handlerType: ${handlerType}` } };
}
// ❌ AVOID - assumes unknown values are valid
if (handlerType === 'query') {
// handle query
} else {
// silently treats unknown values as mutation
}
Implementation Status
Active Development: This package is being implemented following cwc-api-implementation-checklist.md.
The checklist tracks 7 phases:
- Package Foundation (config, Express service, health check) - Complete
- Authentication & Context Layer (JWT, request context) - Complete
- Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - Complete
- Access Policy System (route and operation policies) - Complete
- SqlFunction Layer (CRUD operations for all tables)
- CwcApiV1 Routes & Business Logic
- Integration Testing & Final Review
Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
Unit Testing
Test Infrastructure:
- Jest 30.x with ts-jest for TypeScript ESM support
- Test files:
src/__tests__/**/*.test.ts - Setup:
src/__tests__/setup.tsloads unit test environment vialoadDotEnv - Run tests:
pnpm api testorNODE_ENV=unit pnpm --filter cwc-api test
Mock Infrastructure (src/__tests__/mocks/):
| Mock | Purpose |
|---|---|
| SqlClient Mocks | |
createMockSqlClient() |
Full SqlClient mock with jest.fn() for all methods |
mockSelectSuccess/NotFound |
Configure SqlClient for select queries |
mockListSuccess |
Configure SqlClient for list queries |
mockInsertSuccess/Failure |
Configure SqlClient for inserts |
mockUpdateSuccess/NotFound |
Configure SqlClient for updates |
mockDeleteSuccess/NotFound |
Configure SqlClient for deletes |
| AuthClient Mocks | |
createMockAuthClient() |
Full AuthClient mock (verifyToken, renewSession) |
createMockUserJwtPayload(overrides) |
Create UserJwtPayload with custom overrides |
mockVerifyTokenSuccess(client, payload?) |
Configure AuthClient to return success |
mockVerifyTokenFailure(client, error?) |
Configure AuthClient to return failure |
mockRenewSessionSuccess(client, jwt?) |
Configure renewSession to return new JWT |
mockRenewSessionFailure(client, error?) |
Configure renewSession to return failure |
| Route/Handler Mocks | |
createMockRouteConfig(overrides) |
Create route config for testing |
createMockOperationContext(overrides) |
Create operation context for testing |
createMockGuestContext() |
Create guest (unauthenticated) context |
createMockAuthenticatedContext(overrides) |
Create authenticated user context |
mockOperationSuccess(routeConfig, data) |
Configure operation to succeed |
mockOperationFailure(routeConfig, code, msg) |
Configure operation to fail |
mockOperationThrows(routeConfig, error) |
Configure operation to throw |
| Config Mocks | |
getUnitConfig() |
Load real config from unit.cwc-api.env |
createMockConfig(overrides) |
Create config with custom overrides |
createMockDevConfig(overrides) |
Mock config with isDev: true |
createMockProdConfig(overrides) |
Mock config with isProd: true |
createMockUnitConfig(overrides) |
Mock config with isUnit: true |
| Logger Mock | |
createMockLogger() |
ILogger mock with logDebug, logError, logInformation |
Testing Patterns:
RequestHandler tests (includes session renewal):
import {
createMockAuthClient,
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
mockRenewSessionSuccess,
getUnitConfig,
} from '../mocks';
describe('RequestHandler', () => {
const unitConfig = getUnitConfig();
const mockAuthClient = createMockAuthClient();
it('should renew session on success', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'query' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
// RequestHandler uses 4 args: options, config, authClient, logger
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
// ...
});
});
QueryHandler/MutationHandler tests (no authClient, no renewal):
import {
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
getUnitConfig,
} from '../mocks';
describe('QueryHandler', () => {
const unitConfig = getUnitConfig();
it('should execute operation', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
// QueryHandler uses 3 args: options, config, logger
const handler = new QueryHandler(options, unitConfig, undefined);
// ...
});
});
Related Packages
Depends On:
cwc-backend-utils(workspace) - SqlClient, Logger, Express service factorycwc-schema(workspace) - Data validationcwc-types(workspace) - Entity types, RuntimeEnvironment
Coordinates With:
cwc-auth- JWT validation, session renewalcwc-sql- Database operations
Consumed By:
cwc-website- Frontend web application
Version 5 (latest)
cwc-api Package
Main business logic API for CWC application. Acts as the central orchestrator that handles CRUD operations, enforces business rules, and coordinates data flow between frontend applications and the database layer.
Architecture
Database-Connected Microservice:
- Uses SqlClient from
cwc-backend-utilsto communicate with cwc-sql - Uses database-backed Logger for error logging
- Never connects directly to database - all data access flows through cwc-sql
- Coordinates with cwc-auth for session renewal
Layered Architecture:
Request → RequestHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
- RequestHandler: Auth → context creation → route access check → delegation → session renewal
- QueryHandler: Operation access check → execute query → format response
- MutationHandler: Operation access check → execute mutation → format response
- SqlFunctions: Atomic database operations (one function per table operation)
Request Pipeline
- Express routes receive incoming HTTP requests
- RequestHandler extracts JWT from Authorization header
- JWT validated → authenticated context; No JWT → guest context
- Route-level access policy checked
- QueryHandler or MutationHandler executes business logic
- SqlFunctions handle database operations through SqlClient
- RequestHandler renews session (if authenticated and not auth error)
- Response returned (with new JWT if session renewed)
Context Layer - Centralized Authentication
Location: src/context/
Architecture Decision: cwc-api does NOT duplicate JWT verification logic. All token validation goes through cwc-auth's /auth/v1/verify-token endpoint via AuthClient.
Trade-off: Additional HTTP call per request, but auth logic stays centralized in cwc-auth (single source of truth).
Files:
context.types.ts- AuthenticatedContext, GuestContext, RequestContext typescreateContext.ts- Creates context from Authorization headerindex.ts- Exports
Context Types:
// Authenticated user (valid JWT)
type AuthenticatedContext = {
isAuthenticated: true;
role: 'project-owner' | 'logged-on-user'; // Determined per-operation
userPkId: number;
username: string;
ownedProjects: string[];
payload: UserJwtPayload;
};
// Unauthenticated request (no/invalid JWT)
type GuestContext = {
isAuthenticated: false;
role: 'guest-user';
};
type RequestContext = AuthenticatedContext | GuestContext;
Usage:
import { createContext } from './context';
import { AuthClient } from 'cwc-backend-utils';
const context = await createContext({
authHeader: req.headers.authorization,
authClient: authClient,
});
if (context.isAuthenticated) {
// TypeScript narrows to AuthenticatedContext
console.log(context.userPkId, context.ownedProjects);
} else {
// TypeScript narrows to GuestContext
console.log(context.role); // 'guest-user'
}
Graceful Degradation:
- Missing token → guest context
- Invalid token → guest context
- Expired token → guest context
- Auth service error → guest context (logged)
- No errors thrown; auth failures result in guest access
Handler Architecture
Location: src/handlers/
Files:
handler.types.ts- Type definitions for handlers, operations, and responsesRequestHandler.ts- Entry point: route access check, delegation, session renewalQueryHandler.ts- Read operations with pagination supportMutationHandler.ts- Write operationsresponseUtils.ts- Shared response creation utilities (error responses, status code mapping)index.ts- Exports
Handler Flow:
RequestHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied (NO renewal)
├── buildOperationContext() → projectId, resourceId from path
├── delegate to QueryHandler or MutationHandler
│ ├── checkOperationAccess() → 403 if denied
│ ├── execute operation()
│ └── format response (no JWT)
├── if authenticated AND not auth error (401/403):
│ └── renewSession() → merge JWT into response
└── return final response
Session Renewal Strategy:
- Centralized in RequestHandler (not Query/MutationHandler)
- Renews on ALL requests except auth errors (401, 403)
- Even failed operations (404, 500) trigger renewal to keep session active
- New JWT merged into response body (
response.body.jwt) for success responses - Renewal failure doesn't fail the request (graceful degradation, logged)
- Guest users never trigger renewal
Error Code Mapping:
| Error Code | HTTP Status |
|---|---|
UNAUTHORIZED |
401 |
FORBIDDEN |
403 |
NOT_FOUND |
404 |
ALREADY_EXISTS |
400 |
VALIDATION_ERROR |
400 |
INVALID_PARAMETER |
400 |
DATABASE_ERROR |
500 |
INTERNAL_ERROR |
500 |
Access Policy System
Location: src/policies/
Files:
policy.types.ts- Re-exports RouteAccessResult, OperationAccessResultcheckRouteAccess.ts- Route-level authentication checkcheckOperationAccess.ts- Operation-level access with ownership checkindex.ts- Exports
Role Hierarchy:
guest-user < logged-on-user < project-owner
| Role | Description |
|---|---|
guest-user |
Unauthenticated request (public read-only access) |
logged-on-user |
Authenticated user (may not own the project) |
project-owner |
User owns the project being accessed |
Two-Level Access Control:
Routes specify requiredRole: CwcRole which is checked at two levels:
| requiredRole | Route-Level Check | Operation-Level Check |
|---|---|---|
guest-user |
Anyone allowed | Anyone allowed |
logged-on-user |
Must be authenticated | Must be authenticated |
project-owner |
Must be authenticated | Must be authenticated AND own the project |
Key Design Decision: Route-level for project-owner only checks authentication (no DB lookup). The actual ownership check happens at operation-level using context.ownedProjects from the JWT.
Policy Functions:
// Route-level: Only checks authentication, no ownership
checkRouteAccess(context: RequestContext, requiredRole: CwcRole): RouteAccessResult
// Operation-level: Checks authentication AND ownership for project-owner
checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult
// Helper: Checks if user owns a specific project
isProjectOwner(context: RequestContext, projectId: string | undefined): boolean
Security:
- Ownership verified from JWT claims (
context.ownedProjects.includes(projectId)) projectIdcomes from URL path params (viaoperationContext), not request body- No database lookups for access checks - all data comes from JWT
Tables Handled
Content Tables (Full CRUD + Soft Delete):
- project
- codingSession
- codingSessionContent
- codingSessionAttachment
- comment
- reaction
- contentReport
- abuseReport
Read-Only Tables:
- featureFlag (all fields)
- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
SqlFunction Naming Convention
Each table has a subdirectory in src/sql/ with these functions:
| Function | Purpose |
|---|---|
select{Table}.ts |
Retrieve single record by identifier |
list{Table}s.ts |
Retrieve multiple records with filtering |
insert{Table}.ts |
Create new record |
update{Table}.ts |
Modify existing record |
delete{Table}.ts |
Hard delete (GDPR compliance) |
softDelete{Table}.ts |
Soft delete (set enabled=false) |
Key Principles:
- Single responsibility: Each SqlFunction does one thing
- No business logic: SqlFunctions handle data access only
- Soft-delete awareness: List queries automatically filter
enabled=true - Composability: Queries/Mutations compose multiple SqlFunctions
cwc-auth Integration
Division of Responsibilities:
| Concern | Owner |
|---|---|
| "Is this token valid?" | cwc-auth |
| "Who is this user?" | cwc-auth (via token claims) |
| "Can this user access this endpoint?" | cwc-api |
| "Is this user the owner of this project?" | cwc-api |
Session Renewal Flow:
When cwc-api creates/deletes a project:
- cwc-api performs the database operation
- cwc-api calls cwc-auth
/auth/v1/renew-session - cwc-auth issues new JWT with updated
ownedProjects - cwc-api returns new JWT to client
Response Pattern
Standard Response Envelope:
// Success
{
success: true,
data: { ... },
// Optional: pagination metadata
}
// Error
{
success: false,
errorCode: 'ERROR_CODE',
errorMessage: 'Human-readable message',
// Dev-only: errorDetail
}
Pagination (Offset-based):
{
success: true,
data: [...],
pagination: {
page: 1,
pageSize: 20,
totalCount: 100,
hasMore: true
}
}
Configuration
Required Environment Variables:
RUNTIME_ENVIRONMENT=dev
SERVICE_PORT=5040
DATA_URI=http://localhost:5020/data/v1
AUTH_URI=http://localhost:5005/auth/v1
APP_URL=http://localhost:3000
CORS_ORIGIN=*
DEV_CORS_ORIGIN=http://localhost:3000
RATE_LIMITER_POINTS=15
RATE_LIMITER_DURATION=1
DEBUG_MODE=OFF
LOG_ERRORS_TO_DATABASE=ON
USER_JWT_SECRET=...
SQL_CLIENT_API_KEY=...
API Versioning
URL-based versioning:
/api/v1/*routes to CwcApiV1 handlers- Each version lives in
src/apis/CwcApiV{N}/ - Route definitions in
routes.ts, organized alphabetically
Critical Rules
SqlClient Usage:
- Never provide
createdDateormodifiedDate- handled by cwc-sql automatically - Always use SqlFunctions, never construct queries directly in business logic
Guest User Handling:
- No JWT required - RequestHandler creates context with
role: 'guest-user' - Guest users have read-only access to public content
Soft Delete:
- All content tables support soft delete (
enabled=false) - Hard delete available for GDPR compliance
- List queries automatically filter
enabled=true
Error and Informational Messages:
Use actual field names in internal error messages, not "pretty" names. This avoids ambiguity about which field is being referenced (e.g., projectId vs projectPkId).
// ✅ GOOD - uses actual field name
reason: 'projectId is required for ownership check'
// ❌ AVOID - ambiguous, could be projectId or projectPkId
reason: 'Project ID required for ownership check'
This applies to reason, errorDetail, log messages, and any internal diagnostic messages. User-facing messages like errorMessage can use natural language ("Access denied", "Resource not found").
Explicit Value Checking:
When checking discriminated union values (like handlerType, requiredRole, etc.), explicitly check all expected values rather than using a default else case. Return an error for unknown values.
This pattern serves two purposes:
- Safety - Fails fast on unexpected values rather than silently misbehaving
- Readability - The code is self-documenting; when reading
if (requiredRole === 'project-owner'), the intent is immediately clear. With anelseblock, readers must mentally reconstruct "this must be project-owner because we already checked the other values above."
Code that is easy to read and understand on the first pass is never an accident - it requires diligence. Developer experience is a top priority; always write code with the next reader in mind.
// ✅ GOOD - explicitly checks all valid values, self-documenting
if (handlerType === 'query') {
// handle query
} else if (handlerType === 'mutation') {
// handle mutation
} else {
// return error for unknown handlerType
return { statusCode: 500, body: { errorDetail: `Unknown handlerType: ${handlerType}` } };
}
// ❌ AVOID - requires mental reconstruction to understand what case this handles
if (handlerType === 'query') {
// handle query
} else {
// reader must infer this is mutation
}
Implementation Status
Active Development: This package is being implemented following cwc-api-implementation-checklist.md.
The checklist tracks 7 phases:
- Package Foundation (config, Express service, health check) - Complete
- Authentication & Context Layer (JWT, request context) - Complete
- Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - Complete
- Access Policy System (route and operation policies) - Complete
- SqlFunction Layer (CRUD operations for all tables)
- CwcApiV1 Routes & Business Logic
- Integration Testing & Final Review
Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
Unit Testing
Test Infrastructure:
- Jest 30.x with ts-jest for TypeScript ESM support
- Test files:
src/__tests__/**/*.test.ts - Setup:
src/__tests__/setup.tsloads unit test environment vialoadDotEnv - Run tests:
pnpm api testorNODE_ENV=unit pnpm --filter cwc-api test
Mock Infrastructure (src/__tests__/mocks/):
| Mock | Purpose |
|---|---|
| SqlClient Mocks | |
createMockSqlClient() |
Full SqlClient mock with jest.fn() for all methods |
mockSelectSuccess/NotFound |
Configure SqlClient for select queries |
mockListSuccess |
Configure SqlClient for list queries |
mockInsertSuccess/Failure |
Configure SqlClient for inserts |
mockUpdateSuccess/NotFound |
Configure SqlClient for updates |
mockDeleteSuccess/NotFound |
Configure SqlClient for deletes |
| AuthClient Mocks | |
createMockAuthClient() |
Full AuthClient mock (verifyToken, renewSession) |
createMockUserJwtPayload(overrides) |
Create UserJwtPayload with custom overrides |
mockVerifyTokenSuccess(client, payload?) |
Configure AuthClient to return success |
mockVerifyTokenFailure(client, error?) |
Configure AuthClient to return failure |
mockRenewSessionSuccess(client, jwt?) |
Configure renewSession to return new JWT |
mockRenewSessionFailure(client, error?) |
Configure renewSession to return failure |
| Route/Handler Mocks | |
createMockRouteConfig(overrides) |
Create route config for testing |
createMockOperationContext(overrides) |
Create operation context for testing |
createMockGuestContext() |
Create guest (unauthenticated) context |
createMockAuthenticatedContext(overrides) |
Create authenticated user context |
mockOperationSuccess(routeConfig, data) |
Configure operation to succeed |
mockOperationFailure(routeConfig, code, msg) |
Configure operation to fail |
mockOperationThrows(routeConfig, error) |
Configure operation to throw |
| Config Mocks | |
getUnitConfig() |
Load real config from unit.cwc-api.env |
createMockConfig(overrides) |
Create config with custom overrides |
createMockDevConfig(overrides) |
Mock config with isDev: true |
createMockProdConfig(overrides) |
Mock config with isProd: true |
createMockUnitConfig(overrides) |
Mock config with isUnit: true |
| Logger Mock | |
createMockLogger() |
ILogger mock with logDebug, logError, logInformation |
Testing Patterns:
RequestHandler tests (includes session renewal):
import {
createMockAuthClient,
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
mockRenewSessionSuccess,
getUnitConfig,
} from '../mocks';
describe('RequestHandler', () => {
const unitConfig = getUnitConfig();
const mockAuthClient = createMockAuthClient();
it('should renew session on success', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'query' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
// RequestHandler uses 4 args: options, config, authClient, logger
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
// ...
});
});
QueryHandler/MutationHandler tests (no authClient, no renewal):
import {
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
getUnitConfig,
} from '../mocks';
describe('QueryHandler', () => {
const unitConfig = getUnitConfig();
it('should execute operation', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
// QueryHandler uses 3 args: options, config, logger
const handler = new QueryHandler(options, unitConfig, undefined);
// ...
});
});
Related Packages
Depends On:
cwc-backend-utils(workspace) - SqlClient, Logger, Express service factorycwc-schema(workspace) - Data validationcwc-types(workspace) - Entity types, RuntimeEnvironment
Coordinates With:
cwc-auth- JWT validation, session renewalcwc-sql- Database operations
Consumed By:
cwc-website- Frontend web application
packages/cwc-api/cwc-api-implementation-checklist.md2 versions
Version 1
cwc-api Implementation Checklist
This file tracks progress on the cwc-api implementation across coding sessions.
Update checkbox status as tasks are completed: [ ] → [x]
Phase 1: Package Foundation
Package Structure
- Create
package.jsonwith dependencies - Create
tsconfig.jsonextending base config - Add package shortcut script to root
package.json
Configuration
- Create
src/config/config.types.ts(CwcApiConfig, CwcApiConfigSecrets) - Create
src/config/loadConfig.ts - Create
src/config/index.ts - Generate .env files using cwc-configuration-helper (verified auto-discovery works)
Express Service
- Create
src/index.ts(Express service entry point) - Add health check endpoint (
/health/v1) - Verify service starts and responds to health check
Unit Tests
- Set up test infrastructure (jest.config.js, setup.ts, mocks/)
- Create
src/__tests__/config.test.ts- Test config loading - Create
src/__tests__/healthCheck.test.ts- Test health endpoint
Security Review
- Verify secrets are not logged (config.secrets always REDACTED)
- Verify CORS configuration is appropriate (via createExpressService)
- Verify rate limiting is configured (via createExpressService)
Documentation Updates
- Update
CLAUDE.mdwith package architecture - Update
README.mdwith setup instructions
Phase 2: Authentication & Context Layer
Architecture Decision: cwc-api does NOT duplicate JWT verification logic. Instead, cwc-api
calls cwc-auth's /auth/v1/verify-token endpoint to validate tokens. This keeps auth logic
centralized in cwc-auth (single source of truth) at the cost of an additional HTTP call per request.
Architecture Refinement (Session 020): Shared types moved to cwc-types, AuthClient moved to cwc-backend-utils for reuse by other services.
Prerequisites (cwc-auth work)
- Add
/auth/v1/verify-tokenendpoint to cwc-auth ServiceApiV1- Input: JWT token (from Authorization header)
- Output:
{ success: true, payload: UserJwtPayload }or{ success: false, error: ... }
Shared Auth Types (cwc-types)
- Create
cwc-types/src/authTypes.ts- Define
UserJwtPayloadtype (moved from cwc-auth for sharing) - Define
CwcRoletype:'project-owner' | 'logged-on-user' | 'guest-user' - Define
VerifyTokenResponsetypes (success/error) - Define
VerifyTokenResulttype
- Define
- Update cwc-auth to import
UserJwtPayloadfrom cwc-types
AuthClient (cwc-backend-utils)
- Create
cwc-backend-utils/src/AuthClient/AuthClient.ts- HTTP POST to cwc-auth
/auth/v1/verify-token - Handles success and error responses
- Returns typed
VerifyTokenResult
- HTTP POST to cwc-auth
- Export from
cwc-backend-utils/src/index.ts
Request Context (cwc-api)
- Create
src/context/context.types.ts- Define
AuthenticatedContextandGuestContexttypes - Define
RequestContextunion type
- Define
- Create
src/context/createContext.ts- Uses AuthClient.verifyToken()
- Returns authenticated context on success
- Returns guest context on failure (no JWT, invalid, expired)
- Create
src/context/index.ts
Unit Tests
- Create
src/__tests__/mocks/authClient.mock.ts- AuthClient mock helpers - Create
src/__tests__/context/createContext.test.ts- Test authenticated and guest contexts (10 tests)
Security Review
- Verify cwc-auth endpoint validates JWT signature (HS256, proper secret)
- Verify expired tokens are rejected by cwc-auth
- Verify invalid tokens create guest context (no error details leaked to client)
- Verify guest context has no elevated privileges
- Verify userPkId comes from cwc-auth response, not request body
Documentation Updates
- Update
CLAUDE.mdwith auth integration patterns - Update
README.mdwith authentication details
Phase 3: Handler Architecture
Architecture Refinement (Session 021): Session renewal happens on EVERY request (queries and mutations) to keep sessions active, not just on specific triggers. This is the RPC-style approach where all endpoints use POST.
Prerequisites (cwc-types, cwc-backend-utils)
- Add
RenewSessionResulttypes to cwc-types/authTypes.ts - Add
renewSession()method to AuthClient in cwc-backend-utils - Update AuthClient mocks in cwc-api with renewSession helpers
Route Handler
- Create
src/handlers/handler.types.ts- Define error codes (
CwcApiErrorCode) - Define response types (
CwcApiSuccessResponse,CwcApiErrorResponse,CwcApiHandlerResponse) - Define route config types (
CwcApiRouteConfig,CwcApiHandlerType) - Define operation types (
OperationContext,OperationResult,CwcApiOperation) - Define handler options types (
RouteHandlerOptions,QueryHandlerOptions,MutationHandlerOptions) - Define access policy stubs for Phase 4 (
RouteAccessResult,OperationAccessResult)
- Define error codes (
- Create
src/handlers/RouteHandler.ts- Check route-level access policy (role vs allowedRoles)
- Build operation context from path params
- Delegate to QueryHandler or MutationHandler based on handlerType
- Return 401 for unauthenticated access denied
- Return 403 for authenticated but unauthorized
- Create
src/handlers/QueryHandler.ts- Operation-level access check (stub for Phase 4)
- Execute query operation
- Renew session (always, to keep session active)
- Format response with data, pagination, and new JWT
- Create
src/handlers/MutationHandler.ts- Operation-level access check (stub for Phase 4)
- Execute mutation operation
- Renew session (always, to keep session active)
- Format response with data and new JWT
- Create
src/handlers/index.ts
Unit Tests
- Create
src/__tests__/handlers/RouteHandler.test.ts(14 tests) - Create
src/__tests__/handlers/QueryHandler.test.ts(15 tests) - Create
src/__tests__/handlers/MutationHandler.test.ts(15 tests) - Create
src/__tests__/mocks/routeConfig.mock.ts-
createMockRouteConfig(),createMockOperationContext() -
createMockGuestContext(),createMockAuthenticatedContext() -
mockOperationSuccess(),mockOperationFailure(),mockOperationThrows()
-
Security Review
- Verify error responses don't leak internal details (errorDetail only in dev mode)
- Verify operation access check is stubbed (always allowed for Phase 3)
- Verify session renewal failure doesn't fail the request (graceful degradation)
- Verify guest users don't get session renewal
Documentation Updates
- Update
CLAUDE.mdwith handler architecture patterns - Update
README.mdwith request pipeline details
Phase 4: Access Policy System
Policy Types
- Create
src/policies/policy.types.ts- Re-export
RouteAccessResult,OperationAccessResultfrom handler.types.ts
- Re-export
Route Policies
- Create
src/policies/checkRouteAccess.ts- Check authentication only (no ownership check)
- Uses role hierarchy: guest-user < logged-on-user < project-owner
Operation Policies
- Create
src/policies/checkOperationAccess.ts- Check authentication AND ownership for project-owner routes
-
isProjectOwner()helper usescontext.ownedProjects.includes(projectId) -
effectiveRoledetermines the user's role for this operation
Policy Enforcement
- Update
src/handlers/handler.types.ts- ReplaceallowedRoles[]withrequiredRole: CwcRole - Update
src/handlers/RequestHandler.ts- Use importedcheckRouteAccess - Update
src/handlers/QueryHandler.ts- Use importedcheckOperationAccess - Update
src/handlers/MutationHandler.ts- Use importedcheckOperationAccess - Create
src/policies/index.ts
Unit Tests
- Create
src/__tests__/policies/checkRouteAccess.test.ts - Create
src/__tests__/policies/checkOperationAccess.test.ts - Update
src/__tests__/mocks/routeConfig.mock.ts- UserequiredRole - Update existing handler tests to use
requiredRole
Security Review
- Verify no privilege escalation paths (guest → user, user → owner)
- Verify ownership checks use
context.ownedProjectsfrom JWT (not request body) - Verify
projectIdcomes from path params (operationContext), not payload - Verify route-level only checks authentication (no DB lookup for ownership)
- Verify generic error messages in production (
errorDetailonly in dev)
Documentation Updates
- Update
CLAUDE.mdwith access policy patterns - Update
README.mdwith role-based access details
Phase 5: SqlFunction Layer
Infrastructure
- Create
src/sql/sql.types.ts- Define SqlFunction input/output types
- Define pagination types (offset-based)
- Create
src/sql/index.ts
Content Tables (Full CRUD + Soft Delete)
project/
- Create
src/sql/project/selectProject.ts - Create
src/sql/project/listProjects.ts - Create
src/sql/project/insertProject.ts - Create
src/sql/project/updateProject.ts - Create
src/sql/project/deleteProject.ts(hard delete) - Create
src/sql/project/softDeleteProject.ts - Create
src/sql/project/index.ts
codingSession/
- Create
src/sql/codingSession/selectCodingSession.ts - Create
src/sql/codingSession/listCodingSessions.ts - Create
src/sql/codingSession/insertCodingSession.ts - Create
src/sql/codingSession/updateCodingSession.ts - Create
src/sql/codingSession/deleteCodingSession.ts - Create
src/sql/codingSession/softDeleteCodingSession.ts - Create
src/sql/codingSession/index.ts
codingSessionContent/
- Create
src/sql/codingSessionContent/selectCodingSessionContent.ts - Create
src/sql/codingSessionContent/listCodingSessionContents.ts - Create
src/sql/codingSessionContent/insertCodingSessionContent.ts - Create
src/sql/codingSessionContent/updateCodingSessionContent.ts - Create
src/sql/codingSessionContent/deleteCodingSessionContent.ts - Create
src/sql/codingSessionContent/softDeleteCodingSessionContent.ts - Create
src/sql/codingSessionContent/index.ts
codingSessionAttachment/
- Create
src/sql/codingSessionAttachment/selectCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/listCodingSessionAttachments.ts - Create
src/sql/codingSessionAttachment/insertCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/updateCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/deleteCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/softDeleteCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/index.ts
comment/
- Create
src/sql/comment/selectComment.ts - Create
src/sql/comment/listComments.ts - Create
src/sql/comment/insertComment.ts - Create
src/sql/comment/updateComment.ts - Create
src/sql/comment/deleteComment.ts - Create
src/sql/comment/softDeleteComment.ts - Create
src/sql/comment/index.ts
reaction/
- Create
src/sql/reaction/selectReaction.ts - Create
src/sql/reaction/listReactions.ts - Create
src/sql/reaction/insertReaction.ts - Create
src/sql/reaction/updateReaction.ts - Create
src/sql/reaction/deleteReaction.ts - Create
src/sql/reaction/softDeleteReaction.ts - Create
src/sql/reaction/index.ts
contentReport/
- Create
src/sql/contentReport/selectContentReport.ts - Create
src/sql/contentReport/listContentReports.ts - Create
src/sql/contentReport/insertContentReport.ts - Create
src/sql/contentReport/updateContentReport.ts - Create
src/sql/contentReport/deleteContentReport.ts - Create
src/sql/contentReport/softDeleteContentReport.ts - Create
src/sql/contentReport/index.ts
abuseReport/
- Create
src/sql/abuseReport/selectAbuseReport.ts - Create
src/sql/abuseReport/listAbuseReports.ts - Create
src/sql/abuseReport/insertAbuseReport.ts - Create
src/sql/abuseReport/updateAbuseReport.ts - Create
src/sql/abuseReport/deleteAbuseReport.ts - Create
src/sql/abuseReport/softDeleteAbuseReport.ts - Create
src/sql/abuseReport/index.ts
Read-Only Tables
featureFlag/
- Create
src/sql/featureFlag/selectFeatureFlag.ts - Create
src/sql/featureFlag/listFeatureFlags.ts - Create
src/sql/featureFlag/index.ts
user/ (Limited Fields)
- Create
src/sql/user/selectUser.ts(userPkId, enabled, username, loginType, loginDate, otpValidated) - Create
src/sql/user/listUsers.ts(same limited fields) - Create
src/sql/user/index.ts
Unit Tests
- Create
src/__tests__/sql/project.test.ts- Test all project SqlFunctions - Create
src/__tests__/sql/codingSession.test.ts- Test all codingSession SqlFunctions - Create
src/__tests__/sql/codingSessionContent.test.ts - Create
src/__tests__/sql/codingSessionAttachment.test.ts - Create
src/__tests__/sql/comment.test.ts - Create
src/__tests__/sql/reaction.test.ts - Create
src/__tests__/sql/contentReport.test.ts - Create
src/__tests__/sql/abuseReport.test.ts - Create
src/__tests__/sql/featureFlag.test.ts - Create
src/__tests__/sql/user.test.ts
Security Review
- Verify all SqlFunctions use SqlClient (no raw SQL construction)
- Verify list queries filter
enabled=trueby default (soft-delete) - Verify user SqlFunctions only expose limited fields
- Verify no mass assignment vulnerabilities (explicit field mapping)
- Verify pagination limits are enforced (prevent data dumping)
Documentation Updates
- Update
CLAUDE.mdwith SqlFunction patterns - Update
README.mdwith data access layer details
Phase 6: CwcApiV1 Routes & Business Logic
Route Structure
- Create
src/apis/CwcApiV1/routes.ts - Create
src/apis/CwcApiV1/types.ts - Create
src/apis/CwcApiV1/index.ts
Project Operations
- Create
src/apis/CwcApiV1/queries/getProject.ts - Create
src/apis/CwcApiV1/queries/listProjects.ts - Create
src/apis/CwcApiV1/mutations/projectCreate.ts - Create
src/apis/CwcApiV1/mutations/projectUpdate.ts - Create
src/apis/CwcApiV1/mutations/projectDelete.ts
Coding Session Operations
- Create
src/apis/CwcApiV1/queries/getCodingSession.ts - Create
src/apis/CwcApiV1/queries/listCodingSessions.ts - Create
src/apis/CwcApiV1/mutations/codingSessionCreate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionUpdate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionDelete.ts
Coding Session Content Operations
- Create
src/apis/CwcApiV1/queries/getCodingSessionContent.ts - Create
src/apis/CwcApiV1/queries/listCodingSessionContents.ts - Create
src/apis/CwcApiV1/mutations/codingSessionContentCreate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionContentUpdate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionContentDelete.ts
Coding Session Attachment Operations
- Create
src/apis/CwcApiV1/queries/getCodingSessionAttachment.ts - Create
src/apis/CwcApiV1/queries/listCodingSessionAttachments.ts - Create
src/apis/CwcApiV1/mutations/codingSessionAttachmentCreate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionAttachmentUpdate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionAttachmentDelete.ts
Comment Operations
- Create
src/apis/CwcApiV1/queries/getComment.ts - Create
src/apis/CwcApiV1/queries/listComments.ts - Create
src/apis/CwcApiV1/mutations/commentCreate.ts - Create
src/apis/CwcApiV1/mutations/commentUpdate.ts - Create
src/apis/CwcApiV1/mutations/commentDelete.ts
Reaction Operations
- Create
src/apis/CwcApiV1/queries/getReaction.ts - Create
src/apis/CwcApiV1/queries/listReactions.ts - Create
src/apis/CwcApiV1/mutations/reactionCreate.ts - Create
src/apis/CwcApiV1/mutations/reactionDelete.ts
Content Report Operations
- Create
src/apis/CwcApiV1/queries/getContentReport.ts - Create
src/apis/CwcApiV1/queries/listContentReports.ts - Create
src/apis/CwcApiV1/mutations/contentReportCreate.ts - Create
src/apis/CwcApiV1/mutations/contentReportUpdate.ts
Abuse Report Operations
- Create
src/apis/CwcApiV1/queries/getAbuseReport.ts - Create
src/apis/CwcApiV1/queries/listAbuseReports.ts - Create
src/apis/CwcApiV1/mutations/abuseReportCreate.ts - Create
src/apis/CwcApiV1/mutations/abuseReportUpdate.ts
Route Registration
- Register all routes in
routes.ts - Wire up routes to Express in
src/index.ts
Unit Tests
- Create
src/__tests__/apis/CwcApiV1/queries/getProject.test.ts - Create
src/__tests__/apis/CwcApiV1/queries/listProjects.test.ts - Create
src/__tests__/apis/CwcApiV1/mutations/projectCreate.test.ts - Create
src/__tests__/apis/CwcApiV1/mutations/projectUpdate.test.ts - Create
src/__tests__/apis/CwcApiV1/mutations/projectDelete.test.ts - Create tests for remaining query/mutation endpoints (follow same pattern)
Security Review
- Verify IDOR prevention (users can only access their own resources)
- Verify business logic doesn't allow unauthorized state transitions
- Verify content report/abuse report workflows don't expose reporter identity
- Verify rate limiting is appropriate per operation type
- Verify all user input is validated via cwc-schema before processing
Documentation Updates
- Update
CLAUDE.mdwith API route patterns - Update
README.mdwith complete endpoint documentation
Phase 7: Integration Testing & Final Review
Integration Tests
- Create
src/__tests__/integration/directory - Write end-to-end tests for complete request flows
- Write tests for cross-component interactions
- Write tests for error scenarios across layers
Security Review (Final)
- Verify security-focused test coverage (auth bypass, privilege escalation, IDOR)
- Verify error handling tests don't leak sensitive information
- Conduct final end-to-end security review of all phases
- Document any security considerations for deployment
Documentation Updates
- Update
CLAUDE.mdwith testing patterns and final review - Update
README.mdwith testing instructions and final review
Session Notes
Session 1
- Initial checklist created
- Specification reviewed and clarified:
- Guest access: Create context with 'guest-user' role for unauthenticated requests
- Session renewal: cwc-api calls cwc-auth /renew-session
- Pagination: Offset-based (page/pageSize)
- Soft delete: All content tables
- RouteHandler class chosen over middleware approach
- SqlFunction naming:
listinstead ofselectMultiple - Added read-only SqlFunctions for featureFlag and user (limited fields)
Session 2 (020)
Phase 1 COMPLETED
- Added
devscript to package.json usingtsx(better ESM support than ts-node) - Fixed cwc-backend-utils ESM issues:
loadDotEnv.ts: Added ESM-compatible__dirnameusingimport.meta.urlexpress/service.ts: Fixed wildcard route syntax for path-to-regexp 8.x (/*→/{*splat})
- Service starts successfully and health check responds (POST /health/v1)
- Added
Architecture Decision: Centralized Authentication
- cwc-api will NOT duplicate JWT verification logic
- cwc-api will call cwc-auth's
/auth/v1/verify-tokenendpoint - Trade-off: Additional HTTP call per request, but auth logic stays centralized
- BLOCKER: Phase 2 cannot proceed until cwc-auth has
/verify-tokenendpoint
Phase 2 Revised
- Updated checklist to reflect API-call approach
- Removed local JWT utilities (verifyUserJwt, extractJwtFromHeader)
- Added prerequisite: cwc-auth must implement /verify-token first
Phase 2 COMPLETED
- Implemented shared auth types in cwc-types (
src/authTypes.ts):UserJwtPayload,CwcRole,VerifyTokenResponse,VerifyTokenResult
- Updated cwc-auth to import
UserJwtPayloadfrom cwc-types (backwards-compatible re-export) - Created
AuthClientin cwc-backend-utils (src/AuthClient/):- HTTP client for cwc-auth, similar pattern to SqlClient
verifyToken(authHeader)returns typedVerifyTokenResult- Handles 401 responses, network errors, timeouts
- Created context layer in cwc-api (
src/context/):context.types.ts:AuthenticatedContext,GuestContext,RequestContextcreateContext.ts: Uses AuthClient, returns authenticated or guest context
- Created test mocks (
src/__tests__/mocks/authClient.mock.ts):createMockAuthClient(),createMockUserJwtPayload()mockVerifyTokenSuccess(),mockVerifyTokenFailure()
- All 10 context tests pass, all 38 cwc-api tests pass
- All affected packages typecheck: cwc-types, cwc-auth, cwc-backend-utils, cwc-api
- Note: AuthClient unit tests deferred (cwc-backend-utils has no test infrastructure)
- Functionality verified through cwc-api context tests
- Implemented shared auth types in cwc-types (
Session 3 (021)
Phase 3 COMPLETED
- Added
RenewSessionResulttypes to cwc-types/authTypes.ts - Added
renewSession()method to AuthClient in cwc-backend-utils - Created handler architecture:
handler.types.ts: Error codes, response types, route config, operation types, access policy stubsRouteHandler.ts: Route access check, delegates to Query/MutationHandlerQueryHandler.ts: Execute query, renew session, format responseMutationHandler.ts: Execute mutation, renew session, format response
- Created comprehensive test mocks (
routeConfig.mock.ts):createMockRouteConfig(),createMockOperationContext()createMockGuestContext(),createMockAuthenticatedContext()mockOperationSuccess(),mockOperationFailure(),mockOperationThrows()
- Updated config mocks with consistent naming:
getUnitConfig(),createDevConfig(),createProdConfig() - All 44 handler tests pass (14 RouteHandler + 15 QueryHandler + 15 MutationHandler)
- Added
Key Design Decisions:
- Session renewal on EVERY request (not just triggers) to keep sessions active
- RPC-style API: All endpoints use POST, no GET/PUT/DELETE
- Graceful degradation: Session renewal failure doesn't fail the request
- Operation access check stubbed for Phase 4 (always allowed for now)
Version 2 (latest)
cwc-api Implementation Checklist
This file tracks progress on the cwc-api implementation across coding sessions.
Update checkbox status as tasks are completed: [ ] → [x]
Phase 1: Package Foundation
Package Structure
- Create
package.jsonwith dependencies - Create
tsconfig.jsonextending base config - Add package shortcut script to root
package.json
Configuration
- Create
src/config/config.types.ts(CwcApiConfig, CwcApiConfigSecrets) - Create
src/config/loadConfig.ts - Create
src/config/index.ts - Generate .env files using cwc-configuration-helper (verified auto-discovery works)
Express Service
- Create
src/index.ts(Express service entry point) - Add health check endpoint (
/health/v1) - Verify service starts and responds to health check
Unit Tests
- Set up test infrastructure (jest.config.js, setup.ts, mocks/)
- Create
src/__tests__/config.test.ts- Test config loading - Create
src/__tests__/healthCheck.test.ts- Test health endpoint
Security Review
- Verify secrets are not logged (config.secrets always REDACTED)
- Verify CORS configuration is appropriate (via createExpressService)
- Verify rate limiting is configured (via createExpressService)
Documentation Updates
- Update
CLAUDE.mdwith package architecture - Update
README.mdwith setup instructions
Phase 2: Authentication & Context Layer
Architecture Decision: cwc-api does NOT duplicate JWT verification logic. Instead, cwc-api
calls cwc-auth's /auth/v1/verify-token endpoint to validate tokens. This keeps auth logic
centralized in cwc-auth (single source of truth) at the cost of an additional HTTP call per request.
Architecture Refinement (Session 020): Shared types moved to cwc-types, AuthClient moved to cwc-backend-utils for reuse by other services.
Prerequisites (cwc-auth work)
- Add
/auth/v1/verify-tokenendpoint to cwc-auth ServiceApiV1- Input: JWT token (from Authorization header)
- Output:
{ success: true, payload: UserJwtPayload }or{ success: false, error: ... }
Shared Auth Types (cwc-types)
- Create
cwc-types/src/authTypes.ts- Define
UserJwtPayloadtype (moved from cwc-auth for sharing) - Define
CwcRoletype:'project-owner' | 'logged-on-user' | 'guest-user' - Define
VerifyTokenResponsetypes (success/error) - Define
VerifyTokenResulttype
- Define
- Update cwc-auth to import
UserJwtPayloadfrom cwc-types
AuthClient (cwc-backend-utils)
- Create
cwc-backend-utils/src/AuthClient/AuthClient.ts- HTTP POST to cwc-auth
/auth/v1/verify-token - Handles success and error responses
- Returns typed
VerifyTokenResult
- HTTP POST to cwc-auth
- Export from
cwc-backend-utils/src/index.ts
Request Context (cwc-api)
- Create
src/context/context.types.ts- Define
AuthenticatedContextandGuestContexttypes - Define
RequestContextunion type
- Define
- Create
src/context/createContext.ts- Uses AuthClient.verifyToken()
- Returns authenticated context on success
- Returns guest context on failure (no JWT, invalid, expired)
- Create
src/context/index.ts
Unit Tests
- Create
src/__tests__/mocks/authClient.mock.ts- AuthClient mock helpers - Create
src/__tests__/context/createContext.test.ts- Test authenticated and guest contexts (10 tests)
Security Review
- Verify cwc-auth endpoint validates JWT signature (HS256, proper secret)
- Verify expired tokens are rejected by cwc-auth
- Verify invalid tokens create guest context (no error details leaked to client)
- Verify guest context has no elevated privileges
- Verify userPkId comes from cwc-auth response, not request body
Documentation Updates
- Update
CLAUDE.mdwith auth integration patterns - Update
README.mdwith authentication details
Phase 3: Handler Architecture
Architecture Refinement (Session 021): Session renewal happens on EVERY request (queries and mutations) to keep sessions active, not just on specific triggers. This is the RPC-style approach where all endpoints use POST.
Prerequisites (cwc-types, cwc-backend-utils)
- Add
RenewSessionResulttypes to cwc-types/authTypes.ts - Add
renewSession()method to AuthClient in cwc-backend-utils - Update AuthClient mocks in cwc-api with renewSession helpers
Route Handler
- Create
src/handlers/handler.types.ts- Define error codes (
CwcApiErrorCode) - Define response types (
CwcApiSuccessResponse,CwcApiErrorResponse,CwcApiHandlerResponse) - Define route config types (
CwcApiRouteConfig,CwcApiHandlerType) - Define operation types (
OperationContext,OperationResult,CwcApiOperation) - Define handler options types (
RouteHandlerOptions,QueryHandlerOptions,MutationHandlerOptions) - Define access policy stubs for Phase 4 (
RouteAccessResult,OperationAccessResult)
- Define error codes (
- Create
src/handlers/RouteHandler.ts- Check route-level access policy (role vs allowedRoles)
- Build operation context from path params
- Delegate to QueryHandler or MutationHandler based on handlerType
- Return 401 for unauthenticated access denied
- Return 403 for authenticated but unauthorized
- Create
src/handlers/QueryHandler.ts- Operation-level access check (stub for Phase 4)
- Execute query operation
- Renew session (always, to keep session active)
- Format response with data, pagination, and new JWT
- Create
src/handlers/MutationHandler.ts- Operation-level access check (stub for Phase 4)
- Execute mutation operation
- Renew session (always, to keep session active)
- Format response with data and new JWT
- Create
src/handlers/index.ts
Unit Tests
- Create
src/__tests__/handlers/RouteHandler.test.ts(14 tests) - Create
src/__tests__/handlers/QueryHandler.test.ts(15 tests) - Create
src/__tests__/handlers/MutationHandler.test.ts(15 tests) - Create
src/__tests__/mocks/routeConfig.mock.ts-
createMockRouteConfig(),createMockOperationContext() -
createMockGuestContext(),createMockAuthenticatedContext() -
mockOperationSuccess(),mockOperationFailure(),mockOperationThrows()
-
Security Review
- Verify error responses don't leak internal details (errorDetail only in dev mode)
- Verify operation access check is stubbed (always allowed for Phase 3)
- Verify session renewal failure doesn't fail the request (graceful degradation)
- Verify guest users don't get session renewal
Documentation Updates
- Update
CLAUDE.mdwith handler architecture patterns - Update
README.mdwith request pipeline details
Phase 4: Access Policy System
Policy Types
- Create
src/policies/policy.types.ts- Re-export
RouteAccessResult,OperationAccessResultfrom handler.types.ts
- Re-export
Route Policies
- Create
src/policies/checkRouteAccess.ts- Check authentication only (no ownership check)
- Uses role hierarchy: guest-user < logged-on-user < project-owner
Operation Policies
- Create
src/policies/checkOperationAccess.ts- Check authentication AND ownership for project-owner routes
-
isProjectOwner()helper usescontext.ownedProjects.includes(projectId) -
effectiveRoledetermines the user's role for this operation
Policy Enforcement
- Update
src/handlers/handler.types.ts- ReplaceallowedRoles[]withrequiredRole: CwcRole - Update
src/handlers/RequestHandler.ts- Use importedcheckRouteAccess - Update
src/handlers/QueryHandler.ts- Use importedcheckOperationAccess - Update
src/handlers/MutationHandler.ts- Use importedcheckOperationAccess - Create
src/policies/index.ts
Unit Tests
- Create
src/__tests__/policies/checkRouteAccess.test.ts - Create
src/__tests__/policies/checkOperationAccess.test.ts - Update
src/__tests__/mocks/routeConfig.mock.ts- UserequiredRole - Update existing handler tests to use
requiredRole
Security Review
- Verify no privilege escalation paths (guest → user, user → owner)
- Verify ownership checks use
context.ownedProjectsfrom JWT (not request body) - Verify
projectIdcomes from path params (operationContext), not payload - Verify route-level only checks authentication (no DB lookup for ownership)
- Verify generic error messages in production (
errorDetailonly in dev)
Documentation Updates
- Update
CLAUDE.mdwith access policy patterns - Update
README.mdwith role-based access details
Phase 5: SqlFunction Layer
Infrastructure
- Create
src/sql/sql.types.ts- Define SqlFunction input/output types
- Define pagination types (offset-based)
- Create
src/sql/index.ts
Content Tables (Full CRUD + Soft Delete)
project/
- Create
src/sql/project/selectProject.ts - Create
src/sql/project/listProjects.ts - Create
src/sql/project/insertProject.ts - Create
src/sql/project/updateProject.ts - Create
src/sql/project/deleteProject.ts(hard delete) - Create
src/sql/project/softDeleteProject.ts - Create
src/sql/project/index.ts
codingSession/
- Create
src/sql/codingSession/selectCodingSession.ts - Create
src/sql/codingSession/listCodingSessions.ts - Create
src/sql/codingSession/insertCodingSession.ts - Create
src/sql/codingSession/updateCodingSession.ts - Create
src/sql/codingSession/deleteCodingSession.ts - Create
src/sql/codingSession/softDeleteCodingSession.ts - Create
src/sql/codingSession/index.ts
codingSessionContent/
- Create
src/sql/codingSessionContent/selectCodingSessionContent.ts - Create
src/sql/codingSessionContent/listCodingSessionContents.ts - Create
src/sql/codingSessionContent/insertCodingSessionContent.ts - Create
src/sql/codingSessionContent/updateCodingSessionContent.ts - Create
src/sql/codingSessionContent/deleteCodingSessionContent.ts - Create
src/sql/codingSessionContent/softDeleteCodingSessionContent.ts - Create
src/sql/codingSessionContent/index.ts
codingSessionAttachment/
- Create
src/sql/codingSessionAttachment/selectCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/listCodingSessionAttachments.ts - Create
src/sql/codingSessionAttachment/insertCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/updateCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/deleteCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/softDeleteCodingSessionAttachment.ts - Create
src/sql/codingSessionAttachment/index.ts
comment/
- Create
src/sql/comment/selectComment.ts - Create
src/sql/comment/listComments.ts - Create
src/sql/comment/insertComment.ts - Create
src/sql/comment/updateComment.ts - Create
src/sql/comment/deleteComment.ts - Create
src/sql/comment/softDeleteComment.ts - Create
src/sql/comment/index.ts
reaction/
- Create
src/sql/reaction/selectReaction.ts - Create
src/sql/reaction/listReactions.ts - Create
src/sql/reaction/insertReaction.ts - Create
src/sql/reaction/updateReaction.ts - Create
src/sql/reaction/deleteReaction.ts - Create
src/sql/reaction/softDeleteReaction.ts - Create
src/sql/reaction/index.ts
contentReport/
- Create
src/sql/contentReport/selectContentReport.ts - Create
src/sql/contentReport/listContentReports.ts - Create
src/sql/contentReport/insertContentReport.ts - Create
src/sql/contentReport/updateContentReport.ts - Create
src/sql/contentReport/deleteContentReport.ts - Create
src/sql/contentReport/softDeleteContentReport.ts - Create
src/sql/contentReport/index.ts
abuseReport/
- Create
src/sql/abuseReport/selectAbuseReport.ts - Create
src/sql/abuseReport/listAbuseReports.ts - Create
src/sql/abuseReport/insertAbuseReport.ts - Create
src/sql/abuseReport/updateAbuseReport.ts - Create
src/sql/abuseReport/deleteAbuseReport.ts - Create
src/sql/abuseReport/softDeleteAbuseReport.ts - Create
src/sql/abuseReport/index.ts
Read-Only Tables
featureFlag/
- Create
src/sql/featureFlag/selectFeatureFlag.ts - Create
src/sql/featureFlag/listFeatureFlags.ts - Create
src/sql/featureFlag/index.ts
user/ (Limited Fields)
- Create
src/sql/user/selectUser.ts(userPkId, enabled, username, loginType, loginDate, otpValidated) - Create
src/sql/user/listUsers.ts(same limited fields) - Create
src/sql/user/index.ts
Unit Tests
- Create
src/__tests__/sql/project.test.ts- Test all project SqlFunctions - Create
src/__tests__/sql/codingSession.test.ts- Test all codingSession SqlFunctions - Create
src/__tests__/sql/codingSessionContent.test.ts - Create
src/__tests__/sql/codingSessionAttachment.test.ts - Create
src/__tests__/sql/comment.test.ts - Create
src/__tests__/sql/reaction.test.ts - Create
src/__tests__/sql/contentReport.test.ts - Create
src/__tests__/sql/abuseReport.test.ts - Create
src/__tests__/sql/featureFlag.test.ts - Create
src/__tests__/sql/user.test.ts
Security Review
- Verify all SqlFunctions use SqlClient (no raw SQL construction)
- Verify list queries filter
enabled=trueby default (soft-delete) - Verify user SqlFunctions only expose limited fields
- Verify no mass assignment vulnerabilities (explicit field mapping)
- Verify pagination limits are enforced (prevent data dumping)
Documentation Updates
- Update
CLAUDE.mdwith SqlFunction patterns - Update
README.mdwith data access layer details
Phase 6: CwcApiV1 Routes & Business Logic
Route Structure
- Create
src/apis/CwcApiV1/routes.ts - Create
src/apis/CwcApiV1/types.ts - Create
src/apis/CwcApiV1/index.ts
Project Operations
- Create
src/apis/CwcApiV1/queries/getProject.ts - Create
src/apis/CwcApiV1/queries/listProjects.ts - Create
src/apis/CwcApiV1/mutations/projectCreate.ts - Create
src/apis/CwcApiV1/mutations/projectUpdate.ts - Create
src/apis/CwcApiV1/mutations/projectDelete.ts
Coding Session Operations
- Create
src/apis/CwcApiV1/queries/getCodingSession.ts - Create
src/apis/CwcApiV1/queries/listCodingSessions.ts - Create
src/apis/CwcApiV1/mutations/codingSessionCreate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionUpdate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionDelete.ts
Coding Session Content Operations
- Create
src/apis/CwcApiV1/queries/getCodingSessionContent.ts - Create
src/apis/CwcApiV1/queries/listCodingSessionContents.ts - Create
src/apis/CwcApiV1/mutations/codingSessionContentCreate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionContentUpdate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionContentDelete.ts
Coding Session Attachment Operations
- Create
src/apis/CwcApiV1/queries/getCodingSessionAttachment.ts - Create
src/apis/CwcApiV1/queries/listCodingSessionAttachments.ts - Create
src/apis/CwcApiV1/mutations/codingSessionAttachmentCreate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionAttachmentUpdate.ts - Create
src/apis/CwcApiV1/mutations/codingSessionAttachmentDelete.ts
Comment Operations
- Create
src/apis/CwcApiV1/queries/getComment.ts - Create
src/apis/CwcApiV1/queries/listComments.ts - Create
src/apis/CwcApiV1/mutations/commentCreate.ts - Create
src/apis/CwcApiV1/mutations/commentUpdate.ts - Create
src/apis/CwcApiV1/mutations/commentDelete.ts
Reaction Operations
- Create
src/apis/CwcApiV1/queries/getReaction.ts - Create
src/apis/CwcApiV1/queries/listReactions.ts - Create
src/apis/CwcApiV1/mutations/reactionCreate.ts - Create
src/apis/CwcApiV1/mutations/reactionDelete.ts
Content Report Operations
- Create
src/apis/CwcApiV1/queries/getContentReport.ts - Create
src/apis/CwcApiV1/queries/listContentReports.ts - Create
src/apis/CwcApiV1/mutations/contentReportCreate.ts - Create
src/apis/CwcApiV1/mutations/contentReportUpdate.ts
Abuse Report Operations
- Create
src/apis/CwcApiV1/queries/getAbuseReport.ts - Create
src/apis/CwcApiV1/queries/listAbuseReports.ts - Create
src/apis/CwcApiV1/mutations/abuseReportCreate.ts - Create
src/apis/CwcApiV1/mutations/abuseReportUpdate.ts
Route Registration
- Register all routes in
routes.ts - Wire up routes to Express in
src/index.ts
Unit Tests
- Create
src/__tests__/apis/CwcApiV1/queries/getProject.test.ts - Create
src/__tests__/apis/CwcApiV1/queries/listProjects.test.ts - Create
src/__tests__/apis/CwcApiV1/mutations/projectCreate.test.ts - Create
src/__tests__/apis/CwcApiV1/mutations/projectUpdate.test.ts - Create
src/__tests__/apis/CwcApiV1/mutations/projectDelete.test.ts - Create tests for remaining query/mutation endpoints (follow same pattern)
Security Review
- Verify IDOR prevention (users can only access their own resources)
- Verify business logic doesn't allow unauthorized state transitions
- Verify content report/abuse report workflows don't expose reporter identity
- Verify rate limiting is appropriate per operation type
- Verify all user input is validated via cwc-schema before processing
Documentation Updates
- Update
CLAUDE.mdwith API route patterns - Update
README.mdwith complete endpoint documentation
Phase 7: Integration Testing & Final Review
Integration Tests
- Create
src/__tests__/integration/directory - Write end-to-end tests for complete request flows
- Write tests for cross-component interactions
- Write tests for error scenarios across layers
Security Review (Final)
- Verify security-focused test coverage (auth bypass, privilege escalation, IDOR)
- Verify error handling tests don't leak sensitive information
- Conduct final end-to-end security review of all phases
- Document any security considerations for deployment
Documentation Updates
- Update
CLAUDE.mdwith testing patterns and final review - Update
README.mdwith testing instructions and final review
Session Notes
Session 1
- Initial checklist created
- Specification reviewed and clarified:
- Guest access: Create context with 'guest-user' role for unauthenticated requests
- Session renewal: cwc-api calls cwc-auth /renew-session
- Pagination: Offset-based (page/pageSize)
- Soft delete: All content tables
- RouteHandler class chosen over middleware approach
- SqlFunction naming:
listinstead ofselectMultiple - Added read-only SqlFunctions for featureFlag and user (limited fields)
Session 2 (020)
Phase 1 COMPLETED
- Added
devscript to package.json usingtsx(better ESM support than ts-node) - Fixed cwc-backend-utils ESM issues:
loadDotEnv.ts: Added ESM-compatible__dirnameusingimport.meta.urlexpress/service.ts: Fixed wildcard route syntax for path-to-regexp 8.x (/*→/{*splat})
- Service starts successfully and health check responds (POST /health/v1)
- Added
Architecture Decision: Centralized Authentication
- cwc-api will NOT duplicate JWT verification logic
- cwc-api will call cwc-auth's
/auth/v1/verify-tokenendpoint - Trade-off: Additional HTTP call per request, but auth logic stays centralized
- BLOCKER: Phase 2 cannot proceed until cwc-auth has
/verify-tokenendpoint
Phase 2 Revised
- Updated checklist to reflect API-call approach
- Removed local JWT utilities (verifyUserJwt, extractJwtFromHeader)
- Added prerequisite: cwc-auth must implement /verify-token first
Phase 2 COMPLETED
- Implemented shared auth types in cwc-types (
src/authTypes.ts):UserJwtPayload,CwcRole,VerifyTokenResponse,VerifyTokenResult
- Updated cwc-auth to import
UserJwtPayloadfrom cwc-types (backwards-compatible re-export) - Created
AuthClientin cwc-backend-utils (src/AuthClient/):- HTTP client for cwc-auth, similar pattern to SqlClient
verifyToken(authHeader)returns typedVerifyTokenResult- Handles 401 responses, network errors, timeouts
- Created context layer in cwc-api (
src/context/):context.types.ts:AuthenticatedContext,GuestContext,RequestContextcreateContext.ts: Uses AuthClient, returns authenticated or guest context
- Created test mocks (
src/__tests__/mocks/authClient.mock.ts):createMockAuthClient(),createMockUserJwtPayload()mockVerifyTokenSuccess(),mockVerifyTokenFailure()
- All 10 context tests pass, all 38 cwc-api tests pass
- All affected packages typecheck: cwc-types, cwc-auth, cwc-backend-utils, cwc-api
- Note: AuthClient unit tests deferred (cwc-backend-utils has no test infrastructure)
- Functionality verified through cwc-api context tests
- Implemented shared auth types in cwc-types (
Session 3 (021)
Phase 3 COMPLETED
- Added
RenewSessionResulttypes to cwc-types/authTypes.ts - Added
renewSession()method to AuthClient in cwc-backend-utils - Created handler architecture:
handler.types.ts: Error codes, response types, route config, operation types, access policy stubsRouteHandler.ts: Route access check, delegates to Query/MutationHandlerQueryHandler.ts: Execute query, renew session, format responseMutationHandler.ts: Execute mutation, renew session, format response
- Created comprehensive test mocks (
routeConfig.mock.ts):createMockRouteConfig(),createMockOperationContext()createMockGuestContext(),createMockAuthenticatedContext()mockOperationSuccess(),mockOperationFailure(),mockOperationThrows()
- Updated config mocks with consistent naming:
getUnitConfig(),createDevConfig(),createProdConfig() - All 44 handler tests pass (14 RouteHandler + 15 QueryHandler + 15 MutationHandler)
- Added
Key Design Decisions:
- Session renewal on EVERY request (not just triggers) to keep sessions active
- RPC-style API: All endpoints use POST, no GET/PUT/DELETE
- Graceful degradation: Session renewal failure doesn't fail the request
- Operation access check stubbed for Phase 4 (always allowed for now)
Session 4 (023)
Phase 4 COMPLETED
- Created policy module (
src/policies/):policy.types.ts: Re-exports RouteAccessResult, OperationAccessResultcheckRouteAccess.ts: Route-level authentication checkcheckOperationAccess.ts: Operation-level access with ownership checkisProjectOwner()helper functionindex.ts: Exports
- Updated handler types:
- Replaced
allowedRoles: CwcRole[]withrequiredRole: CwcRole - Single field for both route and operation level access control
- Replaced
- Updated handlers to use policy functions:
- RequestHandler uses
checkRouteAccess(context, routeConfig.requiredRole) - QueryHandler uses
checkOperationAccess(context, operationContext, routeConfig.requiredRole) - MutationHandler uses same pattern as QueryHandler
- RequestHandler uses
- Created policy tests:
checkRouteAccess.test.ts(9 tests)checkOperationAccess.test.ts(14 tests)
- Updated all handler tests to use
requiredRole - All 121 tests pass
- Created policy module (
Key Design Decisions:
- Role Hierarchy:
guest-user < logged-on-user < project-owner - Two-Level Access Control:
- Route-level: Only checks authentication (no DB lookup)
- Operation-level: Checks authentication AND ownership for
project-owner
- requiredRole interpretation differs by level:
- Route:
project-ownermeans "must be authenticated" - Operation:
project-ownermeans "must own the project"
- Route:
- Ownership from JWT only: Uses
context.ownedProjects.includes(projectId), no database lookup - projectId from path params: Comes from
operationContext.projectId, not request body (security)
- Role Hierarchy:
packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { MutationHandler } from '../../handlers/MutationHandler';
import {
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
getUnitConfig,
createMockDevConfig,
createMockProdConfig,
createMockLogger,
} from '../mocks';
import type { MutationHandlerOptions } from '../../handlers/handler.types';
describe('MutationHandler', () => {
const unitConfig = getUnitConfig();
const mockProdConfig = createMockProdConfig();
beforeEach(() => {
jest.clearAllMocks();
});
describe('Successful Operations', () => {
it('should return 200 with data on successful mutation', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1, created: true });
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Item' },
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.data).toEqual({ id: 1, created: true });
}
});
it('should return 200 for all operations (RPC-style, no 201)', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 999, status: 'created' });
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
// All POST operations return 200, not 201
expect(response.statusCode).toBe(200);
});
it('should not include jwt in response (handled by RequestHandler)', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
});
describe('Error Responses', () => {
it('should return 404 for NOT_FOUND error code', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(404);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('NOT_FOUND');
expect(response.body.errorMessage).toBe('Resource not found');
}
});
it('should return 400 for ALREADY_EXISTS error code', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'ALREADY_EXISTS', 'Resource already exists');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(400);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('ALREADY_EXISTS');
}
});
it('should return 400 for VALIDATION_ERROR', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(400);
expect(response.body.success).toBe(false);
});
it('should return 500 for DATABASE_ERROR', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(500);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('DATABASE_ERROR');
}
});
});
describe('Exception Handling', () => {
it('should catch errors and return 500 response', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationThrows(routeConfig, new Error('Unexpected database error'));
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(500);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('INTERNAL_ERROR');
expect(response.body.errorMessage).toBe('An internal error occurred');
}
});
it('should include error detail when isDev is true', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
const mockDevConfig = createMockDevConfig();
mockOperationThrows(routeConfig, new Error('Detailed error message'));
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, mockDevConfig, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBe('Detailed error message');
}
});
it('should not include error detail in production mode', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationThrows(routeConfig, new Error('Detailed error message'));
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, mockProdConfig, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBeUndefined();
}
});
it('should log errors when logger is provided', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationThrows(routeConfig, new Error('Test error'));
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Operation Access', () => {
it('should allow authenticated access for logged-on-user routes', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
requiredRole: 'logged-on-user',
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should deny non-owner access for project-owner routes', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
requiredRole: 'project-owner',
});
const context = createMockAuthenticatedContext({
ownedProjects: ['other-project'],
});
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context, projectId: 'target-project' },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(403);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');
}
});
it('should allow owner access for project-owner routes', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
requiredRole: 'project-owner',
});
const context = createMockAuthenticatedContext({
ownedProjects: ['target-project'],
});
mockOperationSuccess(routeConfig, { id: 1, updated: true });
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'Updated' },
operationContext: { context, projectId: 'target-project' },
};
const handler = new MutationHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.data).toEqual({ id: 1, updated: true });
}
});
});
});
packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { QueryHandler } from '../../handlers/QueryHandler';
import {
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
getUnitConfig,
createMockDevConfig,
createMockProdConfig,
createMockLogger,
} from '../mocks';
import type { QueryHandlerOptions } from '../../handlers/handler.types';
describe('QueryHandler', () => {
const unitConfig = getUnitConfig();
const mockProdConfig = createMockProdConfig();
beforeEach(() => {
jest.clearAllMocks();
});
describe('Successful Operations', () => {
it('should return 200 with data on successful query', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1, name: 'test' });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.data).toEqual({ id: 1, name: 'test' });
}
});
it('should include pagination when operation returns it', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
const pagination = { page: 1, pageSize: 20, totalCount: 100, hasMore: true };
mockOperationSuccess(routeConfig, [{ id: 1 }, { id: 2 }], pagination);
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.pagination).toEqual(pagination);
}
});
it('should not include jwt in response (handled by RequestHandler)', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
});
describe('Error Responses', () => {
it('should return 404 for NOT_FOUND error code', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(404);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('NOT_FOUND');
expect(response.body.errorMessage).toBe('Resource not found');
}
});
it('should return 400 for VALIDATION_ERROR', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(400);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('VALIDATION_ERROR');
}
});
it('should return 403 for OPERATION_ACCESS_DENIED', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationFailure(routeConfig, 'OPERATION_ACCESS_DENIED', 'Not permitted');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(403);
expect(response.body.success).toBe(false);
});
it('should return 500 for DATABASE_ERROR', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(500);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('DATABASE_ERROR');
}
});
});
describe('Exception Handling', () => {
it('should catch errors and return 500 response', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationThrows(routeConfig, new Error('Unexpected database error'));
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(500);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('INTERNAL_ERROR');
expect(response.body.errorMessage).toBe('An internal error occurred');
}
});
it('should include error detail when isDev is true', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
const mockDevConfig = createMockDevConfig();
mockOperationThrows(routeConfig, new Error('Detailed error message'));
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, mockDevConfig, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBe('Detailed error message');
}
});
it('should not include error detail in production mode', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationThrows(routeConfig, new Error('Detailed error message'));
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
// mockProdConfig has isDev: false
const handler = new QueryHandler(options, mockProdConfig, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBeUndefined();
}
});
it('should log errors when logger is provided', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationThrows(routeConfig, new Error('Test error'));
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Operation Access', () => {
it('should allow guest access for guest-user routes', async () => {
const routeConfig = createMockRouteConfig({ requiredRole: 'guest-user' });
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should deny guest access for logged-on-user routes', async () => {
const routeConfig = createMockRouteConfig({ requiredRole: 'logged-on-user' });
const context = createMockGuestContext();
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(403);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');
}
});
it('should deny non-owner access for project-owner routes', async () => {
const routeConfig = createMockRouteConfig({ requiredRole: 'project-owner' });
const context = createMockAuthenticatedContext({
ownedProjects: ['other-project'],
});
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context, projectId: 'target-project' },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(403);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('OPERATION_ACCESS_DENIED');
}
});
it('should allow owner access for project-owner routes', async () => {
const routeConfig = createMockRouteConfig({ requiredRole: 'project-owner' });
const context = createMockAuthenticatedContext({
ownedProjects: ['target-project'],
});
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context, projectId: 'target-project' },
};
const handler = new QueryHandler(options, unitConfig, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts2 versions
Version 1
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { RequestHandler } from '../../handlers/RequestHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
mockRenewSessionSuccess,
mockRenewSessionFailure,
getUnitConfig,
createMockDevConfig,
createMockProdConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { RequestHandlerOptions } from '../../handlers/handler.types';
describe('RequestHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const unitConfig = getUnitConfig();
const mockDevConfig = createMockDevConfig();
const mockProdConfig = createMockProdConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
jest.clearAllMocks();
});
describe('Route Access Control', () => {
it('should return 401 for guest user accessing authenticated-only route', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'logged-on-user',
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(401);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('UNAUTHORIZED');
expect(response.body.errorMessage).toBe('Access denied');
}
// No session renewal on auth errors
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should return 401 for guest user accessing project-owner route', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'project-owner',
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(401);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('UNAUTHORIZED');
}
// No session renewal on auth errors
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should allow guest user to access guest-user routes', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'guest-user',
handlerType: 'query',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
// No renewal for guest users
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should allow authenticated user to access logged-on-user routes', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'logged-on-user',
handlerType: 'query',
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should allow logged-on-user to access project-owner routes at route level (ownership checked at operation level)', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'project-owner',
handlerType: 'mutation',
});
// Authenticated user with owned projects (will pass operation check)
const context = createMockAuthenticatedContext({
role: 'logged-on-user',
ownedProjects: ['test-project'],
});
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
pathParams: { projectId: 'test-project' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Route access allows authenticated users for project-owner routes
// Ownership is verified at operation level
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should include error detail in dev mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
path: '/projects/123',
requiredRole: 'logged-on-user',
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, mockDevConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBe('Authentication required');
}
});
it('should not include error detail in prod mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'logged-on-user',
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, mockProdConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBeUndefined();
}
});
});
describe('Session Renewal', () => {
it('should call renewSession for authenticated users on successful operation', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['logged-on-user'],
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBe('new-jwt-token');
}
});
it('should call renewSession for authenticated users on failed operation (NOT_FOUND)', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['logged-on-user'],
});
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Session renewed even on failed operation (user is still active)
expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
expect(response.statusCode).toBe(404);
expect(response.body.success).toBe(false);
});
it('should call renewSession for authenticated users on VALIDATION_ERROR', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
allowedRoles: ['logged-on-user'],
});
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Session renewed even on validation error
expect(mockAuthClient.renewSession).toHaveBeenCalled();
expect(response.statusCode).toBe(400);
});
it('should call renewSession for authenticated users on DATABASE_ERROR', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['logged-on-user'],
});
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Session renewed even on database error
expect(mockAuthClient.renewSession).toHaveBeenCalled();
expect(response.statusCode).toBe(500);
});
it('should NOT call renewSession on 401 UNAUTHORIZED', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user'], // Guest not allowed
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(401);
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should NOT call renewSession on 403 FORBIDDEN', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: [], // No roles allowed
});
const context = createMockAuthenticatedContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(403);
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should NOT call renewSession for guest users', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['guest-user'],
handlerType: 'query',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
it('should succeed operation when renewal fails (graceful handling)', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
allowedRoles: ['logged-on-user'],
});
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationSuccess(routeConfig, { id: 1, mutated: true });
mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined(); // No JWT since renewal failed
expect(response.body.data).toEqual({ id: 1, mutated: true });
}
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Handler Delegation', () => {
it('should delegate to QueryHandler for query handlerType', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['guest-user'],
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { data: 'from query' });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: { page: 1 },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.data).toEqual({ data: 'from query' });
}
// Query operation was called
expect(routeConfig.operation).toHaveBeenCalledWith(
{ page: 1 },
expect.objectContaining({ context })
);
});
it('should delegate to MutationHandler for mutation handlerType', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
allowedRoles: ['logged-on-user'],
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 123, created: true });
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Project' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.data).toEqual({ id: 123, created: true });
expect(response.body.jwt).toBeDefined(); // Session renewed
}
});
});
describe('Operation Context', () => {
it('should pass projectId from path params to operation context', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['guest-user'],
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'my-project-123' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
await handler.processRequest();
expect(routeConfig.operation).toHaveBeenCalledWith(
{},
expect.objectContaining({
projectId: 'my-project-123',
})
);
});
it('should pass resourceId from path params to operation context', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['guest-user'],
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
await handler.processRequest();
expect(routeConfig.operation).toHaveBeenCalledWith(
{},
expect.objectContaining({
projectId: 'proj-1',
resourceId: 'resource-456',
})
);
});
it('should handle missing path params gracefully', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['guest-user'],
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
// No pathParams
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
await handler.processRequest();
expect(routeConfig.operation).toHaveBeenCalledWith(
{},
expect.objectContaining({
projectId: undefined,
resourceId: undefined,
})
);
});
});
describe('Error Handling', () => {
it('should catch errors and return 500 response', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['guest-user'],
});
const context = createMockGuestContext();
mockOperationThrows(routeConfig, new Error('Unexpected error'));
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(500);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('INTERNAL_ERROR');
}
});
it('should log errors when logger is provided', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
allowedRoles: ['guest-user'],
});
const context = createMockGuestContext();
const mockLogger = createMockLogger();
mockOperationThrows(routeConfig, new Error('Test error'));
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
});
Version 2 (latest)
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { RequestHandler } from '../../handlers/RequestHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
mockRenewSessionSuccess,
mockRenewSessionFailure,
getUnitConfig,
createMockDevConfig,
createMockProdConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { RequestHandlerOptions } from '../../handlers/handler.types';
describe('RequestHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const unitConfig = getUnitConfig();
const mockDevConfig = createMockDevConfig();
const mockProdConfig = createMockProdConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
jest.clearAllMocks();
});
describe('Route Access Control', () => {
it('should return 401 for guest user accessing authenticated-only route', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'logged-on-user',
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(401);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('UNAUTHORIZED');
expect(response.body.errorMessage).toBe('Access denied');
}
// No session renewal on auth errors
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should return 401 for guest user accessing project-owner route', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'project-owner',
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(401);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('UNAUTHORIZED');
}
// No session renewal on auth errors
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should allow guest user to access guest-user routes', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'guest-user',
handlerType: 'query',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
// No renewal for guest users
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should allow authenticated user to access logged-on-user routes', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'logged-on-user',
handlerType: 'query',
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should allow logged-on-user to access project-owner routes at route level (ownership checked at operation level)', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'project-owner',
handlerType: 'mutation',
});
// Authenticated user with owned projects (will pass operation check)
const context = createMockAuthenticatedContext({
role: 'logged-on-user',
ownedProjects: ['test-project'],
});
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
pathParams: { projectId: 'test-project' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Route access allows authenticated users for project-owner routes
// Ownership is verified at operation level
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should include error detail in dev mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
path: '/projects/123',
requiredRole: 'logged-on-user',
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, mockDevConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBe('Authentication required');
}
});
it('should not include error detail in prod mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'logged-on-user',
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, mockProdConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBeUndefined();
}
});
});
describe('Session Renewal', () => {
it('should call renewSession for authenticated users on successful operation', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'logged-on-user',
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBe('new-jwt-token');
}
});
it('should call renewSession for authenticated users on failed operation (NOT_FOUND)', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'logged-on-user',
});
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Session renewed even on failed operation (user is still active)
expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
expect(response.statusCode).toBe(404);
expect(response.body.success).toBe(false);
});
it('should call renewSession for authenticated users on VALIDATION_ERROR', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
requiredRole: 'logged-on-user',
});
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Session renewed even on validation error
expect(mockAuthClient.renewSession).toHaveBeenCalled();
expect(response.statusCode).toBe(400);
});
it('should call renewSession for authenticated users on DATABASE_ERROR', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'logged-on-user',
});
const context = createMockAuthenticatedContext();
mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Session renewed even on database error
expect(mockAuthClient.renewSession).toHaveBeenCalled();
expect(response.statusCode).toBe(500);
});
it('should NOT call renewSession on 401 UNAUTHORIZED', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'logged-on-user', // Guest not allowed
});
const context = createMockGuestContext();
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(401);
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should NOT call renewSession on 403 OPERATION_ACCESS_DENIED', async () => {
// Test 403 from operation-level access denial (user doesn't own project)
const routeConfig = createMockRouteConfig({
requiredRole: 'project-owner',
handlerType: 'mutation',
});
const context = createMockAuthenticatedContext({
ownedProjects: ['other-project'], // Doesn't own the target project
});
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
pathParams: { projectId: 'not-owned-project' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(403);
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should NOT call renewSession for guest users', async () => {
const routeConfig = createMockRouteConfig({
requiredRole: 'guest-user',
handlerType: 'query',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
it('should succeed operation when renewal fails (graceful handling)', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
requiredRole: 'logged-on-user',
});
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationSuccess(routeConfig, { id: 1, mutated: true });
mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined(); // No JWT since renewal failed
expect(response.body.data).toEqual({ id: 1, mutated: true });
}
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Handler Delegation', () => {
it('should delegate to QueryHandler for query handlerType', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'guest-user',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { data: 'from query' });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: { page: 1 },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.data).toEqual({ data: 'from query' });
}
// Query operation was called
expect(routeConfig.operation).toHaveBeenCalledWith(
{ page: 1 },
expect.objectContaining({ context })
);
});
it('should delegate to MutationHandler for mutation handlerType', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'mutation',
requiredRole: 'logged-on-user',
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 123, created: true });
mockRenewSessionSuccess(mockAuthClient);
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Project' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.data).toEqual({ id: 123, created: true });
expect(response.body.jwt).toBeDefined(); // Session renewed
}
});
});
describe('Operation Context', () => {
it('should pass projectId from path params to operation context', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'guest-user',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'my-project-123' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
await handler.processRequest();
expect(routeConfig.operation).toHaveBeenCalledWith(
{},
expect.objectContaining({
projectId: 'my-project-123',
})
);
});
it('should pass resourceId from path params to operation context', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'guest-user',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
await handler.processRequest();
expect(routeConfig.operation).toHaveBeenCalledWith(
{},
expect.objectContaining({
projectId: 'proj-1',
resourceId: 'resource-456',
})
);
});
it('should handle missing path params gracefully', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'guest-user',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
// No pathParams
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
await handler.processRequest();
expect(routeConfig.operation).toHaveBeenCalledWith(
{},
expect.objectContaining({
projectId: undefined,
resourceId: undefined,
})
);
});
});
describe('Error Handling', () => {
it('should catch errors and return 500 response', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'guest-user',
});
const context = createMockGuestContext();
mockOperationThrows(routeConfig, new Error('Unexpected error'));
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(500);
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('INTERNAL_ERROR');
}
});
it('should log errors when logger is provided', async () => {
const routeConfig = createMockRouteConfig({
handlerType: 'query',
requiredRole: 'guest-user',
});
const context = createMockGuestContext();
const mockLogger = createMockLogger();
mockOperationThrows(routeConfig, new Error('Test error'));
const options: RequestHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
});
packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts
'use strict';
import { jest } from '@jest/globals';
import type { CwcRole } from 'cwc-types';
import type {
CwcApiRouteConfig,
CwcApiHandlerType,
OperationContext,
OperationResult,
} from '../../handlers/handler.types';
import type { GuestContext, AuthenticatedContext } from '../../context';
/**
* Creates a mock route configuration for testing
*
* Default requiredRole is 'guest-user' (public access).
* Override with 'logged-on-user' or 'project-owner' for authenticated routes.
*/
export function createMockRouteConfig<TPayload = unknown, TResult = unknown>(
overrides: Partial<CwcApiRouteConfig<TPayload, TResult>> = {}
): CwcApiRouteConfig<TPayload, TResult> {
return {
path: '/test',
handlerType: 'query' as CwcApiHandlerType,
requiredRole: 'guest-user' as CwcRole,
operation: jest.fn<(p: TPayload, c: OperationContext) => Promise<OperationResult<TResult>>>().mockResolvedValue({
success: true,
data: {} as TResult,
}),
...overrides,
};
}
/**
* Creates a mock operation context for testing
*/
export function createMockOperationContext(
overrides: Partial<OperationContext> = {}
): OperationContext {
return {
context: createMockGuestContext(),
...overrides,
};
}
/**
* Creates a mock guest context for testing
*/
export function createMockGuestContext(): GuestContext {
return {
isAuthenticated: false,
role: 'guest-user',
};
}
/**
* Creates a mock authenticated context for testing
*/
export function createMockAuthenticatedContext(
overrides: Partial<Omit<AuthenticatedContext, 'isAuthenticated'>> = {}
): AuthenticatedContext {
return {
isAuthenticated: true,
role: 'logged-on-user',
userPkId: 123,
username: 'testuser@example.com',
ownedProjects: ['project-1', 'project-2'],
payload: {
jti: 'test-jwt-id',
sub: 123,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 900,
login: {
username: 'testuser@example.com',
deviceId: 'test-device-id',
userJwtId: 'test-jwt-id',
loginType: 'cwc',
kulo: false,
ownedProjects: ['project-1', 'project-2'],
isGuestUser: false,
},
},
...overrides,
};
}
/**
* Configures mock operation to return success
*/
export function mockOperationSuccess<TResult>(
routeConfig: CwcApiRouteConfig<unknown, TResult>,
data: TResult,
pagination?: { page: number; pageSize: number; totalCount: number; hasMore: boolean }
): void {
const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;
mockFn.mockResolvedValueOnce({
success: true,
data,
...(pagination ? { pagination } : {}),
});
}
/**
* Configures mock operation to return failure
*/
export function mockOperationFailure(
routeConfig: CwcApiRouteConfig,
errorCode: string = 'NOT_FOUND',
errorMessage: string = 'Resource not found'
): void {
const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;
mockFn.mockResolvedValueOnce({
success: false,
errorCode: errorCode as any,
errorMessage,
});
}
/**
* Configures mock operation to throw an error
*/
export function mockOperationThrows(
routeConfig: CwcApiRouteConfig,
error: Error = new Error('Unexpected error')
): void {
const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;
mockFn.mockRejectedValueOnce(error);
}
packages/cwc-api/src/__tests__/policies/checkOperationAccess.test.ts2 versions
Version 1
'use strict';
import { checkOperationAccess, isProjectOwner } from '../../policies';
import {
createMockGuestContext,
createMockAuthenticatedContext,
createMockOperationContext,
} from '../mocks/routeConfig.mock';
describe('isProjectOwner', () => {
it('should return true when user owns the project', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1', 'project-2'],
});
expect(isProjectOwner(context, 'project-1')).toBe(true);
expect(isProjectOwner(context, 'project-2')).toBe(true);
});
it('should return false when user does not own the project', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1', 'project-2'],
});
expect(isProjectOwner(context, 'project-3')).toBe(false);
});
it('should return false for guest context', () => {
const context = createMockGuestContext();
expect(isProjectOwner(context, 'project-1')).toBe(false);
});
it('should return false when projectId is undefined', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1'],
});
expect(isProjectOwner(context, undefined)).toBe(false);
});
it('should return false when ownedProjects is empty', () => {
const context = createMockAuthenticatedContext({
ownedProjects: [],
});
expect(isProjectOwner(context, 'project-1')).toBe(false);
});
});
describe('checkOperationAccess', () => {
describe('requiredRole: guest-user (public access)', () => {
it('should allow guest users', () => {
const context = createMockGuestContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('guest-user');
expect(result.reason).toBeUndefined();
});
it('should allow authenticated users', () => {
const context = createMockAuthenticatedContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('logged-on-user');
expect(result.reason).toBeUndefined();
});
it('should allow project owners', () => {
const context = createMockAuthenticatedContext({ role: 'project-owner' });
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('project-owner');
expect(result.reason).toBeUndefined();
});
});
describe('requiredRole: logged-on-user (authenticated access)', () => {
it('should deny guest users', () => {
const context = createMockGuestContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'logged-on-user');
expect(result.allowed).toBe(false);
expect(result.reason).toBe('Authentication required');
expect(result.effectiveRole).toBeUndefined();
});
it('should allow authenticated users', () => {
const context = createMockAuthenticatedContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'logged-on-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('logged-on-user');
expect(result.reason).toBeUndefined();
});
it('should allow project owners', () => {
const context = createMockAuthenticatedContext({ role: 'project-owner' });
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'logged-on-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('project-owner');
expect(result.reason).toBeUndefined();
});
});
describe('requiredRole: project-owner (owner access)', () => {
it('should deny guest users', () => {
const context = createMockGuestContext();
const operationContext = createMockOperationContext({
context,
projectId: 'project-1',
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.allowed).toBe(false);
expect(result.reason).toBe('Authentication required');
expect(result.effectiveRole).toBeUndefined();
});
it('should deny authenticated users who do not own the project', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1', 'project-2'],
});
const operationContext = createMockOperationContext({
context,
projectId: 'project-3',
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.allowed).toBe(false);
expect(result.reason).toBe("User does not own project 'project-3'");
expect(result.effectiveRole).toBeUndefined();
});
it('should allow authenticated users who own the project', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1', 'project-2'],
});
const operationContext = createMockOperationContext({
context,
projectId: 'project-1',
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('project-owner');
expect(result.reason).toBeUndefined();
});
it('should deny when projectId is not provided', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1'],
});
const operationContext = createMockOperationContext({
context,
projectId: undefined,
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.allowed).toBe(false);
expect(result.reason).toBe('Project ID required for ownership check');
expect(result.effectiveRole).toBeUndefined();
});
});
describe('effectiveRole determination', () => {
it('should return guest-user for guests on public routes', () => {
const context = createMockGuestContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.effectiveRole).toBe('guest-user');
});
it('should return logged-on-user for authenticated users on public routes', () => {
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.effectiveRole).toBe('logged-on-user');
});
it('should return logged-on-user for authenticated users on authenticated routes', () => {
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'logged-on-user');
expect(result.effectiveRole).toBe('logged-on-user');
});
it('should return project-owner when user owns the project', () => {
const context = createMockAuthenticatedContext({
role: 'logged-on-user',
ownedProjects: ['project-1'],
});
const operationContext = createMockOperationContext({
context,
projectId: 'project-1',
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.effectiveRole).toBe('project-owner');
});
});
});
Version 2 (latest)
'use strict';
import { checkOperationAccess, isProjectOwner } from '../../policies';
import {
createMockGuestContext,
createMockAuthenticatedContext,
createMockOperationContext,
} from '../mocks/routeConfig.mock';
describe('isProjectOwner', () => {
it('should return true when user owns the project', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1', 'project-2'],
});
expect(isProjectOwner(context, 'project-1')).toBe(true);
expect(isProjectOwner(context, 'project-2')).toBe(true);
});
it('should return false when user does not own the project', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1', 'project-2'],
});
expect(isProjectOwner(context, 'project-3')).toBe(false);
});
it('should return false for guest context', () => {
const context = createMockGuestContext();
expect(isProjectOwner(context, 'project-1')).toBe(false);
});
it('should return false when projectId is undefined', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1'],
});
expect(isProjectOwner(context, undefined)).toBe(false);
});
it('should return false when ownedProjects is empty', () => {
const context = createMockAuthenticatedContext({
ownedProjects: [],
});
expect(isProjectOwner(context, 'project-1')).toBe(false);
});
});
describe('checkOperationAccess', () => {
describe('requiredRole: guest-user (public access)', () => {
it('should allow guest users', () => {
const context = createMockGuestContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('guest-user');
expect(result.reason).toBeUndefined();
});
it('should allow authenticated users', () => {
const context = createMockAuthenticatedContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('logged-on-user');
expect(result.reason).toBeUndefined();
});
it('should allow project owners', () => {
const context = createMockAuthenticatedContext({ role: 'project-owner' });
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('project-owner');
expect(result.reason).toBeUndefined();
});
});
describe('requiredRole: logged-on-user (authenticated access)', () => {
it('should deny guest users', () => {
const context = createMockGuestContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'logged-on-user');
expect(result.allowed).toBe(false);
expect(result.reason).toBe('Authentication required');
expect(result.effectiveRole).toBeUndefined();
});
it('should allow authenticated users', () => {
const context = createMockAuthenticatedContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'logged-on-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('logged-on-user');
expect(result.reason).toBeUndefined();
});
it('should allow project owners', () => {
const context = createMockAuthenticatedContext({ role: 'project-owner' });
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'logged-on-user');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('project-owner');
expect(result.reason).toBeUndefined();
});
});
describe('requiredRole: project-owner (owner access)', () => {
it('should deny guest users', () => {
const context = createMockGuestContext();
const operationContext = createMockOperationContext({
context,
projectId: 'project-1',
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.allowed).toBe(false);
expect(result.reason).toBe('Authentication required');
expect(result.effectiveRole).toBeUndefined();
});
it('should deny authenticated users who do not own the project', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1', 'project-2'],
});
const operationContext = createMockOperationContext({
context,
projectId: 'project-3',
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.allowed).toBe(false);
expect(result.reason).toBe("User does not own project 'project-3'");
expect(result.effectiveRole).toBeUndefined();
});
it('should allow authenticated users who own the project', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1', 'project-2'],
});
const operationContext = createMockOperationContext({
context,
projectId: 'project-1',
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.allowed).toBe(true);
expect(result.effectiveRole).toBe('project-owner');
expect(result.reason).toBeUndefined();
});
it('should deny when projectId is not provided', () => {
const context = createMockAuthenticatedContext({
ownedProjects: ['project-1'],
});
const operationContext = createMockOperationContext({
context,
projectId: undefined,
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.allowed).toBe(false);
expect(result.reason).toBe('projectId is required for ownership check');
expect(result.effectiveRole).toBeUndefined();
});
});
describe('effectiveRole determination', () => {
it('should return guest-user for guests on public routes', () => {
const context = createMockGuestContext();
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.effectiveRole).toBe('guest-user');
});
it('should return logged-on-user for authenticated users on public routes', () => {
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'guest-user');
expect(result.effectiveRole).toBe('logged-on-user');
});
it('should return logged-on-user for authenticated users on authenticated routes', () => {
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
const operationContext = createMockOperationContext({ context });
const result = checkOperationAccess(context, operationContext, 'logged-on-user');
expect(result.effectiveRole).toBe('logged-on-user');
});
it('should return project-owner when user owns the project', () => {
const context = createMockAuthenticatedContext({
role: 'logged-on-user',
ownedProjects: ['project-1'],
});
const operationContext = createMockOperationContext({
context,
projectId: 'project-1',
});
const result = checkOperationAccess(context, operationContext, 'project-owner');
expect(result.effectiveRole).toBe('project-owner');
});
});
});
packages/cwc-api/src/__tests__/policies/checkRouteAccess.test.ts
'use strict';
import { checkRouteAccess } from '../../policies';
import { createMockGuestContext, createMockAuthenticatedContext } from '../mocks/routeConfig.mock';
describe('checkRouteAccess', () => {
describe('requiredRole: guest-user (public access)', () => {
it('should allow guest users', () => {
const context = createMockGuestContext();
const result = checkRouteAccess(context, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.reason).toBeUndefined();
});
it('should allow authenticated users', () => {
const context = createMockAuthenticatedContext();
const result = checkRouteAccess(context, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.reason).toBeUndefined();
});
it('should allow project owners', () => {
const context = createMockAuthenticatedContext({ role: 'project-owner' });
const result = checkRouteAccess(context, 'guest-user');
expect(result.allowed).toBe(true);
expect(result.reason).toBeUndefined();
});
});
describe('requiredRole: logged-on-user (authenticated access)', () => {
it('should deny guest users', () => {
const context = createMockGuestContext();
const result = checkRouteAccess(context, 'logged-on-user');
expect(result.allowed).toBe(false);
expect(result.reason).toBe('Authentication required');
});
it('should allow authenticated users', () => {
const context = createMockAuthenticatedContext();
const result = checkRouteAccess(context, 'logged-on-user');
expect(result.allowed).toBe(true);
expect(result.reason).toBeUndefined();
});
it('should allow project owners', () => {
const context = createMockAuthenticatedContext({ role: 'project-owner' });
const result = checkRouteAccess(context, 'logged-on-user');
expect(result.allowed).toBe(true);
expect(result.reason).toBeUndefined();
});
});
describe('requiredRole: project-owner (owner access)', () => {
it('should deny guest users', () => {
const context = createMockGuestContext();
const result = checkRouteAccess(context, 'project-owner');
expect(result.allowed).toBe(false);
expect(result.reason).toBe('Authentication required');
});
it('should allow authenticated users (ownership checked at operation level)', () => {
const context = createMockAuthenticatedContext();
const result = checkRouteAccess(context, 'project-owner');
expect(result.allowed).toBe(true);
expect(result.reason).toBeUndefined();
});
it('should allow project owners', () => {
const context = createMockAuthenticatedContext({ role: 'project-owner' });
const result = checkRouteAccess(context, 'project-owner');
expect(result.allowed).toBe(true);
expect(result.reason).toBeUndefined();
});
});
});
packages/cwc-api/src/handlers/handler.types.ts
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext } from '../context';
// ============================================================================
// Error Codes
// ============================================================================
/**
* Error codes for cwc-api operations
*/
export type CwcApiErrorCode =
// Access control errors
| 'UNAUTHORIZED' // No valid authentication
| 'FORBIDDEN' // Authenticated but not allowed
| 'ROUTE_ACCESS_DENIED' // Role cannot access this route
| 'OPERATION_ACCESS_DENIED' // Role cannot perform this operation
// Resource errors
| 'NOT_FOUND' // Resource does not exist
| 'ALREADY_EXISTS' // Duplicate resource
// Validation errors
| 'VALIDATION_ERROR' // Request payload validation failed
| 'INVALID_PARAMETER' // Invalid query/path parameter
// System errors
| 'INTERNAL_ERROR' // Unexpected server error
| 'DATABASE_ERROR' // Database operation failed
| 'AUTH_SERVICE_ERROR'; // cwc-auth communication failed
// ============================================================================
// HTTP Status Codes
// ============================================================================
export type CwcApiSuccessStatusCode = 200;
export type CwcApiErrorStatusCode = 400 | 401 | 403 | 404 | 500;
export type CwcApiStatusCode = CwcApiSuccessStatusCode | CwcApiErrorStatusCode;
// ============================================================================
// Response Types
// ============================================================================
/**
* Pagination metadata for list responses
*/
export type CwcApiPagination = {
page: number;
pageSize: number;
totalCount: number;
hasMore: boolean;
};
/**
* Success response envelope
*/
export type CwcApiSuccessResponse<T = unknown> = {
success: true;
data: T;
pagination?: CwcApiPagination | undefined;
jwt?: string | undefined; // New JWT if session was renewed
};
/**
* Error response envelope
*/
export type CwcApiErrorResponse = {
success: false;
errorCode: CwcApiErrorCode;
errorMessage: string;
errorDetail?: string | undefined; // Dev-only
};
/**
* Union of all response types
*/
export type CwcApiResponse<T = unknown> = CwcApiSuccessResponse<T> | CwcApiErrorResponse;
/**
* Full handler response with HTTP status code
*/
export type CwcApiHandlerResponse<T = unknown> = {
statusCode: CwcApiStatusCode;
body: CwcApiResponse<T>;
};
// ============================================================================
// Route Configuration
// ============================================================================
/**
* Handler type: query (read-only) or mutation (write)
*/
export type CwcApiHandlerType = 'query' | 'mutation';
/**
* Route configuration for cwc-api endpoints
*/
export type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {
// Route identification
path: string;
handlerType: CwcApiHandlerType;
// Access control - minimum role required (uses role hierarchy)
// guest-user < logged-on-user < project-owner
requiredRole: CwcRole;
// Operation (injected worker function)
operation: CwcApiOperation<TPayload, TResult>;
// Debug flag
debug?: boolean | undefined;
};
/**
* Map of route paths to configurations
*/
export type CwcApiRouteConfigs = {
[key: string]: CwcApiRouteConfig;
};
// ============================================================================
// Operation Types
// ============================================================================
/**
* Operation context passed to worker functions
*/
export type OperationContext = {
context: RequestContext;
projectId?: string | undefined; // For project-scoped operations
resourceId?: string | undefined; // For resource-specific operations
};
/**
* Operation result from worker functions
*/
export type OperationResult<T = unknown> =
| { success: true; data: T; pagination?: CwcApiPagination | undefined }
| { success: false; errorCode: CwcApiErrorCode; errorMessage: string };
/**
* Operation function signature (worker)
*/
export type CwcApiOperation<TPayload = unknown, TResult = unknown> = (
payload: TPayload,
operationContext: OperationContext
) => Promise<OperationResult<TResult>>;
// ============================================================================
// Handler Options
// ============================================================================
/**
* Base options for all handlers
*/
export type BaseHandlerOptions = {
context: RequestContext;
routeConfig: CwcApiRouteConfig;
authHeader: string | undefined;
};
/**
* Options for RequestHandler
*/
export type RequestHandlerOptions = BaseHandlerOptions & {
payload: unknown;
pathParams?: Record<string, string> | undefined;
};
/**
* Options for QueryHandler
*/
export type QueryHandlerOptions = BaseHandlerOptions & {
payload: unknown;
operationContext: OperationContext;
};
/**
* Options for MutationHandler
*/
export type MutationHandlerOptions = QueryHandlerOptions;
// ============================================================================
// Handler Interface
// ============================================================================
/**
* Interface for all handlers
*/
export interface CwcApiHandler {
processRequest(): Promise<CwcApiHandlerResponse>;
}
// ============================================================================
// Access Policy Types (Stubs for Phase 4)
// ============================================================================
/**
* Route access check result
*/
export type RouteAccessResult = {
allowed: boolean;
reason?: string | undefined;
};
/**
* Operation access check result
*/
export type OperationAccessResult = {
allowed: boolean;
effectiveRole?: CwcRole | undefined; // Role for this specific operation
reason?: string | undefined;
};
/**
* Route access policy checker interface (Phase 4)
*/
export interface RouteAccessPolicy {
checkRouteAccess(context: RequestContext, routeConfig: CwcApiRouteConfig): RouteAccessResult;
}
/**
* Operation access policy checker interface (Phase 4)
*/
export interface OperationAccessPolicy {
checkOperationAccess(
context: RequestContext,
operationContext: OperationContext
): Promise<OperationAccessResult>;
}
packages/cwc-api/src/handlers/MutationHandler.ts2 versions
Version 1
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
MutationHandlerOptions,
} from './handler.types';
import {
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from './responseUtils';
import { checkOperationAccess } from '../policies';
const codeLocation = 'handlers/MutationHandler.ts';
/**
* MutationHandler - Handles write operations
*
* Responsibilities:
* 1. Check operation-level access (Phase 4 stub)
* 2. Execute the mutation operation
* 3. Format successful response with data
*
* Note: Session renewal is handled by RequestHandler
*/
export class MutationHandler implements CwcApiHandler {
private options: MutationHandlerOptions;
private config: CwcApiConfig;
private logger: ILogger | undefined;
constructor(
options: MutationHandlerOptions,
config: CwcApiConfig,
logger: ILogger | undefined
) {
this.options = options;
this.config = config;
this.logger = logger;
}
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, operationContext } = this.options;
try {
// Step 1: Check operation-level access (Phase 4 implementation)
const operationAccess = await this.checkOperationAccess();
if (!operationAccess.allowed) {
return createOperationDeniedResponse(this.config, operationAccess.reason);
}
// Step 2: Execute the mutation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return createOperationErrorResponse(result.errorCode, result.errorMessage);
}
// Step 3: Format response (JWT added by RequestHandler, always 200 for RPC-style API)
return {
statusCode: 200,
body: {
success: true,
data: result.data,
},
};
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error executing mutation operation for ${routeConfig.path}`,
error,
});
return createInternalErrorResponse(this.config, error);
}
}
/**
* Check if the current context can perform this operation
* Phase 3: Stub - always returns allowed
* Phase 4: Will check ownership, resource access, etc.
*/
private async checkOperationAccess(): Promise<OperationAccessResult> {
// Phase 4 will implement actual access policy checks
return { allowed: true };
}
}
Version 2 (latest)
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
MutationHandlerOptions,
} from './handler.types';
import {
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from './responseUtils';
import { checkOperationAccess } from '../policies';
const codeLocation = 'handlers/MutationHandler.ts';
/**
* MutationHandler - Handles write operations
*
* Responsibilities:
* 1. Check operation-level access (Phase 4 stub)
* 2. Execute the mutation operation
* 3. Format successful response with data
*
* Note: Session renewal is handled by RequestHandler
*/
export class MutationHandler implements CwcApiHandler {
private options: MutationHandlerOptions;
private config: CwcApiConfig;
private logger: ILogger | undefined;
constructor(
options: MutationHandlerOptions,
config: CwcApiConfig,
logger: ILogger | undefined
) {
this.options = options;
this.config = config;
this.logger = logger;
}
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, operationContext } = this.options;
try {
// Step 1: Check operation-level access (authentication + ownership for project-owner)
const operationAccess = checkOperationAccess(context, operationContext, routeConfig.requiredRole);
if (!operationAccess.allowed) {
return createOperationDeniedResponse(this.config, operationAccess.reason);
}
// Step 2: Execute the mutation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return createOperationErrorResponse(result.errorCode, result.errorMessage);
}
// Step 3: Format response (JWT added by RequestHandler, always 200 for RPC-style API)
return {
statusCode: 200,
body: {
success: true,
data: result.data,
},
};
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error executing mutation operation for ${routeConfig.path}`,
error,
});
return createInternalErrorResponse(this.config, error);
}
}
}
packages/cwc-api/src/handlers/QueryHandler.ts2 versions
Version 1
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
QueryHandlerOptions,
} from './handler.types';
import {
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from './responseUtils';
import { checkOperationAccess } from '../policies';
const codeLocation = 'handlers/QueryHandler.ts';
/**
* QueryHandler - Handles read-only operations
*
* Responsibilities:
* 1. Check operation-level access (Phase 4 stub)
* 2. Execute the query operation
* 3. Format successful response with data/pagination
*
* Note: Session renewal is handled by RequestHandler
*/
export class QueryHandler implements CwcApiHandler {
private options: QueryHandlerOptions;
private config: CwcApiConfig;
private logger: ILogger | undefined;
constructor(
options: QueryHandlerOptions,
config: CwcApiConfig,
logger: ILogger | undefined
) {
this.options = options;
this.config = config;
this.logger = logger;
}
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, operationContext } = this.options;
try {
// Step 1: Check operation-level access (Phase 4 implementation)
const operationAccess = await this.checkOperationAccess();
if (!operationAccess.allowed) {
return createOperationDeniedResponse(this.config, operationAccess.reason);
}
// Step 2: Execute the operation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return createOperationErrorResponse(result.errorCode, result.errorMessage);
}
// Step 3: Format response (JWT added by RequestHandler)
return {
statusCode: 200,
body: {
success: true,
data: result.data,
...(result.pagination ? { pagination: result.pagination } : {}),
},
};
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error executing query operation for ${routeConfig.path}`,
error,
});
return createInternalErrorResponse(this.config, error);
}
}
/**
* Check if the current context can perform this operation
* Phase 3: Stub - always returns allowed
* Phase 4: Will check ownership, resource access, etc.
*/
private async checkOperationAccess(): Promise<OperationAccessResult> {
// Phase 4 will implement actual access policy checks
// For now, route-level access is sufficient
return { allowed: true };
}
}
Version 2 (latest)
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
QueryHandlerOptions,
} from './handler.types';
import {
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from './responseUtils';
import { checkOperationAccess } from '../policies';
const codeLocation = 'handlers/QueryHandler.ts';
/**
* QueryHandler - Handles read-only operations
*
* Responsibilities:
* 1. Check operation-level access (Phase 4 stub)
* 2. Execute the query operation
* 3. Format successful response with data/pagination
*
* Note: Session renewal is handled by RequestHandler
*/
export class QueryHandler implements CwcApiHandler {
private options: QueryHandlerOptions;
private config: CwcApiConfig;
private logger: ILogger | undefined;
constructor(
options: QueryHandlerOptions,
config: CwcApiConfig,
logger: ILogger | undefined
) {
this.options = options;
this.config = config;
this.logger = logger;
}
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, operationContext } = this.options;
try {
// Step 1: Check operation-level access (authentication + ownership for project-owner)
const operationAccess = checkOperationAccess(context, operationContext, routeConfig.requiredRole);
if (!operationAccess.allowed) {
return createOperationDeniedResponse(this.config, operationAccess.reason);
}
// Step 2: Execute the operation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return createOperationErrorResponse(result.errorCode, result.errorMessage);
}
// Step 3: Format response (JWT added by RequestHandler)
return {
statusCode: 200,
body: {
success: true,
data: result.data,
...(result.pagination ? { pagination: result.pagination } : {}),
},
};
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error executing query operation for ${routeConfig.path}`,
error,
});
return createInternalErrorResponse(this.config, error);
}
}
}
packages/cwc-api/src/handlers/RequestHandler.ts3 versions
Version 1
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
CwcApiSuccessResponse,
RequestHandlerOptions,
OperationContext,
} from './handler.types';
import { QueryHandler } from './QueryHandler';
import { MutationHandler } from './MutationHandler';
import { createInternalErrorResponse } from './responseUtils';
import { checkRouteAccess } from '../policies';
const codeLocation = 'handlers/RequestHandler.ts';
/**
* RequestHandler - Entry point for processing API requests
*
* Responsibilities:
* 1. Check route-level access based on context role
* 2. Build operation context with path params
* 3. Delegate to QueryHandler or MutationHandler based on handlerType
* 4. Renew session for authenticated users (except on auth errors)
*/
export class RequestHandler implements CwcApiHandler {
private options: RequestHandlerOptions;
private config: CwcApiConfig;
private authClient: AuthClient;
private logger: ILogger | undefined;
constructor(
options: RequestHandlerOptions,
config: CwcApiConfig,
authClient: AuthClient,
logger: ILogger | undefined
) {
this.options = options;
this.config = config;
this.authClient = authClient;
this.logger = logger;
}
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, authHeader, pathParams } = this.options;
try {
// Step 1: Check route-level access
const routeAccess = this.checkRouteAccess();
if (!routeAccess.allowed) {
// No session renewal for auth errors
return this.createAccessDeniedResponse(routeAccess.reason);
}
// Step 2: Build operation context
const operationContext: OperationContext = {
context,
projectId: pathParams?.['projectId'],
resourceId: pathParams?.['resourceId'],
};
// Step 3: Delegate to appropriate handler
let response: CwcApiHandlerResponse;
if (routeConfig.handlerType === 'query') {
const queryHandler = new QueryHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.logger
);
response = await queryHandler.processRequest();
} else {
const mutationHandler = new MutationHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.logger
);
response = await mutationHandler.processRequest();
}
// Step 4: Renew session for authenticated users (except on auth errors)
const isAuthError = response.statusCode === 401 || response.statusCode === 403;
if (context.isAuthenticated && !isAuthError) {
const renewResult = await this.authClient.renewSession(authHeader);
if (renewResult.success && response.body.success) {
// Add JWT to successful response
(response.body as CwcApiSuccessResponse).jwt = renewResult.jwt;
} else if (!renewResult.success) {
// Log warning but don't fail the operation
this.logger?.logError({
userPkId: context.userPkId,
codeLocation,
message: `Session renewal failed for ${routeConfig.path}`,
error: renewResult.error,
});
}
}
return response;
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error processing request ${routeConfig.path}`,
error,
});
return createInternalErrorResponse(this.config, error);
}
}
/**
* Check if the current context role can access this route
* Compares context.role against routeConfig.allowedRoles
*/
private checkRouteAccess(): RouteAccessResult {
const { context, routeConfig } = this.options;
const { allowedRoles } = routeConfig;
// Check if the current role is in allowedRoles
if (allowedRoles.includes(context.role)) {
return { allowed: true };
}
// For authenticated users with 'project-owner' role requirement,
// 'logged-on-user' should also be allowed (ownership check happens at operation level)
if (
context.isAuthenticated &&
context.role === 'logged-on-user' &&
allowedRoles.includes('project-owner')
) {
return { allowed: true };
}
return {
allowed: false,
reason: `Role '${context.role}' cannot access route ${routeConfig.path}`,
};
}
private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {
const { context } = this.options;
// Use 401 for unauthenticated, 403 for authenticated but not allowed
const statusCode = context.isAuthenticated ? 403 : 401;
const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage: 'Access denied',
...(this.config.isDev && reason ? { errorDetail: reason } : {}),
},
};
}
}
Version 2
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
CwcApiSuccessResponse,
RequestHandlerOptions,
OperationContext,
} from './handler.types';
import { QueryHandler } from './QueryHandler';
import { MutationHandler } from './MutationHandler';
import { createInternalErrorResponse } from './responseUtils';
import { checkRouteAccess } from '../policies';
const codeLocation = 'handlers/RequestHandler.ts';
/**
* RequestHandler - Entry point for processing API requests
*
* Responsibilities:
* 1. Check route-level access based on context role
* 2. Build operation context with path params
* 3. Delegate to QueryHandler or MutationHandler based on handlerType
* 4. Renew session for authenticated users (except on auth errors)
*/
export class RequestHandler implements CwcApiHandler {
private options: RequestHandlerOptions;
private config: CwcApiConfig;
private authClient: AuthClient;
private logger: ILogger | undefined;
constructor(
options: RequestHandlerOptions,
config: CwcApiConfig,
authClient: AuthClient,
logger: ILogger | undefined
) {
this.options = options;
this.config = config;
this.authClient = authClient;
this.logger = logger;
}
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, authHeader, pathParams } = this.options;
try {
// Step 1: Check route-level access (authentication only, no ownership check)
const routeAccess = checkRouteAccess(context, routeConfig.requiredRole);
if (!routeAccess.allowed) {
// No session renewal for auth errors
return this.createAccessDeniedResponse(routeAccess.reason);
}
// Step 2: Build operation context
const operationContext: OperationContext = {
context,
projectId: pathParams?.['projectId'],
resourceId: pathParams?.['resourceId'],
};
// Step 3: Delegate to appropriate handler
let response: CwcApiHandlerResponse;
if (routeConfig.handlerType === 'query') {
const queryHandler = new QueryHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.logger
);
response = await queryHandler.processRequest();
} else {
const mutationHandler = new MutationHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.logger
);
response = await mutationHandler.processRequest();
}
// Step 4: Renew session for authenticated users (except on auth errors)
const isAuthError = response.statusCode === 401 || response.statusCode === 403;
if (context.isAuthenticated && !isAuthError) {
const renewResult = await this.authClient.renewSession(authHeader);
if (renewResult.success && response.body.success) {
// Add JWT to successful response
(response.body as CwcApiSuccessResponse).jwt = renewResult.jwt;
} else if (!renewResult.success) {
// Log warning but don't fail the operation
this.logger?.logError({
userPkId: context.userPkId,
codeLocation,
message: `Session renewal failed for ${routeConfig.path}`,
error: renewResult.error,
});
}
}
return response;
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error processing request ${routeConfig.path}`,
error,
});
return createInternalErrorResponse(this.config, error);
}
}
private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {
const { context } = this.options;
// Use 401 for unauthenticated, 403 for authenticated but not allowed
const statusCode = context.isAuthenticated ? 403 : 401;
const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage: 'Access denied',
...(this.config.isDev && reason ? { errorDetail: reason } : {}),
},
};
}
}
Version 3 (latest)
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
CwcApiSuccessResponse,
RequestHandlerOptions,
OperationContext,
} from './handler.types';
import { QueryHandler } from './QueryHandler';
import { MutationHandler } from './MutationHandler';
import { createInternalErrorResponse } from './responseUtils';
import { checkRouteAccess } from '../policies';
const codeLocation = 'handlers/RequestHandler.ts';
/**
* RequestHandler - Entry point for processing API requests
*
* Responsibilities:
* 1. Check route-level access based on context role
* 2. Build operation context with path params
* 3. Delegate to QueryHandler or MutationHandler based on handlerType
* 4. Renew session for authenticated users (except on auth errors)
*/
export class RequestHandler implements CwcApiHandler {
private options: RequestHandlerOptions;
private config: CwcApiConfig;
private authClient: AuthClient;
private logger: ILogger | undefined;
constructor(
options: RequestHandlerOptions,
config: CwcApiConfig,
authClient: AuthClient,
logger: ILogger | undefined
) {
this.options = options;
this.config = config;
this.authClient = authClient;
this.logger = logger;
}
public async processRequest(): Promise<CwcApiHandlerResponse> {
const { context, routeConfig, payload, authHeader, pathParams } = this.options;
try {
// Step 1: Check route-level access (authentication only, no ownership check)
const routeAccess = checkRouteAccess(context, routeConfig.requiredRole);
if (!routeAccess.allowed) {
// No session renewal for auth errors
return this.createAccessDeniedResponse(routeAccess.reason);
}
// Step 2: Build operation context
const operationContext: OperationContext = {
context,
projectId: pathParams?.['projectId'],
resourceId: pathParams?.['resourceId'],
};
// Step 3: Delegate to appropriate handler
let response: CwcApiHandlerResponse;
if (routeConfig.handlerType === 'query') {
const queryHandler = new QueryHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.logger
);
response = await queryHandler.processRequest();
} else if (routeConfig.handlerType === 'mutation') {
const mutationHandler = new MutationHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.logger
);
response = await mutationHandler.processRequest();
} else {
// Unknown handler type - this should never happen with proper typing
// but we handle it explicitly to fail fast if configuration is wrong
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDev
? { errorDetail: `Unknown handlerType: ${routeConfig.handlerType}` }
: {}),
},
};
}
// Step 4: Renew session for authenticated users (except on auth errors)
const isAuthError = response.statusCode === 401 || response.statusCode === 403;
if (context.isAuthenticated && !isAuthError) {
const renewResult = await this.authClient.renewSession(authHeader);
if (renewResult.success && response.body.success) {
// Add JWT to successful response
(response.body as CwcApiSuccessResponse).jwt = renewResult.jwt;
} else if (!renewResult.success) {
// Log warning but don't fail the operation
this.logger?.logError({
userPkId: context.userPkId,
codeLocation,
message: `Session renewal failed for ${routeConfig.path}`,
error: renewResult.error,
});
}
}
return response;
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error processing request ${routeConfig.path}`,
error,
});
return createInternalErrorResponse(this.config, error);
}
}
private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {
const { context } = this.options;
// Use 401 for unauthenticated, 403 for authenticated but not allowed
const statusCode = context.isAuthenticated ? 403 : 401;
const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage: 'Access denied',
...(this.config.isDev && reason ? { errorDetail: reason } : {}),
},
};
}
}
packages/cwc-api/src/policies/checkOperationAccess.ts3 versions
Version 1
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext } from '../context';
import type { OperationContext } from '../handlers/handler.types';
import type { OperationAccessResult } from './policy.types';
/**
* Check if the user owns the specified project.
*
* Uses context.ownedProjects which is populated from JWT claims verified by cwc-auth.
* SECURITY: Never use projectId from request body - only from path params.
*
* @param context - The request context
* @param projectId - The project ID to check ownership for
* @returns true if the user owns the project, false otherwise
*/
export function isProjectOwner(
context: RequestContext,
projectId: string | undefined
): boolean {
if (!context.isAuthenticated) {
return false;
}
if (!projectId) {
return false;
}
return context.ownedProjects.includes(projectId);
}
/**
* Check if the current context can perform an operation based on required role.
*
* Operation-level access checks both authentication AND ownership for project-owner routes.
* This is where the actual ownership verification happens.
*
* Role hierarchy (from least to most privileged):
* - guest-user: No authentication required
* - logged-on-user: Must be authenticated
* - project-owner: Must be authenticated AND own the project
*
* @param context - The request context (authenticated or guest)
* @param operationContext - The operation context containing projectId
* @param requiredRole - The minimum role required to perform the operation
* @returns OperationAccessResult indicating whether access is allowed and the effective role
*/
export function checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult {
// guest-user: anyone can perform the operation
if (requiredRole === 'guest-user') {
return {
allowed: true,
effectiveRole: context.role,
};
}
// Must be authenticated for logged-on-user or project-owner
if (!context.isAuthenticated) {
return {
allowed: false,
reason: 'Authentication required',
};
}
// logged-on-user: authenticated is enough
if (requiredRole === 'logged-on-user') {
return {
allowed: true,
effectiveRole: context.role,
};
}
// project-owner: must own the project
const { projectId } = operationContext;
if (!isProjectOwner(context, projectId)) {
return {
allowed: false,
reason: projectId
? `User does not own project '${projectId}'`
: 'Project ID required for ownership check',
};
}
return {
allowed: true,
effectiveRole: 'project-owner',
};
}
Version 2
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext } from '../context';
import type { OperationContext } from '../handlers/handler.types';
import type { OperationAccessResult } from './policy.types';
/**
* Check if the user owns the specified project.
*
* Uses context.ownedProjects which is populated from JWT claims verified by cwc-auth.
* SECURITY: Never use projectId from request body - only from path params.
*
* @param context - The request context
* @param projectId - The project ID to check ownership for
* @returns true if the user owns the project, false otherwise
*/
export function isProjectOwner(
context: RequestContext,
projectId: string | undefined
): boolean {
if (!context.isAuthenticated) {
return false;
}
if (!projectId) {
return false;
}
return context.ownedProjects.includes(projectId);
}
/**
* Check if the current context can perform an operation based on required role.
*
* Operation-level access checks both authentication AND ownership for project-owner routes.
* This is where the actual ownership verification happens.
*
* Role hierarchy (from least to most privileged):
* - guest-user: No authentication required
* - logged-on-user: Must be authenticated
* - project-owner: Must be authenticated AND own the project
*
* @param context - The request context (authenticated or guest)
* @param operationContext - The operation context containing projectId
* @param requiredRole - The minimum role required to perform the operation
* @returns OperationAccessResult indicating whether access is allowed and the effective role
*/
export function checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult {
// guest-user: anyone can perform the operation
if (requiredRole === 'guest-user') {
return {
allowed: true,
effectiveRole: context.role,
};
}
// Must be authenticated for logged-on-user or project-owner
if (!context.isAuthenticated) {
return {
allowed: false,
reason: 'Authentication required',
};
}
// logged-on-user: authenticated is enough
if (requiredRole === 'logged-on-user') {
return {
allowed: true,
effectiveRole: context.role,
};
}
// project-owner: must own the project
const { projectId } = operationContext;
if (!isProjectOwner(context, projectId)) {
return {
allowed: false,
reason: projectId
? `User does not own project '${projectId}'`
: 'projectId is required for ownership check',
};
}
return {
allowed: true,
effectiveRole: 'project-owner',
};
}
Version 3 (latest)
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext } from '../context';
import type { OperationContext } from '../handlers/handler.types';
import type { OperationAccessResult } from './policy.types';
/**
* Check if the user owns the specified project.
*
* Uses context.ownedProjects which is populated from JWT claims verified by cwc-auth.
* SECURITY: Never use projectId from request body - only from path params.
*
* @param context - The request context
* @param projectId - The project ID to check ownership for
* @returns true if the user owns the project, false otherwise
*/
export function isProjectOwner(
context: RequestContext,
projectId: string | undefined
): boolean {
if (!context.isAuthenticated) {
return false;
}
if (!projectId) {
return false;
}
return context.ownedProjects.includes(projectId);
}
/**
* Check if the current context can perform an operation based on required role.
*
* Operation-level access checks both authentication AND ownership for project-owner routes.
* This is where the actual ownership verification happens.
*
* Role hierarchy (from least to most privileged):
* - guest-user: No authentication required
* - logged-on-user: Must be authenticated
* - project-owner: Must be authenticated AND own the project
*
* @param context - The request context (authenticated or guest)
* @param operationContext - The operation context containing projectId
* @param requiredRole - The minimum role required to perform the operation
* @returns OperationAccessResult indicating whether access is allowed and the effective role
*/
export function checkOperationAccess(
context: RequestContext,
operationContext: OperationContext,
requiredRole: CwcRole
): OperationAccessResult {
// guest-user: anyone can perform the operation
if (requiredRole === 'guest-user') {
return {
allowed: true,
effectiveRole: context.role,
};
}
// Must be authenticated for logged-on-user or project-owner
if (!context.isAuthenticated) {
return {
allowed: false,
reason: 'Authentication required',
};
}
// logged-on-user: authenticated is enough
if (requiredRole === 'logged-on-user') {
return {
allowed: true,
effectiveRole: context.role,
};
}
// project-owner: must own the project
if (requiredRole === 'project-owner') {
const { projectId } = operationContext;
if (!isProjectOwner(context, projectId)) {
return {
allowed: false,
reason: projectId
? `User does not own project '${projectId}'`
: 'projectId is required for ownership check',
};
}
return {
allowed: true,
effectiveRole: 'project-owner',
};
}
// Unknown role - fail fast if configuration is wrong
return {
allowed: false,
reason: `Unknown requiredRole: ${requiredRole}`,
};
}
packages/cwc-api/src/policies/checkRouteAccess.ts2 versions
Version 1
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext } from '../context';
import type { RouteAccessResult } from './policy.types';
/**
* Check if the current context can access a route based on required role.
*
* Route-level access only checks authentication status, not ownership.
* Ownership is verified at the operation level for 'project-owner' routes.
*
* Role hierarchy (from least to most privileged):
* - guest-user: No authentication required
* - logged-on-user: Must be authenticated
* - project-owner: Must be authenticated (ownership checked at operation level)
*
* @param context - The request context (authenticated or guest)
* @param requiredRole - The minimum role required to access the route
* @returns RouteAccessResult indicating whether access is allowed
*/
export function checkRouteAccess(
context: RequestContext,
requiredRole: CwcRole
): RouteAccessResult {
// guest-user: anyone can access
if (requiredRole === 'guest-user') {
return { allowed: true };
}
// logged-on-user or project-owner: must be authenticated
// (ownership is checked at operation level for project-owner)
if (!context.isAuthenticated) {
return {
allowed: false,
reason: 'Authentication required',
};
}
return { allowed: true };
}
Version 2 (latest)
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext } from '../context';
import type { RouteAccessResult } from './policy.types';
/**
* Check if the current context can access a route based on required role.
*
* Route-level access only checks authentication status, not ownership.
* Ownership is verified at the operation level for 'project-owner' routes.
*
* Role hierarchy (from least to most privileged):
* - guest-user: No authentication required
* - logged-on-user: Must be authenticated
* - project-owner: Must be authenticated (ownership checked at operation level)
*
* @param context - The request context (authenticated or guest)
* @param requiredRole - The minimum role required to access the route
* @returns RouteAccessResult indicating whether access is allowed
*/
export function checkRouteAccess(
context: RequestContext,
requiredRole: CwcRole
): RouteAccessResult {
// guest-user: anyone can access
if (requiredRole === 'guest-user') {
return { allowed: true };
}
// logged-on-user or project-owner: must be authenticated
// (ownership is checked at operation level for project-owner)
if (requiredRole === 'logged-on-user' || requiredRole === 'project-owner') {
if (!context.isAuthenticated) {
return {
allowed: false,
reason: 'Authentication required',
};
}
return { allowed: true };
}
// Unknown role - fail fast if configuration is wrong
return {
allowed: false,
reason: `Unknown requiredRole: ${requiredRole}`,
};
}
packages/cwc-api/src/policies/index.ts
'use strict';
/**
* Access Policy Module
*
* Provides route-level and operation-level access control for cwc-api endpoints.
*
* Usage:
* - Route-level: checkRouteAccess(context, requiredRole)
* - Operation-level: checkOperationAccess(context, operationContext, requiredRole)
*/
// Types
export type { RouteAccessResult, OperationAccessResult } from './policy.types';
// Route access
export { checkRouteAccess } from './checkRouteAccess';
// Operation access
export { checkOperationAccess, isProjectOwner } from './checkOperationAccess';
packages/cwc-api/src/policies/policy.types.ts
'use strict';
/**
* Policy Types
*
* Re-exports access result types from handler.types.ts for use in policy modules.
* This keeps policy-related types centralized for easier imports.
*/
export type { RouteAccessResult, OperationAccessResult } from '../handlers/handler.types';