laravel-chronicle/core
Chronicle provides cryptographically verifiable audit logging for Laravel. It records events in an append-only, hash-chained ledger to make tampering detectable, with features like verifiable exports, signed checkpoints, key rotation, and external anchoring.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require laravel-chronicle/core
php artisan chronicle:install
--migrate flag if needed) and configure .env with signing keys.CHRONICLE_PRIVATE_KEY and CHRONICLE_PUBLIC_KEY are set (or use chronicle:key:generate to create a new keypair).First Record:
use Chronicle\Facades\Chronicle;
Chronicle::record()
->actor(auth()->user())
->action('user.login')
->subject(null) // No subject if irrelevant
->commit();
Automatic Model Auditing:
Add HasChronicle trait to a model (e.g., User):
use Chronicle\Eloquent\HasChronicle;
class User extends Model
{
use HasChronicle;
}
Now created, updated, and deleted events are auto-logged.
Structured Logging:
Chronicle::record() for custom events with metadata/tags:
Chronicle::record()
->actor($adminUser)
->action('payment.processed')
->subject($payment)
->metadata(['amount' => $payment->amount])
->tags(['finance', 'audit'])
->commit();
['finance']) for filtering.Model Observers: For third-party models, register observers:
Chronicle::observe(Order::class, function ($model) {
return [
'action' => match ($model->event) {
'created' => 'order.created',
'updated' => 'order.updated',
'deleted' => 'order.deleted',
},
'subject' => $model,
];
});
Querying:
// Find all actions by a user
Chronicle\Entry\Entry::forActor($user)->get();
// Stream large datasets
Chronicle\Entry\Entry::stream()->each(fn ($entry) => /* ... */);
Checkpoints & Anchoring:
chronicle:checkpoint via cron (e.g., daily) to create verifiable snapshots:
* * * * * php artisan chronicle:checkpoint --anchor
config/chronicle.php for external tamper-proofing.Verification:
php artisan chronicle:verify --since-last-checkpoint
--checkpoints-only --anchors for a lightweight audit.Event Dispatching:
Tie Chronicle to Laravel events (e.g., OrderPaid):
event(new OrderPaid($order));
// In EventServiceProvider:
protected $listen = [
OrderPaid::class => [\App\Listeners\LogOrderPayment::class],
];
// Listener
public function handle(OrderPaid $event) {
Chronicle::record()
->actor($event->user)
->action('order.paid')
->subject($event->order)
->commit();
}
API Audit Trails: Use middleware to log API requests:
public function handle($request, Closure $next) {
$response = $next($request);
Chronicle::record()
->actor($request->user())
->action('api.request')
->subject($request->route())
->metadata(['method' => $request->method(), 'path' => $request->path()])
->commit();
return $response;
}
Retention Policies: Prune old entries (e.g., older than 2 years):
php artisan chronicle:prune --older-than=2years --dry-run
--dry-run first, then remove flag for execution.Key Rotation: Rotate keys during deployments:
# Generate new key
php artisan chronicle:key:generate --id=chronicle-key-2
# Rotate (creates boundary checkpoint)
php artisan chronicle:key:rotate chronicle-key-2
# Update config/chronicle.php:
'active' => 'chronicle-key-2'
Exports for Compliance: Generate verifiable exports for auditors:
php artisan chronicle:export storage/app/chronicle-export-$(date +%Y%m%d)
entries.ndjson, manifest.json, and signature.json for offline verification.Missing ext-sodium/ext-openssl:
Class 'Chronicle\Signing\Ed25519SigningProvider' not found.sudo apt install php-sodium php-openssl or equivalent).Key Configuration:
CHRONICLE_ACTIVE_KEY or including private_key in retired keys.chronicle:key:list to verify the ring:
php artisan chronicle:key:list --with-counts
private_key: null but keep public_key.Database Transactions:
DB_TRANSACTION_TIMEOUT in .env.ULID Collisions:
Chronicle::record()->ulid()).Anchoring Failures:
chronicle:anchor:retry and monitor chronicle:anchor:verify.Metadata Serialization:
->withoutMetadata() for problematic fields.Verify Ledger:
php artisan chronicle:verify --since-last-checkpoint
php artisan chronicle:verify --entry=01H5Z...
Inspect Entries:
php artisan chronicle:show 01H5Z...
php artisan chronicle:stats
Logs:
config/chronicle.php:
'logging' => [
'level' => 'debug',
],
storage/logs/chronicle.log for errors.Custom Signing Providers:
Chronicle\Signing\SigningProviderInterface for KMS/HSM:
class GcpKmsSigningProvider implements SigningProviderInterface {
public function sign(string $data): string { /* ... */ }
public function verify(string $data, string $signature): bool { /* ... */ }
}
config/chronicle.php:
'keys' => [
'gcp-kms-key' => [
'provider' => App\Providers\GcpKmsSigningProvider::class,
'algorithm' => 'ed25519',
'private_key' => null, // Handled by KMS
'public_key' => env('GCP_KMS_PUBLIC_KEY'),
],
],
Anchoring Providers:
class EthereumAnchor implements AnchoringProviderInterface {
public function anchor(string $digest): ?string { /* ... */ }
public function verify(string $anchorId, string $digest): bool { /* ... */ }
}
Query Scopes:
use Chronicle\Entry\Entry;
Entry::macro('forIp', function ($ip) {
return $this->whereHas('metadata', fn ($q) => $q->where('ip', $ip));
});
Event Modifiers:
How can I help you explore Laravel packages today?