What is a Consent Bundle?
A consent bundle is a self-contained package that enables offline authorization. It bundles everything a device needs to verify grant tokens, enforce scopes, and produce tamper-evident audit logs without any network connectivity. Consent bundles are issued by the Grantex API after a standard authorization flow completes. The end-user must have already consented to the requested scopes before a bundle can be created.ConsentBundle Fields
| Field | Type | Description |
|---|---|---|
bundleId | string | Unique identifier for this bundle (e.g. cb_01HXYZ...) |
grantToken | string | RS256-signed Grantex grant token (JWT) |
jwksSnapshot | JWKSSnapshot | Server’s public keys for offline verification |
offlineAuditKey | OfflineAuditKey | Ed25519 key pair for signing audit entries |
checkpointAt | number | Unix timestamp (ms) of last successful cloud sync |
syncEndpoint | string | URL for uploading audit entries when reconnected |
offlineExpiresAt | string | ISO-8601 timestamp after which offline operation is disallowed |
JWKSSnapshot
| Field | Type | Description |
|---|---|---|
keys | JWK[] | Array of JSON Web Keys from the server’s JWKS endpoint |
fetchedAt | string | ISO-8601 timestamp when keys were fetched |
validUntil | string | ISO-8601 timestamp after which the snapshot should be refreshed |
OfflineAuditKey
| Field | Type | Description |
|---|---|---|
publicKey | string | PEM-encoded Ed25519 public key (also stored server-side) |
privateKey | string | PEM-encoded Ed25519 private key (device-only) |
algorithm | string | Algorithm identifier (default 'Ed25519') |
Lifecycle
A consent bundle follows a four-stage lifecycle:1. Create (Online)
The developer callsPOST /v1/consent-bundles (or createConsentBundle() in the SDK) while the device is online. The server:
- Validates the API key and agent registration
- Verifies the end-user has consented to the requested scopes
- Issues a fresh grant token
- Snapshots the current JWKS
- Generates an Ed25519 key pair (public key stored server-side, both keys returned)
- Returns the complete
ConsentBundle
2. Use (Offline)
While offline, the device loads the bundle and uses its contents:grantTokenis passed tocreateOfflineVerifierfor JWT verificationjwksSnapshotprovides the signing keys for signature validationofflineAuditKeysigns each audit entry for tamper evidenceofflineExpiresAtis checked before each operation; the device must stop operating after this time
3. Sync (Online)
When connectivity returns, the device uploads its offline audit entries viaPOST /v1/audit/offline-sync. The server:
- Validates the Ed25519 signatures using the stored public key
- Verifies the hash chain integrity
- Checks grant revocation status
- Stores accepted entries in the cloud audit log
- Returns a
SyncResultwith acceptance/rejection counts and revocation status
4. Refresh or Expire
Bundles have a finite lifetime controlled byofflineTTL. When the TTL is running low:
shouldRefresh(bundle)returnstruewhen less than 20% of the TTL remainsrefreshBundle(bundle, apiKey)calls the server to get a fresh bundle with extendedofflineExpiresAt, a new JWKS snapshot, and rotated audit keys
Storage Options
Encrypted File (Default)
AES-256-GCM encryption with a passphrase. The encryption key is derived from the passphrase via SHA-256. File format:Keychain (macOS / iOS)
On Apple platforms, use the system Keychain for hardware-backed storage. Thestorage: 'keychain' option (when supported) stores the bundle in the Keychain with kSecAttrAccessibleWhenUnlocked protection.
Best for: macOS and iOS applications.
Secure Enclave
On devices with hardware security modules, thestorage: 'secure-enclave' option stores the bundle encryption key in the Secure Enclave and only the ciphertext on disk. Requires platform-specific setup.
Best for: iOS devices with Secure Enclave, Android devices with StrongBox.
Android EncryptedSharedPreferences
Android applications should useEncryptedSharedPreferences from the Jetpack Security library. See the Android guide for details.
Encryption
The bundle contains sensitive material (grant token, Ed25519 private key). It must always be encrypted at rest. What is encrypted:- The entire
ConsentBundleJSON is serialized and encrypted as a single unit - AES-256-GCM provides both confidentiality and integrity
- The GCM auth tag detects any modification to the ciphertext
- The file path on disk is not hidden
- The file size is visible (reveals approximate bundle size)
- The encryption key derivation uses SHA-256 of the passphrase — use a strong passphrase or a hardware-generated key
BundleTamperedError is thrown if:
- The file is too short (missing IV or auth tag)
- The GCM auth tag does not match (wrong key or modified file)
- The decrypted JSON is malformed
Refresh Logic
TheshouldRefresh() function uses a simple heuristic: if less than 20% of the total TTL remains, it is time to refresh.
| Scenario | offlineTTL | Refresh strategy |
|---|---|---|
| Mobile app with intermittent connectivity | 24h | Check shouldRefresh on each app foreground |
| Raspberry Pi with daily Wi-Fi window | 72h | Cron job during expected connectivity window |
| Laptop with frequent connectivity | 8h | Background timer every 30 minutes |
| Air-gapped with weekly data transfer | 168h | Include refresh in data transfer protocol |
Bundle Revocation
Bundles can be revoked server-side viaPOST /v1/consent-bundles/:id/revoke. Revocation is checked at:
- Bundle creation — a revoked grant cannot be bundled
- Sync time — the sync response includes
revocation_status - Status check —
GET /v1/consent-bundles/:id/revocation-statusreturns the current status
Listing Bundles
Developers can list all bundles for their account:Next Steps
- Offline Authorization — architecture and security model
- Quickstart: Gemma 4 — hands-on tutorial
- Consent Bundles API — REST API endpoints
- Gemma 4 SDK Reference — full API reference