The Security Gap
Consider a concrete example: a home automation agent running Gemma 4 on a Raspberry Pi. The user has consented to let the agent read temperature sensors and control the thermostat. The Pi connects to Wi-Fi daily but operates offline most of the time. With a cloud-hosted agent, authorization is straightforward:- The agent calls the thermostat API
- The API verifies the grant token with Grantex
- If the token is valid and the scopes match, the action proceeds
- An audit entry is logged in the cloud
- The agent calls the thermostat API (local network, or direct GPIO)
- Who verifies the grant token? The Grantex API is unreachable
- Who checks whether the grant has been revoked? The revocation list is on the server
- Who logs the action? The cloud audit log is unreachable
Consent Bundles: The Missing Piece
The core insight is that offline authorization does not need a live server — it needs a pre-authorized cryptographic package that the device can verify locally. We call this a consent bundle. It contains:- A signed grant token (RS256 JWT) — the same token format Grantex uses online, with all the standard claims (
agt,sub,scp,grnt,exp) - A JWKS snapshot — the server’s public keys at the time of issuance, used to verify the token’s RSA signature without a network call
- An Ed25519 key pair — used to sign every offline audit entry, so the server can verify their authenticity at sync time
- An offline expiry — a hard deadline after which the bundle is no longer valid, separate from the token’s own
expclaim
How It Works
Here is the complete flow on a Raspberry Pi running the Python SDK:verifier.verify() call runs in under 5ms on a Pi 5. It decodes the JWT, resolves the signing key from the JWKS snapshot by kid, verifies the RS256 signature, checks expiry with clock-skew tolerance, and enforces required scopes. No network call. No latency spike.
The audit log appends a JSONL entry with a SHA-256 hash chain linking each entry to the previous one, plus an Ed25519 signature from the key pair in the bundle. Tampering with any entry breaks the chain and invalidates all subsequent signatures.
What Happens at Sync Time
When the Pi reconnects, it uploads the offline audit entries:-
Verifies the hash chain — each entry’s
prevHashmust match the previous entry’shash. If any entry has been modified, inserted, or deleted, the chain breaks. - Verifies Ed25519 signatures — the server stored the public key when the bundle was created. It verifies that each entry was signed by the corresponding private key. This proves the entries came from the device that received the original bundle.
-
Checks revocation — if the grant was revoked while the device was offline, the response includes
revocation_status: "revoked"with a timestamp. The device learns that it should stop operating and can determine which actions occurred before versus after the revocation.
What About Revocation?
This is the honest limitation: if a grant is revoked while the device is offline, the device will not know until it syncs. During that window, the agent continues operating with the cached token. This is inherent to any offline system. Even certificate revocation in TLS has the same problem: if you cannot reach the CRL or OCSP responder, you are working with stale revocation data. The mitigation is straightforward: use shortofflineTTL values for sensitive operations. A medical device agent should use 1h, not 72h. A home automation agent with daily Wi-Fi connectivity can safely use 72h.
The shouldRefresh() function alerts when 80% of the TTL has elapsed, giving the device time to refresh the bundle during the next connectivity window.
Platform Coverage
@grantex/gemma (TypeScript) and grantex-gemma (Python) ship today. They work anywhere Gemma 4 runs:
| Platform | SDK | Storage | Notes |
|---|---|---|---|
| Raspberry Pi | Python | AES-256-GCM encrypted file | ~3ms verify on Pi 5 |
| Linux / macOS | TypeScript or Python | Encrypted file | Server-side agents with intermittent connectivity |
| Android | Kotlin (Nimbus JOSE) | EncryptedSharedPreferences | Hardware-backed Keystore |
| iOS | Swift (CryptoKit) | Keychain | Secure Enclave on supported devices |
withGrantexAuth() to add offline verification and audit logging automatically.