fix(audit): preserve source event timestamps
This commit is contained in:
parent
eee122506c
commit
fdf9286e34
@ -31,7 +31,9 @@ async function request<T = unknown>(opts: FetchOptions): Promise<T> {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => '');
|
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<T>;
|
return res.json() as Promise<T>;
|
||||||
@ -43,11 +45,14 @@ export interface AuditEntry {
|
|||||||
userId: string;
|
userId: string;
|
||||||
action: string;
|
action: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
timestamp?: string;
|
||||||
details?: Record<string, unknown>;
|
details?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST /audit — fire-and-forget audit write. Returns { accepted: true }. */
|
/** 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 posted = 0;
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@ -73,7 +78,7 @@ export interface TelemetryEvent {
|
|||||||
|
|
||||||
/** POST /telemetry/events — batch ingest telemetry events. */
|
/** POST /telemetry/events — batch ingest telemetry events. */
|
||||||
export async function postTelemetryEvents(
|
export async function postTelemetryEvents(
|
||||||
events: TelemetryEvent[],
|
events: TelemetryEvent[]
|
||||||
): Promise<{ accepted: number; rejected: number }> {
|
): Promise<{ accepted: number; rejected: number }> {
|
||||||
if (events.length === 0) return { accepted: 0, rejected: 0 };
|
if (events.length === 0) return { accepted: 0, rejected: 0 };
|
||||||
return request<{ accepted: number; rejected: number }>({
|
return request<{ accepted: number; rejected: number }>({
|
||||||
@ -102,7 +107,9 @@ export async function postUsageRecord(record: UsageRecord): Promise<unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Post multiple usage records (budget flush). */
|
/** 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 posted = 0;
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
for (const record of records) {
|
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. */
|
/** GET /flags/poll — poll current flag values for this product. */
|
||||||
export async function pollFlags(
|
export async function pollFlags(
|
||||||
platform?: string,
|
platform?: string
|
||||||
): Promise<{ flags: Record<string, boolean>; productId: string }> {
|
): Promise<{ flags: Record<string, boolean>; productId: string }> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (platform) params.set('platform', platform);
|
if (platform) params.set('platform', platform);
|
||||||
|
|||||||
@ -52,6 +52,30 @@ describe('auditRoutes', () => {
|
|||||||
expect(repoMock.create).toHaveBeenCalled();
|
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 () => {
|
it('POST /audit returns 400 on invalid payload', async () => {
|
||||||
const app = await buildApp();
|
const app = await buildApp();
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export async function auditRoutes(app: FastifyInstance) {
|
|||||||
id: `aud_${crypto.randomUUID()}`,
|
id: `aud_${crypto.randomUUID()}`,
|
||||||
productId,
|
productId,
|
||||||
...input,
|
...input,
|
||||||
|
timestamp: input.timestamp ?? new Date().toISOString(),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
// Fire-and-forget — don't await
|
// Fire-and-forget — don't await
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export interface AuditDoc {
|
|||||||
action: string;
|
action: string;
|
||||||
category: string;
|
category: string;
|
||||||
details: Record<string, unknown>;
|
details: Record<string, unknown>;
|
||||||
|
timestamp?: string;
|
||||||
ipAddress?: string;
|
ipAddress?: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@ -22,6 +23,7 @@ export const CreateAuditSchema = z.object({
|
|||||||
action: z.string().min(1),
|
action: z.string().min(1),
|
||||||
category: z.string().default('general'),
|
category: z.string().default('general'),
|
||||||
details: z.record(z.unknown()).default({}),
|
details: z.record(z.unknown()).default({}),
|
||||||
|
timestamp: z.string().datetime().optional(),
|
||||||
ipAddress: z.string().optional(),
|
ipAddress: z.string().optional(),
|
||||||
userAgent: z.string().optional(),
|
userAgent: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user