The Sentinel facade
Everything goes through one facade, RoundlyConsulting\Sentinel\Facades\Sentinel — import it, there is no global alias. for($model) returns a handle bound to one seal, flat verbs act on the model you hand them, model($class) works on every row of a model, and five sub-accessors group the rest: keys(), ledger(), idempotency(), nonces() and signatures().
use RoundlyConsulting\Sentinel\Enums\Algorithm;
use RoundlyConsulting\Sentinel\Facades\Sentinel;
// One model: a handle bound to a seal (the default one when unnamed)
Sentinel::for($invoice)->verify();
Sentinel::for($invoice, 'identity')->verify();
Sentinel::for($invoice)->by($admin)->because('INC-88: refund fixed by the DBA')->acknowledge();
// Flat verbs act on the model you hand them
Sentinel::seal($invoice, reason: 'Recomputed lines');
Sentinel::verify($invoice, 'financial');
Sentinel::verifyAll($invoice)->allIntact();
// Every row of a model
Sentinel::model(Invoice::class)->scan();
// Sub-accessors
Sentinel::keys()->ring('http')->import('acme-2026-10', Algorithm::Ed25519, $partnerPublicKeyPem, owner: $partner);
Sentinel::ledger()->checkpoint();
Sentinel::idempotency()->run("charge:{$order->id}", scope: 'billing', callback: fn () => $gateway->charge($order));
Sentinel::nonces()->issue('password-reset', ttl: 900, subject: $user);
Sentinel::signatures()->current($request);Model seals
A seal name the model does not declare throws SealingMisconfiguredException::unknownSeal (the message lists the declared seals); a null seal is the model’s default.
| Method | Returns | Does |
|---|---|---|
for(Model $model, ?string $seal = null) | SealHandle | A handle bound to one seal (the default one when unnamed). |
model(string $class) | ModelSeals | Class-level operations over every row of a model. |
sealables() | list<class-string<Model>> | sentinel.models first, then every class with seal rows or ledger entries. |
check() | HealthReport | The installation health check. |
seal(Model $model, ?string $seal = null, ?string $reason = null, ?Model $actor = null) | SealResult | Explicit seal; refused on a tampered model. |
verify(Model $model, ?string $seal = null) | VerificationResult | Verify one seal. |
verifyOrFail(Model $model, ?string $seal = null) | VerificationResult | Verify or throw TamperedModelException. |
verifyAll(Model $model) | VerificationReport | Every declared seal of one model. |
verifyMany(iterable $models, ?string $seal = null) | VerificationReport | Many models; a null seal = every seal of each. |
isIntact(Model $model) | bool | True when every seal is intact. |
acknowledge(Model $model, string $reason, ?Model $actor = null, ?string $seal = null) | AcknowledgementResult | Accept an out-of-band change with a reason. |
unseal(Model $model, string $reason, ?Model $actor = null, ?string $seal = null) | bool | Remove a seal deliberately; returns whether a seal row existed. |
ledgerHistory(Model $model, ?string $seal = null, int $limit = 50) | list<LedgerRecord> | Ledger records, newest first (limit 1–1000). |
currentSeal(Model $model, ?string $seal = null) | ?SealRecord | The stored seal row, unverified. |
scan(ScanOptions $options) | ScanReport | Chunked verification of every row. |
reseal(ResealOptions $options) | ResealReport | Re-seal intact rows with the current key or definition. |
resealWhere(ResealWhereRequest $request) | ResealReport | Acknowledge every selected row. |
updateAndReseal(UpdateAndResealRequest $request) | ResealReport | A verified mass update. |
sealMissing(BaselineOptions $options) | ResealReport | Baseline rows that never had a seal. |
withoutSealing(Closure $callback, string $reason) | mixed | Run the callback with sealing suspended. |
withoutVerification(Closure $callback) | mixed | Run the callback with verify-on-retrieve suspended. |
$result = Sentinel::seal($invoice, reason: 'Recomputed lines'); // SealResult
$result = Sentinel::verify($invoice); // the default seal
$result = Sentinel::verifyOrFail($invoice, 'financial');
$report = Sentinel::verifyAll($invoice); // every seal
$report = Sentinel::verifyMany(Invoice::query()->latest()->limit(50)->get(), 'financial');
$ok = Sentinel::isIntact($invoice);
$removed = Sentinel::unseal($invoice, 'GDPR erasure #12', $admin, 'identity');
$history = Sentinel::ledgerHistory($invoice, 'financial', limit: 20); // list<LedgerRecord>
$stored = Sentinel::currentSeal($invoice); // ?SealRecord
$classes = Sentinel::sealables(); // [Invoice::class, …]Keys
| Method | Returns | Does |
|---|---|---|
keys() | KeysAccessor | Rings, key inventory and ring handles. |
generateKey(GenerateKeyRequest $request) | GeneratedKey | Generate a key (database by default, or env lines). |
importKey(ImportKeyRequest $request) | KeyInfo | Import a partner’s (or your own) key. |
rotateKey(RotateKeyRequest $request) | RotationResult | Rotate a ring’s signing key. |
revokeKey(RevokeKeyRequest $request) | KeyInfo | Revoke a database key (reason required). |
retireKey(string $ring, string $keyId) | KeyInfo | End a database key’s verification period now. |
listKeys(?string $ring = null) | list<KeyInfo> | Null = every ring; never material. |
findKey(string $ring, string $keyId) | ?KeyInfo | One key in one ring. |
currentKey(?string $ring = null) | KeyInfo | The signing key, or NoSigningKeyException. |
extend(string $driver, Closure $factory) | static | Register a key-store driver. |
Ledger
| Method | Returns | Does |
|---|---|---|
ledger() | LedgerAccessor | Checkpoints, verification, history, head, anchors. |
checkpoint(?CheckpointOptions $options = null) | ?CheckpointResult | One batch; null = nothing pending. |
verifyLedger(?LedgerVerifyOptions $options = null) | LedgerReport | Checkpoints, anchors, pending entries, entity heads. |
ledgerHead(?string $connection = null) | ?CheckpointRecord | The newest checkpoint, unverified. |
anchors() | list<string> | The configured anchor names. |
extendAnchor(string $driver, Closure $factory) | static | Register an anchor driver. |
Idempotency, nonces and pruning
| Method | Returns | Does |
|---|---|---|
idempotency() | IdempotencyAccessor | run() and forget() for jobs, commands and webhooks. |
runIdempotent(IdempotentCall $call) | IdempotentResult | Run a callback at most once per key and scope. |
forgetIdempotencyKey(string $key, string $scope) | bool | Whether the key existed. |
nonces() | NoncesAccessor | Issue, consume, single-use URLs. |
issueNonce(IssueNonceRequest $request) | IssuedNonce | A purpose-bound, single-use token. |
consumeNonce(ConsumeNonceRequest $request) | bool | True exactly once. |
signedRoute(SignedRouteRequest $request) | string | A single-use signed URL. |
prune(?PruneOptions $options = null) | PruneResult | Delete expired idempotency keys and nonces. |
HTTP message signatures
| Method | Returns | Does |
|---|---|---|
signatures() | SignaturesAccessor | Sign, verify, current, owner, contentDigest. |
signRequest(RequestInterface $request, string $keyId, ?SigningOptions $options = null) | RequestInterface | Sign any PSR-7 request. |
verifyRequestSignature(Request $request, ?string $profile = null) | VerifiedSignature | Or HttpSignatureException (401). |
verifyResponseSignature(ResponseInterface|ClientResponse $response, ?string $profile = null) | VerifiedSignature | A signed response (PSR-7 or Laravel client). |
verifiedSignature(Request $request) | ?VerifiedSignature | What sentinel.signed verified on this request. |
signatureOwner(Request|VerifiedSignature $from) | ?Model | The owner of the signing key. |
Handles and sub-accessors
All are final readonly objects returned by the manager; each method calls one manager method. A ring handle refuses a key id of another ring (UnknownKeyException), and a ModelSeals query of another model class is refused (SealingMisconfiguredException::queryModelMismatch). ModelSeals methods take the same defaults as the option objects (chunk 500, checkLedger true, maxFindings 1000); checkSchema and upgradeFormat exist only on ScanOptions / ResealOptions and the commands.
| Entry | Class | Methods |
|---|---|---|
Sentinel::for($model, ?$seal) | SealHandle | by(?Model $actor), because(string $reason), name(), definition(), seal(), verify(), verifyOrFail(), isIntact() (this seal only), acknowledge(?string $reason = null), unseal(?string $reason = null), current(), history(int $limit = 50) |
Sentinel::model(Invoice::class) | ModelSeals | definition(?string $seal = null), seals(), scan(…), reseal(…), resealWhere(…), updateAndReseal(…), sealMissing(…), find($id), findOrFail($id), unsealedQuery(?string $seal = null) |
Sentinel::keys() | Accessors\KeysAccessor | ring(?string $ring = null), all(), rings() |
Sentinel::keys()->ring($ring) | Accessors\KeyRingHandle | name(), current(), find(string $keyId), all(), generate(…), import(…), rotate(?Algorithm $algorithm = null, ?CarbonInterface $activatesAt = null), revoke(string $keyId, string $reason, ?Model $actor = null), retire(string $keyId) |
Sentinel::ledger() | Accessors\LedgerAccessor | checkpoint(?string $connection = null), verify(?string $connection = null, bool $entities = true, int $chunk = 1000), history(Model $model, ?string $seal = null, int $limit = 50), head(?string $connection = null), anchors() |
Sentinel::idempotency() | Accessors\IdempotencyAccessor | run(string $key, string $scope, Closure $callback, ?string $fingerprint = null, ?int $ttl = null, ?int $lease = null), forget(string $key, string $scope) |
Sentinel::nonces() | Accessors\NoncesAccessor | issue(string $purpose, ?int $ttl = null, ?Model $subject = null), consume(…), consumeOrFail(…), signedRoute(string $name, array $parameters = [], ?int $ttl = null) |
Sentinel::signatures() | Accessors\SignaturesAccessor | sign(…), verify(Request $request, ?string $profile = null), verifyResponse(…), current(Request $request), owner(Request|VerifiedSignature $from), contentDigest(string $body, DigestAlgorithm $algorithm = DigestAlgorithm::Sha256) |
The model trait shorthand
The HasSeals trait, its query scopes and the verifySeals() collection macro call the same manager — so Sentinel::fake() sees them too:
$invoice->seal('financial'); // SealResult
$invoice->verifySeal('identity'); // VerificationResult
$invoice->verifySealOrFail(); // TamperedModelException when not intact
$invoice->isIntact(); // every seal
$invoice->acknowledgeTampering('INC-88: refund fixed by the DBA', $admin);
Invoice::query()->whereSealed()->count();
Invoice::query()->whereNotSealed('identity')->get();
Invoice::query()->withSeals()->get()->verifySeals();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.