From 7b4832b3e4d4dd6bbbf8a770e2c5f3683e22ec48 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Tue, 13 Oct 2026 17:14:00 +0200 Subject: [PATCH] feat(admin): count revoked html-view capabilities in the logout audit line The admin forced logout already revoked the user's rendered-HTML capabilities but recorded only the cookie sessions and web-tab capabilities it retired. The `user.logout` audit entry now carries `revokedHtmlViews` next to `revokedWebviews`, and the revocation test reads it back from admin-audit.jsonl. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/routes/admin-routes.ts | 4 ++-- test/webview-capability-revocation.test.ts | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/web/routes/admin-routes.ts b/src/web/routes/admin-routes.ts index 5f0705d4..582006e1 100644 --- a/src/web/routes/admin-routes.ts +++ b/src/web/routes/admin-routes.ts @@ -184,8 +184,8 @@ export function registerAdminRoutes(app: FastifyInstance, ctx: SessionPort & Aut // Web-tab proxy capabilities are a second credential the cookie purge does not // touch; a forced logout that left them alive would not be a logout. const revokedWebviews = webviewCapabilities.revokeOwner(normalizeUsername(username)); - htmlViewCapabilities.revokeOwner(normalizeUsername(username)); - audit(req, 'user.logout', username, { revoked, revokedWebviews }); + const revokedHtmlViews = htmlViewCapabilities.revokeOwner(normalizeUsername(username)); + audit(req, 'user.logout', username, { revoked, revokedWebviews, revokedHtmlViews }); return { success: true, data: { revoked } }; }); diff --git a/test/webview-capability-revocation.test.ts b/test/webview-capability-revocation.test.ts index 16135b98..17944f50 100644 --- a/test/webview-capability-revocation.test.ts +++ b/test/webview-capability-revocation.test.ts @@ -9,7 +9,9 @@ * The rendered-HTML capabilities (`htmlViewCapabilities`) are pinned at the same sites. */ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dataPath } from '../src/config/instance.js'; import { WebviewCapabilityStore, webviewCapabilities } from '../src/webview-capabilities.js'; import { HtmlViewCapabilityStore, htmlViewCapabilities } from '../src/html-view-capabilities.js'; import { createRouteTestHarness, type RouteTestHarness } from './routes/_route-test-utils.js'; @@ -157,6 +159,16 @@ describe('admin routes (multi-user)', () => { expect(htmlViewCapabilities.resolve(bossPage)).toBeDefined(); webviewCapabilities.revokeWebview('wv-boss-forced'); htmlViewCapabilities.revokeOwner('boss'); + + // The audit line counts both kinds of capability it retired. + await vi.waitFor(() => { + const entries = readFileSync(dataPath('admin-audit.jsonl'), 'utf8') + .trim() + .split('\n') + .map((line) => JSON.parse(line)); + const logout = entries.filter((e) => e.action === 'user.logout' && e.target === 'PEON').pop(); + expect(logout?.detail).toMatchObject({ revokedWebviews: 1, revokedHtmlViews: 1 }); + }); }); it('deleting a user revokes whatever that user had open', async () => {