Acknowledging changes
A model changed outside the application stays refused until someone with a reason accepts the change. Accepting it is an acknowledgement: a re-seal that records who, why, what changed and what the status was — inside the MAC’d ledger.
$result = Sentinel::for($invoice)->by($admin)->because('INC-88: refund fixed by the DBA')->acknowledge();
$result->acknowledged; // false when the model was intact (nothing written)
$result->before; // the VerificationResult it replaced
$invoice->acknowledgeTampering('INC-88: refund fixed by the DBA', $admin);AcknowledgementResult has acknowledged (false when the model was intact — nothing is written), before (the VerificationResult it replaced) and seal (the new SealResult).
Rules
- A reason is always required, trimmed, 1–sealing.reason_max_length characters (AcknowledgementDeniedException::reason / reasonTooLong).
- An actor is required outside the console (actorRequired). Without by(), the logged-in user is used; in the console without one, the actor is recorded as the system.
- The policy decides: by default the Gate ability in acknowledgement.ability (when set), called with the model and the seal name. Bind Contracts\AcknowledgementPolicy to replace it — for example with a two-person approval.
- Verify-on-retrieve is suspended for the call, so a tampered model can be loaded.
- The new ledger entry (event acknowledged) carries the previous status, the changed attribute names, the actor and the reason; TamperAcknowledged fires after commit. Editing a stored reason or actor afterwards breaks the entry’s MAC (EntryInvalid).
use Illuminate\Support\Facades\Gate;
Gate::define('acknowledge-tampering', fn (User $user, Model $model, string $seal): bool => $user->isAdmin());
// SENTINEL_ACKNOWLEDGE_ABILITY=acknowledge-tamperingFlat forms
Sentinel::acknowledge($invoice, 'INC-88: refund fixed by the DBA', $admin, 'financial'); // AcknowledgementResult
Sentinel::unseal($invoice, 'GDPR erasure #12', $admin, 'identity'); // bool: a seal row existed
Sentinel::ledgerHistory($invoice, 'financial', limit: 20); // list<LedgerRecord>, newest first (1–1000)
Sentinel::currentSeal($invoice); // ?SealRecord — the default seal, unverifiedUnsealing
unseal($reason) deletes the seal row and appends an unsealed tombstone (once — a repeated unseal writes nothing) recording the reason, the actor and the status the seal had; SealRemoved fires after commit. Removing a seal is as strong as accepting a change, so it runs the same reason, actor and policy checks. Afterwards the seal verifies Unsealed (lenient) or Missing(unsealed) (strict); a strict seal comes back through acknowledge(), never seal().
use RoundlyConsulting\Sentinel\Actions\Seals\UnsealModelAction;
use RoundlyConsulting\Sentinel\DataTransferObjects\UnsealRequest;
Sentinel::for($invoice, 'identity')->by($admin)->unseal('GDPR erasure #12');
Sentinel::unseal($invoice, 'GDPR erasure #12', $admin, 'identity');
app(UnsealModelAction::class)->execute(new UnsealRequest($invoice, 'identity', 'GDPR erasure #12', $admin));Loading a tampered model
A seal that verifies on retrieve with Reaction::Throw refuses to load the tampered row — including in route-model binding, before the acknowledging controller runs. Load it for the acknowledgement screen with find() / findOrFail(), which suspend verify-on-retrieve for that one query (global scopes stay; findOrFail() is a 404 in a route):
Route::bind('tamperedInvoice', fn (string $id) => Sentinel::model(Invoice::class)->findOrFail($id));
Route::post('/admin/invoices/{tamperedInvoice}/acknowledge', AcknowledgeInvoice::class);
$invoice = Sentinel::withoutVerification(fn () => Invoice::query()->find($id)); // any query, verify-on-retrieve offVerification stays on everywhere else; acknowledge() itself suspends it only inside its own call. Both work under the fake.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.