NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Sentinel for Laravel

Middleware, rule and scopes

AliasClassParametersBehaviour
sentinel.verifiedHttp\Middleware\VerifySealsnone = every route model; or param[@seal],…Verifies route models after route-model binding; a failure answers middleware.verified_status (409) with a generic message — never the status or reason.
sentinel.idempotentHttp\Middleware\EnsureIdempotencyoptional (default) or required, optional TTL secondsIdempotency keys.
sentinel.signedHttp\Middleware\VerifyHttpSignatureoptional profile nameHTTP message signatures.
sentinel.single-useHttp\Middleware\ConsumeSingleUseUrl—Single-use URLs.

Recommended order: sentinel.signed → auth → sentinel.idempotent → (route-model bindings) → sentinel.verified.

sentinel.verified

Route::get('/invoices/{invoice}', ShowInvoice::class)->middleware('sentinel.verified');
Route::put('/invoices/{invoice}/lines/{line}', UpdateLine::class)->middleware('sentinel.verified:invoice@financial');
  • It runs after route-model binding — declared as route middleware inside the web or api groups, it does. A named parameter without @seal verifies every seal of that model.
  • A failure with middleware.verified_reaction = abort (the default) throws SealVerificationFailedHttpException: status middleware.verified_status (409) and a generic translated message — the status and reason are never revealed to the client. Its getPrevious() is the TamperedModelException, for your exception handler’s report().
  • With report, the finding is reported (TamperDetected, log) and the request continues.
  • A named parameter that is unbound or not sealable throws SealingMisconfiguredException (500, fail closed).

Typed parameters

Each middleware has a static helper validated when the route is declared — a typo fails when the routes load, not at request time:

HelperValidates
VerifySeals::using(string ...$parameters)Each ^[A-Za-z0-9_]{1,64}(@[a-z0-9_.-]{1,64})?$; none = every sealable parameter.
EnsureIdempotency::optional(?int $ttl = null) / required(?int $ttl = null)TTL 60–2 592 000 seconds (the middleware’s own range).
VerifyHttpSignature::profile(?string $profile = null)The profile exists under sentinel.signatures.profiles; null = the default profile.
use RoundlyConsulting\Sentinel\Http\Middleware\EnsureIdempotency;
use RoundlyConsulting\Sentinel\Http\Middleware\VerifyHttpSignature;
use RoundlyConsulting\Sentinel\Http\Middleware\VerifySeals;

Route::put('/invoices/{invoice}', UpdateInvoice::class)->middleware(VerifySeals::using('invoice@financial'));
Route::post('/orders', StoreOrder::class)->middleware(EnsureIdempotency::required(ttl: 3600));
Route::post('/partner/events', PartnerEvents::class)->middleware(VerifyHttpSignature::profile('partners'));

IntactSeal and verifySeals()

use RoundlyConsulting\Sentinel\Rules\IntactSeal;

$request->validate([
    'invoice_id' => ['required', new IntactSeal(Invoice::class, seal: 'financial')],                        // the key
    'invoice_number' => ['required', new IntactSeal(Invoice::class, seal: 'financial', column: 'number')],  // a unique column
]);

$report = Invoice::query()->withSeals()->get()->verifySeals();   // reuses the eager-loaded seals
Invoice::query()->whereSealed()->count();
Invoice::query()->whereNotSealed('identity')->get();

new IntactSeal(string $model, ?string $seal = null, ?string $column = null): the input is the model’s key, or the value of column — an invalid column name throws SealingMisconfiguredException::invalidColumn when the rule is built, not when it runs. The model is loaded with verify-on-retrieve suspended, then verified (one seal, or every seal when none is named): a missing model fails like exists, one that is not intact with sentinel::validation.intact_seal — the status and reason are never revealed.

Macros

MacroDoes
Http::withIdempotencyKey(?string $key = null)Sends the idempotency.header header (Idempotency-Key) with the given key, or a fresh UUIDv7, serialized as an RFC 9651 string.
Http::withSignature(string $keyId, ?SigningOptions $options = null)Signs the final request (RFC 9421) with a key of the outbound ring, through SentinelManager::signRequest().
$collection->verifySeals(?string $seal = null)A VerificationReport over an Eloquent collection; reuses eager-loaded seal rows.

The HTTP-client macros are registered on PendingRequest at boot, and verifySeals() on Eloquent collections — each only when no macro of that name exists yet.

Query scopes

Invoice::query()->whereSealed()->get();               // has a seal row for the default seal
Invoice::query()->whereSealed('identity')->get();
Invoice::query()->whereNotSealed()->count();          // no seal row (unsealed or deleted)
Invoice::query()->withSeals()->get();                 // eager-load sentinelSeals

$invoice->sentinelSeals is the MorphMany relation to the stored Models\Seal rows.

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 crypto

By 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.