Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions packages/audit/src/log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ vi.mock('drizzle-orm', () => ({
or: vi.fn(),
sql: vi.fn(),
}))
const { mockGetRequestContext } = vi.hoisted(() => ({
mockGetRequestContext: vi.fn(),
}))

vi.mock('@sim/logger', () => ({
createLogger: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}),
getRequestContext: mockGetRequestContext,
}))
vi.mock('@sim/utils/id', () => ({
generateId: () => 'test-uuid-123',
Expand Down Expand Up @@ -181,6 +186,43 @@ describe('recordAudit', () => {
)
})

it('records the surface the request came from', async () => {
mockGetRequestContext.mockReturnValueOnce({
requestId: 'req-1',
client: { surface: 'cli', version: '2.1.16', source: 'header' },
})

recordAudit({
workspaceId: 'ws-1',
actorId: 'user-1',
actorName: 'Test',
actorEmail: 'test@test.com',
action: AuditAction.WORKFLOW_CREATED,
resourceType: AuditResourceType.WORKFLOW,
})

await flush()

expect(dbChainMockFns.values).toHaveBeenCalledWith(expect.objectContaining({ surface: 'cli' }))
})

it('records no surface outside a request', async () => {
mockGetRequestContext.mockReturnValueOnce(undefined)

recordAudit({
workspaceId: 'ws-1',
actorId: 'user-1',
actorName: 'Test',
actorEmail: 'test@test.com',
action: AuditAction.WORKFLOW_CREATED,
resourceType: AuditResourceType.WORKFLOW,
})

await flush()

expect(dbChainMockFns.values.mock.calls.at(-1)?.[0].surface).toBeUndefined()
})

it('records null when x-forwarded-for is absent', async () => {
const request = new Request('https://example.com', {
headers: { 'x-real-ip': '10.0.0.1' },
Expand Down
7 changes: 6 additions & 1 deletion packages/audit/src/log.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { auditLog, db, user } from '@sim/db'
import { createLogger } from '@sim/logger'
import { createLogger, getRequestContext } from '@sim/logger'
import { createClientIpResolver } from '@sim/security/ip'
import { generateShortId } from '@sim/utils/id'
import { eq } from 'drizzle-orm'
Expand Down Expand Up @@ -66,6 +66,10 @@ export function recordAuditBatch(entries: AuditLogParams[]): void {
* insert paths so the write shape cannot drift between them. Actor fields
* are taken as-is — lazy actor resolution is layered on top by
* {@link recordAudit} only.
*
* The surface comes from the ambient request context, which the route wrapper
* resolves once per request, so every entry recorded while serving a request
* is attributed without each caller passing it. It is absent outside a request.
*/
function buildAuditRow(
params: AuditLogParams,
Expand All @@ -86,6 +90,7 @@ function buildAuditRow(
metadata: params.metadata ?? {},
ipAddress: params.request ? clientIpResolver.resolve(params.request.headers) : undefined,
userAgent: params.request?.headers.get('user-agent') ?? undefined,
surface: getRequestContext()?.client?.surface,
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/db/migrations/0344_audit_log_surface.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "audit_log" ADD COLUMN "surface" text;
Loading
Loading