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

Canonical format

These are the exact bytes Sentinel MACs or signs. The format is frozen: any change ships as sentinel.seal/2 beside version 1, verifiers keep every older format’s code path, and a file of known-answer vectors fails the build on any drift. It matters because a seal can be re-verified outside Sentinel — an auditor’s script, another language — and a seal written on SQLite verifies on PostgreSQL or MySQL.

Seal what the database holds

Attribute values are always read from the database in raw driver form — when sealing, with SELECT … FOR UPDATE inside the sealing transaction, after the host’s write; when verifying, with a fresh query or the retrieved model’s raw original attributes. Database defaults, triggers and generated columns are therefore covered, and write and read-back are identical by construction: jsonb re-ordering, numeric padding, SQLite REAL affinity, boolean representation and datetime rounding never cause a false alarm.

Normalisation per tag

Every rule rejects with CanonicalizationException rather than guessing. Null keeps the declared tag, so null ≠ '' ≠ '0' ≠ 0 ≠ false:

TagCanonical valueRejects
strThe exact bytes; must be valid UTF-8.invalid UTF-8 (invalid_utf8) — declare binary()
intDecimal without + or leading zeros; -0 → 0; arbitrary length kept exact.anything else (not_integer)
dec:NSign + integer part + . + exactly N digits; a float becomes its shortest round-trip digits, never an exponent; -0.00 → 0.00.more than N non-zero fractional digits (not_representable), NaN/INF, exponent strings
flt:NRounded half-even to N, .-separated, no thousands separator (lossy by declaration).NaN/INF
bool'1' / '0'anything but bool, 0/1, t/f, true/false (not_boolean)
dtA zone-less value is taken as written (Y-m-d\TH:i:s.uuuuuu); an offset-bearing value is converted to UTC (…\Z). The two forms never coincide.unparseable (invalid_datetime)
dateY-m-d for a date or midnight; any other time is kept, in the dt form.invalid_date
jsonJCS of the decoded value (big integers kept as strings, depth ≤ 64).invalid JSON (invalid_json), depth > 64
binbase64url—
autoint → int, bool → bool, string → str, null → null.float (float_requires_declaration)

The seal document

{"alg":"hmac-sha256","at":"2026-10-02T18:30:00.000000Z","ctx":"","f":[["a:amount","dec:2","10.50"],["a:paid","bool","1"],["a:status","str","paid"],["c:lines","json","[{\"qty\":1,\"sku\":\"A-1\"}]"]],"id":"42","kid":"default-20261002-k3f9qa","prev":null,"ring":"default","scope":"","seal":"financial","table":"invoices","type":"App\\Models\\Invoice","v":"sentinel.seal/1","ver":"7"}
  • Members (all strings or null, keys in JCS order): alg, at (sealed_at in UTC with microseconds), ctx (sentinel.context), f (field tuples sorted by name), id, kid, ring, prev (base64url SHA-256 of the previous seal’s MAC, or null), scope, seal, table (unprefixed), type (the morph class), v = sentinel.seal/1 and ver.
  • The MAC input is the UTF-8 bytes of that JCS text — RFC 8785, with exact integers instead of rounding beyond 2⁵³.
  • Domain separation binds row, model, table, seal, tenant scope, app context, version and chain; the other documents use their own v (sentinel.seal-attributes/1, sentinel.ledger/1, sentinel.checkpoint/1, sentinel.field/1, sentinel.anchor/1), and HKDF info strings differ per purpose.
  • Not bound: the connection name, which is rename-fragile — use scope() for database-per-tenant setups.

Field tags

Each field tag is the first 16 bytes of an HMAC-SHA-256 over the field’s identity and value, base64url-encoded and stored as field_tags. They are produced only for HMAC keys with field tags on; asymmetric seals store null. On a failed MAC the changed attributes are the names whose recomputed tag differs. Tampering with tags only misleads the diagnostics — no decision is ever taken from them: whether only computed values drifted is proven by a separate attribute MAC.

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.