From f5756dbaa39566ff7673c0e9668ca8e068628193 Mon Sep 17 00:00:00 2001 From: DanMat Date: Sun, 16 Aug 2026 11:00:50 -0400 Subject: [PATCH] Retention: prune audit rows older than API_AUDIT_RETENTION_DAYS Registers a `nimbus prune` maintenance task (the new plugin maintenance capability) that deletes api_audit_log rows older than the retention window (API_AUDIT_RETENTION_DAYS, default 30; 0 keeps everything). Boundary test asserts the task is registered on load. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 2 ++ src/ApiAdvancedPlugin.php | 14 ++++++++++++++ tests/PackageIntegrationTest.php | 12 ++++++++---- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e764f1b..f13c851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,3 +13,5 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). the plugin owns — the reason/IP/path for a rejection, and the token id/name and `resource:action` for a scope denial (never the presented token). An **API audit** admin page shows a 24-hour summary and the most recent failures. +- Retention: a `nimbus prune` maintenance task drops audit rows older than + `API_AUDIT_RETENTION_DAYS` (default 30; `0` keeps everything). diff --git a/src/ApiAdvancedPlugin.php b/src/ApiAdvancedPlugin.php index 2520d20..5210aed 100644 --- a/src/ApiAdvancedPlugin.php +++ b/src/ApiAdvancedPlugin.php @@ -57,5 +57,19 @@ static function (mixed $payload) use ($recorder): void { '🛡️', static fn (Request $request): string => (new AuditView())->html($log->recent(), $log->summary()), ); + + // Retention: `nimbus prune` drops audit rows older than the window + // (API_AUDIT_RETENTION_DAYS, default 30; 0 keeps everything). + $context->maintenance()->register('prune-audit', static function () use ($storage): int { + $days = (int) (getenv('API_AUDIT_RETENTION_DAYS') ?: '30'); + if ($days <= 0) { + return 0; + } + + return $storage()->execute( + 'DELETE FROM ' . Schema::TABLE . ' WHERE occurred_at < :cutoff', + ['cutoff' => date('Y-m-d H:i:s', (int) strtotime("-{$days} days"))], + ); + }); } } diff --git a/tests/PackageIntegrationTest.php b/tests/PackageIntegrationTest.php index 235b4ca..454baa3 100644 --- a/tests/PackageIntegrationTest.php +++ b/tests/PackageIntegrationTest.php @@ -10,6 +10,7 @@ use Nimbus\Plugin\PluginLoader; use Nimbus\Support\CoreEvents; use Nimbus\Support\EventDispatcher; +use Nimbus\Support\MaintenanceRegistry; use NimbusCMS\ApiAdvanced\ApiAdvancedPlugin; use PHPUnit\Framework\TestCase; @@ -69,17 +70,19 @@ public function test_the_package_is_typed_as_a_nimbus_plugin(): void self::assertSame('nimbuscms-plugin', $this->manifest()['type']); } - public function test_discovery_registers_the_migration_listeners_and_admin_page(): void + public function test_discovery_registers_the_migration_listeners_admin_page_and_retention(): void { - $migrations = new MigrationRegistry(); - $events = new EventDispatcher(); - $adminPages = new AdminPageRegistry(); + $migrations = new MigrationRegistry(); + $events = new EventDispatcher(); + $adminPages = new AdminPageRegistry(); + $maintenance = new MaintenanceRegistry(); $loader = new PluginLoader($this->installedAs()); $diagnostics = $loader->load(new PluginCapabilities( migrations: $migrations, events: $events, adminPages: $adminPages, + maintenance: $maintenance, )); self::assertSame([], $diagnostics, 'a correctly installed package must load cleanly'); @@ -89,6 +92,7 @@ public function test_discovery_registers_the_migration_listeners_and_admin_page( self::assertTrue($events->hasListeners(CoreEvents::API_TOKEN_REJECTED), 'the rejection listener'); self::assertTrue($events->hasListeners(CoreEvents::API_ACCESS_DENIED), 'the scope-denial listener'); self::assertSame(['api-audit'], array_column($adminPages->all(), 'slug'), 'its admin page'); + self::assertSame(['nimbuscms.api-advanced:prune-audit'], array_column($maintenance->all(), 'name'), 'its retention task'); } public function test_disabling_the_package_registers_nothing(): void