kanvigo / audit-chronicle
Optional Chronicle bridge for Kanvigo's pluggable audit layer: a fail-closed compliance-ledger AuditSink backed by laravel-chronicle/core (hash-chained, tamper-evident, crypto-shreddable).
Requires
- php: ^8.4
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/support: ^13.0
- kanvigo/audit-contracts: ^0.1.0
- laravel-chronicle/core: 1.13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
Suggests
- laravel-chronicle/anchor-s3: S3 Object Lock (WORM) anchoring for checkpoints; the install command scaffolds it when present.
- laravel-chronicle/kms-aws: Keep the signing/encryption key material in AWS KMS instead of the local environment.
README
You run Kanvigo yourself, and someone — an auditor, a customer's security team, a regulation — now needs you to prove what happened on your board and prove the record wasn't edited after the fact. Kanvigo's built-in activity feed shows recent changes, but it's a mutable table: fine for "who moved this task", useless as evidence.
This package turns that requirement into an opt-in install. It writes every audit event into a Chronicle compliance ledger — append-only, hash-chained and tamper-evident, with signed checkpoints, optional WORM storage and per-person GDPR erasure. If you don't have this requirement, you never install it and never carry its weight.
Is this for you?
Reach for it when you need to answer "yes" to any of these:
- You have to prove integrity. A hash chain plus signed checkpoints lets you demonstrate to a third party that no entry was altered or removed — not just assert it.
- You have a retention mandate. WORM anchoring (S3 Object Lock) makes the record physically un-deletable for a fixed period, even by an admin with root.
- You operate under GDPR and still need the log. Per-subject crypto-shredding erases one person's data on request while the chain around it still verifies.
- You can't afford a silent gap. In fail-closed mode, if the ledger can't record an action, the action doesn't happen.
If none of that applies, the default Kanvigo activity feed is the right tool and you can skip this entirely.
What you're signing up for
Compliance-grade guarantees cost something, and it's fairer to name it up front:
- Every audited mutation must run inside a database transaction. Fail-closed
writes join that transaction so a failed record rolls the action back; an event
emitted outside a transaction throws. This is the price of "no record → no
action". If that's too strict,
syncmode relaxes to a best-effort write after commit. - Chronicle is young. You're taking on a dependency whose crypto you should review before you rely on it (see Before you trust it). It's pinned to an exact version for that reason.
- New moving parts in production. Signing keys, scheduled integrity checks, and — if you want them — S3 and KMS. The install command hands you the checklist.
Core Kanvigo never depends on this package. All of Chronicle's baggage
(ext-sodium / ext-openssl / AWS) lives here, behind the stable
kanvigo/audit-contracts
AuditSink interface — so nothing in your audited code knows or cares that
Chronicle is the backend, and you can swap it out later without touching a call
site.
Get it running
composer require kanvigo/audit-chronicle php artisan audit:chronicle:install
The install command publishes config, runs Chronicle's own installer, mints a
signing key, registers the sink in your config/audit.php, and prints the
production-hardening checklist. Put the printed key material in your .env and
you're recording.
Configuration
Most of what you'll touch lives in config/audit-chronicle.php:
policy—fail-closed(default) orsync. This is the big decision; see the trade-off above.categories— whichAuditCategoryvalues reach the ledger (default: all). Narrow it if you only need certain events on the record.actor_type— the reference type stamped for the actor (defaultuser; set it to yourUsermodel class to enable Chronicle reference hydration).system_subject— the stand-in subject for events that have none.
Chronicle's own chain, signing, encryption and anchoring settings live in
config/chronicle.php.
Hardening for production
The install command prints these; the short version:
- Integrity — schedule
chronicle:checkpointandchronicle:verify --since-last-checkpointso drift is caught early, not at audit time. - WORM anchoring —
composer require laravel-chronicle/anchor-s3, configure S3 Object Lock (Governance mode, 7-year retention), setCHRONICLE_ANCHORING_ENABLED=true. - GDPR erasure — enable per-subject encryption
(
CHRONICLE_ENCRYPTION_ENABLED=trueplus a dedicatedCHRONICLE_ENCRYPTION_KEY) sochronicle:subject:erase {type} {id}crypto-shreds one person: the ciphertext stays, the chain still verifies, the content is gone.
Before you trust it
Chronicle is young enough that you should review the crypto rather than take it on faith. The parts that matter:
- hash-chain construction and the canonical-payload serialization,
- checkpoint/export signature generation and verification,
- XChaCha20-Poly1305 nonce handling in the per-subject encryption,
- signing- and encryption-key storage — prefer
laravel-chronicle/kms-awsto keep key material out of the app environment.
And the escape hatch, worth knowing before you commit: because the sink sits behind
the AuditSink interface, if this package is ever abandoned you can replace the
backend without changing any audited code.
How events land in the ledger
If you're integrating or debugging, this is how each AuditEvent maps onto a
Chronicle entry:
| Audit event | Chronicle entry |
|---|---|
action (namespaced by category) |
content.status_changed, authn.login, … (dot-notation, as Chronicle requires); the raw action is kept in metadata's _audit envelope |
category |
added as a tag, and recorded in _audit |
actorId (or none) |
actor reference (actor_type, id), or Chronicle's system actor |
subjectType / subjectId |
subject reference; an event with no subject falls back to the actor, then to a synthetic system subject (Chronicle requires a subject) |
metadata, tags, context |
passed through; context carries source/IP/user-agent/token name |
occurredAt, idempotencyKey |
recorded in the _audit metadata envelope |
The bridge passes pre-resolved (type, id) references — it never hydrates a model
per write, so it keeps working even after the subject has been deleted.
Testing
composer test # Pint + PHPStan (level 6) + Pest
The suite runs on SQLite by default; set DB_CONNECTION=pgsql (plus the usual
DB_* vars) to exercise the append-only + hash-chain behaviour against PostgreSQL.
License
MIT.