From ea5adcc6ca3e4ecbc8122158f7bc1e5016c72d6c Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 31 Mar 2026 23:56:35 -0700 Subject: [PATCH] =?UTF-8?q?fix(backend):=20harden=20Phase=20A=20endpoints?= =?UTF-8?q?=20=E2=80=94=20Zod=20validation,=20feature=20flag=20gate,=20sta?= =?UTF-8?q?te=20filtering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 bugs fixed in recent Phase A code: 1. POST /api/context-message: Add Zod schema validation, feature flag gate (ai_context_messages.enabled), and safe body parsing. Previously had no validation and unsafe 'as' cast that could null-ptr on missing body. 2. GET /api/timers/availability: Filter out dismissed/completed/fired timers. Previously included inactive timers in occupied intervals, causing the endpoint to report less free time than actually available. 3. agent-actions/routes.ts: Import PRODUCT_ID from product-config.ts instead of hardcoding 'chronomind' string. Ensures consistency if product identity changes. Also: Add EXTRACTION_SERVICE_URL + PLATFORM_SERVICE_URL to .env.example. All 219 backend tests pass. No breaking changes. --- backend/.env.example | 4 +++ backend/src/modules/agent-actions/routes.ts | 3 +- backend/src/modules/timers/routes.ts | 6 +++- backend/src/server.ts | 33 +++++++++++++-------- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 37bf508..dd0845b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -16,3 +16,7 @@ FIELD_ENCRYPT_KEY_PROVIDER=memory FIELD_ENCRYPT_KEY= FIELD_ENCRYPT_MEK_NAME=chronomind-mek AZURE_KEYVAULT_URL= + +# Platform + extraction services (optional — for AI context messages) +PLATFORM_SERVICE_URL=http://localhost:4003 +EXTRACTION_SERVICE_URL=http://localhost:4005 diff --git a/backend/src/modules/agent-actions/routes.ts b/backend/src/modules/agent-actions/routes.ts index dc02bd7..e235642 100644 --- a/backend/src/modules/agent-actions/routes.ts +++ b/backend/src/modules/agent-actions/routes.ts @@ -19,8 +19,7 @@ import { BatchApproveSchema, type AgentActionDoc, } from './types.js'; - -const PRODUCT_ID = 'chronomind'; +import { PRODUCT_ID } from '../../lib/product-config.js'; export async function agentActionRoutes(app: FastifyInstance) { // ── Feature flag gate ───────────────────────────────────── diff --git a/backend/src/modules/timers/routes.ts b/backend/src/modules/timers/routes.ts index be13ef1..a070d60 100644 --- a/backend/src/modules/timers/routes.ts +++ b/backend/src/modules/timers/routes.ts @@ -233,13 +233,17 @@ export async function timerRoutes(app: FastifyInstance) { } // Fetch all timers in the window (active or upcoming) - const { items: timers } = await repo.listTimers(auth.sub, PRODUCT_ID, { + const { items: allTimers } = await repo.listTimers(auth.sub, PRODUCT_ID, { limit: 100, offset: 0, sortBy: 'targetTime', sortOrder: 'asc', }); + // Exclude timers that no longer occupy time slots + const INACTIVE_STATES = new Set(['dismissed', 'completed', 'fired']); + const timers = allTimers.filter(t => !INACTIVE_STATES.has(t.state)); + // Build occupied intervals from timers that overlap the window const occupied: Array<{ start: number; end: number }> = []; for (const t of timers) { diff --git a/backend/src/server.ts b/backend/src/server.ts index 8b061de..90e1d7f 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -17,8 +17,9 @@ import { initCosmosIfNeeded } from './lib/cosmos-init.js'; import { initDatastore } from './lib/datastore.js'; import { initEncryption } from './lib/field-encrypt.js'; import { config } from './lib/config.js'; -import { getAllFlags } from './lib/feature-flags.js'; -import { generateContextMessage } from './lib/ai-context.js'; +import { getAllFlags, isFeatureEnabled } from './lib/feature-flags.js'; +import { generateContextMessage, type ContextMessageInput } from './lib/ai-context.js'; +import { z } from 'zod'; import { getBufferedEvents, flushEvents } from './lib/telemetry.js'; import { PRODUCT_ID, productConfig } from './lib/product-config.js'; @@ -58,19 +59,25 @@ await app.register(webhookRoutes, { prefix: '/api' }); await app.register(agentActionRoutes, { prefix: '/api' }); // ── Phase A.4: Context-aware AI messages ───────────────────────── -app.post('/api/context-message', async req => { - const body = req.body as { - timerLabel: string; - category?: string; - urgency?: string; - minutesBefore: number; - timeOfDay?: string; - recentTimerLabels?: string[]; - }; - if (!body.timerLabel || typeof body.minutesBefore !== 'number') { +const ContextMessageSchema = z.object({ + timerLabel: z.string().min(1).max(500), + category: z.string().max(128).optional(), + urgency: z.string().max(64).optional(), + minutesBefore: z.number().min(0).max(10080), + timeOfDay: z.string().max(64).optional(), + recentTimerLabels: z.array(z.string().max(500)).max(10).optional(), +}); + +app.post('/api/context-message', async (req, reply) => { + if (!isFeatureEnabled('ai_context_messages.enabled')) { return { message: 'Get ready!', source: 'generic' as const }; } - return generateContextMessage(body); + const parsed = ContextMessageSchema.safeParse(req.body); + if (!parsed.success) { + reply.code(400); + return { message: 'Get ready!', source: 'generic' as const }; + } + return generateContextMessage(parsed.data as ContextMessageInput); }); // ── Bootstrap (no auth) ──────────────────────────────────────────