It's time to continue work on the cwc-api package, let's start on phase 3 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 analyze Phase 3 requirements and explore the existing codebase to create a detailed plan. Let me start by examining what's already in place and understand the patterns we should follow.
Task
{
"subagent_type": "Explore",
"prompt": "Explore the cwc-api package to understand:\n1. Current src/ structure and what exists from Phase 1 and 2\n2. The context layer implementation (context.types.ts, createContext.ts)\n3. The Express service setup in src/index.ts\n4. How routes are currently handled\n\nAlso explore cwc-auth package for:\n1. Handler patterns used (how routes delegate to handlers)\n2. Session renewal endpoint implementation\n3. Response formatting patterns\n\nFinally, look at cwc-backend-utils for:\n1. AuthClient implementation\n2. Any shared response types or patterns\n\nFocus on understanding the architectural patterns already established.",
"description": "Explore cwc-api and related packages"
}Tool Result
Perfect! Now let me compile a comprehensive exploration report. I have all the necessary information about the architectural patterns.
## Comprehensive Exploration Report
I've systematically examined the **cwc-api**, **cwc-auth**, and **cwc-backend-utils** packages to understand the established architectural patterns. Here's my detailed findings:
---
## 1. CWC-API Current Structure
### File Organization
**Location:** `/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src`
**Current structure:**
```
src/
├── index.ts # Express service entry point
├── config/
│ ├── index.ts
│ ├── config.types.ts
│ └── loadConfig.ts
├── context/
│ ├── index.ts
│ ├── context.types.ts # RequestContext types
│ └── createContext.ts # JWT verification & context creation
└── __tests__/
├── config.test.ts
├── healthCheck.test.ts
├── context/
│ └── createContext.test.ts
└── mocks/
├── authClient.mock.ts
├── sqlClient.mock.ts
├── config.mock.ts
└── logger.mock.ts
```
### Context Layer (Phase 2 - Complete)
**Location:** `src/context/`
The context layer provides centralized authentication without duplicating JWT verification logic:
1. **context.types.ts** defines:
```typescript
type AuthenticatedContext = {
isAuthenticated: true;
role: Exclude<CwcRole, 'guest-user'>; // 'project-owner' | 'logged-on-user'
userPkId: number;
username: string;
ownedProjects: string[];
payload: UserJwtPayload; // Full JWT payload
};
type GuestContext = {
isAuthenticated: false;
role: 'guest-user';
};
type RequestContext = AuthenticatedContext | GuestContext; // Discriminated union
```
2. **createContext.ts** implements:
- Takes `authHeader` and `authClient` as inputs
- Calls `authClient.verifyToken(authHeader)` to validate JWT via cwc-auth service
- Returns `AuthenticatedContext` on success, `GuestContext` on failure
- **Graceful degradation:** No JWT, invalid token, expired token, or auth service error all result in guest context (logged but no error thrown)
- Uses cwc-auth's `/verify-token` endpoint (internal service endpoint)
### Express Service Setup (Phase 1 - Complete)
**Location:** `src/index.ts`
The main entry point demonstrates the standard service pattern:
```typescript
// 1. Load environment variables with loadDotEnv utility
loadDotEnv({
serviceName: 'cwc-api',
environment: process.env['RUNTIME_ENVIRONMENT'] as RuntimeEnvironment,
debug: process.env['DEBUG_MODE'] === 'ON',
});
// 2. Load & validate configuration
const config = loadConfig();
// 3. Create BackendUtilsConfig adapter
const backendConfig = createBackendUtilsConfig(config);
// 4. Create Logger (database-backed for errors)
const logger = new Logger({ config: backendConfig, serviceName: 'cwc-api' });
// 5. Create API instances (health check + CwcApiV1 when implemented)
const healthApi: ExpressApi = {
version: 1,
path: '/health/v1',
handler: healthHandler,
};
const apis: ExpressApi[] = [healthApi];
// 6. Create Express service with middleware
const service = createExpressService({
config: backendConfig,
serviceName: 'cwc-api',
apis,
allowGet: false,
allowOptions: true,
allowPost: true,
payloadLimit: undefined,
});
// 7. Start service with graceful shutdown handlers
service.start(apis);
```
**Note:** SqlClient and AuthClient creation are commented out until Phase 3 (Handler Architecture) is implemented.
---
## 2. CWC-AUTH Handler Patterns (Reference Implementation)
### Architecture Overview
CWC-auth provides a complete, production-grade handler pattern that cwc-api should follow for Phase 3+.
**File Structure:**
```
src/apis/
├── LogonApiV1/ # Public endpoints: logon, logoff
│ ├── index.ts # ExpressApi implementation
│ ├── LogonHandler.ts # Handler class (orchestration)
│ ├── routes.ts # Route configuration factory
│ ├── types.ts # Types for this API version
│ └── workers/
│ ├── logonWorker.ts
│ └── logoffWorker.ts
└── ServiceApiV1/ # Internal service endpoints: renew-session
├── index.ts # ExpressApi implementation
├── ServiceHandler.ts # Handler class
├── routes.ts # Route configuration factory
├── types.ts # Types for this API version
└── workers/
└── renewSessionWorker.ts
```
### Handler Pattern - Three-Layer Delegation
**Layer 1: ExpressApi Class (index.ts)**
- Implements `ExpressApi` interface with `path`, `version`, and `handler()` method
- Receives raw Express `Request` and `Response`
- Validates HTTP method and route existence
- Extracts headers/body from request
- Creates handler instance and orchestrates response
```typescript
export class LogonApiV1 implements ExpressApi {
public get path(): string { return '/auth/v1'; }
public get version(): number { return 1; }
public async handler(req: Request, res: Response, _next: NextFunction): Promise<void> {
// 1. Validate route exists
const routeConfig = this.routes[req.path];
if (!routeConfig) {
res.status(404).json({ ... });
return;
}
// 2. Validate HTTP method
if (req.method !== routeConfig.method) {
res.status(404).json({ ... });
return;
}
// 3. Extract request data
const authHeader = req.headers['authorization'] as string | undefined;
const payload: AuthRequestPayload = req.body ?? {};
// 4. Create handler and execute
const handler = new LogonHandler(
{ payload, authHeader, routeConfig },
this.config,
this.logger
);
const response = await handler.processRequest();
// 5. Send response
res.status(response.statusCode).json({
data: response.data,
jwt: response.jwt,
});
}
}
```
**Layer 2: Handler Class (LogonHandler.ts)**
- Encapsulates a single route's business logic
- Receives request details in constructor
- Implements `AuthRouteHandler` interface with `processRequest()` method
- Delegates actual work to worker function
- Transforms worker result to standard response format
- Handles errors with appropriate HTTP status codes
```typescript
export class LogonHandler implements AuthRouteHandler {
constructor(
options: AuthRouteHandlerOptions, // payload, authHeader, routeConfig
config: CwcAuthConfig,
logger: ILogger | undefined
) { ... }
public async processRequest(): Promise<AuthRouteHandlerResponse> {
try {
const result = await this.routeConfig.worker(options);
if (result.success) {
return {
statusCode: 200,
data: { success: true, jwtType: 'user', ... },
jwt: result.jwt,
};
}
// Error handling with dev-only details
return {
statusCode: 401,
data: {
success: false,
jwtType: undefined,
...(this.config.isDevelopment && {
errorCode: result.errorCode,
errorDetail: result.errorDetail,
}),
},
jwt: undefined,
};
} catch (error) {
this.logger?.logError({ ... });
return { statusCode: 401, data: { success: false, ... }, jwt: undefined };
}
}
}
```
**Layer 3: Worker Function (logonWorker.ts)**
- Pure business logic function
- Receives injected dependencies (sqlClient, config, logger)
- Returns success/failure result (never throws)
- Composed from reusable auth functions (createAuthenticateUser, etc.)
```typescript
export function createLogonWorker(deps: AuthDependencies) {
const authenticateUser = createAuthenticateUser(deps);
return async function logonWorker(
options: AuthFunctionOptions
): Promise<AuthFunctionResult> {
const result = await authenticateUser({
username: options.payload['username'] as string,
password: options.payload['password'] as string,
kulo: options.payload['kulo'] as boolean | undefined,
});
if (result.success) {
return { success: true, jwt: result.jwt, userPkId: result.userPkId, ... };
}
return { success: false, errorCode: result.errorCode, errorDetail: result.errorDetail };
};
}
```
### Route Configuration Pattern
Routes are centralized in `routes.ts` as a factory function:
```typescript
export const getRoutes = (deps: AuthDependencies): AuthRouteConfigs => {
const logonWorker = createLogonWorker(deps);
const logoffWorker = createLogoffWorker(deps);
return {
'/logon': {
handlerType: 'logon',
path: '/logon',
method: 'POST',
requiresAuth: false,
worker: logonWorker,
},
'/logoff': {
handlerType: 'logoff',
path: '/logoff',
method: 'POST',
requiresAuth: true,
worker: logoffWorker,
},
};
};
```
### Response Formatting Pattern
**LogonApiV1 Response:**
```typescript
type AuthRouteHandlerResponse = {
statusCode: 200 | 401;
data: {
success: boolean;
loggedOff?: boolean;
jwtType: AuthJwtType; // 'user' | 'temp' | undefined
errorCode?: string; // Dev-only
errorDetail?: string; // Dev-only
};
jwt: string | undefined;
};
```
**ServiceApiV1 Response (for renew-session):**
```typescript
type ServiceRouteHandlerResponse = {
statusCode: 200 | 401 | 403;
data: {
success: boolean;
jwtType: AuthJwtType;
payload?: UserJwtPayload; // Optionally include updated claims
errorCode?: string; // Dev-only
errorDetail?: string; // Dev-only
};
jwt: string | undefined;
};
```
### Session Renewal Pattern
The `ServiceApiV1` demonstrates internal service-to-service pattern:
- Protected by Docker network isolation + JWT validation
- `renewSessionWorker` re-queries database for fresh `ownedProjects`
- Returns new JWT with updated claims
- Used by cwc-api after create/delete project operations
---
## 3. CWC-Backend-Utils Shared Components
### AuthClient Pattern
**Location:** `packages/cwc-backend-utils/src/AuthClient/AuthClient.ts`
Provides HTTP client for cwc-auth service, following same pattern as SqlClient:
```typescript
export class AuthClient {
async verifyToken(authHeader: string | undefined): Promise<VerifyTokenResult> {
if (!authHeader) return { success: false, error: 'MISSING_TOKEN' };
try {
const response = await axios.post<VerifyTokenResponse>(
`${this.config.authUri}/verify-token`,
{},
{ headers: { Authorization: authHeader }, timeout: 5000 }
);
if (response.data.success) {
return { success: true, payload: response.data.payload };
}
return { success: false, error: response.data.errorCode ?? 'UNKNOWN_ERROR' };
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
return { success: false, error: 'INVALID_TOKEN' };
}
this.logger?.logError({ ... });
return { success: false, error: 'AUTH_SERVICE_ERROR' };
}
}
}
```
**Key characteristics:**
- Graceful error handling (returns typed result, never throws)
- Logs unexpected network errors only (not auth failures)
- Follows same config + logger + clientName pattern as SqlClient
### Express Service Factory Pattern
**From cwc-backend-utils:**
- Applied consistently across cwc-auth and cwc-api
- Automatically applies middleware: rate limiting, Helmet, CORS, invalid routes
- Takes `ExpressApi[]` array for modular endpoint registration
### Configuration Adapter Pattern
Both services use config adapter function to convert service-specific config to `BackendUtilsConfig`:
```typescript
function createBackendUtilsConfig(apiConfig: CwcApiConfig): BackendUtilsConfig {
return {
debugMode: apiConfig.debugMode,
dataUri: apiConfig.dataUri,
logErrorsToDatabase: apiConfig.logErrorsToDatabase,
// ... rest of config fields
};
}
```
---
## 4. Type System and Response Patterns
### Role Types (from cwc-types)
```typescript
type CwcRole = 'project-owner' | 'logged-on-user' | 'guest-user';
```
**Usage in context:**
- `project-owner`: User owns the specific project/content being accessed
- `logged-on-user`: Authenticated user but not necessarily owner
- `guest-user`: Unauthenticated request (read-only access)
### JWT Payload Structure
```typescript
type UserJwtPayload = {
jti: string; // userJwtId (UUID)
sub: number; // userPkId
iat: number; // Issued at (Unix timestamp)
exp: number; // Expiration (Unix timestamp)
login: CwcLoginClaims; // Custom claims
};
type CwcLoginClaims = {
username: string;
deviceId: string;
userJwtId: string;
loginType: 'cwc' | 'facebook' | 'google';
kulo: boolean; // Keep-user-logged-on flag
ownedProjects: string[]; // Array of projectId (natural keys)
isGuestUser: boolean; // Always false in JWT
};
```
### Verify Token Response Types (from cwc-types)
```typescript
type VerifyTokenSuccessResponse = {
success: true;
jwtType: undefined;
payload: UserJwtPayload;
};
type VerifyTokenErrorResponse = {
success: false;
jwtType: undefined;
errorCode?: string; // Dev-only
errorDetail?: string; // Dev-only
};
type VerifyTokenResult =
| { success: true; payload: UserJwtPayload }
| { success: false; error: string };
```
---
## 5. Critical Patterns & Rules Summary
### Phase 1-2 Complete ✓
- Express service setup with graceful shutdown
- Config loading and validation
- Context layer with graceful degradation
- Health check endpoint
### Phase 3 Ready (Handler Architecture)
When implementing cwc-api handlers following auth pattern:
**Must follow:**
1. Three-layer delegation: ExpressApi → Handler → Worker
2. Route factory function returning route configs
3. Handler class implementing route handler interface
4. Worker functions with injected dependencies
5. Graceful error handling (log + return error result, never throw)
6. Dev-only error details (errorCode, errorDetail only in isDevelopment mode)
7. Type-safe response envelopes
8. Standard HTTP status codes (200, 401, 403, 404)
**Key differences for cwc-api:**
- Must create SqlClient in index.ts (commented out for Phase 2)
- Must create AuthClient for JWT verification
- Response structure will include `data` + `pagination` metadata (offset-based)
- Will implement QueryHandler (read) and MutationHandler (write) in addition to RouteHandler
### Configuration
**cwc-api required environment variables:**
```bash
RUNTIME_ENVIRONMENT=dev|test|prod
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|ON
LOG_ERRORS_TO_DATABASE=ON|OFF
USER_JWT_SECRET=...
SQL_CLIENT_API_KEY=...
```
### Testing Infrastructure
- Mock infrastructure in `src/__tests__/mocks/`
- Mock functions for SqlClient, AuthClient, Logger, Config
- Test config loading from `unit.cwc-api.env`
- Setup file loads environment via `loadDotEnv`
---
## Key Files Reference
**cwc-api:**
- `/packages/cwc-api/src/index.ts` - Express service entry point
- `/packages/cwc-api/src/context/context.types.ts` - Context type definitions
- `/packages/cwc-api/src/context/createContext.ts` - JWT verification logic
- `/packages/cwc-api/CLAUDE.md` - Package documentation
**cwc-auth (reference):**
- `/packages/cwc-auth/src/index.ts` - Express service entry point with SqlClient creation
- `/packages/cwc-auth/src/apis/LogonApiV1/index.ts` - ExpressApi implementation
- `/packages/cwc-auth/src/apis/LogonApiV1/LogonHandler.ts` - Handler pattern
- `/packages/cwc-auth/src/apis/LogonApiV1/routes.ts` - Route factory pattern
- `/packages/cwc-auth/src/apis/LogonApiV1/workers/logonWorker.ts` - Worker pattern
- `/packages/cwc-auth/CLAUDE.md` - Complete auth architecture
**cwc-backend-utils:**
- `/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts` - JWT verification client
- `/packages/cwc-backend-utils/CLAUDE.md` - Shared utilities documentation
**cwc-types:**
- `/packages/cwc-types/src/authTypes.ts` - Auth type definitions
- `/packages/cwc-types/CLAUDE.md` - Type system documentation
agentId: a026c7c (for resuming to continue this agent's work if needed)Excellent! The exploration gives me a clear picture of the architecture. Let me now create a detailed plan for Phase 3 based on the existing patterns and the checklist requirements.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-auth/src/apis/LogonApiV1/types.ts"
}Tool Result
1→import type { AuthJwtType, AuthErrorCode } from '../../auth';
2→
3→/**
4→ * Request payload for auth endpoints
5→ */
6→export type AuthRequestPayload = {
7→ [key: string]: unknown;
8→};
9→
10→/**
11→ * Response data structure for auth API
12→ */
13→export type AuthRouteHandlerResponseData = {
14→ success: boolean;
15→ loggedOff?: boolean | undefined;
16→ jwtType: AuthJwtType;
17→ // Dev-only error details
18→ errorCode?: AuthErrorCode | undefined;
19→ errorDetail?: string | undefined;
20→};
21→
22→/**
23→ * Full response structure for auth endpoints
24→ */
25→export type AuthRouteHandlerResponse = {
26→ statusCode: 200 | 401;
27→ data: AuthRouteHandlerResponseData;
28→ jwt: string | undefined;
29→};
30→
31→/**
32→ * Handler types for different auth operations
33→ */
34→export type AuthHandlerType = 'logon' | 'logoff';
35→
36→/**
37→ * Auth function result type
38→ */
39→export type AuthFunctionResult = {
40→ success: boolean;
41→ jwt?: string | undefined;
42→ errorCode?: AuthErrorCode | undefined;
43→ errorDetail?: string | undefined;
44→ userPkId?: number | undefined;
45→ username?: string | undefined;
46→};
47→
48→/**
49→ * Auth function options
50→ */
51→export type AuthFunctionOptions = {
52→ payload: AuthRequestPayload;
53→ authHeader?: string | undefined;
54→};
55→
56→/**
57→ * Auth worker function type
58→ */
59→export type AuthFunction = (
60→ options: AuthFunctionOptions
61→) => Promise<AuthFunctionResult>;
62→
63→/**
64→ * Route configuration for auth endpoints
65→ */
66→export type AuthRouteConfig = {
67→ handlerType: AuthHandlerType;
68→ path: string;
69→ worker: AuthFunction;
70→ method: 'POST';
71→ requiresAuth: boolean;
72→ debug?: boolean;
73→};
74→
75→/**
76→ * Map of route paths to configurations
77→ */
78→export type AuthRouteConfigs = {
79→ [key: string]: AuthRouteConfig;
80→};
81→
82→/**
83→ * Options for creating auth handlers
84→ */
85→export type AuthRouteHandlerOptions = {
86→ payload: AuthRequestPayload;
87→ authHeader: string | undefined;
88→ routeConfig: AuthRouteConfig;
89→};
90→
91→/**
92→ * Auth route handler interface
93→ */
94→export interface AuthRouteHandler {
95→ processRequest: () => Promise<AuthRouteHandlerResponse>;
96→}
97→
<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/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>
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-types/src/authTypes.ts"
}Tool Result
1→/**
2→ * Auth Types - Shared authentication types for CWC services
3→ *
4→ * These types are used by cwc-auth (JWT creation) and consuming services
5→ * (JWT verification via AuthClient).
6→ */
7→
8→import type { CwcLoginClaims } from './entityTypes';
9→
10→/**
11→ * UserJwtPayload structure - decoded JWT payload
12→ * Used by cwc-auth (creation) and consuming services (verification)
13→ */
14→export type UserJwtPayload = {
15→ jti: string; // userJwtId (UUID) - references userJwt table
16→ sub: number; // userPkId
17→ iat: number; // Issued at (Unix timestamp)
18→ exp: number; // Expiration (Unix timestamp)
19→ login: CwcLoginClaims;
20→};
21→
22→/**
23→ * Roles for access control across CWC services
24→ */
25→export type CwcRole = 'project-owner' | 'logged-on-user' | 'guest-user';
26→
27→/**
28→ * Success response from cwc-auth /verify-token
29→ */
30→export type VerifyTokenSuccessResponse = {
31→ success: true;
32→ jwtType: undefined;
33→ payload: UserJwtPayload;
34→};
35→
36→/**
37→ * Error response from cwc-auth /verify-token
38→ */
39→export type VerifyTokenErrorResponse = {
40→ success: false;
41→ jwtType: undefined;
42→ errorCode?: string | undefined; // Dev-only
43→ errorDetail?: string | undefined; // Dev-only
44→};
45→
46→/**
47→ * Union of verify-token responses
48→ */
49→export type VerifyTokenResponse = VerifyTokenSuccessResponse | VerifyTokenErrorResponse;
50→
51→/**
52→ * Result from AuthClient.verifyToken()
53→ */
54→export type VerifyTokenResult =
55→ | { success: true; payload: UserJwtPayload }
56→ | { success: false; error: string };
57→
<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 check if AuthClient already has a renewSession method, which is needed for mutation handlers.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts"
}Tool Result
1→import axios from 'axios';
2→import type { ILogger } from '../backendUtils.types';
3→import type { VerifyTokenResponse, VerifyTokenResult, VerifyTokenErrorResponse } from 'cwc-types';
4→
5→const codeLocation = 'AuthClient/AuthClient.ts';
6→
7→export type AuthClientConfig = {
8→ authUri: string;
9→ timeout?: number | undefined;
10→};
11→
12→export type AuthClientOptions = {
13→ config: AuthClientConfig;
14→ logger: ILogger | undefined;
15→ clientName: string;
16→};
17→
18→/**
19→ * HTTP client for cwc-auth service
20→ * Similar pattern to SqlClient for cwc-sql
21→ */
22→export class AuthClient {
23→ private config: AuthClientConfig;
24→ private logger: ILogger | undefined;
25→ private clientName: string;
26→
27→ constructor(options: AuthClientOptions) {
28→ this.config = options.config;
29→ this.logger = options.logger;
30→ this.clientName = options.clientName;
31→ }
32→
33→ /**
34→ * Verify a JWT by calling cwc-auth /verify-token
35→ */
36→ async verifyToken(authHeader: string | undefined): Promise<VerifyTokenResult> {
37→ if (!authHeader) {
38→ return { success: false, error: 'MISSING_TOKEN' };
39→ }
40→
41→ try {
42→ const response = await axios.post<VerifyTokenResponse>(
43→ `${this.config.authUri}/verify-token`,
44→ {},
45→ {
46→ headers: { Authorization: authHeader },
47→ timeout: this.config.timeout ?? 5000,
48→ }
49→ );
50→
51→ const data = response.data;
52→
53→ if (data.success) {
54→ return { success: true, payload: data.payload };
55→ }
56→
57→ // data.success is false, so it's a VerifyTokenErrorResponse
58→ return { success: false, error: data.errorCode ?? 'UNKNOWN_ERROR' };
59→ } catch (error) {
60→ // Handle 401 responses (expected for invalid tokens)
61→ if (axios.isAxiosError(error) && error.response?.status === 401) {
62→ const errorData = error.response.data as VerifyTokenErrorResponse;
63→ return { success: false, error: errorData.errorCode ?? 'INVALID_TOKEN' };
64→ }
65→
66→ // Log unexpected errors
67→ this.logger?.logError({
68→ userPkId: undefined,
69→ codeLocation,
70→ message: `[${this.clientName}] Failed to verify token with auth service`,
71→ error,
72→ });
73→
74→ return { success: false, error: 'AUTH_SERVICE_ERROR' };
75→ }
76→ }
77→}
78→
<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-auth/src/apis/ServiceApiV1/workers/renewSessionWorker.ts"
}Tool Result
1→'use strict';
2→
3→import type { AuthDependencies } from '../../../auth';
4→import type { ServiceFunctionOptions, ServiceFunctionResult } from '../types';
5→import { createRenewSession } from '../../../auth';
6→
7→/**
8→ * Creates the renew session worker function with injected dependencies
9→ *
10→ * @param deps - Auth dependencies (sqlClient, config, logger)
11→ * @returns Worker function that renews a user session
12→ */
13→export function createRenewSessionWorker(deps: AuthDependencies) {
14→ const renewSession = createRenewSession(deps);
15→
16→ return async function renewSessionWorker(
17→ options: ServiceFunctionOptions
18→ ): Promise<ServiceFunctionResult> {
19→ const { authHeader } = options;
20→ const result = await renewSession(authHeader);
21→
22→ if (result.success) {
23→ return {
24→ success: true,
25→ jwt: result.jwt,
26→ };
27→ }
28→
29→ return {
30→ success: false,
31→ errorCode: result.errorCode,
32→ errorDetail: result.errorDetail,
33→ };
34→ };
35→}
36→
<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 that AuthClient currently only has verifyToken(). Let me check the types used by ServiceApiV1 for renew-session.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-auth/src/apis/ServiceApiV1/types.ts"
}Tool Result
1→import type { AuthJwtType, AuthErrorCode } from '../../auth';
2→import type { UserJwtPayload } from '../../jwt';
3→
4→/**
5→ * Request payload for service endpoints
6→ */
7→export type ServiceRequestPayload = {
8→ [key: string]: unknown;
9→};
10→
11→/**
12→ * Response data structure for service API
13→ */
14→export type ServiceRouteHandlerResponseData = {
15→ success: boolean;
16→ jwtType: AuthJwtType;
17→ payload?: UserJwtPayload | undefined;
18→ // Dev-only error details
19→ errorCode?: AuthErrorCode | undefined;
20→ errorDetail?: string | undefined;
21→};
22→
23→/**
24→ * Full response structure for service endpoints
25→ */
26→export type ServiceRouteHandlerResponse = {
27→ statusCode: 200 | 401 | 403;
28→ data: ServiceRouteHandlerResponseData;
29→ jwt: string | undefined;
30→};
31→
32→/**
33→ * Handler types for service operations
34→ */
35→export type ServiceHandlerType = 'renew-session' | 'verify-token';
36→
37→/**
38→ * Service function result type
39→ */
40→export type ServiceFunctionResult = {
41→ success: boolean;
42→ jwt?: string | undefined;
43→ payload?: UserJwtPayload | undefined;
44→ errorCode?: AuthErrorCode | undefined;
45→ errorDetail?: string | undefined;
46→};
47→
48→/**
49→ * Service function options
50→ */
51→export type ServiceFunctionOptions = {
52→ authHeader?: string | undefined;
53→};
54→
55→/**
56→ * Service worker function type
57→ */
58→export type ServiceFunction = (
59→ options: ServiceFunctionOptions
60→) => Promise<ServiceFunctionResult>;
61→
62→/**
63→ * Route configuration for service endpoints
64→ */
65→export type ServiceRouteConfig = {
66→ handlerType: ServiceHandlerType;
67→ path: string;
68→ worker: ServiceFunction;
69→ method: 'POST';
70→ debug?: boolean;
71→};
72→
73→/**
74→ * Map of route paths to configurations
75→ */
76→export type ServiceRouteConfigs = {
77→ [key: string]: ServiceRouteConfig;
78→};
79→
80→/**
81→ * Options for creating service handlers
82→ */
83→export type ServiceRouteHandlerOptions = {
84→ authHeader: string | undefined;
85→ routeConfig: ServiceRouteConfig;
86→};
87→
88→/**
89→ * Service route handler interface
90→ */
91→export interface ServiceRouteHandler {
92→ processRequest: () => Promise<ServiceRouteHandlerResponse>;
93→}
94→
<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-auth/src/apis/ServiceApiV1/index.ts"
}Tool Result
1→'use strict';
2→
3→import { NextFunction, Request, Response } from 'express';
4→import type { ExpressApi, ILogger, SqlClient } from 'cwc-backend-utils';
5→import type { CwcAuthConfig } from '../../config';
6→import type {
7→ ServiceRouteConfigs,
8→ ServiceRouteHandlerOptions,
9→ ServiceRouteHandlerResponse,
10→} from './types';
11→import { getRoutes } from './routes';
12→import { ServiceHandler } from './ServiceHandler';
13→
14→const codeLocation = 'apis/ServiceApiV1/index.ts';
15→
16→/**
17→ * ServiceApiV1 - Handles /auth/v1/renew-session route
18→ * For internal service-to-service calls (protected by Docker network isolation + JWT)
19→ */
20→export class ServiceApiV1 implements ExpressApi {
21→ private routes: ServiceRouteConfigs;
22→ private config: CwcAuthConfig;
23→ private logger: ILogger | undefined;
24→
25→ constructor(
26→ config: CwcAuthConfig,
27→ sqlClient: SqlClient,
28→ logger: ILogger | undefined
29→ ) {
30→ this.config = config;
31→ this.logger = logger;
32→ this.routes = getRoutes({ sqlClient, config, logger });
33→ }
34→
35→ public get path(): string {
36→ return '/auth/v1';
37→ }
38→
39→ public get version(): number {
40→ return 1;
41→ }
42→
43→ // NOTE: HTTPS enforcement should be handled at nginx/reverse proxy level
44→ // NOTE: Service access is protected by Docker network isolation + JWT validation
45→
46→ /**
47→ * Main request handler
48→ */
49→ public async handler(req: Request, res: Response, next: NextFunction): Promise<void> {
50→ const logError = (message: string, error: unknown, value: unknown = undefined): void => {
51→ this.logger?.logError({
52→ userPkId: undefined,
53→ codeLocation,
54→ message,
55→ error,
56→ value,
57→ });
58→ };
59→
60→ try {
61→ if (res.statusCode !== 200 || res.writableEnded) {
62→ return;
63→ }
64→
65→ const { path, method } = req;
66→
67→ // Validate path exists
68→ if (!path || path === '/') {
69→ res.status(404).json({
70→ data: { success: false, jwtType: undefined },
71→ jwt: undefined,
72→ });
73→ return;
74→ }
75→
76→ // Find route config
77→ const routeConfig = this.routes[path];
78→ if (!routeConfig) {
79→ // This route is not handled by ServiceApiV1, let it pass through
80→ next();
81→ return;
82→ }
83→
84→ // Validate HTTP method
85→ if (method !== routeConfig.method) {
86→ res.status(404).json({
87→ data: { success: false, jwtType: undefined },
88→ jwt: undefined,
89→ });
90→ return;
91→ }
92→
93→ // Get auth header
94→ const authHeader = req.headers['authorization'] as string | undefined;
95→
96→ // Create handler options
97→ const handlerOptions: ServiceRouteHandlerOptions = {
98→ authHeader,
99→ routeConfig,
100→ };
101→
102→ // Create and execute handler
103→ const handler = new ServiceHandler(handlerOptions, this.config, this.logger);
104→ const response: ServiceRouteHandlerResponse = await handler.processRequest();
105→
106→ // Send response
107→ res.status(response.statusCode).json({
108→ data: response.data,
109→ jwt: response.jwt,
110→ });
111→ } catch (error) {
112→ logError('ServiceApiV1.handler - ERROR', error);
113→ res.status(401).json({
114→ data: { success: false, jwtType: undefined },
115→ jwt: undefined,
116→ });
117→ }
118→ }
119→}
120→
<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>
Now I have a comprehensive understanding. Let me design the plan for Phase 3 by launching a Plan agent.
Task
{
"subagent_type": "Plan",
"prompt": "Design the Phase 3 Handler Architecture for cwc-api package.\n\n## Context from Exploration\n\n**Current State (Phase 1 & 2 Complete):**\n- Express service running with health endpoint at `/health/v1`\n- Context layer complete: `src/context/createContext.ts` creates `AuthenticatedContext` or `GuestContext`\n- Uses `AuthClient.verifyToken()` from cwc-backend-utils to validate JWTs via cwc-auth\n\n**Reference Implementation (cwc-auth):**\n- 3-layer pattern: ExpressApi (index.ts) → Handler class → Worker function\n- ExpressApi validates route/method, extracts headers, creates handler, sends response\n- Handler class implements `processRequest()` method, delegates to worker, formats response\n- Worker function contains business logic with injected dependencies\n- Types defined in `types.ts`, routes in `routes.ts` as factory function\n\n**Key Files:**\n- cwc-auth ExpressApi: `/packages/cwc-auth/src/apis/LogonApiV1/index.ts`\n- cwc-auth Handler: `/packages/cwc-auth/src/apis/LogonApiV1/LogonHandler.ts`\n- cwc-auth Types: `/packages/cwc-auth/src/apis/LogonApiV1/types.ts`\n- cwc-auth Routes: `/packages/cwc-auth/src/apis/LogonApiV1/routes.ts`\n- cwc-api Context: `/packages/cwc-api/src/context/context.types.ts`\n- AuthClient: `/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts`\n\n**Phase 3 Checklist Items:**\n1. Create `src/handlers/handler.types.ts` - route/query/mutation handler types\n2. Create `src/handlers/RouteHandler.ts` - Extract JWT, create context, check route access, delegate\n3. Create `src/handlers/QueryHandler.ts` - Operation access check, execute query, format response\n4. Create `src/handlers/MutationHandler.ts` - Operation access check, execute mutation, renew session\n5. Create `src/handlers/index.ts` - Exports\n\n## Identified Prerequisites\n\n**CRITICAL PREREQUISITE - AuthClient.renewSession():**\n- Current AuthClient only has `verifyToken()` method\n- MutationHandler needs `renewSession()` to call cwc-auth `/renew-session` after project create/delete\n- Requires:\n 1. Add `RenewSessionResult` type to cwc-types/src/authTypes.ts\n 2. Add `renewSession()` method to cwc-backend-utils AuthClient\n\n**Response Types:**\n- cwc-api needs standardized response envelope: `{ success, data?, pagination?, errorCode?, errorMessage? }`\n- Different from cwc-auth which uses `{ data, jwt }` envelope\n\n## Architecture Decisions to Consider\n\n1. **Where do access policy checks happen?**\n - Phase 4 implements the actual policies\n - Phase 3 should define interfaces/types that Phase 4 implements\n - RouteHandler calls `checkRouteAccess(context, routeConfig)` - returns allow/deny\n - Query/MutationHandler calls `checkOperationAccess(context, operation, resourceId)` - returns allow/deny\n\n2. **How do handlers integrate with CwcApiV1 (Phase 6)?**\n - CwcApiV1 will be the ExpressApi class (like LogonApiV1)\n - RouteHandler receives request details and orchestrates the flow\n - Operations will be registered in routes.ts as factory function\n\n3. **Session renewal trigger:**\n - Only after project create/delete operations (changes ownedProjects claim)\n - MutationHandler checks if operation requires session renewal\n - Calls `authClient.renewSession(authHeader)` and includes new JWT in response\n\n## Required Deliverables\n\nPlease design:\n1. Complete type definitions for handler.types.ts\n2. RouteHandler class structure and processRequest flow\n3. QueryHandler class structure \n4. MutationHandler class structure with session renewal logic\n5. How to integrate with existing createContext()\n6. Interface definitions for access policy checks (stubs for Phase 4)\n7. Response formatting patterns\n8. Test strategy for unit tests\n\nConsider:\n- Following cwc-auth patterns where applicable\n- Defining interfaces that Phase 4-6 will implement\n- Error handling patterns (dev-only error details)\n- Type safety throughout\n",
"description": "Design Phase 3 handler architecture"
}Tool Result
Now I have a comprehensive understanding of the codebase. Let me present my design for the Phase 3 Handler Architecture.
---
# Phase 3 Handler Architecture Design for cwc-api
## Overview
This design document outlines the handler architecture for cwc-api Phase 3. The architecture follows the established 3-layer pattern from cwc-auth (ExpressApi -> Handler -> Worker) while adapting it for cwc-api's specific needs: role-based access control, query/mutation separation, and session renewal for ownership-changing operations.
---
## 1. Prerequisites (Before Phase 3 Implementation)
### 1.1 Add RenewSessionResult Type to cwc-types
**File:** `/packages/cwc-types/src/authTypes.ts`
```typescript
/**
* Success response from cwc-auth /renew-session
*/
export type RenewSessionSuccessResponse = {
success: true;
jwtType: 'user';
jwt: string;
};
/**
* Error response from cwc-auth /renew-session
*/
export type RenewSessionErrorResponse = {
success: false;
jwtType: undefined;
errorCode?: string | undefined; // Dev-only
errorDetail?: string | undefined; // Dev-only
};
/**
* Union of renew-session responses
*/
export type RenewSessionResponse = RenewSessionSuccessResponse | RenewSessionErrorResponse;
/**
* Result from AuthClient.renewSession()
*/
export type RenewSessionResult =
| { success: true; jwt: string }
| { success: false; error: string };
```
### 1.2 Add renewSession Method to AuthClient
**File:** `/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts`
Add the `renewSession()` method following the same pattern as `verifyToken()`:
```typescript
/**
* Renew a session by calling cwc-auth /renew-session
* Used after project create/delete to update ownedProjects claim
*/
async renewSession(authHeader: string | undefined): Promise<RenewSessionResult> {
if (!authHeader) {
return { success: false, error: 'MISSING_TOKEN' };
}
try {
const response = await axios.post<RenewSessionResponse>(
`${this.config.authUri}/renew-session`,
{},
{
headers: { Authorization: authHeader },
timeout: this.config.timeout ?? 5000,
}
);
const data = response.data;
if (data.success) {
return { success: true, jwt: data.jwt };
}
return { success: false, error: data.errorCode ?? 'UNKNOWN_ERROR' };
} catch (error) {
// Handle 401 responses
if (axios.isAxiosError(error) && error.response?.status === 401) {
const errorData = error.response.data as RenewSessionErrorResponse;
return { success: false, error: errorData.errorCode ?? 'RENEWAL_FAILED' };
}
this.logger?.logError({
userPkId: undefined,
codeLocation,
message: `[${this.clientName}] Failed to renew session with auth service`,
error,
});
return { success: false, error: 'AUTH_SERVICE_ERROR' };
}
}
```
### 1.3 Update Mock AuthClient
**File:** `/packages/cwc-api/src/__tests__/mocks/authClient.mock.ts`
Add mock helpers for renewSession:
```typescript
export function createMockAuthClient(): jest.Mocked<AuthClient> {
return {
verifyToken: jest.fn<(authHeader: string | undefined) => Promise<VerifyTokenResult>>(),
renewSession: jest.fn<(authHeader: string | undefined) => Promise<RenewSessionResult>>(),
} as unknown as jest.Mocked<AuthClient>;
}
export function mockRenewSessionSuccess(
mockAuthClient: jest.Mocked<AuthClient>,
jwt: string = 'new-jwt-token'
): void {
mockAuthClient.renewSession.mockResolvedValueOnce({
success: true,
jwt,
});
}
export function mockRenewSessionFailure(
mockAuthClient: jest.Mocked<AuthClient>,
error: string = 'RENEWAL_FAILED'
): void {
mockAuthClient.renewSession.mockResolvedValueOnce({
success: false,
error,
});
}
```
---
## 2. Handler Type Definitions
**File:** `/packages/cwc-api/src/handlers/handler.types.ts`
```typescript
'use strict';
import type { CwcRole } from 'cwc-types';
import type { RequestContext, AuthenticatedContext } 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
| 'SESSION_RENEWAL_FAILED'; // Failed to renew session after mutation
// ============================================================================
// HTTP Methods and Status Codes
// ============================================================================
export type CwcApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
export type CwcApiSuccessStatusCode = 200 | 201;
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';
/**
* Operations that require session renewal (changes ownedProjects claim)
*/
export type SessionRenewalTrigger = 'project-create' | 'project-delete';
/**
* Route configuration for cwc-api endpoints
*/
export type CwcApiRouteConfig<TPayload = unknown, TResult = unknown> = {
// Route identification
path: string;
method: CwcApiHttpMethod;
handlerType: CwcApiHandlerType;
// Access control
allowedRoles: CwcRole[];
// Operation (injected worker function)
operation: CwcApiOperation<TPayload, TResult>;
// Session renewal trigger (only for mutations)
sessionRenewalTrigger?: SessionRenewalTrigger | undefined;
// 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 }
| { 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 RouteHandler
*/
export type RouteHandlerOptions = BaseHandlerOptions & {
payload: unknown;
pathParams?: Record<string, string> | undefined;
queryParams?: Record<string, string> | undefined;
};
/**
* Options for QueryHandler
*/
export type QueryHandlerOptions = BaseHandlerOptions & {
payload: unknown;
operationContext: OperationContext;
};
/**
* Options for MutationHandler
*/
export type MutationHandlerOptions = QueryHandlerOptions;
// ============================================================================
// Handler Interfaces
// ============================================================================
/**
* 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>;
}
```
---
## 3. RouteHandler Class
**File:** `/packages/cwc-api/src/handlers/RouteHandler.ts`
```typescript
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
RouteHandlerOptions,
RouteAccessResult,
OperationContext,
} from './handler.types';
import { QueryHandler } from './QueryHandler';
import { MutationHandler } from './MutationHandler';
const codeLocation = 'handlers/RouteHandler.ts';
/**
* RouteHandler - 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
*/
export class RouteHandler implements CwcApiHandler {
private options: RouteHandlerOptions;
private config: CwcApiConfig;
private authClient: AuthClient;
private logger: ILogger | undefined;
constructor(
options: RouteHandlerOptions,
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) {
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
if (routeConfig.handlerType === 'query') {
const queryHandler = new QueryHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.logger
);
return queryHandler.processRequest();
} else {
const mutationHandler = new MutationHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.authClient,
this.logger
);
return mutationHandler.processRequest();
}
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error processing route ${routeConfig.path}`,
error,
});
return this.createInternalErrorResponse(error);
}
}
/**
* Check if the current context role can access this route
* Phase 3: Simple role check against allowedRoles
* Phase 4: Will use RouteAccessPolicy interface
*/
private checkRouteAccess(): RouteAccessResult {
const { context, routeConfig } = this.options;
const { allowedRoles } = routeConfig;
// Guest role is always 'guest-user'
// Authenticated role starts as 'logged-on-user'
const currentRole = context.role;
if (allowedRoles.includes(currentRole)) {
return { allowed: true };
}
// For authenticated users, also check if 'logged-on-user' is allowed
// (project-owner check happens at operation level)
if (context.isAuthenticated && allowedRoles.includes('logged-on-user')) {
return { allowed: true };
}
return {
allowed: false,
reason: `Role '${currentRole}' 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.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
}
```
---
## 4. QueryHandler Class
**File:** `/packages/cwc-api/src/handlers/QueryHandler.ts`
```typescript
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
QueryHandlerOptions,
OperationAccessResult,
} from './handler.types';
const codeLocation = 'handlers/QueryHandler.ts';
/**
* QueryHandler - Handles read-only (GET) operations
*
* Responsibilities:
* 1. Check operation-level access (Phase 4 stub)
* 2. Execute the query operation
* 3. Format successful response with data/pagination
*/
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 this.createOperationDeniedResponse(operationAccess.reason);
}
// Step 2: Execute the operation
const result = await routeConfig.operation(payload, operationContext);
// Step 3: Format response
if (result.success) {
return {
statusCode: 200,
body: {
success: true,
data: result.data,
...(result.pagination ? { pagination: result.pagination } : {}),
},
};
}
return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error executing query operation for ${routeConfig.path}`,
error,
});
return this.createInternalErrorResponse(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 };
}
private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
return {
statusCode: 403,
body: {
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createOperationErrorResponse(
errorCode: string,
errorMessage: string
): CwcApiHandlerResponse {
// Map error codes to HTTP status codes
const statusCode = this.mapErrorCodeToStatus(errorCode);
return {
statusCode,
body: {
success: false,
errorCode: errorCode as any,
errorMessage,
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
private mapErrorCodeToStatus(errorCode: string): 400 | 401 | 403 | 404 | 500 {
switch (errorCode) {
case 'NOT_FOUND':
return 404;
case 'VALIDATION_ERROR':
case 'INVALID_PARAMETER':
return 400;
case 'UNAUTHORIZED':
return 401;
case 'FORBIDDEN':
case 'OPERATION_ACCESS_DENIED':
return 403;
default:
return 500;
}
}
}
```
---
## 5. MutationHandler Class
**File:** `/packages/cwc-api/src/handlers/MutationHandler.ts`
```typescript
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
MutationHandlerOptions,
OperationAccessResult,
} from './handler.types';
const codeLocation = 'handlers/MutationHandler.ts';
/**
* MutationHandler - Handles write operations (POST, PUT, DELETE)
*
* Responsibilities:
* 1. Check operation-level access (Phase 4 stub)
* 2. Execute the mutation operation
* 3. Renew session if operation triggers ownership change (project create/delete)
* 4. Format response with data and optional new JWT
*/
export class MutationHandler implements CwcApiHandler {
private options: MutationHandlerOptions;
private config: CwcApiConfig;
private authClient: AuthClient;
private logger: ILogger | undefined;
constructor(
options: MutationHandlerOptions,
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, operationContext, authHeader } = this.options;
try {
// Step 1: Check operation-level access (Phase 4 implementation)
const operationAccess = await this.checkOperationAccess();
if (!operationAccess.allowed) {
return this.createOperationDeniedResponse(operationAccess.reason);
}
// Step 2: Execute the mutation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
}
// Step 3: Check if session renewal is needed
let newJwt: string | undefined;
if (routeConfig.sessionRenewalTrigger && context.isAuthenticated) {
const renewResult = await this.renewSession(authHeader);
if (renewResult.success) {
newJwt = renewResult.jwt;
} else {
// Log warning but don't fail the operation
// The mutation succeeded, client will refresh on next request
this.logger?.logError({
userPkId: context.userPkId,
codeLocation,
message: `Session renewal failed after ${routeConfig.sessionRenewalTrigger}`,
error: renewResult.error,
});
}
}
// Step 4: Format response
const statusCode = this.getSuccessStatusCode();
return {
statusCode,
body: {
success: true,
data: result.data,
...(newJwt ? { jwt: newJwt } : {}),
},
};
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error executing mutation operation for ${routeConfig.path}`,
error,
});
return this.createInternalErrorResponse(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 };
}
/**
* Renew session with cwc-auth to get updated ownedProjects claim
*/
private async renewSession(
authHeader: string | undefined
): Promise<{ success: true; jwt: string } | { success: false; error: string }> {
const result = await this.authClient.renewSession(authHeader);
return result;
}
/**
* Determine success status code based on operation
* POST (create) = 201, PUT/DELETE = 200
*/
private getSuccessStatusCode(): 200 | 201 {
const { routeConfig } = this.options;
return routeConfig.method === 'POST' ? 201 : 200;
}
private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
return {
statusCode: 403,
body: {
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createOperationErrorResponse(
errorCode: string,
errorMessage: string
): CwcApiHandlerResponse {
const statusCode = this.mapErrorCodeToStatus(errorCode);
return {
statusCode,
body: {
success: false,
errorCode: errorCode as any,
errorMessage,
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
private mapErrorCodeToStatus(errorCode: string): 400 | 401 | 403 | 404 | 500 {
switch (errorCode) {
case 'NOT_FOUND':
return 404;
case 'VALIDATION_ERROR':
case 'INVALID_PARAMETER':
case 'ALREADY_EXISTS':
return 400;
case 'UNAUTHORIZED':
return 401;
case 'FORBIDDEN':
case 'OPERATION_ACCESS_DENIED':
return 403;
default:
return 500;
}
}
}
```
---
## 6. Handler Index Exports
**File:** `/packages/cwc-api/src/handlers/index.ts`
```typescript
'use strict';
// Types
export type {
// Error codes
CwcApiErrorCode,
// HTTP
CwcApiHttpMethod,
CwcApiStatusCode,
CwcApiSuccessStatusCode,
CwcApiErrorStatusCode,
// Response types
CwcApiPagination,
CwcApiSuccessResponse,
CwcApiErrorResponse,
CwcApiResponse,
CwcApiHandlerResponse,
// Route configuration
CwcApiHandlerType,
SessionRenewalTrigger,
CwcApiRouteConfig,
CwcApiRouteConfigs,
// Operation types
OperationContext,
OperationResult,
CwcApiOperation,
// Handler options
BaseHandlerOptions,
RouteHandlerOptions,
QueryHandlerOptions,
MutationHandlerOptions,
// Handler interface
CwcApiHandler,
// Access policy types (Phase 4)
RouteAccessResult,
OperationAccessResult,
RouteAccessPolicy,
OperationAccessPolicy,
} from './handler.types';
// Handlers
export { RouteHandler } from './RouteHandler';
export { QueryHandler } from './QueryHandler';
export { MutationHandler } from './MutationHandler';
```
---
## 7. Integration with CwcApiV1 (Phase 6 Preview)
The handlers are designed to integrate with the CwcApiV1 ExpressApi class (Phase 6). Here's how it will work:
**File Preview:** `/packages/cwc-api/src/apis/CwcApiV1/index.ts`
```typescript
// Phase 6 will implement this
public async handler(req: Request, res: Response, _next: NextFunction): Promise<void> {
// 1. Find route config
const routeConfig = this.routes[`${req.method}:${req.path}`];
// 2. Create context using existing createContext()
const context = await createContext({
authHeader: req.headers.authorization,
authClient: this.authClient,
});
// 3. Create and execute RouteHandler
const handler = new RouteHandler(
{
context,
routeConfig,
authHeader: req.headers.authorization,
payload: req.body,
pathParams: req.params,
queryParams: req.query as Record<string, string>,
},
this.config,
this.authClient,
this.logger
);
const response = await handler.processRequest();
// 4. Send response
res.status(response.statusCode).json(response.body);
}
```
---
## 8. Test Strategy
### 8.1 Unit Test Structure
```
src/__tests__/handlers/
RouteHandler.test.ts
QueryHandler.test.ts
MutationHandler.test.ts
```
### 8.2 RouteHandler Tests
```typescript
// RouteHandler.test.ts
describe('RouteHandler', () => {
describe('Route Access Control', () => {
it('should return 401 for guest user accessing authenticated route');
it('should return 403 for logged-on-user accessing project-owner-only route');
it('should allow guest user to access guest-allowed routes');
it('should allow authenticated user to access logged-on-user routes');
});
describe('Handler Delegation', () => {
it('should delegate to QueryHandler for query handlerType');
it('should delegate to MutationHandler for mutation handlerType');
});
describe('Operation Context', () => {
it('should pass projectId from path params to operation context');
it('should pass resourceId from path params to operation context');
});
describe('Error Handling', () => {
it('should catch errors and return 500 response');
it('should include error detail in development mode');
it('should not include error detail in production mode');
});
});
```
### 8.3 QueryHandler Tests
```typescript
// QueryHandler.test.ts
describe('QueryHandler', () => {
describe('Successful Operations', () => {
it('should return 200 with data on successful query');
it('should include pagination when operation returns it');
});
describe('Error Responses', () => {
it('should return 404 for NOT_FOUND error code');
it('should return 400 for VALIDATION_ERROR');
it('should return 500 for unknown errors');
});
describe('Operation Access', () => {
it('should stub operation access as allowed (Phase 3)');
});
});
```
### 8.4 MutationHandler Tests
```typescript
// MutationHandler.test.ts
describe('MutationHandler', () => {
describe('Successful Operations', () => {
it('should return 201 for POST operations');
it('should return 200 for PUT operations');
it('should return 200 for DELETE operations');
});
describe('Session Renewal', () => {
it('should call renewSession for project-create trigger');
it('should call renewSession for project-delete trigger');
it('should not call renewSession when no trigger configured');
it('should include new JWT in response on successful renewal');
it('should log warning but succeed when renewal fails');
it('should not attempt renewal for guest context');
});
describe('Error Responses', () => {
it('should return 404 for NOT_FOUND error code');
it('should return 400 for ALREADY_EXISTS error code');
it('should return 500 for database errors');
});
});
```
### 8.5 Mock Extensions Needed
Add to `/packages/cwc-api/src/__tests__/mocks/`:
```typescript
// routeConfig.mock.ts
export function createMockRouteConfig(
overrides: Partial<CwcApiRouteConfig> = {}
): CwcApiRouteConfig {
return {
path: '/test',
method: 'GET',
handlerType: 'query',
allowedRoles: ['guest-user', 'logged-on-user'],
operation: jest.fn().mockResolvedValue({ success: true, data: {} }),
...overrides,
};
}
export function createMockOperationContext(
overrides: Partial<OperationContext> = {}
): OperationContext {
return {
context: { isAuthenticated: false, role: 'guest-user' },
...overrides,
};
}
```
---
## 9. Implementation Order
1. **Phase 3a: Prerequisites**
- Add `RenewSessionResult` types to cwc-types
- Add `renewSession()` method to AuthClient
- Add mock helpers for renewSession
2. **Phase 3b: Core Types**
- Create `src/handlers/handler.types.ts`
3. **Phase 3c: QueryHandler**
- Create `src/handlers/QueryHandler.ts`
- Create `src/__tests__/handlers/QueryHandler.test.ts`
4. **Phase 3d: MutationHandler**
- Create `src/handlers/MutationHandler.ts`
- Create `src/__tests__/handlers/MutationHandler.test.ts`
5. **Phase 3e: RouteHandler**
- Create `src/handlers/RouteHandler.ts`
- Create `src/__tests__/handlers/RouteHandler.test.ts`
6. **Phase 3f: Exports**
- Create `src/handlers/index.ts`
- Update `src/__tests__/mocks/index.ts` with new mocks
---
## 10. Key Design Decisions
### 10.1 Why Separate Query and Mutation Handlers?
- **Session Renewal**: Only mutations can trigger session renewal (project create/delete)
- **HTTP Status Codes**: POST returns 201, GET always returns 200
- **Future Caching**: Queries could be cached, mutations cannot
- **Single Responsibility**: Each handler has a focused purpose
### 10.2 Why Phase 4 Stubs for Access Policies?
- **Separation of Concerns**: Handler architecture is independent of access policy logic
- **Testability**: Handlers can be tested without complex policy setup
- **Incremental Development**: Phase 4 implements policies, Phase 3 provides the hooks
### 10.3 Why Return Objects Instead of Throwing?
- **Consistent Flow**: All responses go through the same formatting
- **No Try/Catch Pollution**: Operation errors are handled as data, not exceptions
- **Type Safety**: OperationResult union type enforces handling both cases
### 10.4 Why JWT in Response Body (Not Header)?
- **Consistency**: All data in JSON body, following cwc-auth pattern
- **Client Simplicity**: Client always reads response body, no header inspection
- **Optional Field**: Only present when session was renewed
---
### Critical Files for Implementation
1. **`/packages/cwc-types/src/authTypes.ts`** - Add RenewSessionResult types (prerequisite)
2. **`/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts`** - Add renewSession method (prerequisite)
3. **`/packages/cwc-api/src/handlers/handler.types.ts`** - Core type definitions for entire handler system
4. **`/packages/cwc-api/src/handlers/MutationHandler.ts`** - Most complex handler with session renewal logic
5. **`/packages/cwc-api/src/context/createContext.ts`** - Existing context creation to integrate with
agentId: a5789af (for resuming to continue this agent's work if needed)The Plan agent has provided a comprehensive design. Let me now write the plan file with the implementation approach.
Phase 3: Handler Architecture Implementation Plan
Overview
Implement the handler architecture for cwc-api following the 3-layer pattern established in cwc-auth (ExpressApi → Handler → Worker), adapted for cwc-api's query/mutation separation and role-based access control.
Prerequisites (Before Phase 3 Core Work)
1. Add RenewSessionResult Types to cwc-types
File: packages/cwc-types/src/authTypes.ts
Add types for session renewal:
RenewSessionSuccessResponse- success: true, jwtType: 'user', jwt: stringRenewSessionErrorResponse- success: false, jwtType: undefined, errorCode?, errorDetail?RenewSessionResponse- union of aboveRenewSessionResult- simplified result for AuthClient consumers
2. Add renewSession() Method to AuthClient
File: packages/cwc-backend-utils/src/AuthClient/AuthClient.ts
Add renewSession(authHeader) method following same pattern as verifyToken():
- POST to
${authUri}/renew-session - Returns
RenewSessionResult - Graceful error handling (no throws)
3. Update AuthClient Mocks
File: packages/cwc-api/src/__tests__/mocks/authClient.mock.ts
Add:
renewSessionmethod tocreateMockAuthClient()mockRenewSessionSuccess(client, jwt?)helpermockRenewSessionFailure(client, error?)helper
Phase 3 Implementation
1. Create handler.types.ts
File: packages/cwc-api/src/handlers/handler.types.ts
Define all handler types:
- Error Codes:
CwcApiErrorCodeunion type - Response Types:
CwcApiSuccessResponse<T>,CwcApiErrorResponse,CwcApiPagination - Route Config:
CwcApiRouteConfig,CwcApiHandlerType('query' | 'mutation') - Operation Types:
OperationContext,OperationResult<T>,CwcApiOperation - Handler Options:
RouteHandlerOptions,QueryHandlerOptions,MutationHandlerOptions - Access Policy Interfaces (Phase 4 stubs):
RouteAccessResult,OperationAccessResult
2. Create QueryHandler.ts
File: packages/cwc-api/src/handlers/QueryHandler.ts
Responsibilities:
- Check operation-level access (stub for Phase 4)
- Execute query operation
- Format response with data/pagination
Key aspects:
- No session renewal needed
- Maps error codes to HTTP status (404, 400, 403, 500)
- Returns
CwcApiHandlerResponse
3. Create MutationHandler.ts
File: packages/cwc-api/src/handlers/MutationHandler.ts
Responsibilities:
- Check operation-level access (stub for Phase 4)
- Execute mutation operation
- Renew session if
sessionRenewalTriggeris set (project-create, project-delete) - Format response with data and optional new JWT
Key aspects:
- Uses AuthClient.renewSession() for ownership-changing operations
- Returns 201 for POST, 200 for PUT/DELETE
- Logs warning but doesn't fail if renewal fails
4. Create RouteHandler.ts
File: packages/cwc-api/src/handlers/RouteHandler.ts
Responsibilities:
- Check route-level access based on context role vs allowedRoles
- Build OperationContext from path params
- Delegate to QueryHandler or MutationHandler based on handlerType
Key aspects:
- Uses existing
RequestContextfrom Phase 2 - Returns 401 for unauthenticated access denied
- Returns 403 for authenticated but unauthorized
5. Create index.ts
File: packages/cwc-api/src/handlers/index.ts
Export all types and handler classes.
Unit Tests
Test Files
src/__tests__/handlers/RouteHandler.test.tssrc/__tests__/handlers/QueryHandler.test.tssrc/__tests__/handlers/MutationHandler.test.ts
Test Coverage
RouteHandler:
- Route access control (401 for guests, 403 for unauthorized roles)
- Handler delegation (query vs mutation)
- Operation context building from path params
- Error handling with dev-only details
QueryHandler:
- Successful operations (200 with data/pagination)
- Error response mapping (404, 400, 403, 500)
- Operation access stub (always allowed for Phase 3)
MutationHandler:
- Successful operations (201 for POST, 200 for PUT/DELETE)
- Session renewal (called for project-create/delete triggers)
- New JWT included in response when renewed
- Graceful handling when renewal fails
- Error response mapping
New Mocks Needed
File: src/__tests__/mocks/routeConfig.mock.ts
createMockRouteConfig(overrides)- Create test route configscreateMockOperationContext(overrides)- Create test operation contexts
Documentation Updates
Update CLAUDE.md
Add section documenting:
- Handler architecture pattern
- Route configuration structure
- Response envelope format
- Session renewal flow
Update README.md
Add:
- Request pipeline documentation
- Handler delegation flow
Implementation Order
Prerequisites (cwc-types, cwc-backend-utils)
- Add RenewSessionResult types
- Add AuthClient.renewSession()
- Update mocks
Core Types
- Create handler.types.ts
Handlers (order: QueryHandler → MutationHandler → RouteHandler)
- Each with corresponding unit tests
Exports
- Create index.ts
Documentation
- Update CLAUDE.md and README.md
Key Files to Modify/Create
| File | Action |
|---|---|
packages/cwc-types/src/authTypes.ts |
Add RenewSession types |
packages/cwc-types/src/index.ts |
Export new types |
packages/cwc-backend-utils/src/AuthClient/AuthClient.ts |
Add renewSession() |
packages/cwc-api/src/handlers/handler.types.ts |
Create |
packages/cwc-api/src/handlers/QueryHandler.ts |
Create |
packages/cwc-api/src/handlers/MutationHandler.ts |
Create |
packages/cwc-api/src/handlers/RouteHandler.ts |
Create |
packages/cwc-api/src/handlers/index.ts |
Create |
packages/cwc-api/src/__tests__/mocks/authClient.mock.ts |
Add renewSession mocks |
packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts |
Create |
packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts |
Create |
packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts |
Create |
packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts |
Create |
packages/cwc-api/CLAUDE.md |
Update handler section |
packages/cwc-api/cwc-api-implementation-checklist.md |
Update checkboxes |
Verification Steps
pnpm --filter cwc-types typecheck- Types compilepnpm --filter cwc-backend-utils typecheck- AuthClient updates compilepnpm --filter cwc-api typecheck- All handler code compilesNODE_ENV=unit pnpm --filter cwc-api test- All tests passpnpm typecheck- Full monorepo typecheck passes
Tool Result
File created successfully at: /Users/jeffbazinet/.claude/plans/steady-foraging-glade.md
Let me verify one thing about the config to ensure the plan is accurate.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/config/config.types.ts"
}Tool Result
1→import type { RuntimeEnvironment } from 'cwc-types';
2→
3→/**
4→ * Secret configuration values for cwc-api
5→ * These values must be provided via secrets file, never committed to code
6→ */
7→export type CwcApiConfigSecrets = {
8→ userJwtSecret: string;
9→ sqlClientApiKey: string;
10→};
11→
12→/**
13→ * Configuration for the cwc-api microservice
14→ */
15→export type CwcApiConfig = {
16→ // Environment (derived - skipped in .env generation)
17→ runtimeEnvironment: RuntimeEnvironment;
18→ isProduction: boolean;
19→ isDevelopment: boolean;
20→ isTest: boolean;
21→ isUnitTest: boolean;
22→ isEndToEnd: boolean;
23→
24→ // Service
25→ name: string;
26→ servicePort: number;
27→ dataUri: string;
28→ authUri: string;
29→ appUrl: string;
30→
31→ // Security
32→ corsOrigin: string;
33→
34→ // Rate limiting
35→ rateLimiterPoints: number;
36→ rateLimiterDuration: number;
37→
38→ // Development settings
39→ devCorsOrigin: string;
40→
41→ // Debugging
42→ debugMode: boolean;
43→ logErrorsToDatabase: boolean;
44→
45→ // E2E testing
46→ endToEndMockValues?: Record<string, string> | undefined;
47→
48→ // Secrets (nested)
49→ secrets: CwcApiConfigSecrets;
50→};
51→
<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 config already has isDevelopment, so the plan is accurate. The plan is ready for review.
Summary
The Phase 3 implementation has two prerequisites that need to be completed first:
- Add RenewSessionResult types to cwc-types - needed for AuthClient.renewSession()
- Add AuthClient.renewSession() method to cwc-backend-utils - needed for MutationHandler to renew sessions after project create/delete
Then the core Phase 3 work:
handler.types.ts- All type definitions for the handler systemQueryHandler.ts- Read-only operations (GET)MutationHandler.ts- Write operations (POST/PUT/DELETE) with session renewalRouteHandler.ts- Orchestrator that checks route access and delegates- Unit tests for each handler
- Documentation updates
The plan defines interfaces for Phase 4 (access policies) as stubs so the handlers can be tested independently.
Feedback on your plan: 1. `2. QueryHandler.ts - Read-only operations (GET), 3. MutationHandler.ts - Write operations (POST/PUT/DELETE) with session renewal` cwc-api is an RPC-style api and we will only use POST. 2. Is it of great benefit to have `sessionRenewalTrigger` or would it be better to always renew the session after a mutation, and possibly after queries as well? One benefit of this is keeping a session alive as long as the user is active.
Good feedback. Let me address both points:
1. RPC-style API (POST only)
This simplifies the design - we won't need to map HTTP methods to operations or return different status codes based on method. All endpoints will be POST. The query/mutation distinction becomes purely logical (read vs write operations), not HTTP-method based.
2. Session Renewal Strategy
This is an important design decision. Let me clarify the options:
With "always renew on every request", the architecture simplifies significantly. Let me update the plan.
Phase 3: Handler Architecture Implementation Plan
Overview
Implement the handler architecture for cwc-api following the 3-layer pattern established in cwc-auth (ExpressApi → Handler → Worker), adapted for cwc-api's role-based access control.
Key Design Decisions:
- RPC-style API: All endpoints use POST (no GET/PUT/DELETE)
- Session renewal: Always renew on every request to keep sessions active
- Query/Mutation distinction: Logical separation for pagination and future caching
Tool Result
The file /Users/jeffbazinet/.claude/plans/steady-foraging-glade.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→# Phase 3: Handler Architecture Implementation Plan
2→
3→## Overview
4→
5→Implement the handler architecture for cwc-api following the 3-layer pattern established in cwc-auth (ExpressApi → Handler → Worker), adapted for cwc-api's role-based access control.
6→
7→**Key Design Decisions:**
8→- **RPC-style API:** All endpoints use POST (no GET/PUT/DELETE)
9→- **Session renewal:** Always renew on every request to keep sessions active
10→- **Query/Mutation distinction:** Logical separation for pagination and future caching
11→
12→---
13→
14→## Prerequisites (Before Phase 3 Core Work)2. Create QueryHandler.ts
File: packages/cwc-api/src/handlers/QueryHandler.ts
Responsibilities:
- Check operation-level access (stub for Phase 4)
- Execute query operation
- Renew session (always, to keep session active)
- Format response with data/pagination and new JWT
Key aspects:
- Calls AuthClient.renewSession() on every request
- Maps error codes to HTTP status (404, 400, 403, 500)
- Returns
CwcApiHandlerResponsewith optional pagination
3. Create MutationHandler.ts
File: packages/cwc-api/src/handlers/MutationHandler.ts
Responsibilities:
- Check operation-level access (stub for Phase 4)
- Execute mutation operation
- Renew session (always, to keep session active)
- Format response with data and new JWT
Key aspects:
- Calls AuthClient.renewSession() on every request
- Returns 200 for all POST operations (RPC-style, no 201)
- Logs warning but doesn't fail if renewal fails
Tool Result
The file /Users/jeffbazinet/.claude/plans/steady-foraging-glade.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
56→- **Operation Types:** `OperationContext`, `OperationResult<T>`, `CwcApiOperation`
57→- **Handler Options:** `RouteHandlerOptions`, `QueryHandlerOptions`, `MutationHandlerOptions`
58→- **Access Policy Interfaces (Phase 4 stubs):** `RouteAccessResult`, `OperationAccessResult`
59→
60→### 2. Create QueryHandler.ts
61→
62→**File:** `packages/cwc-api/src/handlers/QueryHandler.ts`
63→
64→Responsibilities:
65→1. Check operation-level access (stub for Phase 4)
66→2. Execute query operation
67→3. Renew session (always, to keep session active)
68→4. Format response with data/pagination and new JWT
69→
70→Key aspects:
71→- Calls AuthClient.renewSession() on every request
72→- Maps error codes to HTTP status (404, 400, 403, 500)
73→- Returns `CwcApiHandlerResponse` with optional pagination
74→
75→### 3. Create MutationHandler.ts
76→
77→**File:** `packages/cwc-api/src/handlers/MutationHandler.ts`
78→
79→Responsibilities:
80→1. Check operation-level access (stub for Phase 4)
81→2. Execute mutation operation
82→3. Renew session (always, to keep session active)
83→4. Format response with data and new JWT
84→
85→Key aspects:
86→- Calls AuthClient.renewSession() on every request
87→- Returns 200 for all POST operations (RPC-style, no 201)
88→- Logs warning but doesn't fail if renewal fails
89→
90→### 4. Create RouteHandler.ts
91→
92→**File:** `packages/cwc-api/src/handlers/RouteHandler.ts`- Route Config:
CwcApiRouteConfig(path, handlerType: 'query' | 'mutation', allowedRoles) - Operation Types:
OperationContext,OperationResult<T>,CwcApiOperation - Note: No
sessionRenewalTriggerneeded - always renew on every request
Tool Result
The file /Users/jeffbazinet/.claude/plans/steady-foraging-glade.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
51→
52→Define all handler types:
53→- **Error Codes:** `CwcApiErrorCode` union type
54→- **Response Types:** `CwcApiSuccessResponse<T>`, `CwcApiErrorResponse`, `CwcApiPagination`
55→- **Route Config:** `CwcApiRouteConfig` (path, handlerType: 'query' | 'mutation', allowedRoles)
56→- **Operation Types:** `OperationContext`, `OperationResult<T>`, `CwcApiOperation`
57→- **Note:** No `sessionRenewalTrigger` needed - always renew on every request
58→- **Handler Options:** `RouteHandlerOptions`, `QueryHandlerOptions`, `MutationHandlerOptions`
59→- **Access Policy Interfaces (Phase 4 stubs):** `RouteAccessResult`, `OperationAccessResult`
60→
61→### 2. Create QueryHandler.tsQueryHandler:
- Successful operations (200 with data/pagination)
- Session renewal (called on every request for authenticated users)
- New JWT included in response when renewed
- Graceful handling when renewal fails
- No renewal attempted for guest users
- Error response mapping (404, 400, 403, 500)
- Operation access stub (always allowed for Phase 3)
MutationHandler:
- Successful operations (200 for all POST operations)
- Session renewal (called on every request for authenticated users)
- New JWT included in response when renewed
- Graceful handling when renewal fails
- No renewal attempted for guest users
- Error response mapping
Tool Result
The file /Users/jeffbazinet/.claude/plans/steady-foraging-glade.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 125→- Handler delegation (query vs mutation) 126→- Operation context building from path params 127→- Error handling with dev-only details 128→ 129→**QueryHandler:** 130→- Successful operations (200 with data/pagination) 131→- Session renewal (called on every request for authenticated users) 132→- New JWT included in response when renewed 133→- Graceful handling when renewal fails 134→- No renewal attempted for guest users 135→- Error response mapping (404, 400, 403, 500) 136→- Operation access stub (always allowed for Phase 3) 137→ 138→**MutationHandler:** 139→- Successful operations (200 for all POST operations) 140→- Session renewal (called on every request for authenticated users) 141→- New JWT included in response when renewed 142→- Graceful handling when renewal fails 143→- No renewal attempted for guest users 144→- Error response mapping 145→ 146→### New Mocks Needed 147→ 148→**File:** `src/__tests__/mocks/routeConfig.mock.ts`
Let me read the full plan to see if there are any other sections that need updating.
Read
{
"file_path": "/Users/jeffbazinet/.claude/plans/steady-foraging-glade.md"
}Tool Result
1→# Phase 3: Handler Architecture Implementation Plan
2→
3→## Overview
4→
5→Implement the handler architecture for cwc-api following the 3-layer pattern established in cwc-auth (ExpressApi → Handler → Worker), adapted for cwc-api's role-based access control.
6→
7→**Key Design Decisions:**
8→- **RPC-style API:** All endpoints use POST (no GET/PUT/DELETE)
9→- **Session renewal:** Always renew on every request to keep sessions active
10→- **Query/Mutation distinction:** Logical separation for pagination and future caching
11→
12→---
13→
14→## Prerequisites (Before Phase 3 Core Work)
15→
16→### 1. Add RenewSessionResult Types to cwc-types
17→
18→**File:** `packages/cwc-types/src/authTypes.ts`
19→
20→Add types for session renewal:
21→- `RenewSessionSuccessResponse` - success: true, jwtType: 'user', jwt: string
22→- `RenewSessionErrorResponse` - success: false, jwtType: undefined, errorCode?, errorDetail?
23→- `RenewSessionResponse` - union of above
24→- `RenewSessionResult` - simplified result for AuthClient consumers
25→
26→### 2. Add renewSession() Method to AuthClient
27→
28→**File:** `packages/cwc-backend-utils/src/AuthClient/AuthClient.ts`
29→
30→Add `renewSession(authHeader)` method following same pattern as `verifyToken()`:
31→- POST to `${authUri}/renew-session`
32→- Returns `RenewSessionResult`
33→- Graceful error handling (no throws)
34→
35→### 3. Update AuthClient Mocks
36→
37→**File:** `packages/cwc-api/src/__tests__/mocks/authClient.mock.ts`
38→
39→Add:
40→- `renewSession` method to `createMockAuthClient()`
41→- `mockRenewSessionSuccess(client, jwt?)` helper
42→- `mockRenewSessionFailure(client, error?)` helper
43→
44→---
45→
46→## Phase 3 Implementation
47→
48→### 1. Create handler.types.ts
49→
50→**File:** `packages/cwc-api/src/handlers/handler.types.ts`
51→
52→Define all handler types:
53→- **Error Codes:** `CwcApiErrorCode` union type
54→- **Response Types:** `CwcApiSuccessResponse<T>`, `CwcApiErrorResponse`, `CwcApiPagination`
55→- **Route Config:** `CwcApiRouteConfig` (path, handlerType: 'query' | 'mutation', allowedRoles)
56→- **Operation Types:** `OperationContext`, `OperationResult<T>`, `CwcApiOperation`
57→- **Note:** No `sessionRenewalTrigger` needed - always renew on every request
58→- **Handler Options:** `RouteHandlerOptions`, `QueryHandlerOptions`, `MutationHandlerOptions`
59→- **Access Policy Interfaces (Phase 4 stubs):** `RouteAccessResult`, `OperationAccessResult`
60→
61→### 2. Create QueryHandler.ts
62→
63→**File:** `packages/cwc-api/src/handlers/QueryHandler.ts`
64→
65→Responsibilities:
66→1. Check operation-level access (stub for Phase 4)
67→2. Execute query operation
68→3. Renew session (always, to keep session active)
69→4. Format response with data/pagination and new JWT
70→
71→Key aspects:
72→- Calls AuthClient.renewSession() on every request
73→- Maps error codes to HTTP status (404, 400, 403, 500)
74→- Returns `CwcApiHandlerResponse` with optional pagination
75→
76→### 3. Create MutationHandler.ts
77→
78→**File:** `packages/cwc-api/src/handlers/MutationHandler.ts`
79→
80→Responsibilities:
81→1. Check operation-level access (stub for Phase 4)
82→2. Execute mutation operation
83→3. Renew session (always, to keep session active)
84→4. Format response with data and new JWT
85→
86→Key aspects:
87→- Calls AuthClient.renewSession() on every request
88→- Returns 200 for all POST operations (RPC-style, no 201)
89→- Logs warning but doesn't fail if renewal fails
90→
91→### 4. Create RouteHandler.ts
92→
93→**File:** `packages/cwc-api/src/handlers/RouteHandler.ts`
94→
95→Responsibilities:
96→1. Check route-level access based on context role vs allowedRoles
97→2. Build OperationContext from path params
98→3. Delegate to QueryHandler or MutationHandler based on handlerType
99→
100→Key aspects:
101→- Uses existing `RequestContext` from Phase 2
102→- Returns 401 for unauthenticated access denied
103→- Returns 403 for authenticated but unauthorized
104→
105→### 5. Create index.ts
106→
107→**File:** `packages/cwc-api/src/handlers/index.ts`
108→
109→Export all types and handler classes.
110→
111→---
112→
113→## Unit Tests
114→
115→### Test Files
116→
117→- `src/__tests__/handlers/RouteHandler.test.ts`
118→- `src/__tests__/handlers/QueryHandler.test.ts`
119→- `src/__tests__/handlers/MutationHandler.test.ts`
120→
121→### Test Coverage
122→
123→**RouteHandler:**
124→- Route access control (401 for guests, 403 for unauthorized roles)
125→- Handler delegation (query vs mutation)
126→- Operation context building from path params
127→- Error handling with dev-only details
128→
129→**QueryHandler:**
130→- Successful operations (200 with data/pagination)
131→- Session renewal (called on every request for authenticated users)
132→- New JWT included in response when renewed
133→- Graceful handling when renewal fails
134→- No renewal attempted for guest users
135→- Error response mapping (404, 400, 403, 500)
136→- Operation access stub (always allowed for Phase 3)
137→
138→**MutationHandler:**
139→- Successful operations (200 for all POST operations)
140→- Session renewal (called on every request for authenticated users)
141→- New JWT included in response when renewed
142→- Graceful handling when renewal fails
143→- No renewal attempted for guest users
144→- Error response mapping
145→
146→### New Mocks Needed
147→
148→**File:** `src/__tests__/mocks/routeConfig.mock.ts`
149→
150→- `createMockRouteConfig(overrides)` - Create test route configs
151→- `createMockOperationContext(overrides)` - Create test operation contexts
152→
153→---
154→
155→## Documentation Updates
156→
157→### Update CLAUDE.md
158→
159→Add section documenting:
160→- Handler architecture pattern
161→- Route configuration structure
162→- Response envelope format
163→- Session renewal flow
164→
165→### Update README.md
166→
167→Add:
168→- Request pipeline documentation
169→- Handler delegation flow
170→
171→---
172→
173→## Implementation Order
174→
175→1. **Prerequisites (cwc-types, cwc-backend-utils)**
176→ - Add RenewSessionResult types
177→ - Add AuthClient.renewSession()
178→ - Update mocks
179→
180→2. **Core Types**
181→ - Create handler.types.ts
182→
183→3. **Handlers (order: QueryHandler → MutationHandler → RouteHandler)**
184→ - Each with corresponding unit tests
185→
186→4. **Exports**
187→ - Create index.ts
188→
189→5. **Documentation**
190→ - Update CLAUDE.md and README.md
191→
192→---
193→
194→## Key Files to Modify/Create
195→
196→| File | Action |
197→|------|--------|
198→| `packages/cwc-types/src/authTypes.ts` | Add RenewSession types |
199→| `packages/cwc-types/src/index.ts` | Export new types |
200→| `packages/cwc-backend-utils/src/AuthClient/AuthClient.ts` | Add renewSession() |
201→| `packages/cwc-api/src/handlers/handler.types.ts` | Create |
202→| `packages/cwc-api/src/handlers/QueryHandler.ts` | Create |
203→| `packages/cwc-api/src/handlers/MutationHandler.ts` | Create |
204→| `packages/cwc-api/src/handlers/RouteHandler.ts` | Create |
205→| `packages/cwc-api/src/handlers/index.ts` | Create |
206→| `packages/cwc-api/src/__tests__/mocks/authClient.mock.ts` | Add renewSession mocks |
207→| `packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts` | Create |
208→| `packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts` | Create |
209→| `packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts` | Create |
210→| `packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts` | Create |
211→| `packages/cwc-api/CLAUDE.md` | Update handler section |
212→| `packages/cwc-api/cwc-api-implementation-checklist.md` | Update checkboxes |
213→
214→---
215→
216→## Verification Steps
217→
218→1. `pnpm --filter cwc-types typecheck` - Types compile
219→2. `pnpm --filter cwc-backend-utils typecheck` - AuthClient updates compile
220→3. `pnpm --filter cwc-api typecheck` - All handler code compiles
221→4. `NODE_ENV=unit pnpm --filter cwc-api test` - All tests pass
222→5. `pnpm typecheck` - Full monorepo typecheck passes
223→
<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 plan is now updated with your feedback:
- RPC-style API: All endpoints use POST (documented in overview)
- Session renewal: Always renew on every request for authenticated users (both QueryHandler and MutationHandler)
- No
sessionRenewalTrigger: Simplified - always renew - Returns 200 for all operations: No 201 status codes
The plan has a clean implementation order:
- Prerequisites first: RenewSessionResult types in cwc-types, renewSession() in AuthClient
- Core types: handler.types.ts
- Handlers: QueryHandler → MutationHandler → RouteHandler (each with unit tests)
- Documentation: CLAUDE.md and checklist updates
Let me set up the todo list and start implementation.
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "in_progress",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "pending",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "pending",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "pending",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "pending",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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-types/src/authTypes.ts"
}Tool Result
1→/**
2→ * Auth Types - Shared authentication types for CWC services
3→ *
4→ * These types are used by cwc-auth (JWT creation) and consuming services
5→ * (JWT verification via AuthClient).
6→ */
7→
8→import type { CwcLoginClaims } from './entityTypes';
9→
10→/**
11→ * UserJwtPayload structure - decoded JWT payload
12→ * Used by cwc-auth (creation) and consuming services (verification)
13→ */
14→export type UserJwtPayload = {
15→ jti: string; // userJwtId (UUID) - references userJwt table
16→ sub: number; // userPkId
17→ iat: number; // Issued at (Unix timestamp)
18→ exp: number; // Expiration (Unix timestamp)
19→ login: CwcLoginClaims;
20→};
21→
22→/**
23→ * Roles for access control across CWC services
24→ */
25→export type CwcRole = 'project-owner' | 'logged-on-user' | 'guest-user';
26→
27→/**
28→ * Success response from cwc-auth /verify-token
29→ */
30→export type VerifyTokenSuccessResponse = {
31→ success: true;
32→ jwtType: undefined;
33→ payload: UserJwtPayload;
34→};
35→
36→/**
37→ * Error response from cwc-auth /verify-token
38→ */
39→export type VerifyTokenErrorResponse = {
40→ success: false;
41→ jwtType: undefined;
42→ errorCode?: string | undefined; // Dev-only
43→ errorDetail?: string | undefined; // Dev-only
44→};
45→
46→/**
47→ * Union of verify-token responses
48→ */
49→export type VerifyTokenResponse = VerifyTokenSuccessResponse | VerifyTokenErrorResponse;
50→
51→/**
52→ * Result from AuthClient.verifyToken()
53→ */
54→export type VerifyTokenResult =
55→ | { success: true; payload: UserJwtPayload }
56→ | { success: false; error: string };
57→
<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-types/src/index.ts"
}Tool Result
1→/**
2→ * CWC Types - Shared TypeScript types for CWC application
3→ *
4→ * This package contains all shared type definitions used across
5→ * the CWC monorepo packages.
6→ */
7→
8→// Export all entity types
9→export * from './entityTypes.js';
10→
11→// Export config types
12→export * from './config.types.js';
13→
14→// Export session data types
15→export * from './sessionData.types.js';
16→
17→// Export auth types
18→export * from './authTypes.js';
19→
<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>
Now I'll add the RenewSession types to authTypes.ts.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-types/src/authTypes.ts",
"old_string": "/**\n * Result from AuthClient.verifyToken()\n */\nexport type VerifyTokenResult =\n | { success: true; payload: UserJwtPayload }\n | { success: false; error: string };",
"new_string": "/**\n * Result from AuthClient.verifyToken()\n */\nexport type VerifyTokenResult =\n | { success: true; payload: UserJwtPayload }\n | { success: false; error: string };\n\n// ============================================================================\n// Renew Session Types\n// ============================================================================\n\n/**\n * Success response from cwc-auth /renew-session\n */\nexport type RenewSessionSuccessResponse = {\n success: true;\n jwtType: 'user';\n jwt: string;\n};\n\n/**\n * Error response from cwc-auth /renew-session\n */\nexport type RenewSessionErrorResponse = {\n success: false;\n jwtType: undefined;\n errorCode?: string | undefined; // Dev-only\n errorDetail?: string | undefined; // Dev-only\n};\n\n/**\n * Union of renew-session responses\n */\nexport type RenewSessionResponse = RenewSessionSuccessResponse | RenewSessionErrorResponse;\n\n/**\n * Result from AuthClient.renewSession()\n */\nexport type RenewSessionResult =\n | { success: true; jwt: string }\n | { success: false; error: string };"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-types/src/authTypes.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
47→ * Union of verify-token responses
48→ */
49→export type VerifyTokenResponse = VerifyTokenSuccessResponse | VerifyTokenErrorResponse;
50→
51→/**
52→ * Result from AuthClient.verifyToken()
53→ */
54→export type VerifyTokenResult =
55→ | { success: true; payload: UserJwtPayload }
56→ | { success: false; error: string };
57→
58→// ============================================================================
59→// Renew Session Types
60→// ============================================================================
61→
62→/**
63→ * Success response from cwc-auth /renew-session
64→ */
65→export type RenewSessionSuccessResponse = {
66→ success: true;
67→ jwtType: 'user';
68→ jwt: string;
69→};
70→
71→/**
72→ * Error response from cwc-auth /renew-session
73→ */
74→export type RenewSessionErrorResponse = {
75→ success: false;
76→ jwtType: undefined;
77→ errorCode?: string | undefined; // Dev-only
78→ errorDetail?: string | undefined; // Dev-only
79→};
80→
81→/**
82→ * Union of renew-session responses
83→ */
84→export type RenewSessionResponse = RenewSessionSuccessResponse | RenewSessionErrorResponse;
85→
86→/**
87→ * Result from AuthClient.renewSession()
88→ */
89→export type RenewSessionResult =
90→ | { success: true; jwt: string }
91→ | { success: false; error: string };
92→Let me verify the types compile correctly.
Bash
{
"command": "pnpm --filter cwc-types typecheck",
"description": "Typecheck cwc-types package"
}Tool Result
> cwc-types@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-types > tsc --noEmit
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "in_progress",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "pending",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "pending",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "pending",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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
Now let me add the renewSession() method to AuthClient.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts"
}Tool Result
1→import axios from 'axios';
2→import type { ILogger } from '../backendUtils.types';
3→import type { VerifyTokenResponse, VerifyTokenResult, VerifyTokenErrorResponse } from 'cwc-types';
4→
5→const codeLocation = 'AuthClient/AuthClient.ts';
6→
7→export type AuthClientConfig = {
8→ authUri: string;
9→ timeout?: number | undefined;
10→};
11→
12→export type AuthClientOptions = {
13→ config: AuthClientConfig;
14→ logger: ILogger | undefined;
15→ clientName: string;
16→};
17→
18→/**
19→ * HTTP client for cwc-auth service
20→ * Similar pattern to SqlClient for cwc-sql
21→ */
22→export class AuthClient {
23→ private config: AuthClientConfig;
24→ private logger: ILogger | undefined;
25→ private clientName: string;
26→
27→ constructor(options: AuthClientOptions) {
28→ this.config = options.config;
29→ this.logger = options.logger;
30→ this.clientName = options.clientName;
31→ }
32→
33→ /**
34→ * Verify a JWT by calling cwc-auth /verify-token
35→ */
36→ async verifyToken(authHeader: string | undefined): Promise<VerifyTokenResult> {
37→ if (!authHeader) {
38→ return { success: false, error: 'MISSING_TOKEN' };
39→ }
40→
41→ try {
42→ const response = await axios.post<VerifyTokenResponse>(
43→ `${this.config.authUri}/verify-token`,
44→ {},
45→ {
46→ headers: { Authorization: authHeader },
47→ timeout: this.config.timeout ?? 5000,
48→ }
49→ );
50→
51→ const data = response.data;
52→
53→ if (data.success) {
54→ return { success: true, payload: data.payload };
55→ }
56→
57→ // data.success is false, so it's a VerifyTokenErrorResponse
58→ return { success: false, error: data.errorCode ?? 'UNKNOWN_ERROR' };
59→ } catch (error) {
60→ // Handle 401 responses (expected for invalid tokens)
61→ if (axios.isAxiosError(error) && error.response?.status === 401) {
62→ const errorData = error.response.data as VerifyTokenErrorResponse;
63→ return { success: false, error: errorData.errorCode ?? 'INVALID_TOKEN' };
64→ }
65→
66→ // Log unexpected errors
67→ this.logger?.logError({
68→ userPkId: undefined,
69→ codeLocation,
70→ message: `[${this.clientName}] Failed to verify token with auth service`,
71→ error,
72→ });
73→
74→ return { success: false, error: 'AUTH_SERVICE_ERROR' };
75→ }
76→ }
77→}
78→
<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-backend-utils/src/AuthClient/AuthClient.ts",
"old_string": "import axios from 'axios';\nimport type { ILogger } from '../backendUtils.types';\nimport type { VerifyTokenResponse, VerifyTokenResult, VerifyTokenErrorResponse } from 'cwc-types';",
"new_string": "import axios from 'axios';\nimport type { ILogger } from '../backendUtils.types';\nimport type {\n VerifyTokenResponse,\n VerifyTokenResult,\n VerifyTokenErrorResponse,\n RenewSessionResponse,\n RenewSessionResult,\n RenewSessionErrorResponse,\n} from 'cwc-types';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→import axios from 'axios';
2→import type { ILogger } from '../backendUtils.types';
3→import type {
4→ VerifyTokenResponse,
5→ VerifyTokenResult,
6→ VerifyTokenErrorResponse,
7→ RenewSessionResponse,
8→ RenewSessionResult,
9→ RenewSessionErrorResponse,
10→} from 'cwc-types';
11→
12→const codeLocation = 'AuthClient/AuthClient.ts';
13→
14→export type AuthClientConfig = {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts",
"old_string": " return { success: false, error: 'AUTH_SERVICE_ERROR' };\n }\n }\n}",
"new_string": " return { success: false, error: 'AUTH_SERVICE_ERROR' };\n }\n }\n\n /**\n * Renew a session by calling cwc-auth /renew-session\n * Used to keep sessions active and refresh JWT claims (e.g., ownedProjects)\n */\n async renewSession(authHeader: string | undefined): Promise<RenewSessionResult> {\n if (!authHeader) {\n return { success: false, error: 'MISSING_TOKEN' };\n }\n\n try {\n const response = await axios.post<RenewSessionResponse>(\n `${this.config.authUri}/renew-session`,\n {},\n {\n headers: { Authorization: authHeader },\n timeout: this.config.timeout ?? 5000,\n }\n );\n\n const data = response.data;\n\n if (data.success) {\n return { success: true, jwt: data.jwt };\n }\n\n // data.success is false, so it's a RenewSessionErrorResponse\n return { success: false, error: data.errorCode ?? 'UNKNOWN_ERROR' };\n } catch (error) {\n // Handle 401 responses (expected for invalid/expired tokens)\n if (axios.isAxiosError(error) && error.response?.status === 401) {\n const errorData = error.response.data as RenewSessionErrorResponse;\n return { success: false, error: errorData.errorCode ?? 'RENEWAL_FAILED' };\n }\n\n // Log unexpected errors\n this.logger?.logError({\n userPkId: undefined,\n codeLocation,\n message: `[${this.clientName}] Failed to renew session with auth service`,\n error,\n });\n\n return { success: false, error: 'AUTH_SERVICE_ERROR' };\n }\n }\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-backend-utils/src/AuthClient/AuthClient.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
77→ message: `[${this.clientName}] Failed to verify token with auth service`,
78→ error,
79→ });
80→
81→ return { success: false, error: 'AUTH_SERVICE_ERROR' };
82→ }
83→ }
84→
85→ /**
86→ * Renew a session by calling cwc-auth /renew-session
87→ * Used to keep sessions active and refresh JWT claims (e.g., ownedProjects)
88→ */
89→ async renewSession(authHeader: string | undefined): Promise<RenewSessionResult> {
90→ if (!authHeader) {
91→ return { success: false, error: 'MISSING_TOKEN' };
92→ }
93→
94→ try {
95→ const response = await axios.post<RenewSessionResponse>(
96→ `${this.config.authUri}/renew-session`,
97→ {},
98→ {
99→ headers: { Authorization: authHeader },
100→ timeout: this.config.timeout ?? 5000,
101→ }
102→ );
103→
104→ const data = response.data;
105→
106→ if (data.success) {
107→ return { success: true, jwt: data.jwt };
108→ }
109→
110→ // data.success is false, so it's a RenewSessionErrorResponse
111→ return { success: false, error: data.errorCode ?? 'UNKNOWN_ERROR' };
112→ } catch (error) {
113→ // Handle 401 responses (expected for invalid/expired tokens)
114→ if (axios.isAxiosError(error) && error.response?.status === 401) {
115→ const errorData = error.response.data as RenewSessionErrorResponse;
116→ return { success: false, error: errorData.errorCode ?? 'RENEWAL_FAILED' };
117→ }
118→
119→ // Log unexpected errors
120→ this.logger?.logError({
121→ userPkId: undefined,
122→ codeLocation,
123→ message: `[${this.clientName}] Failed to renew session with auth service`,
124→ error,
125→ });
126→
127→ return { success: false, error: 'AUTH_SERVICE_ERROR' };
128→ }
129→ }
130→}
131→Bash
{
"command": "pnpm --filter cwc-backend-utils typecheck",
"description": "Typecheck cwc-backend-utils package"
}Tool Result
> cwc-backend-utils@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-backend-utils > tsc --noEmit
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "in_progress",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "pending",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "pending",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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/authClient.mock.ts"
}Tool Result
1→'use strict';
2→
3→import { jest } from '@jest/globals';
4→import type { AuthClient } from 'cwc-backend-utils';
5→import type { VerifyTokenResult, UserJwtPayload, CwcLoginClaims } from 'cwc-types';
6→
7→/**
8→ * Creates a mock AuthClient for testing
9→ */
10→export function createMockAuthClient(): jest.Mocked<AuthClient> {
11→ return {
12→ verifyToken: jest.fn<(authHeader: string | undefined) => Promise<VerifyTokenResult>>(),
13→ } as unknown as jest.Mocked<AuthClient>;
14→}
15→
16→/**
17→ * Creates a mock UserJwtPayload for testing
18→ */
19→export function createMockUserJwtPayload(overrides: Partial<UserJwtPayload> = {}): UserJwtPayload {
20→ const defaultLogin: CwcLoginClaims = {
21→ username: 'testuser@example.com',
22→ deviceId: 'test-device-id',
23→ userJwtId: 'test-jwt-id',
24→ loginType: 'cwc',
25→ kulo: false,
26→ ownedProjects: ['project-1', 'project-2'],
27→ isGuestUser: false,
28→ };
29→
30→ return {
31→ jti: 'test-jwt-id',
32→ sub: 123,
33→ iat: Math.floor(Date.now() / 1000),
34→ exp: Math.floor(Date.now() / 1000) + 900, // 15 minutes
35→ login: defaultLogin,
36→ ...overrides,
37→ };
38→}
39→
40→/**
41→ * Configures mock AuthClient to return success with payload
42→ */
43→export function mockVerifyTokenSuccess(
44→ mockAuthClient: jest.Mocked<AuthClient>,
45→ payload: UserJwtPayload = createMockUserJwtPayload()
46→): void {
47→ mockAuthClient.verifyToken.mockResolvedValueOnce({
48→ success: true,
49→ payload,
50→ });
51→}
52→
53→/**
54→ * Configures mock AuthClient to return failure
55→ */
56→export function mockVerifyTokenFailure(
57→ mockAuthClient: jest.Mocked<AuthClient>,
58→ error: string = 'INVALID_TOKEN'
59→): void {
60→ mockAuthClient.verifyToken.mockResolvedValueOnce({
61→ success: false,
62→ error,
63→ });
64→}
65→
<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/authClient.mock.ts",
"old_string": "import type { VerifyTokenResult, UserJwtPayload, CwcLoginClaims } from 'cwc-types';",
"new_string": "import type {\n VerifyTokenResult,\n RenewSessionResult,\n UserJwtPayload,\n CwcLoginClaims,\n} from 'cwc-types';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/authClient.mock.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 { jest } from '@jest/globals';
4→import type { AuthClient } from 'cwc-backend-utils';
5→import type {
6→ VerifyTokenResult,
7→ RenewSessionResult,
8→ UserJwtPayload,
9→ CwcLoginClaims,
10→} from 'cwc-types';
11→
12→/**
13→ * Creates a mock AuthClient for testing
14→ */Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/authClient.mock.ts",
"old_string": "/**\n * Creates a mock AuthClient for testing\n */\nexport function createMockAuthClient(): jest.Mocked<AuthClient> {\n return {\n verifyToken: jest.fn<(authHeader: string | undefined) => Promise<VerifyTokenResult>>(),\n } as unknown as jest.Mocked<AuthClient>;\n}",
"new_string": "/**\n * Creates a mock AuthClient for testing\n */\nexport function createMockAuthClient(): jest.Mocked<AuthClient> {\n return {\n verifyToken: jest.fn<(authHeader: string | undefined) => Promise<VerifyTokenResult>>(),\n renewSession: jest.fn<(authHeader: string | undefined) => Promise<RenewSessionResult>>(),\n } as unknown as jest.Mocked<AuthClient>;\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/authClient.mock.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
8→ UserJwtPayload,
9→ CwcLoginClaims,
10→} from 'cwc-types';
11→
12→/**
13→ * Creates a mock AuthClient for testing
14→ */
15→export function createMockAuthClient(): jest.Mocked<AuthClient> {
16→ return {
17→ verifyToken: jest.fn<(authHeader: string | undefined) => Promise<VerifyTokenResult>>(),
18→ renewSession: jest.fn<(authHeader: string | undefined) => Promise<RenewSessionResult>>(),
19→ } as unknown as jest.Mocked<AuthClient>;
20→}
21→
22→/**
23→ * Creates a mock UserJwtPayload for testing
24→ */Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/authClient.mock.ts",
"old_string": "/**\n * Configures mock AuthClient to return failure\n */\nexport function mockVerifyTokenFailure(\n mockAuthClient: jest.Mocked<AuthClient>,\n error: string = 'INVALID_TOKEN'\n): void {\n mockAuthClient.verifyToken.mockResolvedValueOnce({\n success: false,\n error,\n });\n}",
"new_string": "/**\n * Configures mock AuthClient to return failure\n */\nexport function mockVerifyTokenFailure(\n mockAuthClient: jest.Mocked<AuthClient>,\n error: string = 'INVALID_TOKEN'\n): void {\n mockAuthClient.verifyToken.mockResolvedValueOnce({\n success: false,\n error,\n });\n}\n\n/**\n * Configures mock AuthClient to return success for renewSession with new JWT\n */\nexport function mockRenewSessionSuccess(\n mockAuthClient: jest.Mocked<AuthClient>,\n jwt: string = 'new-jwt-token'\n): void {\n mockAuthClient.renewSession.mockResolvedValueOnce({\n success: true,\n jwt,\n });\n}\n\n/**\n * Configures mock AuthClient to return failure for renewSession\n */\nexport function mockRenewSessionFailure(\n mockAuthClient: jest.Mocked<AuthClient>,\n error: string = 'RENEWAL_FAILED'\n): void {\n mockAuthClient.renewSession.mockResolvedValueOnce({\n success: false,\n error,\n });\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/authClient.mock.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
55→ payload,
56→ });
57→}
58→
59→/**
60→ * Configures mock AuthClient to return failure
61→ */
62→export function mockVerifyTokenFailure(
63→ mockAuthClient: jest.Mocked<AuthClient>,
64→ error: string = 'INVALID_TOKEN'
65→): void {
66→ mockAuthClient.verifyToken.mockResolvedValueOnce({
67→ success: false,
68→ error,
69→ });
70→}
71→
72→/**
73→ * Configures mock AuthClient to return success for renewSession with new JWT
74→ */
75→export function mockRenewSessionSuccess(
76→ mockAuthClient: jest.Mocked<AuthClient>,
77→ jwt: string = 'new-jwt-token'
78→): void {
79→ mockAuthClient.renewSession.mockResolvedValueOnce({
80→ success: true,
81→ jwt,
82→ });
83→}
84→
85→/**
86→ * Configures mock AuthClient to return failure for renewSession
87→ */
88→export function mockRenewSessionFailure(
89→ mockAuthClient: jest.Mocked<AuthClient>,
90→ error: string = 'RENEWAL_FAILED'
91→): void {
92→ mockAuthClient.renewSession.mockResolvedValueOnce({
93→ success: false,
94→ error,
95→ });
96→}
97→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
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "completed",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "in_progress",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "pending",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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
Now let me create the handlers directory and the handler.types.ts file.
Bash
{
"command": "mkdir -p /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers",
"description": "Create handlers directory"
}Tool Result
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/handler.types.ts",
"content": "'use strict';\n\nimport type { CwcRole } from 'cwc-types';\nimport type { RequestContext } from '../context';\n\n// ============================================================================\n// Error Codes\n// ============================================================================\n\n/**\n * Error codes for cwc-api operations\n */\nexport type CwcApiErrorCode =\n // Access control errors\n | 'UNAUTHORIZED' // No valid authentication\n | 'FORBIDDEN' // Authenticated but not allowed\n | 'ROUTE_ACCESS_DENIED' // Role cannot access this route\n | 'OPERATION_ACCESS_DENIED' // Role cannot perform this operation\n // Resource errors\n | 'NOT_FOUND' // Resource does not exist\n | 'ALREADY_EXISTS' // Duplicate resource\n // Validation errors\n | 'VALIDATION_ERROR' // Request payload validation failed\n | 'INVALID_PARAMETER' // Invalid query/path parameter\n // System errors\n | 'INTERNAL_ERROR' // Unexpected server error\n | 'DATABASE_ERROR' // Database operation failed\n | 'AUTH_SERVICE_ERROR'; // cwc-auth communication failed\n\n// ============================================================================\n// HTTP Status Codes\n// ============================================================================\n\nexport type CwcApiSuccessStatusCode = 200;\nexport type CwcApiErrorStatusCode = 400 | 401 | 403 | 404 | 500;\nexport type CwcApiStatusCode = CwcApiSuccessStatusCode | CwcApiErrorStatusCode;\n\n// ============================================================================\n// Response Types\n// ============================================================================\n\n/**\n * Pagination metadata for list responses\n */\nexport type CwcApiPagination = {\n page: number;\n pageSize: number;\n totalCount: number;\n hasMore: boolean;\n};\n\n/**\n * Success response envelope\n */\nexport type CwcApiSuccessResponse<T = unknown> = {\n success: true;\n data: T;\n pagination?: CwcApiPagination | undefined;\n jwt?: string | undefined; // New JWT if session was renewed\n};\n\n/**\n * Error response envelope\n */\nexport type CwcApiErrorResponse = {\n success: false;\n errorCode: CwcApiErrorCode;\n errorMessage: string;\n errorDetail?: string | undefined; // Dev-only\n};\n\n/**\n * Union of all response types\n */\nexport type CwcApiResponse<T = unknown> = CwcApiSuccessResponse<T> | CwcApiErrorResponse;\n\n/**\n * Full handler response with HTTP status code\n */\nexport type CwcApiHandlerResponse<T = unknown> = {\n statusCode: CwcApiStatusCode;\n body: CwcApiResponse<T>;\n};\n\n// ============================================================================\n// Route Configuration\n// ============================================================================\n\n/**\n * Handler type: query (read-only) or mutation (write)\n */\nexport type CwcApiHandlerType = 'query' | 'mutation';\n\n/**\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};\n\n/**\n * Map of route paths to configurations\n */\nexport type CwcApiRouteConfigs = {\n [key: string]: CwcApiRouteConfig;\n};\n\n// ============================================================================\n// Operation Types\n// ============================================================================\n\n/**\n * Operation context passed to worker functions\n */\nexport type OperationContext = {\n context: RequestContext;\n projectId?: string | undefined; // For project-scoped operations\n resourceId?: string | undefined; // For resource-specific operations\n};\n\n/**\n * Operation result from worker functions\n */\nexport type OperationResult<T = unknown> =\n | { success: true; data: T; pagination?: CwcApiPagination | undefined }\n | { success: false; errorCode: CwcApiErrorCode; errorMessage: string };\n\n/**\n * Operation function signature (worker)\n */\nexport type CwcApiOperation<TPayload = unknown, TResult = unknown> = (\n payload: TPayload,\n operationContext: OperationContext\n) => Promise<OperationResult<TResult>>;\n\n// ============================================================================\n// Handler Options\n// ============================================================================\n\n/**\n * Base options for all handlers\n */\nexport type BaseHandlerOptions = {\n context: RequestContext;\n routeConfig: CwcApiRouteConfig;\n authHeader: string | undefined;\n};\n\n/**\n * Options for RouteHandler\n */\nexport type RouteHandlerOptions = BaseHandlerOptions & {\n payload: unknown;\n pathParams?: Record<string, string> | undefined;\n};\n\n/**\n * Options for QueryHandler\n */\nexport type QueryHandlerOptions = BaseHandlerOptions & {\n payload: unknown;\n operationContext: OperationContext;\n};\n\n/**\n * Options for MutationHandler\n */\nexport type MutationHandlerOptions = QueryHandlerOptions;\n\n// ============================================================================\n// Handler Interface\n// ============================================================================\n\n/**\n * Interface for all handlers\n */\nexport interface CwcApiHandler {\n processRequest(): Promise<CwcApiHandlerResponse>;\n}\n\n// ============================================================================\n// Access Policy Types (Stubs for Phase 4)\n// ============================================================================\n\n/**\n * Route access check result\n */\nexport type RouteAccessResult = {\n allowed: boolean;\n reason?: string | undefined;\n};\n\n/**\n * Operation access check result\n */\nexport type OperationAccessResult = {\n allowed: boolean;\n effectiveRole?: CwcRole | undefined; // Role for this specific operation\n reason?: string | undefined;\n};\n\n/**\n * Route access policy checker interface (Phase 4)\n */\nexport interface RouteAccessPolicy {\n checkRouteAccess(context: RequestContext, routeConfig: CwcApiRouteConfig): RouteAccessResult;\n}\n\n/**\n * Operation access policy checker interface (Phase 4)\n */\nexport interface OperationAccessPolicy {\n checkOperationAccess(\n context: RequestContext,\n operationContext: OperationContext\n ): Promise<OperationAccessResult>;\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/handler.types.ts
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
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "completed",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "completed",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "in_progress",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "pending",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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
Now let me create the QueryHandler.ts file.
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts",
"content": "'use strict';\n\nimport type { ILogger, AuthClient } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n QueryHandlerOptions,\n OperationAccessResult,\n CwcApiErrorCode,\n} from './handler.types';\n\nconst codeLocation = 'handlers/QueryHandler.ts';\n\n/**\n * QueryHandler - Handles read-only operations\n *\n * Responsibilities:\n * 1. Check operation-level access (Phase 4 stub)\n * 2. Execute the query operation\n * 3. Renew session (always, to keep session active)\n * 4. Format successful response with data/pagination and new JWT\n */\nexport class QueryHandler implements CwcApiHandler {\n private options: QueryHandlerOptions;\n private config: CwcApiConfig;\n private authClient: AuthClient;\n private logger: ILogger | undefined;\n\n constructor(\n options: QueryHandlerOptions,\n config: CwcApiConfig,\n authClient: AuthClient,\n logger: ILogger | undefined\n ) {\n this.options = options;\n this.config = config;\n this.authClient = authClient;\n this.logger = logger;\n }\n\n public async processRequest(): Promise<CwcApiHandlerResponse> {\n const { context, routeConfig, payload, operationContext, authHeader } = 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 this.createOperationDeniedResponse(operationAccess.reason);\n }\n\n // Step 2: Execute the operation\n const result = await routeConfig.operation(payload, operationContext);\n\n if (!result.success) {\n return this.createOperationErrorResponse(result.errorCode, result.errorMessage);\n }\n\n // Step 3: Renew session if authenticated\n let newJwt: string | undefined;\n if (context.isAuthenticated) {\n const renewResult = await this.authClient.renewSession(authHeader);\n if (renewResult.success) {\n newJwt = renewResult.jwt;\n } else {\n // Log warning but don't fail the operation\n this.logger?.logError({\n userPkId: context.userPkId,\n codeLocation,\n message: `Session renewal failed for query operation`,\n error: renewResult.error,\n });\n }\n }\n\n // Step 4: Format response\n return {\n statusCode: 200,\n body: {\n success: true,\n data: result.data,\n ...(result.pagination ? { pagination: result.pagination } : {}),\n ...(newJwt ? { jwt: newJwt } : {}),\n },\n };\n } catch (error) {\n this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error executing query operation for ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(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\n private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {\n return {\n statusCode: 403,\n body: {\n success: false,\n errorCode: 'OPERATION_ACCESS_DENIED',\n errorMessage: 'Operation not permitted',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createOperationErrorResponse(\n errorCode: CwcApiErrorCode,\n errorMessage: string\n ): CwcApiHandlerResponse {\n const statusCode = this.mapErrorCodeToStatus(errorCode);\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage,\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n\n private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {\n switch (errorCode) {\n case 'NOT_FOUND':\n return 404;\n case 'VALIDATION_ERROR':\n case 'INVALID_PARAMETER':\n case 'ALREADY_EXISTS':\n return 400;\n case 'UNAUTHORIZED':\n return 401;\n case 'FORBIDDEN':\n case 'ROUTE_ACCESS_DENIED':\n case 'OPERATION_ACCESS_DENIED':\n return 403;\n case 'INTERNAL_ERROR':\n case 'DATABASE_ERROR':\n case 'AUTH_SERVICE_ERROR':\n return 500;\n default:\n return 500;\n }\n }\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts
Now let me create the test file for QueryHandler. First, I'll create the routeConfig mock helper, then the test file.
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts",
"content": "'use strict';\n\nimport { jest } from '@jest/globals';\nimport type { CwcRole } from 'cwc-types';\nimport type {\n CwcApiRouteConfig,\n CwcApiHandlerType,\n OperationContext,\n OperationResult,\n} from '../../handlers/handler.types';\nimport type { RequestContext, GuestContext, AuthenticatedContext } from '../../context';\n\n/**\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}\n\n/**\n * Creates a mock operation context for testing\n */\nexport function createMockOperationContext(\n overrides: Partial<OperationContext> = {}\n): OperationContext {\n return {\n context: createMockGuestContext(),\n ...overrides,\n };\n}\n\n/**\n * Creates a mock guest context for testing\n */\nexport function createMockGuestContext(): GuestContext {\n return {\n isAuthenticated: false,\n role: 'guest-user',\n };\n}\n\n/**\n * Creates a mock authenticated context for testing\n */\nexport function createMockAuthenticatedContext(\n overrides: Partial<Omit<AuthenticatedContext, 'isAuthenticated'>> = {}\n): AuthenticatedContext {\n return {\n isAuthenticated: true,\n role: 'logged-on-user',\n userPkId: 123,\n username: 'testuser@example.com',\n ownedProjects: ['project-1', 'project-2'],\n payload: {\n jti: 'test-jwt-id',\n sub: 123,\n iat: Math.floor(Date.now() / 1000),\n exp: Math.floor(Date.now() / 1000) + 900,\n login: {\n username: 'testuser@example.com',\n deviceId: 'test-device-id',\n userJwtId: 'test-jwt-id',\n loginType: 'cwc',\n kulo: false,\n ownedProjects: ['project-1', 'project-2'],\n isGuestUser: false,\n },\n },\n ...overrides,\n };\n}\n\n/**\n * Configures mock operation to return success\n */\nexport function mockOperationSuccess<TResult>(\n routeConfig: CwcApiRouteConfig<unknown, TResult>,\n data: TResult,\n pagination?: { page: number; pageSize: number; totalCount: number; hasMore: boolean }\n): void {\n const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;\n mockFn.mockResolvedValueOnce({\n success: true,\n data,\n ...(pagination ? { pagination } : {}),\n });\n}\n\n/**\n * Configures mock operation to return failure\n */\nexport function mockOperationFailure(\n routeConfig: CwcApiRouteConfig,\n errorCode: string = 'NOT_FOUND',\n errorMessage: string = 'Resource not found'\n): void {\n const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;\n mockFn.mockResolvedValueOnce({\n success: false,\n errorCode: errorCode as any,\n errorMessage,\n });\n}\n\n/**\n * Configures mock operation to throw an error\n */\nexport function mockOperationThrows(\n routeConfig: CwcApiRouteConfig,\n error: Error = new Error('Unexpected error')\n): void {\n const mockFn = routeConfig.operation as jest.MockedFunction<typeof routeConfig.operation>;\n mockFn.mockRejectedValueOnce(error);\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts
Bash
{
"command": "mkdir -p /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers",
"description": "Create handlers test directory"
}Tool Result
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/context/createContext.test.ts"
}Tool Result
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { createContext } from '../../context/createContext';
5→import {
6→ createMockAuthClient,
7→ createMockUserJwtPayload,
8→ mockVerifyTokenSuccess,
9→ mockVerifyTokenFailure,
10→} from '../mocks';
11→import type { AuthClient } from 'cwc-backend-utils';
12→
13→describe('createContext', () => {
14→ let mockAuthClient: jest.Mocked<AuthClient>;
15→
16→ beforeEach(() => {
17→ mockAuthClient = createMockAuthClient();
18→ jest.clearAllMocks();
19→ });
20→
21→ describe('Guest Context', () => {
22→ it('should return guest context when no auth header is provided', async () => {
23→ const result = await createContext({
24→ authHeader: undefined,
25→ authClient: mockAuthClient,
26→ });
27→
28→ expect(result.isAuthenticated).toBe(false);
29→ expect(result.role).toBe('guest-user');
30→ expect(mockAuthClient.verifyToken).not.toHaveBeenCalled();
31→ });
32→
33→ it('should return guest context when auth header is empty string', async () => {
34→ mockVerifyTokenFailure(mockAuthClient, 'MISSING_TOKEN');
35→
36→ const result = await createContext({
37→ authHeader: '',
38→ authClient: mockAuthClient,
39→ });
40→
41→ // Empty string is still passed to verifyToken, which returns failure
42→ expect(result.isAuthenticated).toBe(false);
43→ expect(result.role).toBe('guest-user');
44→ });
45→
46→ it('should return guest context when token verification fails with INVALID_TOKEN', async () => {
47→ mockVerifyTokenFailure(mockAuthClient, 'INVALID_TOKEN');
48→
49→ const result = await createContext({
50→ authHeader: 'Bearer invalid-token',
51→ authClient: mockAuthClient,
52→ });
53→
54→ expect(result.isAuthenticated).toBe(false);
55→ expect(result.role).toBe('guest-user');
56→ expect(mockAuthClient.verifyToken).toHaveBeenCalledWith('Bearer invalid-token');
57→ });
58→
59→ it('should return guest context when token verification fails with TOKEN_EXPIRED', async () => {
60→ mockVerifyTokenFailure(mockAuthClient, 'TOKEN_EXPIRED');
61→
62→ const result = await createContext({
63→ authHeader: 'Bearer expired-token',
64→ authClient: mockAuthClient,
65→ });
66→
67→ expect(result.isAuthenticated).toBe(false);
68→ expect(result.role).toBe('guest-user');
69→ });
70→
71→ it('should return guest context when auth service returns error', async () => {
72→ mockVerifyTokenFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');
73→
74→ const result = await createContext({
75→ authHeader: 'Bearer some-token',
76→ authClient: mockAuthClient,
77→ });
78→
79→ expect(result.isAuthenticated).toBe(false);
80→ expect(result.role).toBe('guest-user');
81→ });
82→ });
83→
84→ describe('Authenticated Context', () => {
85→ it('should return authenticated context with correct user data on valid token', async () => {
86→ const mockPayload = createMockUserJwtPayload({
87→ sub: 456,
88→ login: {
89→ username: 'authenticateduser@test.com',
90→ deviceId: 'device-abc',
91→ userJwtId: 'jwt-abc',
92→ loginType: 'cwc',
93→ kulo: false,
94→ ownedProjects: ['proj-a', 'proj-b', 'proj-c'],
95→ isGuestUser: false,
96→ },
97→ });
98→ mockVerifyTokenSuccess(mockAuthClient, mockPayload);
99→
100→ const result = await createContext({
101→ authHeader: 'Bearer valid-token',
102→ authClient: mockAuthClient,
103→ });
104→
105→ expect(result.isAuthenticated).toBe(true);
106→ if (result.isAuthenticated) {
107→ expect(result.role).toBe('logged-on-user');
108→ expect(result.userPkId).toBe(456);
109→ expect(result.username).toBe('authenticateduser@test.com');
110→ expect(result.ownedProjects).toEqual(['proj-a', 'proj-b', 'proj-c']);
111→ expect(result.payload).toBe(mockPayload);
112→ }
113→ });
114→
115→ it('should return authenticated context with empty owned projects array', async () => {
116→ const mockPayload = createMockUserJwtPayload({
117→ login: {
118→ username: 'newuser@test.com',
119→ deviceId: 'device-new',
120→ userJwtId: 'jwt-new',
121→ loginType: 'cwc',
122→ kulo: false,
123→ ownedProjects: [],
124→ isGuestUser: false,
125→ },
126→ });
127→ mockVerifyTokenSuccess(mockAuthClient, mockPayload);
128→
129→ const result = await createContext({
130→ authHeader: 'Bearer valid-token',
131→ authClient: mockAuthClient,
132→ });
133→
134→ expect(result.isAuthenticated).toBe(true);
135→ if (result.isAuthenticated) {
136→ expect(result.ownedProjects).toEqual([]);
137→ }
138→ });
139→
140→ it('should use default role of logged-on-user for all authenticated users', async () => {
141→ mockVerifyTokenSuccess(mockAuthClient);
142→
143→ const result = await createContext({
144→ authHeader: 'Bearer valid-token',
145→ authClient: mockAuthClient,
146→ });
147→
148→ expect(result.isAuthenticated).toBe(true);
149→ if (result.isAuthenticated) {
150→ // Role starts as 'logged-on-user', actual project-owner check happens per-operation
151→ expect(result.role).toBe('logged-on-user');
152→ }
153→ });
154→
155→ it('should include full payload in authenticated context', async () => {
156→ const mockPayload = createMockUserJwtPayload();
157→ mockVerifyTokenSuccess(mockAuthClient, mockPayload);
158→
159→ const result = await createContext({
160→ authHeader: 'Bearer valid-token',
161→ authClient: mockAuthClient,
162→ });
163→
164→ expect(result.isAuthenticated).toBe(true);
165→ if (result.isAuthenticated) {
166→ expect(result.payload).toEqual(mockPayload);
167→ expect(result.payload.jti).toBe(mockPayload.jti);
168→ expect(result.payload.sub).toBe(mockPayload.sub);
169→ expect(result.payload.iat).toBe(mockPayload.iat);
170→ expect(result.payload.exp).toBe(mockPayload.exp);
171→ expect(result.payload.login).toBe(mockPayload.login);
172→ }
173→ });
174→ });
175→
176→ describe('Authorization header handling', () => {
177→ it('should pass full authorization header to verifyToken', async () => {
178→ mockVerifyTokenSuccess(mockAuthClient);
179→
180→ await createContext({
181→ authHeader: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test',
182→ authClient: mockAuthClient,
183→ });
184→
185→ expect(mockAuthClient.verifyToken).toHaveBeenCalledWith(
186→ 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test'
187→ );
188→ });
189→ });
190→});
191→
<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__/mocks/index.ts"
}Tool Result
1→'use strict';
2→
3→export {
4→ createMockSqlClient,
5→ mockSelectSuccess,
6→ mockSelectNotFound,
7→ mockListSuccess,
8→ mockInsertSuccess,
9→ mockInsertFailure,
10→ mockUpdateSuccess,
11→ mockUpdateNotFound,
12→ mockDeleteSuccess,
13→ mockDeleteNotFound,
14→} from './sqlClient.mock';
15→
16→export {
17→ getTestConfig,
18→ createMockConfig,
19→ createProductionConfig,
20→ createUnitTestConfig,
21→} from './config.mock';
22→
23→export { createMockLogger } from './logger.mock';
24→
25→export {
26→ createMockAuthClient,
27→ createMockUserJwtPayload,
28→ mockVerifyTokenSuccess,
29→ mockVerifyTokenFailure,
30→} from './authClient.mock';
31→
<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/index.ts",
"old_string": "export {\n createMockAuthClient,\n createMockUserJwtPayload,\n mockVerifyTokenSuccess,\n mockVerifyTokenFailure,\n} from './authClient.mock';",
"new_string": "export {\n createMockAuthClient,\n createMockUserJwtPayload,\n mockVerifyTokenSuccess,\n mockVerifyTokenFailure,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n} from './authClient.mock';\n\nexport {\n createMockRouteConfig,\n createMockOperationContext,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n} from './routeConfig.mock';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
21→} from './config.mock';
22→
23→export { createMockLogger } from './logger.mock';
24→
25→export {
26→ createMockAuthClient,
27→ createMockUserJwtPayload,
28→ mockVerifyTokenSuccess,
29→ mockVerifyTokenFailure,
30→ mockRenewSessionSuccess,
31→ mockRenewSessionFailure,
32→} from './authClient.mock';
33→
34→export {
35→ createMockRouteConfig,
36→ createMockOperationContext,
37→ createMockGuestContext,
38→ createMockAuthenticatedContext,
39→ mockOperationSuccess,
40→ mockOperationFailure,
41→ mockOperationThrows,
42→} from './routeConfig.mock';
43→Now let me create the QueryHandler test file.
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"content": "'use strict';\n\nimport { describe, it, expect, beforeEach, jest } from '@jest/globals';\nimport { QueryHandler } from '../../handlers/QueryHandler';\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getTestConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';\nimport type { AuthClient } from 'cwc-backend-utils';\nimport type { QueryHandlerOptions } from '../../handlers/handler.types';\n\ndescribe('QueryHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const testConfig = getTestConfig();\n const prodConfig = createProductionConfig();\n\n beforeEach(() => {\n mockAuthClient = createMockAuthClient();\n jest.clearAllMocks();\n });\n\n describe('Successful Operations', () => {\n it('should return 200 with data on successful query', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1, name: 'test' });\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, testConfig, 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: 1, name: 'test' });\n }\n });\n\n it('should include pagination when operation returns it', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const pagination = { page: 1, pageSize: 20, totalCount: 100, hasMore: true };\n mockOperationSuccess(routeConfig, [{ id: 1 }, { id: 2 }], pagination);\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, testConfig, 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.pagination).toEqual(pagination);\n }\n });\n });\n\n describe('Session Renewal', () => {\n it('should call renewSession for authenticated users', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new QueryHandler(options, testConfig, 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 not call renewSession for guest users', 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, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\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 const context = createMockAuthenticatedContext();\n const mockLogger = createMockLogger();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new QueryHandler(options, testConfig, 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 });\n }\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n\n it('should not include jwt in response when renewal fails', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new QueryHandler(options, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.jwt).toBeUndefined();\n }\n });\n });\n\n describe('Error Responses', () => {\n it('should return 404 for NOT_FOUND error code', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');\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, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(404);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('NOT_FOUND');\n expect(response.body.errorMessage).toBe('Resource not found');\n }\n });\n\n it('should return 400 for VALIDATION_ERROR', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');\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, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(400);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('VALIDATION_ERROR');\n }\n });\n\n it('should return 403 for OPERATION_ACCESS_DENIED', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationFailure(routeConfig, 'OPERATION_ACCESS_DENIED', 'Not permitted');\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, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(403);\n expect(response.body.success).toBe(false);\n });\n\n it('should return 500 for DATABASE_ERROR', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');\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, testConfig, 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('DATABASE_ERROR');\n }\n });\n });\n\n describe('Exception Handling', () => {\n it('should catch errors and return 500 response', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationThrows(routeConfig, new Error('Unexpected database error'));\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, testConfig, 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 expect(response.body.errorMessage).toBe('An internal error occurred');\n }\n });\n\n it('should include error detail in development mode', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n // testConfig has isDevelopment: true\n const handler = new QueryHandler(options, testConfig, 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('Detailed error message');\n }\n });\n\n it('should not include error detail in production mode', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n // prodConfig has isDevelopment: false\n const handler = new QueryHandler(options, prodConfig, 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\n it('should log errors when logger is provided', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockAuthenticatedContext();\n const mockLogger = createMockLogger();\n mockOperationThrows(routeConfig, new Error('Test error'));\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new QueryHandler(options, testConfig, mockAuthClient, mockLogger);\n await handler.processRequest();\n\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n });\n\n 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, testConfig, mockAuthClient, 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});\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts
Let me run the tests to verify they pass.
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPattern=\"QueryHandler\" 2>&1",
"description": "Run QueryHandler tests",
"timeout": 120000
}Tool Result
Exit code 1 > cwc-api@1.0.0 test /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > RUNTIME_ENVIRONMENT=unit jest "--testPathPattern=QueryHandler" 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 /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=QueryHandler"` Exit status 1
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"QueryHandler\" 2>&1",
"description": "Run QueryHandler tests with correct option",
"timeout": 120000
}Tool Result
Exit code 1
> cwc-api@1.0.0 test /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api
> RUNTIME_ENVIRONMENT=unit jest "--testPathPatterns=QueryHandler"
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)
FAIL src/__tests__/handlers/QueryHandler.test.ts
QueryHandler
Successful Operations
✓ should return 200 with data on successful query (2 ms)
✓ should include pagination when operation returns it
Session Renewal
✓ should call renewSession for authenticated users (1 ms)
✓ should not call renewSession for guest users
✓ should succeed operation when renewal fails (graceful handling) (1 ms)
✓ should not include jwt in response when renewal fails
Error Responses
✓ should return 404 for NOT_FOUND error code
✓ should return 400 for VALIDATION_ERROR (1 ms)
✓ should return 403 for OPERATION_ACCESS_DENIED
✓ should return 500 for DATABASE_ERROR
Exception Handling
✓ should catch errors and return 500 response
✕ should include error detail in development mode (1 ms)
✓ should not include error detail in production mode
✓ should log errors when logger is provided (1 ms)
Operation Access (Phase 3 Stub)
✓ should allow operation access (stub always returns allowed)
● QueryHandler › Exception Handling › should include error detail in development mode
expect(received).toBe(expected) // Object.is equality
Expected: "Detailed error message"
Received: undefined
316 | expect(response.body.success).toBe(false);
317 | if (!response.body.success) {
> 318 | expect(response.body.errorDetail).toBe('Detailed error message');
| ^
319 | }
320 | });
321 |
at Object.<anonymous> (src/__tests__/handlers/QueryHandler.test.ts:318:43)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 14 passed, 15 total
Snapshots: 0 total
Time: 0.316 s
Ran all test suites matching QueryHandler.
/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 "--testPathPatterns=QueryHandler"`
Exit status 1Let me check what getTestConfig returns to understand the issue.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts"
}Tool Result
1→'use strict';
2→
3→import type { CwcApiConfig } from '../../config/config.types';
4→import { loadConfig } from '../../config';
5→
6→/**
7→ * Gets the real config loaded from unit.cwc-api.env
8→ * Use this as the base for test config
9→ */
10→export function getTestConfig(): CwcApiConfig {
11→ return loadConfig();
12→}
13→
14→/**
15→ * Creates a mock config by applying overrides to the real test config
16→ * Useful when you need to test specific config variations (e.g., production mode)
17→ */
18→export function createMockConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
19→ const baseConfig = loadConfig();
20→ return { ...baseConfig, ...overrides };
21→}
22→
23→/**
24→ * Creates a production config for testing production error behavior
25→ * Applies production-like settings while preserving secrets from env
26→ */
27→export function createProductionConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
28→ return createMockConfig({
29→ runtimeEnvironment: 'prod',
30→ isProduction: true,
31→ isDevelopment: false,
32→ isTest: false,
33→ isUnitTest: false,
34→ isEndToEnd: false,
35→ debugMode: false,
36→ ...overrides,
37→ });
38→}
39→
40→/**
41→ * Creates a unit test config (should match what's loaded from env)
42→ * Provided for compatibility with existing tests
43→ */
44→export function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
45→ return createMockConfig({
46→ runtimeEnvironment: 'unit',
47→ isProduction: false,
48→ isDevelopment: false,
49→ isTest: false,
50→ isUnitTest: true,
51→ isEndToEnd: false,
52→ debugMode: false,
53→ ...overrides,
54→ });
55→}
56→
<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 add a development config helper for the test. Let me update the config mock and fix the test.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts",
"old_string": "/**\n * Creates a unit test config (should match what's loaded from env)\n * Provided for compatibility with existing tests\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}",
"new_string": "/**\n * Creates a unit test config (should match what's loaded from env)\n * Provided for compatibility with existing tests\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a development config for testing dev-only error behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createDevelopmentConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
36→ ...overrides,
37→ });
38→}
39→
40→/**
41→ * Creates a unit test config (should match what's loaded from env)
42→ * Provided for compatibility with existing tests
43→ */
44→export function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
45→ return createMockConfig({
46→ runtimeEnvironment: 'unit',
47→ isProduction: false,
48→ isDevelopment: false,
49→ isTest: false,
50→ isUnitTest: true,
51→ isEndToEnd: false,
52→ debugMode: false,
53→ ...overrides,
54→ });
55→}
56→
57→/**
58→ * Creates a development config for testing dev-only error behavior
59→ * Has isDevelopment: true so error details are exposed
60→ */
61→export function createDevelopmentConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
62→ return createMockConfig({
63→ runtimeEnvironment: 'dev',
64→ isProduction: false,
65→ isDevelopment: true,
66→ isTest: false,
67→ isUnitTest: false,
68→ isEndToEnd: false,
69→ debugMode: true,
70→ ...overrides,
71→ });
72→}
73→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts",
"old_string": "export {\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createUnitTestConfig,\n} from './config.mock';",
"new_string": "export {\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createUnitTestConfig,\n createDevelopmentConfig,\n} from './config.mock';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
12→ mockDeleteSuccess,
13→ mockDeleteNotFound,
14→} from './sqlClient.mock';
15→
16→export {
17→ getTestConfig,
18→ createMockConfig,
19→ createProductionConfig,
20→ createUnitTestConfig,
21→ createDevelopmentConfig,
22→} from './config.mock';
23→
24→export { createMockLogger } from './logger.mock';
25→
26→export {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getTestConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getTestConfig,\n createProductionConfig,\n createDevelopmentConfig,\n createMockLogger,\n} from '../mocks';"
}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:
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { QueryHandler } from '../../handlers/QueryHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationFailure,
12→ mockOperationThrows,
13→ mockRenewSessionSuccess,
14→ mockRenewSessionFailure,
15→ getTestConfig,
16→ createProductionConfig,
17→ createDevelopmentConfig,
18→ createMockLogger,
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": " it('should include error detail in development mode', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n // testConfig has isDevelopment: true\n const handler = new QueryHandler(options, testConfig, 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('Detailed error message');\n }\n });",
"new_string": " it('should include error detail in development mode', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const devConfig = createDevelopmentConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n // devConfig has isDevelopment: true\n const handler = new QueryHandler(options, devConfig, 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('Detailed error message');\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:
296→ expect(response.body.errorMessage).toBe('An internal error occurred');
297→ }
298→ });
299→
300→ it('should include error detail in development mode', async () => {
301→ const routeConfig = createMockRouteConfig();
302→ const context = createMockGuestContext();
303→ const devConfig = createDevelopmentConfig();
304→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
305→
306→ const options: QueryHandlerOptions = {
307→ context,
308→ routeConfig,
309→ authHeader: undefined,
310→ payload: {},
311→ operationContext: { context },
312→ };
313→
314→ // devConfig has isDevelopment: true
315→ const handler = new QueryHandler(options, devConfig, mockAuthClient, undefined);
316→ const response = await handler.processRequest();
317→
318→ expect(response.body.success).toBe(false);
319→ if (!response.body.success) {
320→ expect(response.body.errorDetail).toBe('Detailed error message');
321→ }
322→ });
323→
324→ it('should not include error detail in production mode', async () => {
325→ const routeConfig = createMockRouteConfig();
326→ const context = createMockGuestContext();Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"QueryHandler\" 2>&1",
"description": "Run QueryHandler tests again",
"timeout": 120000
}Can you explain why you created `createDevelopmentConfig`? We have a runtime environment specifically for unit testing: `unit`. There are special .env files for the `unit` environment.
You're right. I shouldn't have created a separate createDevelopmentConfig function. The unit environment is specifically for testing, and it has its own behavior. For tests that need to verify behavior under specific conditions like isDevelopment: true, we should use createMockConfig({ isDevelopment: true }) to override just that specific property.
Let me revert my changes to config.mock.ts and fix the test properly.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts",
"old_string": "/**\n * Creates a unit test config (should match what's loaded from env)\n * Provided for compatibility with existing tests\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a development config for testing dev-only error behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createDevelopmentConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}",
"new_string": "/**\n * Creates a unit test config (should match what's loaded from env)\n * Provided for compatibility with existing tests\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
36→ ...overrides,
37→ });
38→}
39→
40→/**
41→ * Creates a unit test config (should match what's loaded from env)
42→ * Provided for compatibility with existing tests
43→ */
44→export function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
45→ return createMockConfig({
46→ runtimeEnvironment: 'unit',
47→ isProduction: false,
48→ isDevelopment: false,
49→ isTest: false,
50→ isUnitTest: true,
51→ isEndToEnd: false,
52→ debugMode: false,
53→ ...overrides,
54→ });
55→}
56→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts",
"old_string": "export {\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createUnitTestConfig,\n createDevelopmentConfig,\n} from './config.mock';",
"new_string": "export {\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createUnitTestConfig,\n} from './config.mock';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
12→ mockDeleteSuccess,
13→ mockDeleteNotFound,
14→} from './sqlClient.mock';
15→
16→export {
17→ getTestConfig,
18→ createMockConfig,
19→ createProductionConfig,
20→ createUnitTestConfig,
21→} from './config.mock';
22→
23→export { createMockLogger } from './logger.mock';
24→
25→export {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getTestConfig,\n createProductionConfig,\n createDevelopmentConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';"
}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:
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { QueryHandler } from '../../handlers/QueryHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationFailure,
12→ mockOperationThrows,
13→ mockRenewSessionSuccess,
14→ mockRenewSessionFailure,
15→ getTestConfig,
16→ createMockConfig,
17→ createProductionConfig,
18→ createMockLogger,
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": " it('should include error detail in development mode', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const devConfig = createDevelopmentConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n // devConfig has isDevelopment: true\n const handler = new QueryHandler(options, devConfig, 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('Detailed error message');\n }\n });",
"new_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const configWithDevMode = createMockConfig({ isDevelopment: true });\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, configWithDevMode, 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('Detailed error message');\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:
296→ expect(response.body.errorMessage).toBe('An internal error occurred');
297→ }
298→ });
299→
300→ it('should include error detail when isDevelopment is true', async () => {
301→ const routeConfig = createMockRouteConfig();
302→ const context = createMockGuestContext();
303→ const configWithDevMode = createMockConfig({ isDevelopment: true });
304→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
305→
306→ const options: QueryHandlerOptions = {
307→ context,
308→ routeConfig,
309→ authHeader: undefined,
310→ payload: {},
311→ operationContext: { context },
312→ };
313→
314→ const handler = new QueryHandler(options, configWithDevMode, mockAuthClient, undefined);
315→ const response = await handler.processRequest();
316→
317→ expect(response.body.success).toBe(false);
318→ if (!response.body.success) {
319→ expect(response.body.errorDetail).toBe('Detailed error message');
320→ }
321→ });
322→
323→ it('should not include error detail in production mode', async () => {
324→ const routeConfig = createMockRouteConfig();
325→ const context = createMockGuestContext();Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"QueryHandler\" 2>&1",
"description": "Run QueryHandler tests again",
"timeout": 120000
}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 "--testPathPatterns=QueryHandler"
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/QueryHandler.test.ts
QueryHandler
Successful Operations
✓ should return 200 with data on successful query (2 ms)
✓ should include pagination when operation returns it
Session Renewal
✓ should call renewSession for authenticated users
✓ should not call renewSession for guest users (1 ms)
✓ should succeed operation when renewal fails (graceful handling)
✓ should not include jwt in response when renewal fails (1 ms)
Error Responses
✓ should return 404 for NOT_FOUND error code
✓ should return 400 for VALIDATION_ERROR
✓ should return 403 for OPERATION_ACCESS_DENIED (1 ms)
✓ should return 500 for DATABASE_ERROR
Exception Handling
✓ should catch errors and return 500 response (1 ms)
✓ should include error detail when isDevelopment is true
✓ should not include error detail in production mode
✓ should log errors when logger is provided
Operation Access (Phase 3 Stub)
✓ should allow operation access (stub always returns allowed) (1 ms)
Test Suites: 1 passed, 1 total
Tests: 15 passed, 15 total
Snapshots: 0 total
Time: 0.272 s, estimated 1 s
Ran all test suites matching QueryHandler.All 15 QueryHandler tests pass. Now let me update the todo list and create the MutationHandler.
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "completed",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "completed",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "in_progress",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "pending",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "pending",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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/handlers/MutationHandler.ts",
"content": "'use strict';\n\nimport type { ILogger, AuthClient } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n MutationHandlerOptions,\n OperationAccessResult,\n CwcApiErrorCode,\n} from './handler.types';\n\nconst codeLocation = 'handlers/MutationHandler.ts';\n\n/**\n * MutationHandler - Handles write operations\n *\n * Responsibilities:\n * 1. Check operation-level access (Phase 4 stub)\n * 2. Execute the mutation operation\n * 3. Renew session (always, to keep session active)\n * 4. Format successful response with data and new JWT\n */\nexport class MutationHandler implements CwcApiHandler {\n private options: MutationHandlerOptions;\n private config: CwcApiConfig;\n private authClient: AuthClient;\n private logger: ILogger | undefined;\n\n constructor(\n options: MutationHandlerOptions,\n config: CwcApiConfig,\n authClient: AuthClient,\n logger: ILogger | undefined\n ) {\n this.options = options;\n this.config = config;\n this.authClient = authClient;\n this.logger = logger;\n }\n\n public async processRequest(): Promise<CwcApiHandlerResponse> {\n const { context, routeConfig, payload, operationContext, authHeader } = 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 this.createOperationDeniedResponse(operationAccess.reason);\n }\n\n // Step 2: Execute the mutation\n const result = await routeConfig.operation(payload, operationContext);\n\n if (!result.success) {\n return this.createOperationErrorResponse(result.errorCode, result.errorMessage);\n }\n\n // Step 3: Renew session if authenticated\n let newJwt: string | undefined;\n if (context.isAuthenticated) {\n const renewResult = await this.authClient.renewSession(authHeader);\n if (renewResult.success) {\n newJwt = renewResult.jwt;\n } else {\n // Log warning but don't fail the operation\n this.logger?.logError({\n userPkId: context.userPkId,\n codeLocation,\n message: `Session renewal failed for mutation operation`,\n error: renewResult.error,\n });\n }\n }\n\n // Step 4: Format response (always 200 for RPC-style API)\n return {\n statusCode: 200,\n body: {\n success: true,\n data: result.data,\n ...(newJwt ? { jwt: newJwt } : {}),\n },\n };\n } catch (error) {\n this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error executing mutation operation for ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(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\n private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {\n return {\n statusCode: 403,\n body: {\n success: false,\n errorCode: 'OPERATION_ACCESS_DENIED',\n errorMessage: 'Operation not permitted',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createOperationErrorResponse(\n errorCode: CwcApiErrorCode,\n errorMessage: string\n ): CwcApiHandlerResponse {\n const statusCode = this.mapErrorCodeToStatus(errorCode);\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage,\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n\n private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {\n switch (errorCode) {\n case 'NOT_FOUND':\n return 404;\n case 'VALIDATION_ERROR':\n case 'INVALID_PARAMETER':\n case 'ALREADY_EXISTS':\n return 400;\n case 'UNAUTHORIZED':\n return 401;\n case 'FORBIDDEN':\n case 'ROUTE_ACCESS_DENIED':\n case 'OPERATION_ACCESS_DENIED':\n return 403;\n case 'INTERNAL_ERROR':\n case 'DATABASE_ERROR':\n case 'AUTH_SERVICE_ERROR':\n return 500;\n default:\n return 500;\n }\n }\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"content": "'use strict';\n\nimport { describe, it, expect, beforeEach, jest } from '@jest/globals';\nimport { MutationHandler } from '../../handlers/MutationHandler';\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';\nimport type { AuthClient } from 'cwc-backend-utils';\nimport type { MutationHandlerOptions } from '../../handlers/handler.types';\n\ndescribe('MutationHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const testConfig = getTestConfig();\n const prodConfig = createProductionConfig();\n\n beforeEach(() => {\n mockAuthClient = createMockAuthClient();\n jest.clearAllMocks();\n });\n\n describe('Successful Operations', () => {\n it('should return 200 with data on successful mutation', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1, created: true });\n mockRenewSessionSuccess(mockAuthClient);\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: { name: 'New Item' },\n operationContext: { context },\n };\n\n const handler = new MutationHandler(options, testConfig, 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: 1, created: true });\n }\n });\n\n it('should return 200 for all operations (RPC-style, no 201)', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 999, status: 'created' });\n mockRenewSessionSuccess(mockAuthClient);\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, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n // All POST operations return 200, not 201\n expect(response.statusCode).toBe(200);\n });\n });\n\n describe('Session Renewal', () => {\n it('should call renewSession for authenticated users', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new MutationHandler(options, testConfig, 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 not call renewSession for guest users', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n const handler = new MutationHandler(options, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(mockAuthClient.renewSession).not.toHaveBeenCalled();\n expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\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({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const mockLogger = createMockLogger();\n mockOperationSuccess(routeConfig, { id: 1, mutated: true });\n mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new MutationHandler(options, testConfig, 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\n it('should not include jwt in response when renewal fails', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer old-token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new MutationHandler(options, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.jwt).toBeUndefined();\n }\n });\n });\n\n describe('Error Responses', () => {\n it('should return 404 for NOT_FOUND error code', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');\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, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(404);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('NOT_FOUND');\n expect(response.body.errorMessage).toBe('Resource not found');\n }\n });\n\n it('should return 400 for ALREADY_EXISTS error code', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'ALREADY_EXISTS', 'Resource already exists');\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, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(400);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('ALREADY_EXISTS');\n }\n });\n\n it('should return 400 for VALIDATION_ERROR', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');\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, testConfig, mockAuthClient, undefined);\n const response = await handler.processRequest();\n\n expect(response.statusCode).toBe(400);\n expect(response.body.success).toBe(false);\n });\n\n it('should return 500 for DATABASE_ERROR', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');\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, testConfig, 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('DATABASE_ERROR');\n }\n });\n });\n\n describe('Exception Handling', () => {\n it('should catch errors and return 500 response', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationThrows(routeConfig, new Error('Unexpected database error'));\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, testConfig, 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 expect(response.body.errorMessage).toBe('An internal error occurred');\n }\n });\n\n it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const configWithDevMode = createMockConfig({ isDevelopment: true });\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, configWithDevMode, 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('Detailed error message');\n }\n });\n\n it('should not include error detail in production mode', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, prodConfig, 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\n it('should log errors when logger is provided', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const mockLogger = createMockLogger();\n mockOperationThrows(routeConfig, new Error('Test error'));\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, testConfig, mockAuthClient, mockLogger);\n await handler.processRequest();\n\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n });\n\n 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 mockRenewSessionSuccess(mockAuthClient);\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, testConfig, mockAuthClient, 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});\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"MutationHandler\" 2>&1",
"description": "Run MutationHandler tests",
"timeout": 120000
}maybe I was wrong about `createDevelopmentConfig` - I see that you created `createProductionConfig` and `createUnitTestConfig`, so maybe following that pattern with `createDevelopmentConfig` makes sense - I misunderstood its purpose. I would also like to ask about the naming of the `getTestConfig` mock function - the comment mentions `unit`, so shouldn't it be named `getUnitConfig` as we also have a runtime environement named `test`. Feel free to disagree with me and share your thoughts!
Good points! Let me share my thoughts:
On createDevelopmentConfig: I agree with you now. Following the pattern of createProductionConfig and createUnitTestConfig, having createDevelopmentConfig makes sense for consistency. Each provides a clearly-named way to test environment-specific behavior.
On getTestConfig vs getUnitConfig: You're right. The function loads config from unit.cwc-api.env, so getTestConfig is ambiguous since we have both unit and test runtime environments. Better options:
getUnitConfig()- clearer, matches the pattern- Or keep
getTestConfigwith a deprecation note and addgetUnitConfigas the preferred name
Let me update the code to:
- Add back
createDevelopmentConfig - Rename
getTestConfigtogetUnitConfig(with backwards-compatible alias if needed)
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts",
"old_string": "'use strict';\n\nimport type { CwcApiConfig } from '../../config/config.types';\nimport { loadConfig } from '../../config';\n\n/**\n * Gets the real config loaded from unit.cwc-api.env\n * Use this as the base for test config\n */\nexport function getTestConfig(): CwcApiConfig {\n return loadConfig();\n}",
"new_string": "'use strict';\n\nimport type { CwcApiConfig } from '../../config/config.types';\nimport { loadConfig } from '../../config';\n\n/**\n * Gets the real config loaded from unit.cwc-api.env\n * Use this as the base for test config\n */\nexport function getUnitConfig(): CwcApiConfig {\n return loadConfig();\n}\n\n/**\n * @deprecated Use getUnitConfig() instead. Kept for backwards compatibility.\n */\nexport function getTestConfig(): CwcApiConfig {\n return getUnitConfig();\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.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 { CwcApiConfig } from '../../config/config.types';
4→import { loadConfig } from '../../config';
5→
6→/**
7→ * Gets the real config loaded from unit.cwc-api.env
8→ * Use this as the base for test config
9→ */
10→export function getUnitConfig(): CwcApiConfig {
11→ return loadConfig();
12→}
13→
14→/**
15→ * @deprecated Use getUnitConfig() instead. Kept for backwards compatibility.
16→ */
17→export function getTestConfig(): CwcApiConfig {
18→ return getUnitConfig();
19→}
20→
21→/**
22→ * Creates a mock config by applying overrides to the real test config
23→ * Useful when you need to test specific config variations (e.g., production mode)Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts",
"old_string": "/**\n * Creates a unit test config (should match what's loaded from env)\n * Provided for compatibility with existing tests\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}",
"new_string": "/**\n * Creates a unit test config (should match what's loaded from env)\n * Provided for compatibility with existing tests\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a development config for testing dev-mode behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createDevelopmentConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
43→ ...overrides,
44→ });
45→}
46→
47→/**
48→ * Creates a unit test config (should match what's loaded from env)
49→ * Provided for compatibility with existing tests
50→ */
51→export function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
52→ return createMockConfig({
53→ runtimeEnvironment: 'unit',
54→ isProduction: false,
55→ isDevelopment: false,
56→ isTest: false,
57→ isUnitTest: true,
58→ isEndToEnd: false,
59→ debugMode: false,
60→ ...overrides,
61→ });
62→}
63→
64→/**
65→ * Creates a development config for testing dev-mode behavior
66→ * Has isDevelopment: true so error details are exposed
67→ */
68→export function createDevelopmentConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
69→ return createMockConfig({
70→ runtimeEnvironment: 'dev',
71→ isProduction: false,
72→ isDevelopment: true,
73→ isTest: false,
74→ isUnitTest: false,
75→ isEndToEnd: false,
76→ debugMode: true,
77→ ...overrides,
78→ });
79→}
80→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts",
"old_string": "export {\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createUnitTestConfig,\n} from './config.mock';",
"new_string": "export {\n getUnitConfig,\n getTestConfig, // @deprecated - use getUnitConfig\n createMockConfig,\n createProductionConfig,\n createUnitTestConfig,\n createDevelopmentConfig,\n} from './config.mock';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
12→ mockDeleteSuccess,
13→ mockDeleteNotFound,
14→} from './sqlClient.mock';
15→
16→export {
17→ getUnitConfig,
18→ getTestConfig, // @deprecated - use getUnitConfig
19→ createMockConfig,
20→ createProductionConfig,
21→ createUnitTestConfig,
22→ createDevelopmentConfig,
23→} from './config.mock';
24→
25→export { createMockLogger } from './logger.mock';
26→
27→export {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createDevelopmentConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';"
}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:
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { QueryHandler } from '../../handlers/QueryHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationFailure,
12→ mockOperationThrows,
13→ mockRenewSessionSuccess,
14→ mockRenewSessionFailure,
15→ getUnitConfig,
16→ createDevelopmentConfig,
17→ createProductionConfig,
18→ createMockLogger,
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {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('QueryHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const testConfig = getTestConfig();\n const prodConfig = createProductionConfig();",
"new_string": "describe('QueryHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const prodConfig = createProductionConfig();"
}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:
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {
24→ let mockAuthClient: jest.Mocked<AuthClient>;
25→ const unitConfig = getUnitConfig();
26→ const prodConfig = createProductionConfig();
27→
28→ beforeEach(() => {
29→ mockAuthClient = createMockAuthClient();
30→ jest.clearAllMocks();Edit
{
"replace_all": true,
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": "testConfig",
"new_string": "unitConfig"
}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. All occurrences of 'testConfig' were successfully replaced with 'unitConfig'.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const configWithDevMode = createMockConfig({ isDevelopment: true });\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, configWithDevMode, 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('Detailed error message');\n }\n });",
"new_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const devConfig = createDevelopmentConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, devConfig, 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('Detailed error message');\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:
296→ expect(response.body.errorMessage).toBe('An internal error occurred');
297→ }
298→ });
299→
300→ it('should include error detail when isDevelopment is true', async () => {
301→ const routeConfig = createMockRouteConfig();
302→ const context = createMockGuestContext();
303→ const devConfig = createDevelopmentConfig();
304→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
305→
306→ const options: QueryHandlerOptions = {
307→ context,
308→ routeConfig,
309→ authHeader: undefined,
310→ payload: {},
311→ operationContext: { context },
312→ };
313→
314→ const handler = new QueryHandler(options, devConfig, mockAuthClient, undefined);
315→ const response = await handler.processRequest();
316→
317→ expect(response.body.success).toBe(false);
318→ if (!response.body.success) {
319→ expect(response.body.errorDetail).toBe('Detailed error message');
320→ }
321→ });
322→
323→ it('should not include error detail in production mode', async () => {
324→ const routeConfig = createMockRouteConfig();
325→ const context = createMockGuestContext();Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getTestConfig,\n createMockConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createDevelopmentConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';"
}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:
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { MutationHandler } from '../../handlers/MutationHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationFailure,
12→ mockOperationThrows,
13→ mockRenewSessionSuccess,
14→ mockRenewSessionFailure,
15→ getUnitConfig,
16→ createDevelopmentConfig,
17→ createProductionConfig,
18→ createMockLogger,
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { MutationHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('MutationHandler', () => {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('MutationHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const testConfig = getTestConfig();\n const prodConfig = createProductionConfig();",
"new_string": "describe('MutationHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const prodConfig = createProductionConfig();"
}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:
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { MutationHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('MutationHandler', () => {
24→ let mockAuthClient: jest.Mocked<AuthClient>;
25→ const unitConfig = getUnitConfig();
26→ const prodConfig = createProductionConfig();
27→
28→ beforeEach(() => {
29→ mockAuthClient = createMockAuthClient();
30→ jest.clearAllMocks();Edit
{
"replace_all": true,
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": "testConfig",
"new_string": "unitConfig"
}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. All occurrences of 'testConfig' were successfully replaced with 'unitConfig'.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const configWithDevMode = createMockConfig({ isDevelopment: true });\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, configWithDevMode, 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('Detailed error message');\n }\n });",
"new_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const devConfig = createDevelopmentConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, devConfig, 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('Detailed error message');\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:
294→ expect(response.body.errorMessage).toBe('An internal error occurred');
295→ }
296→ });
297→
298→ it('should include error detail when isDevelopment is true', async () => {
299→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
300→ const context = createMockAuthenticatedContext();
301→ const devConfig = createDevelopmentConfig();
302→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
303→
304→ const options: MutationHandlerOptions = {
305→ context,
306→ routeConfig,
307→ authHeader: 'Bearer token',
308→ payload: {},
309→ operationContext: { context },
310→ };
311→
312→ const handler = new MutationHandler(options, devConfig, mockAuthClient, undefined);
313→ const response = await handler.processRequest();
314→
315→ expect(response.body.success).toBe(false);
316→ if (!response.body.success) {
317→ expect(response.body.errorDetail).toBe('Detailed error message');
318→ }
319→ });
320→
321→ it('should not include error detail in production mode', async () => {
322→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
323→ const context = createMockAuthenticatedContext();Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"Handler\" 2>&1",
"description": "Run all Handler tests",
"timeout": 120000
}One more comment on the choice of names - I would like to keep a 1:1 mapping (if possible) from our runtime envs (test, dev, prod, unit, e2e) with the function names, so rather than `createDevelopment...` use createDev...`
Good point! Let me rename to match the runtime environment names exactly:
dev→createDevConfigprod→createProdConfigunit→createUnitConfigtest→createTestConfig(if needed)e2e→createE2eConfig(if needed)
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts",
"old_string": "/**\n * Creates a production config for testing production error behavior\n * Applies production-like settings while preserving secrets from env\n */\nexport function createProductionConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'prod',\n isProduction: true,\n isDevelopment: false,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a unit test config (should match what's loaded from env)\n * Provided for compatibility with existing tests\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a development config for testing dev-mode behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createDevelopmentConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}",
"new_string": "/**\n * Creates a prod config for testing production error behavior\n * Applies production-like settings while preserving secrets from env\n */\nexport function createProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'prod',\n isProduction: true,\n isDevelopment: false,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * @deprecated Use createProdConfig() instead. Kept for backwards compatibility.\n */\nexport function createProductionConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createProdConfig(overrides);\n}\n\n/**\n * Creates a unit config (should match what's loaded from unit.cwc-api.env)\n */\nexport function createUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * @deprecated Use createUnitConfig() instead. Kept for backwards compatibility.\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createUnitConfig(overrides);\n}\n\n/**\n * Creates a dev config for testing dev-mode behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
26→ const baseConfig = loadConfig();
27→ return { ...baseConfig, ...overrides };
28→}
29→
30→/**
31→ * Creates a prod config for testing production error behavior
32→ * Applies production-like settings while preserving secrets from env
33→ */
34→export function createProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
35→ return createMockConfig({
36→ runtimeEnvironment: 'prod',
37→ isProduction: true,
38→ isDevelopment: false,
39→ isTest: false,
40→ isUnitTest: false,
41→ isEndToEnd: false,
42→ debugMode: false,
43→ ...overrides,
44→ });
45→}
46→
47→/**
48→ * @deprecated Use createProdConfig() instead. Kept for backwards compatibility.
49→ */
50→export function createProductionConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
51→ return createProdConfig(overrides);
52→}
53→
54→/**
55→ * Creates a unit config (should match what's loaded from unit.cwc-api.env)
56→ */
57→export function createUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
58→ return createMockConfig({
59→ runtimeEnvironment: 'unit',
60→ isProduction: false,
61→ isDevelopment: false,
62→ isTest: false,
63→ isUnitTest: true,
64→ isEndToEnd: false,
65→ debugMode: false,
66→ ...overrides,
67→ });
68→}
69→
70→/**
71→ * @deprecated Use createUnitConfig() instead. Kept for backwards compatibility.
72→ */
73→export function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
74→ return createUnitConfig(overrides);
75→}
76→
77→/**
78→ * Creates a dev config for testing dev-mode behavior
79→ * Has isDevelopment: true so error details are exposed
80→ */
81→export function createDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
82→ return createMockConfig({
83→ runtimeEnvironment: 'dev',
84→ isProduction: false,
85→ isDevelopment: true,
86→ isTest: false,
87→ isUnitTest: false,
88→ isEndToEnd: false,
89→ debugMode: true,
90→ ...overrides,
91→ });
92→}
93→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts",
"old_string": "export {\n getUnitConfig,\n getTestConfig, // @deprecated - use getUnitConfig\n createMockConfig,\n createProductionConfig,\n createUnitTestConfig,\n createDevelopmentConfig,\n} from './config.mock';",
"new_string": "export {\n getUnitConfig,\n getTestConfig, // @deprecated - use getUnitConfig\n createMockConfig,\n createProdConfig,\n createProductionConfig, // @deprecated - use createProdConfig\n createUnitConfig,\n createUnitTestConfig, // @deprecated - use createUnitConfig\n createDevConfig,\n} from './config.mock';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
12→ mockDeleteSuccess,
13→ mockDeleteNotFound,
14→} from './sqlClient.mock';
15→
16→export {
17→ getUnitConfig,
18→ getTestConfig, // @deprecated - use getUnitConfig
19→ createMockConfig,
20→ createProdConfig,
21→ createProductionConfig, // @deprecated - use createProdConfig
22→ createUnitConfig,
23→ createUnitTestConfig, // @deprecated - use createUnitConfig
24→ createDevConfig,
25→} from './config.mock';
26→
27→export { createMockLogger } from './logger.mock';
28→
29→export {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createDevelopmentConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createDevConfig,\n createProdConfig,\n createMockLogger,\n} from '../mocks';"
}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:
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { QueryHandler } from '../../handlers/QueryHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationFailure,
12→ mockOperationThrows,
13→ mockRenewSessionSuccess,
14→ mockRenewSessionFailure,
15→ getUnitConfig,
16→ createDevConfig,
17→ createProdConfig,
18→ createMockLogger,
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {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('QueryHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const prodConfig = createProductionConfig();",
"new_string": "describe('QueryHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const prodConfig = createProdConfig();"
}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:
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {
24→ let mockAuthClient: jest.Mocked<AuthClient>;
25→ const unitConfig = getUnitConfig();
26→ const prodConfig = createProdConfig();
27→
28→ beforeEach(() => {
29→ mockAuthClient = createMockAuthClient();
30→ jest.clearAllMocks();Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const devConfig = createDevelopmentConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));",
"new_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const devConfig = createDevConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));"
}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:
296→ expect(response.body.errorMessage).toBe('An internal error occurred');
297→ }
298→ });
299→
300→ it('should include error detail when isDevelopment is true', async () => {
301→ const routeConfig = createMockRouteConfig();
302→ const context = createMockGuestContext();
303→ const devConfig = createDevConfig();
304→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
305→
306→ const options: QueryHandlerOptions = {
307→ context,
308→ routeConfig,Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createDevelopmentConfig,\n createProductionConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createDevConfig,\n createProdConfig,\n createMockLogger,\n} from '../mocks';"
}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:
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { MutationHandler } from '../../handlers/MutationHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationFailure,
12→ mockOperationThrows,
13→ mockRenewSessionSuccess,
14→ mockRenewSessionFailure,
15→ getUnitConfig,
16→ createDevConfig,
17→ createProdConfig,
18→ createMockLogger,
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { MutationHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('MutationHandler', () => {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('MutationHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const prodConfig = createProductionConfig();",
"new_string": "describe('MutationHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const prodConfig = createProdConfig();"
}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:
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { MutationHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('MutationHandler', () => {
24→ let mockAuthClient: jest.Mocked<AuthClient>;
25→ const unitConfig = getUnitConfig();
26→ const prodConfig = createProdConfig();
27→
28→ beforeEach(() => {
29→ mockAuthClient = createMockAuthClient();
30→ jest.clearAllMocks();Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const devConfig = createDevelopmentConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));",
"new_string": " it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const devConfig = createDevConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));"
}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:
294→ expect(response.body.errorMessage).toBe('An internal error occurred');
295→ }
296→ });
297→
298→ it('should include error detail when isDevelopment is true', async () => {
299→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
300→ const context = createMockAuthenticatedContext();
301→ const devConfig = createDevConfig();
302→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
303→
304→ const options: MutationHandlerOptions = {
305→ context,
306→ routeConfig,Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"Handler\" 2>&1",
"description": "Run all Handler tests",
"timeout": 120000
}I don't see a reason to keep the deprecated functions - we just wrote this code. Do you agree?
You're right. Since we just wrote this code and there's no existing codebase depending on these names, we should remove the deprecated functions entirely. Let me clean that up.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts",
"old_string": "'use strict';\n\nimport type { CwcApiConfig } from '../../config/config.types';\nimport { loadConfig } from '../../config';\n\n/**\n * Gets the real config loaded from unit.cwc-api.env\n * Use this as the base for test config\n */\nexport function getUnitConfig(): CwcApiConfig {\n return loadConfig();\n}\n\n/**\n * @deprecated Use getUnitConfig() instead. Kept for backwards compatibility.\n */\nexport function getTestConfig(): CwcApiConfig {\n return getUnitConfig();\n}\n\n/**\n * Creates a mock config by applying overrides to the real test config\n * Useful when you need to test specific config variations (e.g., production mode)\n */\nexport function createMockConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n const baseConfig = loadConfig();\n return { ...baseConfig, ...overrides };\n}\n\n/**\n * Creates a prod config for testing production error behavior\n * Applies production-like settings while preserving secrets from env\n */\nexport function createProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'prod',\n isProduction: true,\n isDevelopment: false,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * @deprecated Use createProdConfig() instead. Kept for backwards compatibility.\n */\nexport function createProductionConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createProdConfig(overrides);\n}\n\n/**\n * Creates a unit config (should match what's loaded from unit.cwc-api.env)\n */\nexport function createUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * @deprecated Use createUnitConfig() instead. Kept for backwards compatibility.\n */\nexport function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createUnitConfig(overrides);\n}\n\n/**\n * Creates a dev config for testing dev-mode behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}",
"new_string": "'use strict';\n\nimport type { CwcApiConfig } from '../../config/config.types';\nimport { loadConfig } from '../../config';\n\n/**\n * Gets the real config loaded from unit.cwc-api.env\n */\nexport function getUnitConfig(): CwcApiConfig {\n return loadConfig();\n}\n\n/**\n * Creates a mock config by applying overrides to the loaded unit config\n * Useful when you need to test specific config variations\n */\nexport function createMockConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n const baseConfig = loadConfig();\n return { ...baseConfig, ...overrides };\n}\n\n/**\n * Creates a prod config for testing production error behavior\n */\nexport function createProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'prod',\n isProduction: true,\n isDevelopment: false,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a unit config (should match what's loaded from unit.cwc-api.env)\n */\nexport function createUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a dev config for testing dev-mode behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.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 { CwcApiConfig } from '../../config/config.types';
4→import { loadConfig } from '../../config';
5→
6→/**
7→ * Gets the real config loaded from unit.cwc-api.env
8→ */
9→export function getUnitConfig(): CwcApiConfig {
10→ return loadConfig();
11→}
12→
13→/**
14→ * Creates a mock config by applying overrides to the loaded unit config
15→ * Useful when you need to test specific config variations
16→ */
17→export function createMockConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
18→ const baseConfig = loadConfig();
19→ return { ...baseConfig, ...overrides };
20→}
21→
22→/**
23→ * Creates a prod config for testing production error behavior
24→ */
25→export function createProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
26→ return createMockConfig({
27→ runtimeEnvironment: 'prod',
28→ isProduction: true,
29→ isDevelopment: false,
30→ isTest: false,
31→ isUnitTest: false,
32→ isEndToEnd: false,
33→ debugMode: false,
34→ ...overrides,
35→ });
36→}
37→
38→/**
39→ * Creates a unit config (should match what's loaded from unit.cwc-api.env)
40→ */
41→export function createUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
42→ return createMockConfig({
43→ runtimeEnvironment: 'unit',
44→ isProduction: false,
45→ isDevelopment: false,
46→ isTest: false,
47→ isUnitTest: true,
48→ isEndToEnd: false,
49→ debugMode: false,
50→ ...overrides,
51→ });
52→}
53→
54→/**
55→ * Creates a dev config for testing dev-mode behavior
56→ * Has isDevelopment: true so error details are exposed
57→ */
58→export function createDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
59→ return createMockConfig({
60→ runtimeEnvironment: 'dev',
61→ isProduction: false,
62→ isDevelopment: true,
63→ isTest: false,
64→ isUnitTest: false,
65→ isEndToEnd: false,
66→ debugMode: true,
67→ ...overrides,
68→ });
69→}
70→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts",
"old_string": "export {\n getUnitConfig,\n getTestConfig, // @deprecated - use getUnitConfig\n createMockConfig,\n createProdConfig,\n createProductionConfig, // @deprecated - use createProdConfig\n createUnitConfig,\n createUnitTestConfig, // @deprecated - use createUnitConfig\n createDevConfig,\n} from './config.mock';",
"new_string": "export {\n getUnitConfig,\n createMockConfig,\n createProdConfig,\n createUnitConfig,\n createDevConfig,\n} from './config.mock';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
12→ mockDeleteSuccess,
13→ mockDeleteNotFound,
14→} from './sqlClient.mock';
15→
16→export {
17→ getUnitConfig,
18→ createMockConfig,
19→ createProdConfig,
20→ createUnitConfig,
21→ createDevConfig,
22→} from './config.mock';
23→
24→export { createMockLogger } from './logger.mock';
25→
26→export {Grep
{
"pattern": "getTestConfig|createProductionConfig|createUnitTestConfig",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__",
"output_mode": "files_with_matches"
}Tool Result
No files found
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"Handler\" 2>&1",
"description": "Run all Handler tests",
"timeout": 120000
}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 "--testPathPatterns=Handler"
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 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/QueryHandler.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)
Test Suites: 2 passed, 2 total
Tests: 30 passed, 30 total
Snapshots: 0 total
Time: 0.359 s, estimated 1 s
Ran all test suites matching Handler.All 30 tests pass. Now let me update the todo list and create the RouteHandler.
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "completed",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "completed",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "in_progress",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "pending",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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/handlers/RouteHandler.ts",
"content": "'use strict';\n\nimport type { ILogger, AuthClient } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n RouteHandlerOptions,\n RouteAccessResult,\n OperationContext,\n} from './handler.types';\nimport { QueryHandler } from './QueryHandler';\nimport { MutationHandler } from './MutationHandler';\n\nconst codeLocation = 'handlers/RouteHandler.ts';\n\n/**\n * RouteHandler - Entry point for processing API requests\n *\n * Responsibilities:\n * 1. Check route-level access based on context role\n * 2. Build operation context with path params\n * 3. Delegate to QueryHandler or MutationHandler based on handlerType\n */\nexport class RouteHandler implements CwcApiHandler {\n private options: RouteHandlerOptions;\n private config: CwcApiConfig;\n private authClient: AuthClient;\n private logger: ILogger | undefined;\n\n constructor(\n options: RouteHandlerOptions,\n config: CwcApiConfig,\n authClient: AuthClient,\n logger: ILogger | undefined\n ) {\n this.options = options;\n this.config = config;\n this.authClient = authClient;\n this.logger = logger;\n }\n\n 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 return this.createAccessDeniedResponse(routeAccess.reason);\n }\n\n // Step 2: Build operation context\n const operationContext: OperationContext = {\n context,\n projectId: pathParams?.['projectId'],\n resourceId: pathParams?.['resourceId'],\n };\n\n // Step 3: Delegate to appropriate handler\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.authClient,\n this.logger\n );\n return 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.authClient,\n this.logger\n );\n return mutationHandler.processRequest();\n }\n } catch (error) {\n this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error processing route ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(error);\n }\n }\n\n /**\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(reason?: string): CwcApiHandlerResponse {\n const { context } = this.options;\n\n // Use 401 for unauthenticated, 403 for authenticated but not allowed\n const statusCode = context.isAuthenticated ? 403 : 401;\n const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage: 'Access denied',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RouteHandler.ts
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts",
"content": "'use strict';\n\nimport { describe, it, expect, beforeEach, jest } from '@jest/globals';\nimport { RouteHandler } from '../../handlers/RouteHandler';\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationThrows,\n mockRenewSessionSuccess,\n getUnitConfig,\n createDevConfig,\n createProdConfig,\n createMockLogger,\n} from '../mocks';\nimport type { AuthClient } from 'cwc-backend-utils';\nimport type { RouteHandlerOptions, CwcApiRouteConfig } from '../../handlers/handler.types';\n\ndescribe('RouteHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const devConfig = createDevConfig();\n const prodConfig = createProdConfig();\n\n beforeEach(() => {\n mockAuthClient = createMockAuthClient();\n jest.clearAllMocks();\n });\n\n 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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RouteHandler(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 });\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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RouteHandler(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 });\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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RouteHandler(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 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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RouteHandler(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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n };\n\n const handler = new RouteHandler(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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RouteHandler(options, devConfig, 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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RouteHandler(options, prodConfig, 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 });\n\n 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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: { page: 1 },\n };\n\n const handler = new RouteHandler(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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: { name: 'New Project' },\n };\n\n const handler = new RouteHandler(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(); // Mutation renews session\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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n pathParams: { projectId: 'my-project-123' },\n };\n\n const handler = new RouteHandler(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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },\n };\n\n const handler = new RouteHandler(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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n // No pathParams\n };\n\n const handler = new RouteHandler(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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RouteHandler(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: RouteHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n };\n\n const handler = new RouteHandler(options, unitConfig, mockAuthClient, mockLogger);\n await handler.processRequest();\n\n expect(mockLogger.logError).toHaveBeenCalled();\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__/handlers/RouteHandler.test.ts
I'll continue by running the RouteHandler tests to verify they pass.
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"Handler\"",
"description": "Run all Handler tests",
"timeout": 60000
}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 "--testPathPatterns=Handler"
PASS src/__tests__/handlers/RouteHandler.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/MutationHandler.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/QueryHandler.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)
Test Suites: 3 passed, 3 total
Tests: 44 passed, 44 total
Snapshots: 0 total
Time: 0.361 s, estimated 1 s
Ran all test suites matching Handler.All 44 handler tests pass (15 QueryHandler + 15 MutationHandler + 14 RouteHandler). Let me update the todo list and continue with the remaining tasks.
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "completed",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "completed",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "in_progress",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "pending",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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
Now let me create the handlers/index.ts file.
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 RouteHandler
162→ */
163→export type RouteHandlerOptions = 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>
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/index.ts",
"content": "'use strict';\n\n// Handler classes\nexport { RouteHandler } from './RouteHandler';\nexport { QueryHandler } from './QueryHandler';\nexport { MutationHandler } from './MutationHandler';\n\n// Types\nexport type {\n // Error codes\n CwcApiErrorCode,\n // Status codes\n CwcApiSuccessStatusCode,\n CwcApiErrorStatusCode,\n CwcApiStatusCode,\n // Response types\n CwcApiPagination,\n CwcApiSuccessResponse,\n CwcApiErrorResponse,\n CwcApiResponse,\n CwcApiHandlerResponse,\n // Route configuration\n CwcApiHandlerType,\n CwcApiRouteConfig,\n CwcApiRouteConfigs,\n // Operation types\n OperationContext,\n OperationResult,\n CwcApiOperation,\n // Handler options\n BaseHandlerOptions,\n RouteHandlerOptions,\n QueryHandlerOptions,\n MutationHandlerOptions,\n // Handler interface\n CwcApiHandler,\n // Access policy types (Phase 4)\n RouteAccessResult,\n OperationAccessResult,\n RouteAccessPolicy,\n OperationAccessPolicy,\n} from './handler.types';\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/index.ts
Let me verify the typecheck passes.
Bash
{
"command": "pnpm --filter cwc-api typecheck",
"description": "Typecheck cwc-api package",
"timeout": 30000
}Tool Result
Exit code 2 > cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > tsc --noEmit src/__tests__/handlers/RouteHandler.test.ts(19,36): error TS6196: 'CwcApiRouteConfig' is declared but never used. src/__tests__/mocks/routeConfig.mock.ts(11,15): error TS6196: 'RequestContext' is declared but never used. /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api: ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL cwc-api@1.0.0 typecheck: `tsc --noEmit` Exit status 2
Let me fix the unused imports.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts",
"old_string": "import type { AuthClient } from 'cwc-backend-utils';\nimport type { RouteHandlerOptions, CwcApiRouteConfig } from '../../handlers/handler.types';",
"new_string": "import type { AuthClient } from 'cwc-backend-utils';\nimport type { RouteHandlerOptions } from '../../handlers/handler.types';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
14→ createDevConfig,
15→ createProdConfig,
16→ createMockLogger,
17→} from '../mocks';
18→import type { AuthClient } from 'cwc-backend-utils';
19→import type { RouteHandlerOptions } from '../../handlers/handler.types';
20→
21→describe('RouteHandler', () => {
22→ let mockAuthClient: jest.Mocked<AuthClient>;
23→ const unitConfig = getUnitConfig();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 { RequestContext, 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": "import type { RequestContext, GuestContext, AuthenticatedContext } from '../../context';",
"new_string": "import type { GuestContext, AuthenticatedContext } from '../../context';"
}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:
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→ */Bash
{
"command": "pnpm --filter cwc-api typecheck",
"description": "Typecheck cwc-api package",
"timeout": 30000
}Tool Result
> cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > tsc --noEmit
Typecheck passes. Let me update the todo list and continue with the CLAUDE.md documentation update.
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "completed",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "completed",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "completed",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "in_progress",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "pending",
"activeForm": "Updating implementation checklist"
}
]
}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
Now I need to update the CLAUDE.md with handler architecture documentation. The existing file already has some handler information. Let me update the Mock Infrastructure section and add more detail about the handler testing mocks.
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 → RouteHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
18→```
19→
20→- **RouteHandler**: Auth → context creation → route access check → delegation
21→- **QueryHandler**: Operation access check → execute query → format response
22→- **MutationHandler**: Operation access check → execute mutation → session renewal → 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. RouteHandler 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. Response returned (with new JWT if session renewed)
34→
35→## Context Layer - Centralized Authentication
36→
37→**Location:** `src/context/`
38→
39→**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.
40→
41→**Trade-off:** Additional HTTP call per request, but auth logic stays centralized in cwc-auth (single source of truth).
42→
43→**Files:**
44→- `context.types.ts` - AuthenticatedContext, GuestContext, RequestContext types
45→- `createContext.ts` - Creates context from Authorization header
46→- `index.ts` - Exports
47→
48→**Context Types:**
49→
50→```typescript
51→// Authenticated user (valid JWT)
52→type AuthenticatedContext = {
53→ isAuthenticated: true;
54→ role: 'project-owner' | 'logged-on-user'; // Determined per-operation
55→ userPkId: number;
56→ username: string;
57→ ownedProjects: string[];
58→ payload: UserJwtPayload;
59→};
60→
61→// Unauthenticated request (no/invalid JWT)
62→type GuestContext = {
63→ isAuthenticated: false;
64→ role: 'guest-user';
65→};
66→
67→type RequestContext = AuthenticatedContext | GuestContext;
68→```
69→
70→**Usage:**
71→```typescript
72→import { createContext } from './context';
73→import { AuthClient } from 'cwc-backend-utils';
74→
75→const context = await createContext({
76→ authHeader: req.headers.authorization,
77→ authClient: authClient,
78→});
79→
80→if (context.isAuthenticated) {
81→ // TypeScript narrows to AuthenticatedContext
82→ console.log(context.userPkId, context.ownedProjects);
83→} else {
84→ // TypeScript narrows to GuestContext
85→ console.log(context.role); // 'guest-user'
86→}
87→```
88→
89→**Graceful Degradation:**
90→- Missing token → guest context
91→- Invalid token → guest context
92→- Expired token → guest context
93→- Auth service error → guest context (logged)
94→- No errors thrown; auth failures result in guest access
95→
96→## Role-Based Access Control
97→
98→**Roles:**
99→
100→| Role | Description |
101→|------|-------------|
102→| `project-owner` | User owns the project being accessed |
103→| `logged-on-user` | Authenticated user (may not own the project) |
104→| `guest-user` | Unauthenticated request (read-only access) |
105→
106→**Access Policy Levels:**
107→
108→- **Route-level**: Can this role call this endpoint at all?
109→- **Operation-level**: Can this role execute this specific operation?
110→- **Context-aware**: Does the user have the right relationship to the data? (e.g., ownership)
111→
112→## Tables Handled
113→
114→**Content Tables (Full CRUD + Soft Delete):**
115→
116→- project
117→- codingSession
118→- codingSessionContent
119→- codingSessionAttachment
120→- comment
121→- reaction
122→- contentReport
123→- abuseReport
124→
125→**Read-Only Tables:**
126→
127→- featureFlag (all fields)
128→- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
129→
130→## SqlFunction Naming Convention
131→
132→Each table has a subdirectory in `src/sql/` with these functions:
133→
134→| Function | Purpose |
135→|----------|---------|
136→| `select{Table}.ts` | Retrieve single record by identifier |
137→| `list{Table}s.ts` | Retrieve multiple records with filtering |
138→| `insert{Table}.ts` | Create new record |
139→| `update{Table}.ts` | Modify existing record |
140→| `delete{Table}.ts` | Hard delete (GDPR compliance) |
141→| `softDelete{Table}.ts` | Soft delete (set enabled=false) |
142→
143→**Key Principles:**
144→
145→- Single responsibility: Each SqlFunction does one thing
146→- No business logic: SqlFunctions handle data access only
147→- Soft-delete awareness: List queries automatically filter `enabled=true`
148→- Composability: Queries/Mutations compose multiple SqlFunctions
149→
150→## cwc-auth Integration
151→
152→**Division of Responsibilities:**
153→
154→| Concern | Owner |
155→|---------|-------|
156→| "Is this token valid?" | cwc-auth |
157→| "Who is this user?" | cwc-auth (via token claims) |
158→| "Can this user access this endpoint?" | cwc-api |
159→| "Is this user the owner of this project?" | cwc-api |
160→
161→**Session Renewal Flow:**
162→
163→When cwc-api creates/deletes a project:
164→1. cwc-api performs the database operation
165→2. cwc-api calls cwc-auth `/auth/v1/renew-session`
166→3. cwc-auth issues new JWT with updated `ownedProjects`
167→4. cwc-api returns new JWT to client
168→
169→## Response Pattern
170→
171→**Standard Response Envelope:**
172→
173→```typescript
174→// Success
175→{
176→ success: true,
177→ data: { ... },
178→ // Optional: pagination metadata
179→}
180→
181→// Error
182→{
183→ success: false,
184→ errorCode: 'ERROR_CODE',
185→ errorMessage: 'Human-readable message',
186→ // Dev-only: errorDetail
187→}
188→```
189→
190→**Pagination (Offset-based):**
191→
192→```typescript
193→{
194→ success: true,
195→ data: [...],
196→ pagination: {
197→ page: 1,
198→ pageSize: 20,
199→ totalCount: 100,
200→ hasMore: true
201→ }
202→}
203→```
204→
205→## Configuration
206→
207→**Required Environment Variables:**
208→
209→```bash
210→RUNTIME_ENVIRONMENT=dev
211→SERVICE_PORT=5040
212→DATA_URI=http://localhost:5020/data/v1
213→AUTH_URI=http://localhost:5005/auth/v1
214→APP_URL=http://localhost:3000
215→CORS_ORIGIN=*
216→DEV_CORS_ORIGIN=http://localhost:3000
217→RATE_LIMITER_POINTS=15
218→RATE_LIMITER_DURATION=1
219→DEBUG_MODE=OFF
220→LOG_ERRORS_TO_DATABASE=ON
221→USER_JWT_SECRET=...
222→SQL_CLIENT_API_KEY=...
223→```
224→
225→## API Versioning
226→
227→**URL-based versioning:**
228→
229→- `/api/v1/*` routes to CwcApiV1 handlers
230→- Each version lives in `src/apis/CwcApiV{N}/`
231→- Route definitions in `routes.ts`, organized alphabetically
232→
233→## Critical Rules
234→
235→**SqlClient Usage:**
236→
237→- Never provide `createdDate` or `modifiedDate` - handled by cwc-sql automatically
238→- Always use SqlFunctions, never construct queries directly in business logic
239→
240→**Guest User Handling:**
241→
242→- No JWT required - RouteHandler creates context with `role: 'guest-user'`
243→- Guest users have read-only access to public content
244→
245→**Soft Delete:**
246→
247→- All content tables support soft delete (`enabled=false`)
248→- Hard delete available for GDPR compliance
249→- List queries automatically filter `enabled=true`
250→
251→## Implementation Status
252→
253→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
254→
255→The checklist tracks 7 phases:
256→1. Package Foundation (config, Express service, health check) - **Complete**
257→2. Authentication & Context Layer (JWT, request context) - **Complete**
258→3. Handler Architecture (RouteHandler, QueryHandler, MutationHandler)
259→4. Access Policy System (route and operation policies)
260→5. SqlFunction Layer (CRUD operations for all tables)
261→6. CwcApiV1 Routes & Business Logic
262→7. Integration Testing & Final Review
263→
264→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
265→
266→## Unit Testing
267→
268→**Test Infrastructure:**
269→
270→- Jest 30.x with ts-jest for TypeScript ESM support
271→- Test files: `src/__tests__/**/*.test.ts`
272→- Setup: `src/__tests__/setup.ts` loads unit test environment via `loadDotEnv`
273→- Run tests: `pnpm api test` or `NODE_ENV=unit pnpm --filter cwc-api test`
274→
275→**Mock Infrastructure (`src/__tests__/mocks/`):**
276→
277→| Mock | Purpose |
278→|------|---------|
279→| `createMockSqlClient()` | Full SqlClient mock with jest.fn() for all methods |
280→| `mockSelectSuccess/NotFound` | Configure SqlClient for select queries |
281→| `mockListSuccess` | Configure SqlClient for list queries |
282→| `mockInsertSuccess/Failure` | Configure SqlClient for inserts |
283→| `mockUpdateSuccess/NotFound` | Configure SqlClient for updates |
284→| `mockDeleteSuccess/NotFound` | Configure SqlClient for deletes |
285→| `createMockAuthClient()` | Full AuthClient mock with jest.fn() for verifyToken |
286→| `createMockUserJwtPayload(overrides)` | Create UserJwtPayload with custom overrides |
287→| `mockVerifyTokenSuccess(client, payload?)` | Configure AuthClient to return success |
288→| `mockVerifyTokenFailure(client, error?)` | Configure AuthClient to return failure |
289→| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |
290→| `getTestConfig()` | Load real config from unit.cwc-api.env |
291→| `createMockConfig(overrides)` | Create config with custom overrides |
292→| `createProductionConfig()` | Config for testing production behavior |
293→| `createUnitTestConfig()` | Config explicitly set for unit test environment |
294→
295→**Testing Pattern:**
296→
297→```typescript
298→import { createMockSqlClient, mockSelectSuccess, getTestConfig } from '../mocks';
299→
300→describe('MyFunction', () => {
301→ const config = getTestConfig();
302→ const mockSqlClient = createMockSqlClient();
303→
304→ it('should return data on success', async () => {
305→ mockSelectSuccess(mockSqlClient, { id: 1, name: 'test' });
306→ // ... test logic
307→ });
308→});
309→```
310→
311→## Related Packages
312→
313→**Depends On:**
314→
315→- `cwc-backend-utils` (workspace) - SqlClient, Logger, Express service factory
316→- `cwc-schema` (workspace) - Data validation
317→- `cwc-types` (workspace) - Entity types, RuntimeEnvironment
318→
319→**Coordinates With:**
320→
321→- `cwc-auth` - JWT validation, session renewal
322→- `cwc-sql` - Database operations
323→
324→**Consumed By:**
325→
326→- `cwc-website` - Frontend web application
327→
<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 with the handler architecture documentation and update the Mock Infrastructure section.
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?\n- **Context-aware**: Does the user have the right relationship to the data? (e.g., ownership)",
"new_string": "## Handler Architecture\n\n**Location:** `src/handlers/`\n\n**Files:**\n- `handler.types.ts` - Type definitions for handlers, operations, and responses\n- `RouteHandler.ts` - Entry point: route access check, delegates to Query/Mutation\n- `QueryHandler.ts` - Read operations with pagination support\n- `MutationHandler.ts` - Write operations with session renewal\n- `index.ts` - Exports\n\n**Handler Flow:**\n\n```\nRouteHandler.processRequest()\n├── checkRouteAccess() → 401/403 if denied\n├── buildOperationContext() → projectId, resourceId from path\n└── delegate to QueryHandler or MutationHandler\n ├── checkOperationAccess() → (stub for Phase 4)\n ├── execute operation()\n ├── renewSession() → always for authenticated users\n └── format response with new JWT\n```\n\n**Session Renewal Strategy:**\n\n- Always renew session on every request (keeps sessions active)\n- Both QueryHandler and MutationHandler call `AuthClient.renewSession()`\n- New JWT returned in response body (`response.body.jwt`)\n- Renewal failure doesn't fail the request (graceful degradation)\n\n**Error Code Mapping:**\n\n| Error Code | HTTP Status |\n|------------|-------------|\n| `UNAUTHORIZED` | 401 |\n| `FORBIDDEN` | 403 |\n| `NOT_FOUND` | 404 |\n| `ALREADY_EXISTS` | 400 |\n| `VALIDATION_ERROR` | 400 |\n| `INVALID_PARAMETER` | 400 |\n| `DATABASE_ERROR` | 500 |\n| `INTERNAL_ERROR` | 500 |\n\n## 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)"
}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:
92→- Expired token → guest context
93→- Auth service error → guest context (logged)
94→- No errors thrown; auth failures result in guest access
95→
96→## Handler Architecture
97→
98→**Location:** `src/handlers/`
99→
100→**Files:**
101→- `handler.types.ts` - Type definitions for handlers, operations, and responses
102→- `RouteHandler.ts` - Entry point: route access check, delegates to Query/Mutation
103→- `QueryHandler.ts` - Read operations with pagination support
104→- `MutationHandler.ts` - Write operations with session renewal
105→- `index.ts` - Exports
106→
107→**Handler Flow:**
108→
109→```
110→RouteHandler.processRequest()
111→├── checkRouteAccess() → 401/403 if denied
112→├── buildOperationContext() → projectId, resourceId from path
113→└── delegate to QueryHandler or MutationHandler
114→ ├── checkOperationAccess() → (stub for Phase 4)
115→ ├── execute operation()
116→ ├── renewSession() → always for authenticated users
117→ └── format response with new JWT
118→```
119→
120→**Session Renewal Strategy:**
121→
122→- Always renew session on every request (keeps sessions active)
123→- Both QueryHandler and MutationHandler call `AuthClient.renewSession()`
124→- New JWT returned in response body (`response.body.jwt`)
125→- Renewal failure doesn't fail the request (graceful degradation)
126→
127→**Error Code Mapping:**
128→
129→| Error Code | HTTP Status |
130→|------------|-------------|
131→| `UNAUTHORIZED` | 401 |
132→| `FORBIDDEN` | 403 |
133→| `NOT_FOUND` | 404 |
134→| `ALREADY_EXISTS` | 400 |
135→| `VALIDATION_ERROR` | 400 |
136→| `INVALID_PARAMETER` | 400 |
137→| `DATABASE_ERROR` | 500 |
138→| `INTERNAL_ERROR` | 500 |
139→
140→## Role-Based Access Control
141→
142→**Roles:**
143→
144→| Role | Description |
145→|------|-------------|
146→| `project-owner` | User owns the project being accessed |
147→| `logged-on-user` | Authenticated user (may not own the project) |
148→| `guest-user` | Unauthenticated request (read-only access) |
149→
150→**Access Policy Levels:**
151→
152→- **Route-level**: Can this role call this endpoint at all?
153→- **Operation-level**: Can this role execute this specific operation? (Phase 4)
154→- **Context-aware**: Does the user have the right relationship to the data? (e.g., ownership)
155→
156→## Tables Handled
157→
158→**Content Tables (Full CRUD + Soft Delete):**Now let me update the Mock Infrastructure section with the new mocks.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Mock Infrastructure (`src/__tests__/mocks/`):**\n\n| Mock | Purpose |\n|------|---------|\n| `createMockSqlClient()` | Full SqlClient mock with jest.fn() for all methods |\n| `mockSelectSuccess/NotFound` | Configure SqlClient for select queries |\n| `mockListSuccess` | Configure SqlClient for list queries |\n| `mockInsertSuccess/Failure` | Configure SqlClient for inserts |\n| `mockUpdateSuccess/NotFound` | Configure SqlClient for updates |\n| `mockDeleteSuccess/NotFound` | Configure SqlClient for deletes |\n| `createMockAuthClient()` | Full AuthClient mock with jest.fn() for verifyToken |\n| `createMockUserJwtPayload(overrides)` | Create UserJwtPayload with custom overrides |\n| `mockVerifyTokenSuccess(client, payload?)` | Configure AuthClient to return success |\n| `mockVerifyTokenFailure(client, error?)` | Configure AuthClient to return failure |\n| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |\n| `getTestConfig()` | Load real config from unit.cwc-api.env |\n| `createMockConfig(overrides)` | Create config with custom overrides |\n| `createProductionConfig()` | Config for testing production behavior |\n| `createUnitTestConfig()` | Config explicitly set for unit test environment |\n\n**Testing Pattern:**\n\n```typescript\nimport { createMockSqlClient, mockSelectSuccess, getTestConfig } from '../mocks';\n\ndescribe('MyFunction', () => {\n const config = getTestConfig();\n const mockSqlClient = createMockSqlClient();\n\n it('should return data on success', async () => {\n mockSelectSuccess(mockSqlClient, { id: 1, name: 'test' });\n // ... test logic\n });\n});\n```",
"new_string": "**Mock Infrastructure (`src/__tests__/mocks/`):**\n\n| Mock | Purpose |\n|------|---------|\n| **SqlClient Mocks** | |\n| `createMockSqlClient()` | Full SqlClient mock with jest.fn() for all methods |\n| `mockSelectSuccess/NotFound` | Configure SqlClient for select queries |\n| `mockListSuccess` | Configure SqlClient for list queries |\n| `mockInsertSuccess/Failure` | Configure SqlClient for inserts |\n| `mockUpdateSuccess/NotFound` | Configure SqlClient for updates |\n| `mockDeleteSuccess/NotFound` | Configure SqlClient for deletes |\n| **AuthClient Mocks** | |\n| `createMockAuthClient()` | Full AuthClient mock (verifyToken, renewSession) |\n| `createMockUserJwtPayload(overrides)` | Create UserJwtPayload with custom overrides |\n| `mockVerifyTokenSuccess(client, payload?)` | Configure AuthClient to return success |\n| `mockVerifyTokenFailure(client, error?)` | Configure AuthClient to return failure |\n| `mockRenewSessionSuccess(client, jwt?)` | Configure renewSession to return new JWT |\n| `mockRenewSessionFailure(client, error?)` | Configure renewSession to return failure |\n| **Route/Handler Mocks** | |\n| `createMockRouteConfig(overrides)` | Create route config for testing |\n| `createMockOperationContext(overrides)` | Create operation context for testing |\n| `createMockGuestContext()` | Create guest (unauthenticated) context |\n| `createMockAuthenticatedContext(overrides)` | Create authenticated user context |\n| `mockOperationSuccess(routeConfig, data)` | Configure operation to succeed |\n| `mockOperationFailure(routeConfig, code, msg)` | Configure operation to fail |\n| `mockOperationThrows(routeConfig, error)` | Configure operation to throw |\n| **Config Mocks** | |\n| `getUnitConfig()` | Load real config from unit.cwc-api.env |\n| `createMockConfig(overrides)` | Create config with custom overrides |\n| `createDevConfig(overrides)` | Config with isDevelopment: true |\n| `createProdConfig(overrides)` | Config with isProduction: true |\n| `createUnitConfig(overrides)` | Config with isUnitTest: true |\n| **Logger Mock** | |\n| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |\n\n**Testing Pattern:**\n\n```typescript\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockRenewSessionSuccess,\n getUnitConfig,\n} from '../mocks';\n\ndescribe('MyHandler', () => {\n const config = getUnitConfig();\n const mockAuthClient = createMockAuthClient();\n\n it('should handle operation success', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt');\n // ... test logic\n });\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:
315→- Test files: `src/__tests__/**/*.test.ts`
316→- Setup: `src/__tests__/setup.ts` loads unit test environment via `loadDotEnv`
317→- Run tests: `pnpm api test` or `NODE_ENV=unit pnpm --filter cwc-api test`
318→
319→**Mock Infrastructure (`src/__tests__/mocks/`):**
320→
321→| Mock | Purpose |
322→|------|---------|
323→| **SqlClient Mocks** | |
324→| `createMockSqlClient()` | Full SqlClient mock with jest.fn() for all methods |
325→| `mockSelectSuccess/NotFound` | Configure SqlClient for select queries |
326→| `mockListSuccess` | Configure SqlClient for list queries |
327→| `mockInsertSuccess/Failure` | Configure SqlClient for inserts |
328→| `mockUpdateSuccess/NotFound` | Configure SqlClient for updates |
329→| `mockDeleteSuccess/NotFound` | Configure SqlClient for deletes |
330→| **AuthClient Mocks** | |
331→| `createMockAuthClient()` | Full AuthClient mock (verifyToken, renewSession) |
332→| `createMockUserJwtPayload(overrides)` | Create UserJwtPayload with custom overrides |
333→| `mockVerifyTokenSuccess(client, payload?)` | Configure AuthClient to return success |
334→| `mockVerifyTokenFailure(client, error?)` | Configure AuthClient to return failure |
335→| `mockRenewSessionSuccess(client, jwt?)` | Configure renewSession to return new JWT |
336→| `mockRenewSessionFailure(client, error?)` | Configure renewSession to return failure |
337→| **Route/Handler Mocks** | |
338→| `createMockRouteConfig(overrides)` | Create route config for testing |
339→| `createMockOperationContext(overrides)` | Create operation context for testing |
340→| `createMockGuestContext()` | Create guest (unauthenticated) context |
341→| `createMockAuthenticatedContext(overrides)` | Create authenticated user context |
342→| `mockOperationSuccess(routeConfig, data)` | Configure operation to succeed |
343→| `mockOperationFailure(routeConfig, code, msg)` | Configure operation to fail |
344→| `mockOperationThrows(routeConfig, error)` | Configure operation to throw |
345→| **Config Mocks** | |
346→| `getUnitConfig()` | Load real config from unit.cwc-api.env |
347→| `createMockConfig(overrides)` | Create config with custom overrides |
348→| `createDevConfig(overrides)` | Config with isDevelopment: true |
349→| `createProdConfig(overrides)` | Config with isProduction: true |
350→| `createUnitConfig(overrides)` | Config with isUnitTest: true |
351→| **Logger Mock** | |
352→| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |
353→
354→**Testing Pattern:**
355→
356→```typescript
357→import {
358→ createMockAuthClient,
359→ createMockRouteConfig,
360→ createMockAuthenticatedContext,
361→ mockOperationSuccess,
362→ mockRenewSessionSuccess,
363→ getUnitConfig,
364→} from '../mocks';
365→
366→describe('MyHandler', () => {
367→ const config = getUnitConfig();
368→ const mockAuthClient = createMockAuthClient();
369→
370→ it('should handle operation success', async () => {
371→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
372→ const context = createMockAuthenticatedContext();
373→ mockOperationSuccess(routeConfig, { id: 1 });
374→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
375→ // ... test logic
376→ });
377→});
378→```
379→
380→## Related Packages
381→
382→**Depends On:**Now let me update the Implementation Status to mark Phase 3 as complete.
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 (RouteHandler, QueryHandler, MutationHandler)\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 (RouteHandler, 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"
}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: 291→- All content tables support soft delete (`enabled=false`) 292→- Hard delete available for GDPR compliance 293→- List queries automatically filter `enabled=true` 294→ 295→## Implementation Status 296→ 297→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`. 298→ 299→The checklist tracks 7 phases: 300→1. Package Foundation (config, Express service, health check) - **Complete** 301→2. Authentication & Context Layer (JWT, request context) - **Complete** 302→3. Handler Architecture (RouteHandler, QueryHandler, MutationHandler) - **Complete** 303→4. Access Policy System (route and operation policies) 304→5. SqlFunction Layer (CRUD operations for all tables) 305→6. CwcApiV1 Routes & Business Logic 306→7. Integration Testing & Final Review 307→ 308→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates 309→ 310→## Unit Testing 311→ 312→**Test Infrastructure:**
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "completed",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "completed",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "completed",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "completed",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "in_progress",
"activeForm": "Updating implementation checklist"
}
]
}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
Now let me update the implementation checklist.
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→### Route Handler
101→- [ ] Create `src/handlers/handler.types.ts`
102→ - [ ] Define route handler input/output types
103→ - [ ] Define query/mutation handler types
104→- [ ] Create `src/handlers/RouteHandler.ts`
105→ - [ ] Extract JWT from Authorization header
106→ - [ ] Verify JWT or create guest context
107→ - [ ] Check route-level access policy
108→ - [ ] Delegate to QueryHandler or MutationHandler
109→ - [ ] Handle session renewal response
110→- [ ] Create `src/handlers/QueryHandler.ts`
111→ - [ ] Operation-level access check
112→ - [ ] Execute query
113→ - [ ] Format response
114→- [ ] Create `src/handlers/MutationHandler.ts`
115→ - [ ] Operation-level access check
116→ - [ ] Execute mutation
117→ - [ ] Call cwc-auth /renew-session if needed
118→ - [ ] Format response with new JWT if renewed
119→- [ ] Create `src/handlers/index.ts`
120→
121→### Unit Tests
122→- [ ] Create `src/__tests__/handlers/RouteHandler.test.ts`
123→- [ ] Create `src/__tests__/handlers/QueryHandler.test.ts`
124→- [ ] Create `src/__tests__/handlers/MutationHandler.test.ts`
125→
126→### Security Review
127→- [ ] Verify error responses don't leak internal details (stack traces, SQL errors)
128→- [ ] Verify input validation occurs before processing
129→- [ ] Verify response data is filtered (no sensitive fields exposed)
130→- [ ] Verify session renewal only occurs for legitimate mutations
131→
132→### Documentation Updates
133→- [ ] Update `CLAUDE.md` with handler architecture patterns
134→- [ ] Update `README.md` with request pipeline details
135→
136→---
137→
138→## Phase 4: Access Policy System
139→
140→### Policy Types
141→- [ ] Create `src/policies/policy.types.ts`
142→ - [ ] Define `RoutePolicy` type
143→ - [ ] Define `OperationPolicy` type
144→ - [ ] Define policy check result types
145→
146→### Route Policies
147→- [ ] Create `src/policies/routePolicies.ts`
148→ - [ ] Define which roles can access each route
149→ - [ ] Define `allowUnauthenticated` routes for guest access
150→
151→### Operation Policies
152→- [ ] Create `src/policies/operationPolicies.ts`
153→ - [ ] Define which roles can execute which operations
154→ - [ ] Define context-aware checks (e.g., project ownership)
155→
156→### Policy Enforcement
157→- [ ] Create `src/policies/checkRouteAccess.ts`
158→- [ ] Create `src/policies/checkOperationAccess.ts`
159→- [ ] Create `src/policies/index.ts`
160→
161→### Unit Tests
162→- [ ] Create `src/__tests__/policies/routePolicies.test.ts`
163→- [ ] Create `src/__tests__/policies/operationPolicies.test.ts`
164→- [ ] Create `src/__tests__/policies/checkRouteAccess.test.ts`
165→- [ ] Create `src/__tests__/policies/checkOperationAccess.test.ts`
166→
167→### Security Review
168→- [ ] Verify no privilege escalation paths (guest → user, user → owner)
169→- [ ] Verify ownership checks use userPkId from context (not request body)
170→- [ ] Verify all mutation routes require authentication
171→- [ ] Verify default-deny policy (routes not explicitly allowed are blocked)
172→- [ ] Verify policy checks cannot be bypassed via parameter manipulation
173→
174→### Documentation Updates
175→- [ ] Update `CLAUDE.md` with access policy patterns
176→- [ ] Update `README.md` with role-based access details
177→
178→---
179→
180→## Phase 5: SqlFunction Layer
181→
182→### Infrastructure
183→- [ ] Create `src/sql/sql.types.ts`
184→ - [ ] Define SqlFunction input/output types
185→ - [ ] Define pagination types (offset-based)
186→- [ ] Create `src/sql/index.ts`
187→
188→### Content Tables (Full CRUD + Soft Delete)
189→
190→#### project/
191→- [ ] Create `src/sql/project/selectProject.ts`
192→- [ ] Create `src/sql/project/listProjects.ts`
193→- [ ] Create `src/sql/project/insertProject.ts`
194→- [ ] Create `src/sql/project/updateProject.ts`
195→- [ ] Create `src/sql/project/deleteProject.ts` (hard delete)
196→- [ ] Create `src/sql/project/softDeleteProject.ts`
197→- [ ] Create `src/sql/project/index.ts`
198→
199→#### codingSession/
200→- [ ] Create `src/sql/codingSession/selectCodingSession.ts`
201→- [ ] Create `src/sql/codingSession/listCodingSessions.ts`
202→- [ ] Create `src/sql/codingSession/insertCodingSession.ts`
203→- [ ] Create `src/sql/codingSession/updateCodingSession.ts`
204→- [ ] Create `src/sql/codingSession/deleteCodingSession.ts`
205→- [ ] Create `src/sql/codingSession/softDeleteCodingSession.ts`
206→- [ ] Create `src/sql/codingSession/index.ts`
207→
208→#### codingSessionContent/
209→- [ ] Create `src/sql/codingSessionContent/selectCodingSessionContent.ts`
210→- [ ] Create `src/sql/codingSessionContent/listCodingSessionContents.ts`
211→- [ ] Create `src/sql/codingSessionContent/insertCodingSessionContent.ts`
212→- [ ] Create `src/sql/codingSessionContent/updateCodingSessionContent.ts`
213→- [ ] Create `src/sql/codingSessionContent/deleteCodingSessionContent.ts`
214→- [ ] Create `src/sql/codingSessionContent/softDeleteCodingSessionContent.ts`
215→- [ ] Create `src/sql/codingSessionContent/index.ts`
216→
217→#### codingSessionAttachment/
218→- [ ] Create `src/sql/codingSessionAttachment/selectCodingSessionAttachment.ts`
219→- [ ] Create `src/sql/codingSessionAttachment/listCodingSessionAttachments.ts`
220→- [ ] Create `src/sql/codingSessionAttachment/insertCodingSessionAttachment.ts`
221→- [ ] Create `src/sql/codingSessionAttachment/updateCodingSessionAttachment.ts`
222→- [ ] Create `src/sql/codingSessionAttachment/deleteCodingSessionAttachment.ts`
223→- [ ] Create `src/sql/codingSessionAttachment/softDeleteCodingSessionAttachment.ts`
224→- [ ] Create `src/sql/codingSessionAttachment/index.ts`
225→
226→#### comment/
227→- [ ] Create `src/sql/comment/selectComment.ts`
228→- [ ] Create `src/sql/comment/listComments.ts`
229→- [ ] Create `src/sql/comment/insertComment.ts`
230→- [ ] Create `src/sql/comment/updateComment.ts`
231→- [ ] Create `src/sql/comment/deleteComment.ts`
232→- [ ] Create `src/sql/comment/softDeleteComment.ts`
233→- [ ] Create `src/sql/comment/index.ts`
234→
235→#### reaction/
236→- [ ] Create `src/sql/reaction/selectReaction.ts`
237→- [ ] Create `src/sql/reaction/listReactions.ts`
238→- [ ] Create `src/sql/reaction/insertReaction.ts`
239→- [ ] Create `src/sql/reaction/updateReaction.ts`
240→- [ ] Create `src/sql/reaction/deleteReaction.ts`
241→- [ ] Create `src/sql/reaction/softDeleteReaction.ts`
242→- [ ] Create `src/sql/reaction/index.ts`
243→
244→#### contentReport/
245→- [ ] Create `src/sql/contentReport/selectContentReport.ts`
246→- [ ] Create `src/sql/contentReport/listContentReports.ts`
247→- [ ] Create `src/sql/contentReport/insertContentReport.ts`
248→- [ ] Create `src/sql/contentReport/updateContentReport.ts`
249→- [ ] Create `src/sql/contentReport/deleteContentReport.ts`
250→- [ ] Create `src/sql/contentReport/softDeleteContentReport.ts`
251→- [ ] Create `src/sql/contentReport/index.ts`
252→
253→#### abuseReport/
254→- [ ] Create `src/sql/abuseReport/selectAbuseReport.ts`
255→- [ ] Create `src/sql/abuseReport/listAbuseReports.ts`
256→- [ ] Create `src/sql/abuseReport/insertAbuseReport.ts`
257→- [ ] Create `src/sql/abuseReport/updateAbuseReport.ts`
258→- [ ] Create `src/sql/abuseReport/deleteAbuseReport.ts`
259→- [ ] Create `src/sql/abuseReport/softDeleteAbuseReport.ts`
260→- [ ] Create `src/sql/abuseReport/index.ts`
261→
262→### Read-Only Tables
263→
264→#### featureFlag/
265→- [ ] Create `src/sql/featureFlag/selectFeatureFlag.ts`
266→- [ ] Create `src/sql/featureFlag/listFeatureFlags.ts`
267→- [ ] Create `src/sql/featureFlag/index.ts`
268→
269→#### user/ (Limited Fields)
270→- [ ] Create `src/sql/user/selectUser.ts` (userPkId, enabled, username, loginType, loginDate, otpValidated)
271→- [ ] Create `src/sql/user/listUsers.ts` (same limited fields)
272→- [ ] Create `src/sql/user/index.ts`
273→
274→### Unit Tests
275→- [ ] Create `src/__tests__/sql/project.test.ts` - Test all project SqlFunctions
276→- [ ] Create `src/__tests__/sql/codingSession.test.ts` - Test all codingSession SqlFunctions
277→- [ ] Create `src/__tests__/sql/codingSessionContent.test.ts`
278→- [ ] Create `src/__tests__/sql/codingSessionAttachment.test.ts`
279→- [ ] Create `src/__tests__/sql/comment.test.ts`
280→- [ ] Create `src/__tests__/sql/reaction.test.ts`
281→- [ ] Create `src/__tests__/sql/contentReport.test.ts`
282→- [ ] Create `src/__tests__/sql/abuseReport.test.ts`
283→- [ ] Create `src/__tests__/sql/featureFlag.test.ts`
284→- [ ] Create `src/__tests__/sql/user.test.ts`
285→
286→### Security Review
287→- [ ] Verify all SqlFunctions use SqlClient (no raw SQL construction)
288→- [ ] Verify list queries filter `enabled=true` by default (soft-delete)
289→- [ ] Verify user SqlFunctions only expose limited fields
290→- [ ] Verify no mass assignment vulnerabilities (explicit field mapping)
291→- [ ] Verify pagination limits are enforced (prevent data dumping)
292→
293→### Documentation Updates
294→- [ ] Update `CLAUDE.md` with SqlFunction patterns
295→- [ ] Update `README.md` with data access layer details
296→
297→---
298→
299→## Phase 6: CwcApiV1 Routes & Business Logic
300→
301→### Route Structure
302→- [ ] Create `src/apis/CwcApiV1/routes.ts`
303→- [ ] Create `src/apis/CwcApiV1/types.ts`
304→- [ ] Create `src/apis/CwcApiV1/index.ts`
305→
306→### Project Operations
307→- [ ] Create `src/apis/CwcApiV1/queries/getProject.ts`
308→- [ ] Create `src/apis/CwcApiV1/queries/listProjects.ts`
309→- [ ] Create `src/apis/CwcApiV1/mutations/projectCreate.ts`
310→- [ ] Create `src/apis/CwcApiV1/mutations/projectUpdate.ts`
311→- [ ] Create `src/apis/CwcApiV1/mutations/projectDelete.ts`
312→
313→### Coding Session Operations
314→- [ ] Create `src/apis/CwcApiV1/queries/getCodingSession.ts`
315→- [ ] Create `src/apis/CwcApiV1/queries/listCodingSessions.ts`
316→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionCreate.ts`
317→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionUpdate.ts`
318→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionDelete.ts`
319→
320→### Coding Session Content Operations
321→- [ ] Create `src/apis/CwcApiV1/queries/getCodingSessionContent.ts`
322→- [ ] Create `src/apis/CwcApiV1/queries/listCodingSessionContents.ts`
323→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionContentCreate.ts`
324→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionContentUpdate.ts`
325→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionContentDelete.ts`
326→
327→### Coding Session Attachment Operations
328→- [ ] Create `src/apis/CwcApiV1/queries/getCodingSessionAttachment.ts`
329→- [ ] Create `src/apis/CwcApiV1/queries/listCodingSessionAttachments.ts`
330→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionAttachmentCreate.ts`
331→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionAttachmentUpdate.ts`
332→- [ ] Create `src/apis/CwcApiV1/mutations/codingSessionAttachmentDelete.ts`
333→
334→### Comment Operations
335→- [ ] Create `src/apis/CwcApiV1/queries/getComment.ts`
336→- [ ] Create `src/apis/CwcApiV1/queries/listComments.ts`
337→- [ ] Create `src/apis/CwcApiV1/mutations/commentCreate.ts`
338→- [ ] Create `src/apis/CwcApiV1/mutations/commentUpdate.ts`
339→- [ ] Create `src/apis/CwcApiV1/mutations/commentDelete.ts`
340→
341→### Reaction Operations
342→- [ ] Create `src/apis/CwcApiV1/queries/getReaction.ts`
343→- [ ] Create `src/apis/CwcApiV1/queries/listReactions.ts`
344→- [ ] Create `src/apis/CwcApiV1/mutations/reactionCreate.ts`
345→- [ ] Create `src/apis/CwcApiV1/mutations/reactionDelete.ts`
346→
347→### Content Report Operations
348→- [ ] Create `src/apis/CwcApiV1/queries/getContentReport.ts`
349→- [ ] Create `src/apis/CwcApiV1/queries/listContentReports.ts`
350→- [ ] Create `src/apis/CwcApiV1/mutations/contentReportCreate.ts`
351→- [ ] Create `src/apis/CwcApiV1/mutations/contentReportUpdate.ts`
352→
353→### Abuse Report Operations
354→- [ ] Create `src/apis/CwcApiV1/queries/getAbuseReport.ts`
355→- [ ] Create `src/apis/CwcApiV1/queries/listAbuseReports.ts`
356→- [ ] Create `src/apis/CwcApiV1/mutations/abuseReportCreate.ts`
357→- [ ] Create `src/apis/CwcApiV1/mutations/abuseReportUpdate.ts`
358→
359→### Route Registration
360→- [ ] Register all routes in `routes.ts`
361→- [ ] Wire up routes to Express in `src/index.ts`
362→
363→### Unit Tests
364→- [ ] Create `src/__tests__/apis/CwcApiV1/queries/getProject.test.ts`
365→- [ ] Create `src/__tests__/apis/CwcApiV1/queries/listProjects.test.ts`
366→- [ ] Create `src/__tests__/apis/CwcApiV1/mutations/projectCreate.test.ts`
367→- [ ] Create `src/__tests__/apis/CwcApiV1/mutations/projectUpdate.test.ts`
368→- [ ] Create `src/__tests__/apis/CwcApiV1/mutations/projectDelete.test.ts`
369→- [ ] Create tests for remaining query/mutation endpoints (follow same pattern)
370→
371→### Security Review
372→- [ ] Verify IDOR prevention (users can only access their own resources)
373→- [ ] Verify business logic doesn't allow unauthorized state transitions
374→- [ ] Verify content report/abuse report workflows don't expose reporter identity
375→- [ ] Verify rate limiting is appropriate per operation type
376→- [ ] Verify all user input is validated via cwc-schema before processing
377→
378→### Documentation Updates
379→- [ ] Update `CLAUDE.md` with API route patterns
380→- [ ] Update `README.md` with complete endpoint documentation
381→
382→---
383→
384→## Phase 7: Integration Testing & Final Review
385→
386→### Integration Tests
387→- [ ] Create `src/__tests__/integration/` directory
388→- [ ] Write end-to-end tests for complete request flows
389→- [ ] Write tests for cross-component interactions
390→- [ ] Write tests for error scenarios across layers
391→
392→### Security Review (Final)
393→- [ ] Verify security-focused test coverage (auth bypass, privilege escalation, IDOR)
394→- [ ] Verify error handling tests don't leak sensitive information
395→- [ ] Conduct final end-to-end security review of all phases
396→- [ ] Document any security considerations for deployment
397→
398→### Documentation Updates
399→- [ ] Update `CLAUDE.md` with testing patterns and final review
400→- [ ] Update `README.md` with testing instructions and final review
401→
402→---
403→
404→## Session Notes
405→
406→<!-- Add notes about decisions, blockers, or context for future sessions -->
407→
408→### Session 1
409→- Initial checklist created
410→- Specification reviewed and clarified:
411→ - Guest access: Create context with 'guest-user' role for unauthenticated requests
412→ - Session renewal: cwc-api calls cwc-auth /renew-session
413→ - Pagination: Offset-based (page/pageSize)
414→ - Soft delete: All content tables
415→ - RouteHandler class chosen over middleware approach
416→ - SqlFunction naming: `list` instead of `selectMultiple`
417→ - Added read-only SqlFunctions for featureFlag and user (limited fields)
418→
419→### Session 2 (020)
420→- **Phase 1 COMPLETED**
421→ - Added `dev` script to package.json using `tsx` (better ESM support than ts-node)
422→ - Fixed cwc-backend-utils ESM issues:
423→ - `loadDotEnv.ts`: Added ESM-compatible `__dirname` using `import.meta.url`
424→ - `express/service.ts`: Fixed wildcard route syntax for path-to-regexp 8.x (`/*` → `/{*splat}`)
425→ - Service starts successfully and health check responds (POST /health/v1)
426→
427→- **Architecture Decision: Centralized Authentication**
428→ - cwc-api will NOT duplicate JWT verification logic
429→ - cwc-api will call cwc-auth's `/auth/v1/verify-token` endpoint
430→ - Trade-off: Additional HTTP call per request, but auth logic stays centralized
431→ - **BLOCKER:** Phase 2 cannot proceed until cwc-auth has `/verify-token` endpoint
432→
433→- **Phase 2 Revised**
434→ - Updated checklist to reflect API-call approach
435→ - Removed local JWT utilities (verifyUserJwt, extractJwtFromHeader)
436→ - Added prerequisite: cwc-auth must implement /verify-token first
437→
438→- **Phase 2 COMPLETED**
439→ - Implemented shared auth types in cwc-types (`src/authTypes.ts`):
440→ - `UserJwtPayload`, `CwcRole`, `VerifyTokenResponse`, `VerifyTokenResult`
441→ - Updated cwc-auth to import `UserJwtPayload` from cwc-types (backwards-compatible re-export)
442→ - Created `AuthClient` in cwc-backend-utils (`src/AuthClient/`):
443→ - HTTP client for cwc-auth, similar pattern to SqlClient
444→ - `verifyToken(authHeader)` returns typed `VerifyTokenResult`
445→ - Handles 401 responses, network errors, timeouts
446→ - Created context layer in cwc-api (`src/context/`):
447→ - `context.types.ts`: `AuthenticatedContext`, `GuestContext`, `RequestContext`
448→ - `createContext.ts`: Uses AuthClient, returns authenticated or guest context
449→ - Created test mocks (`src/__tests__/mocks/authClient.mock.ts`):
450→ - `createMockAuthClient()`, `createMockUserJwtPayload()`
451→ - `mockVerifyTokenSuccess()`, `mockVerifyTokenFailure()`
452→ - All 10 context tests pass, all 38 cwc-api tests pass
453→ - All affected packages typecheck: cwc-types, cwc-auth, cwc-backend-utils, cwc-api
454→ - **Note:** AuthClient unit tests deferred (cwc-backend-utils has no test infrastructure)
455→ - Functionality verified through cwc-api context tests
456→
<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 3: Handler Architecture\n\n### Route Handler\n- [ ] Create `src/handlers/handler.types.ts`\n - [ ] Define route handler input/output types\n - [ ] Define query/mutation handler types\n- [ ] Create `src/handlers/RouteHandler.ts`\n - [ ] Extract JWT from Authorization header\n - [ ] Verify JWT or create guest context\n - [ ] Check route-level access policy\n - [ ] Delegate to QueryHandler or MutationHandler\n - [ ] Handle session renewal response\n- [ ] Create `src/handlers/QueryHandler.ts`\n - [ ] Operation-level access check\n - [ ] Execute query\n - [ ] Format response\n- [ ] Create `src/handlers/MutationHandler.ts`\n - [ ] Operation-level access check\n - [ ] Execute mutation\n - [ ] Call cwc-auth /renew-session if needed\n - [ ] Format response with new JWT if renewed\n- [ ] Create `src/handlers/index.ts`\n\n### Unit Tests\n- [ ] Create `src/__tests__/handlers/RouteHandler.test.ts`\n- [ ] Create `src/__tests__/handlers/QueryHandler.test.ts`\n- [ ] Create `src/__tests__/handlers/MutationHandler.test.ts`\n\n### Security Review\n- [ ] Verify error responses don't leak internal details (stack traces, SQL errors)\n- [ ] Verify input validation occurs before processing\n- [ ] Verify response data is filtered (no sensitive fields exposed)\n- [ ] Verify session renewal only occurs for legitimate mutations\n\n### Documentation Updates\n- [ ] Update `CLAUDE.md` with handler architecture patterns\n- [ ] Update `README.md` with request pipeline details",
"new_string": "## Phase 3: Handler Architecture\n\n**Architecture Refinement (Session 021):** Session renewal happens on EVERY request (queries and mutations)\nto keep sessions active, not just on specific triggers. This is the RPC-style approach where all\nendpoints use POST.\n\n### Prerequisites (cwc-types, cwc-backend-utils)\n- [x] Add `RenewSessionResult` types to cwc-types/authTypes.ts\n- [x] Add `renewSession()` method to AuthClient in cwc-backend-utils\n- [x] Update AuthClient mocks in cwc-api with renewSession helpers\n\n### Route Handler\n- [x] Create `src/handlers/handler.types.ts`\n - [x] Define error codes (`CwcApiErrorCode`)\n - [x] Define response types (`CwcApiSuccessResponse`, `CwcApiErrorResponse`, `CwcApiHandlerResponse`)\n - [x] Define route config types (`CwcApiRouteConfig`, `CwcApiHandlerType`)\n - [x] Define operation types (`OperationContext`, `OperationResult`, `CwcApiOperation`)\n - [x] Define handler options types (`RouteHandlerOptions`, `QueryHandlerOptions`, `MutationHandlerOptions`)\n - [x] Define access policy stubs for Phase 4 (`RouteAccessResult`, `OperationAccessResult`)\n- [x] Create `src/handlers/RouteHandler.ts`\n - [x] Check route-level access policy (role vs allowedRoles)\n - [x] Build operation context from path params\n - [x] Delegate to QueryHandler or MutationHandler based on handlerType\n - [x] Return 401 for unauthenticated access denied\n - [x] Return 403 for authenticated but unauthorized\n- [x] Create `src/handlers/QueryHandler.ts`\n - [x] Operation-level access check (stub for Phase 4)\n - [x] Execute query operation\n - [x] Renew session (always, to keep session active)\n - [x] Format response with data, pagination, and new JWT\n- [x] Create `src/handlers/MutationHandler.ts`\n - [x] Operation-level access check (stub for Phase 4)\n - [x] Execute mutation operation\n - [x] Renew session (always, to keep session active)\n - [x] Format response with data and new JWT\n- [x] Create `src/handlers/index.ts`\n\n### Unit Tests\n- [x] Create `src/__tests__/handlers/RouteHandler.test.ts` (14 tests)\n- [x] Create `src/__tests__/handlers/QueryHandler.test.ts` (15 tests)\n- [x] Create `src/__tests__/handlers/MutationHandler.test.ts` (15 tests)\n- [x] Create `src/__tests__/mocks/routeConfig.mock.ts`\n - [x] `createMockRouteConfig()`, `createMockOperationContext()`\n - [x] `createMockGuestContext()`, `createMockAuthenticatedContext()`\n - [x] `mockOperationSuccess()`, `mockOperationFailure()`, `mockOperationThrows()`\n\n### Security Review\n- [x] Verify error responses don't leak internal details (errorDetail only in dev mode)\n- [x] Verify operation access check is stubbed (always allowed for Phase 3)\n- [x] Verify session renewal failure doesn't fail the request (graceful degradation)\n- [x] Verify guest users don't get session renewal\n\n### Documentation Updates\n- [x] Update `CLAUDE.md` with handler architecture patterns\n- [ ] Update `README.md` with request pipeline 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:
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 SystemNow let me add session notes for Session 021.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/cwc-api-implementation-checklist.md",
"old_string": " - All 10 context tests pass, all 38 cwc-api tests pass\n - All affected packages typecheck: cwc-types, cwc-auth, cwc-backend-utils, cwc-api\n - **Note:** AuthClient unit tests deferred (cwc-backend-utils has no test infrastructure)\n - Functionality verified through cwc-api context tests",
"new_string": " - All 10 context tests pass, all 38 cwc-api tests pass\n - All affected packages typecheck: cwc-types, cwc-auth, cwc-backend-utils, cwc-api\n - **Note:** AuthClient unit tests deferred (cwc-backend-utils has no test infrastructure)\n - Functionality verified through cwc-api context tests\n\n### 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)"
}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: 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→
Let me run a full typecheck to verify everything is in order.
Bash
{
"command": "pnpm typecheck 2>&1 | tail -30",
"description": "Run full monorepo typecheck",
"timeout": 60000
}Tool Result
cwc-configuration-helper:typecheck: cwc-transcript-parser:typecheck: cwc-transcript-parser:typecheck: > cwc-transcript-parser@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-transcript-parser cwc-transcript-parser:typecheck: > tsc --noEmit cwc-transcript-parser:typecheck: cwc-auth:typecheck: cache miss, executing 96962a1b5e275b2d cwc-storage:typecheck: cache miss, executing ee5621bd7f683900 cwc-api:typecheck: cache miss, executing 574a472c9d4fc760 cwc-sql:typecheck: cache miss, executing fbdefbd41eb292fa cwc-storage:typecheck: cwc-storage:typecheck: > cwc-storage@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-storage cwc-storage:typecheck: > tsc --noEmit cwc-storage:typecheck: cwc-api:typecheck: cwc-api:typecheck: > cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api cwc-api:typecheck: > tsc --noEmit cwc-api:typecheck: cwc-auth:typecheck: cwc-auth:typecheck: > cwc-auth@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-auth cwc-auth:typecheck: > tsc --noEmit cwc-auth:typecheck: cwc-sql:typecheck: cwc-sql:typecheck: > cwc-sql@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-sql cwc-sql:typecheck: > tsc --noEmit cwc-sql:typecheck: Tasks: 14 successful, 14 total Cached: 4 cached, 14 total Time: 2.199s
All 14 packages typecheck successfully. Let me mark the final task as complete.
TodoWrite
{
"todos": [
{
"content": "Add RenewSessionResult types to cwc-types/authTypes.ts",
"status": "completed",
"activeForm": "Adding RenewSessionResult types to cwc-types"
},
{
"content": "Add renewSession() method to AuthClient in cwc-backend-utils",
"status": "completed",
"activeForm": "Adding renewSession() method to AuthClient"
},
{
"content": "Update AuthClient mocks in cwc-api with renewSession helpers",
"status": "completed",
"activeForm": "Updating AuthClient mocks"
},
{
"content": "Create handler.types.ts with all handler type definitions",
"status": "completed",
"activeForm": "Creating handler.types.ts"
},
{
"content": "Create QueryHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating QueryHandler.ts"
},
{
"content": "Create MutationHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating MutationHandler.ts"
},
{
"content": "Create RouteHandler.ts with unit tests",
"status": "completed",
"activeForm": "Creating RouteHandler.ts"
},
{
"content": "Create handlers/index.ts exports",
"status": "completed",
"activeForm": "Creating handlers/index.ts"
},
{
"content": "Update CLAUDE.md with handler architecture documentation",
"status": "completed",
"activeForm": "Updating CLAUDE.md"
},
{
"content": "Update implementation checklist with Phase 3 completion",
"status": "completed",
"activeForm": "Updating implementation checklist"
}
]
}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 3 Complete!
The Handler Architecture for cwc-api has been fully implemented:
Prerequisites Completed:
- Added
RenewSessionResulttypes to cwc-types - Added
renewSession()method to AuthClient in cwc-backend-utils - Updated AuthClient mocks with renewSession helpers
Handler Architecture:
handler.types.ts- Error codes, response types, route config, operation types, access policy stubsRouteHandler.ts- Entry point: route access check, delegates to Query/MutationHandlerQueryHandler.ts- Read operations with pagination and session renewalMutationHandler.ts- Write operations with session renewalhandlers/index.ts- Exports all types and handlers
Test Coverage:
- 44 tests total (14 RouteHandler + 15 QueryHandler + 15 MutationHandler)
- Comprehensive mocks in
routeConfig.mock.ts - Config mocks renamed for consistency:
getUnitConfig(),createDevConfig(),createProdConfig()
Key Design Decisions:
- Session renewal on EVERY request (not just triggers) to keep sessions active
- RPC-style API: All endpoints use POST
- Graceful degradation: Session renewal failure doesn't fail the request
- Operation access check stubbed for Phase 4 (always allowed for now)
All packages typecheck (14 successful) and all handler tests pass.
Excellent work! I am very particular about naming because I am easily confused!
In this code, in MutationHandler.tests.ts:
const unitConfig = getUnitConfig();
const prodConfig = createProdConfig();
I would like to change createProdConfig to createMockProdConfig, and prodConfig to mockProdConfig because these are mock config objects, not loaded using the actual config system as is the case for const unitConfig = getUnitConfig();
Let's apply this QueryHandler.test.ts and RouteHandler.test.ts and associated files.
Thank you!
Absolutely, that's a good distinction - getUnitConfig() loads the real config while the create* functions create mock configs. Let me update the naming across all files.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts",
"old_string": "/**\n * Creates a prod config for testing production error behavior\n */\nexport function createProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'prod',\n isProduction: true,\n isDevelopment: false,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a unit config (should match what's loaded from unit.cwc-api.env)\n */\nexport function createUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a dev config for testing dev-mode behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}",
"new_string": "/**\n * Creates a mock prod config for testing production error behavior\n */\nexport function createMockProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'prod',\n isProduction: true,\n isDevelopment: false,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a mock unit config (should match what's loaded from unit.cwc-api.env)\n */\nexport function createMockUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'unit',\n isProduction: false,\n isDevelopment: false,\n isTest: false,\n isUnitTest: true,\n isEndToEnd: false,\n debugMode: false,\n ...overrides,\n });\n}\n\n/**\n * Creates a mock dev config for testing dev-mode behavior\n * Has isDevelopment: true so error details are exposed\n */\nexport function createMockDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {\n return createMockConfig({\n runtimeEnvironment: 'dev',\n isProduction: false,\n isDevelopment: true,\n isTest: false,\n isUnitTest: false,\n isEndToEnd: false,\n debugMode: true,\n ...overrides,\n });\n}"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/config.mock.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
18→ const baseConfig = loadConfig();
19→ return { ...baseConfig, ...overrides };
20→}
21→
22→/**
23→ * Creates a mock prod config for testing production error behavior
24→ */
25→export function createMockProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
26→ return createMockConfig({
27→ runtimeEnvironment: 'prod',
28→ isProduction: true,
29→ isDevelopment: false,
30→ isTest: false,
31→ isUnitTest: false,
32→ isEndToEnd: false,
33→ debugMode: false,
34→ ...overrides,
35→ });
36→}
37→
38→/**
39→ * Creates a mock unit config (should match what's loaded from unit.cwc-api.env)
40→ */
41→export function createMockUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
42→ return createMockConfig({
43→ runtimeEnvironment: 'unit',
44→ isProduction: false,
45→ isDevelopment: false,
46→ isTest: false,
47→ isUnitTest: true,
48→ isEndToEnd: false,
49→ debugMode: false,
50→ ...overrides,
51→ });
52→}
53→
54→/**
55→ * Creates a mock dev config for testing dev-mode behavior
56→ * Has isDevelopment: true so error details are exposed
57→ */
58→export function createMockDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
59→ return createMockConfig({
60→ runtimeEnvironment: 'dev',
61→ isProduction: false,
62→ isDevelopment: true,
63→ isTest: false,
64→ isUnitTest: false,
65→ isEndToEnd: false,
66→ debugMode: true,
67→ ...overrides,
68→ });
69→}
70→Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts",
"old_string": "export {\n getUnitConfig,\n createMockConfig,\n createProdConfig,\n createUnitConfig,\n createDevConfig,\n} from './config.mock';",
"new_string": "export {\n getUnitConfig,\n createMockConfig,\n createMockProdConfig,\n createMockUnitConfig,\n createMockDevConfig,\n} from './config.mock';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/mocks/index.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
12→ mockDeleteSuccess,
13→ mockDeleteNotFound,
14→} from './sqlClient.mock';
15→
16→export {
17→ getUnitConfig,
18→ createMockConfig,
19→ createMockProdConfig,
20→ createMockUnitConfig,
21→ createMockDevConfig,
22→} from './config.mock';
23→
24→export { createMockLogger } from './logger.mock';
25→
26→export {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createDevConfig,\n createProdConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createMockDevConfig,\n createMockProdConfig,\n createMockLogger,\n} from '../mocks';"
}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:
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { MutationHandler } from '../../handlers/MutationHandler';
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 { MutationHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('MutationHandler', () => {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('MutationHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const prodConfig = createProdConfig();",
"new_string": "describe('MutationHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const mockProdConfig = createMockProdConfig();"
}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:
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { MutationHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('MutationHandler', () => {
24→ let mockAuthClient: jest.Mocked<AuthClient>;
25→ const unitConfig = getUnitConfig();
26→ const mockProdConfig = createMockProdConfig();
27→
28→ beforeEach(() => {
29→ mockAuthClient = createMockAuthClient();
30→ jest.clearAllMocks();Grep
{
"pattern": "prodConfig|devConfig",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"output_mode": "content"
}Tool Result
301: const devConfig = createDevConfig(); 312: const handler = new MutationHandler(options, devConfig, mockAuthClient, undefined); 334: const handler = new MutationHandler(options, prodConfig, mockAuthClient, undefined);
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": "const devConfig = createDevConfig();",
"new_string": "const mockDevConfig = createMockDevConfig();"
}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:
297→
298→ it('should include error detail when isDevelopment is true', async () => {
299→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
300→ const context = createMockAuthenticatedContext();
301→ const mockDevConfig = createMockDevConfig();
302→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
303→
304→ const options: MutationHandlerOptions = {
305→ context,Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": "const handler = new MutationHandler(options, devConfig, mockAuthClient, undefined);",
"new_string": "const handler = new MutationHandler(options, mockDevConfig, mockAuthClient, undefined);"
}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:
308→ payload: {},
309→ operationContext: { context },
310→ };
311→
312→ const handler = new MutationHandler(options, mockDevConfig, mockAuthClient, undefined);
313→ const response = await handler.processRequest();
314→
315→ expect(response.body.success).toBe(false);
316→ if (!response.body.success) {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": "const handler = new MutationHandler(options, prodConfig, mockAuthClient, undefined);",
"new_string": "const handler = new MutationHandler(options, mockProdConfig, mockAuthClient, undefined);"
}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:
330→ payload: {},
331→ operationContext: { context },
332→ };
333→
334→ const handler = new MutationHandler(options, mockProdConfig, mockAuthClient, undefined);
335→ const response = await handler.processRequest();
336→
337→ expect(response.body.success).toBe(false);
338→ if (!response.body.success) {Now let me update QueryHandler.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→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationFailure,
12→ mockOperationThrows,
13→ mockRenewSessionSuccess,
14→ mockRenewSessionFailure,
15→ getUnitConfig,
16→ createDevConfig,
17→ createProdConfig,
18→ createMockLogger,
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {
24→ let mockAuthClient: jest.Mocked<AuthClient>;
25→ const unitConfig = getUnitConfig();
26→ const prodConfig = createProdConfig();
27→
28→ beforeEach(() => {
29→ mockAuthClient = createMockAuthClient();
30→ jest.clearAllMocks();
31→ });
32→
33→ describe('Successful Operations', () => {
34→ it('should return 200 with data on successful query', async () => {
35→ const routeConfig = createMockRouteConfig();
36→ const context = createMockGuestContext();
37→ mockOperationSuccess(routeConfig, { id: 1, name: 'test' });
38→
39→ const options: QueryHandlerOptions = {
40→ context,
41→ routeConfig,
42→ authHeader: undefined,
43→ payload: {},
44→ operationContext: { context },
45→ };
46→
47→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
48→ const response = await handler.processRequest();
49→
50→ expect(response.statusCode).toBe(200);
51→ expect(response.body.success).toBe(true);
52→ if (response.body.success) {
53→ expect(response.body.data).toEqual({ id: 1, name: 'test' });
54→ }
55→ });
56→
57→ it('should include pagination when operation returns it', async () => {
58→ const routeConfig = createMockRouteConfig();
59→ const context = createMockGuestContext();
60→ const pagination = { page: 1, pageSize: 20, totalCount: 100, hasMore: true };
61→ mockOperationSuccess(routeConfig, [{ id: 1 }, { id: 2 }], pagination);
62→
63→ const options: QueryHandlerOptions = {
64→ context,
65→ routeConfig,
66→ authHeader: undefined,
67→ payload: {},
68→ operationContext: { context },
69→ };
70→
71→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
72→ const response = await handler.processRequest();
73→
74→ expect(response.statusCode).toBe(200);
75→ expect(response.body.success).toBe(true);
76→ if (response.body.success) {
77→ expect(response.body.pagination).toEqual(pagination);
78→ }
79→ });
80→ });
81→
82→ describe('Session Renewal', () => {
83→ it('should call renewSession for authenticated users', async () => {
84→ const routeConfig = createMockRouteConfig();
85→ const context = createMockAuthenticatedContext();
86→ mockOperationSuccess(routeConfig, { id: 1 });
87→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
88→
89→ const options: QueryHandlerOptions = {
90→ context,
91→ routeConfig,
92→ authHeader: 'Bearer old-token',
93→ payload: {},
94→ operationContext: { context },
95→ };
96→
97→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
98→ const response = await handler.processRequest();
99→
100→ expect(mockAuthClient.renewSession).toHaveBeenCalledWith('Bearer old-token');
101→ expect(response.statusCode).toBe(200);
102→ expect(response.body.success).toBe(true);
103→ if (response.body.success) {
104→ expect(response.body.jwt).toBe('new-jwt-token');
105→ }
106→ });
107→
108→ it('should not call renewSession for guest users', async () => {
109→ const routeConfig = createMockRouteConfig();
110→ const context = createMockGuestContext();
111→ mockOperationSuccess(routeConfig, { id: 1 });
112→
113→ const options: QueryHandlerOptions = {
114→ context,
115→ routeConfig,
116→ authHeader: undefined,
117→ payload: {},
118→ operationContext: { context },
119→ };
120→
121→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
122→ const response = await handler.processRequest();
123→
124→ expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
125→ expect(response.statusCode).toBe(200);
126→ expect(response.body.success).toBe(true);
127→ if (response.body.success) {
128→ expect(response.body.jwt).toBeUndefined();
129→ }
130→ });
131→
132→ it('should succeed operation when renewal fails (graceful handling)', async () => {
133→ const routeConfig = createMockRouteConfig();
134→ const context = createMockAuthenticatedContext();
135→ const mockLogger = createMockLogger();
136→ mockOperationSuccess(routeConfig, { id: 1 });
137→ mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
138→
139→ const options: QueryHandlerOptions = {
140→ context,
141→ routeConfig,
142→ authHeader: 'Bearer old-token',
143→ payload: {},
144→ operationContext: { context },
145→ };
146→
147→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, mockLogger);
148→ const response = await handler.processRequest();
149→
150→ expect(response.statusCode).toBe(200);
151→ expect(response.body.success).toBe(true);
152→ if (response.body.success) {
153→ expect(response.body.jwt).toBeUndefined(); // No JWT since renewal failed
154→ expect(response.body.data).toEqual({ id: 1 });
155→ }
156→ expect(mockLogger.logError).toHaveBeenCalled();
157→ });
158→
159→ it('should not include jwt in response when renewal fails', async () => {
160→ const routeConfig = createMockRouteConfig();
161→ const context = createMockAuthenticatedContext();
162→ mockOperationSuccess(routeConfig, { id: 1 });
163→ mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');
164→
165→ const options: QueryHandlerOptions = {
166→ context,
167→ routeConfig,
168→ authHeader: 'Bearer old-token',
169→ payload: {},
170→ operationContext: { context },
171→ };
172→
173→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
174→ const response = await handler.processRequest();
175→
176→ expect(response.body.success).toBe(true);
177→ if (response.body.success) {
178→ expect(response.body.jwt).toBeUndefined();
179→ }
180→ });
181→ });
182→
183→ describe('Error Responses', () => {
184→ it('should return 404 for NOT_FOUND error code', async () => {
185→ const routeConfig = createMockRouteConfig();
186→ const context = createMockGuestContext();
187→ mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
188→
189→ const options: QueryHandlerOptions = {
190→ context,
191→ routeConfig,
192→ authHeader: undefined,
193→ payload: {},
194→ operationContext: { context },
195→ };
196→
197→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
198→ const response = await handler.processRequest();
199→
200→ expect(response.statusCode).toBe(404);
201→ expect(response.body.success).toBe(false);
202→ if (!response.body.success) {
203→ expect(response.body.errorCode).toBe('NOT_FOUND');
204→ expect(response.body.errorMessage).toBe('Resource not found');
205→ }
206→ });
207→
208→ it('should return 400 for VALIDATION_ERROR', async () => {
209→ const routeConfig = createMockRouteConfig();
210→ const context = createMockGuestContext();
211→ mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
212→
213→ const options: QueryHandlerOptions = {
214→ context,
215→ routeConfig,
216→ authHeader: undefined,
217→ payload: {},
218→ operationContext: { context },
219→ };
220→
221→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
222→ const response = await handler.processRequest();
223→
224→ expect(response.statusCode).toBe(400);
225→ expect(response.body.success).toBe(false);
226→ if (!response.body.success) {
227→ expect(response.body.errorCode).toBe('VALIDATION_ERROR');
228→ }
229→ });
230→
231→ it('should return 403 for OPERATION_ACCESS_DENIED', async () => {
232→ const routeConfig = createMockRouteConfig();
233→ const context = createMockGuestContext();
234→ mockOperationFailure(routeConfig, 'OPERATION_ACCESS_DENIED', 'Not permitted');
235→
236→ const options: QueryHandlerOptions = {
237→ context,
238→ routeConfig,
239→ authHeader: undefined,
240→ payload: {},
241→ operationContext: { context },
242→ };
243→
244→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
245→ const response = await handler.processRequest();
246→
247→ expect(response.statusCode).toBe(403);
248→ expect(response.body.success).toBe(false);
249→ });
250→
251→ it('should return 500 for DATABASE_ERROR', async () => {
252→ const routeConfig = createMockRouteConfig();
253→ const context = createMockGuestContext();
254→ mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
255→
256→ const options: QueryHandlerOptions = {
257→ context,
258→ routeConfig,
259→ authHeader: undefined,
260→ payload: {},
261→ operationContext: { context },
262→ };
263→
264→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
265→ const response = await handler.processRequest();
266→
267→ expect(response.statusCode).toBe(500);
268→ expect(response.body.success).toBe(false);
269→ if (!response.body.success) {
270→ expect(response.body.errorCode).toBe('DATABASE_ERROR');
271→ }
272→ });
273→ });
274→
275→ describe('Exception Handling', () => {
276→ it('should catch errors and return 500 response', async () => {
277→ const routeConfig = createMockRouteConfig();
278→ const context = createMockGuestContext();
279→ mockOperationThrows(routeConfig, new Error('Unexpected database error'));
280→
281→ const options: QueryHandlerOptions = {
282→ context,
283→ routeConfig,
284→ authHeader: undefined,
285→ payload: {},
286→ operationContext: { context },
287→ };
288→
289→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
290→ const response = await handler.processRequest();
291→
292→ expect(response.statusCode).toBe(500);
293→ expect(response.body.success).toBe(false);
294→ if (!response.body.success) {
295→ expect(response.body.errorCode).toBe('INTERNAL_ERROR');
296→ expect(response.body.errorMessage).toBe('An internal error occurred');
297→ }
298→ });
299→
300→ it('should include error detail when isDevelopment is true', async () => {
301→ const routeConfig = createMockRouteConfig();
302→ const context = createMockGuestContext();
303→ const devConfig = createDevConfig();
304→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
305→
306→ const options: QueryHandlerOptions = {
307→ context,
308→ routeConfig,
309→ authHeader: undefined,
310→ payload: {},
311→ operationContext: { context },
312→ };
313→
314→ const handler = new QueryHandler(options, devConfig, mockAuthClient, undefined);
315→ const response = await handler.processRequest();
316→
317→ expect(response.body.success).toBe(false);
318→ if (!response.body.success) {
319→ expect(response.body.errorDetail).toBe('Detailed error message');
320→ }
321→ });
322→
323→ it('should not include error detail in production mode', async () => {
324→ const routeConfig = createMockRouteConfig();
325→ const context = createMockGuestContext();
326→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
327→
328→ const options: QueryHandlerOptions = {
329→ context,
330→ routeConfig,
331→ authHeader: undefined,
332→ payload: {},
333→ operationContext: { context },
334→ };
335→
336→ // prodConfig has isDevelopment: false
337→ const handler = new QueryHandler(options, prodConfig, mockAuthClient, undefined);
338→ const response = await handler.processRequest();
339→
340→ expect(response.body.success).toBe(false);
341→ if (!response.body.success) {
342→ expect(response.body.errorDetail).toBeUndefined();
343→ }
344→ });
345→
346→ it('should log errors when logger is provided', async () => {
347→ const routeConfig = createMockRouteConfig();
348→ const context = createMockAuthenticatedContext();
349→ const mockLogger = createMockLogger();
350→ mockOperationThrows(routeConfig, new Error('Test error'));
351→
352→ const options: QueryHandlerOptions = {
353→ context,
354→ routeConfig,
355→ authHeader: 'Bearer token',
356→ payload: {},
357→ operationContext: { context },
358→ };
359→
360→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, mockLogger);
361→ await handler.processRequest();
362→
363→ expect(mockLogger.logError).toHaveBeenCalled();
364→ });
365→ });
366→
367→ describe('Operation Access (Phase 3 Stub)', () => {
368→ it('should allow operation access (stub always returns allowed)', async () => {
369→ const routeConfig = createMockRouteConfig();
370→ const context = createMockGuestContext();
371→ mockOperationSuccess(routeConfig, { id: 1 });
372→
373→ const options: QueryHandlerOptions = {
374→ context,
375→ routeConfig,
376→ authHeader: undefined,
377→ payload: {},
378→ operationContext: { context },
379→ };
380→
381→ const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
382→ const response = await handler.processRequest();
383→
384→ // Operation access is stubbed to always allow in Phase 3
385→ expect(response.statusCode).toBe(200);
386→ expect(response.body.success).toBe(true);
387→ });
388→ });
389→});
390→
<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__/handlers/QueryHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createDevConfig,\n createProdConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createMockDevConfig,\n createMockProdConfig,\n createMockLogger,\n} from '../mocks';"
}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:
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { QueryHandler } from '../../handlers/QueryHandler';
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 { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {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('QueryHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const prodConfig = createProdConfig();",
"new_string": "describe('QueryHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const mockProdConfig = createMockProdConfig();"
}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:
19→} from '../mocks';
20→import type { AuthClient } from 'cwc-backend-utils';
21→import type { QueryHandlerOptions } from '../../handlers/handler.types';
22→
23→describe('QueryHandler', () => {
24→ let mockAuthClient: jest.Mocked<AuthClient>;
25→ const unitConfig = getUnitConfig();
26→ const mockProdConfig = createMockProdConfig();
27→
28→ beforeEach(() => {
29→ mockAuthClient = createMockAuthClient();
30→ jest.clearAllMocks();Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": "const devConfig = createDevConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, devConfig, mockAuthClient, undefined);",
"new_string": "const mockDevConfig = createMockDevConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, mockDevConfig, mockAuthClient, undefined);"
}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:
299→
300→ it('should include error detail when isDevelopment is true', async () => {
301→ const routeConfig = createMockRouteConfig();
302→ const context = createMockGuestContext();
303→ const mockDevConfig = createMockDevConfig();
304→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
305→
306→ const options: QueryHandlerOptions = {
307→ context,
308→ routeConfig,
309→ authHeader: undefined,
310→ payload: {},
311→ operationContext: { context },
312→ };
313→
314→ const handler = new QueryHandler(options, mockDevConfig, mockAuthClient, undefined);
315→ const response = await handler.processRequest();
316→
317→ expect(response.body.success).toBe(false);
318→ if (!response.body.success) {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts",
"old_string": "// prodConfig has isDevelopment: false\n const handler = new QueryHandler(options, prodConfig, mockAuthClient, undefined);",
"new_string": "// mockProdConfig has isDevelopment: false\n const handler = new QueryHandler(options, mockProdConfig, mockAuthClient, undefined);"
}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:
332→ payload: {},
333→ operationContext: { context },
334→ };
335→
336→ // mockProdConfig has isDevelopment: false
337→ const handler = new QueryHandler(options, mockProdConfig, mockAuthClient, undefined);
338→ const response = await handler.processRequest();
339→
340→ expect(response.body.success).toBe(false);
341→ if (!response.body.success) {Now let me update RouteHandler.test.ts.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts",
"old_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationThrows,\n mockRenewSessionSuccess,\n getUnitConfig,\n createDevConfig,\n createProdConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationThrows,\n mockRenewSessionSuccess,\n getUnitConfig,\n createMockDevConfig,\n createMockProdConfig,\n createMockLogger,\n} from '../mocks';"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.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 { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { RouteHandler } from '../../handlers/RouteHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationThrows,
12→ mockRenewSessionSuccess,
13→ getUnitConfig,
14→ createMockDevConfig,
15→ createMockProdConfig,
16→ createMockLogger,
17→} from '../mocks';
18→import type { AuthClient } from 'cwc-backend-utils';
19→import type { RouteHandlerOptions } from '../../handlers/handler.types';
20→
21→describe('RouteHandler', () => {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts",
"old_string": "describe('RouteHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const devConfig = createDevConfig();\n const prodConfig = createProdConfig();",
"new_string": "describe('RouteHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const mockDevConfig = createMockDevConfig();\n const mockProdConfig = createMockProdConfig();"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
17→} from '../mocks';
18→import type { AuthClient } from 'cwc-backend-utils';
19→import type { RouteHandlerOptions } from '../../handlers/handler.types';
20→
21→describe('RouteHandler', () => {
22→ let mockAuthClient: jest.Mocked<AuthClient>;
23→ const unitConfig = getUnitConfig();
24→ const mockDevConfig = createMockDevConfig();
25→ const mockProdConfig = createMockProdConfig();
26→
27→ beforeEach(() => {
28→ mockAuthClient = createMockAuthClient();
29→ jest.clearAllMocks();Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts",
"old_string": "const handler = new RouteHandler(options, devConfig, mockAuthClient, undefined);",
"new_string": "const handler = new RouteHandler(options, mockDevConfig, mockAuthClient, undefined);"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
160→ authHeader: undefined,
161→ payload: {},
162→ };
163→
164→ const handler = new RouteHandler(options, mockDevConfig, mockAuthClient, undefined);
165→ const response = await handler.processRequest();
166→
167→ expect(response.body.success).toBe(false);
168→ if (!response.body.success) {Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts",
"old_string": "const handler = new RouteHandler(options, prodConfig, mockAuthClient, undefined);",
"new_string": "const handler = new RouteHandler(options, mockProdConfig, mockAuthClient, undefined);"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
183→ authHeader: undefined,
184→ payload: {},
185→ };
186→
187→ const handler = new RouteHandler(options, mockProdConfig, mockAuthClient, undefined);
188→ const response = await handler.processRequest();
189→
190→ expect(response.body.success).toBe(false);
191→ if (!response.body.success) {Now let me update the CLAUDE.md to reflect the new naming convention.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "| **Config Mocks** | |\n| `getUnitConfig()` | Load real config from unit.cwc-api.env |\n| `createMockConfig(overrides)` | Create config with custom overrides |\n| `createDevConfig(overrides)` | Config with isDevelopment: true |\n| `createProdConfig(overrides)` | Config with isProduction: true |\n| `createUnitConfig(overrides)` | Config with isUnitTest: true |",
"new_string": "| **Config Mocks** | |\n| `getUnitConfig()` | Load real config from unit.cwc-api.env |\n| `createMockConfig(overrides)` | Create config with custom overrides |\n| `createMockDevConfig(overrides)` | Mock config with isDevelopment: true |\n| `createMockProdConfig(overrides)` | Mock config with isProduction: true |\n| `createMockUnitConfig(overrides)` | Mock config with isUnitTest: true |"
}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: 341→| `createMockAuthenticatedContext(overrides)` | Create authenticated user context | 342→| `mockOperationSuccess(routeConfig, data)` | Configure operation to succeed | 343→| `mockOperationFailure(routeConfig, code, msg)` | Configure operation to fail | 344→| `mockOperationThrows(routeConfig, error)` | Configure operation to throw | 345→| **Config Mocks** | | 346→| `getUnitConfig()` | Load real config from unit.cwc-api.env | 347→| `createMockConfig(overrides)` | Create config with custom overrides | 348→| `createMockDevConfig(overrides)` | Mock config with isDevelopment: true | 349→| `createMockProdConfig(overrides)` | Mock config with isProduction: true | 350→| `createMockUnitConfig(overrides)` | Mock config with isUnitTest: true | 351→| **Logger Mock** | | 352→| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation | 353→ 354→**Testing Pattern:**
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Testing Pattern:**\n\n```typescript\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockRenewSessionSuccess,\n getUnitConfig,\n} from '../mocks';\n\ndescribe('MyHandler', () => {\n const config = getUnitConfig();\n const mockAuthClient = createMockAuthClient();\n\n it('should handle operation success', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt');\n // ... test logic\n });\n});\n```",
"new_string": "**Testing Pattern:**\n\n```typescript\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockRenewSessionSuccess,\n getUnitConfig,\n createMockProdConfig,\n} from '../mocks';\n\ndescribe('MyHandler', () => {\n const unitConfig = getUnitConfig(); // Real config from unit.cwc-api.env\n const mockProdConfig = createMockProdConfig(); // Mock config for prod behavior\n const mockAuthClient = createMockAuthClient();\n\n it('should handle operation success', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt');\n // ... test logic using unitConfig or mockProdConfig\n });\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:
350→| `createMockUnitConfig(overrides)` | Mock config with isUnitTest: true |
351→| **Logger Mock** | |
352→| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |
353→
354→**Testing Pattern:**
355→
356→```typescript
357→import {
358→ createMockAuthClient,
359→ createMockRouteConfig,
360→ createMockAuthenticatedContext,
361→ mockOperationSuccess,
362→ mockRenewSessionSuccess,
363→ getUnitConfig,
364→ createMockProdConfig,
365→} from '../mocks';
366→
367→describe('MyHandler', () => {
368→ const unitConfig = getUnitConfig(); // Real config from unit.cwc-api.env
369→ const mockProdConfig = createMockProdConfig(); // Mock config for prod behavior
370→ const mockAuthClient = createMockAuthClient();
371→
372→ it('should handle operation success', async () => {
373→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
374→ const context = createMockAuthenticatedContext();
375→ mockOperationSuccess(routeConfig, { id: 1 });
376→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
377→ // ... test logic using unitConfig or mockProdConfig
378→ });
379→});
380→```
381→
382→## Related Packages
383→
384→**Depends On:**Let me run the tests to verify everything works correctly.
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"Handler\"",
"description": "Run all Handler tests",
"timeout": 60000
}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 "--testPathPatterns=Handler"
PASS src/__tests__/handlers/RouteHandler.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__/handlers/QueryHandler.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/MutationHandler.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)
Test Suites: 3 passed, 3 total
Tests: 44 passed, 44 total
Snapshots: 0 total
Time: 0.404 s, estimated 1 s
Ran all test suites matching Handler.All 44 tests pass. Let me verify the typecheck as well.
Bash
{
"command": "pnpm --filter cwc-api typecheck",
"description": "Typecheck cwc-api package",
"timeout": 30000
}Tool Result
> cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > tsc --noEmit
Done! The naming has been updated for clarity:
Renamed Functions:
createProdConfig→createMockProdConfigcreateDevConfig→createMockDevConfigcreateUnitConfig→createMockUnitConfig
Renamed Variables in Tests:
prodConfig→mockProdConfigdevConfig→mockDevConfig
Files Updated:
config.mock.ts- Function namesmocks/index.ts- ExportsMutationHandler.test.ts- Imports and variable namesQueryHandler.test.ts- Imports and variable namesRouteHandler.test.ts- Imports and variable namesCLAUDE.md- Documentation and example
The distinction is now clear:
getUnitConfig()/unitConfig→ Loads real config fromunit.cwc-api.envcreateMockProdConfig()/mockProdConfig→ Creates mock config for testing prod behavior
All 44 tests pass and typecheck is clean.
In MutationHandler, and QueryHandler, looking at the following code in processRequest
// Step 2: Execute the operation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
}
If the operation fails, the error response is returned before the session is renewed.
Is that intentional?
Actually, that brings up the issue of whether or not the Query and Mutation handlers should be doing the session renew or if that should be handled by RouteHandler? I'm also wondering if RouteHandler would be named better as RequestHandler. Please share your thoughts with me.
These are excellent observations. Let me share my thoughts on each:
1. Session Renewal on Failed Operations
You're right - currently if the operation fails, we return before session renewal:
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return this.createOperationErrorResponse(...); // Session never renewed
}
// Only renewed on success
const renewalResult = await this.renewSession();
Is this intentional? Honestly, I don't think I gave this enough thought. Given your stated goal of "keep session alive as long as the user is active", I'd argue:
- A failed request (NOT_FOUND, VALIDATION_ERROR) still indicates the user is actively using the app
- The user made a legitimate request - the session should stay alive
- Only truly exceptional errors (maybe DATABASE_ERROR, INTERNAL_ERROR) might warrant not renewing
Recommendation: Renew on all requests, regardless of operation success/failure.
2. Where Should Session Renewal Happen?
This ties directly to the above issue. Currently both QueryHandler and MutationHandler have nearly identical renewal logic. Moving it to RouteHandler would:
| Benefit | Explanation |
|---|---|
| DRY | Remove duplicate code from Query/MutationHandler |
| Single responsibility | RouteHandler handles infrastructure concerns (auth, access, session); Query/MutationHandler focus on business logic |
| Consistent behavior | Renewal happens for all requests, success or failure |
| Cleaner delegation | Query/MutationHandler just return their result; RouteHandler wraps with JWT |
Revised flow:
RouteHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied
├── buildOperationContext()
├── delegate to QueryHandler or MutationHandler
│ └── execute operation → return raw result
├── renewSession() → always, for authenticated users
└── format final response (merge result + new JWT)
Recommendation: Move session renewal to RouteHandler.
3. RouteHandler vs RequestHandler
| Name | Implies |
|---|---|
| RouteHandler | Handles route-level concerns (per route config) |
| RequestHandler | Handles the full HTTP request lifecycle |
I think RequestHandler is more accurate because:
- It's the entry point for processing each HTTP request
- It orchestrates multiple concerns: access, delegation, session, response formatting
- "Route" suggests configuration, while "Request" suggests processing
However, if you think of it as "the handler that uses a RouteConfig to process requests", then RouteHandler still works.
My preference: RequestHandler - it better describes what it does (process a request end-to-end).
Would you like me to:
- Move session renewal to RouteHandler (or RequestHandler)?
- Rename RouteHandler to RequestHandler?
- Ensure renewal happens on all requests (success and failure)?
Yes, let's go with RequestHandler and move the session renewal into RequestHandler and renew the session on all requests, except when there is an authentication error.
Let me read the existing plan file first, then create a new plan for this refactoring task.
Read
{
"file_path": "/Users/jeffbazinet/.claude/plans/steady-foraging-glade.md"
}Tool Result
1→# Phase 3: Handler Architecture Implementation Plan
2→
3→## Overview
4→
5→Implement the handler architecture for cwc-api following the 3-layer pattern established in cwc-auth (ExpressApi → Handler → Worker), adapted for cwc-api's role-based access control.
6→
7→**Key Design Decisions:**
8→- **RPC-style API:** All endpoints use POST (no GET/PUT/DELETE)
9→- **Session renewal:** Always renew on every request to keep sessions active
10→- **Query/Mutation distinction:** Logical separation for pagination and future caching
11→
12→---
13→
14→## Prerequisites (Before Phase 3 Core Work)
15→
16→### 1. Add RenewSessionResult Types to cwc-types
17→
18→**File:** `packages/cwc-types/src/authTypes.ts`
19→
20→Add types for session renewal:
21→- `RenewSessionSuccessResponse` - success: true, jwtType: 'user', jwt: string
22→- `RenewSessionErrorResponse` - success: false, jwtType: undefined, errorCode?, errorDetail?
23→- `RenewSessionResponse` - union of above
24→- `RenewSessionResult` - simplified result for AuthClient consumers
25→
26→### 2. Add renewSession() Method to AuthClient
27→
28→**File:** `packages/cwc-backend-utils/src/AuthClient/AuthClient.ts`
29→
30→Add `renewSession(authHeader)` method following same pattern as `verifyToken()`:
31→- POST to `${authUri}/renew-session`
32→- Returns `RenewSessionResult`
33→- Graceful error handling (no throws)
34→
35→### 3. Update AuthClient Mocks
36→
37→**File:** `packages/cwc-api/src/__tests__/mocks/authClient.mock.ts`
38→
39→Add:
40→- `renewSession` method to `createMockAuthClient()`
41→- `mockRenewSessionSuccess(client, jwt?)` helper
42→- `mockRenewSessionFailure(client, error?)` helper
43→
44→---
45→
46→## Phase 3 Implementation
47→
48→### 1. Create handler.types.ts
49→
50→**File:** `packages/cwc-api/src/handlers/handler.types.ts`
51→
52→Define all handler types:
53→- **Error Codes:** `CwcApiErrorCode` union type
54→- **Response Types:** `CwcApiSuccessResponse<T>`, `CwcApiErrorResponse`, `CwcApiPagination`
55→- **Route Config:** `CwcApiRouteConfig` (path, handlerType: 'query' | 'mutation', allowedRoles)
56→- **Operation Types:** `OperationContext`, `OperationResult<T>`, `CwcApiOperation`
57→- **Note:** No `sessionRenewalTrigger` needed - always renew on every request
58→- **Handler Options:** `RouteHandlerOptions`, `QueryHandlerOptions`, `MutationHandlerOptions`
59→- **Access Policy Interfaces (Phase 4 stubs):** `RouteAccessResult`, `OperationAccessResult`
60→
61→### 2. Create QueryHandler.ts
62→
63→**File:** `packages/cwc-api/src/handlers/QueryHandler.ts`
64→
65→Responsibilities:
66→1. Check operation-level access (stub for Phase 4)
67→2. Execute query operation
68→3. Renew session (always, to keep session active)
69→4. Format response with data/pagination and new JWT
70→
71→Key aspects:
72→- Calls AuthClient.renewSession() on every request
73→- Maps error codes to HTTP status (404, 400, 403, 500)
74→- Returns `CwcApiHandlerResponse` with optional pagination
75→
76→### 3. Create MutationHandler.ts
77→
78→**File:** `packages/cwc-api/src/handlers/MutationHandler.ts`
79→
80→Responsibilities:
81→1. Check operation-level access (stub for Phase 4)
82→2. Execute mutation operation
83→3. Renew session (always, to keep session active)
84→4. Format response with data and new JWT
85→
86→Key aspects:
87→- Calls AuthClient.renewSession() on every request
88→- Returns 200 for all POST operations (RPC-style, no 201)
89→- Logs warning but doesn't fail if renewal fails
90→
91→### 4. Create RouteHandler.ts
92→
93→**File:** `packages/cwc-api/src/handlers/RouteHandler.ts`
94→
95→Responsibilities:
96→1. Check route-level access based on context role vs allowedRoles
97→2. Build OperationContext from path params
98→3. Delegate to QueryHandler or MutationHandler based on handlerType
99→
100→Key aspects:
101→- Uses existing `RequestContext` from Phase 2
102→- Returns 401 for unauthenticated access denied
103→- Returns 403 for authenticated but unauthorized
104→
105→### 5. Create index.ts
106→
107→**File:** `packages/cwc-api/src/handlers/index.ts`
108→
109→Export all types and handler classes.
110→
111→---
112→
113→## Unit Tests
114→
115→### Test Files
116→
117→- `src/__tests__/handlers/RouteHandler.test.ts`
118→- `src/__tests__/handlers/QueryHandler.test.ts`
119→- `src/__tests__/handlers/MutationHandler.test.ts`
120→
121→### Test Coverage
122→
123→**RouteHandler:**
124→- Route access control (401 for guests, 403 for unauthorized roles)
125→- Handler delegation (query vs mutation)
126→- Operation context building from path params
127→- Error handling with dev-only details
128→
129→**QueryHandler:**
130→- Successful operations (200 with data/pagination)
131→- Session renewal (called on every request for authenticated users)
132→- New JWT included in response when renewed
133→- Graceful handling when renewal fails
134→- No renewal attempted for guest users
135→- Error response mapping (404, 400, 403, 500)
136→- Operation access stub (always allowed for Phase 3)
137→
138→**MutationHandler:**
139→- Successful operations (200 for all POST operations)
140→- Session renewal (called on every request for authenticated users)
141→- New JWT included in response when renewed
142→- Graceful handling when renewal fails
143→- No renewal attempted for guest users
144→- Error response mapping
145→
146→### New Mocks Needed
147→
148→**File:** `src/__tests__/mocks/routeConfig.mock.ts`
149→
150→- `createMockRouteConfig(overrides)` - Create test route configs
151→- `createMockOperationContext(overrides)` - Create test operation contexts
152→
153→---
154→
155→## Documentation Updates
156→
157→### Update CLAUDE.md
158→
159→Add section documenting:
160→- Handler architecture pattern
161→- Route configuration structure
162→- Response envelope format
163→- Session renewal flow
164→
165→### Update README.md
166→
167→Add:
168→- Request pipeline documentation
169→- Handler delegation flow
170→
171→---
172→
173→## Implementation Order
174→
175→1. **Prerequisites (cwc-types, cwc-backend-utils)**
176→ - Add RenewSessionResult types
177→ - Add AuthClient.renewSession()
178→ - Update mocks
179→
180→2. **Core Types**
181→ - Create handler.types.ts
182→
183→3. **Handlers (order: QueryHandler → MutationHandler → RouteHandler)**
184→ - Each with corresponding unit tests
185→
186→4. **Exports**
187→ - Create index.ts
188→
189→5. **Documentation**
190→ - Update CLAUDE.md and README.md
191→
192→---
193→
194→## Key Files to Modify/Create
195→
196→| File | Action |
197→|------|--------|
198→| `packages/cwc-types/src/authTypes.ts` | Add RenewSession types |
199→| `packages/cwc-types/src/index.ts` | Export new types |
200→| `packages/cwc-backend-utils/src/AuthClient/AuthClient.ts` | Add renewSession() |
201→| `packages/cwc-api/src/handlers/handler.types.ts` | Create |
202→| `packages/cwc-api/src/handlers/QueryHandler.ts` | Create |
203→| `packages/cwc-api/src/handlers/MutationHandler.ts` | Create |
204→| `packages/cwc-api/src/handlers/RouteHandler.ts` | Create |
205→| `packages/cwc-api/src/handlers/index.ts` | Create |
206→| `packages/cwc-api/src/__tests__/mocks/authClient.mock.ts` | Add renewSession mocks |
207→| `packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts` | Create |
208→| `packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts` | Create |
209→| `packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts` | Create |
210→| `packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts` | Create |
211→| `packages/cwc-api/CLAUDE.md` | Update handler section |
212→| `packages/cwc-api/cwc-api-implementation-checklist.md` | Update checkboxes |
213→
214→---
215→
216→## Verification Steps
217→
218→1. `pnpm --filter cwc-types typecheck` - Types compile
219→2. `pnpm --filter cwc-backend-utils typecheck` - AuthClient updates compile
220→3. `pnpm --filter cwc-api typecheck` - All handler code compiles
221→4. `NODE_ENV=unit pnpm --filter cwc-api test` - All tests pass
222→5. `pnpm typecheck` - Full monorepo typecheck passes
223→
<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 existing plan was for Phase 3 implementation (now complete). This is a different task - a refactoring of the handler architecture. Let me explore the current implementation to create an accurate plan.
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, AuthClient } from 'cwc-backend-utils';
4→import type { CwcApiConfig } from '../config';
5→import type {
6→ CwcApiHandler,
7→ CwcApiHandlerResponse,
8→ QueryHandlerOptions,
9→ OperationAccessResult,
10→ CwcApiErrorCode,
11→} from './handler.types';
12→
13→const codeLocation = 'handlers/QueryHandler.ts';
14→
15→/**
16→ * QueryHandler - Handles read-only operations
17→ *
18→ * Responsibilities:
19→ * 1. Check operation-level access (Phase 4 stub)
20→ * 2. Execute the query operation
21→ * 3. Renew session (always, to keep session active)
22→ * 4. Format successful response with data/pagination and new JWT
23→ */
24→export class QueryHandler implements CwcApiHandler {
25→ private options: QueryHandlerOptions;
26→ private config: CwcApiConfig;
27→ private authClient: AuthClient;
28→ private logger: ILogger | undefined;
29→
30→ constructor(
31→ options: QueryHandlerOptions,
32→ config: CwcApiConfig,
33→ authClient: AuthClient,
34→ logger: ILogger | undefined
35→ ) {
36→ this.options = options;
37→ this.config = config;
38→ this.authClient = authClient;
39→ this.logger = logger;
40→ }
41→
42→ public async processRequest(): Promise<CwcApiHandlerResponse> {
43→ const { context, routeConfig, payload, operationContext, authHeader } = this.options;
44→
45→ try {
46→ // Step 1: Check operation-level access (Phase 4 implementation)
47→ const operationAccess = await this.checkOperationAccess();
48→ if (!operationAccess.allowed) {
49→ return this.createOperationDeniedResponse(operationAccess.reason);
50→ }
51→
52→ // Step 2: Execute the operation
53→ const result = await routeConfig.operation(payload, operationContext);
54→
55→ if (!result.success) {
56→ return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
57→ }
58→
59→ // Step 3: Renew session if authenticated
60→ let newJwt: string | undefined;
61→ if (context.isAuthenticated) {
62→ const renewResult = await this.authClient.renewSession(authHeader);
63→ if (renewResult.success) {
64→ newJwt = renewResult.jwt;
65→ } else {
66→ // Log warning but don't fail the operation
67→ this.logger?.logError({
68→ userPkId: context.userPkId,
69→ codeLocation,
70→ message: `Session renewal failed for query operation`,
71→ error: renewResult.error,
72→ });
73→ }
74→ }
75→
76→ // Step 4: Format response
77→ return {
78→ statusCode: 200,
79→ body: {
80→ success: true,
81→ data: result.data,
82→ ...(result.pagination ? { pagination: result.pagination } : {}),
83→ ...(newJwt ? { jwt: newJwt } : {}),
84→ },
85→ };
86→ } catch (error) {
87→ this.logger?.logError({
88→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
89→ codeLocation,
90→ message: `Error executing query operation for ${routeConfig.path}`,
91→ error,
92→ });
93→
94→ return this.createInternalErrorResponse(error);
95→ }
96→ }
97→
98→ /**
99→ * Check if the current context can perform this operation
100→ * Phase 3: Stub - always returns allowed
101→ * Phase 4: Will check ownership, resource access, etc.
102→ */
103→ private async checkOperationAccess(): Promise<OperationAccessResult> {
104→ // Phase 4 will implement actual access policy checks
105→ // For now, route-level access is sufficient
106→ return { allowed: true };
107→ }
108→
109→ private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
110→ return {
111→ statusCode: 403,
112→ body: {
113→ success: false,
114→ errorCode: 'OPERATION_ACCESS_DENIED',
115→ errorMessage: 'Operation not permitted',
116→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
117→ },
118→ };
119→ }
120→
121→ private createOperationErrorResponse(
122→ errorCode: CwcApiErrorCode,
123→ errorMessage: string
124→ ): CwcApiHandlerResponse {
125→ const statusCode = this.mapErrorCodeToStatus(errorCode);
126→
127→ return {
128→ statusCode,
129→ body: {
130→ success: false,
131→ errorCode,
132→ errorMessage,
133→ },
134→ };
135→ }
136→
137→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
138→ return {
139→ statusCode: 500,
140→ body: {
141→ success: false,
142→ errorCode: 'INTERNAL_ERROR',
143→ errorMessage: 'An internal error occurred',
144→ ...(this.config.isDevelopment && error instanceof Error
145→ ? { errorDetail: error.message }
146→ : {}),
147→ },
148→ };
149→ }
150→
151→ private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
152→ switch (errorCode) {
153→ case 'NOT_FOUND':
154→ return 404;
155→ case 'VALIDATION_ERROR':
156→ case 'INVALID_PARAMETER':
157→ case 'ALREADY_EXISTS':
158→ return 400;
159→ case 'UNAUTHORIZED':
160→ return 401;
161→ case 'FORBIDDEN':
162→ case 'ROUTE_ACCESS_DENIED':
163→ case 'OPERATION_ACCESS_DENIED':
164→ return 403;
165→ case 'INTERNAL_ERROR':
166→ case 'DATABASE_ERROR':
167→ case 'AUTH_SERVICE_ERROR':
168→ return 500;
169→ default:
170→ return 500;
171→ }
172→ }
173→}
174→
<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/MutationHandler.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→ MutationHandlerOptions,
9→ OperationAccessResult,
10→ CwcApiErrorCode,
11→} from './handler.types';
12→
13→const codeLocation = 'handlers/MutationHandler.ts';
14→
15→/**
16→ * MutationHandler - Handles write operations
17→ *
18→ * Responsibilities:
19→ * 1. Check operation-level access (Phase 4 stub)
20→ * 2. Execute the mutation operation
21→ * 3. Renew session (always, to keep session active)
22→ * 4. Format successful response with data and new JWT
23→ */
24→export class MutationHandler implements CwcApiHandler {
25→ private options: MutationHandlerOptions;
26→ private config: CwcApiConfig;
27→ private authClient: AuthClient;
28→ private logger: ILogger | undefined;
29→
30→ constructor(
31→ options: MutationHandlerOptions,
32→ config: CwcApiConfig,
33→ authClient: AuthClient,
34→ logger: ILogger | undefined
35→ ) {
36→ this.options = options;
37→ this.config = config;
38→ this.authClient = authClient;
39→ this.logger = logger;
40→ }
41→
42→ public async processRequest(): Promise<CwcApiHandlerResponse> {
43→ const { context, routeConfig, payload, operationContext, authHeader } = this.options;
44→
45→ try {
46→ // Step 1: Check operation-level access (Phase 4 implementation)
47→ const operationAccess = await this.checkOperationAccess();
48→ if (!operationAccess.allowed) {
49→ return this.createOperationDeniedResponse(operationAccess.reason);
50→ }
51→
52→ // Step 2: Execute the mutation
53→ const result = await routeConfig.operation(payload, operationContext);
54→
55→ if (!result.success) {
56→ return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
57→ }
58→
59→ // Step 3: Renew session if authenticated
60→ let newJwt: string | undefined;
61→ if (context.isAuthenticated) {
62→ const renewResult = await this.authClient.renewSession(authHeader);
63→ if (renewResult.success) {
64→ newJwt = renewResult.jwt;
65→ } else {
66→ // Log warning but don't fail the operation
67→ this.logger?.logError({
68→ userPkId: context.userPkId,
69→ codeLocation,
70→ message: `Session renewal failed for mutation operation`,
71→ error: renewResult.error,
72→ });
73→ }
74→ }
75→
76→ // Step 4: Format response (always 200 for RPC-style API)
77→ return {
78→ statusCode: 200,
79→ body: {
80→ success: true,
81→ data: result.data,
82→ ...(newJwt ? { jwt: newJwt } : {}),
83→ },
84→ };
85→ } catch (error) {
86→ this.logger?.logError({
87→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
88→ codeLocation,
89→ message: `Error executing mutation operation for ${routeConfig.path}`,
90→ error,
91→ });
92→
93→ return this.createInternalErrorResponse(error);
94→ }
95→ }
96→
97→ /**
98→ * Check if the current context can perform this operation
99→ * Phase 3: Stub - always returns allowed
100→ * Phase 4: Will check ownership, resource access, etc.
101→ */
102→ private async checkOperationAccess(): Promise<OperationAccessResult> {
103→ // Phase 4 will implement actual access policy checks
104→ return { allowed: true };
105→ }
106→
107→ private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
108→ return {
109→ statusCode: 403,
110→ body: {
111→ success: false,
112→ errorCode: 'OPERATION_ACCESS_DENIED',
113→ errorMessage: 'Operation not permitted',
114→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
115→ },
116→ };
117→ }
118→
119→ private createOperationErrorResponse(
120→ errorCode: CwcApiErrorCode,
121→ errorMessage: string
122→ ): CwcApiHandlerResponse {
123→ const statusCode = this.mapErrorCodeToStatus(errorCode);
124→
125→ return {
126→ statusCode,
127→ body: {
128→ success: false,
129→ errorCode,
130→ errorMessage,
131→ },
132→ };
133→ }
134→
135→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
136→ return {
137→ statusCode: 500,
138→ body: {
139→ success: false,
140→ errorCode: 'INTERNAL_ERROR',
141→ errorMessage: 'An internal error occurred',
142→ ...(this.config.isDevelopment && error instanceof Error
143→ ? { errorDetail: error.message }
144→ : {}),
145→ },
146→ };
147→ }
148→
149→ private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
150→ switch (errorCode) {
151→ case 'NOT_FOUND':
152→ return 404;
153→ case 'VALIDATION_ERROR':
154→ case 'INVALID_PARAMETER':
155→ case 'ALREADY_EXISTS':
156→ return 400;
157→ case 'UNAUTHORIZED':
158→ return 401;
159→ case 'FORBIDDEN':
160→ case 'ROUTE_ACCESS_DENIED':
161→ case 'OPERATION_ACCESS_DENIED':
162→ return 403;
163→ case 'INTERNAL_ERROR':
164→ case 'DATABASE_ERROR':
165→ case 'AUTH_SERVICE_ERROR':
166→ return 500;
167→ default:
168→ return 500;
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>
I have a clear understanding of the current implementation. The task is well-defined, so I'll proceed directly to writing the plan.
Handler Refactoring: RequestHandler + Centralized Session Renewal
Overview
Refactor the handler architecture to:
- Rename
RouteHandler→RequestHandler - Move session renewal from Query/MutationHandler to RequestHandler
- Renew session on ALL requests (success or failure) except authentication errors (401/403)
Rationale
- Single responsibility: RequestHandler handles infrastructure concerns (auth, access, session)
- DRY: Remove duplicate renewal logic from Query/MutationHandler
- Consistent behavior: Session renewed for all user activity, not just successful operations
- Better naming: "Request" better describes processing the full HTTP request lifecycle
Implementation Plan
1. Rename RouteHandler → RequestHandler
Files:
src/handlers/RouteHandler.ts→src/handlers/RequestHandler.tssrc/__tests__/handlers/RouteHandler.test.ts→src/__tests__/handlers/RequestHandler.test.ts
Changes:
- Rename file
- Rename class
RouteHandler→RequestHandler - Update
codeLocationconstant
2. Update handler.types.ts
File: src/handlers/handler.types.ts
- Rename
RouteHandlerOptions→RequestHandlerOptions
3. Refactor RequestHandler (formerly RouteHandler)
File: src/handlers/RequestHandler.ts
New flow:
processRequest()
├── checkRouteAccess() → 401/403 if denied (NO renewal)
├── buildOperationContext()
├── delegate to QueryHandler or MutationHandler
│ └── returns CwcApiHandlerResponse (without JWT)
├── if response is NOT 401/403 AND user is authenticated:
│ └── renewSession() → merge JWT into response
└── return final response
Key logic:
// After delegation
const response = await delegateToHandler();
// Skip renewal for auth errors (401, 403) or guest users
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) {
response.body.jwt = renewResult.jwt;
}
// Log warning if renewal fails (don't fail the request)
}
return response;
4. Simplify QueryHandler
File: src/handlers/QueryHandler.ts
Remove:
authClientfrom constructorauthHeaderfrom options destructuring- Session renewal logic (Step 3)
- JWT merging in response
Keep:
- Operation access check (Phase 4 stub)
- Execute operation
- Format response (WITHOUT jwt field)
- Error handling
5. Simplify MutationHandler
File: src/handlers/MutationHandler.ts
Same changes as QueryHandler:
- Remove
authClient,authHeader, and all renewal logic - Return response without jwt field
6. Update index.ts
File: src/handlers/index.ts
- Update export:
RouteHandler→RequestHandler - Update type export:
RouteHandlerOptions→RequestHandlerOptions
7. Update Test Files
RequestHandler.test.ts:
- Rename file and describe block
- Add tests for session renewal in RequestHandler:
- Renewal on successful operation
- Renewal on failed operation (NOT_FOUND, VALIDATION_ERROR, etc.)
- NO renewal on 401 (UNAUTHORIZED)
- NO renewal on 403 (FORBIDDEN)
- Graceful handling when renewal fails
QueryHandler.test.ts:
- Remove all session renewal tests
- Remove
mockAuthClientfrom handler constructor - Remove
mockRenewSessionSuccess/Failurecalls
MutationHandler.test.ts:
- Same changes as QueryHandler tests
Files to Modify
| File | Action |
|---|---|
src/handlers/RouteHandler.ts |
Rename to RequestHandler.ts, add renewal logic |
src/handlers/QueryHandler.ts |
Remove authClient, remove renewal logic |
src/handlers/MutationHandler.ts |
Remove authClient, remove renewal logic |
src/handlers/handler.types.ts |
Rename RouteHandlerOptions → RequestHandlerOptions |
src/handlers/index.ts |
Update exports |
src/__tests__/handlers/RouteHandler.test.ts |
Rename, update tests for new renewal behavior |
src/__tests__/handlers/QueryHandler.test.ts |
Remove renewal tests |
src/__tests__/handlers/MutationHandler.test.ts |
Remove renewal tests |
CLAUDE.md |
Update handler architecture documentation |
Test Coverage Changes
RequestHandler tests (add):
- Session renewal on 200 success response
- Session renewal on 404 NOT_FOUND response
- Session renewal on 400 VALIDATION_ERROR response
- Session renewal on 500 DATABASE_ERROR response
- NO renewal on 401 UNAUTHORIZED (route access denied for guest)
- NO renewal on 403 FORBIDDEN (route access denied for wrong role)
- Graceful handling when renewal fails (operation still succeeds)
- No renewal for guest users
QueryHandler/MutationHandler tests (remove):
- All tests in "Session Renewal" describe blocks
- Related mock setup (mockRenewSessionSuccess, etc.)
Verification Steps
pnpm --filter cwc-api typecheck- All types compileNODE_ENV=unit pnpm --filter cwc-api test- All tests pass- Verify test count is similar (tests moved, not removed)
Tool Result
The file /Users/jeffbazinet/.claude/plans/steady-foraging-glade.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→# Handler Refactoring: RequestHandler + Centralized Session Renewal
2→
3→## Overview
4→
5→Refactor the handler architecture to:
6→1. Rename `RouteHandler` → `RequestHandler`
7→2. Move session renewal from Query/MutationHandler to RequestHandler
8→3. Renew session on ALL requests (success or failure) except authentication errors (401/403)
9→
10→## Rationale
11→
12→- **Single responsibility**: RequestHandler handles infrastructure concerns (auth, access, session)
13→- **DRY**: Remove duplicate renewal logic from Query/MutationHandler
14→- **Consistent behavior**: Session renewed for all user activity, not just successful operations
15→- **Better naming**: "Request" better describes processing the full HTTP request lifecycle
16→
17→---
18→
19→## Implementation Plan
20→
21→### 1. Rename RouteHandler → RequestHandler
22→
23→**Files:**
24→- `src/handlers/RouteHandler.ts` → `src/handlers/RequestHandler.ts`
25→- `src/__tests__/handlers/RouteHandler.test.ts` → `src/__tests__/handlers/RequestHandler.test.ts`
26→
27→**Changes:**
28→- Rename file
29→- Rename class `RouteHandler` → `RequestHandler`
30→- Update `codeLocation` constant
31→
32→### 2. Update handler.types.ts
33→
34→**File:** `src/handlers/handler.types.ts`
35→
36→- Rename `RouteHandlerOptions` → `RequestHandlerOptions`
37→
38→### 3. Refactor RequestHandler (formerly RouteHandler)
39→
40→**File:** `src/handlers/RequestHandler.ts`
41→
42→**New flow:**
43→```
44→processRequest()
45→├── checkRouteAccess() → 401/403 if denied (NO renewal)
46→├── buildOperationContext()
47→├── delegate to QueryHandler or MutationHandler
48→│ └── returns CwcApiHandlerResponse (without JWT)
49→├── if response is NOT 401/403 AND user is authenticated:
50→│ └── renewSession() → merge JWT into response
51→└── return final response
52→```
53→
54→**Key logic:**
55→```typescript
56→// After delegation
57→const response = await delegateToHandler();
58→
59→// Skip renewal for auth errors (401, 403) or guest users
60→const isAuthError = response.statusCode === 401 || response.statusCode === 403;
61→if (context.isAuthenticated && !isAuthError) {
62→ const renewResult = await this.authClient.renewSession(authHeader);
63→ if (renewResult.success && response.body.success) {
64→ response.body.jwt = renewResult.jwt;
65→ }
66→ // Log warning if renewal fails (don't fail the request)
67→}
68→
69→return response;
70→```
71→
72→### 4. Simplify QueryHandler
73→
74→**File:** `src/handlers/QueryHandler.ts`
75→
76→**Remove:**
77→- `authClient` from constructor
78→- `authHeader` from options destructuring
79→- Session renewal logic (Step 3)
80→- JWT merging in response
81→
82→**Keep:**
83→- Operation access check (Phase 4 stub)
84→- Execute operation
85→- Format response (WITHOUT jwt field)
86→- Error handling
87→
88→### 5. Simplify MutationHandler
89→
90→**File:** `src/handlers/MutationHandler.ts`
91→
92→**Same changes as QueryHandler:**
93→- Remove `authClient`, `authHeader`, and all renewal logic
94→- Return response without jwt field
95→
96→### 6. Update index.ts
97→
98→**File:** `src/handlers/index.ts`
99→
100→- Update export: `RouteHandler` → `RequestHandler`
101→- Update type export: `RouteHandlerOptions` → `RequestHandlerOptions`
102→
103→### 7. Update Test Files
104→
105→**RequestHandler.test.ts:**
106→- Rename file and describe block
107→- Add tests for session renewal in RequestHandler:
108→ - Renewal on successful operation
109→ - Renewal on failed operation (NOT_FOUND, VALIDATION_ERROR, etc.)
110→ - NO renewal on 401 (UNAUTHORIZED)
111→ - NO renewal on 403 (FORBIDDEN)
112→ - Graceful handling when renewal fails
113→
114→**QueryHandler.test.ts:**
115→- Remove all session renewal tests
116→- Remove `mockAuthClient` from handler constructor
117→- Remove `mockRenewSessionSuccess/Failure` calls
118→
119→**MutationHandler.test.ts:**
120→- Same changes as QueryHandler tests
121→
122→---
123→
124→## Files to Modify
125→
126→| File | Action |
127→|------|--------|
128→| `src/handlers/RouteHandler.ts` | Rename to RequestHandler.ts, add renewal logic |
129→| `src/handlers/QueryHandler.ts` | Remove authClient, remove renewal logic |
130→| `src/handlers/MutationHandler.ts` | Remove authClient, remove renewal logic |
131→| `src/handlers/handler.types.ts` | Rename RouteHandlerOptions → RequestHandlerOptions |
132→| `src/handlers/index.ts` | Update exports |
133→| `src/__tests__/handlers/RouteHandler.test.ts` | Rename, update tests for new renewal behavior |
134→| `src/__tests__/handlers/QueryHandler.test.ts` | Remove renewal tests |
135→| `src/__tests__/handlers/MutationHandler.test.ts` | Remove renewal tests |
136→| `CLAUDE.md` | Update handler architecture documentation |
137→
138→---
139→
140→## Test Coverage Changes
141→
142→**RequestHandler tests (add):**
143→- Session renewal on 200 success response
144→- Session renewal on 404 NOT_FOUND response
145→- Session renewal on 400 VALIDATION_ERROR response
146→- Session renewal on 500 DATABASE_ERROR response
147→- NO renewal on 401 UNAUTHORIZED (route access denied for guest)
148→- NO renewal on 403 FORBIDDEN (route access denied for wrong role)
149→- Graceful handling when renewal fails (operation still succeeds)
150→- No renewal for guest users
151→
152→**QueryHandler/MutationHandler tests (remove):**
153→- All tests in "Session Renewal" describe blocks
154→- Related mock setup (mockRenewSessionSuccess, etc.)
155→
156→---
157→
158→## Verification Steps
159→
160→1. `pnpm --filter cwc-api typecheck` - All types compile
161→2. `NODE_ENV=unit pnpm --filter cwc-api test` - All tests pass
162→3. Verify test count is similar (tests moved, not removed)
163→The plan is ready. It covers:
- Renaming: RouteHandler → RequestHandler (file, class, types, tests)
- Moving session renewal: From Query/MutationHandler to RequestHandler
- New renewal logic: Renew on ALL requests except 401/403 auth errors
- Test updates: Move renewal tests to RequestHandler, simplify Query/MutationHandler tests
Let me set up the todo list and start implementing.
TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "pending",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "pending",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "pending",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "pending",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "pending",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "pending",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "pending",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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
TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "in_progress",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "pending",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "pending",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "pending",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "pending",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "pending",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "pending",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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 * Options for RouteHandler\n */\nexport type RouteHandlerOptions = BaseHandlerOptions & {\n payload: unknown;\n pathParams?: Record<string, string> | undefined;\n};",
"new_string": "/**\n * Options for RequestHandler\n */\nexport type RequestHandlerOptions = BaseHandlerOptions & {\n payload: unknown;\n pathParams?: Record<string, string> | 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:
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→ */TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "in_progress",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "pending",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "pending",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "pending",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "pending",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "pending",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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
Now let me create the new RequestHandler.ts file with the session renewal logic.
Read
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RouteHandler.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→ RouteHandlerOptions,
9→ RouteAccessResult,
10→ OperationContext,
11→} from './handler.types';
12→import { QueryHandler } from './QueryHandler';
13→import { MutationHandler } from './MutationHandler';
14→
15→const codeLocation = 'handlers/RouteHandler.ts';
16→
17→/**
18→ * RouteHandler - Entry point for processing API requests
19→ *
20→ * Responsibilities:
21→ * 1. Check route-level access based on context role
22→ * 2. Build operation context with path params
23→ * 3. Delegate to QueryHandler or MutationHandler based on handlerType
24→ */
25→export class RouteHandler implements CwcApiHandler {
26→ private options: RouteHandlerOptions;
27→ private config: CwcApiConfig;
28→ private authClient: AuthClient;
29→ private logger: ILogger | undefined;
30→
31→ constructor(
32→ options: RouteHandlerOptions,
33→ config: CwcApiConfig,
34→ authClient: AuthClient,
35→ logger: ILogger | undefined
36→ ) {
37→ this.options = options;
38→ this.config = config;
39→ this.authClient = authClient;
40→ this.logger = logger;
41→ }
42→
43→ public async processRequest(): Promise<CwcApiHandlerResponse> {
44→ const { context, routeConfig, payload, authHeader, pathParams } = this.options;
45→
46→ try {
47→ // Step 1: Check route-level access
48→ const routeAccess = this.checkRouteAccess();
49→ if (!routeAccess.allowed) {
50→ return this.createAccessDeniedResponse(routeAccess.reason);
51→ }
52→
53→ // Step 2: Build operation context
54→ const operationContext: OperationContext = {
55→ context,
56→ projectId: pathParams?.['projectId'],
57→ resourceId: pathParams?.['resourceId'],
58→ };
59→
60→ // Step 3: Delegate to appropriate handler
61→ if (routeConfig.handlerType === 'query') {
62→ const queryHandler = new QueryHandler(
63→ {
64→ context,
65→ routeConfig,
66→ authHeader,
67→ payload,
68→ operationContext,
69→ },
70→ this.config,
71→ this.authClient,
72→ this.logger
73→ );
74→ return queryHandler.processRequest();
75→ } else {
76→ const mutationHandler = new MutationHandler(
77→ {
78→ context,
79→ routeConfig,
80→ authHeader,
81→ payload,
82→ operationContext,
83→ },
84→ this.config,
85→ this.authClient,
86→ this.logger
87→ );
88→ return mutationHandler.processRequest();
89→ }
90→ } catch (error) {
91→ this.logger?.logError({
92→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
93→ codeLocation,
94→ message: `Error processing route ${routeConfig.path}`,
95→ error,
96→ });
97→
98→ return this.createInternalErrorResponse(error);
99→ }
100→ }
101→
102→ /**
103→ * Check if the current context role can access this route
104→ * Compares context.role against routeConfig.allowedRoles
105→ */
106→ private checkRouteAccess(): RouteAccessResult {
107→ const { context, routeConfig } = this.options;
108→ const { allowedRoles } = routeConfig;
109→
110→ // Check if the current role is in allowedRoles
111→ if (allowedRoles.includes(context.role)) {
112→ return { allowed: true };
113→ }
114→
115→ // For authenticated users with 'project-owner' role requirement,
116→ // 'logged-on-user' should also be allowed (ownership check happens at operation level)
117→ if (
118→ context.isAuthenticated &&
119→ context.role === 'logged-on-user' &&
120→ allowedRoles.includes('project-owner')
121→ ) {
122→ return { allowed: true };
123→ }
124→
125→ return {
126→ allowed: false,
127→ reason: `Role '${context.role}' cannot access route ${routeConfig.path}`,
128→ };
129→ }
130→
131→ private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {
132→ const { context } = this.options;
133→
134→ // Use 401 for unauthenticated, 403 for authenticated but not allowed
135→ const statusCode = context.isAuthenticated ? 403 : 401;
136→ const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';
137→
138→ return {
139→ statusCode,
140→ body: {
141→ success: false,
142→ errorCode,
143→ errorMessage: 'Access denied',
144→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
145→ },
146→ };
147→ }
148→
149→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
150→ return {
151→ statusCode: 500,
152→ body: {
153→ success: false,
154→ errorCode: 'INTERNAL_ERROR',
155→ errorMessage: 'An internal error occurred',
156→ ...(this.config.isDevelopment && error instanceof Error
157→ ? { errorDetail: error.message }
158→ : {}),
159→ },
160→ };
161→ }
162→}
163→
<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>
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts",
"content": "'use strict';\n\nimport 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';\n\nconst codeLocation = 'handlers/RequestHandler.ts';\n\n/**\n * RequestHandler - Entry point for processing API requests\n *\n * Responsibilities:\n * 1. Check route-level access based on context role\n * 2. Build operation context with path params\n * 3. Delegate to QueryHandler or MutationHandler based on handlerType\n * 4. Renew session for authenticated users (except on auth errors)\n */\nexport class RequestHandler implements CwcApiHandler {\n private options: RequestHandlerOptions;\n private config: CwcApiConfig;\n private authClient: AuthClient;\n private logger: ILogger | undefined;\n\n constructor(\n options: RequestHandlerOptions,\n config: CwcApiConfig,\n authClient: AuthClient,\n logger: ILogger | undefined\n ) {\n this.options = options;\n this.config = config;\n this.authClient = authClient;\n this.logger = logger;\n }\n\n 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 }\n\n // Step 2: Build operation context\n const operationContext: OperationContext = {\n context,\n projectId: pathParams?.['projectId'],\n resourceId: pathParams?.['resourceId'],\n };\n\n // 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 }\n\n // Step 4: Renew session for authenticated users (except on auth errors)\n const isAuthError = response.statusCode === 401 || response.statusCode === 403;\n if (context.isAuthenticated && !isAuthError) {\n const renewResult = await this.authClient.renewSession(authHeader);\n if (renewResult.success && response.body.success) {\n // Add JWT to successful response\n (response.body as CwcApiSuccessResponse).jwt = renewResult.jwt;\n } else if (!renewResult.success) {\n // Log warning but don't fail the operation\n this.logger?.logError({\n userPkId: context.userPkId,\n codeLocation,\n message: `Session renewal failed for ${routeConfig.path}`,\n error: renewResult.error,\n });\n }\n }\n\n return response;\n } catch (error) {\n this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error processing request ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(error);\n }\n }\n\n /**\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(reason?: string): CwcApiHandlerResponse {\n const { context } = this.options;\n\n // Use 401 for unauthenticated, 403 for authenticated but not allowed\n const statusCode = context.isAuthenticated ? 403 : 401;\n const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage: 'Access denied',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts
Bash
{
"command": "rm /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RouteHandler.ts",
"description": "Delete old RouteHandler.ts file"
}Tool Result
TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "in_progress",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "pending",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "pending",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "pending",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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
Now let me update QueryHandler to remove authClient and session renewal.
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts",
"content": "'use strict';\n\nimport type { ILogger } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n QueryHandlerOptions,\n OperationAccessResult,\n CwcApiErrorCode,\n} from './handler.types';\n\nconst codeLocation = 'handlers/QueryHandler.ts';\n\n/**\n * QueryHandler - Handles read-only operations\n *\n * Responsibilities:\n * 1. Check operation-level access (Phase 4 stub)\n * 2. Execute the query operation\n * 3. Format successful response with data/pagination\n *\n * Note: Session renewal is handled by RequestHandler\n */\nexport class QueryHandler implements CwcApiHandler {\n private options: QueryHandlerOptions;\n private config: CwcApiConfig;\n private logger: ILogger | undefined;\n\n constructor(\n options: QueryHandlerOptions,\n config: CwcApiConfig,\n logger: ILogger | undefined\n ) {\n this.options = options;\n this.config = config;\n this.logger = logger;\n }\n\n 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 this.createOperationDeniedResponse(operationAccess.reason);\n }\n\n // Step 2: Execute the operation\n const result = await routeConfig.operation(payload, operationContext);\n\n if (!result.success) {\n return this.createOperationErrorResponse(result.errorCode, result.errorMessage);\n }\n\n // Step 3: Format response (JWT added by RequestHandler)\n return {\n statusCode: 200,\n body: {\n success: true,\n data: result.data,\n ...(result.pagination ? { pagination: result.pagination } : {}),\n },\n };\n } catch (error) {\n this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error executing query operation for ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(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\n private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {\n return {\n statusCode: 403,\n body: {\n success: false,\n errorCode: 'OPERATION_ACCESS_DENIED',\n errorMessage: 'Operation not permitted',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createOperationErrorResponse(\n errorCode: CwcApiErrorCode,\n errorMessage: string\n ): CwcApiHandlerResponse {\n const statusCode = this.mapErrorCodeToStatus(errorCode);\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage,\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n\n private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {\n switch (errorCode) {\n case 'NOT_FOUND':\n return 404;\n case 'VALIDATION_ERROR':\n case 'INVALID_PARAMETER':\n case 'ALREADY_EXISTS':\n return 400;\n case 'UNAUTHORIZED':\n return 401;\n case 'FORBIDDEN':\n case 'ROUTE_ACCESS_DENIED':\n case 'OPERATION_ACCESS_DENIED':\n return 403;\n case 'INTERNAL_ERROR':\n case 'DATABASE_ERROR':\n case 'AUTH_SERVICE_ERROR':\n return 500;\n default:\n return 500;\n }\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:
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→ CwcApiErrorCode,
11→} from './handler.types';
12→
13→const codeLocation = 'handlers/QueryHandler.ts';
14→
15→/**
16→ * QueryHandler - Handles read-only operations
17→ *
18→ * Responsibilities:
19→ * 1. Check operation-level access (Phase 4 stub)
20→ * 2. Execute the query operation
21→ * 3. Format successful response with data/pagination
22→ *
23→ * Note: Session renewal is handled by RequestHandler
24→ */
25→export class QueryHandler implements CwcApiHandler {
26→ private options: QueryHandlerOptions;
27→ private config: CwcApiConfig;
28→ private logger: ILogger | undefined;
29→
30→ constructor(
31→ options: QueryHandlerOptions,
32→ config: CwcApiConfig,
33→ logger: ILogger | undefined
34→ ) {
35→ this.options = options;
36→ this.config = config;
37→ this.logger = logger;
38→ }
39→
40→ public async processRequest(): Promise<CwcApiHandlerResponse> {
41→ const { context, routeConfig, payload, operationContext } = this.options;
42→
43→ try {
44→ // Step 1: Check operation-level access (Phase 4 implementation)
45→ const operationAccess = await this.checkOperationAccess();
46→ if (!operationAccess.allowed) {
47→ return this.createOperationDeniedResponse(operationAccess.reason);
48→ }
49→
50→ // Step 2: Execute the operation
51→ const result = await routeConfig.operation(payload, operationContext);
52→
53→ if (!result.success) {
54→ return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
55→ }
56→
57→ // Step 3: Format response (JWT added by RequestHandler)
58→ return {
59→ statusCode: 200,
60→ body: {
61→ success: true,
62→ data: result.data,
63→ ...(result.pagination ? { pagination: result.pagination } : {}),
64→ },
65→ };
66→ } catch (error) {
67→ this.logger?.logError({
68→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
69→ codeLocation,
70→ message: `Error executing query operation for ${routeConfig.path}`,
71→ error,
72→ });
73→
74→ return this.createInternalErrorResponse(error);
75→ }
76→ }
77→
78→ /**
79→ * Check if the current context can perform this operation
80→ * Phase 3: Stub - always returns allowed
81→ * Phase 4: Will check ownership, resource access, etc.
82→ */
83→ private async checkOperationAccess(): Promise<OperationAccessResult> {
84→ // Phase 4 will implement actual access policy checks
85→ // For now, route-level access is sufficient
86→ return { allowed: true };
87→ }
88→
89→ private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
90→ return {
91→ statusCode: 403,
92→ body: {
93→ success: false,
94→ errorCode: 'OPERATION_ACCESS_DENIED',
95→ errorMessage: 'Operation not permitted',
96→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
97→ },
98→ };
99→ }
100→
101→ private createOperationErrorResponse(
102→ errorCode: CwcApiErrorCode,
103→ errorMessage: string
104→ ): CwcApiHandlerResponse {
105→ const statusCode = this.mapErrorCodeToStatus(errorCode);
106→
107→ return {
108→ statusCode,
109→ body: {
110→ success: false,
111→ errorCode,
112→ errorMessage,
113→ },
114→ };
115→ }
116→
117→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
118→ return {
119→ statusCode: 500,
120→ body: {
121→ success: false,
122→ errorCode: 'INTERNAL_ERROR',
123→ errorMessage: 'An internal error occurred',
124→ ...(this.config.isDevelopment && error instanceof Error
125→ ? { errorDetail: error.message }
126→ : {}),
127→ },
128→ };
129→ }
130→
131→ private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
132→ switch (errorCode) {
133→ case 'NOT_FOUND':
134→ return 404;
135→ case 'VALIDATION_ERROR':
136→ case 'INVALID_PARAMETER':
137→ case 'ALREADY_EXISTS':
138→ return 400;
139→ case 'UNAUTHORIZED':
140→ return 401;
141→ case 'FORBIDDEN':
142→ case 'ROUTE_ACCESS_DENIED':
143→ case 'OPERATION_ACCESS_DENIED':
144→ return 403;
145→ case 'INTERNAL_ERROR':
146→ case 'DATABASE_ERROR':
147→ case 'AUTH_SERVICE_ERROR':
148→ return 500;
149→ default:
150→ return 500;
151→ }
152→ }
153→}
154→TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "completed",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "in_progress",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "pending",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "pending",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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/handlers/MutationHandler.ts",
"content": "'use strict';\n\nimport type { ILogger } from 'cwc-backend-utils';\nimport type { CwcApiConfig } from '../config';\nimport type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n MutationHandlerOptions,\n OperationAccessResult,\n CwcApiErrorCode,\n} from './handler.types';\n\nconst codeLocation = 'handlers/MutationHandler.ts';\n\n/**\n * MutationHandler - Handles write operations\n *\n * Responsibilities:\n * 1. Check operation-level access (Phase 4 stub)\n * 2. Execute the mutation operation\n * 3. Format successful response with data\n *\n * Note: Session renewal is handled by RequestHandler\n */\nexport class MutationHandler implements CwcApiHandler {\n private options: MutationHandlerOptions;\n private config: CwcApiConfig;\n private logger: ILogger | undefined;\n\n constructor(\n options: MutationHandlerOptions,\n config: CwcApiConfig,\n logger: ILogger | undefined\n ) {\n this.options = options;\n this.config = config;\n this.logger = logger;\n }\n\n 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 this.createOperationDeniedResponse(operationAccess.reason);\n }\n\n // Step 2: Execute the mutation\n const result = await routeConfig.operation(payload, operationContext);\n\n if (!result.success) {\n return this.createOperationErrorResponse(result.errorCode, result.errorMessage);\n }\n\n // Step 3: Format response (JWT added by RequestHandler, always 200 for RPC-style API)\n return {\n statusCode: 200,\n body: {\n success: true,\n data: result.data,\n },\n };\n } catch (error) {\n this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error executing mutation operation for ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(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\n private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {\n return {\n statusCode: 403,\n body: {\n success: false,\n errorCode: 'OPERATION_ACCESS_DENIED',\n errorMessage: 'Operation not permitted',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createOperationErrorResponse(\n errorCode: CwcApiErrorCode,\n errorMessage: string\n ): CwcApiHandlerResponse {\n const statusCode = this.mapErrorCodeToStatus(errorCode);\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage,\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n\n private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {\n switch (errorCode) {\n case 'NOT_FOUND':\n return 404;\n case 'VALIDATION_ERROR':\n case 'INVALID_PARAMETER':\n case 'ALREADY_EXISTS':\n return 400;\n case 'UNAUTHORIZED':\n return 401;\n case 'FORBIDDEN':\n case 'ROUTE_ACCESS_DENIED':\n case 'OPERATION_ACCESS_DENIED':\n return 403;\n case 'INTERNAL_ERROR':\n case 'DATABASE_ERROR':\n case 'AUTH_SERVICE_ERROR':\n return 500;\n default:\n return 500;\n }\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:
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→ CwcApiErrorCode,
11→} from './handler.types';
12→
13→const codeLocation = 'handlers/MutationHandler.ts';
14→
15→/**
16→ * MutationHandler - Handles write operations
17→ *
18→ * Responsibilities:
19→ * 1. Check operation-level access (Phase 4 stub)
20→ * 2. Execute the mutation operation
21→ * 3. Format successful response with data
22→ *
23→ * Note: Session renewal is handled by RequestHandler
24→ */
25→export class MutationHandler implements CwcApiHandler {
26→ private options: MutationHandlerOptions;
27→ private config: CwcApiConfig;
28→ private logger: ILogger | undefined;
29→
30→ constructor(
31→ options: MutationHandlerOptions,
32→ config: CwcApiConfig,
33→ logger: ILogger | undefined
34→ ) {
35→ this.options = options;
36→ this.config = config;
37→ this.logger = logger;
38→ }
39→
40→ public async processRequest(): Promise<CwcApiHandlerResponse> {
41→ const { context, routeConfig, payload, operationContext } = this.options;
42→
43→ try {
44→ // Step 1: Check operation-level access (Phase 4 implementation)
45→ const operationAccess = await this.checkOperationAccess();
46→ if (!operationAccess.allowed) {
47→ return this.createOperationDeniedResponse(operationAccess.reason);
48→ }
49→
50→ // Step 2: Execute the mutation
51→ const result = await routeConfig.operation(payload, operationContext);
52→
53→ if (!result.success) {
54→ return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
55→ }
56→
57→ // Step 3: Format response (JWT added by RequestHandler, always 200 for RPC-style API)
58→ return {
59→ statusCode: 200,
60→ body: {
61→ success: true,
62→ data: result.data,
63→ },
64→ };
65→ } catch (error) {
66→ this.logger?.logError({
67→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
68→ codeLocation,
69→ message: `Error executing mutation operation for ${routeConfig.path}`,
70→ error,
71→ });
72→
73→ return this.createInternalErrorResponse(error);
74→ }
75→ }
76→
77→ /**
78→ * Check if the current context can perform this operation
79→ * Phase 3: Stub - always returns allowed
80→ * Phase 4: Will check ownership, resource access, etc.
81→ */
82→ private async checkOperationAccess(): Promise<OperationAccessResult> {
83→ // Phase 4 will implement actual access policy checks
84→ return { allowed: true };
85→ }
86→
87→ private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
88→ return {
89→ statusCode: 403,
90→ body: {
91→ success: false,
92→ errorCode: 'OPERATION_ACCESS_DENIED',
93→ errorMessage: 'Operation not permitted',
94→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
95→ },
96→ };
97→ }
98→
99→ private createOperationErrorResponse(
100→ errorCode: CwcApiErrorCode,
101→ errorMessage: string
102→ ): CwcApiHandlerResponse {
103→ const statusCode = this.mapErrorCodeToStatus(errorCode);
104→
105→ return {
106→ statusCode,
107→ body: {
108→ success: false,
109→ errorCode,
110→ errorMessage,
111→ },
112→ };
113→ }
114→
115→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
116→ return {
117→ statusCode: 500,
118→ body: {
119→ success: false,
120→ errorCode: 'INTERNAL_ERROR',
121→ errorMessage: 'An internal error occurred',
122→ ...(this.config.isDevelopment && error instanceof Error
123→ ? { errorDetail: error.message }
124→ : {}),
125→ },
126→ };
127→ }
128→
129→ private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
130→ switch (errorCode) {
131→ case 'NOT_FOUND':
132→ return 404;
133→ case 'VALIDATION_ERROR':
134→ case 'INVALID_PARAMETER':
135→ case 'ALREADY_EXISTS':
136→ return 400;
137→ case 'UNAUTHORIZED':
138→ return 401;
139→ case 'FORBIDDEN':
140→ case 'ROUTE_ACCESS_DENIED':
141→ case 'OPERATION_ACCESS_DENIED':
142→ return 403;
143→ case 'INTERNAL_ERROR':
144→ case 'DATABASE_ERROR':
145→ case 'AUTH_SERVICE_ERROR':
146→ return 500;
147→ default:
148→ return 500;
149→ }
150→ }
151→}
152→TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "completed",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "completed",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "in_progress",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "pending",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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/index.ts"
}Tool Result
1→'use strict';
2→
3→// Handler classes
4→export { RouteHandler } from './RouteHandler';
5→export { QueryHandler } from './QueryHandler';
6→export { MutationHandler } from './MutationHandler';
7→
8→// Types
9→export type {
10→ // Error codes
11→ CwcApiErrorCode,
12→ // Status codes
13→ CwcApiSuccessStatusCode,
14→ CwcApiErrorStatusCode,
15→ CwcApiStatusCode,
16→ // Response types
17→ CwcApiPagination,
18→ CwcApiSuccessResponse,
19→ CwcApiErrorResponse,
20→ CwcApiResponse,
21→ CwcApiHandlerResponse,
22→ // Route configuration
23→ CwcApiHandlerType,
24→ CwcApiRouteConfig,
25→ CwcApiRouteConfigs,
26→ // Operation types
27→ OperationContext,
28→ OperationResult,
29→ CwcApiOperation,
30→ // Handler options
31→ BaseHandlerOptions,
32→ RouteHandlerOptions,
33→ QueryHandlerOptions,
34→ MutationHandlerOptions,
35→ // Handler interface
36→ CwcApiHandler,
37→ // Access policy types (Phase 4)
38→ RouteAccessResult,
39→ OperationAccessResult,
40→ RouteAccessPolicy,
41→ OperationAccessPolicy,
42→} from './handler.types';
43→
<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>
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/index.ts",
"content": "'use strict';\n\n// Handler classes\nexport { RequestHandler } from './RequestHandler';\nexport { QueryHandler } from './QueryHandler';\nexport { MutationHandler } from './MutationHandler';\n\n// Types\nexport type {\n // Error codes\n CwcApiErrorCode,\n // Status codes\n CwcApiSuccessStatusCode,\n CwcApiErrorStatusCode,\n CwcApiStatusCode,\n // Response types\n CwcApiPagination,\n CwcApiSuccessResponse,\n CwcApiErrorResponse,\n CwcApiResponse,\n CwcApiHandlerResponse,\n // Route configuration\n CwcApiHandlerType,\n CwcApiRouteConfig,\n CwcApiRouteConfigs,\n // Operation types\n OperationContext,\n OperationResult,\n CwcApiOperation,\n // Handler options\n BaseHandlerOptions,\n RequestHandlerOptions,\n QueryHandlerOptions,\n MutationHandlerOptions,\n // Handler interface\n CwcApiHandler,\n // Access policy types (Phase 4)\n RouteAccessResult,\n OperationAccessResult,\n RouteAccessPolicy,\n OperationAccessPolicy,\n} from './handler.types';\n"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/index.ts has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→'use strict';
2→
3→// Handler classes
4→export { RequestHandler } from './RequestHandler';
5→export { QueryHandler } from './QueryHandler';
6→export { MutationHandler } from './MutationHandler';
7→
8→// Types
9→export type {
10→ // Error codes
11→ CwcApiErrorCode,
12→ // Status codes
13→ CwcApiSuccessStatusCode,
14→ CwcApiErrorStatusCode,
15→ CwcApiStatusCode,
16→ // Response types
17→ CwcApiPagination,
18→ CwcApiSuccessResponse,
19→ CwcApiErrorResponse,
20→ CwcApiResponse,
21→ CwcApiHandlerResponse,
22→ // Route configuration
23→ CwcApiHandlerType,
24→ CwcApiRouteConfig,
25→ CwcApiRouteConfigs,
26→ // Operation types
27→ OperationContext,
28→ OperationResult,
29→ CwcApiOperation,
30→ // Handler options
31→ BaseHandlerOptions,
32→ RequestHandlerOptions,
33→ QueryHandlerOptions,
34→ MutationHandlerOptions,
35→ // Handler interface
36→ CwcApiHandler,
37→ // Access policy types (Phase 4)
38→ RouteAccessResult,
39→ OperationAccessResult,
40→ RouteAccessPolicy,
41→ OperationAccessPolicy,
42→} from './handler.types';
43→Let me run a quick typecheck before updating the tests.
Bash
{
"command": "pnpm --filter cwc-api typecheck 2>&1 | head -40",
"description": "Typecheck cwc-api package",
"timeout": 30000
}Tool Result
> cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > tsc --noEmit src/__tests__/handlers/MutationHandler.test.ts(48,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(72,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(95,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(119,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(145,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(171,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(195,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(219,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(242,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(262,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(287,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(312,83): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(334,84): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(357,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/MutationHandler.test.ts(379,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(47,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(71,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(97,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(121,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(147,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(173,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(197,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(221,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(244,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(264,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(289,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(314,80): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(337,81): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(360,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/QueryHandler.test.ts(381,77): error TS2554: Expected 3 arguments, but got 4. src/__tests__/handlers/RouteHandler.test.ts(4,30): error TS2307: Cannot find module '../../handlers/RouteHandler' or its corresponding type declarations. src/__tests__/handlers/RouteHandler.test.ts(19,15): error TS2724: '"../../handlers/handler.types"' has no exported member named 'RouteHandlerOptions'. Did you mean 'RequestHandlerOptions'? /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api: ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL cwc-api@1.0.0 typecheck: `tsc --noEmit` Exit status 2
Expected errors - the tests need to be updated. Let me update the test files now.
TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "completed",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "completed",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "completed",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "in_progress",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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/RouteHandler.test.ts"
}Tool Result
1→'use strict';
2→
3→import { describe, it, expect, beforeEach, jest } from '@jest/globals';
4→import { RouteHandler } from '../../handlers/RouteHandler';
5→import {
6→ createMockAuthClient,
7→ createMockRouteConfig,
8→ createMockGuestContext,
9→ createMockAuthenticatedContext,
10→ mockOperationSuccess,
11→ mockOperationThrows,
12→ mockRenewSessionSuccess,
13→ getUnitConfig,
14→ createMockDevConfig,
15→ createMockProdConfig,
16→ createMockLogger,
17→} from '../mocks';
18→import type { AuthClient } from 'cwc-backend-utils';
19→import type { RouteHandlerOptions } from '../../handlers/handler.types';
20→
21→describe('RouteHandler', () => {
22→ let mockAuthClient: jest.Mocked<AuthClient>;
23→ const unitConfig = getUnitConfig();
24→ const mockDevConfig = createMockDevConfig();
25→ const mockProdConfig = createMockProdConfig();
26→
27→ beforeEach(() => {
28→ mockAuthClient = createMockAuthClient();
29→ jest.clearAllMocks();
30→ });
31→
32→ describe('Route Access Control', () => {
33→ it('should return 401 for guest user accessing authenticated-only route', async () => {
34→ const routeConfig = createMockRouteConfig({
35→ allowedRoles: ['logged-on-user', 'project-owner'],
36→ });
37→ const context = createMockGuestContext();
38→
39→ const options: RouteHandlerOptions = {
40→ context,
41→ routeConfig,
42→ authHeader: undefined,
43→ payload: {},
44→ };
45→
46→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
47→ const response = await handler.processRequest();
48→
49→ expect(response.statusCode).toBe(401);
50→ expect(response.body.success).toBe(false);
51→ if (!response.body.success) {
52→ expect(response.body.errorCode).toBe('UNAUTHORIZED');
53→ expect(response.body.errorMessage).toBe('Access denied');
54→ }
55→ });
56→
57→ it('should return 403 for authenticated user accessing owner-only route when not allowed', async () => {
58→ const routeConfig = createMockRouteConfig({
59→ allowedRoles: [], // No roles allowed
60→ });
61→ const context = createMockAuthenticatedContext();
62→
63→ const options: RouteHandlerOptions = {
64→ context,
65→ routeConfig,
66→ authHeader: 'Bearer token',
67→ payload: {},
68→ };
69→
70→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
71→ const response = await handler.processRequest();
72→
73→ expect(response.statusCode).toBe(403);
74→ expect(response.body.success).toBe(false);
75→ if (!response.body.success) {
76→ expect(response.body.errorCode).toBe('FORBIDDEN');
77→ }
78→ });
79→
80→ it('should allow guest user to access guest-allowed routes', async () => {
81→ const routeConfig = createMockRouteConfig({
82→ allowedRoles: ['guest-user', 'logged-on-user'],
83→ handlerType: 'query',
84→ });
85→ const context = createMockGuestContext();
86→ mockOperationSuccess(routeConfig, { id: 1 });
87→
88→ const options: RouteHandlerOptions = {
89→ context,
90→ routeConfig,
91→ authHeader: undefined,
92→ payload: {},
93→ };
94→
95→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
96→ const response = await handler.processRequest();
97→
98→ expect(response.statusCode).toBe(200);
99→ expect(response.body.success).toBe(true);
100→ });
101→
102→ it('should allow authenticated user to access logged-on-user routes', async () => {
103→ const routeConfig = createMockRouteConfig({
104→ allowedRoles: ['logged-on-user'],
105→ handlerType: 'query',
106→ });
107→ const context = createMockAuthenticatedContext();
108→ mockOperationSuccess(routeConfig, { id: 1 });
109→ mockRenewSessionSuccess(mockAuthClient);
110→
111→ const options: RouteHandlerOptions = {
112→ context,
113→ routeConfig,
114→ authHeader: 'Bearer token',
115→ payload: {},
116→ };
117→
118→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
119→ const response = await handler.processRequest();
120→
121→ expect(response.statusCode).toBe(200);
122→ expect(response.body.success).toBe(true);
123→ });
124→
125→ it('should allow logged-on-user to access project-owner routes (ownership checked at operation level)', async () => {
126→ const routeConfig = createMockRouteConfig({
127→ allowedRoles: ['project-owner'],
128→ handlerType: 'mutation',
129→ });
130→ const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
131→ mockOperationSuccess(routeConfig, { id: 1 });
132→ mockRenewSessionSuccess(mockAuthClient);
133→
134→ const options: RouteHandlerOptions = {
135→ context,
136→ routeConfig,
137→ authHeader: 'Bearer token',
138→ payload: {},
139→ };
140→
141→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
142→ const response = await handler.processRequest();
143→
144→ // Route access allows logged-on-user for project-owner routes
145→ // Actual ownership check happens at operation level (Phase 4)
146→ expect(response.statusCode).toBe(200);
147→ expect(response.body.success).toBe(true);
148→ });
149→
150→ it('should include error detail in dev mode for access denied', async () => {
151→ const routeConfig = createMockRouteConfig({
152→ path: '/projects/123',
153→ allowedRoles: ['logged-on-user'],
154→ });
155→ const context = createMockGuestContext();
156→
157→ const options: RouteHandlerOptions = {
158→ context,
159→ routeConfig,
160→ authHeader: undefined,
161→ payload: {},
162→ };
163→
164→ const handler = new RouteHandler(options, mockDevConfig, mockAuthClient, undefined);
165→ const response = await handler.processRequest();
166→
167→ expect(response.body.success).toBe(false);
168→ if (!response.body.success) {
169→ expect(response.body.errorDetail).toContain('guest-user');
170→ expect(response.body.errorDetail).toContain('/projects/123');
171→ }
172→ });
173→
174→ it('should not include error detail in prod mode for access denied', async () => {
175→ const routeConfig = createMockRouteConfig({
176→ allowedRoles: ['logged-on-user'],
177→ });
178→ const context = createMockGuestContext();
179→
180→ const options: RouteHandlerOptions = {
181→ context,
182→ routeConfig,
183→ authHeader: undefined,
184→ payload: {},
185→ };
186→
187→ const handler = new RouteHandler(options, mockProdConfig, mockAuthClient, undefined);
188→ const response = await handler.processRequest();
189→
190→ expect(response.body.success).toBe(false);
191→ if (!response.body.success) {
192→ expect(response.body.errorDetail).toBeUndefined();
193→ }
194→ });
195→ });
196→
197→ describe('Handler Delegation', () => {
198→ it('should delegate to QueryHandler for query handlerType', async () => {
199→ const routeConfig = createMockRouteConfig({
200→ handlerType: 'query',
201→ allowedRoles: ['guest-user'],
202→ });
203→ const context = createMockGuestContext();
204→ mockOperationSuccess(routeConfig, { data: 'from query' });
205→
206→ const options: RouteHandlerOptions = {
207→ context,
208→ routeConfig,
209→ authHeader: undefined,
210→ payload: { page: 1 },
211→ };
212→
213→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
214→ const response = await handler.processRequest();
215→
216→ expect(response.statusCode).toBe(200);
217→ expect(response.body.success).toBe(true);
218→ if (response.body.success) {
219→ expect(response.body.data).toEqual({ data: 'from query' });
220→ }
221→ // Query operation was called
222→ expect(routeConfig.operation).toHaveBeenCalledWith(
223→ { page: 1 },
224→ expect.objectContaining({ context })
225→ );
226→ });
227→
228→ it('should delegate to MutationHandler for mutation handlerType', async () => {
229→ const routeConfig = createMockRouteConfig({
230→ handlerType: 'mutation',
231→ allowedRoles: ['logged-on-user'],
232→ });
233→ const context = createMockAuthenticatedContext();
234→ mockOperationSuccess(routeConfig, { id: 123, created: true });
235→ mockRenewSessionSuccess(mockAuthClient);
236→
237→ const options: RouteHandlerOptions = {
238→ context,
239→ routeConfig,
240→ authHeader: 'Bearer token',
241→ payload: { name: 'New Project' },
242→ };
243→
244→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
245→ const response = await handler.processRequest();
246→
247→ expect(response.statusCode).toBe(200);
248→ expect(response.body.success).toBe(true);
249→ if (response.body.success) {
250→ expect(response.body.data).toEqual({ id: 123, created: true });
251→ expect(response.body.jwt).toBeDefined(); // Mutation renews session
252→ }
253→ });
254→ });
255→
256→ describe('Operation Context', () => {
257→ it('should pass projectId from path params to operation context', async () => {
258→ const routeConfig = createMockRouteConfig({
259→ handlerType: 'query',
260→ allowedRoles: ['guest-user'],
261→ });
262→ const context = createMockGuestContext();
263→ mockOperationSuccess(routeConfig, { id: 1 });
264→
265→ const options: RouteHandlerOptions = {
266→ context,
267→ routeConfig,
268→ authHeader: undefined,
269→ payload: {},
270→ pathParams: { projectId: 'my-project-123' },
271→ };
272→
273→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
274→ await handler.processRequest();
275→
276→ expect(routeConfig.operation).toHaveBeenCalledWith(
277→ {},
278→ expect.objectContaining({
279→ projectId: 'my-project-123',
280→ })
281→ );
282→ });
283→
284→ it('should pass resourceId from path params to operation context', async () => {
285→ const routeConfig = createMockRouteConfig({
286→ handlerType: 'query',
287→ allowedRoles: ['guest-user'],
288→ });
289→ const context = createMockGuestContext();
290→ mockOperationSuccess(routeConfig, { id: 1 });
291→
292→ const options: RouteHandlerOptions = {
293→ context,
294→ routeConfig,
295→ authHeader: undefined,
296→ payload: {},
297→ pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },
298→ };
299→
300→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
301→ await handler.processRequest();
302→
303→ expect(routeConfig.operation).toHaveBeenCalledWith(
304→ {},
305→ expect.objectContaining({
306→ projectId: 'proj-1',
307→ resourceId: 'resource-456',
308→ })
309→ );
310→ });
311→
312→ it('should handle missing path params gracefully', async () => {
313→ const routeConfig = createMockRouteConfig({
314→ handlerType: 'query',
315→ allowedRoles: ['guest-user'],
316→ });
317→ const context = createMockGuestContext();
318→ mockOperationSuccess(routeConfig, { id: 1 });
319→
320→ const options: RouteHandlerOptions = {
321→ context,
322→ routeConfig,
323→ authHeader: undefined,
324→ payload: {},
325→ // No pathParams
326→ };
327→
328→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
329→ await handler.processRequest();
330→
331→ expect(routeConfig.operation).toHaveBeenCalledWith(
332→ {},
333→ expect.objectContaining({
334→ projectId: undefined,
335→ resourceId: undefined,
336→ })
337→ );
338→ });
339→ });
340→
341→ describe('Error Handling', () => {
342→ it('should catch errors and return 500 response', async () => {
343→ const routeConfig = createMockRouteConfig({
344→ handlerType: 'query',
345→ allowedRoles: ['guest-user'],
346→ });
347→ const context = createMockGuestContext();
348→ mockOperationThrows(routeConfig, new Error('Unexpected error'));
349→
350→ const options: RouteHandlerOptions = {
351→ context,
352→ routeConfig,
353→ authHeader: undefined,
354→ payload: {},
355→ };
356→
357→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
358→ const response = await handler.processRequest();
359→
360→ expect(response.statusCode).toBe(500);
361→ expect(response.body.success).toBe(false);
362→ if (!response.body.success) {
363→ expect(response.body.errorCode).toBe('INTERNAL_ERROR');
364→ }
365→ });
366→
367→ it('should log errors when logger is provided', async () => {
368→ const routeConfig = createMockRouteConfig({
369→ handlerType: 'query',
370→ allowedRoles: ['guest-user'],
371→ });
372→ const context = createMockGuestContext();
373→ const mockLogger = createMockLogger();
374→ mockOperationThrows(routeConfig, new Error('Test error'));
375→
376→ const options: RouteHandlerOptions = {
377→ context,
378→ routeConfig,
379→ authHeader: undefined,
380→ payload: {},
381→ };
382→
383→ const handler = new RouteHandler(options, unitConfig, mockAuthClient, mockLogger);
384→ await handler.processRequest();
385→
386→ expect(mockLogger.logError).toHaveBeenCalled();
387→ });
388→ });
389→});
390→
<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>
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts",
"content": "'use strict';\n\nimport { describe, it, expect, beforeEach, jest } from '@jest/globals';\nimport { RequestHandler } from '../../handlers/RequestHandler';\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n mockRenewSessionSuccess,\n mockRenewSessionFailure,\n getUnitConfig,\n createMockDevConfig,\n createMockProdConfig,\n createMockLogger,\n} from '../mocks';\nimport type { AuthClient } from 'cwc-backend-utils';\nimport type { RequestHandlerOptions } from '../../handlers/handler.types';\n\ndescribe('RequestHandler', () => {\n let mockAuthClient: jest.Mocked<AuthClient>;\n const unitConfig = getUnitConfig();\n const mockDevConfig = createMockDevConfig();\n const mockProdConfig = createMockProdConfig();\n\n beforeEach(() => {\n mockAuthClient = createMockAuthClient();\n jest.clearAllMocks();\n });\n\n 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 });\n\n 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 });\n\n 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});\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RequestHandler.test.ts
Bash
{
"command": "rm /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts",
"description": "Delete old RouteHandler.test.ts"
}Tool Result
TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "completed",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "completed",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "completed",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "completed",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "in_progress",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "pending",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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__/handlers/QueryHandler.test.ts",
"content": "'use strict';\n\nimport { describe, it, expect, beforeEach, jest } from '@jest/globals';\nimport { QueryHandler } from '../../handlers/QueryHandler';\nimport {\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n getUnitConfig,\n createMockDevConfig,\n createMockProdConfig,\n createMockLogger,\n} from '../mocks';\nimport type { QueryHandlerOptions } from '../../handlers/handler.types';\n\ndescribe('QueryHandler', () => {\n const unitConfig = getUnitConfig();\n const mockProdConfig = createMockProdConfig();\n\n beforeEach(() => {\n jest.clearAllMocks();\n });\n\n describe('Successful Operations', () => {\n it('should return 200 with data on successful query', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationSuccess(routeConfig, { id: 1, name: 'test' });\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 if (response.body.success) {\n expect(response.body.data).toEqual({ id: 1, name: 'test' });\n }\n });\n\n it('should include pagination when operation returns it', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const pagination = { page: 1, pageSize: 20, totalCount: 100, hasMore: true };\n mockOperationSuccess(routeConfig, [{ id: 1 }, { id: 2 }], pagination);\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 if (response.body.success) {\n expect(response.body.pagination).toEqual(pagination);\n }\n });\n\n it('should not include jwt in response (handled by RequestHandler)', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\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 if (response.body.success) {\n expect(response.body.jwt).toBeUndefined();\n }\n });\n });\n\n describe('Error Responses', () => {\n it('should return 404 for NOT_FOUND error code', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');\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(404);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('NOT_FOUND');\n expect(response.body.errorMessage).toBe('Resource not found');\n }\n });\n\n it('should return 400 for VALIDATION_ERROR', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');\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(400);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('VALIDATION_ERROR');\n }\n });\n\n it('should return 403 for OPERATION_ACCESS_DENIED', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationFailure(routeConfig, 'OPERATION_ACCESS_DENIED', 'Not permitted');\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 });\n\n it('should return 500 for DATABASE_ERROR', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');\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(500);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('DATABASE_ERROR');\n }\n });\n });\n\n describe('Exception Handling', () => {\n it('should catch errors and return 500 response', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationThrows(routeConfig, new Error('Unexpected database error'));\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(500);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('INTERNAL_ERROR');\n expect(response.body.errorMessage).toBe('An internal error occurred');\n }\n });\n\n it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n const mockDevConfig = createMockDevConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, mockDevConfig, 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('Detailed error message');\n }\n });\n\n it('should not include error detail in production mode', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockGuestContext();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: undefined,\n payload: {},\n operationContext: { context },\n };\n\n // mockProdConfig has isDevelopment: false\n const handler = new QueryHandler(options, mockProdConfig, 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\n it('should log errors when logger is provided', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockAuthenticatedContext();\n const mockLogger = createMockLogger();\n mockOperationThrows(routeConfig, new Error('Test error'));\n\n const options: QueryHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: {},\n operationContext: { context },\n };\n\n const handler = new QueryHandler(options, unitConfig, mockLogger);\n await handler.processRequest();\n\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n });\n\n 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});\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:
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 isDevelopment 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 isDevelopment: 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→TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "completed",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "completed",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "completed",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "completed",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "completed",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "in_progress",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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__/handlers/MutationHandler.test.ts",
"content": "'use strict';\n\nimport { describe, it, expect, beforeEach, jest } from '@jest/globals';\nimport { MutationHandler } from '../../handlers/MutationHandler';\nimport {\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n getUnitConfig,\n createMockDevConfig,\n createMockProdConfig,\n createMockLogger,\n} from '../mocks';\nimport type { MutationHandlerOptions } from '../../handlers/handler.types';\n\ndescribe('MutationHandler', () => {\n const unitConfig = getUnitConfig();\n const mockProdConfig = createMockProdConfig();\n\n beforeEach(() => {\n jest.clearAllMocks();\n });\n\n describe('Successful Operations', () => {\n it('should return 200 with data on successful mutation', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1, created: true });\n\n const options: MutationHandlerOptions = {\n context,\n routeConfig,\n authHeader: 'Bearer token',\n payload: { name: 'New Item' },\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 if (response.body.success) {\n expect(response.body.data).toEqual({ id: 1, created: true });\n }\n });\n\n it('should return 200 for all operations (RPC-style, no 201)', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 999, status: 'created' });\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 // All POST operations return 200, not 201\n expect(response.statusCode).toBe(200);\n });\n\n it('should not include jwt in response (handled by RequestHandler)', 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 expect(response.statusCode).toBe(200);\n expect(response.body.success).toBe(true);\n if (response.body.success) {\n expect(response.body.jwt).toBeUndefined();\n }\n });\n });\n\n describe('Error Responses', () => {\n it('should return 404 for NOT_FOUND error code', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');\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(404);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('NOT_FOUND');\n expect(response.body.errorMessage).toBe('Resource not found');\n }\n });\n\n it('should return 400 for ALREADY_EXISTS error code', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'ALREADY_EXISTS', 'Resource already exists');\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(400);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('ALREADY_EXISTS');\n }\n });\n\n it('should return 400 for VALIDATION_ERROR', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');\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(400);\n expect(response.body.success).toBe(false);\n });\n\n it('should return 500 for DATABASE_ERROR', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');\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(500);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('DATABASE_ERROR');\n }\n });\n });\n\n describe('Exception Handling', () => {\n it('should catch errors and return 500 response', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationThrows(routeConfig, new Error('Unexpected database error'));\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(500);\n expect(response.body.success).toBe(false);\n if (!response.body.success) {\n expect(response.body.errorCode).toBe('INTERNAL_ERROR');\n expect(response.body.errorMessage).toBe('An internal error occurred');\n }\n });\n\n it('should include error detail when isDevelopment is true', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const mockDevConfig = createMockDevConfig();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, mockDevConfig, 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('Detailed error message');\n }\n });\n\n it('should not include error detail in production mode', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationThrows(routeConfig, new Error('Detailed error message'));\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, mockProdConfig, 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\n it('should log errors when logger is provided', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n const mockLogger = createMockLogger();\n mockOperationThrows(routeConfig, new Error('Test error'));\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, mockLogger);\n await handler.processRequest();\n\n expect(mockLogger.logError).toHaveBeenCalled();\n });\n });\n\n 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});\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:
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→ createMockGuestContext,
8→ createMockAuthenticatedContext,
9→ mockOperationSuccess,
10→ mockOperationFailure,
11→ mockOperationThrows,
12→ getUnitConfig,
13→ createMockDevConfig,
14→ createMockProdConfig,
15→ createMockLogger,
16→} from '../mocks';
17→import type { MutationHandlerOptions } from '../../handlers/handler.types';
18→
19→describe('MutationHandler', () => {
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 mutation', async () => {
29→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
30→ const context = createMockAuthenticatedContext();
31→ mockOperationSuccess(routeConfig, { id: 1, created: true });
32→
33→ const options: MutationHandlerOptions = {
34→ context,
35→ routeConfig,
36→ authHeader: 'Bearer token',
37→ payload: { name: 'New Item' },
38→ operationContext: { context },
39→ };
40→
41→ const handler = new MutationHandler(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, created: true });
48→ }
49→ });
50→
51→ it('should return 200 for all operations (RPC-style, no 201)', async () => {
52→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
53→ const context = createMockAuthenticatedContext();
54→ mockOperationSuccess(routeConfig, { id: 999, status: 'created' });
55→
56→ const options: MutationHandlerOptions = {
57→ context,
58→ routeConfig,
59→ authHeader: 'Bearer token',
60→ payload: {},
61→ operationContext: { context },
62→ };
63→
64→ const handler = new MutationHandler(options, unitConfig, undefined);
65→ const response = await handler.processRequest();
66→
67→ // All POST operations return 200, not 201
68→ expect(response.statusCode).toBe(200);
69→ });
70→
71→ it('should not include jwt in response (handled by RequestHandler)', async () => {
72→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
73→ const context = createMockAuthenticatedContext();
74→ mockOperationSuccess(routeConfig, { id: 1 });
75→
76→ const options: MutationHandlerOptions = {
77→ context,
78→ routeConfig,
79→ authHeader: 'Bearer token',
80→ payload: {},
81→ operationContext: { context },
82→ };
83→
84→ const handler = new MutationHandler(options, unitConfig, undefined);
85→ const response = await handler.processRequest();
86→
87→ expect(response.statusCode).toBe(200);
88→ expect(response.body.success).toBe(true);
89→ if (response.body.success) {
90→ expect(response.body.jwt).toBeUndefined();
91→ }
92→ });
93→ });
94→
95→ describe('Error Responses', () => {
96→ it('should return 404 for NOT_FOUND error code', async () => {
97→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
98→ const context = createMockAuthenticatedContext();
99→ mockOperationFailure(routeConfig, 'NOT_FOUND', 'Resource not found');
100→
101→ const options: MutationHandlerOptions = {
102→ context,
103→ routeConfig,
104→ authHeader: 'Bearer token',
105→ payload: {},
106→ operationContext: { context },
107→ };
108→
109→ const handler = new MutationHandler(options, unitConfig, undefined);
110→ const response = await handler.processRequest();
111→
112→ expect(response.statusCode).toBe(404);
113→ expect(response.body.success).toBe(false);
114→ if (!response.body.success) {
115→ expect(response.body.errorCode).toBe('NOT_FOUND');
116→ expect(response.body.errorMessage).toBe('Resource not found');
117→ }
118→ });
119→
120→ it('should return 400 for ALREADY_EXISTS error code', async () => {
121→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
122→ const context = createMockAuthenticatedContext();
123→ mockOperationFailure(routeConfig, 'ALREADY_EXISTS', 'Resource already exists');
124→
125→ const options: MutationHandlerOptions = {
126→ context,
127→ routeConfig,
128→ authHeader: 'Bearer token',
129→ payload: {},
130→ operationContext: { context },
131→ };
132→
133→ const handler = new MutationHandler(options, unitConfig, undefined);
134→ const response = await handler.processRequest();
135→
136→ expect(response.statusCode).toBe(400);
137→ expect(response.body.success).toBe(false);
138→ if (!response.body.success) {
139→ expect(response.body.errorCode).toBe('ALREADY_EXISTS');
140→ }
141→ });
142→
143→ it('should return 400 for VALIDATION_ERROR', async () => {
144→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
145→ const context = createMockAuthenticatedContext();
146→ mockOperationFailure(routeConfig, 'VALIDATION_ERROR', 'Invalid input');
147→
148→ const options: MutationHandlerOptions = {
149→ context,
150→ routeConfig,
151→ authHeader: 'Bearer token',
152→ payload: {},
153→ operationContext: { context },
154→ };
155→
156→ const handler = new MutationHandler(options, unitConfig, undefined);
157→ const response = await handler.processRequest();
158→
159→ expect(response.statusCode).toBe(400);
160→ expect(response.body.success).toBe(false);
161→ });
162→
163→ it('should return 500 for DATABASE_ERROR', async () => {
164→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
165→ const context = createMockAuthenticatedContext();
166→ mockOperationFailure(routeConfig, 'DATABASE_ERROR', 'Database connection failed');
167→
168→ const options: MutationHandlerOptions = {
169→ context,
170→ routeConfig,
171→ authHeader: 'Bearer token',
172→ payload: {},
173→ operationContext: { context },
174→ };
175→
176→ const handler = new MutationHandler(options, unitConfig, undefined);
177→ const response = await handler.processRequest();
178→
179→ expect(response.statusCode).toBe(500);
180→ expect(response.body.success).toBe(false);
181→ if (!response.body.success) {
182→ expect(response.body.errorCode).toBe('DATABASE_ERROR');
183→ }
184→ });
185→ });
186→
187→ describe('Exception Handling', () => {
188→ it('should catch errors and return 500 response', async () => {
189→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
190→ const context = createMockAuthenticatedContext();
191→ mockOperationThrows(routeConfig, new Error('Unexpected database error'));
192→
193→ const options: MutationHandlerOptions = {
194→ context,
195→ routeConfig,
196→ authHeader: 'Bearer token',
197→ payload: {},
198→ operationContext: { context },
199→ };
200→
201→ const handler = new MutationHandler(options, unitConfig, undefined);
202→ const response = await handler.processRequest();
203→
204→ expect(response.statusCode).toBe(500);
205→ expect(response.body.success).toBe(false);
206→ if (!response.body.success) {
207→ expect(response.body.errorCode).toBe('INTERNAL_ERROR');
208→ expect(response.body.errorMessage).toBe('An internal error occurred');
209→ }
210→ });
211→
212→ it('should include error detail when isDevelopment is true', async () => {
213→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
214→ const context = createMockAuthenticatedContext();
215→ const mockDevConfig = createMockDevConfig();
216→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
217→
218→ const options: MutationHandlerOptions = {
219→ context,
220→ routeConfig,
221→ authHeader: 'Bearer token',
222→ payload: {},
223→ operationContext: { context },
224→ };
225→
226→ const handler = new MutationHandler(options, mockDevConfig, undefined);
227→ const response = await handler.processRequest();
228→
229→ expect(response.body.success).toBe(false);
230→ if (!response.body.success) {
231→ expect(response.body.errorDetail).toBe('Detailed error message');
232→ }
233→ });
234→
235→ it('should not include error detail in production mode', async () => {
236→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
237→ const context = createMockAuthenticatedContext();
238→ mockOperationThrows(routeConfig, new Error('Detailed error message'));
239→
240→ const options: MutationHandlerOptions = {
241→ context,
242→ routeConfig,
243→ authHeader: 'Bearer token',
244→ payload: {},
245→ operationContext: { context },
246→ };
247→
248→ const handler = new MutationHandler(options, mockProdConfig, undefined);
249→ const response = await handler.processRequest();
250→
251→ expect(response.body.success).toBe(false);
252→ if (!response.body.success) {
253→ expect(response.body.errorDetail).toBeUndefined();
254→ }
255→ });
256→
257→ it('should log errors when logger is provided', async () => {
258→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
259→ const context = createMockAuthenticatedContext();
260→ const mockLogger = createMockLogger();
261→ mockOperationThrows(routeConfig, new Error('Test error'));
262→
263→ const options: MutationHandlerOptions = {
264→ context,
265→ routeConfig,
266→ authHeader: 'Bearer token',
267→ payload: {},
268→ operationContext: { context },
269→ };
270→
271→ const handler = new MutationHandler(options, unitConfig, mockLogger);
272→ await handler.processRequest();
273→
274→ expect(mockLogger.logError).toHaveBeenCalled();
275→ });
276→ });
277→
278→ describe('Operation Access (Phase 3 Stub)', () => {
279→ it('should allow operation access (stub always returns allowed)', async () => {
280→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
281→ const context = createMockAuthenticatedContext();
282→ mockOperationSuccess(routeConfig, { id: 1 });
283→
284→ const options: MutationHandlerOptions = {
285→ context,
286→ routeConfig,
287→ authHeader: 'Bearer token',
288→ payload: {},
289→ operationContext: { context },
290→ };
291→
292→ const handler = new MutationHandler(options, unitConfig, undefined);
293→ const response = await handler.processRequest();
294→
295→ // Operation access is stubbed to always allow in Phase 3
296→ expect(response.statusCode).toBe(200);
297→ expect(response.body.success).toBe(true);
298→ });
299→ });
300→});
301→I'll continue with the handler refactoring. Let me run the tests to verify all changes compile and pass, then update the CLAUDE.md documentation.
TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "completed",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "completed",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "completed",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "completed",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "completed",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "completed",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Run tests to verify changes",
"status": "in_progress",
"activeForm": "Running tests to verify changes"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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 -- --testPathPattern=\"Handler\"",
"description": "Run handler tests to verify changes",
"timeout": 60000
}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=Handler" /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=Handler"` Exit status 1
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"Handler\"",
"description": "Run handler tests with correct option",
"timeout": 60000
}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 "--testPathPatterns=Handler"
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: 🛠️ 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__/handlers/QueryHandler.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/MutationHandler.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: 3 passed, 3 total
Tests: 46 passed, 46 total
Snapshots: 0 total
Time: 0.406 s, estimated 1 s
Ran all test suites matching Handler.All 46 handler tests pass. Let me also run the full test suite to ensure nothing is broken.
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test",
"description": "Run full cwc-api test suite",
"timeout": 60000
}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__/handlers/RequestHandler.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__/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)
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 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/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__/config.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__/healthCheck.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: 6 passed, 6 total
Tests: 84 passed, 84 total
Snapshots: 0 total
Time: 0.398 s, estimated 1 s
Ran all test suites.All 84 tests pass. Let me also run typecheck to ensure everything compiles.
Bash
{
"command": "pnpm --filter cwc-api typecheck",
"description": "Type check cwc-api package",
"timeout": 60000
}Tool Result
Exit code 2 > cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > tsc --noEmit src/__tests__/handlers/MutationHandler.test.ts(7,3): error TS6133: 'createMockGuestContext' is declared but its value is never read. /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api: ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL cwc-api@1.0.0 typecheck: `tsc --noEmit` Exit status 2
There's an unused import. Let me fix that.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts",
"old_string": "import {\n createMockRouteConfig,\n createMockGuestContext,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n getUnitConfig,\n createMockDevConfig,\n createMockProdConfig,\n createMockLogger,\n} from '../mocks';",
"new_string": "import {\n createMockRouteConfig,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockOperationFailure,\n mockOperationThrows,\n getUnitConfig,\n createMockDevConfig,\n createMockProdConfig,\n createMockLogger,\n} from '../mocks';"
}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:
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();Bash
{
"command": "pnpm --filter cwc-api typecheck",
"description": "Type check cwc-api package",
"timeout": 60000
}Tool Result
> cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > tsc --noEmit
Typecheck passes. Now let me update the CLAUDE.md with the new handler architecture.
TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "completed",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "completed",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "completed",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "completed",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "completed",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "completed",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Run tests to verify changes",
"status": "completed",
"activeForm": "Running tests to verify changes"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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 → RouteHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
18→```
19→
20→- **RouteHandler**: Auth → context creation → route access check → delegation
21→- **QueryHandler**: Operation access check → execute query → format response
22→- **MutationHandler**: Operation access check → execute mutation → session renewal → 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. RouteHandler 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. Response returned (with new JWT if session renewed)
34→
35→## Context Layer - Centralized Authentication
36→
37→**Location:** `src/context/`
38→
39→**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.
40→
41→**Trade-off:** Additional HTTP call per request, but auth logic stays centralized in cwc-auth (single source of truth).
42→
43→**Files:**
44→- `context.types.ts` - AuthenticatedContext, GuestContext, RequestContext types
45→- `createContext.ts` - Creates context from Authorization header
46→- `index.ts` - Exports
47→
48→**Context Types:**
49→
50→```typescript
51→// Authenticated user (valid JWT)
52→type AuthenticatedContext = {
53→ isAuthenticated: true;
54→ role: 'project-owner' | 'logged-on-user'; // Determined per-operation
55→ userPkId: number;
56→ username: string;
57→ ownedProjects: string[];
58→ payload: UserJwtPayload;
59→};
60→
61→// Unauthenticated request (no/invalid JWT)
62→type GuestContext = {
63→ isAuthenticated: false;
64→ role: 'guest-user';
65→};
66→
67→type RequestContext = AuthenticatedContext | GuestContext;
68→```
69→
70→**Usage:**
71→```typescript
72→import { createContext } from './context';
73→import { AuthClient } from 'cwc-backend-utils';
74→
75→const context = await createContext({
76→ authHeader: req.headers.authorization,
77→ authClient: authClient,
78→});
79→
80→if (context.isAuthenticated) {
81→ // TypeScript narrows to AuthenticatedContext
82→ console.log(context.userPkId, context.ownedProjects);
83→} else {
84→ // TypeScript narrows to GuestContext
85→ console.log(context.role); // 'guest-user'
86→}
87→```
88→
89→**Graceful Degradation:**
90→- Missing token → guest context
91→- Invalid token → guest context
92→- Expired token → guest context
93→- Auth service error → guest context (logged)
94→- No errors thrown; auth failures result in guest access
95→
96→## Handler Architecture
97→
98→**Location:** `src/handlers/`
99→
100→**Files:**
101→- `handler.types.ts` - Type definitions for handlers, operations, and responses
102→- `RouteHandler.ts` - Entry point: route access check, delegates to Query/Mutation
103→- `QueryHandler.ts` - Read operations with pagination support
104→- `MutationHandler.ts` - Write operations with session renewal
105→- `index.ts` - Exports
106→
107→**Handler Flow:**
108→
109→```
110→RouteHandler.processRequest()
111→├── checkRouteAccess() → 401/403 if denied
112→├── buildOperationContext() → projectId, resourceId from path
113→└── delegate to QueryHandler or MutationHandler
114→ ├── checkOperationAccess() → (stub for Phase 4)
115→ ├── execute operation()
116→ ├── renewSession() → always for authenticated users
117→ └── format response with new JWT
118→```
119→
120→**Session Renewal Strategy:**
121→
122→- Always renew session on every request (keeps sessions active)
123→- Both QueryHandler and MutationHandler call `AuthClient.renewSession()`
124→- New JWT returned in response body (`response.body.jwt`)
125→- Renewal failure doesn't fail the request (graceful degradation)
126→
127→**Error Code Mapping:**
128→
129→| Error Code | HTTP Status |
130→|------------|-------------|
131→| `UNAUTHORIZED` | 401 |
132→| `FORBIDDEN` | 403 |
133→| `NOT_FOUND` | 404 |
134→| `ALREADY_EXISTS` | 400 |
135→| `VALIDATION_ERROR` | 400 |
136→| `INVALID_PARAMETER` | 400 |
137→| `DATABASE_ERROR` | 500 |
138→| `INTERNAL_ERROR` | 500 |
139→
140→## Role-Based Access Control
141→
142→**Roles:**
143→
144→| Role | Description |
145→|------|-------------|
146→| `project-owner` | User owns the project being accessed |
147→| `logged-on-user` | Authenticated user (may not own the project) |
148→| `guest-user` | Unauthenticated request (read-only access) |
149→
150→**Access Policy Levels:**
151→
152→- **Route-level**: Can this role call this endpoint at all?
153→- **Operation-level**: Can this role execute this specific operation? (Phase 4)
154→- **Context-aware**: Does the user have the right relationship to the data? (e.g., ownership)
155→
156→## Tables Handled
157→
158→**Content Tables (Full CRUD + Soft Delete):**
159→
160→- project
161→- codingSession
162→- codingSessionContent
163→- codingSessionAttachment
164→- comment
165→- reaction
166→- contentReport
167→- abuseReport
168→
169→**Read-Only Tables:**
170→
171→- featureFlag (all fields)
172→- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
173→
174→## SqlFunction Naming Convention
175→
176→Each table has a subdirectory in `src/sql/` with these functions:
177→
178→| Function | Purpose |
179→|----------|---------|
180→| `select{Table}.ts` | Retrieve single record by identifier |
181→| `list{Table}s.ts` | Retrieve multiple records with filtering |
182→| `insert{Table}.ts` | Create new record |
183→| `update{Table}.ts` | Modify existing record |
184→| `delete{Table}.ts` | Hard delete (GDPR compliance) |
185→| `softDelete{Table}.ts` | Soft delete (set enabled=false) |
186→
187→**Key Principles:**
188→
189→- Single responsibility: Each SqlFunction does one thing
190→- No business logic: SqlFunctions handle data access only
191→- Soft-delete awareness: List queries automatically filter `enabled=true`
192→- Composability: Queries/Mutations compose multiple SqlFunctions
193→
194→## cwc-auth Integration
195→
196→**Division of Responsibilities:**
197→
198→| Concern | Owner |
199→|---------|-------|
200→| "Is this token valid?" | cwc-auth |
201→| "Who is this user?" | cwc-auth (via token claims) |
202→| "Can this user access this endpoint?" | cwc-api |
203→| "Is this user the owner of this project?" | cwc-api |
204→
205→**Session Renewal Flow:**
206→
207→When cwc-api creates/deletes a project:
208→1. cwc-api performs the database operation
209→2. cwc-api calls cwc-auth `/auth/v1/renew-session`
210→3. cwc-auth issues new JWT with updated `ownedProjects`
211→4. cwc-api returns new JWT to client
212→
213→## Response Pattern
214→
215→**Standard Response Envelope:**
216→
217→```typescript
218→// Success
219→{
220→ success: true,
221→ data: { ... },
222→ // Optional: pagination metadata
223→}
224→
225→// Error
226→{
227→ success: false,
228→ errorCode: 'ERROR_CODE',
229→ errorMessage: 'Human-readable message',
230→ // Dev-only: errorDetail
231→}
232→```
233→
234→**Pagination (Offset-based):**
235→
236→```typescript
237→{
238→ success: true,
239→ data: [...],
240→ pagination: {
241→ page: 1,
242→ pageSize: 20,
243→ totalCount: 100,
244→ hasMore: true
245→ }
246→}
247→```
248→
249→## Configuration
250→
251→**Required Environment Variables:**
252→
253→```bash
254→RUNTIME_ENVIRONMENT=dev
255→SERVICE_PORT=5040
256→DATA_URI=http://localhost:5020/data/v1
257→AUTH_URI=http://localhost:5005/auth/v1
258→APP_URL=http://localhost:3000
259→CORS_ORIGIN=*
260→DEV_CORS_ORIGIN=http://localhost:3000
261→RATE_LIMITER_POINTS=15
262→RATE_LIMITER_DURATION=1
263→DEBUG_MODE=OFF
264→LOG_ERRORS_TO_DATABASE=ON
265→USER_JWT_SECRET=...
266→SQL_CLIENT_API_KEY=...
267→```
268→
269→## API Versioning
270→
271→**URL-based versioning:**
272→
273→- `/api/v1/*` routes to CwcApiV1 handlers
274→- Each version lives in `src/apis/CwcApiV{N}/`
275→- Route definitions in `routes.ts`, organized alphabetically
276→
277→## Critical Rules
278→
279→**SqlClient Usage:**
280→
281→- Never provide `createdDate` or `modifiedDate` - handled by cwc-sql automatically
282→- Always use SqlFunctions, never construct queries directly in business logic
283→
284→**Guest User Handling:**
285→
286→- No JWT required - RouteHandler creates context with `role: 'guest-user'`
287→- Guest users have read-only access to public content
288→
289→**Soft Delete:**
290→
291→- All content tables support soft delete (`enabled=false`)
292→- Hard delete available for GDPR compliance
293→- List queries automatically filter `enabled=true`
294→
295→## Implementation Status
296→
297→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
298→
299→The checklist tracks 7 phases:
300→1. Package Foundation (config, Express service, health check) - **Complete**
301→2. Authentication & Context Layer (JWT, request context) - **Complete**
302→3. Handler Architecture (RouteHandler, QueryHandler, MutationHandler) - **Complete**
303→4. Access Policy System (route and operation policies)
304→5. SqlFunction Layer (CRUD operations for all tables)
305→6. CwcApiV1 Routes & Business Logic
306→7. Integration Testing & Final Review
307→
308→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
309→
310→## Unit Testing
311→
312→**Test Infrastructure:**
313→
314→- Jest 30.x with ts-jest for TypeScript ESM support
315→- Test files: `src/__tests__/**/*.test.ts`
316→- Setup: `src/__tests__/setup.ts` loads unit test environment via `loadDotEnv`
317→- Run tests: `pnpm api test` or `NODE_ENV=unit pnpm --filter cwc-api test`
318→
319→**Mock Infrastructure (`src/__tests__/mocks/`):**
320→
321→| Mock | Purpose |
322→|------|---------|
323→| **SqlClient Mocks** | |
324→| `createMockSqlClient()` | Full SqlClient mock with jest.fn() for all methods |
325→| `mockSelectSuccess/NotFound` | Configure SqlClient for select queries |
326→| `mockListSuccess` | Configure SqlClient for list queries |
327→| `mockInsertSuccess/Failure` | Configure SqlClient for inserts |
328→| `mockUpdateSuccess/NotFound` | Configure SqlClient for updates |
329→| `mockDeleteSuccess/NotFound` | Configure SqlClient for deletes |
330→| **AuthClient Mocks** | |
331→| `createMockAuthClient()` | Full AuthClient mock (verifyToken, renewSession) |
332→| `createMockUserJwtPayload(overrides)` | Create UserJwtPayload with custom overrides |
333→| `mockVerifyTokenSuccess(client, payload?)` | Configure AuthClient to return success |
334→| `mockVerifyTokenFailure(client, error?)` | Configure AuthClient to return failure |
335→| `mockRenewSessionSuccess(client, jwt?)` | Configure renewSession to return new JWT |
336→| `mockRenewSessionFailure(client, error?)` | Configure renewSession to return failure |
337→| **Route/Handler Mocks** | |
338→| `createMockRouteConfig(overrides)` | Create route config for testing |
339→| `createMockOperationContext(overrides)` | Create operation context for testing |
340→| `createMockGuestContext()` | Create guest (unauthenticated) context |
341→| `createMockAuthenticatedContext(overrides)` | Create authenticated user context |
342→| `mockOperationSuccess(routeConfig, data)` | Configure operation to succeed |
343→| `mockOperationFailure(routeConfig, code, msg)` | Configure operation to fail |
344→| `mockOperationThrows(routeConfig, error)` | Configure operation to throw |
345→| **Config Mocks** | |
346→| `getUnitConfig()` | Load real config from unit.cwc-api.env |
347→| `createMockConfig(overrides)` | Create config with custom overrides |
348→| `createMockDevConfig(overrides)` | Mock config with isDevelopment: true |
349→| `createMockProdConfig(overrides)` | Mock config with isProduction: true |
350→| `createMockUnitConfig(overrides)` | Mock config with isUnitTest: true |
351→| **Logger Mock** | |
352→| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |
353→
354→**Testing Pattern:**
355→
356→```typescript
357→import {
358→ createMockAuthClient,
359→ createMockRouteConfig,
360→ createMockAuthenticatedContext,
361→ mockOperationSuccess,
362→ mockRenewSessionSuccess,
363→ getUnitConfig,
364→ createMockProdConfig,
365→} from '../mocks';
366→
367→describe('MyHandler', () => {
368→ const unitConfig = getUnitConfig(); // Real config from unit.cwc-api.env
369→ const mockProdConfig = createMockProdConfig(); // Mock config for prod behavior
370→ const mockAuthClient = createMockAuthClient();
371→
372→ it('should handle operation success', async () => {
373→ const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
374→ const context = createMockAuthenticatedContext();
375→ mockOperationSuccess(routeConfig, { id: 1 });
376→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
377→ // ... test logic using unitConfig or mockProdConfig
378→ });
379→});
380→```
381→
382→## Related Packages
383→
384→**Depends On:**
385→
386→- `cwc-backend-utils` (workspace) - SqlClient, Logger, Express service factory
387→- `cwc-schema` (workspace) - Data validation
388→- `cwc-types` (workspace) - Entity types, RuntimeEnvironment
389→
390→**Coordinates With:**
391→
392→- `cwc-auth` - JWT validation, session renewal
393→- `cwc-sql` - Database operations
394→
395→**Consumed By:**
396→
397→- `cwc-website` - Frontend web application
398→
<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>
Now let me update the CLAUDE.md file with the new handler architecture.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Layered Architecture:**\n\n```\nRequest → RouteHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql\n```\n\n- **RouteHandler**: Auth → context creation → route access check → delegation\n- **QueryHandler**: Operation access check → execute query → format response\n- **MutationHandler**: Operation access check → execute mutation → session renewal → format response\n- **SqlFunctions**: Atomic database operations (one function per table operation)",
"new_string": "**Layered Architecture:**\n\n```\nRequest → RequestHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql\n```\n\n- **RequestHandler**: Auth → context creation → route access check → delegation → session renewal\n- **QueryHandler**: Operation access check → execute query → format response\n- **MutationHandler**: Operation access check → execute mutation → format response\n- **SqlFunctions**: Atomic database operations (one function per table operation)"
}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:
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 requestsEdit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "## Request Pipeline\n\n1. Express routes receive incoming HTTP requests\n2. RouteHandler extracts JWT from Authorization header\n3. JWT validated → authenticated context; No JWT → guest context\n4. Route-level access policy checked\n5. QueryHandler or MutationHandler executes business logic\n6. SqlFunctions handle database operations through SqlClient\n7. Response returned (with new JWT if session renewed)",
"new_string": "## Request Pipeline\n\n1. Express routes receive incoming HTTP requests\n2. RequestHandler extracts JWT from Authorization header\n3. JWT validated → authenticated context; No JWT → guest context\n4. Route-level access policy checked\n5. QueryHandler or MutationHandler executes business logic\n6. SqlFunctions handle database operations through SqlClient\n7. RequestHandler renews session (if authenticated and not auth error)\n8. Response returned (with new JWT if session renewed)"
}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:
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/`Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "## Handler Architecture\n\n**Location:** `src/handlers/`\n\n**Files:**\n- `handler.types.ts` - Type definitions for handlers, operations, and responses\n- `RouteHandler.ts` - Entry point: route access check, delegates to Query/Mutation\n- `QueryHandler.ts` - Read operations with pagination support\n- `MutationHandler.ts` - Write operations with session renewal\n- `index.ts` - Exports\n\n**Handler Flow:**\n\n```\nRouteHandler.processRequest()\n├── checkRouteAccess() → 401/403 if denied\n├── buildOperationContext() → projectId, resourceId from path\n└── delegate to QueryHandler or MutationHandler\n ├── checkOperationAccess() → (stub for Phase 4)\n ├── execute operation()\n ├── renewSession() → always for authenticated users\n └── format response with new JWT\n```\n\n**Session Renewal Strategy:**\n\n- Always renew session on every request (keeps sessions active)\n- Both QueryHandler and MutationHandler call `AuthClient.renewSession()`\n- New JWT returned in response body (`response.body.jwt`)\n- Renewal failure doesn't fail the request (graceful degradation)",
"new_string": "## Handler Architecture\n\n**Location:** `src/handlers/`\n\n**Files:**\n- `handler.types.ts` - Type definitions for handlers, operations, and responses\n- `RequestHandler.ts` - Entry point: route access check, delegation, session renewal\n- `QueryHandler.ts` - Read operations with pagination support\n- `MutationHandler.ts` - Write operations\n- `index.ts` - Exports\n\n**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```\n\n**Session Renewal Strategy:**\n\n- Centralized in RequestHandler (not Query/MutationHandler)\n- Renews on ALL requests except auth errors (401, 403)\n- Even failed operations (404, 500) trigger renewal to keep session active\n- New JWT merged into response body (`response.body.jwt`) for success responses\n- Renewal failure doesn't fail the request (graceful degradation, logged)\n- Guest users never trigger renewal"
}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:
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→- `index.ts` - Exports
107→
108→**Handler Flow:**
109→
110→```
111→RequestHandler.processRequest()
112→├── checkRouteAccess() → 401/403 if denied (NO renewal)
113→├── buildOperationContext() → projectId, resourceId from path
114→├── delegate to QueryHandler or MutationHandler
115→│ ├── checkOperationAccess() → (stub for Phase 4)
116→│ ├── execute operation()
117→│ └── format response (no JWT)
118→├── if authenticated AND not auth error (401/403):
119→│ └── renewSession() → merge JWT into response
120→└── return final response
121→```
122→
123→**Session Renewal Strategy:**
124→
125→- Centralized in RequestHandler (not Query/MutationHandler)
126→- Renews on ALL requests except auth errors (401, 403)
127→- Even failed operations (404, 500) trigger renewal to keep session active
128→- New JWT merged into response body (`response.body.jwt`) for success responses
129→- Renewal failure doesn't fail the request (graceful degradation, logged)
130→- Guest users never trigger renewal
131→
132→**Error Code Mapping:**
133→
134→| Error Code | HTTP Status |Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Guest User Handling:**\n\n- No JWT required - RouteHandler creates context with `role: 'guest-user'`\n- Guest users have read-only access to public content",
"new_string": "**Guest User Handling:**\n\n- No JWT required - RequestHandler creates context with `role: 'guest-user'`\n- Guest users have read-only access to public content"
}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: 285→ 286→- Never provide `createdDate` or `modifiedDate` - handled by cwc-sql automatically 287→- Always use SqlFunctions, never construct queries directly in business logic 288→ 289→**Guest User Handling:** 290→ 291→- No JWT required - RequestHandler creates context with `role: 'guest-user'` 292→- Guest users have read-only access to public content 293→ 294→**Soft Delete:** 295→ 296→- All content tables support soft delete (`enabled=false`)
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "The checklist tracks 7 phases:\n1. Package Foundation (config, Express service, health check) - **Complete**\n2. Authentication & Context Layer (JWT, request context) - **Complete**\n3. Handler Architecture (RouteHandler, 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",
"new_string": "The 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"
}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: 300→## Implementation Status 301→ 302→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`. 303→ 304→The checklist tracks 7 phases: 305→1. Package Foundation (config, Express service, health check) - **Complete** 306→2. Authentication & Context Layer (JWT, request context) - **Complete** 307→3. Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - **Complete** 308→4. Access Policy System (route and operation policies) 309→5. SqlFunction Layer (CRUD operations for all tables) 310→6. CwcApiV1 Routes & Business Logic 311→7. Integration Testing & Final Review 312→ 313→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates 314→ 315→## Unit Testing
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Testing Pattern:**\n\n```typescript\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockRenewSessionSuccess,\n getUnitConfig,\n createMockProdConfig,\n} from '../mocks';\n\ndescribe('MyHandler', () => {\n const unitConfig = getUnitConfig(); // Real config from unit.cwc-api.env\n const mockProdConfig = createMockProdConfig(); // Mock config for prod behavior\n const mockAuthClient = createMockAuthClient();\n\n it('should handle operation success', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt');\n // ... test logic using unitConfig or mockProdConfig\n });\n});\n```",
"new_string": "**Testing Patterns:**\n\nRequestHandler tests (includes session renewal):\n```typescript\nimport {\n createMockAuthClient,\n createMockRouteConfig,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n mockRenewSessionSuccess,\n getUnitConfig,\n} from '../mocks';\n\ndescribe('RequestHandler', () => {\n const unitConfig = getUnitConfig();\n const mockAuthClient = createMockAuthClient();\n\n it('should renew session on success', async () => {\n const routeConfig = createMockRouteConfig({ handlerType: 'query' });\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n mockRenewSessionSuccess(mockAuthClient, 'new-jwt');\n // RequestHandler uses 4 args: options, config, authClient, logger\n const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);\n // ...\n });\n});\n```\n\nQueryHandler/MutationHandler tests (no authClient, no renewal):\n```typescript\nimport {\n createMockRouteConfig,\n createMockAuthenticatedContext,\n mockOperationSuccess,\n getUnitConfig,\n} from '../mocks';\n\ndescribe('QueryHandler', () => {\n const unitConfig = getUnitConfig();\n\n it('should execute operation', async () => {\n const routeConfig = createMockRouteConfig();\n const context = createMockAuthenticatedContext();\n mockOperationSuccess(routeConfig, { id: 1 });\n // QueryHandler uses 3 args: options, config, logger\n const handler = new QueryHandler(options, unitConfig, undefined);\n // ...\n });\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:
355→| `createMockUnitConfig(overrides)` | Mock config with isUnitTest: true |
356→| **Logger Mock** | |
357→| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |
358→
359→**Testing Patterns:**
360→
361→RequestHandler tests (includes session renewal):
362→```typescript
363→import {
364→ createMockAuthClient,
365→ createMockRouteConfig,
366→ createMockAuthenticatedContext,
367→ mockOperationSuccess,
368→ mockRenewSessionSuccess,
369→ getUnitConfig,
370→} from '../mocks';
371→
372→describe('RequestHandler', () => {
373→ const unitConfig = getUnitConfig();
374→ const mockAuthClient = createMockAuthClient();
375→
376→ it('should renew session on success', async () => {
377→ const routeConfig = createMockRouteConfig({ handlerType: 'query' });
378→ const context = createMockAuthenticatedContext();
379→ mockOperationSuccess(routeConfig, { id: 1 });
380→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
381→ // RequestHandler uses 4 args: options, config, authClient, logger
382→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
383→ // ...
384→ });
385→});
386→```
387→
388→QueryHandler/MutationHandler tests (no authClient, no renewal):
389→```typescript
390→import {
391→ createMockRouteConfig,
392→ createMockAuthenticatedContext,
393→ mockOperationSuccess,
394→ getUnitConfig,
395→} from '../mocks';
396→
397→describe('QueryHandler', () => {
398→ const unitConfig = getUnitConfig();
399→
400→ it('should execute operation', async () => {
401→ const routeConfig = createMockRouteConfig();
402→ const context = createMockAuthenticatedContext();
403→ mockOperationSuccess(routeConfig, { id: 1 });
404→ // QueryHandler uses 3 args: options, config, logger
405→ const handler = new QueryHandler(options, unitConfig, undefined);
406→ // ...
407→ });
408→});
409→```
410→
411→## Related Packages
412→
413→**Depends On:**TodoWrite
{
"todos": [
{
"content": "Rename RouteHandlerOptions to RequestHandlerOptions in handler.types.ts",
"status": "completed",
"activeForm": "Renaming RouteHandlerOptions to RequestHandlerOptions"
},
{
"content": "Rename RouteHandler.ts to RequestHandler.ts and update class name",
"status": "completed",
"activeForm": "Renaming RouteHandler to RequestHandler"
},
{
"content": "Add session renewal logic to RequestHandler",
"status": "completed",
"activeForm": "Adding session renewal to RequestHandler"
},
{
"content": "Remove session renewal from QueryHandler",
"status": "completed",
"activeForm": "Removing session renewal from QueryHandler"
},
{
"content": "Remove session renewal from MutationHandler",
"status": "completed",
"activeForm": "Removing session renewal from MutationHandler"
},
{
"content": "Update handlers/index.ts exports",
"status": "completed",
"activeForm": "Updating handlers/index.ts exports"
},
{
"content": "Rename and update RequestHandler.test.ts",
"status": "completed",
"activeForm": "Updating RequestHandler tests"
},
{
"content": "Update QueryHandler.test.ts (remove renewal tests)",
"status": "completed",
"activeForm": "Updating QueryHandler tests"
},
{
"content": "Update MutationHandler.test.ts (remove renewal tests)",
"status": "completed",
"activeForm": "Updating MutationHandler tests"
},
{
"content": "Run tests to verify changes",
"status": "completed",
"activeForm": "Running tests to verify changes"
},
{
"content": "Update CLAUDE.md with new handler architecture",
"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
The handler refactoring is complete. Here's a summary of all changes:
Handler Architecture Refactoring Complete
Renamed: RouteHandler → RequestHandler
Centralized Session Renewal:
- Session renewal moved from Query/MutationHandler to RequestHandler
- Renews on ALL requests (success or failure) except auth errors (401/403)
- Keeps sessions active as long as users are making requests
Files Changed:
| File | Change |
|---|---|
handler.types.ts |
Renamed RouteHandlerOptions → RequestHandlerOptions |
RequestHandler.ts |
New file (replaced RouteHandler.ts) with session renewal logic |
QueryHandler.ts |
Removed authClient, removed renewal logic (3-arg constructor) |
MutationHandler.ts |
Removed authClient, removed renewal logic (3-arg constructor) |
handlers/index.ts |
Updated exports |
RequestHandler.test.ts |
New file with session renewal tests |
QueryHandler.test.ts |
Removed renewal tests, updated to 3-arg constructor |
MutationHandler.test.ts |
Removed renewal tests, updated to 3-arg constructor |
CLAUDE.md |
Updated handler architecture documentation |
Test Results:
- All 84 tests pass
- Typecheck passes
New Session Renewal Behavior:
RequestHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied (NO renewal)
├── delegate to QueryHandler or MutationHandler
├── if authenticated AND not auth error (401/403):
│ └── renewSession() → merge JWT into response
└── return final response
Thanks for doing that! in the handlers, I see functions that appear to be duplicates. If there is nothing specifically tied to the type of handler, it might be better to locate the code in a shared code file.createOperationDeniedResponse, createOperationErrorResponse, createInternalErrorResponse, mapErrorCodeToStatus
You're right - let me explore the handlers to understand the duplication and plan for consolidation.
Read
{
"file_path": "/Users/jeffbazinet/.claude/plans/steady-foraging-glade.md"
}Tool Result
1→# Handler Refactoring: RequestHandler + Centralized Session Renewal
2→
3→## Overview
4→
5→Refactor the handler architecture to:
6→1. Rename `RouteHandler` → `RequestHandler`
7→2. Move session renewal from Query/MutationHandler to RequestHandler
8→3. Renew session on ALL requests (success or failure) except authentication errors (401/403)
9→
10→## Rationale
11→
12→- **Single responsibility**: RequestHandler handles infrastructure concerns (auth, access, session)
13→- **DRY**: Remove duplicate renewal logic from Query/MutationHandler
14→- **Consistent behavior**: Session renewed for all user activity, not just successful operations
15→- **Better naming**: "Request" better describes processing the full HTTP request lifecycle
16→
17→---
18→
19→## Implementation Plan
20→
21→### 1. Rename RouteHandler → RequestHandler
22→
23→**Files:**
24→- `src/handlers/RouteHandler.ts` → `src/handlers/RequestHandler.ts`
25→- `src/__tests__/handlers/RouteHandler.test.ts` → `src/__tests__/handlers/RequestHandler.test.ts`
26→
27→**Changes:**
28→- Rename file
29→- Rename class `RouteHandler` → `RequestHandler`
30→- Update `codeLocation` constant
31→
32→### 2. Update handler.types.ts
33→
34→**File:** `src/handlers/handler.types.ts`
35→
36→- Rename `RouteHandlerOptions` → `RequestHandlerOptions`
37→
38→### 3. Refactor RequestHandler (formerly RouteHandler)
39→
40→**File:** `src/handlers/RequestHandler.ts`
41→
42→**New flow:**
43→```
44→processRequest()
45→├── checkRouteAccess() → 401/403 if denied (NO renewal)
46→├── buildOperationContext()
47→├── delegate to QueryHandler or MutationHandler
48→│ └── returns CwcApiHandlerResponse (without JWT)
49→├── if response is NOT 401/403 AND user is authenticated:
50→│ └── renewSession() → merge JWT into response
51→└── return final response
52→```
53→
54→**Key logic:**
55→```typescript
56→// After delegation
57→const response = await delegateToHandler();
58→
59→// Skip renewal for auth errors (401, 403) or guest users
60→const isAuthError = response.statusCode === 401 || response.statusCode === 403;
61→if (context.isAuthenticated && !isAuthError) {
62→ const renewResult = await this.authClient.renewSession(authHeader);
63→ if (renewResult.success && response.body.success) {
64→ response.body.jwt = renewResult.jwt;
65→ }
66→ // Log warning if renewal fails (don't fail the request)
67→}
68→
69→return response;
70→```
71→
72→### 4. Simplify QueryHandler
73→
74→**File:** `src/handlers/QueryHandler.ts`
75→
76→**Remove:**
77→- `authClient` from constructor
78→- `authHeader` from options destructuring
79→- Session renewal logic (Step 3)
80→- JWT merging in response
81→
82→**Keep:**
83→- Operation access check (Phase 4 stub)
84→- Execute operation
85→- Format response (WITHOUT jwt field)
86→- Error handling
87→
88→### 5. Simplify MutationHandler
89→
90→**File:** `src/handlers/MutationHandler.ts`
91→
92→**Same changes as QueryHandler:**
93→- Remove `authClient`, `authHeader`, and all renewal logic
94→- Return response without jwt field
95→
96→### 6. Update index.ts
97→
98→**File:** `src/handlers/index.ts`
99→
100→- Update export: `RouteHandler` → `RequestHandler`
101→- Update type export: `RouteHandlerOptions` → `RequestHandlerOptions`
102→
103→### 7. Update Test Files
104→
105→**RequestHandler.test.ts:**
106→- Rename file and describe block
107→- Add tests for session renewal in RequestHandler:
108→ - Renewal on successful operation
109→ - Renewal on failed operation (NOT_FOUND, VALIDATION_ERROR, etc.)
110→ - NO renewal on 401 (UNAUTHORIZED)
111→ - NO renewal on 403 (FORBIDDEN)
112→ - Graceful handling when renewal fails
113→
114→**QueryHandler.test.ts:**
115→- Remove all session renewal tests
116→- Remove `mockAuthClient` from handler constructor
117→- Remove `mockRenewSessionSuccess/Failure` calls
118→
119→**MutationHandler.test.ts:**
120→- Same changes as QueryHandler tests
121→
122→---
123→
124→## Files to Modify
125→
126→| File | Action |
127→|------|--------|
128→| `src/handlers/RouteHandler.ts` | Rename to RequestHandler.ts, add renewal logic |
129→| `src/handlers/QueryHandler.ts` | Remove authClient, remove renewal logic |
130→| `src/handlers/MutationHandler.ts` | Remove authClient, remove renewal logic |
131→| `src/handlers/handler.types.ts` | Rename RouteHandlerOptions → RequestHandlerOptions |
132→| `src/handlers/index.ts` | Update exports |
133→| `src/__tests__/handlers/RouteHandler.test.ts` | Rename, update tests for new renewal behavior |
134→| `src/__tests__/handlers/QueryHandler.test.ts` | Remove renewal tests |
135→| `src/__tests__/handlers/MutationHandler.test.ts` | Remove renewal tests |
136→| `CLAUDE.md` | Update handler architecture documentation |
137→
138→---
139→
140→## Test Coverage Changes
141→
142→**RequestHandler tests (add):**
143→- Session renewal on 200 success response
144→- Session renewal on 404 NOT_FOUND response
145→- Session renewal on 400 VALIDATION_ERROR response
146→- Session renewal on 500 DATABASE_ERROR response
147→- NO renewal on 401 UNAUTHORIZED (route access denied for guest)
148→- NO renewal on 403 FORBIDDEN (route access denied for wrong role)
149→- Graceful handling when renewal fails (operation still succeeds)
150→- No renewal for guest users
151→
152→**QueryHandler/MutationHandler tests (remove):**
153→- All tests in "Session Renewal" describe blocks
154→- Related mock setup (mockRenewSessionSuccess, etc.)
155→
156→---
157→
158→## Verification Steps
159→
160→1. `pnpm --filter cwc-api typecheck` - All types compile
161→2. `NODE_ENV=unit pnpm --filter cwc-api test` - All tests pass
162→3. Verify test count is similar (tests moved, not removed)
163→
<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→ CwcApiErrorCode,
11→} from './handler.types';
12→
13→const codeLocation = 'handlers/QueryHandler.ts';
14→
15→/**
16→ * QueryHandler - Handles read-only operations
17→ *
18→ * Responsibilities:
19→ * 1. Check operation-level access (Phase 4 stub)
20→ * 2. Execute the query operation
21→ * 3. Format successful response with data/pagination
22→ *
23→ * Note: Session renewal is handled by RequestHandler
24→ */
25→export class QueryHandler implements CwcApiHandler {
26→ private options: QueryHandlerOptions;
27→ private config: CwcApiConfig;
28→ private logger: ILogger | undefined;
29→
30→ constructor(
31→ options: QueryHandlerOptions,
32→ config: CwcApiConfig,
33→ logger: ILogger | undefined
34→ ) {
35→ this.options = options;
36→ this.config = config;
37→ this.logger = logger;
38→ }
39→
40→ public async processRequest(): Promise<CwcApiHandlerResponse> {
41→ const { context, routeConfig, payload, operationContext } = this.options;
42→
43→ try {
44→ // Step 1: Check operation-level access (Phase 4 implementation)
45→ const operationAccess = await this.checkOperationAccess();
46→ if (!operationAccess.allowed) {
47→ return this.createOperationDeniedResponse(operationAccess.reason);
48→ }
49→
50→ // Step 2: Execute the operation
51→ const result = await routeConfig.operation(payload, operationContext);
52→
53→ if (!result.success) {
54→ return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
55→ }
56→
57→ // Step 3: Format response (JWT added by RequestHandler)
58→ return {
59→ statusCode: 200,
60→ body: {
61→ success: true,
62→ data: result.data,
63→ ...(result.pagination ? { pagination: result.pagination } : {}),
64→ },
65→ };
66→ } catch (error) {
67→ this.logger?.logError({
68→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
69→ codeLocation,
70→ message: `Error executing query operation for ${routeConfig.path}`,
71→ error,
72→ });
73→
74→ return this.createInternalErrorResponse(error);
75→ }
76→ }
77→
78→ /**
79→ * Check if the current context can perform this operation
80→ * Phase 3: Stub - always returns allowed
81→ * Phase 4: Will check ownership, resource access, etc.
82→ */
83→ private async checkOperationAccess(): Promise<OperationAccessResult> {
84→ // Phase 4 will implement actual access policy checks
85→ // For now, route-level access is sufficient
86→ return { allowed: true };
87→ }
88→
89→ private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
90→ return {
91→ statusCode: 403,
92→ body: {
93→ success: false,
94→ errorCode: 'OPERATION_ACCESS_DENIED',
95→ errorMessage: 'Operation not permitted',
96→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
97→ },
98→ };
99→ }
100→
101→ private createOperationErrorResponse(
102→ errorCode: CwcApiErrorCode,
103→ errorMessage: string
104→ ): CwcApiHandlerResponse {
105→ const statusCode = this.mapErrorCodeToStatus(errorCode);
106→
107→ return {
108→ statusCode,
109→ body: {
110→ success: false,
111→ errorCode,
112→ errorMessage,
113→ },
114→ };
115→ }
116→
117→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
118→ return {
119→ statusCode: 500,
120→ body: {
121→ success: false,
122→ errorCode: 'INTERNAL_ERROR',
123→ errorMessage: 'An internal error occurred',
124→ ...(this.config.isDevelopment && error instanceof Error
125→ ? { errorDetail: error.message }
126→ : {}),
127→ },
128→ };
129→ }
130→
131→ private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
132→ switch (errorCode) {
133→ case 'NOT_FOUND':
134→ return 404;
135→ case 'VALIDATION_ERROR':
136→ case 'INVALID_PARAMETER':
137→ case 'ALREADY_EXISTS':
138→ return 400;
139→ case 'UNAUTHORIZED':
140→ return 401;
141→ case 'FORBIDDEN':
142→ case 'ROUTE_ACCESS_DENIED':
143→ case 'OPERATION_ACCESS_DENIED':
144→ return 403;
145→ case 'INTERNAL_ERROR':
146→ case 'DATABASE_ERROR':
147→ case 'AUTH_SERVICE_ERROR':
148→ return 500;
149→ default:
150→ return 500;
151→ }
152→ }
153→}
154→
<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→
16→const codeLocation = 'handlers/RequestHandler.ts';
17→
18→/**
19→ * RequestHandler - Entry point for processing API requests
20→ *
21→ * Responsibilities:
22→ * 1. Check route-level access based on context role
23→ * 2. Build operation context with path params
24→ * 3. Delegate to QueryHandler or MutationHandler based on handlerType
25→ * 4. Renew session for authenticated users (except on auth errors)
26→ */
27→export class RequestHandler implements CwcApiHandler {
28→ private options: RequestHandlerOptions;
29→ private config: CwcApiConfig;
30→ private authClient: AuthClient;
31→ private logger: ILogger | undefined;
32→
33→ constructor(
34→ options: RequestHandlerOptions,
35→ config: CwcApiConfig,
36→ authClient: AuthClient,
37→ logger: ILogger | undefined
38→ ) {
39→ this.options = options;
40→ this.config = config;
41→ this.authClient = authClient;
42→ this.logger = logger;
43→ }
44→
45→ public async processRequest(): Promise<CwcApiHandlerResponse> {
46→ const { context, routeConfig, payload, authHeader, pathParams } = this.options;
47→
48→ try {
49→ // Step 1: Check route-level access
50→ const routeAccess = this.checkRouteAccess();
51→ if (!routeAccess.allowed) {
52→ // No session renewal for auth errors
53→ return this.createAccessDeniedResponse(routeAccess.reason);
54→ }
55→
56→ // Step 2: Build operation context
57→ const operationContext: OperationContext = {
58→ context,
59→ projectId: pathParams?.['projectId'],
60→ resourceId: pathParams?.['resourceId'],
61→ };
62→
63→ // Step 3: Delegate to appropriate handler
64→ let response: CwcApiHandlerResponse;
65→ if (routeConfig.handlerType === 'query') {
66→ const queryHandler = new QueryHandler(
67→ {
68→ context,
69→ routeConfig,
70→ authHeader,
71→ payload,
72→ operationContext,
73→ },
74→ this.config,
75→ this.logger
76→ );
77→ response = await queryHandler.processRequest();
78→ } else {
79→ const mutationHandler = new MutationHandler(
80→ {
81→ context,
82→ routeConfig,
83→ authHeader,
84→ payload,
85→ operationContext,
86→ },
87→ this.config,
88→ this.logger
89→ );
90→ response = await mutationHandler.processRequest();
91→ }
92→
93→ // Step 4: Renew session for authenticated users (except on auth errors)
94→ const isAuthError = response.statusCode === 401 || response.statusCode === 403;
95→ if (context.isAuthenticated && !isAuthError) {
96→ const renewResult = await this.authClient.renewSession(authHeader);
97→ if (renewResult.success && response.body.success) {
98→ // Add JWT to successful response
99→ (response.body as CwcApiSuccessResponse).jwt = renewResult.jwt;
100→ } else if (!renewResult.success) {
101→ // Log warning but don't fail the operation
102→ this.logger?.logError({
103→ userPkId: context.userPkId,
104→ codeLocation,
105→ message: `Session renewal failed for ${routeConfig.path}`,
106→ error: renewResult.error,
107→ });
108→ }
109→ }
110→
111→ return response;
112→ } catch (error) {
113→ this.logger?.logError({
114→ userPkId: context.isAuthenticated ? context.userPkId : undefined,
115→ codeLocation,
116→ message: `Error processing request ${routeConfig.path}`,
117→ error,
118→ });
119→
120→ return this.createInternalErrorResponse(error);
121→ }
122→ }
123→
124→ /**
125→ * Check if the current context role can access this route
126→ * Compares context.role against routeConfig.allowedRoles
127→ */
128→ private checkRouteAccess(): RouteAccessResult {
129→ const { context, routeConfig } = this.options;
130→ const { allowedRoles } = routeConfig;
131→
132→ // Check if the current role is in allowedRoles
133→ if (allowedRoles.includes(context.role)) {
134→ return { allowed: true };
135→ }
136→
137→ // For authenticated users with 'project-owner' role requirement,
138→ // 'logged-on-user' should also be allowed (ownership check happens at operation level)
139→ if (
140→ context.isAuthenticated &&
141→ context.role === 'logged-on-user' &&
142→ allowedRoles.includes('project-owner')
143→ ) {
144→ return { allowed: true };
145→ }
146→
147→ return {
148→ allowed: false,
149→ reason: `Role '${context.role}' cannot access route ${routeConfig.path}`,
150→ };
151→ }
152→
153→ private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {
154→ const { context } = this.options;
155→
156→ // Use 401 for unauthenticated, 403 for authenticated but not allowed
157→ const statusCode = context.isAuthenticated ? 403 : 401;
158→ const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';
159→
160→ return {
161→ statusCode,
162→ body: {
163→ success: false,
164→ errorCode,
165→ errorMessage: 'Access denied',
166→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
167→ },
168→ };
169→ }
170→
171→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
172→ return {
173→ statusCode: 500,
174→ body: {
175→ success: false,
176→ errorCode: 'INTERNAL_ERROR',
177→ errorMessage: 'An internal error occurred',
178→ ...(this.config.isDevelopment && error instanceof Error
179→ ? { errorDetail: error.message }
180→ : {}),
181→ },
182→ };
183→ }
184→}
185→
<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 can see the duplication clearly. Let me compare:
QueryHandler & MutationHandler (identical):
createOperationDeniedResponse(reason?)- 403 OPERATION_ACCESS_DENIEDcreateOperationErrorResponse(errorCode, errorMessage)- uses mapErrorCodeToStatuscreateInternalErrorResponse(error)- 500 INTERNAL_ERRORmapErrorCodeToStatus(errorCode)- error code to HTTP status mapping
RequestHandler:
createAccessDeniedResponse(reason?)- different (401 vs 403 based on auth)createInternalErrorResponse(error)- identical to Query/MutationHandler
The duplication is clear - 4 functions duplicated between Query/MutationHandler, and createInternalErrorResponse duplicated across all 3.
Let me write the plan for consolidation.
Handler Response Utilities: Extract Shared Functions
Overview
Extract duplicate response creation functions from handlers into a shared utility module.
Duplicated Functions
| Function | QueryHandler | MutationHandler | RequestHandler |
|---|---|---|---|
createOperationDeniedResponse |
✓ | ✓ | - |
createOperationErrorResponse |
✓ | ✓ | - |
createInternalErrorResponse |
✓ | ✓ | ✓ |
mapErrorCodeToStatus |
✓ | ✓ | - |
createAccessDeniedResponse |
- | - | ✓ (unique) |
Note: createAccessDeniedResponse is unique to RequestHandler (determines 401 vs 403 based on auth status).
Implementation Plan
1. Create src/handlers/responseUtils.ts
New file with shared functions:
import type { CwcApiConfig } from '../config';
import type { CwcApiHandlerResponse, CwcApiErrorCode } from './handler.types';
export function mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
// ... existing logic
}
export function createOperationDeniedResponse(
config: CwcApiConfig,
reason?: string
): CwcApiHandlerResponse {
// ... existing logic
}
export function createOperationErrorResponse(
errorCode: CwcApiErrorCode,
errorMessage: string
): CwcApiHandlerResponse {
// ... existing logic
}
export function createInternalErrorResponse(
config: CwcApiConfig,
error: unknown
): CwcApiHandlerResponse {
// ... existing logic
}
2. Update QueryHandler.ts
- Import shared functions from
./responseUtils - Remove private methods:
createOperationDeniedResponse,createOperationErrorResponse,createInternalErrorResponse,mapErrorCodeToStatus - Update calls to use imported functions (pass
this.configwhere needed)
3. Update MutationHandler.ts
- Same changes as QueryHandler
4. Update RequestHandler.ts
- Import
createInternalErrorResponsefrom./responseUtils - Remove private method
createInternalErrorResponse - Keep
createAccessDeniedResponse(unique logic)
5. Update handlers/index.ts
- Export response utilities if needed externally (optional)
Files to Modify
| File | Action |
|---|---|
src/handlers/responseUtils.ts |
Create - shared response utilities |
src/handlers/QueryHandler.ts |
Remove private methods, import from responseUtils |
src/handlers/MutationHandler.ts |
Remove private methods, import from responseUtils |
src/handlers/RequestHandler.ts |
Remove createInternalErrorResponse, import from responseUtils |
src/handlers/index.ts |
Optionally export responseUtils |
Verification
pnpm --filter cwc-api typecheck- All types compileNODE_ENV=unit pnpm --filter cwc-api test- All 84 tests pass- No behavior changes - pure refactoring
Tool Result
The file /Users/jeffbazinet/.claude/plans/steady-foraging-glade.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
1→# Handler Response Utilities: Extract Shared Functions
2→
3→## Overview
4→
5→Extract duplicate response creation functions from handlers into a shared utility module.
6→
7→## Duplicated Functions
8→
9→| Function | QueryHandler | MutationHandler | RequestHandler |
10→|----------|--------------|-----------------|----------------|
11→| `createOperationDeniedResponse` | ✓ | ✓ | - |
12→| `createOperationErrorResponse` | ✓ | ✓ | - |
13→| `createInternalErrorResponse` | ✓ | ✓ | ✓ |
14→| `mapErrorCodeToStatus` | ✓ | ✓ | - |
15→| `createAccessDeniedResponse` | - | - | ✓ (unique) |
16→
17→Note: `createAccessDeniedResponse` is unique to RequestHandler (determines 401 vs 403 based on auth status).
18→
19→---
20→
21→## Implementation Plan
22→
23→### 1. Create `src/handlers/responseUtils.ts`
24→
25→New file with shared functions:
26→
27→```typescript
28→import type { CwcApiConfig } from '../config';
29→import type { CwcApiHandlerResponse, CwcApiErrorCode } from './handler.types';
30→
31→export function mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
32→ // ... existing logic
33→}
34→
35→export function createOperationDeniedResponse(
36→ config: CwcApiConfig,
37→ reason?: string
38→): CwcApiHandlerResponse {
39→ // ... existing logic
40→}
41→
42→export function createOperationErrorResponse(
43→ errorCode: CwcApiErrorCode,
44→ errorMessage: string
45→): CwcApiHandlerResponse {
46→ // ... existing logic
47→}
48→
49→export function createInternalErrorResponse(
50→ config: CwcApiConfig,
51→ error: unknown
52→): CwcApiHandlerResponse {
53→ // ... existing logic
54→}
55→```
56→
57→### 2. Update QueryHandler.ts
58→
59→- Import shared functions from `./responseUtils`
60→- Remove private methods: `createOperationDeniedResponse`, `createOperationErrorResponse`, `createInternalErrorResponse`, `mapErrorCodeToStatus`
61→- Update calls to use imported functions (pass `this.config` where needed)
62→
63→### 3. Update MutationHandler.ts
64→
65→- Same changes as QueryHandler
66→
67→### 4. Update RequestHandler.ts
68→
69→- Import `createInternalErrorResponse` from `./responseUtils`
70→- Remove private method `createInternalErrorResponse`
71→- Keep `createAccessDeniedResponse` (unique logic)
72→
73→### 5. Update handlers/index.ts
74→
75→- Export response utilities if needed externally (optional)
76→
77→---
78→
79→## Files to Modify
80→
81→| File | Action |
82→|------|--------|
83→| `src/handlers/responseUtils.ts` | **Create** - shared response utilities |
84→| `src/handlers/QueryHandler.ts` | Remove private methods, import from responseUtils |
85→| `src/handlers/MutationHandler.ts` | Remove private methods, import from responseUtils |
86→| `src/handlers/RequestHandler.ts` | Remove createInternalErrorResponse, import from responseUtils |
87→| `src/handlers/index.ts` | Optionally export responseUtils |
88→
89→---
90→
91→## Verification
92→
93→1. `pnpm --filter cwc-api typecheck` - All types compile
94→2. `NODE_ENV=unit pnpm --filter cwc-api test` - All 84 tests pass
95→3. No behavior changes - pure refactoring
96→This is a straightforward refactoring. I've already read all three handlers and identified the exact duplication. The plan is ready - extracting 4 functions into a shared responseUtils.ts file.
No questions needed - this is pure code deduplication with no behavior changes.
TodoWrite
{
"todos": [
{
"content": "Create src/handlers/responseUtils.ts with shared functions",
"status": "in_progress",
"activeForm": "Creating responseUtils.ts with shared functions"
},
{
"content": "Update QueryHandler.ts to use responseUtils",
"status": "pending",
"activeForm": "Updating QueryHandler to use responseUtils"
},
{
"content": "Update MutationHandler.ts to use responseUtils",
"status": "pending",
"activeForm": "Updating MutationHandler to use responseUtils"
},
{
"content": "Update RequestHandler.ts to use responseUtils",
"status": "pending",
"activeForm": "Updating RequestHandler to use responseUtils"
},
{
"content": "Run tests to verify refactoring",
"status": "pending",
"activeForm": "Running tests to verify refactoring"
}
]
}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/handlers/responseUtils.ts",
"content": "'use strict';\n\nimport type { CwcApiConfig } from '../config';\nimport type { CwcApiHandlerResponse, CwcApiErrorCode } from './handler.types';\n\n/**\n * Maps error codes to appropriate HTTP status codes\n */\nexport function mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {\n switch (errorCode) {\n case 'NOT_FOUND':\n return 404;\n case 'VALIDATION_ERROR':\n case 'INVALID_PARAMETER':\n case 'ALREADY_EXISTS':\n return 400;\n case 'UNAUTHORIZED':\n return 401;\n case 'FORBIDDEN':\n case 'ROUTE_ACCESS_DENIED':\n case 'OPERATION_ACCESS_DENIED':\n return 403;\n case 'INTERNAL_ERROR':\n case 'DATABASE_ERROR':\n case 'AUTH_SERVICE_ERROR':\n return 500;\n default:\n return 500;\n }\n}\n\n/**\n * Creates a 403 response for operation access denied\n */\nexport function createOperationDeniedResponse(\n config: CwcApiConfig,\n reason?: string\n): CwcApiHandlerResponse {\n return {\n statusCode: 403,\n body: {\n success: false,\n errorCode: 'OPERATION_ACCESS_DENIED',\n errorMessage: 'Operation not permitted',\n ...(config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n}\n\n/**\n * Creates an error response with appropriate status code based on error code\n */\nexport function createOperationErrorResponse(\n errorCode: CwcApiErrorCode,\n errorMessage: string\n): CwcApiHandlerResponse {\n const statusCode = mapErrorCodeToStatus(errorCode);\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage,\n },\n };\n}\n\n/**\n * Creates a 500 internal error response\n */\nexport function createInternalErrorResponse(\n config: CwcApiConfig,\n error: unknown\n): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n}\n"
}Tool Result
File created successfully at: /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/responseUtils.ts
TodoWrite
{
"todos": [
{
"content": "Create src/handlers/responseUtils.ts with shared functions",
"status": "completed",
"activeForm": "Creating responseUtils.ts with shared functions"
},
{
"content": "Update QueryHandler.ts to use responseUtils",
"status": "in_progress",
"activeForm": "Updating QueryHandler to use responseUtils"
},
{
"content": "Update MutationHandler.ts to use responseUtils",
"status": "pending",
"activeForm": "Updating MutationHandler to use responseUtils"
},
{
"content": "Update RequestHandler.ts to use responseUtils",
"status": "pending",
"activeForm": "Updating RequestHandler to use responseUtils"
},
{
"content": "Run tests to verify refactoring",
"status": "pending",
"activeForm": "Running tests to verify refactoring"
}
]
}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 {\n CwcApiHandler,\n CwcApiHandlerResponse,\n QueryHandlerOptions,\n OperationAccessResult,\n CwcApiErrorCode,\n} from './handler.types';",
"new_string": "import type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n QueryHandlerOptions,\n OperationAccessResult,\n} from './handler.types';\nimport {\n createOperationDeniedResponse,\n createOperationErrorResponse,\n createInternalErrorResponse,\n} from './responseUtils';"
}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→ 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→/**Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts",
"old_string": " // Step 1: Check operation-level access (Phase 4 implementation)\n const operationAccess = await this.checkOperationAccess();\n if (!operationAccess.allowed) {\n return this.createOperationDeniedResponse(operationAccess.reason);\n }\n\n // Step 2: Execute the operation\n const result = await routeConfig.operation(payload, operationContext);\n\n if (!result.success) {\n return this.createOperationErrorResponse(result.errorCode, result.errorMessage);\n }",
"new_string": " // 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 }\n\n // Step 2: Execute the operation\n const result = await routeConfig.operation(payload, operationContext);\n\n if (!result.success) {\n return createOperationErrorResponse(result.errorCode, result.errorMessage);\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:
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,Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/QueryHandler.ts",
"old_string": " this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error executing query operation for ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(error);\n }\n }",
"new_string": " this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error executing query operation for ${routeConfig.path}`,\n error,\n });\n\n return createInternalErrorResponse(this.config, error);\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:
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 allowedRead
{
"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→ private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
94→ return {
95→ statusCode: 403,
96→ body: {
97→ success: false,
98→ errorCode: 'OPERATION_ACCESS_DENIED',
99→ errorMessage: 'Operation not permitted',
100→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
101→ },
102→ };
103→ }
104→
105→ private createOperationErrorResponse(
106→ errorCode: CwcApiErrorCode,
107→ errorMessage: string
108→ ): CwcApiHandlerResponse {
109→ const statusCode = this.mapErrorCodeToStatus(errorCode);
110→
111→ return {
112→ statusCode,
113→ body: {
114→ success: false,
115→ errorCode,
116→ errorMessage,
117→ },
118→ };
119→ }
120→
121→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
122→ return {
123→ statusCode: 500,
124→ body: {
125→ success: false,
126→ errorCode: 'INTERNAL_ERROR',
127→ errorMessage: 'An internal error occurred',
128→ ...(this.config.isDevelopment && error instanceof Error
129→ ? { errorDetail: error.message }
130→ : {}),
131→ },
132→ };
133→ }
134→
135→ private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
136→ switch (errorCode) {
137→ case 'NOT_FOUND':
138→ return 404;
139→ case 'VALIDATION_ERROR':
140→ case 'INVALID_PARAMETER':
141→ case 'ALREADY_EXISTS':
142→ return 400;
143→ case 'UNAUTHORIZED':
144→ return 401;
145→ case 'FORBIDDEN':
146→ case 'ROUTE_ACCESS_DENIED':
147→ case 'OPERATION_ACCESS_DENIED':
148→ return 403;
149→ case 'INTERNAL_ERROR':
150→ case 'DATABASE_ERROR':
151→ case 'AUTH_SERVICE_ERROR':
152→ return 500;
153→ default:
154→ return 500;
155→ }
156→ }
157→}
158→
<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/QueryHandler.ts",
"old_string": " /**\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\n private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {\n return {\n statusCode: 403,\n body: {\n success: false,\n errorCode: 'OPERATION_ACCESS_DENIED',\n errorMessage: 'Operation not permitted',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createOperationErrorResponse(\n errorCode: CwcApiErrorCode,\n errorMessage: string\n ): CwcApiHandlerResponse {\n const statusCode = this.mapErrorCodeToStatus(errorCode);\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage,\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n\n private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {\n switch (errorCode) {\n case 'NOT_FOUND':\n return 404;\n case 'VALIDATION_ERROR':\n case 'INVALID_PARAMETER':\n case 'ALREADY_EXISTS':\n return 400;\n case 'UNAUTHORIZED':\n return 401;\n case 'FORBIDDEN':\n case 'ROUTE_ACCESS_DENIED':\n case 'OPERATION_ACCESS_DENIED':\n return 403;\n case 'INTERNAL_ERROR':\n case 'DATABASE_ERROR':\n case 'AUTH_SERVICE_ERROR':\n return 500;\n default:\n return 500;\n }\n }\n}",
"new_string": " /**\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}"
}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:
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→TodoWrite
{
"todos": [
{
"content": "Create src/handlers/responseUtils.ts with shared functions",
"status": "completed",
"activeForm": "Creating responseUtils.ts with shared functions"
},
{
"content": "Update QueryHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating QueryHandler to use responseUtils"
},
{
"content": "Update MutationHandler.ts to use responseUtils",
"status": "in_progress",
"activeForm": "Updating MutationHandler to use responseUtils"
},
{
"content": "Update RequestHandler.ts to use responseUtils",
"status": "pending",
"activeForm": "Updating RequestHandler to use responseUtils"
},
{
"content": "Run tests to verify refactoring",
"status": "pending",
"activeForm": "Running tests to verify refactoring"
}
]
}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/MutationHandler.ts",
"old_string": "import type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n MutationHandlerOptions,\n OperationAccessResult,\n CwcApiErrorCode,\n} from './handler.types';",
"new_string": "import type {\n CwcApiHandler,\n CwcApiHandlerResponse,\n MutationHandlerOptions,\n OperationAccessResult,\n} from './handler.types';\nimport {\n createOperationDeniedResponse,\n createOperationErrorResponse,\n createInternalErrorResponse,\n} from './responseUtils';"
}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→ 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→/**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 this.createOperationDeniedResponse(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 this.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 this.createInternalErrorResponse(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→ private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
92→ return {
93→ statusCode: 403,
94→ body: {
95→ success: false,
96→ errorCode: 'OPERATION_ACCESS_DENIED',
97→ errorMessage: 'Operation not permitted',
98→ ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
99→ },
100→ };
101→ }
102→
103→ private createOperationErrorResponse(
104→ errorCode: CwcApiErrorCode,
105→ errorMessage: string
106→ ): CwcApiHandlerResponse {
107→ const statusCode = this.mapErrorCodeToStatus(errorCode);
108→
109→ return {
110→ statusCode,
111→ body: {
112→ success: false,
113→ errorCode,
114→ errorMessage,
115→ },
116→ };
117→ }
118→
119→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
120→ return {
121→ statusCode: 500,
122→ body: {
123→ success: false,
124→ errorCode: 'INTERNAL_ERROR',
125→ errorMessage: 'An internal error occurred',
126→ ...(this.config.isDevelopment && error instanceof Error
127→ ? { errorDetail: error.message }
128→ : {}),
129→ },
130→ };
131→ }
132→
133→ private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
134→ switch (errorCode) {
135→ case 'NOT_FOUND':
136→ return 404;
137→ case 'VALIDATION_ERROR':
138→ case 'INVALID_PARAMETER':
139→ case 'ALREADY_EXISTS':
140→ return 400;
141→ case 'UNAUTHORIZED':
142→ return 401;
143→ case 'FORBIDDEN':
144→ case 'ROUTE_ACCESS_DENIED':
145→ case 'OPERATION_ACCESS_DENIED':
146→ return 403;
147→ case 'INTERNAL_ERROR':
148→ case 'DATABASE_ERROR':
149→ case 'AUTH_SERVICE_ERROR':
150→ return 500;
151→ default:
152→ return 500;
153→ }
154→ }
155→}
156→
<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": " // Step 1: Check operation-level access (Phase 4 implementation)\n const operationAccess = await this.checkOperationAccess();\n if (!operationAccess.allowed) {\n return this.createOperationDeniedResponse(operationAccess.reason);\n }\n\n // Step 2: Execute the mutation\n const result = await routeConfig.operation(payload, operationContext);\n\n if (!result.success) {\n return this.createOperationErrorResponse(result.errorCode, result.errorMessage);\n }",
"new_string": " // 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 }\n\n // Step 2: Execute the mutation\n const result = await routeConfig.operation(payload, operationContext);\n\n if (!result.success) {\n return createOperationErrorResponse(result.errorCode, result.errorMessage);\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:
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,Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts",
"old_string": " this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error executing mutation operation for ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(error);\n }\n }",
"new_string": " this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error executing mutation operation for ${routeConfig.path}`,\n error,\n });\n\n return createInternalErrorResponse(this.config, error);\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:
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 allowedEdit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/MutationHandler.ts",
"old_string": " /**\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\n private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {\n return {\n statusCode: 403,\n body: {\n success: false,\n errorCode: 'OPERATION_ACCESS_DENIED',\n errorMessage: 'Operation not permitted',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createOperationErrorResponse(\n errorCode: CwcApiErrorCode,\n errorMessage: string\n ): CwcApiHandlerResponse {\n const statusCode = this.mapErrorCodeToStatus(errorCode);\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage,\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n\n private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {\n switch (errorCode) {\n case 'NOT_FOUND':\n return 404;\n case 'VALIDATION_ERROR':\n case 'INVALID_PARAMETER':\n case 'ALREADY_EXISTS':\n return 400;\n case 'UNAUTHORIZED':\n return 401;\n case 'FORBIDDEN':\n case 'ROUTE_ACCESS_DENIED':\n case 'OPERATION_ACCESS_DENIED':\n return 403;\n case 'INTERNAL_ERROR':\n case 'DATABASE_ERROR':\n case 'AUTH_SERVICE_ERROR':\n return 500;\n default:\n return 500;\n }\n }\n}",
"new_string": " /**\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}"
}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:
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→TodoWrite
{
"todos": [
{
"content": "Create src/handlers/responseUtils.ts with shared functions",
"status": "completed",
"activeForm": "Creating responseUtils.ts with shared functions"
},
{
"content": "Update QueryHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating QueryHandler to use responseUtils"
},
{
"content": "Update MutationHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating MutationHandler to use responseUtils"
},
{
"content": "Update RequestHandler.ts to use responseUtils",
"status": "in_progress",
"activeForm": "Updating RequestHandler to use responseUtils"
},
{
"content": "Run tests to verify refactoring",
"status": "pending",
"activeForm": "Running tests to verify refactoring"
}
]
}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/RequestHandler.ts",
"old_string": "import { QueryHandler } from './QueryHandler';\nimport { MutationHandler } from './MutationHandler';",
"new_string": "import { QueryHandler } from './QueryHandler';\nimport { MutationHandler } from './MutationHandler';\nimport { createInternalErrorResponse } from './responseUtils';"
}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:
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→/**Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts",
"old_string": " this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error processing request ${routeConfig.path}`,\n error,\n });\n\n return this.createInternalErrorResponse(error);\n }\n }",
"new_string": " this.logger?.logError({\n userPkId: context.isAuthenticated ? context.userPkId : undefined,\n codeLocation,\n message: `Error processing request ${routeConfig.path}`,\n error,\n });\n\n return createInternalErrorResponse(this.config, error);\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:
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.allowedRolesRead
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/handlers/RequestHandler.ts",
"offset": 150
}Tool Result
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.isDevelopment && reason ? { errorDetail: reason } : {}),
168→ },
169→ };
170→ }
171→
172→ private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
173→ return {
174→ statusCode: 500,
175→ body: {
176→ success: false,
177→ errorCode: 'INTERNAL_ERROR',
178→ errorMessage: 'An internal error occurred',
179→ ...(this.config.isDevelopment && error instanceof Error
180→ ? { errorDetail: error.message }
181→ : {}),
182→ },
183→ };
184→ }
185→}
186→
<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/RequestHandler.ts",
"old_string": " private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {\n const { context } = this.options;\n\n // Use 401 for unauthenticated, 403 for authenticated but not allowed\n const statusCode = context.isAuthenticated ? 403 : 401;\n const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage: 'Access denied',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\n },\n };\n }\n\n private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {\n return {\n statusCode: 500,\n body: {\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n ...(this.config.isDevelopment && error instanceof Error\n ? { errorDetail: error.message }\n : {}),\n },\n };\n }\n}",
"new_string": " private createAccessDeniedResponse(reason?: string): CwcApiHandlerResponse {\n const { context } = this.options;\n\n // Use 401 for unauthenticated, 403 for authenticated but not allowed\n const statusCode = context.isAuthenticated ? 403 : 401;\n const errorCode = context.isAuthenticated ? 'FORBIDDEN' : 'UNAUTHORIZED';\n\n return {\n statusCode,\n body: {\n success: false,\n errorCode,\n errorMessage: 'Access denied',\n ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),\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:
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.isDevelopment && reason ? { errorDetail: reason } : {}),
168→ },
169→ };
170→ }
171→}
172→TodoWrite
{
"todos": [
{
"content": "Create src/handlers/responseUtils.ts with shared functions",
"status": "completed",
"activeForm": "Creating responseUtils.ts with shared functions"
},
{
"content": "Update QueryHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating QueryHandler to use responseUtils"
},
{
"content": "Update MutationHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating MutationHandler to use responseUtils"
},
{
"content": "Update RequestHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating RequestHandler to use responseUtils"
},
{
"content": "Run tests to verify refactoring",
"status": "in_progress",
"activeForm": "Running tests to verify refactoring"
}
]
}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": "Type check cwc-api package",
"timeout": 60000
}Tool Result
> cwc-api@1.0.0 typecheck /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api > tsc --noEmit
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test",
"description": "Run cwc-api tests",
"timeout": 60000
}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__/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 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__/handlers/MutationHandler.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__/config.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)
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: ⚙️ 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: ⚙️ 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__/healthCheck.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)
Test Suites: 6 passed, 6 total
Tests: 84 passed, 84 total
Snapshots: 0 total
Time: 0.474 s, estimated 1 s
Ran all test suites.TodoWrite
{
"todos": [
{
"content": "Create src/handlers/responseUtils.ts with shared functions",
"status": "completed",
"activeForm": "Creating responseUtils.ts with shared functions"
},
{
"content": "Update QueryHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating QueryHandler to use responseUtils"
},
{
"content": "Update MutationHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating MutationHandler to use responseUtils"
},
{
"content": "Update RequestHandler.ts to use responseUtils",
"status": "completed",
"activeForm": "Updating RequestHandler to use responseUtils"
},
{
"content": "Run tests to verify refactoring",
"status": "completed",
"activeForm": "Running tests to verify refactoring"
}
]
}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
Done. The duplicate response utility functions have been extracted.
Summary
Created: src/handlers/responseUtils.ts with 4 shared functions:
mapErrorCodeToStatus(errorCode)- maps error codes to HTTP status codescreateOperationDeniedResponse(config, reason?)- 403 OPERATION_ACCESS_DENIEDcreateOperationErrorResponse(errorCode, errorMessage)- error response with mapped statuscreateInternalErrorResponse(config, error)- 500 INTERNAL_ERROR
Updated handlers:
QueryHandler.ts- removed 4 private methods, imports from responseUtilsMutationHandler.ts- removed 4 private methods, imports from responseUtilsRequestHandler.ts- removed 1 private method, imports createInternalErrorResponse
Result:
- Typecheck passes
- All 84 tests pass
- Code is now DRY - shared response logic in one place
should we add unit tests for reqponseUtils.ts or do our other tests sufficiently test those functions?
Good question. Let me assess the current coverage:
What the handler tests cover:
createOperationErrorResponse: NOT_FOUND→404, VALIDATION_ERROR→400, DATABASE_ERROR→500, ALREADY_EXISTS→400createInternalErrorResponse: 500 response, errorDetail in dev mode, no errorDetail in prodcreateOperationDeniedResponse: 403 with OPERATION_ACCESS_DENIED (via the Phase 4 stub tests)
What's NOT explicitly tested:
mapErrorCodeToStatusexhaustive coverage (UNAUTHORIZED→401, FORBIDDEN→403, AUTH_SERVICE_ERROR→500, INVALID_PARAMETER→400, ROUTE_ACCESS_DENIED→403, default→500)- Edge cases like
undefinedreason increateOperationDeniedResponse
My recommendation: The handler tests provide good behavioral coverage for the common paths. Dedicated unit tests would add value mainly for:
- Exhaustive
mapErrorCodeToStatuscoverage (all 11 error codes) - Documenting the contract explicitly
However, since these are pure, simple functions and the handlers exercise them heavily, adding dedicated tests is optional - it depends on how thorough you want the test suite to be.
Would you like me to add dedicated unit tests for responseUtils.ts, or are you satisfied with the current handler-level coverage?
Let's add one test per function to verify the shape of the result please.
Write
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/src/__tests__/handlers/responseUtils.test.ts",
"content": "'use strict';\n\nimport { describe, it, expect } from '@jest/globals';\nimport {\n mapErrorCodeToStatus,\n createOperationDeniedResponse,\n createOperationErrorResponse,\n createInternalErrorResponse,\n} from '../../handlers/responseUtils';\nimport { createMockDevConfig, createMockProdConfig } from '../mocks';\n\ndescribe('responseUtils', () => {\n describe('mapErrorCodeToStatus', () => {\n it('should map error codes to appropriate HTTP status codes', () => {\n expect(mapErrorCodeToStatus('NOT_FOUND')).toBe(404);\n expect(mapErrorCodeToStatus('VALIDATION_ERROR')).toBe(400);\n expect(mapErrorCodeToStatus('UNAUTHORIZED')).toBe(401);\n expect(mapErrorCodeToStatus('FORBIDDEN')).toBe(403);\n expect(mapErrorCodeToStatus('DATABASE_ERROR')).toBe(500);\n });\n });\n\n describe('createOperationDeniedResponse', () => {\n it('should return 403 response with OPERATION_ACCESS_DENIED error code', () => {\n const config = createMockProdConfig();\n const response = createOperationDeniedResponse(config, 'Test reason');\n\n expect(response.statusCode).toBe(403);\n expect(response.body).toEqual({\n success: false,\n errorCode: 'OPERATION_ACCESS_DENIED',\n errorMessage: 'Operation not permitted',\n });\n });\n });\n\n describe('createOperationErrorResponse', () => {\n it('should return response with mapped status code and error details', () => {\n const response = createOperationErrorResponse('NOT_FOUND', 'Resource not found');\n\n expect(response.statusCode).toBe(404);\n expect(response.body).toEqual({\n success: false,\n errorCode: 'NOT_FOUND',\n errorMessage: 'Resource not found',\n });\n });\n });\n\n describe('createInternalErrorResponse', () => {\n it('should return 500 response with INTERNAL_ERROR and include errorDetail in dev mode', () => {\n const config = createMockDevConfig();\n const response = createInternalErrorResponse(config, new Error('Test error'));\n\n expect(response.statusCode).toBe(500);\n expect(response.body).toEqual({\n success: false,\n errorCode: 'INTERNAL_ERROR',\n errorMessage: 'An internal error occurred',\n errorDetail: 'Test error',\n });\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__/handlers/responseUtils.test.ts
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test -- --testPathPatterns=\"responseUtils\"",
"description": "Run responseUtils tests",
"timeout": 60000
}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 "--testPathPatterns=responseUtils"
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
responseUtils
mapErrorCodeToStatus
✓ should map error codes to appropriate HTTP status codes (1 ms)
createOperationDeniedResponse
✓ should return 403 response with OPERATION_ACCESS_DENIED error code
createOperationErrorResponse
✓ should return response with mapped status code and error details
createInternalErrorResponse
✓ should return 500 response with INTERNAL_ERROR and include errorDetail in dev mode
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 0.243 s
Ran all test suites matching responseUtils.All 4 tests pass. Let me verify the full test suite still passes.
Bash
{
"command": "NODE_ENV=unit pnpm --filter cwc-api test",
"description": "Run full cwc-api test suite",
"timeout": 60000
}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__/handlers/RequestHandler.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: ⚙️ 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/MutationHandler.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__/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/QueryHandler.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__/context/createContext.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__/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)
Test Suites: 7 passed, 7 total
Tests: 88 passed, 88 total
Snapshots: 0 total
Time: 0.431 s, estimated 1 s
Ran all test suites.Done. Added responseUtils.test.ts with 4 tests - one per function verifying the response shape.
Test count: 84 → 88 tests (all passing)
We accomplished a lot in that session. If there are any noteworthy updates for claude.md based on the planning, coding, and code-review conversations (either package or root level) please add those now. Thank you!
Let me review the current state of the documentation and add any missing learnings from this session.
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→- `index.ts` - Exports
107→
108→**Handler Flow:**
109→
110→```
111→RequestHandler.processRequest()
112→├── checkRouteAccess() → 401/403 if denied (NO renewal)
113→├── buildOperationContext() → projectId, resourceId from path
114→├── delegate to QueryHandler or MutationHandler
115→│ ├── checkOperationAccess() → (stub for Phase 4)
116→│ ├── execute operation()
117→│ └── format response (no JWT)
118→├── if authenticated AND not auth error (401/403):
119→│ └── renewSession() → merge JWT into response
120→└── return final response
121→```
122→
123→**Session Renewal Strategy:**
124→
125→- Centralized in RequestHandler (not Query/MutationHandler)
126→- Renews on ALL requests except auth errors (401, 403)
127→- Even failed operations (404, 500) trigger renewal to keep session active
128→- New JWT merged into response body (`response.body.jwt`) for success responses
129→- Renewal failure doesn't fail the request (graceful degradation, logged)
130→- Guest users never trigger renewal
131→
132→**Error Code Mapping:**
133→
134→| Error Code | HTTP Status |
135→|------------|-------------|
136→| `UNAUTHORIZED` | 401 |
137→| `FORBIDDEN` | 403 |
138→| `NOT_FOUND` | 404 |
139→| `ALREADY_EXISTS` | 400 |
140→| `VALIDATION_ERROR` | 400 |
141→| `INVALID_PARAMETER` | 400 |
142→| `DATABASE_ERROR` | 500 |
143→| `INTERNAL_ERROR` | 500 |
144→
145→## Role-Based Access Control
146→
147→**Roles:**
148→
149→| Role | Description |
150→|------|-------------|
151→| `project-owner` | User owns the project being accessed |
152→| `logged-on-user` | Authenticated user (may not own the project) |
153→| `guest-user` | Unauthenticated request (read-only access) |
154→
155→**Access Policy Levels:**
156→
157→- **Route-level**: Can this role call this endpoint at all?
158→- **Operation-level**: Can this role execute this specific operation? (Phase 4)
159→- **Context-aware**: Does the user have the right relationship to the data? (e.g., ownership)
160→
161→## Tables Handled
162→
163→**Content Tables (Full CRUD + Soft Delete):**
164→
165→- project
166→- codingSession
167→- codingSessionContent
168→- codingSessionAttachment
169→- comment
170→- reaction
171→- contentReport
172→- abuseReport
173→
174→**Read-Only Tables:**
175→
176→- featureFlag (all fields)
177→- user (limited fields: userPkId, enabled, username, loginType, loginDate, otpValidated)
178→
179→## SqlFunction Naming Convention
180→
181→Each table has a subdirectory in `src/sql/` with these functions:
182→
183→| Function | Purpose |
184→|----------|---------|
185→| `select{Table}.ts` | Retrieve single record by identifier |
186→| `list{Table}s.ts` | Retrieve multiple records with filtering |
187→| `insert{Table}.ts` | Create new record |
188→| `update{Table}.ts` | Modify existing record |
189→| `delete{Table}.ts` | Hard delete (GDPR compliance) |
190→| `softDelete{Table}.ts` | Soft delete (set enabled=false) |
191→
192→**Key Principles:**
193→
194→- Single responsibility: Each SqlFunction does one thing
195→- No business logic: SqlFunctions handle data access only
196→- Soft-delete awareness: List queries automatically filter `enabled=true`
197→- Composability: Queries/Mutations compose multiple SqlFunctions
198→
199→## cwc-auth Integration
200→
201→**Division of Responsibilities:**
202→
203→| Concern | Owner |
204→|---------|-------|
205→| "Is this token valid?" | cwc-auth |
206→| "Who is this user?" | cwc-auth (via token claims) |
207→| "Can this user access this endpoint?" | cwc-api |
208→| "Is this user the owner of this project?" | cwc-api |
209→
210→**Session Renewal Flow:**
211→
212→When cwc-api creates/deletes a project:
213→1. cwc-api performs the database operation
214→2. cwc-api calls cwc-auth `/auth/v1/renew-session`
215→3. cwc-auth issues new JWT with updated `ownedProjects`
216→4. cwc-api returns new JWT to client
217→
218→## Response Pattern
219→
220→**Standard Response Envelope:**
221→
222→```typescript
223→// Success
224→{
225→ success: true,
226→ data: { ... },
227→ // Optional: pagination metadata
228→}
229→
230→// Error
231→{
232→ success: false,
233→ errorCode: 'ERROR_CODE',
234→ errorMessage: 'Human-readable message',
235→ // Dev-only: errorDetail
236→}
237→```
238→
239→**Pagination (Offset-based):**
240→
241→```typescript
242→{
243→ success: true,
244→ data: [...],
245→ pagination: {
246→ page: 1,
247→ pageSize: 20,
248→ totalCount: 100,
249→ hasMore: true
250→ }
251→}
252→```
253→
254→## Configuration
255→
256→**Required Environment Variables:**
257→
258→```bash
259→RUNTIME_ENVIRONMENT=dev
260→SERVICE_PORT=5040
261→DATA_URI=http://localhost:5020/data/v1
262→AUTH_URI=http://localhost:5005/auth/v1
263→APP_URL=http://localhost:3000
264→CORS_ORIGIN=*
265→DEV_CORS_ORIGIN=http://localhost:3000
266→RATE_LIMITER_POINTS=15
267→RATE_LIMITER_DURATION=1
268→DEBUG_MODE=OFF
269→LOG_ERRORS_TO_DATABASE=ON
270→USER_JWT_SECRET=...
271→SQL_CLIENT_API_KEY=...
272→```
273→
274→## API Versioning
275→
276→**URL-based versioning:**
277→
278→- `/api/v1/*` routes to CwcApiV1 handlers
279→- Each version lives in `src/apis/CwcApiV{N}/`
280→- Route definitions in `routes.ts`, organized alphabetically
281→
282→## Critical Rules
283→
284→**SqlClient Usage:**
285→
286→- Never provide `createdDate` or `modifiedDate` - handled by cwc-sql automatically
287→- Always use SqlFunctions, never construct queries directly in business logic
288→
289→**Guest User Handling:**
290→
291→- No JWT required - RequestHandler creates context with `role: 'guest-user'`
292→- Guest users have read-only access to public content
293→
294→**Soft Delete:**
295→
296→- All content tables support soft delete (`enabled=false`)
297→- Hard delete available for GDPR compliance
298→- List queries automatically filter `enabled=true`
299→
300→## Implementation Status
301→
302→**Active Development:** This package is being implemented following `cwc-api-implementation-checklist.md`.
303→
304→The checklist tracks 7 phases:
305→1. Package Foundation (config, Express service, health check) - **Complete**
306→2. Authentication & Context Layer (JWT, request context) - **Complete**
307→3. Handler Architecture (RequestHandler, QueryHandler, MutationHandler) - **Complete**
308→4. Access Policy System (route and operation policies)
309→5. SqlFunction Layer (CRUD operations for all tables)
310→6. CwcApiV1 Routes & Business Logic
311→7. Integration Testing & Final Review
312→
313→Each phase includes: Implementation → Unit Tests → Security Review → Documentation Updates
314→
315→## Unit Testing
316→
317→**Test Infrastructure:**
318→
319→- Jest 30.x with ts-jest for TypeScript ESM support
320→- Test files: `src/__tests__/**/*.test.ts`
321→- Setup: `src/__tests__/setup.ts` loads unit test environment via `loadDotEnv`
322→- Run tests: `pnpm api test` or `NODE_ENV=unit pnpm --filter cwc-api test`
323→
324→**Mock Infrastructure (`src/__tests__/mocks/`):**
325→
326→| Mock | Purpose |
327→|------|---------|
328→| **SqlClient Mocks** | |
329→| `createMockSqlClient()` | Full SqlClient mock with jest.fn() for all methods |
330→| `mockSelectSuccess/NotFound` | Configure SqlClient for select queries |
331→| `mockListSuccess` | Configure SqlClient for list queries |
332→| `mockInsertSuccess/Failure` | Configure SqlClient for inserts |
333→| `mockUpdateSuccess/NotFound` | Configure SqlClient for updates |
334→| `mockDeleteSuccess/NotFound` | Configure SqlClient for deletes |
335→| **AuthClient Mocks** | |
336→| `createMockAuthClient()` | Full AuthClient mock (verifyToken, renewSession) |
337→| `createMockUserJwtPayload(overrides)` | Create UserJwtPayload with custom overrides |
338→| `mockVerifyTokenSuccess(client, payload?)` | Configure AuthClient to return success |
339→| `mockVerifyTokenFailure(client, error?)` | Configure AuthClient to return failure |
340→| `mockRenewSessionSuccess(client, jwt?)` | Configure renewSession to return new JWT |
341→| `mockRenewSessionFailure(client, error?)` | Configure renewSession to return failure |
342→| **Route/Handler Mocks** | |
343→| `createMockRouteConfig(overrides)` | Create route config for testing |
344→| `createMockOperationContext(overrides)` | Create operation context for testing |
345→| `createMockGuestContext()` | Create guest (unauthenticated) context |
346→| `createMockAuthenticatedContext(overrides)` | Create authenticated user context |
347→| `mockOperationSuccess(routeConfig, data)` | Configure operation to succeed |
348→| `mockOperationFailure(routeConfig, code, msg)` | Configure operation to fail |
349→| `mockOperationThrows(routeConfig, error)` | Configure operation to throw |
350→| **Config Mocks** | |
351→| `getUnitConfig()` | Load real config from unit.cwc-api.env |
352→| `createMockConfig(overrides)` | Create config with custom overrides |
353→| `createMockDevConfig(overrides)` | Mock config with isDevelopment: true |
354→| `createMockProdConfig(overrides)` | Mock config with isProduction: true |
355→| `createMockUnitConfig(overrides)` | Mock config with isUnitTest: true |
356→| **Logger Mock** | |
357→| `createMockLogger()` | ILogger mock with logDebug, logError, logInformation |
358→
359→**Testing Patterns:**
360→
361→RequestHandler tests (includes session renewal):
362→```typescript
363→import {
364→ createMockAuthClient,
365→ createMockRouteConfig,
366→ createMockAuthenticatedContext,
367→ mockOperationSuccess,
368→ mockRenewSessionSuccess,
369→ getUnitConfig,
370→} from '../mocks';
371→
372→describe('RequestHandler', () => {
373→ const unitConfig = getUnitConfig();
374→ const mockAuthClient = createMockAuthClient();
375→
376→ it('should renew session on success', async () => {
377→ const routeConfig = createMockRouteConfig({ handlerType: 'query' });
378→ const context = createMockAuthenticatedContext();
379→ mockOperationSuccess(routeConfig, { id: 1 });
380→ mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
381→ // RequestHandler uses 4 args: options, config, authClient, logger
382→ const handler = new RequestHandler(options, unitConfig, mockAuthClient, mockLogger);
383→ // ...
384→ });
385→});
386→```
387→
388→QueryHandler/MutationHandler tests (no authClient, no renewal):
389→```typescript
390→import {
391→ createMockRouteConfig,
392→ createMockAuthenticatedContext,
393→ mockOperationSuccess,
394→ getUnitConfig,
395→} from '../mocks';
396→
397→describe('QueryHandler', () => {
398→ const unitConfig = getUnitConfig();
399→
400→ it('should execute operation', async () => {
401→ const routeConfig = createMockRouteConfig();
402→ const context = createMockAuthenticatedContext();
403→ mockOperationSuccess(routeConfig, { id: 1 });
404→ // QueryHandler uses 3 args: options, config, logger
405→ const handler = new QueryHandler(options, unitConfig, undefined);
406→ // ...
407→ });
408→});
409→```
410→
411→## Related Packages
412→
413→**Depends On:**
414→
415→- `cwc-backend-utils` (workspace) - SqlClient, Logger, Express service factory
416→- `cwc-schema` (workspace) - Data validation
417→- `cwc-types` (workspace) - Entity types, RuntimeEnvironment
418→
419→**Coordinates With:**
420→
421→- `cwc-auth` - JWT validation, session renewal
422→- `cwc-sql` - Database operations
423→
424→**Consumed By:**
425→
426→- `cwc-website` - Frontend web application
427→
<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 cwc-api CLAUDE.md is mostly up to date from the earlier refactoring, but I need to add responseUtils.ts to the Handler Architecture files list.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/packages/cwc-api/CLAUDE.md",
"old_string": "**Files:**\n- `handler.types.ts` - Type definitions for handlers, operations, and responses\n- `RequestHandler.ts` - Entry point: route access check, delegation, session renewal\n- `QueryHandler.ts` - Read operations with pagination support\n- `MutationHandler.ts` - Write operations\n- `index.ts` - Exports",
"new_string": "**Files:**\n- `handler.types.ts` - Type definitions for handlers, operations, and responses\n- `RequestHandler.ts` - Entry point: route access check, delegation, session renewal\n- `QueryHandler.ts` - Read operations with pagination support\n- `MutationHandler.ts` - Write operations\n- `responseUtils.ts` - Shared response creation utilities (error responses, status code mapping)\n- `index.ts` - Exports"
}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:
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→```Grep
{
"pattern": "DRY|duplicat|shared.*util",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/CLAUDE.md",
"output_mode": "content"
}Tool Result
30: - Consider both direct changes and indirect impacts (shared types, utilities, etc.) 258: - `cwc-utils`: shared utilities for browser and Node.js (profanity checking, validation helpers, etc.) 260: - `cwc-backend-utils`: shared Node.js utilities that backend/api packages will consume 431:- Shared types in `cwc-types` package; duplicating types in separate projects leads to inconsistencies, incompatibility, confusion, and extra work
The root CLAUDE.md already covers shared utilities and avoiding duplication. The main session learnings are cwc-api specific and now documented:
- Handler Architecture - Updated with RequestHandler (renamed from RouteHandler), centralized session renewal strategy
- responseUtils.ts - Added to the files list as shared response creation utilities
The documentation is now up to date. The key patterns from this session:
- Session renewal centralized in RequestHandler (renews on all requests except 401/403)
- Shared utility extraction within a package (
responseUtils.ts) to avoid handler duplication
I'm not sure if we added the 1 to 1 naming related to runtime environments to a memory file in the past - probably not. Let's add a note about that in the root memory file - keeping the naming consistent makes it easier for me to both understand the code and improves the searchability (we touched on that topic before).
Let me check what's currently documented about runtime environments and naming conventions.
Grep
{
"pattern": "RUNTIME_ENVIRONMENT|RuntimeEnvironment",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/CLAUDE.md",
"output_mode": "content",
"-B": 2,
"-A": 5
}Tool Result
498-### Environment Configuration 499- 500:**NODE_ENV vs RUNTIME_ENVIRONMENT:** 501- 502-| Variable | Purpose | Set By | Values | 503-|----------|---------|--------|--------| 504-| `NODE_ENV` | Build-time behavior | npm/bundlers | `development`, `production`, `test` | 505:| `RUNTIME_ENVIRONMENT` | Application runtime behavior | CWC deployment | `dev`, `test`, `prod`, `unit`, `e2e` | 506- 507-**NODE_ENV (npm/Node.js ecosystem):** 508-- Controls build optimizations (minification, tree-shaking) 509-- Affects dependency installation behavior 510-- CWC does NOT read this in application config 511- 512:**RUNTIME_ENVIRONMENT (CWC application):** 513-- Controls application behavior (email sending, error verbosity, feature flags) 514:- Type: `RuntimeEnvironment` from cwc-types 515-- CWC config system reads this via `loadConfig()` 516- 517-**Rules:** 518:- Test scripts: `RUNTIME_ENVIRONMENT=unit jest` (not `NODE_ENV=unit`) 519:- Backend config: Always read `RUNTIME_ENVIRONMENT`, never `NODE_ENV` 520-- Each package reads configuration from `.env` file tailored to the runtime environment 521- 522-## Development Process 523- 524-### Tool, Framework, Version selection
I see the environment configuration section. Let me add the naming consistency pattern there.
Edit
{
"file_path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/CLAUDE.md",
"old_string": "**Rules:**\n- Test scripts: `RUNTIME_ENVIRONMENT=unit jest` (not `NODE_ENV=unit`)\n- Backend config: Always read `RUNTIME_ENVIRONMENT`, never `NODE_ENV`\n- Each package reads configuration from `.env` file tailored to the runtime environment",
"new_string": "**Rules:**\n- Test scripts: `RUNTIME_ENVIRONMENT=unit jest` (not `NODE_ENV=unit`)\n- Backend config: Always read `RUNTIME_ENVIRONMENT`, never `NODE_ENV`\n- Each package reads configuration from `.env` file tailored to the runtime environment\n\n**1-to-1 Naming Convention:**\n\nUse consistent naming across all runtime environment references for searchability and clarity:\n\n| Runtime Environment | Env File | Config Flag | Mock Function |\n|---------------------|----------|-------------|---------------|\n| `dev` | `dev.cwc-*.env` | `isDevelopment` | `createMockDevConfig()` |\n| `prod` | `prod.cwc-*.env` | `isProduction` | `createMockProdConfig()` |\n| `unit` | `unit.cwc-*.env` | `isUnitTest` | `createMockUnitConfig()` |\n| `e2e` | `e2e.cwc-*.env` | `isE2ETest` | `createMockE2EConfig()` |\n| `test` | `test.cwc-*.env` | `isTest` | `createMockTestConfig()` |\n\nThis consistency enables searching for `Dev` or `Prod` to find all related code paths."
}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/CLAUDE.md",
"offset": 490,
"limit": 50
}Tool Result
490→### Cloud-Agnostic Microservices 491→ 492→CWC uses a microservices architecture deployed as Docker containers potentially deployed across multiple datacenters. 493→ 494→1. Vendor lock-in is a real business risk. Cloud providers can change pricing, deny service access, or deprecate features at any time. 495→2. Cloud-agnostic microservices architecture allows switching hosting providers with minimal effort. 496→3. Preparation for Scale - can scale by adding infrastructure (more containers, load balancers) rather than rewriting code and specific services can be scaled based on actual load patterns 497→ 498→### Environment Configuration 499→ 500→**NODE_ENV vs RUNTIME_ENVIRONMENT:** 501→ 502→| Variable | Purpose | Set By | Values | 503→|----------|---------|--------|--------| 504→| `NODE_ENV` | Build-time behavior | npm/bundlers | `development`, `production`, `test` | 505→| `RUNTIME_ENVIRONMENT` | Application runtime behavior | CWC deployment | `dev`, `test`, `prod`, `unit`, `e2e` | 506→ 507→**NODE_ENV (npm/Node.js ecosystem):** 508→- Controls build optimizations (minification, tree-shaking) 509→- Affects dependency installation behavior 510→- CWC does NOT read this in application config 511→ 512→**RUNTIME_ENVIRONMENT (CWC application):** 513→- Controls application behavior (email sending, error verbosity, feature flags) 514→- Type: `RuntimeEnvironment` from cwc-types 515→- CWC config system reads this via `loadConfig()` 516→ 517→**Rules:** 518→- Test scripts: `RUNTIME_ENVIRONMENT=unit jest` (not `NODE_ENV=unit`) 519→- Backend config: Always read `RUNTIME_ENVIRONMENT`, never `NODE_ENV` 520→- Each package reads configuration from `.env` file tailored to the runtime environment 521→ 522→## Development Process 523→ 524→### Tool, Framework, Version selection 525→ 526→- mainstream, widely accepted, and thoroughly tested & proven tools only 527→- the desire is to use the latest stable versions of the various tools 528→ 529→### Adopt a "roll-your-own" mentality 530→ 531→- we want to minimize the number of unnecessary dependencies to avoid headaches when upgrading our core tech stack 532→- when it makes sense, we will build our own components and utilities rather than relying on a 3rd party package 533→ 534→### Code Review Workflow Patterns 535→ 536→**CRITICAL: When the developer provides comprehensive code review feedback and requests step-by-step discussion.** 537→ 538→#### Developer Should Continue Providing Comprehensive Feedback Lists 539→ <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/CLAUDE.md",
"old_string": "**Rules:**\n- Test scripts: `RUNTIME_ENVIRONMENT=unit jest` (not `NODE_ENV=unit`)\n- Backend config: Always read `RUNTIME_ENVIRONMENT`, never `NODE_ENV`\n- Each package reads configuration from `.env` file tailored to the runtime environment\n\n## Development Process",
"new_string": "**Rules:**\n- Test scripts: `RUNTIME_ENVIRONMENT=unit jest` (not `NODE_ENV=unit`)\n- Backend config: Always read `RUNTIME_ENVIRONMENT`, never `NODE_ENV`\n- Each package reads configuration from `.env` file tailored to the runtime environment\n\n**1-to-1 Naming Convention:**\n\nUse consistent naming across all runtime environment references for searchability and clarity:\n\n| Runtime Environment | Env File | Config Flag | Mock Function |\n|---------------------|----------|-------------|---------------|\n| `dev` | `dev.cwc-*.env` | `isDevelopment` | `createMockDevConfig()` |\n| `prod` | `prod.cwc-*.env` | `isProduction` | `createMockProdConfig()` |\n| `unit` | `unit.cwc-*.env` | `isUnitTest` | `createMockUnitConfig()` |\n| `e2e` | `e2e.cwc-*.env` | `isE2ETest` | `createMockE2EConfig()` |\n| `test` | `test.cwc-*.env` | `isTest` | `createMockTestConfig()` |\n\nThis consistency enables searching for `Dev` or `Prod` to find all related code paths.\n\n## Development Process"
}Tool Result
The file /Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application/CLAUDE.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file: 513→- Controls application behavior (email sending, error verbosity, feature flags) 514→- Type: `RuntimeEnvironment` from cwc-types 515→- CWC config system reads this via `loadConfig()` 516→ 517→**Rules:** 518→- Test scripts: `RUNTIME_ENVIRONMENT=unit jest` (not `NODE_ENV=unit`) 519→- Backend config: Always read `RUNTIME_ENVIRONMENT`, never `NODE_ENV` 520→- Each package reads configuration from `.env` file tailored to the runtime environment 521→ 522→**1-to-1 Naming Convention:** 523→ 524→Use consistent naming across all runtime environment references for searchability and clarity: 525→ 526→| Runtime Environment | Env File | Config Flag | Mock Function | 527→|---------------------|----------|-------------|---------------| 528→| `dev` | `dev.cwc-*.env` | `isDevelopment` | `createMockDevConfig()` | 529→| `prod` | `prod.cwc-*.env` | `isProduction` | `createMockProdConfig()` | 530→| `unit` | `unit.cwc-*.env` | `isUnitTest` | `createMockUnitConfig()` | 531→| `e2e` | `e2e.cwc-*.env` | `isE2ETest` | `createMockE2EConfig()` | 532→| `test` | `test.cwc-*.env` | `isTest` | `createMockTestConfig()` | 533→ 534→This consistency enables searching for `Dev` or `Prod` to find all related code paths. 535→ 536→## Development Process 537→ 538→### Tool, Framework, Version selection 539→ 540→- mainstream, widely accepted, and thoroughly tested & proven tools only
Done. Added the 1-to-1 Naming Convention table to the Environment Configuration section in the root CLAUDE.md. This documents the pattern of keeping naming consistent across:
- Environment files (
dev.cwc-*.env) - Config flags (
isDevelopment) - Mock functions (
createMockDevConfig())
This makes it easy to search for Dev or Prod and find all related code paths.
I see that I broke my own rule on the env name when I created isProduction and isDevelopment etc. I'm going to commit this PR, while I do that, can you all of the places in the codebase that would need to be updated if we decide to change to isProd, isDev, etc?
I'll search for all the config flag usages across the codebase.
Grep
{
"pattern": "isDevelopment|isProduction|isUnitTest|isE2ETest|isTest",
"path": "/Users/jeffbazinet/coding-with-claude/source/coding-with-claude-application",
"output_mode": "content",
"-n": true
}Tool Result
CLAUDE.md:528:| `dev` | `dev.cwc-*.env` | `isDevelopment` | `createMockDevConfig()` |
CLAUDE.md:529:| `prod` | `prod.cwc-*.env` | `isProduction` | `createMockProdConfig()` |
CLAUDE.md:530:| `unit` | `unit.cwc-*.env` | `isUnitTest` | `createMockUnitConfig()` |
CLAUDE.md:531:| `e2e` | `e2e.cwc-*.env` | `isE2ETest` | `createMockE2EConfig()` |
CLAUDE.md:532:| `test` | `test.cwc-*.env` | `isTest` | `createMockTestConfig()` |
packages/cwc-auth/src/index.ts:34: isDevelopment: authConfig.isDevelopment,
packages/cwc-auth/src/index.ts:35: isTest: authConfig.isTest,
packages/cwc-auth/src/index.ts:36: isProduction: authConfig.isProduction,
packages/cwc-auth/src/index.ts:37: isUnitTest: authConfig.isUnitTest,
packages/cwc-auth/src/index.ts:154: if (!config.isProduction) {
packages/cwc-auth/src/__tests__/config.test.ts:89: it('should correctly derive isProduction', () => {
packages/cwc-auth/src/__tests__/config.test.ts:99: const isProduction = test.env === 'prod';
packages/cwc-auth/src/__tests__/config.test.ts:100: expect(isProduction).toBe(test.expected);
packages/cwc-auth/src/__tests__/config.test.ts:104: it('should correctly derive isDevelopment', () => {
packages/cwc-auth/src/__tests__/config.test.ts:114: const isDevelopment = test.env === 'dev';
packages/cwc-auth/src/__tests__/config.test.ts:115: expect(isDevelopment).toBe(test.expected);
packages/cwc-auth/src/__tests__/config.test.ts:119: it('should correctly derive isUnitTest', () => {
packages/cwc-auth/src/__tests__/config.test.ts:129: const isUnitTest = test.env === 'unit';
packages/cwc-auth/src/__tests__/config.test.ts:130: expect(isUnitTest).toBe(test.expected);
packages/cwc-auth/src/auth/createRenewSession.ts:37: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createRenewSession.ts:47: const errorCode = config.isDevelopment
packages/cwc-auth/src/auth/createRenewSession.ts:54: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createRenewSession.ts:79: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createRenewSession.ts:116: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createRenewSession.ts:148: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createRenewSession.ts:206: errorDetail: config.isDevelopment
ai-prompt-history/010-create-cwc-sql.md:40:- isProduction (boolean)
ai-prompt-history/010-create-cwc-sql.md:41:- isDevelopment (boolean)
ai-prompt-history/010-create-cwc-sql.md:42:- isTest (boolean)
ai-prompt-history/010-create-cwc-sql.md:43:- isUnitTest (boolean)
packages/cwc-auth/src/__tests__/mocks/config.mock.ts:30: isProduction: true,
packages/cwc-auth/src/__tests__/mocks/config.mock.ts:31: isDevelopment: false,
packages/cwc-auth/src/__tests__/mocks/config.mock.ts:32: isTest: false,
packages/cwc-auth/src/__tests__/mocks/config.mock.ts:33: isUnitTest: false,
packages/cwc-auth/src/__tests__/mocks/config.mock.ts:47: isProduction: false,
packages/cwc-auth/src/__tests__/mocks/config.mock.ts:48: isDevelopment: false,
packages/cwc-auth/src/__tests__/mocks/config.mock.ts:49: isTest: false,
packages/cwc-auth/src/__tests__/mocks/config.mock.ts:50: isUnitTest: true,
packages/cwc-auth/src/auth/createInvalidateSession.ts:30: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createInvalidateSession.ts:40: const errorCode = config.isDevelopment
packages/cwc-auth/src/auth/createInvalidateSession.ts:47: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createInvalidateSession.ts:72: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createInvalidateSession.ts:92: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createInvalidateSession.ts:119: errorDetail: config.isDevelopment
packages/cwc-storage/CLAUDE.md:87:- Only required in production (`isProduction: true`)
ai-prompt-history/014-cwc-auth.md:22:isProduction based on runtimeEnvironment value
ai-prompt-history/014-cwc-auth.md:23:isDevelopment based on runtimeEnvironment value
ai-prompt-history/014-cwc-auth.md:24:isTest based on runtimeEnvironment value
ai-prompt-history/014-cwc-auth.md:25:isUnitTest based on runtimeEnvironment value
packages/cwc-storage/src/index.ts:29: isDevelopment: storageConfig.isDevelopment,
packages/cwc-storage/src/index.ts:30: isTest: storageConfig.isTest,
packages/cwc-storage/src/index.ts:31: isProduction: storageConfig.isProduction,
packages/cwc-storage/src/index.ts:32: isUnitTest: storageConfig.isUnitTest,
packages/cwc-storage/src/index.ts:122: if (!config.isProduction) {
packages/cwc-auth/src/auth/createAuthenticateUser.ts:46: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createAuthenticateUser.ts:94: errorCode: config.isDevelopment ? internalReason as 'USER_NOT_FOUND' | 'USER_DISABLED' | 'INVALID_PASSWORD' : 'INVALID_CREDENTIALS',
packages/cwc-auth/src/auth/createAuthenticateUser.ts:95: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createAuthenticateUser.ts:136: errorDetail: config.isDevelopment
packages/cwc-auth/src/auth/createAuthenticateUser.ts:202: errorDetail: config.isDevelopment
packages/cwc-backend-utils/src/backendUtils.types.ts:11: isDevelopment: boolean;
packages/cwc-backend-utils/src/backendUtils.types.ts:12: isTest: boolean;
packages/cwc-backend-utils/src/backendUtils.types.ts:13: isProduction: boolean;
packages/cwc-backend-utils/src/backendUtils.types.ts:14: isUnitTest: boolean;
packages/cwc-backend-utils/README.md:268:**Development (`isDevelopment: true`):**
packages/cwc-backend-utils/README.md:274:**Test (`isTest: true`):**
packages/cwc-backend-utils/README.md:280:**Production (`isProduction: true`):**
packages/cwc-backend-utils/README.md:351: isDevelopment: boolean;
packages/cwc-backend-utils/README.md:352: isTest: boolean;
packages/cwc-backend-utils/README.md:353: isProduction: boolean;
packages/cwc-backend-utils/README.md:354: isUnitTest: boolean;
packages/cwc-backend-utils/README.md:468: isDevelopment: process.env.NODE_ENV === 'dev',
packages/cwc-backend-utils/README.md:469: isTest: process.env.NODE_ENV === 'test',
packages/cwc-backend-utils/README.md:470: isProduction: process.env.NODE_ENV === 'prod',
packages/cwc-backend-utils/README.md:471: isUnitTest: process.env.NODE_ENV === 'unit',
packages/cwc-backend-utils/src/express/cors.ts:20: if (config.isDevelopment || config.isEndToEnd) {
packages/cwc-backend-utils/src/express/cors.ts:30: if (config.isTest) {
packages/cwc-backend-utils/src/express/cors.ts:44: if (config.isProduction) {
packages/cwc-backend-utils/src/SqlClientKeysUtil.ts:42: const runningLocally = config.isDevelopment || config.isUnitTest || config.isEndToEnd;
packages/cwc-sql/src/database/connection.ts:25: const useSSL = config.isProduction && !isLocalHost;
packages/cwc-storage/src/config/config.types.ts:17: isProduction: boolean;
packages/cwc-storage/src/config/config.types.ts:18: isDevelopment: boolean;
packages/cwc-storage/src/config/config.types.ts:19: isTest: boolean;
packages/cwc-storage/src/config/config.types.ts:20: isUnitTest: boolean;
packages/cwc-storage/src/config/loadConfig.ts:35: const isProduction = runtimeEnvironment === 'prod';
packages/cwc-storage/src/config/loadConfig.ts:36: const isDevelopment = runtimeEnvironment === 'dev';
packages/cwc-storage/src/config/loadConfig.ts:37: const isTest = runtimeEnvironment === 'test';
packages/cwc-storage/src/config/loadConfig.ts:38: const isUnitTest = runtimeEnvironment === 'unit';
packages/cwc-storage/src/config/loadConfig.ts:45: isProduction,
packages/cwc-storage/src/config/loadConfig.ts:46: isDevelopment,
packages/cwc-storage/src/config/loadConfig.ts:47: isTest,
packages/cwc-storage/src/config/loadConfig.ts:48: isUnitTest,
packages/cwc-auth/src/config/config.types.ts:18: isProduction: boolean;
packages/cwc-auth/src/config/config.types.ts:19: isDevelopment: boolean;
packages/cwc-auth/src/config/config.types.ts:20: isTest: boolean;
packages/cwc-auth/src/config/config.types.ts:21: isUnitTest: boolean;
packages/cwc-sql/src/index.ts:23: isDevelopment: sqlConfig.isDevelopment,
packages/cwc-sql/src/index.ts:24: isTest: sqlConfig.isTest,
packages/cwc-sql/src/index.ts:25: isProduction: sqlConfig.isProduction,
packages/cwc-sql/src/index.ts:26: isUnitTest: sqlConfig.isUnitTest,
packages/cwc-sql/src/index.ts:32: devCorsOrigin: sqlConfig.isDevelopment ? sqlConfig.corsOrigin : '',
packages/cwc-sql/src/index.ts:183: if (!config.isProduction) {
packages/cwc-auth/src/config/loadConfig.ts:35: const isProduction = runtimeEnvironment === 'prod';
packages/cwc-auth/src/config/loadConfig.ts:36: const isDevelopment = runtimeEnvironment === 'dev';
packages/cwc-auth/src/config/loadConfig.ts:37: const isTest = runtimeEnvironment === 'test';
packages/cwc-auth/src/config/loadConfig.ts:38: const isUnitTest = runtimeEnvironment === 'unit';
packages/cwc-auth/src/config/loadConfig.ts:45: isProduction,
packages/cwc-auth/src/config/loadConfig.ts:46: isDevelopment,
packages/cwc-auth/src/config/loadConfig.ts:47: isTest,
packages/cwc-auth/src/config/loadConfig.ts:48: isUnitTest,
packages/cwc-sql/src/__tests__/config.test.ts:77: expect(config.isDevelopment).toBe(true);
packages/cwc-sql/src/__tests__/config.test.ts:78: expect(config.isProduction).toBe(false);
packages/cwc-sql/src/__tests__/config.test.ts:79: expect(config.isTest).toBe(false);
packages/cwc-sql/src/__tests__/config.test.ts:120: expect(config.isProduction).toBe(true);
packages/cwc-sql/src/__tests__/config.test.ts:121: expect(config.isDevelopment).toBe(false);
packages/cwc-sql/src/__tests__/config.test.ts:122: expect(config.isTest).toBe(false);
packages/cwc-sql/src/__tests__/queryCache.test.ts:13: isProduction: false,
packages/cwc-sql/src/__tests__/queryCache.test.ts:14: isDevelopment: false,
packages/cwc-sql/src/__tests__/queryCache.test.ts:15: isTest: false,
packages/cwc-sql/src/__tests__/queryCache.test.ts:16: isUnitTest: true,
packages/cwc-backend-utils/CLAUDE.md:76:**Development (`isDevelopment: true`):**
packages/cwc-backend-utils/CLAUDE.md:82:**Test (`isTest: true`):**
packages/cwc-backend-utils/CLAUDE.md:88:**Production (`isProduction: true`):**
packages/cwc-sql/src/config/config.types.ts:19: isProduction: boolean;
packages/cwc-sql/src/config/config.types.ts:20: isDevelopment: boolean;
packages/cwc-sql/src/config/config.types.ts:21: isTest: boolean;
packages/cwc-sql/src/config/config.types.ts:22: isUnitTest: boolean;
packages/cwc-storage/src/apis/StorageApiV1/index.ts:89: return this.config.isProduction;
packages/cwc-sql/src/config/loadConfig.ts:95: const isProduction = runtimeEnvironment === 'prod';
packages/cwc-sql/src/config/loadConfig.ts:96: const isDevelopment = runtimeEnvironment === 'dev';
packages/cwc-sql/src/config/loadConfig.ts:97: const isTest = runtimeEnvironment === 'test';
packages/cwc-sql/src/config/loadConfig.ts:98: const isUnitTest = runtimeEnvironment === 'unit';
packages/cwc-sql/src/config/loadConfig.ts:105: isProduction,
packages/cwc-sql/src/config/loadConfig.ts:106: isDevelopment,
packages/cwc-sql/src/config/loadConfig.ts:107: isTest,
packages/cwc-sql/src/config/loadConfig.ts:108: isUnitTest,
packages/cwc-auth/src/apis/LogonApiV1/LogonHandler.ts:86: if (this.config.isDevelopment) {
packages/cwc-auth/src/apis/LogonApiV1/LogonHandler.ts:109: if (this.config.isDevelopment) {
packages/cwc-auth/src/apis/ServiceApiV1/ServiceHandler.ts:69: if (this.config.isDevelopment) {
packages/cwc-auth/src/apis/ServiceApiV1/ServiceHandler.ts:92: if (this.config.isDevelopment) {
packages/cwc-configuration-helper/README.md:126: isProduction: boolean;
packages/cwc-configuration-helper/README.md:127: isDevelopment: boolean;
packages/cwc-configuration-helper/README.md:128: isTest: boolean;
packages/cwc-configuration-helper/README.md:129: isUnitTest: boolean;
packages/cwc-configuration-helper/src/nameConverter.ts:13: * - isProduction -> IS_PRODUCTION
packages/cwc-configuration-helper/src/nameConverter.ts:43: * - IS_PRODUCTION -> isProduction
packages/cwc-api/CLAUDE.md:354:| `createMockDevConfig(overrides)` | Mock config with isDevelopment: true |
packages/cwc-api/CLAUDE.md:355:| `createMockProdConfig(overrides)` | Mock config with isProduction: true |
packages/cwc-api/CLAUDE.md:356:| `createMockUnitConfig(overrides)` | Mock config with isUnitTest: true |
packages/cwc-configuration-helper/src/typeParser.ts:170: // Skip computed/derived properties (isProduction, isDevelopment, etc.)
packages/cwc-configuration-helper/src/typeParser.ts:343: 'isProduction',
packages/cwc-configuration-helper/src/typeParser.ts:344: 'isDevelopment',
packages/cwc-configuration-helper/src/typeParser.ts:345: 'isTest',
packages/cwc-configuration-helper/src/typeParser.ts:346: 'isUnitTest',
packages/cwc-configuration-helper/CLAUDE.md:31: isProduction: boolean;
packages/cwc-configuration-helper/CLAUDE.md:32: isDevelopment: boolean;
packages/cwc-configuration-helper/CLAUDE.md:33: isTest: boolean;
packages/cwc-configuration-helper/CLAUDE.md:34: isUnitTest: boolean;
packages/cwc-configuration-helper/CLAUDE.md:81:- `isProduction`, `isDevelopment`, `isTest`, `isUnitTest`, `isEndToEnd`
packages/cwc-api/src/handlers/responseUtils.ts:45: ...(config.isDevelopment && reason ? { errorDetail: reason } : {}),
packages/cwc-api/src/handlers/responseUtils.ts:82: ...(config.isDevelopment && error instanceof Error
packages/cwc-api/src/handlers/RequestHandler.ts:167: ...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts:211: it('should include error detail when isDevelopment is true', async () => {
packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts:216: it('should include error detail when isDevelopment is true', async () => {
packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts:252: // mockProdConfig has isDevelopment: false
packages/cwc-api/src/config/loadConfig.ts:35: const isProduction = runtimeEnvironment === 'prod';
packages/cwc-api/src/config/loadConfig.ts:36: const isDevelopment = runtimeEnvironment === 'dev';
packages/cwc-api/src/config/loadConfig.ts:37: const isTest = runtimeEnvironment === 'test';
packages/cwc-api/src/config/loadConfig.ts:38: const isUnitTest = runtimeEnvironment === 'unit';
packages/cwc-api/src/config/loadConfig.ts:45: isProduction,
packages/cwc-api/src/config/loadConfig.ts:46: isDevelopment,
packages/cwc-api/src/config/loadConfig.ts:47: isTest,
packages/cwc-api/src/config/loadConfig.ts:48: isUnitTest,
packages/cwc-api/src/__tests__/config.test.ts:93: it('should correctly derive isProduction', () => {
packages/cwc-api/src/__tests__/config.test.ts:103: const isProduction = test.env === 'prod';
packages/cwc-api/src/__tests__/config.test.ts:104: expect(isProduction).toBe(test.expected);
packages/cwc-api/src/__tests__/config.test.ts:108: it('should correctly derive isDevelopment', () => {
packages/cwc-api/src/__tests__/config.test.ts:118: const isDevelopment = test.env === 'dev';
packages/cwc-api/src/__tests__/config.test.ts:119: expect(isDevelopment).toBe(test.expected);
packages/cwc-api/src/__tests__/config.test.ts:123: it('should correctly derive isTest', () => {
packages/cwc-api/src/__tests__/config.test.ts:133: const isTest = test.env === 'test';
packages/cwc-api/src/__tests__/config.test.ts:134: expect(isTest).toBe(test.expected);
packages/cwc-api/src/__tests__/config.test.ts:138: it('should correctly derive isUnitTest', () => {
packages/cwc-api/src/__tests__/config.test.ts:148: const isUnitTest = test.env === 'unit';
packages/cwc-api/src/__tests__/config.test.ts:149: expect(isUnitTest).toBe(test.expected);
packages/cwc-api/src/config/config.types.ts:18: isProduction: boolean;
packages/cwc-api/src/config/config.types.ts:19: isDevelopment: boolean;
packages/cwc-api/src/config/config.types.ts:20: isTest: boolean;
packages/cwc-api/src/config/config.types.ts:21: isUnitTest: boolean;
packages/cwc-api/src/index.ts:42: isDevelopment: apiConfig.isDevelopment,
packages/cwc-api/src/index.ts:43: isTest: apiConfig.isTest,
packages/cwc-api/src/index.ts:44: isProduction: apiConfig.isProduction,
packages/cwc-api/src/index.ts:45: isUnitTest: apiConfig.isUnitTest,
packages/cwc-api/src/index.ts:171: if (!config.isProduction) {
packages/cwc-api/src/__tests__/mocks/config.mock.ts:28: isProduction: true,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:29: isDevelopment: false,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:30: isTest: false,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:31: isUnitTest: false,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:44: isProduction: false,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:45: isDevelopment: false,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:46: isTest: false,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:47: isUnitTest: true,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:56: * Has isDevelopment: true so error details are exposed
packages/cwc-api/src/__tests__/mocks/config.mock.ts:61: isProduction: false,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:62: isDevelopment: true,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:63: isTest: false,
packages/cwc-api/src/__tests__/mocks/config.mock.ts:64: isUnitTest: false,Here's a summary of all locations that would need updates if you rename to isProd, isDev, etc.:
Summary by Package
| Package | Files Affected | Usage Count |
|---|---|---|
cwc-api |
7 files | ~35 usages |
cwc-auth |
8 files | ~45 usages |
cwc-sql |
4 files | ~20 usages |
cwc-storage |
4 files | ~15 usages |
cwc-backend-utils |
4 files | ~15 usages |
cwc-configuration-helper |
3 files | ~10 usages |
| Root CLAUDE.md | 1 file | 5 usages |
Files by Category
Type Definitions (4 files):
packages/cwc-backend-utils/src/backendUtils.types.ts- shared interfacepackages/cwc-api/src/config/config.types.tspackages/cwc-auth/src/config/config.types.tspackages/cwc-sql/src/config/config.types.tspackages/cwc-storage/src/config/config.types.ts
Config Loaders (4 files):
packages/cwc-api/src/config/loadConfig.tspackages/cwc-auth/src/config/loadConfig.tspackages/cwc-sql/src/config/loadConfig.tspackages/cwc-storage/src/config/loadConfig.ts
Service Entry Points (4 files):
packages/cwc-api/src/index.tspackages/cwc-auth/src/index.tspackages/cwc-sql/src/index.tspackages/cwc-storage/src/index.ts
Business Logic using isDevelopment (error details):
packages/cwc-api/src/handlers/responseUtils.tspackages/cwc-api/src/handlers/RequestHandler.tspackages/cwc-auth/src/auth/createRenewSession.ts(7 usages)packages/cwc-auth/src/auth/createInvalidateSession.ts(6 usages)packages/cwc-auth/src/auth/createAuthenticateUser.ts(4 usages)packages/cwc-auth/src/apis/LogonApiV1/LogonHandler.tspackages/cwc-auth/src/apis/ServiceApiV1/ServiceHandler.ts
Business Logic using isProduction:
packages/cwc-sql/src/database/connection.ts(SSL check)packages/cwc-storage/src/apis/StorageApiV1/index.tspackages/cwc-backend-utils/src/express/cors.tspackages/cwc-backend-utils/src/SqlClientKeysUtil.ts
Test Files & Mocks (~10 files):
- Config tests in each package
config.mock.tsfiles in cwc-api, cwc-auth
Documentation (5 files):
- Root
CLAUDE.md packages/cwc-api/CLAUDE.mdpackages/cwc-backend-utils/CLAUDE.mdpackages/cwc-backend-utils/README.mdpackages/cwc-configuration-helper/CLAUDE.mdpackages/cwc-configuration-helper/README.md
Configuration Helper (special handling):
packages/cwc-configuration-helper/src/typeParser.ts- hardcoded skip list for derived properties
Total: ~140+ occurrences across ~30 files
This is a significant rename but mostly mechanical. The cwc-configuration-helper typeParser has a hardcoded array of these property names that would need updating.
Session 21: cwc api phase 3
21 files modified
CLAUDE.md
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
CRITICAL: Role and Professional Standards
Claude Code's role on this project is to serve as a "highly experienced senior software engineer and application architect" with "in-depth understanding of the CWC codebase, conventions, and rules."
It is assumed, and imperative, that as a senior engineer, you will do your due diligence:
Required Due Diligence for Every Task
Before proposing any design, architecture, or implementation:
Read all relevant package CLAUDE.md files
- If touching database → read
packages/cwc-database/CLAUDE.md - If defining/modifying types → read
packages/cwc-types/CLAUDE.md - If working with any package → read that package's CLAUDE.md file
- Package-specific conventions MUST be followed
- If touching database → read
Examine existing code in all affected packages
- Check for existing types, schemas, or utilities before proposing new ones
- Understand established patterns and conventions
- Identify dependencies between packages
Understand the full scope
- Identify all packages affected by the feature or change
- Consider both direct changes and indirect impacts (shared types, utilities, etc.)
- Plan across package boundaries, not in isolation
Cross-Package Feature Planning
When working on features that span multiple packages:
- Discovery phase first - Survey the landscape before designing
- Read documentation - All relevant package CLAUDE.md files
- Examine existing implementations - Check for related code/patterns
- Design within constraints - Follow established conventions
- Present context with design - Show what you reviewed and how your design follows patterns
This is not optional. The developer should not have to repeatedly point out missed conventions or overlooked existing code. Professional engineers build on institutional knowledge rather than reinventing or contradicting established patterns.
Package-Specific Documentation
Claude Code automatically loads all CLAUDE.md files recursively. When working in a specific package or on package-specific tasks, refer to these package documentation files for detailed guidance:
cwc-types →
packages/cwc-types/CLAUDE.md- Type generation from database schema
- Entity type patterns (Strict Base + Partial)
- Union type naming conventions
- Request-scoped caching patterns
cwc-database →
packages/cwc-database/CLAUDE.md- Database schema conventions
- Table/index/view naming patterns
- Migration script patterns
- Database design patterns (JWT tables, multi-step processes, etc.)
cwc-deployment →
packages/cwc-deployment/CLAUDE.md- Deployment system architecture
- SSH-based deployment workflows
- Docker container management
- Troubleshooting and error handling
cwc-schema →
packages/cwc-schema/CLAUDE.md- Schema definition patterns
- Runtime validation functions
- Hybrid validation with Zod
- Column type definitions
cwc-utils →
packages/cwc-utils/CLAUDE.md- Shared utilities (browser + Node.js)
- Profanity checking and content filtering
- Cross-platform compatibility guidelines
- Adding new utilities
Project Overview
What is codingwithclaude?
A multi-tenant developer publishing platform: a dynamic, real-time publishing platform that serves as both a public feed for developer content and a private dashboard for external developers (users of the app) to manage and publish their own technical blog posts, organized by "Projects."
Project name & aliases
In this document and prompts from the developer, all of these names or phrases are assumed to refer to the project:
coding-with-claude-applicationcodingwithclaudecoding-with-claudeCWCorcwc
Proactive Documentation Philosophy
CRITICAL: This file is a living knowledge base that must be continuously updated.
As Claude Code works with the developer, it is EXPECTED to proactively capture all learnings, patterns, critical instructions, and feedback in this CLAUDE.md file WITHOUT being reminded. This is a professional partnership where:
- Every gap discovered during planning or analysis → Document the pattern to prevent future occurrences
- Every critical instruction from the developer → Add to relevant sections immediately
- Every "I forgot to do X" moment → Create a checklist or rule to prevent repetition
- Every architectural pattern learned → Document it for consistency
- Every planning session insight → Capture the learning before implementation begins
When to update CLAUDE.md:
- DURING planning sessions - This is where most learning happens through analysis, feedback, and corrections
- After receiving critical feedback - Document the expectation immediately
- After discovering a bug or oversight - Add checks/rules to prevent it
- After analysis reveals gaps - Document what to check in the future
- When the developer explains "this is how we do X" - Add it to the guide
- After implementing a new feature - Capture any additional patterns discovered during execution
Planning sessions are especially critical: The analysis, feedback, and corrections that happen during planning contain the most valuable learnings. Update CLAUDE.md with these insights BEFORE starting implementation, not after.
Professional expectation: The developer should not need to repeatedly point out the same oversights or remind Claude Code to document learnings. Like professional teammates, we learn from each interaction and build institutional knowledge.
Format: When updating this file, maintain clear structure, provide code examples where helpful, and organize related concepts together. Focus exclusively on information that helps Claude Code operate effectively during AI-assisted coding sessions.
Package-Specific Documentation: When learning package-specific patterns, update the appropriate package CLAUDE.md file, not this root file.
CLAUDE.md File Specification
Purpose: CLAUDE.md files are memory files for AI assistants (like Claude Code), NOT documentation for human developers.
What CLAUDE.md IS for:
- Architectural patterns and critical design decisions
- Code conventions, naming rules, and style preferences
- What to check during planning sessions
- Lessons learned and mistakes to avoid
- Project-specific security rules and compliance requirements
- Critical implementation patterns that must be followed
- "If you see X, always do Y" type rules
- Checklists for common operations
What CLAUDE.md is NOT for (belongs in README.md):
- API documentation and endpoint specifications
- Usage examples and tutorials for humans
- Setup and installation instructions
- General explanations and marketing copy
- Step-by-step guides and how-tos
- Detailed configuration walkthroughs
- Complete type definitions (already in code)
- Performance tuning guides for users
File Size Targets:
- Warning threshold: 40,000 characters per file (Claude Code performance degrades)
- Recommended: Keep under 500 lines when possible for fast loading
- Best practice: If a package CLAUDE.md approaches 300-400 lines, review for README-style content
- For large packages: Use concise bullet points; move examples to README
Content Guidelines:
- Be specific and actionable: "Use 2-space indentation" not "Format code properly"
- Focus on patterns: Show the pattern, explain when to use it
- Include context for decisions: Why this approach, not alternatives
- Use code examples sparingly: Only when pattern is complex
- Keep it scannable: Bullet points and clear headers
CLAUDE.md vs README.md:
| CLAUDE.md | README.md |
|---|---|
| For AI assistants | For human developers |
| Patterns and rules | Complete documentation |
| What to check/avoid | How to use and setup |
| Concise and focused | Comprehensive and detailed |
| Loaded on every session | Read when needed |
Documentation Organization in Monorepos
Critical learnings about Claude Code documentation structure:
Claude Code automatically loads all CLAUDE.md files recursively:
- Reads CLAUDE.md in current working directory
- Recurses upward to parent directories (stops at workspace root)
- Discovers nested CLAUDE.md files in subdirectories
- All files are loaded together - they complement, not replace each other
Package-specific CLAUDE.md is the standard pattern for monorepos:
- Root CLAUDE.md contains monorepo-wide conventions (tooling, git workflow, shared patterns)
- Package CLAUDE.md contains package-specific patterns (database schema, deployment, type generation)
- Working from any directory loads both root and relevant package docs automatically
Performance limit: 40,000 characters per file:
- Claude Code shows performance warning when CLAUDE.md exceeds 40k characters
- Solution: Split into package-specific files, not multiple files in
.claude/directory - Only CLAUDE.md files are automatically loaded; other
.mdfiles in.claude/are NOT
Optimize for AI-assisted coding, not human readers:
- Include patterns, conventions, code examples, and strict rules
- Include "what to check during planning" and "lessons learned" sections
- Exclude content primarily for human developers (marketing copy, general explanations)
- Focus on actionable information needed during coding sessions
When to create package CLAUDE.md:
- Package has unique architectural patterns
- Package has specific conventions (schema rules, deployment procedures)
- Package has domain-specific knowledge (auth flows, type generation)
- Package documentation would exceed ~500 lines in root file
File Access Restrictions and Security Boundaries
Claude Code operates under strict file access restrictions to protect sensitive data:
Workspace Boundaries
- Claude Code can ONLY access files within the monorepo root:
./coding-with-claude-application - No access to parent directories, system files, or files outside this workspace
- This is enforced by Claude Code's security model
Prohibited File Access
Claude Code is explicitly blocked from reading or writing:
Environment files:
.envfiles at any location.env.*files (e.g.,.env.local,.env.production,.env.dev)*.envfiles (e.g.,prod.cwc-sql.env,dev.cwc-storage.env,test.cwc-app.env)- Any variation of environment configuration files
Secret and credential files:
- Any directory named
secrets/,secret/, orprivate/ - Any directory with
secret,secrets, orprivatein its path - Any file with
secret,secrets,private, orcredentialsin its filename - Service account JSON files (
service-account-*.json) - Firebase configuration files (
google-services.json,GoogleService-Info.plist) - Any file matching
*credentials*.json
- Any directory named
Rationale:
- Prevents accidental exposure of API keys, database passwords, and authentication tokens
- Protects production credentials and service account keys
- Reduces risk of sensitive data being included in code examples or logs
- Enforces principle of least privilege
These restrictions are enforced in .claude/settings.json and cannot be overridden during a session.
Git Workflow
The developer handles all git operations manually. Claude should:
- Never initiate git commits, pushes, pulls, or any write operations
- Only use git for read-only informational purposes (status, diff, log, show)
- Not proactively suggest git operations unless explicitly asked
Git write operations are blocked in .claude/settings.json to enforce this workflow.
Architecture Overview
Monorepo Structure (future plan)
- root project:
/coding-with-claude-application - packages (apps, microservices, utilities):
cwc-types: shared TypeScript types to be used in all other packagescwc-utils: shared utilities for browser and Node.js (profanity checking, validation helpers, etc.)cwc-deployment: custom deployment CLI for SSH-based deployment to remote serverscwc-backend-utils: shared Node.js utilities that backend/api packages will consumecwc-website: public frontend end web applicationcwc-auth: authentication microservice, providing login, logout, signup, password reset, etc.cwc-api: the main data api used bycwc-websiteto read & write data, enforce auth, role-based access policies, and business rules/logiccwc-dashboard: an administrative web dashboard app for site owners to manage the app & datacwc-admin-api: the admin and data api used by thecwc-dashboardappcwc-database: database scripts to create tables, indexes, views, as well as insert configuration datacwc-schema: shared schema management library that may be used by frontend and backend packagescwc-sql: the only backend service that interacts directly with the database server, uses schema to dynamically generate sql statementscwc-e2e: a set of end-to-end tests
Tech Stack: to be determined as we build each package, update this documentation as we go.
Development Tooling & Infrastructure
Monorepo Management
pnpm v9.x + Turborepo v2.x
- pnpm workspaces for package management and dependency resolution
- Configured in
pnpm-workspace.yaml - Packages located in
packages/* - Uses content-addressable storage for disk efficiency
- Strict dependency resolution prevents phantom dependencies
- Configured in
- Turborepo for task orchestration and caching
- Configured in
turbo.json - Intelligent parallel execution based on dependency graph
- Local caching for faster rebuilds
- Pipeline tasks:
build,dev,test,lint,typecheck
- Configured in
Node.js Version
- Node.js 22 LTS (specified in
.nvmrc) - Required for all development and production environments
- Use
nvmfor version management
Code Quality Tools
TypeScript v5.4+
- Configured in
tsconfig.base.json - Strict mode enabled with enhanced type checking
- JavaScript explicitly disallowed (
allowJs: false) - Monorepo-optimized with composite projects
- Individual packages extend base config
Module Resolution: bundler
- Uses
"moduleResolution": "bundler"in tsconfig.base.json - Uses
"module": "ES2022"(required for bundler resolution) - Allows clean TypeScript imports without
.jsextensions- ✅ Correct:
import { Schema } from './types' - ❌ Not needed:
import { Schema } from './types.js'
- ✅ Correct:
- Still produces correct ES module output in compiled JavaScript
- Designed for TypeScript projects compiled by tsc or bundlers
Why bundler over NodeNext:
- Better DX: No
.jsextensions in TypeScript source files - Modern standard: Industry standard for TypeScript libraries and monorepos
- Same output: Still generates proper ES modules (.js files)
- No trade-offs: Type safety and module compatibility maintained
Note: Previously used "moduleResolution": "NodeNext" which required .js extensions per ES modules spec (e.g., import './types.js'). Switched to bundler in session 007 for cleaner imports across all packages.
ESLint v8.x with TypeScript
- Configured in
.eslintrc.json - Uses
@typescript-eslint/strictruleset - Enforces explicit function return types
- Prohibits
anytype and non-null assertions - Strict boolean expressions required
Prettier v3.x
- Configured in
.prettierrc.json - Standards:
- Single quotes
- 2-space indentation
- 100 character line width
- Trailing commas (ES5)
- LF line endings
Root Scripts
Run from monorepo root using pnpm:
pnpm build- Build all packages (parallel, cached)pnpm dev- Run all packages in dev modepnpm test- Run tests across all packages (parallel, cached)pnpm lint- Lint all packages (parallel, cached)pnpm typecheck- Type-check all packages (parallel, cached)pnpm format- Format all files with Prettierpnpm format:check- Check formatting without changes
Development Workflow
Before starting work:
- Ensure Node 22 is active:
nvm use - Install dependencies:
pnpm install
- Ensure Node 22 is active:
During development:
- Run dev mode:
pnpm dev(in specific package or root) - Format code:
pnpm format
- Run dev mode:
Before committing:
- Type-check:
pnpm typecheck - Lint:
pnpm lint - Format check:
pnpm format:check - Run tests:
pnpm test
- Type-check:
Package Creation Conventions
When creating a new package in the monorepo:
Version Number: Always start new packages at version
1.0.0(not0.0.1)- Example:
"version": "1.0.0"in package.json - This is a project preference for consistency
- Example:
Package Structure:
- Follow existing package patterns (see cwc-types as reference)
- Include
package.json,tsconfig.jsonextending base config - Place source files in
src/directory - Include appropriate
buildandtypecheckscripts
Package Entry Points (CRITICAL - bundler resolution):
- Point
main,types, andexportsto./src/index.ts(NOT./dist) - With
bundlermodule resolution, we reference TypeScript source directly - Example:
"main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" } } - ❌ NEVER use
./dist/index.jsor./dist/index.d.ts
- Point
Package Naming:
- Use
cwc-prefix for all CWC packages - Use kebab-case:
cwc-types,cwc-backend-utils, etc.
- Use
Package Documentation:
- Create
packages/{package-name}/CLAUDE.mdfor package-specific patterns and conventions - Document architecture decisions, design patterns, and critical implementation details
- Keep package docs focused on information needed for AI-assisted coding
- Create
Add Package Shortcut Script:
- Add a shortcut script to root
package.jsonfor the new package - Format:
"package-name-shortcut": "pnpm --filter cwc-package-name" - Example:
"backend-utils": "pnpm --filter cwc-backend-utils" - This allows simplified commands:
pnpm backend-utils add expressinstead ofpnpm --filter cwc-backend-utils add express - Keep shortcuts in alphabetical order in the scripts section
- Add a shortcut script to root
Key Architectural Decisions & Patterns
MariaDB Database
- Strong Schema Enforcement
- Transaction support
- Efficient Joins
- Data normalization
- Sophisticated Querying and Analytics
Details: See packages/cwc-database/CLAUDE.md for complete database schema conventions.
TypeScript
- Strict mode enabled (
strict: true) - Shared types in
cwc-typespackage; duplicating types in separate projects leads to inconsistencies, incompatibility, confusion, and extra work - Never use
any- preferunknownif type is truly unknown - Use string literal union types, not enums
- Use
typefor entity definitions, notinterface - Use
undefined, nevernull- simplifies code by avoiding explicit checks for both values; aligns with TypeScript's optional property syntax (field?: string) - Run
typecheckbefore committing
Details: See packages/cwc-types/CLAUDE.md for complete TypeScript patterns and type generation.
Path Construction (Searchability)
Use concatenated path strings in path.join() for better searchability:
// ✅ GOOD - searchable for "deployment/servers.json"
path.join(secretsPath, 'deployment/servers.json')
// ❌ AVOID - searching for "deployment/servers.json" won't find this
path.join(secretsPath, 'deployment', 'servers.json')
Exception: Directory navigation with .. should remain segmented:
// This is fine - navigating up directories
path.join(__dirname, '..', '..', 'templates')
Naming Conventions for Configuration Values
Clarity is critical for maintainability. Configuration names should clearly indicate:
- What the value is for (its purpose)
- Where it's used (which service/context)
Examples:
sqlClientApiKey- Clear: API key for SQL Client authenticationauthenticationPublicKey- Unclear: Could apply to any auth system
Rule: When naming configuration values, prefer verbose, descriptive names over short, ambiguous ones. When a developer returns to the code after weeks or months, the name should immediately convey the purpose without requiring investigation.
Package-specific prefixes: When a configuration value is only used by one package, prefix it with the package context to avoid ambiguity:
storageLogPath/STORAGE_LOG_PATH- Clear: log path for cwc-storagelogPath/LOG_PATH- Unclear: which service uses this?
Secret and API Key Generation
Use crypto.randomBytes() for generating secrets and API keys:
import crypto from 'crypto';
// Generate a 256-bit (32-byte) cryptographically secure random key
const apiKey = crypto.randomBytes(32).toString('hex'); // 64-character hex string
This produces cryptographically secure random values suitable for:
- API keys (e.g.,
STORAGE_API_KEY) - JWT secrets (e.g.,
USER_JWT_SECRET) - Any symmetric secret requiring high entropy
Cloud-Agnostic Microservices
CWC uses a microservices architecture deployed as Docker containers potentially deployed across multiple datacenters.
- Vendor lock-in is a real business risk. Cloud providers can change pricing, deny service access, or deprecate features at any time.
- Cloud-agnostic microservices architecture allows switching hosting providers with minimal effort.
- Preparation for Scale - can scale by adding infrastructure (more containers, load balancers) rather than rewriting code and specific services can be scaled based on actual load patterns
Environment Configuration
NODE_ENV vs RUNTIME_ENVIRONMENT:
| Variable | Purpose | Set By | Values |
|---|---|---|---|
NODE_ENV |
Build-time behavior | npm/bundlers | development, production, test |
RUNTIME_ENVIRONMENT |
Application runtime behavior | CWC deployment | dev, test, prod, unit, e2e |
NODE_ENV (npm/Node.js ecosystem):
- Controls build optimizations (minification, tree-shaking)
- Affects dependency installation behavior
- CWC does NOT read this in application config
RUNTIME_ENVIRONMENT (CWC application):
- Controls application behavior (email sending, error verbosity, feature flags)
- Type:
RuntimeEnvironmentfrom cwc-types - CWC config system reads this via
loadConfig()
Rules:
- Test scripts:
RUNTIME_ENVIRONMENT=unit jest(notNODE_ENV=unit) - Backend config: Always read
RUNTIME_ENVIRONMENT, neverNODE_ENV - Each package reads configuration from
.envfile tailored to the runtime environment
1-to-1 Naming Convention:
Use consistent naming across all runtime environment references for searchability and clarity:
| Runtime Environment | Env File | Config Flag | Mock Function |
|---|---|---|---|
dev |
dev.cwc-*.env |
isDevelopment |
createMockDevConfig() |
prod |
prod.cwc-*.env |
isProduction |
createMockProdConfig() |
unit |
unit.cwc-*.env |
isUnitTest |
createMockUnitConfig() |
e2e |
e2e.cwc-*.env |
isE2ETest |
createMockE2EConfig() |
test |
test.cwc-*.env |
isTest |
createMockTestConfig() |
This consistency enables searching for Dev or Prod to find all related code paths.
Development Process
Tool, Framework, Version selection
- mainstream, widely accepted, and thoroughly tested & proven tools only
- the desire is to use the latest stable versions of the various tools
Adopt a "roll-your-own" mentality
- we want to minimize the number of unnecessary dependencies to avoid headaches when upgrading our core tech stack
- when it makes sense, we will build our own components and utilities rather than relying on a 3rd party package
Code Review Workflow Patterns
CRITICAL: When the developer provides comprehensive code review feedback and requests step-by-step discussion.
Developer Should Continue Providing Comprehensive Feedback Lists
Encourage the developer to provide ALL feedback items in a single comprehensive list. This is highly valuable because:
- Gives full context about scope of changes
- Allows identification of dependencies between issues
- Helps spot patterns across multiple points
- More efficient than addressing issues one at a time
Never discourage comprehensive feedback. The issue is not the list size, but how Claude Code presents the response.
Recognize Step-by-Step Request Signals
When the developer says any of these phrases:
- "review each of these in order step by step"
- "discuss each point one by one"
- "let's go through these one at a time"
- "walk me through each item"
This is a request for ITERATIVE discussion, not a comprehensive dump of all analysis.
Step-by-Step Review Pattern (Default for Code Reviews)
When developer provides comprehensive feedback with step-by-step request:
✅ Correct approach:
Present ONLY Point 1 with:
- The developer's original feedback for that point
- Claude's analysis and thoughts
- Any clarifying questions needed
- Recommendation for what to do
Wait for developer response and engage in discussion if needed
After Point 1 is resolved, present Point 2 using same format
Continue iteratively through all points
After all points discussed, ask "Ready to implement?" and show summary of agreed changes
Message format for each point:
## Point N: [Topic Name]
**Your Feedback:**
[Quote the developer's original feedback for this point]
**My Analysis:**
[Thoughts on this specific point only]
**Questions:** [If clarification needed]
- Question 1?
- Question 2?
**Recommendation:**
[What Claude thinks should be done]
---
*Waiting for your thoughts on Point N before moving to Point N+1.*
❌ What NOT to do:
- Present all 10-15 points with full analysis at once
- Make the developer reference "Point 7" or scroll to find what they want to discuss
- Skip the iterative conversation pattern when explicitly requested
Alternative: Full Analysis First Pattern
Only use this pattern when developer explicitly requests it:
Developer says:
- "Give me your analysis on all points first"
- "Show me all your recommendations, then we'll discuss"
- "I want to see the big picture before deciding"
In this case:
- Present comprehensive analysis of all points
- Wait for developer to identify which points need discussion
- Focus conversation only on points developer has questions about
Benefits of Step-by-Step Pattern
- Easy to follow: Each message is focused on one decision
- Encourages discussion: Natural to discuss one topic at a time
- No reference confusion: No need to say "regarding Point 7..."
- Clear progress: Both parties know exactly where we are in the review
- Better decisions: Focused attention leads to better analysis
Implementation Phase
After all review points are discussed and decisions made:
- Summarize all agreed changes in a checklist format
- Ask for explicit approval to proceed: "Ready to implement these changes?"
- Proceed with implementation in logical groups
- Update documentation if patterns/learnings emerged during review
Session 010 Learning: This pattern was established after Claude Code incorrectly presented all 14 review points at once despite clear request for step-by-step discussion. This made it difficult for the developer to engage in focused discussion on individual points.
- never log config.secrets, these values are always REDACTED
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 → RouteHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
- RouteHandler: Auth → context creation → route access check → delegation
- QueryHandler: Operation access check → execute query → format response
- MutationHandler: Operation access check → execute mutation → session renewal → format response
- SqlFunctions: Atomic database operations (one function per table operation)
Request Pipeline
- Express routes receive incoming HTTP requests
- RouteHandler 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
- 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 responsesRouteHandler.ts- Entry point: route access check, delegates to Query/MutationQueryHandler.ts- Read operations with pagination supportMutationHandler.ts- Write operations with session renewalindex.ts- Exports
Handler Flow:
RouteHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied
├── buildOperationContext() → projectId, resourceId from path
└── delegate to QueryHandler or MutationHandler
├── checkOperationAccess() → (stub for Phase 4)
├── execute operation()
├── renewSession() → always for authenticated users
└── format response with new JWT
Session Renewal Strategy:
- Always renew session on every request (keeps sessions active)
- Both QueryHandler and MutationHandler call
AuthClient.renewSession() - New JWT returned in response body (
response.body.jwt) - Renewal failure doesn't fail the request (graceful degradation)
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 - RouteHandler 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 (RouteHandler, QueryHandler, MutationHandler)
- 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 |
|---|---|
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 |
createMockAuthClient() |
Full AuthClient mock with jest.fn() for verifyToken |
createMockUserJwtPayload(overrides) |
Create UserJwtPayload with custom overrides |
mockVerifyTokenSuccess(client, payload?) |
Configure AuthClient to return success |
mockVerifyTokenFailure(client, error?) |
Configure AuthClient to return failure |
createMockLogger() |
ILogger mock with logDebug, logError, logInformation |
getTestConfig() |
Load real config from unit.cwc-api.env |
createMockConfig(overrides) |
Create config with custom overrides |
createProductionConfig() |
Config for testing production behavior |
createUnitTestConfig() |
Config explicitly set for unit test environment |
Testing Pattern:
import { createMockSqlClient, mockSelectSuccess, getTestConfig } from '../mocks';
describe('MyFunction', () => {
const config = getTestConfig();
const mockSqlClient = createMockSqlClient();
it('should return data on success', async () => {
mockSelectSuccess(mockSqlClient, { id: 1, name: 'test' });
// ... test logic
});
});
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 → RouteHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
- RouteHandler: Auth → context creation → route access check → delegation
- QueryHandler: Operation access check → execute query → format response
- MutationHandler: Operation access check → execute mutation → session renewal → format response
- SqlFunctions: Atomic database operations (one function per table operation)
Request Pipeline
- Express routes receive incoming HTTP requests
- RouteHandler 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
- 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 responsesRouteHandler.ts- Entry point: route access check, delegates to Query/MutationQueryHandler.ts- Read operations with pagination supportMutationHandler.ts- Write operations with session renewalindex.ts- Exports
Handler Flow:
RouteHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied
├── buildOperationContext() → projectId, resourceId from path
└── delegate to QueryHandler or MutationHandler
├── checkOperationAccess() → (stub for Phase 4)
├── execute operation()
├── renewSession() → always for authenticated users
└── format response with new JWT
Session Renewal Strategy:
- Always renew session on every request (keeps sessions active)
- Both QueryHandler and MutationHandler call
AuthClient.renewSession() - New JWT returned in response body (
response.body.jwt) - Renewal failure doesn't fail the request (graceful degradation)
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 - RouteHandler 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 (RouteHandler, 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 |
createDevConfig(overrides) |
Config with isDevelopment: true |
createProdConfig(overrides) |
Config with isProduction: true |
createUnitConfig(overrides) |
Config with isUnitTest: true |
| Logger Mock | |
createMockLogger() |
ILogger mock with logDebug, logError, logInformation |
Testing Pattern:
import {
createMockAuthClient,
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
mockRenewSessionSuccess,
getUnitConfig,
} from '../mocks';
describe('MyHandler', () => {
const config = getUnitConfig();
const mockAuthClient = createMockAuthClient();
it('should handle operation success', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
// ... test logic
});
});
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 → RouteHandler → QueryHandler/MutationHandler → SqlFunctions → SqlClient → cwc-sql
- RouteHandler: Auth → context creation → route access check → delegation
- QueryHandler: Operation access check → execute query → format response
- MutationHandler: Operation access check → execute mutation → session renewal → format response
- SqlFunctions: Atomic database operations (one function per table operation)
Request Pipeline
- Express routes receive incoming HTTP requests
- RouteHandler 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
- 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 responsesRouteHandler.ts- Entry point: route access check, delegates to Query/MutationQueryHandler.ts- Read operations with pagination supportMutationHandler.ts- Write operations with session renewalindex.ts- Exports
Handler Flow:
RouteHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied
├── buildOperationContext() → projectId, resourceId from path
└── delegate to QueryHandler or MutationHandler
├── checkOperationAccess() → (stub for Phase 4)
├── execute operation()
├── renewSession() → always for authenticated users
└── format response with new JWT
Session Renewal Strategy:
- Always renew session on every request (keeps sessions active)
- Both QueryHandler and MutationHandler call
AuthClient.renewSession() - New JWT returned in response body (
response.body.jwt) - Renewal failure doesn't fail the request (graceful degradation)
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 - RouteHandler 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 (RouteHandler, 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 isDevelopment: true |
createMockProdConfig(overrides) |
Mock config with isProduction: true |
createMockUnitConfig(overrides) |
Mock config with isUnitTest: true |
| Logger Mock | |
createMockLogger() |
ILogger mock with logDebug, logError, logInformation |
Testing Pattern:
import {
createMockAuthClient,
createMockRouteConfig,
createMockAuthenticatedContext,
mockOperationSuccess,
mockRenewSessionSuccess,
getUnitConfig,
createMockProdConfig,
} from '../mocks';
describe('MyHandler', () => {
const unitConfig = getUnitConfig(); // Real config from unit.cwc-api.env
const mockProdConfig = createMockProdConfig(); // Mock config for prod behavior
const mockAuthClient = createMockAuthClient();
it('should handle operation success', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt');
// ... test logic using unitConfig or mockProdConfig
});
});
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 operationsindex.ts- Exports
Handler Flow:
RequestHandler.processRequest()
├── checkRouteAccess() → 401/403 if denied (NO renewal)
├── buildOperationContext() → projectId, resourceId from path
├── delegate to QueryHandler or MutationHandler
│ ├── checkOperationAccess() → (stub for Phase 4)
│ ├── 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 isDevelopment: true |
createMockProdConfig(overrides) |
Mock config with isProduction: true |
createMockUnitConfig(overrides) |
Mock config with isUnitTest: 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() → (stub for Phase 4)
│ ├── 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 isDevelopment: true |
createMockProdConfig(overrides) |
Mock config with isProduction: true |
createMockUnitConfig(overrides) |
Mock config with isUnitTest: 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- Define
RoutePolicytype - Define
OperationPolicytype - Define policy check result types
- Define
Route Policies
- Create
src/policies/routePolicies.ts- Define which roles can access each route
- Define
allowUnauthenticatedroutes for guest access
Operation Policies
- Create
src/policies/operationPolicies.ts- Define which roles can execute which operations
- Define context-aware checks (e.g., project ownership)
Policy Enforcement
- Create
src/policies/checkRouteAccess.ts - Create
src/policies/checkOperationAccess.ts - Create
src/policies/index.ts
Unit Tests
- Create
src/__tests__/policies/routePolicies.test.ts - Create
src/__tests__/policies/operationPolicies.test.ts - Create
src/__tests__/policies/checkRouteAccess.test.ts - Create
src/__tests__/policies/checkOperationAccess.test.ts
Security Review
- Verify no privilege escalation paths (guest → user, user → owner)
- Verify ownership checks use userPkId from context (not request body)
- Verify all mutation routes require authentication
- Verify default-deny policy (routes not explicitly allowed are blocked)
- Verify policy checks cannot be bypassed via parameter manipulation
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 (
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- Define
RoutePolicytype - Define
OperationPolicytype - Define policy check result types
- Define
Route Policies
- Create
src/policies/routePolicies.ts- Define which roles can access each route
- Define
allowUnauthenticatedroutes for guest access
Operation Policies
- Create
src/policies/operationPolicies.ts- Define which roles can execute which operations
- Define context-aware checks (e.g., project ownership)
Policy Enforcement
- Create
src/policies/checkRouteAccess.ts - Create
src/policies/checkOperationAccess.ts - Create
src/policies/index.ts
Unit Tests
- Create
src/__tests__/policies/routePolicies.test.ts - Create
src/__tests__/policies/operationPolicies.test.ts - Create
src/__tests__/policies/checkRouteAccess.test.ts - Create
src/__tests__/policies/checkOperationAccess.test.ts
Security Review
- Verify no privilege escalation paths (guest → user, user → owner)
- Verify ownership checks use userPkId from context (not request body)
- Verify all mutation routes require authentication
- Verify default-deny policy (routes not explicitly allowed are blocked)
- Verify policy checks cannot be bypassed via parameter manipulation
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)
packages/cwc-api/src/__tests__/handlers/MutationHandler.test.ts5 versions
Version 1
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { MutationHandler } from '../../handlers/MutationHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
mockRenewSessionSuccess,
mockRenewSessionFailure,
getTestConfig,
createMockConfig,
createProductionConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { MutationHandlerOptions } from '../../handlers/handler.types';
describe('MutationHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const testConfig = getTestConfig();
const prodConfig = createProductionConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
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 });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Item' },
operationContext: { context },
};
const handler = new MutationHandler(options, testConfig, 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: 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' });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, testConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// All POST operations return 200, not 201
expect(response.statusCode).toBe(200);
});
});
describe('Session Renewal', () => {
it('should call renewSession for authenticated users', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, testConfig, 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 not call renewSession for guest users', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, testConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
it('should succeed operation when renewal fails (graceful handling)', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationSuccess(routeConfig, { id: 1, mutated: true });
mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, testConfig, 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();
});
it('should not include jwt in response when renewal fails', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, testConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
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, testConfig, mockAuthClient, 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, testConfig, mockAuthClient, 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, testConfig, mockAuthClient, 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, testConfig, 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('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, testConfig, 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');
expect(response.body.errorMessage).toBe('An internal error occurred');
}
});
it('should include error detail when isDevelopment is true', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
const configWithDevMode = createMockConfig({ isDevelopment: true });
mockOperationThrows(routeConfig, new Error('Detailed error message'));
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, configWithDevMode, mockAuthClient, 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, prodConfig, mockAuthClient, 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, testConfig, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Operation Access (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, testConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
Version 2
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { MutationHandler } from '../../handlers/MutationHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
mockRenewSessionSuccess,
mockRenewSessionFailure,
getUnitConfig,
createDevConfig,
createProdConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { MutationHandlerOptions } from '../../handlers/handler.types';
describe('MutationHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const unitConfig = getUnitConfig();
const prodConfig = createProdConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
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 });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Item' },
operationContext: { context },
};
const handler = new MutationHandler(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: 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' });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// All POST operations return 200, not 201
expect(response.statusCode).toBe(200);
});
});
describe('Session Renewal', () => {
it('should call renewSession for authenticated users', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(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 not call renewSession for guest users', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
it('should succeed operation when renewal fails (graceful handling)', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationSuccess(routeConfig, { id: 1, mutated: true });
mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(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();
});
it('should not include jwt in response when renewal fails', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
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, mockAuthClient, 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, mockAuthClient, 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, mockAuthClient, 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, 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('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, 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');
expect(response.body.errorMessage).toBe('An internal error occurred');
}
});
it('should include error detail when isDevelopment is true', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
const devConfig = createDevConfig();
mockOperationThrows(routeConfig, new Error('Detailed error message'));
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, devConfig, mockAuthClient, 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, prodConfig, mockAuthClient, 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, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Operation Access (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
Version 3
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { MutationHandler } from '../../handlers/MutationHandler';
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 { MutationHandlerOptions } from '../../handlers/handler.types';
describe('MutationHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const unitConfig = getUnitConfig();
const mockProdConfig = createMockProdConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
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 });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Item' },
operationContext: { context },
};
const handler = new MutationHandler(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: 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' });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// All POST operations return 200, not 201
expect(response.statusCode).toBe(200);
});
});
describe('Session Renewal', () => {
it('should call renewSession for authenticated users', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(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 not call renewSession for guest users', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
it('should succeed operation when renewal fails (graceful handling)', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationSuccess(routeConfig, { id: 1, mutated: true });
mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(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();
});
it('should not include jwt in response when renewal fails', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
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, mockAuthClient, 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, mockAuthClient, 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, mockAuthClient, 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, 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('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, 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');
expect(response.body.errorMessage).toBe('An internal error occurred');
}
});
it('should include error detail when isDevelopment 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, mockAuthClient, 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, mockAuthClient, 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, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Operation Access (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', async () => {
const routeConfig = createMockRouteConfig({ handlerType: 'mutation' });
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: MutationHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
operationContext: { context },
};
const handler = new MutationHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
Version 4
'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 isDevelopment 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 (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', 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();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
Version 5 (latest)
'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 isDevelopment 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 (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', 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();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
packages/cwc-api/src/__tests__/handlers/QueryHandler.test.ts5 versions
Version 1
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { QueryHandler } from '../../handlers/QueryHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
mockRenewSessionSuccess,
mockRenewSessionFailure,
getTestConfig,
createProductionConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { QueryHandlerOptions } from '../../handlers/handler.types';
describe('QueryHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const testConfig = getTestConfig();
const prodConfig = createProductionConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
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, testConfig, 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: 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, testConfig, 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.pagination).toEqual(pagination);
}
});
});
describe('Session Renewal', () => {
it('should call renewSession for authenticated users', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, testConfig, 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 not call renewSession for guest users', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, testConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
it('should succeed operation when renewal fails (graceful handling)', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, testConfig, 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 });
}
expect(mockLogger.logError).toHaveBeenCalled();
});
it('should not include jwt in response when renewal fails', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, testConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
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, testConfig, mockAuthClient, 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, testConfig, mockAuthClient, 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, testConfig, mockAuthClient, 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, testConfig, 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('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, testConfig, 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');
expect(response.body.errorMessage).toBe('An internal error occurred');
}
});
it('should include error detail in development 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 },
};
// testConfig has isDevelopment: true
const handler = new QueryHandler(options, testConfig, mockAuthClient, 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 },
};
// prodConfig has isDevelopment: false
const handler = new QueryHandler(options, prodConfig, mockAuthClient, 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, testConfig, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Operation Access (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, testConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
Version 2
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { QueryHandler } from '../../handlers/QueryHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
mockRenewSessionSuccess,
mockRenewSessionFailure,
getUnitConfig,
createDevConfig,
createProdConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { QueryHandlerOptions } from '../../handlers/handler.types';
describe('QueryHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const unitConfig = getUnitConfig();
const prodConfig = createProdConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
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, 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: 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, 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.pagination).toEqual(pagination);
}
});
});
describe('Session Renewal', () => {
it('should call renewSession for authenticated users', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(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 not call renewSession for guest users', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
it('should succeed operation when renewal fails (graceful handling)', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(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 });
}
expect(mockLogger.logError).toHaveBeenCalled();
});
it('should not include jwt in response when renewal fails', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
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, mockAuthClient, 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, mockAuthClient, 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, mockAuthClient, 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, 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('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, 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');
expect(response.body.errorMessage).toBe('An internal error occurred');
}
});
it('should include error detail when isDevelopment is true', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
const devConfig = createDevConfig();
mockOperationThrows(routeConfig, new Error('Detailed error message'));
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, devConfig, mockAuthClient, 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 },
};
// prodConfig has isDevelopment: false
const handler = new QueryHandler(options, prodConfig, mockAuthClient, 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, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Operation Access (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
Version 3
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { QueryHandler } from '../../handlers/QueryHandler';
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 { QueryHandlerOptions } from '../../handlers/handler.types';
describe('QueryHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const unitConfig = getUnitConfig();
const mockProdConfig = createMockProdConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
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, 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: 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, 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.pagination).toEqual(pagination);
}
});
});
describe('Session Renewal', () => {
it('should call renewSession for authenticated users', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient, 'new-jwt-token');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(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 not call renewSession for guest users', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
if (response.body.success) {
expect(response.body.jwt).toBeUndefined();
}
});
it('should succeed operation when renewal fails (graceful handling)', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
const mockLogger = createMockLogger();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'RENEWAL_FAILED');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(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 });
}
expect(mockLogger.logError).toHaveBeenCalled();
});
it('should not include jwt in response when renewal fails', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionFailure(mockAuthClient, 'AUTH_SERVICE_ERROR');
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer old-token',
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
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, mockAuthClient, 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, mockAuthClient, 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, mockAuthClient, 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, 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('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, 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');
expect(response.body.errorMessage).toBe('An internal error occurred');
}
});
it('should include error detail when isDevelopment 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, mockAuthClient, 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 isDevelopment: false
const handler = new QueryHandler(options, mockProdConfig, mockAuthClient, 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, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
describe('Operation Access (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', async () => {
const routeConfig = createMockRouteConfig();
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: QueryHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
operationContext: { context },
};
const handler = new QueryHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
Version 4
'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 isDevelopment 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 isDevelopment: 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 (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', async () => {
const routeConfig = createMockRouteConfig();
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();
// Operation access is stubbed to always allow in Phase 3
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
});
});
Version 5 (latest)
'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 isDevelopment 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 isDevelopment: 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 (Phase 3 Stub)', () => {
it('should allow operation access (stub always returns allowed)', async () => {
const routeConfig = createMockRouteConfig();
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();
// Operation access is stubbed to always allow in Phase 3
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({
allowedRoles: ['logged-on-user', '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');
expect(response.body.errorMessage).toBe('Access denied');
}
// No session renewal on auth errors
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should return 403 for authenticated user accessing owner-only route when not allowed', 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(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('FORBIDDEN');
}
// No session renewal on auth errors
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should allow guest user to access guest-allowed routes', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['guest-user', 'logged-on-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({
allowedRoles: ['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 (ownership checked at operation level)', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['project-owner'],
handlerType: 'mutation',
});
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
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();
// Route access allows logged-on-user for project-owner routes
// Actual ownership check happens at operation level (Phase 4)
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',
allowedRoles: ['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).toContain('guest-user');
expect(response.body.errorDetail).toContain('/projects/123');
}
});
it('should not include error detail in prod mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['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({
allowedRoles: ['logged-on-user', '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');
expect(response.body.errorMessage).toBe('Access denied');
}
// No session renewal on auth errors
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should return 403 for authenticated user accessing owner-only route when not allowed', 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(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorCode).toBe('FORBIDDEN');
}
// No session renewal on auth errors
expect(mockAuthClient.renewSession).not.toHaveBeenCalled();
});
it('should allow guest user to access guest-allowed routes', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['guest-user', 'logged-on-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({
allowedRoles: ['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 (ownership checked at operation level)', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['project-owner'],
handlerType: 'mutation',
});
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
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();
// Route access allows logged-on-user for project-owner routes
// Actual ownership check happens at operation level (Phase 4)
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',
allowedRoles: ['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).toContain('guest-user');
expect(response.body.errorDetail).toContain('/projects/123');
}
});
it('should not include error detail in prod mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['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();
});
});
});
packages/cwc-api/src/__tests__/handlers/responseUtils.test.ts2 versions
Version 1
'use strict';
import { describe, it, expect } from '@jest/globals';
import {
mapErrorCodeToStatus,
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from '../../handlers/responseUtils';
import { createMockDevConfig, createMockProdConfig } from '../mocks';
describe('responseUtils', () => {
describe('mapErrorCodeToStatus', () => {
it('should map error codes to appropriate HTTP status codes', () => {
expect(mapErrorCodeToStatus('NOT_FOUND')).toBe(404);
expect(mapErrorCodeToStatus('VALIDATION_ERROR')).toBe(400);
expect(mapErrorCodeToStatus('UNAUTHORIZED')).toBe(401);
expect(mapErrorCodeToStatus('FORBIDDEN')).toBe(403);
expect(mapErrorCodeToStatus('DATABASE_ERROR')).toBe(500);
});
});
describe('createOperationDeniedResponse', () => {
it('should return 403 response with OPERATION_ACCESS_DENIED error code', () => {
const config = createMockProdConfig();
const response = createOperationDeniedResponse(config, 'Test reason');
expect(response.statusCode).toBe(403);
expect(response.body).toEqual({
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
});
});
});
describe('createOperationErrorResponse', () => {
it('should return response with mapped status code and error details', () => {
const response = createOperationErrorResponse('NOT_FOUND', 'Resource not found');
expect(response.statusCode).toBe(404);
expect(response.body).toEqual({
success: false,
errorCode: 'NOT_FOUND',
errorMessage: 'Resource not found',
});
});
});
describe('createInternalErrorResponse', () => {
it('should return 500 response with INTERNAL_ERROR and include errorDetail in dev mode', () => {
const config = createMockDevConfig();
const response = createInternalErrorResponse(config, new Error('Test error'));
expect(response.statusCode).toBe(500);
expect(response.body).toEqual({
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
errorDetail: 'Test error',
});
});
});
});
Version 2 (latest)
'use strict';
import { describe, it, expect } from '@jest/globals';
import {
mapErrorCodeToStatus,
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from '../../handlers/responseUtils';
import { createMockDevConfig, createMockProdConfig } from '../mocks';
describe('responseUtils', () => {
describe('mapErrorCodeToStatus', () => {
it('should map error codes to appropriate HTTP status codes', () => {
expect(mapErrorCodeToStatus('NOT_FOUND')).toBe(404);
expect(mapErrorCodeToStatus('VALIDATION_ERROR')).toBe(400);
expect(mapErrorCodeToStatus('UNAUTHORIZED')).toBe(401);
expect(mapErrorCodeToStatus('FORBIDDEN')).toBe(403);
expect(mapErrorCodeToStatus('DATABASE_ERROR')).toBe(500);
});
});
describe('createOperationDeniedResponse', () => {
it('should return 403 response with OPERATION_ACCESS_DENIED error code', () => {
const config = createMockProdConfig();
const response = createOperationDeniedResponse(config, 'Test reason');
expect(response.statusCode).toBe(403);
expect(response.body).toEqual({
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
});
});
});
describe('createOperationErrorResponse', () => {
it('should return response with mapped status code and error details', () => {
const response = createOperationErrorResponse('NOT_FOUND', 'Resource not found');
expect(response.statusCode).toBe(404);
expect(response.body).toEqual({
success: false,
errorCode: 'NOT_FOUND',
errorMessage: 'Resource not found',
});
});
});
describe('createInternalErrorResponse', () => {
it('should return 500 response with INTERNAL_ERROR and include errorDetail in dev mode', () => {
const config = createMockDevConfig();
const response = createInternalErrorResponse(config, new Error('Test error'));
expect(response.statusCode).toBe(500);
expect(response.body).toEqual({
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
errorDetail: 'Test error',
});
});
});
});
packages/cwc-api/src/__tests__/handlers/RouteHandler.test.ts3 versions
Version 1
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { RouteHandler } from '../../handlers/RouteHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationThrows,
mockRenewSessionSuccess,
getUnitConfig,
createDevConfig,
createProdConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { RouteHandlerOptions, CwcApiRouteConfig } from '../../handlers/handler.types';
describe('RouteHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const unitConfig = getUnitConfig();
const devConfig = createDevConfig();
const prodConfig = createProdConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
jest.clearAllMocks();
});
describe('Route Access Control', () => {
it('should return 401 for guest user accessing authenticated-only route', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user', 'project-owner'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(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');
}
});
it('should return 403 for authenticated user accessing owner-only route when not allowed', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: [], // No roles allowed
});
const context = createMockAuthenticatedContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, 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('FORBIDDEN');
}
});
it('should allow guest user to access guest-allowed routes', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['guest-user', 'logged-on-user'],
handlerType: 'query',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should allow authenticated user to access logged-on-user routes', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user'],
handlerType: 'query',
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(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 (ownership checked at operation level)', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['project-owner'],
handlerType: 'mutation',
});
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Route access allows logged-on-user for project-owner routes
// Actual ownership check happens at operation level (Phase 4)
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',
allowedRoles: ['logged-on-user'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, devConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toContain('guest-user');
expect(response.body.errorDetail).toContain('/projects/123');
}
});
it('should not include error detail in prod mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, prodConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBeUndefined();
}
});
});
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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: { page: 1 },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Project' },
};
const handler = new RouteHandler(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(); // Mutation renews session
}
});
});
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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'my-project-123' },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
// No pathParams
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
});
Version 2
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { RouteHandler } from '../../handlers/RouteHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationThrows,
mockRenewSessionSuccess,
getUnitConfig,
createDevConfig,
createProdConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { RouteHandlerOptions } from '../../handlers/handler.types';
describe('RouteHandler', () => {
let mockAuthClient: jest.Mocked<AuthClient>;
const unitConfig = getUnitConfig();
const devConfig = createDevConfig();
const prodConfig = createProdConfig();
beforeEach(() => {
mockAuthClient = createMockAuthClient();
jest.clearAllMocks();
});
describe('Route Access Control', () => {
it('should return 401 for guest user accessing authenticated-only route', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user', 'project-owner'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(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');
}
});
it('should return 403 for authenticated user accessing owner-only route when not allowed', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: [], // No roles allowed
});
const context = createMockAuthenticatedContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, 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('FORBIDDEN');
}
});
it('should allow guest user to access guest-allowed routes', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['guest-user', 'logged-on-user'],
handlerType: 'query',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should allow authenticated user to access logged-on-user routes', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user'],
handlerType: 'query',
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(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 (ownership checked at operation level)', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['project-owner'],
handlerType: 'mutation',
});
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Route access allows logged-on-user for project-owner routes
// Actual ownership check happens at operation level (Phase 4)
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',
allowedRoles: ['logged-on-user'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, devConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toContain('guest-user');
expect(response.body.errorDetail).toContain('/projects/123');
}
});
it('should not include error detail in prod mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, prodConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toBeUndefined();
}
});
});
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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: { page: 1 },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Project' },
};
const handler = new RouteHandler(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(); // Mutation renews session
}
});
});
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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'my-project-123' },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
// No pathParams
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
});
Version 3 (latest)
'use strict';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { RouteHandler } from '../../handlers/RouteHandler';
import {
createMockAuthClient,
createMockRouteConfig,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationThrows,
mockRenewSessionSuccess,
getUnitConfig,
createMockDevConfig,
createMockProdConfig,
createMockLogger,
} from '../mocks';
import type { AuthClient } from 'cwc-backend-utils';
import type { RouteHandlerOptions } from '../../handlers/handler.types';
describe('RouteHandler', () => {
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({
allowedRoles: ['logged-on-user', 'project-owner'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(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');
}
});
it('should return 403 for authenticated user accessing owner-only route when not allowed', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: [], // No roles allowed
});
const context = createMockAuthenticatedContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, 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('FORBIDDEN');
}
});
it('should allow guest user to access guest-allowed routes', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['guest-user', 'logged-on-user'],
handlerType: 'query',
});
const context = createMockGuestContext();
mockOperationSuccess(routeConfig, { id: 1 });
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.statusCode).toBe(200);
expect(response.body.success).toBe(true);
});
it('should allow authenticated user to access logged-on-user routes', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user'],
handlerType: 'query',
});
const context = createMockAuthenticatedContext();
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(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 (ownership checked at operation level)', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['project-owner'],
handlerType: 'mutation',
});
const context = createMockAuthenticatedContext({ role: 'logged-on-user' });
mockOperationSuccess(routeConfig, { id: 1 });
mockRenewSessionSuccess(mockAuthClient);
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
// Route access allows logged-on-user for project-owner routes
// Actual ownership check happens at operation level (Phase 4)
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',
allowedRoles: ['logged-on-user'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, mockDevConfig, mockAuthClient, undefined);
const response = await handler.processRequest();
expect(response.body.success).toBe(false);
if (!response.body.success) {
expect(response.body.errorDetail).toContain('guest-user');
expect(response.body.errorDetail).toContain('/projects/123');
}
});
it('should not include error detail in prod mode for access denied', async () => {
const routeConfig = createMockRouteConfig({
allowedRoles: ['logged-on-user'],
});
const context = createMockGuestContext();
const options: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(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('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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: { page: 1 },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: 'Bearer token',
payload: { name: 'New Project' },
};
const handler = new RouteHandler(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(); // Mutation renews session
}
});
});
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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'my-project-123' },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
pathParams: { projectId: 'proj-1', resourceId: 'resource-456' },
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
// No pathParams
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(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: RouteHandlerOptions = {
context,
routeConfig,
authHeader: undefined,
payload: {},
};
const handler = new RouteHandler(options, unitConfig, mockAuthClient, mockLogger);
await handler.processRequest();
expect(mockLogger.logError).toHaveBeenCalled();
});
});
});
packages/cwc-api/src/__tests__/mocks/authClient.mock.ts2 versions
Version 1
'use strict';
import { jest } from '@jest/globals';
import type { AuthClient } from 'cwc-backend-utils';
import type {
VerifyTokenResult,
RenewSessionResult,
UserJwtPayload,
CwcLoginClaims,
} from 'cwc-types';
/**
* Creates a mock AuthClient for testing
*/
export function createMockAuthClient(): jest.Mocked<AuthClient> {
return {
verifyToken: jest.fn<(authHeader: string | undefined) => Promise<VerifyTokenResult>>(),
} as unknown as jest.Mocked<AuthClient>;
}
/**
* Creates a mock UserJwtPayload for testing
*/
export function createMockUserJwtPayload(overrides: Partial<UserJwtPayload> = {}): UserJwtPayload {
const defaultLogin: CwcLoginClaims = {
username: 'testuser@example.com',
deviceId: 'test-device-id',
userJwtId: 'test-jwt-id',
loginType: 'cwc',
kulo: false,
ownedProjects: ['project-1', 'project-2'],
isGuestUser: false,
};
return {
jti: 'test-jwt-id',
sub: 123,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 900, // 15 minutes
login: defaultLogin,
...overrides,
};
}
/**
* Configures mock AuthClient to return success with payload
*/
export function mockVerifyTokenSuccess(
mockAuthClient: jest.Mocked<AuthClient>,
payload: UserJwtPayload = createMockUserJwtPayload()
): void {
mockAuthClient.verifyToken.mockResolvedValueOnce({
success: true,
payload,
});
}
/**
* Configures mock AuthClient to return failure
*/
export function mockVerifyTokenFailure(
mockAuthClient: jest.Mocked<AuthClient>,
error: string = 'INVALID_TOKEN'
): void {
mockAuthClient.verifyToken.mockResolvedValueOnce({
success: false,
error,
});
}
Version 2 (latest)
'use strict';
import { jest } from '@jest/globals';
import type { AuthClient } from 'cwc-backend-utils';
import type {
VerifyTokenResult,
RenewSessionResult,
UserJwtPayload,
CwcLoginClaims,
} from 'cwc-types';
/**
* Creates a mock AuthClient for testing
*/
export function createMockAuthClient(): jest.Mocked<AuthClient> {
return {
verifyToken: jest.fn<(authHeader: string | undefined) => Promise<VerifyTokenResult>>(),
renewSession: jest.fn<(authHeader: string | undefined) => Promise<RenewSessionResult>>(),
} as unknown as jest.Mocked<AuthClient>;
}
/**
* Creates a mock UserJwtPayload for testing
*/
export function createMockUserJwtPayload(overrides: Partial<UserJwtPayload> = {}): UserJwtPayload {
const defaultLogin: CwcLoginClaims = {
username: 'testuser@example.com',
deviceId: 'test-device-id',
userJwtId: 'test-jwt-id',
loginType: 'cwc',
kulo: false,
ownedProjects: ['project-1', 'project-2'],
isGuestUser: false,
};
return {
jti: 'test-jwt-id',
sub: 123,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 900, // 15 minutes
login: defaultLogin,
...overrides,
};
}
/**
* Configures mock AuthClient to return success with payload
*/
export function mockVerifyTokenSuccess(
mockAuthClient: jest.Mocked<AuthClient>,
payload: UserJwtPayload = createMockUserJwtPayload()
): void {
mockAuthClient.verifyToken.mockResolvedValueOnce({
success: true,
payload,
});
}
/**
* Configures mock AuthClient to return failure
*/
export function mockVerifyTokenFailure(
mockAuthClient: jest.Mocked<AuthClient>,
error: string = 'INVALID_TOKEN'
): void {
mockAuthClient.verifyToken.mockResolvedValueOnce({
success: false,
error,
});
}
/**
* Configures mock AuthClient to return success for renewSession with new JWT
*/
export function mockRenewSessionSuccess(
mockAuthClient: jest.Mocked<AuthClient>,
jwt: string = 'new-jwt-token'
): void {
mockAuthClient.renewSession.mockResolvedValueOnce({
success: true,
jwt,
});
}
/**
* Configures mock AuthClient to return failure for renewSession
*/
export function mockRenewSessionFailure(
mockAuthClient: jest.Mocked<AuthClient>,
error: string = 'RENEWAL_FAILED'
): void {
mockAuthClient.renewSession.mockResolvedValueOnce({
success: false,
error,
});
}
packages/cwc-api/src/__tests__/mocks/config.mock.ts3 versions
Version 1
'use strict';
import type { CwcApiConfig } from '../../config/config.types';
import { loadConfig } from '../../config';
/**
* Gets the real config loaded from unit.cwc-api.env
* Use this as the base for test config
*/
export function getTestConfig(): CwcApiConfig {
return loadConfig();
}
/**
* Creates a mock config by applying overrides to the real test config
* Useful when you need to test specific config variations (e.g., production mode)
*/
export function createMockConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
const baseConfig = loadConfig();
return { ...baseConfig, ...overrides };
}
/**
* Creates a production config for testing production error behavior
* Applies production-like settings while preserving secrets from env
*/
export function createProductionConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'prod',
isProduction: true,
isDevelopment: false,
isTest: false,
isUnitTest: false,
isEndToEnd: false,
debugMode: false,
...overrides,
});
}
/**
* Creates a unit test config (should match what's loaded from env)
* Provided for compatibility with existing tests
*/
export function createUnitTestConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'unit',
isProduction: false,
isDevelopment: false,
isTest: false,
isUnitTest: true,
isEndToEnd: false,
debugMode: false,
...overrides,
});
}
/**
* Creates a development config for testing dev-only error behavior
* Has isDevelopment: true so error details are exposed
*/
export function createDevelopmentConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'dev',
isProduction: false,
isDevelopment: true,
isTest: false,
isUnitTest: false,
isEndToEnd: false,
debugMode: true,
...overrides,
});
}
Version 2
'use strict';
import type { CwcApiConfig } from '../../config/config.types';
import { loadConfig } from '../../config';
/**
* Gets the real config loaded from unit.cwc-api.env
*/
export function getUnitConfig(): CwcApiConfig {
return loadConfig();
}
/**
* Creates a mock config by applying overrides to the loaded unit config
* Useful when you need to test specific config variations
*/
export function createMockConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
const baseConfig = loadConfig();
return { ...baseConfig, ...overrides };
}
/**
* Creates a prod config for testing production error behavior
*/
export function createProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'prod',
isProduction: true,
isDevelopment: false,
isTest: false,
isUnitTest: false,
isEndToEnd: false,
debugMode: false,
...overrides,
});
}
/**
* Creates a unit config (should match what's loaded from unit.cwc-api.env)
*/
export function createUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'unit',
isProduction: false,
isDevelopment: false,
isTest: false,
isUnitTest: true,
isEndToEnd: false,
debugMode: false,
...overrides,
});
}
/**
* Creates a dev config for testing dev-mode behavior
* Has isDevelopment: true so error details are exposed
*/
export function createDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'dev',
isProduction: false,
isDevelopment: true,
isTest: false,
isUnitTest: false,
isEndToEnd: false,
debugMode: true,
...overrides,
});
}
Version 3 (latest)
'use strict';
import type { CwcApiConfig } from '../../config/config.types';
import { loadConfig } from '../../config';
/**
* Gets the real config loaded from unit.cwc-api.env
*/
export function getUnitConfig(): CwcApiConfig {
return loadConfig();
}
/**
* Creates a mock config by applying overrides to the loaded unit config
* Useful when you need to test specific config variations
*/
export function createMockConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
const baseConfig = loadConfig();
return { ...baseConfig, ...overrides };
}
/**
* Creates a mock prod config for testing production error behavior
*/
export function createMockProdConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'prod',
isProduction: true,
isDevelopment: false,
isTest: false,
isUnitTest: false,
isEndToEnd: false,
debugMode: false,
...overrides,
});
}
/**
* Creates a mock unit config (should match what's loaded from unit.cwc-api.env)
*/
export function createMockUnitConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'unit',
isProduction: false,
isDevelopment: false,
isTest: false,
isUnitTest: true,
isEndToEnd: false,
debugMode: false,
...overrides,
});
}
/**
* Creates a mock dev config for testing dev-mode behavior
* Has isDevelopment: true so error details are exposed
*/
export function createMockDevConfig(overrides?: Partial<CwcApiConfig>): CwcApiConfig {
return createMockConfig({
runtimeEnvironment: 'dev',
isProduction: false,
isDevelopment: true,
isTest: false,
isUnitTest: false,
isEndToEnd: false,
debugMode: true,
...overrides,
});
}
packages/cwc-api/src/__tests__/mocks/index.ts3 versions
Version 1
'use strict';
export {
createMockSqlClient,
mockSelectSuccess,
mockSelectNotFound,
mockListSuccess,
mockInsertSuccess,
mockInsertFailure,
mockUpdateSuccess,
mockUpdateNotFound,
mockDeleteSuccess,
mockDeleteNotFound,
} from './sqlClient.mock';
export {
getTestConfig,
createMockConfig,
createProductionConfig,
createUnitTestConfig,
} from './config.mock';
export { createMockLogger } from './logger.mock';
export {
createMockAuthClient,
createMockUserJwtPayload,
mockVerifyTokenSuccess,
mockVerifyTokenFailure,
mockRenewSessionSuccess,
mockRenewSessionFailure,
} from './authClient.mock';
export {
createMockRouteConfig,
createMockOperationContext,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
} from './routeConfig.mock';
Version 2
'use strict';
export {
createMockSqlClient,
mockSelectSuccess,
mockSelectNotFound,
mockListSuccess,
mockInsertSuccess,
mockInsertFailure,
mockUpdateSuccess,
mockUpdateNotFound,
mockDeleteSuccess,
mockDeleteNotFound,
} from './sqlClient.mock';
export {
getUnitConfig,
createMockConfig,
createProdConfig,
createUnitConfig,
createDevConfig,
} from './config.mock';
export { createMockLogger } from './logger.mock';
export {
createMockAuthClient,
createMockUserJwtPayload,
mockVerifyTokenSuccess,
mockVerifyTokenFailure,
mockRenewSessionSuccess,
mockRenewSessionFailure,
} from './authClient.mock';
export {
createMockRouteConfig,
createMockOperationContext,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
} from './routeConfig.mock';
Version 3 (latest)
'use strict';
export {
createMockSqlClient,
mockSelectSuccess,
mockSelectNotFound,
mockListSuccess,
mockInsertSuccess,
mockInsertFailure,
mockUpdateSuccess,
mockUpdateNotFound,
mockDeleteSuccess,
mockDeleteNotFound,
} from './sqlClient.mock';
export {
getUnitConfig,
createMockConfig,
createMockProdConfig,
createMockUnitConfig,
createMockDevConfig,
} from './config.mock';
export { createMockLogger } from './logger.mock';
export {
createMockAuthClient,
createMockUserJwtPayload,
mockVerifyTokenSuccess,
mockVerifyTokenFailure,
mockRenewSessionSuccess,
mockRenewSessionFailure,
} from './authClient.mock';
export {
createMockRouteConfig,
createMockOperationContext,
createMockGuestContext,
createMockAuthenticatedContext,
mockOperationSuccess,
mockOperationFailure,
mockOperationThrows,
} from './routeConfig.mock';
packages/cwc-api/src/__tests__/mocks/routeConfig.mock.ts3 versions
Version 1
'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 { RequestContext, GuestContext, AuthenticatedContext } from '../../context';
/**
* 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,
};
}
/**
* 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);
}
Version 2
'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
*/
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,
};
}
/**
* 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);
}
Version 3 (latest)
'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
*/
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,
};
}
/**
* 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/handlers/handler.types.ts3 versions
Version 1
'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
allowedRoles: 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 RouteHandler
*/
export type RouteHandlerOptions = 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>;
}
Version 2
'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
allowedRoles: 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>;
}
Version 3 (latest)
'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
allowedRoles: 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/index.ts3 versions
Version 1
'use strict';
// Handler classes
export { RouteHandler } from './RouteHandler';
export { QueryHandler } from './QueryHandler';
export { MutationHandler } from './MutationHandler';
// Types
export type {
// Error codes
CwcApiErrorCode,
// Status codes
CwcApiSuccessStatusCode,
CwcApiErrorStatusCode,
CwcApiStatusCode,
// Response types
CwcApiPagination,
CwcApiSuccessResponse,
CwcApiErrorResponse,
CwcApiResponse,
CwcApiHandlerResponse,
// Route configuration
CwcApiHandlerType,
CwcApiRouteConfig,
CwcApiRouteConfigs,
// Operation types
OperationContext,
OperationResult,
CwcApiOperation,
// Handler options
BaseHandlerOptions,
RouteHandlerOptions,
QueryHandlerOptions,
MutationHandlerOptions,
// Handler interface
CwcApiHandler,
// Access policy types (Phase 4)
RouteAccessResult,
OperationAccessResult,
RouteAccessPolicy,
OperationAccessPolicy,
} from './handler.types';
Version 2
'use strict';
// Handler classes
export { RequestHandler } from './RequestHandler';
export { QueryHandler } from './QueryHandler';
export { MutationHandler } from './MutationHandler';
// Types
export type {
// Error codes
CwcApiErrorCode,
// Status codes
CwcApiSuccessStatusCode,
CwcApiErrorStatusCode,
CwcApiStatusCode,
// Response types
CwcApiPagination,
CwcApiSuccessResponse,
CwcApiErrorResponse,
CwcApiResponse,
CwcApiHandlerResponse,
// Route configuration
CwcApiHandlerType,
CwcApiRouteConfig,
CwcApiRouteConfigs,
// Operation types
OperationContext,
OperationResult,
CwcApiOperation,
// Handler options
BaseHandlerOptions,
RequestHandlerOptions,
QueryHandlerOptions,
MutationHandlerOptions,
// Handler interface
CwcApiHandler,
// Access policy types (Phase 4)
RouteAccessResult,
OperationAccessResult,
RouteAccessPolicy,
OperationAccessPolicy,
} from './handler.types';
Version 3 (latest)
'use strict';
// Handler classes
export { RequestHandler } from './RequestHandler';
export { QueryHandler } from './QueryHandler';
export { MutationHandler } from './MutationHandler';
// Types
export type {
// Error codes
CwcApiErrorCode,
// Status codes
CwcApiSuccessStatusCode,
CwcApiErrorStatusCode,
CwcApiStatusCode,
// Response types
CwcApiPagination,
CwcApiSuccessResponse,
CwcApiErrorResponse,
CwcApiResponse,
CwcApiHandlerResponse,
// Route configuration
CwcApiHandlerType,
CwcApiRouteConfig,
CwcApiRouteConfigs,
// Operation types
OperationContext,
OperationResult,
CwcApiOperation,
// Handler options
BaseHandlerOptions,
RequestHandlerOptions,
QueryHandlerOptions,
MutationHandlerOptions,
// Handler interface
CwcApiHandler,
// Access policy types (Phase 4)
RouteAccessResult,
OperationAccessResult,
RouteAccessPolicy,
OperationAccessPolicy,
} from './handler.types';
packages/cwc-api/src/handlers/MutationHandler.ts4 versions
Version 1
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
MutationHandlerOptions,
OperationAccessResult,
CwcApiErrorCode,
} from './handler.types';
const codeLocation = 'handlers/MutationHandler.ts';
/**
* MutationHandler - Handles write operations
*
* Responsibilities:
* 1. Check operation-level access (Phase 4 stub)
* 2. Execute the mutation operation
* 3. Renew session (always, to keep session active)
* 4. Format successful response with data and new JWT
*/
export class MutationHandler implements CwcApiHandler {
private options: MutationHandlerOptions;
private config: CwcApiConfig;
private authClient: AuthClient;
private logger: ILogger | undefined;
constructor(
options: MutationHandlerOptions,
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, operationContext, authHeader } = this.options;
try {
// Step 1: Check operation-level access (Phase 4 implementation)
const operationAccess = await this.checkOperationAccess();
if (!operationAccess.allowed) {
return this.createOperationDeniedResponse(operationAccess.reason);
}
// Step 2: Execute the mutation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
}
// Step 3: Renew session if authenticated
let newJwt: string | undefined;
if (context.isAuthenticated) {
const renewResult = await this.authClient.renewSession(authHeader);
if (renewResult.success) {
newJwt = renewResult.jwt;
} else {
// Log warning but don't fail the operation
this.logger?.logError({
userPkId: context.userPkId,
codeLocation,
message: `Session renewal failed for mutation operation`,
error: renewResult.error,
});
}
}
// Step 4: Format response (always 200 for RPC-style API)
return {
statusCode: 200,
body: {
success: true,
data: result.data,
...(newJwt ? { jwt: newJwt } : {}),
},
};
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error executing mutation operation for ${routeConfig.path}`,
error,
});
return this.createInternalErrorResponse(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 };
}
private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
return {
statusCode: 403,
body: {
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createOperationErrorResponse(
errorCode: CwcApiErrorCode,
errorMessage: string
): CwcApiHandlerResponse {
const statusCode = this.mapErrorCodeToStatus(errorCode);
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage,
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
switch (errorCode) {
case 'NOT_FOUND':
return 404;
case 'VALIDATION_ERROR':
case 'INVALID_PARAMETER':
case 'ALREADY_EXISTS':
return 400;
case 'UNAUTHORIZED':
return 401;
case 'FORBIDDEN':
case 'ROUTE_ACCESS_DENIED':
case 'OPERATION_ACCESS_DENIED':
return 403;
case 'INTERNAL_ERROR':
case 'DATABASE_ERROR':
case 'AUTH_SERVICE_ERROR':
return 500;
default:
return 500;
}
}
}
Version 2
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
MutationHandlerOptions,
OperationAccessResult,
CwcApiErrorCode,
} from './handler.types';
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 this.createOperationDeniedResponse(operationAccess.reason);
}
// Step 2: Execute the mutation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return this.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 this.createInternalErrorResponse(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 };
}
private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
return {
statusCode: 403,
body: {
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createOperationErrorResponse(
errorCode: CwcApiErrorCode,
errorMessage: string
): CwcApiHandlerResponse {
const statusCode = this.mapErrorCodeToStatus(errorCode);
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage,
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
switch (errorCode) {
case 'NOT_FOUND':
return 404;
case 'VALIDATION_ERROR':
case 'INVALID_PARAMETER':
case 'ALREADY_EXISTS':
return 400;
case 'UNAUTHORIZED':
return 401;
case 'FORBIDDEN':
case 'ROUTE_ACCESS_DENIED':
case 'OPERATION_ACCESS_DENIED':
return 403;
case 'INTERNAL_ERROR':
case 'DATABASE_ERROR':
case 'AUTH_SERVICE_ERROR':
return 500;
default:
return 500;
}
}
}
Version 3
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
MutationHandlerOptions,
OperationAccessResult,
} from './handler.types';
import {
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from './responseUtils';
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 4 (latest)
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
MutationHandlerOptions,
OperationAccessResult,
} from './handler.types';
import {
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from './responseUtils';
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 };
}
}
packages/cwc-api/src/handlers/QueryHandler.ts4 versions
Version 1
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
QueryHandlerOptions,
OperationAccessResult,
CwcApiErrorCode,
} from './handler.types';
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. Renew session (always, to keep session active)
* 4. Format successful response with data/pagination and new JWT
*/
export class QueryHandler implements CwcApiHandler {
private options: QueryHandlerOptions;
private config: CwcApiConfig;
private authClient: AuthClient;
private logger: ILogger | undefined;
constructor(
options: QueryHandlerOptions,
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, operationContext, authHeader } = this.options;
try {
// Step 1: Check operation-level access (Phase 4 implementation)
const operationAccess = await this.checkOperationAccess();
if (!operationAccess.allowed) {
return this.createOperationDeniedResponse(operationAccess.reason);
}
// Step 2: Execute the operation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return this.createOperationErrorResponse(result.errorCode, result.errorMessage);
}
// Step 3: Renew session if authenticated
let newJwt: string | undefined;
if (context.isAuthenticated) {
const renewResult = await this.authClient.renewSession(authHeader);
if (renewResult.success) {
newJwt = renewResult.jwt;
} else {
// Log warning but don't fail the operation
this.logger?.logError({
userPkId: context.userPkId,
codeLocation,
message: `Session renewal failed for query operation`,
error: renewResult.error,
});
}
}
// Step 4: Format response
return {
statusCode: 200,
body: {
success: true,
data: result.data,
...(result.pagination ? { pagination: result.pagination } : {}),
...(newJwt ? { jwt: newJwt } : {}),
},
};
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error executing query operation for ${routeConfig.path}`,
error,
});
return this.createInternalErrorResponse(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 };
}
private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
return {
statusCode: 403,
body: {
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createOperationErrorResponse(
errorCode: CwcApiErrorCode,
errorMessage: string
): CwcApiHandlerResponse {
const statusCode = this.mapErrorCodeToStatus(errorCode);
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage,
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
switch (errorCode) {
case 'NOT_FOUND':
return 404;
case 'VALIDATION_ERROR':
case 'INVALID_PARAMETER':
case 'ALREADY_EXISTS':
return 400;
case 'UNAUTHORIZED':
return 401;
case 'FORBIDDEN':
case 'ROUTE_ACCESS_DENIED':
case 'OPERATION_ACCESS_DENIED':
return 403;
case 'INTERNAL_ERROR':
case 'DATABASE_ERROR':
case 'AUTH_SERVICE_ERROR':
return 500;
default:
return 500;
}
}
}
Version 2
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
QueryHandlerOptions,
OperationAccessResult,
CwcApiErrorCode,
} from './handler.types';
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 this.createOperationDeniedResponse(operationAccess.reason);
}
// Step 2: Execute the operation
const result = await routeConfig.operation(payload, operationContext);
if (!result.success) {
return this.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 this.createInternalErrorResponse(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 };
}
private createOperationDeniedResponse(reason?: string): CwcApiHandlerResponse {
return {
statusCode: 403,
body: {
success: false,
errorCode: 'OPERATION_ACCESS_DENIED',
errorMessage: 'Operation not permitted',
...(this.config.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createOperationErrorResponse(
errorCode: CwcApiErrorCode,
errorMessage: string
): CwcApiHandlerResponse {
const statusCode = this.mapErrorCodeToStatus(errorCode);
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage,
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
private mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
switch (errorCode) {
case 'NOT_FOUND':
return 404;
case 'VALIDATION_ERROR':
case 'INVALID_PARAMETER':
case 'ALREADY_EXISTS':
return 400;
case 'UNAUTHORIZED':
return 401;
case 'FORBIDDEN':
case 'ROUTE_ACCESS_DENIED':
case 'OPERATION_ACCESS_DENIED':
return 403;
case 'INTERNAL_ERROR':
case 'DATABASE_ERROR':
case 'AUTH_SERVICE_ERROR':
return 500;
default:
return 500;
}
}
}
Version 3
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
QueryHandlerOptions,
OperationAccessResult,
} from './handler.types';
import {
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from './responseUtils';
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 4 (latest)
'use strict';
import type { ILogger } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
QueryHandlerOptions,
OperationAccessResult,
} from './handler.types';
import {
createOperationDeniedResponse,
createOperationErrorResponse,
createInternalErrorResponse,
} from './responseUtils';
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 };
}
}
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,
RouteAccessResult,
OperationContext,
} from './handler.types';
import { QueryHandler } from './QueryHandler';
import { MutationHandler } from './MutationHandler';
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 this.createInternalErrorResponse(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.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
}
Version 2
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
CwcApiSuccessResponse,
RequestHandlerOptions,
RouteAccessResult,
OperationContext,
} from './handler.types';
import { QueryHandler } from './QueryHandler';
import { MutationHandler } from './MutationHandler';
import { createInternalErrorResponse } from './responseUtils';
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.isDevelopment && 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,
RouteAccessResult,
OperationContext,
} from './handler.types';
import { QueryHandler } from './QueryHandler';
import { MutationHandler } from './MutationHandler';
import { createInternalErrorResponse } from './responseUtils';
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.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
}
packages/cwc-api/src/handlers/responseUtils.ts2 versions
Version 1
'use strict';
import type { CwcApiConfig } from '../config';
import type { CwcApiHandlerResponse, CwcApiErrorCode } from './handler.types';
/**
* Maps error codes to appropriate HTTP status codes
*/
export function mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
switch (errorCode) {
case 'NOT_FOUND':
return 404;
case 'VALIDATION_ERROR':
case 'INVALID_PARAMETER':
case 'ALREADY_EXISTS':
return 400;
case 'UNAUTHORIZED':
return 401;
case 'FORBIDDEN':
case 'ROUTE_ACCESS_DENIED':
case 'OPERATION_ACCESS_DENIED':
return 403;
case 'INTERNAL_ERROR':
case 'DATABASE_ERROR':
case 'AUTH_SERVICE_ERROR':
return 500;
default:
return 500;
}
}
/**
* 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.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
/**
* Creates an error response with appropriate status code based on error code
*/
export function createOperationErrorResponse(
errorCode: CwcApiErrorCode,
errorMessage: string
): CwcApiHandlerResponse {
const statusCode = mapErrorCodeToStatus(errorCode);
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage,
},
};
}
/**
* Creates a 500 internal error response
*/
export function createInternalErrorResponse(
config: CwcApiConfig,
error: unknown
): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
Version 2 (latest)
'use strict';
import type { CwcApiConfig } from '../config';
import type { CwcApiHandlerResponse, CwcApiErrorCode } from './handler.types';
/**
* Maps error codes to appropriate HTTP status codes
*/
export function mapErrorCodeToStatus(errorCode: CwcApiErrorCode): 400 | 401 | 403 | 404 | 500 {
switch (errorCode) {
case 'NOT_FOUND':
return 404;
case 'VALIDATION_ERROR':
case 'INVALID_PARAMETER':
case 'ALREADY_EXISTS':
return 400;
case 'UNAUTHORIZED':
return 401;
case 'FORBIDDEN':
case 'ROUTE_ACCESS_DENIED':
case 'OPERATION_ACCESS_DENIED':
return 403;
case 'INTERNAL_ERROR':
case 'DATABASE_ERROR':
case 'AUTH_SERVICE_ERROR':
return 500;
default:
return 500;
}
}
/**
* 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.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
/**
* Creates an error response with appropriate status code based on error code
*/
export function createOperationErrorResponse(
errorCode: CwcApiErrorCode,
errorMessage: string
): CwcApiHandlerResponse {
const statusCode = mapErrorCodeToStatus(errorCode);
return {
statusCode,
body: {
success: false,
errorCode,
errorMessage,
},
};
}
/**
* Creates a 500 internal error response
*/
export function createInternalErrorResponse(
config: CwcApiConfig,
error: unknown
): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
packages/cwc-api/src/handlers/RouteHandler.ts
'use strict';
import type { ILogger, AuthClient } from 'cwc-backend-utils';
import type { CwcApiConfig } from '../config';
import type {
CwcApiHandler,
CwcApiHandlerResponse,
RouteHandlerOptions,
RouteAccessResult,
OperationContext,
} from './handler.types';
import { QueryHandler } from './QueryHandler';
import { MutationHandler } from './MutationHandler';
const codeLocation = 'handlers/RouteHandler.ts';
/**
* RouteHandler - 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
*/
export class RouteHandler implements CwcApiHandler {
private options: RouteHandlerOptions;
private config: CwcApiConfig;
private authClient: AuthClient;
private logger: ILogger | undefined;
constructor(
options: RouteHandlerOptions,
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) {
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
if (routeConfig.handlerType === 'query') {
const queryHandler = new QueryHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.authClient,
this.logger
);
return queryHandler.processRequest();
} else {
const mutationHandler = new MutationHandler(
{
context,
routeConfig,
authHeader,
payload,
operationContext,
},
this.config,
this.authClient,
this.logger
);
return mutationHandler.processRequest();
}
} catch (error) {
this.logger?.logError({
userPkId: context.isAuthenticated ? context.userPkId : undefined,
codeLocation,
message: `Error processing route ${routeConfig.path}`,
error,
});
return this.createInternalErrorResponse(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.isDevelopment && reason ? { errorDetail: reason } : {}),
},
};
}
private createInternalErrorResponse(error: unknown): CwcApiHandlerResponse {
return {
statusCode: 500,
body: {
success: false,
errorCode: 'INTERNAL_ERROR',
errorMessage: 'An internal error occurred',
...(this.config.isDevelopment && error instanceof Error
? { errorDetail: error.message }
: {}),
},
};
}
}
packages/cwc-backend-utils/src/AuthClient/AuthClient.ts2 versions
Version 1
import axios from 'axios';
import type { ILogger } from '../backendUtils.types';
import type {
VerifyTokenResponse,
VerifyTokenResult,
VerifyTokenErrorResponse,
RenewSessionResponse,
RenewSessionResult,
RenewSessionErrorResponse,
} from 'cwc-types';
const codeLocation = 'AuthClient/AuthClient.ts';
export type AuthClientConfig = {
authUri: string;
timeout?: number | undefined;
};
export type AuthClientOptions = {
config: AuthClientConfig;
logger: ILogger | undefined;
clientName: string;
};
/**
* HTTP client for cwc-auth service
* Similar pattern to SqlClient for cwc-sql
*/
export class AuthClient {
private config: AuthClientConfig;
private logger: ILogger | undefined;
private clientName: string;
constructor(options: AuthClientOptions) {
this.config = options.config;
this.logger = options.logger;
this.clientName = options.clientName;
}
/**
* Verify a JWT by calling cwc-auth /verify-token
*/
async verifyToken(authHeader: string | undefined): Promise<VerifyTokenResult> {
if (!authHeader) {
return { success: false, error: 'MISSING_TOKEN' };
}
try {
const response = await axios.post<VerifyTokenResponse>(
`${this.config.authUri}/verify-token`,
{},
{
headers: { Authorization: authHeader },
timeout: this.config.timeout ?? 5000,
}
);
const data = response.data;
if (data.success) {
return { success: true, payload: data.payload };
}
// data.success is false, so it's a VerifyTokenErrorResponse
return { success: false, error: data.errorCode ?? 'UNKNOWN_ERROR' };
} catch (error) {
// Handle 401 responses (expected for invalid tokens)
if (axios.isAxiosError(error) && error.response?.status === 401) {
const errorData = error.response.data as VerifyTokenErrorResponse;
return { success: false, error: errorData.errorCode ?? 'INVALID_TOKEN' };
}
// Log unexpected errors
this.logger?.logError({
userPkId: undefined,
codeLocation,
message: `[${this.clientName}] Failed to verify token with auth service`,
error,
});
return { success: false, error: 'AUTH_SERVICE_ERROR' };
}
}
}
Version 2 (latest)
import axios from 'axios';
import type { ILogger } from '../backendUtils.types';
import type {
VerifyTokenResponse,
VerifyTokenResult,
VerifyTokenErrorResponse,
RenewSessionResponse,
RenewSessionResult,
RenewSessionErrorResponse,
} from 'cwc-types';
const codeLocation = 'AuthClient/AuthClient.ts';
export type AuthClientConfig = {
authUri: string;
timeout?: number | undefined;
};
export type AuthClientOptions = {
config: AuthClientConfig;
logger: ILogger | undefined;
clientName: string;
};
/**
* HTTP client for cwc-auth service
* Similar pattern to SqlClient for cwc-sql
*/
export class AuthClient {
private config: AuthClientConfig;
private logger: ILogger | undefined;
private clientName: string;
constructor(options: AuthClientOptions) {
this.config = options.config;
this.logger = options.logger;
this.clientName = options.clientName;
}
/**
* Verify a JWT by calling cwc-auth /verify-token
*/
async verifyToken(authHeader: string | undefined): Promise<VerifyTokenResult> {
if (!authHeader) {
return { success: false, error: 'MISSING_TOKEN' };
}
try {
const response = await axios.post<VerifyTokenResponse>(
`${this.config.authUri}/verify-token`,
{},
{
headers: { Authorization: authHeader },
timeout: this.config.timeout ?? 5000,
}
);
const data = response.data;
if (data.success) {
return { success: true, payload: data.payload };
}
// data.success is false, so it's a VerifyTokenErrorResponse
return { success: false, error: data.errorCode ?? 'UNKNOWN_ERROR' };
} catch (error) {
// Handle 401 responses (expected for invalid tokens)
if (axios.isAxiosError(error) && error.response?.status === 401) {
const errorData = error.response.data as VerifyTokenErrorResponse;
return { success: false, error: errorData.errorCode ?? 'INVALID_TOKEN' };
}
// Log unexpected errors
this.logger?.logError({
userPkId: undefined,
codeLocation,
message: `[${this.clientName}] Failed to verify token with auth service`,
error,
});
return { success: false, error: 'AUTH_SERVICE_ERROR' };
}
}
/**
* Renew a session by calling cwc-auth /renew-session
* Used to keep sessions active and refresh JWT claims (e.g., ownedProjects)
*/
async renewSession(authHeader: string | undefined): Promise<RenewSessionResult> {
if (!authHeader) {
return { success: false, error: 'MISSING_TOKEN' };
}
try {
const response = await axios.post<RenewSessionResponse>(
`${this.config.authUri}/renew-session`,
{},
{
headers: { Authorization: authHeader },
timeout: this.config.timeout ?? 5000,
}
);
const data = response.data;
if (data.success) {
return { success: true, jwt: data.jwt };
}
// data.success is false, so it's a RenewSessionErrorResponse
return { success: false, error: data.errorCode ?? 'UNKNOWN_ERROR' };
} catch (error) {
// Handle 401 responses (expected for invalid/expired tokens)
if (axios.isAxiosError(error) && error.response?.status === 401) {
const errorData = error.response.data as RenewSessionErrorResponse;
return { success: false, error: errorData.errorCode ?? 'RENEWAL_FAILED' };
}
// Log unexpected errors
this.logger?.logError({
userPkId: undefined,
codeLocation,
message: `[${this.clientName}] Failed to renew session with auth service`,
error,
});
return { success: false, error: 'AUTH_SERVICE_ERROR' };
}
}
}
packages/cwc-types/src/authTypes.ts
/**
* Auth Types - Shared authentication types for CWC services
*
* These types are used by cwc-auth (JWT creation) and consuming services
* (JWT verification via AuthClient).
*/
import type { CwcLoginClaims } from './entityTypes';
/**
* UserJwtPayload structure - decoded JWT payload
* Used by cwc-auth (creation) and consuming services (verification)
*/
export type UserJwtPayload = {
jti: string; // userJwtId (UUID) - references userJwt table
sub: number; // userPkId
iat: number; // Issued at (Unix timestamp)
exp: number; // Expiration (Unix timestamp)
login: CwcLoginClaims;
};
/**
* Roles for access control across CWC services
*/
export type CwcRole = 'project-owner' | 'logged-on-user' | 'guest-user';
/**
* Success response from cwc-auth /verify-token
*/
export type VerifyTokenSuccessResponse = {
success: true;
jwtType: undefined;
payload: UserJwtPayload;
};
/**
* Error response from cwc-auth /verify-token
*/
export type VerifyTokenErrorResponse = {
success: false;
jwtType: undefined;
errorCode?: string | undefined; // Dev-only
errorDetail?: string | undefined; // Dev-only
};
/**
* Union of verify-token responses
*/
export type VerifyTokenResponse = VerifyTokenSuccessResponse | VerifyTokenErrorResponse;
/**
* Result from AuthClient.verifyToken()
*/
export type VerifyTokenResult =
| { success: true; payload: UserJwtPayload }
| { success: false; error: string };
// ============================================================================
// Renew Session Types
// ============================================================================
/**
* Success response from cwc-auth /renew-session
*/
export type RenewSessionSuccessResponse = {
success: true;
jwtType: 'user';
jwt: string;
};
/**
* Error response from cwc-auth /renew-session
*/
export type RenewSessionErrorResponse = {
success: false;
jwtType: undefined;
errorCode?: string | undefined; // Dev-only
errorDetail?: string | undefined; // Dev-only
};
/**
* Union of renew-session responses
*/
export type RenewSessionResponse = RenewSessionSuccessResponse | RenewSessionErrorResponse;
/**
* Result from AuthClient.renewSession()
*/
export type RenewSessionResult =
| { success: true; jwt: string }
| { success: false; error: string };