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_KEY=
|
||||||
FIELD_ENCRYPT_MEK_NAME=chronomind-mek
|
FIELD_ENCRYPT_MEK_NAME=chronomind-mek
|
||||||
AZURE_KEYVAULT_URL=
|
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,
|
BatchApproveSchema,
|
||||||
type AgentActionDoc,
|
type AgentActionDoc,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
import { PRODUCT_ID } from '../../lib/product-config.js';
|
||||||
const PRODUCT_ID = 'chronomind';
|
|
||||||
|
|
||||||
export async function agentActionRoutes(app: FastifyInstance) {
|
export async function agentActionRoutes(app: FastifyInstance) {
|
||||||
// ── Feature flag gate ─────────────────────────────────────
|
// ── Feature flag gate ─────────────────────────────────────
|
||||||
|
|||||||
@ -233,13 +233,17 @@ export async function timerRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all timers in the window (active or upcoming)
|
// 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,
|
limit: 100,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
sortBy: 'targetTime',
|
sortBy: 'targetTime',
|
||||||
sortOrder: 'asc',
|
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
|
// Build occupied intervals from timers that overlap the window
|
||||||
const occupied: Array<{ start: number; end: number }> = [];
|
const occupied: Array<{ start: number; end: number }> = [];
|
||||||
for (const t of timers) {
|
for (const t of timers) {
|
||||||
|
|||||||
@ -17,8 +17,9 @@ import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
|||||||
import { initDatastore } from './lib/datastore.js';
|
import { initDatastore } from './lib/datastore.js';
|
||||||
import { initEncryption } from './lib/field-encrypt.js';
|
import { initEncryption } from './lib/field-encrypt.js';
|
||||||
import { config } from './lib/config.js';
|
import { config } from './lib/config.js';
|
||||||
import { getAllFlags } from './lib/feature-flags.js';
|
import { getAllFlags, isFeatureEnabled } from './lib/feature-flags.js';
|
||||||
import { generateContextMessage } from './lib/ai-context.js';
|
import { generateContextMessage, type ContextMessageInput } from './lib/ai-context.js';
|
||||||
|
import { z } from 'zod';
|
||||||
import { getBufferedEvents, flushEvents } from './lib/telemetry.js';
|
import { getBufferedEvents, flushEvents } from './lib/telemetry.js';
|
||||||
import { PRODUCT_ID, productConfig } from './lib/product-config.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' });
|
await app.register(agentActionRoutes, { prefix: '/api' });
|
||||||
|
|
||||||
// ── Phase A.4: Context-aware AI messages ─────────────────────────
|
// ── Phase A.4: Context-aware AI messages ─────────────────────────
|
||||||
app.post('/api/context-message', async req => {
|
const ContextMessageSchema = z.object({
|
||||||
const body = req.body as {
|
timerLabel: z.string().min(1).max(500),
|
||||||
timerLabel: string;
|
category: z.string().max(128).optional(),
|
||||||
category?: string;
|
urgency: z.string().max(64).optional(),
|
||||||
urgency?: string;
|
minutesBefore: z.number().min(0).max(10080),
|
||||||
minutesBefore: number;
|
timeOfDay: z.string().max(64).optional(),
|
||||||
timeOfDay?: string;
|
recentTimerLabels: z.array(z.string().max(500)).max(10).optional(),
|
||||||
recentTimerLabels?: string[];
|
});
|
||||||
};
|
|
||||||
if (!body.timerLabel || typeof body.minutesBefore !== 'number') {
|
app.post('/api/context-message', async (req, reply) => {
|
||||||
|
if (!isFeatureEnabled('ai_context_messages.enabled')) {
|
||||||
return { message: 'Get ready!', source: 'generic' as const };
|
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) ──────────────────────────────────────────
|
// ── Bootstrap (no auth) ──────────────────────────────────────────
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user