import { createHmac, timingSafeEqual } from 'node:crypto'; import { appendFileSync, existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { BadRequestException, ForbiddenException, Injectable, ServiceUnavailableException } from '@nestjs/common'; import { APP_ROOT, PATHS } from '../common/app-root.js'; type Row = Record; export interface IngestEnvelope { schema_version: 1; sent_at: string; project_id: string; trace_id: string; receipt: Row; metric: Row; trace: Row; events: Row[]; } const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/; const FORBIDDEN_KEYS = new Set([ 'prompt', 'raw_prompt', 'assistant_summary', 'tool_input', 'tool_output', 'result_content', 'secret', 'password', 'authorization', ]); export function stableJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`; if (value && typeof value === 'object') { const record = value as Row; return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`; } return JSON.stringify(value) ?? 'null'; } export function expectedSignature(token: string, timestamp: string, body: unknown): string { return `sha256=${createHmac('sha256', token).update(`${timestamp}.${stableJson(body)}`).digest('hex')}`; } function containsForbiddenKey(value: unknown): boolean { if (Array.isArray(value)) return value.some(containsForbiddenKey); if (!value || typeof value !== 'object') return false; return Object.entries(value as Row).some(([key, nested]) => ( FORBIDDEN_KEYS.has(key.toLowerCase()) || containsForbiddenKey(nested) )); } function safeEqual(left: string, right: string): boolean { const leftBuffer = Buffer.from(left); const rightBuffer = Buffer.from(right); return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer); } function atomicJson(path: string, value: unknown): void { mkdirSync(dirname(path), { recursive: true }); const temporary = `${path}.${process.pid}.tmp`; writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 }); renameSync(temporary, path); } @Injectable() export class IngestService { ingest(raw: unknown, timestamp: string | undefined, signature: string | undefined) { const token = process.env.CASAN_CP_INGEST_TOKEN; if (!token) throw new ServiceUnavailableException('CASAN_INGEST_DISABLED'); const epoch = Number(timestamp); if (!Number.isInteger(epoch) || Math.abs(Math.floor(Date.now() / 1000) - epoch) > 300) { throw new ForbiddenException('CASAN_INGEST_TIMESTAMP_INVALID'); } const expected = expectedSignature(token, String(timestamp), raw); if (!signature || !safeEqual(signature, expected)) { throw new ForbiddenException('CASAN_INGEST_SIGNATURE_INVALID'); } if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { throw new BadRequestException('CASAN_INGEST_INVALID_ENVELOPE'); } const envelope = raw as Partial; if ( envelope.schema_version !== 1 || typeof envelope.project_id !== 'string' || typeof envelope.trace_id !== 'string' || !SAFE_ID.test(envelope.project_id) || !SAFE_ID.test(envelope.trace_id) || !envelope.metric || !envelope.trace || !Array.isArray(envelope.events) || envelope.events.length > 2000 ) throw new BadRequestException('CASAN_INGEST_INVALID_ENVELOPE'); if (containsForbiddenKey(envelope)) { throw new BadRequestException('CASAN_INGEST_RAW_CONTENT_FORBIDDEN'); } if ( envelope.metric.trace_id !== envelope.trace_id || envelope.trace.trace_id !== envelope.trace_id || envelope.events.some((event) => event.trace_id !== envelope.trace_id) ) throw new BadRequestException('CASAN_INGEST_TRACE_MISMATCH'); const tracePath = join(PATHS.traceDir, `agentic-${envelope.trace_id}.json`); if (existsSync(tracePath)) { return { accepted: true, duplicate: true, trace_id: envelope.trace_id }; } mkdirSync(dirname(PATHS.metrics), { recursive: true }); appendFileSync(PATHS.metrics, `${JSON.stringify(envelope.metric)}\n`, 'utf8'); atomicJson(tracePath, envelope.trace); if (envelope.events.length > 0) { const eventPath = join(PATHS.traceEventDir, `${envelope.trace_id}.jsonl`); mkdirSync(dirname(eventPath), { recursive: true }); appendFileSync(eventPath, `${envelope.events.map((event) => JSON.stringify(event)).join('\n')}\n`, 'utf8'); } const receiptPath = join(APP_ROOT, '.specify', 'state', 'ingested', `${envelope.trace_id}.json`); atomicJson(receiptPath, envelope.receipt ?? {}); return { accepted: true, duplicate: false, project_id: envelope.project_id, trace_id: envelope.trace_id, }; } }