From 51089b22496b7d6fe07af127dc0c7aa3486974c4 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 29 Sep 2026 08:52:05 +0200 Subject: [PATCH] Guard audit writes when advisory lock is unavailable --- packages/platform/src/audit.ts | 3 ++- packages/platform/test/audit-lock.test.ts | 31 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/platform/test/audit-lock.test.ts diff --git a/packages/platform/src/audit.ts b/packages/platform/src/audit.ts index fde97a3..4b4ca90 100644 --- a/packages/platform/src/audit.ts +++ b/packages/platform/src/audit.ts @@ -36,7 +36,8 @@ export async function audit(e: AuditInput, conn?: PoolConnection): Promise const c = conn ?? (await pool.getConnection()); try { if (own) await c.beginTransaction(); - await c.query('SELECT GET_LOCK(?, 10)', ['kc_audit_chain']); + const [locks] = await c.query('SELECT GET_LOCK(?, 10) AS acquired', ['kc_audit_chain']); + if (Number(locks[0]?.acquired) !== 1) throw new Error('Audit lock unavailable'); const last = await one<{ hash: string } & import('mysql2').RowDataPacket>('SELECT hash FROM audit_events ORDER BY id DESC LIMIT 1', [], c); const prev = last?.hash ?? '0'.repeat(64); const ts = new Date(); diff --git a/packages/platform/test/audit-lock.test.ts b/packages/platform/test/audit-lock.test.ts new file mode 100644 index 0000000..0cbc322 --- /dev/null +++ b/packages/platform/test/audit-lock.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from 'vitest'; + +const connection = vi.hoisted(() => ({ + beginTransaction: vi.fn(), + query: vi.fn(), + execute: vi.fn(), + commit: vi.fn(), + rollback: vi.fn(), + release: vi.fn(), +})); + +vi.mock('../src/db.js', () => ({ + pool: { getConnection: vi.fn(async () => connection) }, + one: vi.fn(), + query: vi.fn(), +})); + +import { audit } from '../src/audit.js'; + +describe('audit advisory lock', () => { + it.each([0, null])('does not append an event when GET_LOCK returns %s', async (acquired) => { + vi.clearAllMocks(); + connection.query.mockResolvedValueOnce([[{ acquired }]]).mockResolvedValueOnce([[{ released: null }]]); + + await expect(audit({ actorType: 'system', action: 'test.lock' })).rejects.toThrow('Audit lock unavailable'); + expect(connection.execute).not.toHaveBeenCalled(); + expect(connection.commit).not.toHaveBeenCalled(); + expect(connection.rollback).toHaveBeenCalledOnce(); + expect(connection.release).toHaveBeenCalledOnce(); + }); +});