fix(backend): harden Phase A endpoints — Zod validation, feature flag gate, state filtering
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.
This commit is contained in:
parent
c0e576e15c
commit
ea5adcc6ca
@ -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
|
||||
|
||||
@ -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 ─────────────────────────────────────
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) ──────────────────────────────────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user