From fdf9286e34ffc57cf905e8dc73050107a5444d6d Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Sat, 4 Apr 2026 11:27:21 -0700 Subject: [PATCH] fix(audit): preserve source event timestamps --- .../cowork-service/src/lib/platform-client.ts | 17 +++++++++---- .../src/modules/audit/routes.test.ts | 24 +++++++++++++++++++ .../src/modules/audit/routes.ts | 1 + .../src/modules/audit/types.ts | 2 ++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/services/cowork-service/src/lib/platform-client.ts b/services/cowork-service/src/lib/platform-client.ts index b5504f6e..04c04e10 100644 --- a/services/cowork-service/src/lib/platform-client.ts +++ b/services/cowork-service/src/lib/platform-client.ts @@ -31,7 +31,9 @@ async function request(opts: FetchOptions): Promise { if (!res.ok) { const text = await res.text().catch(() => ''); - throw new Error(`platform-service ${opts.method} ${opts.path} → ${res.status}: ${text.slice(0, 200)}`); + throw new Error( + `platform-service ${opts.method} ${opts.path} → ${res.status}: ${text.slice(0, 200)}` + ); } return res.json() as Promise; @@ -43,11 +45,14 @@ export interface AuditEntry { userId: string; action: string; category?: string; + timestamp?: string; details?: Record; } /** POST /audit — fire-and-forget audit write. Returns { accepted: true }. */ -export async function postAuditEvents(entries: AuditEntry[]): Promise<{ posted: number; errors: number }> { +export async function postAuditEvents( + entries: AuditEntry[] +): Promise<{ posted: number; errors: number }> { let posted = 0; let errors = 0; for (const entry of entries) { @@ -73,7 +78,7 @@ export interface TelemetryEvent { /** POST /telemetry/events — batch ingest telemetry events. */ export async function postTelemetryEvents( - events: TelemetryEvent[], + events: TelemetryEvent[] ): Promise<{ accepted: number; rejected: number }> { if (events.length === 0) return { accepted: 0, rejected: 0 }; return request<{ accepted: number; rejected: number }>({ @@ -102,7 +107,9 @@ export async function postUsageRecord(record: UsageRecord): Promise { } /** Post multiple usage records (budget flush). */ -export async function postUsageRecords(records: UsageRecord[]): Promise<{ posted: number; errors: number }> { +export async function postUsageRecords( + records: UsageRecord[] +): Promise<{ posted: number; errors: number }> { let posted = 0; let errors = 0; for (const record of records) { @@ -120,7 +127,7 @@ export async function postUsageRecords(records: UsageRecord[]): Promise<{ posted /** GET /flags/poll — poll current flag values for this product. */ export async function pollFlags( - platform?: string, + platform?: string ): Promise<{ flags: Record; productId: string }> { const params = new URLSearchParams(); if (platform) params.set('platform', platform); diff --git a/services/platform-service/src/modules/audit/routes.test.ts b/services/platform-service/src/modules/audit/routes.test.ts index 97ec5e4f..9438dfbf 100644 --- a/services/platform-service/src/modules/audit/routes.test.ts +++ b/services/platform-service/src/modules/audit/routes.test.ts @@ -52,6 +52,30 @@ describe('auditRoutes', () => { expect(repoMock.create).toHaveBeenCalled(); }); + it('POST /audit preserves source timestamp when provided', async () => { + repoMock.create.mockResolvedValue(undefined); + const app = await buildApp(); + + const res = await app.inject({ + method: 'POST', + url: '/api/audit', + payload: { + userId: 'user_1', + action: 'tool_executed', + category: 'agent', + timestamp: '2026-04-04T17:45:00.000Z', + details: { eventId: 'evt_cowork_1', taskId: 'task_1' }, + }, + }); + + expect(res.statusCode).toBe(202); + expect(repoMock.create).toHaveBeenCalledWith( + expect.objectContaining({ + timestamp: '2026-04-04T17:45:00.000Z', + }) + ); + }); + it('POST /audit returns 400 on invalid payload', async () => { const app = await buildApp(); diff --git a/services/platform-service/src/modules/audit/routes.ts b/services/platform-service/src/modules/audit/routes.ts index 898770ba..165abba8 100644 --- a/services/platform-service/src/modules/audit/routes.ts +++ b/services/platform-service/src/modules/audit/routes.ts @@ -25,6 +25,7 @@ export async function auditRoutes(app: FastifyInstance) { id: `aud_${crypto.randomUUID()}`, productId, ...input, + timestamp: input.timestamp ?? new Date().toISOString(), createdAt: new Date().toISOString(), }; // Fire-and-forget — don't await diff --git a/services/platform-service/src/modules/audit/types.ts b/services/platform-service/src/modules/audit/types.ts index 87743e55..e33bb17b 100644 --- a/services/platform-service/src/modules/audit/types.ts +++ b/services/platform-service/src/modules/audit/types.ts @@ -11,6 +11,7 @@ export interface AuditDoc { action: string; category: string; details: Record; + timestamp?: string; ipAddress?: string; userAgent?: string; createdAt: string; @@ -22,6 +23,7 @@ export const CreateAuditSchema = z.object({ action: z.string().min(1), category: z.string().default('general'), details: z.record(z.unknown()).default({}), + timestamp: z.string().datetime().optional(), ipAddress: z.string().optional(), userAgent: z.string().optional(), });