Skip to main content

Overview

@grantex/gemma (TypeScript) and grantex-gemma (Python) provide offline authorization for Gemma 4 on-device agents. The SDK handles consent bundle creation, JWT verification without network calls, scope enforcement, tamper-evident audit logging, and cloud sync.

Installation

npm install @grantex/gemma

createConsentBundle

Create a consent bundle from the Grantex API. This is the only call that requires network connectivity.

Parameters

ParameterTypeRequiredDescription
apiKeystringYesGrantex developer API key
baseUrlstringNoAPI base URL (default https://api.grantex.dev)
agentIdstringYesAgent ID requesting the bundle
userIdstringYesEnd-user / principal ID granting consent
scopesstring[]YesScopes the agent is requesting
offlineTTLstringNoOffline validity duration (default '72h')
offlineAuditKeyAlgorithmstringNoAudit signing key algorithm (default 'Ed25519')
storagestringNo'encrypted-file', 'keychain', or 'secure-enclave'
storagePathstringNoFile path when storage is 'encrypted-file'

Returns

Promise<ConsentBundle> — see ConsentBundle type below.

Example

import { createConsentBundle } from '@grantex/gemma';

const bundle = await createConsentBundle({
  apiKey: 'gx_...',
  agentId: 'ag_01HXYZ...',
  userId: 'user_abc123',
  scopes: ['calendar:read', 'email:send'],
  offlineTTL: '72h',
});

Errors

HTTP StatusCodeCause
401UNAUTHORIZEDInvalid API key
404AGENT_NOT_FOUNDAgent ID does not exist
422INVALID_SCOPESOne or more scopes are not registered

createOfflineVerifier

Create an offline JWT verifier using a pre-fetched JWKS snapshot. No network calls are made during verification.

Parameters

ParameterTypeRequiredDescription
jwksSnapshotJWKSSnapshotYesPre-fetched JWKS keys from the consent bundle
clockSkewSecondsnumberNoAllowed clock-skew tolerance (default 30)
requireScopesstring[]NoScopes that must be present in every token
maxDelegationDepthnumberNoMaximum delegation depth allowed (inclusive)
onScopeViolation'throw' | 'log'NoBehaviour on scope failure (default 'throw')

Returns

OfflineVerifier — an object with a single verify(token: string) method.

OfflineVerifier.verify(token)

Verifies a Grantex grant token offline. Returns a VerifiedGrant on success. VerifiedGrant fields:
FieldTypeDescription
agentDIDstringAgent DID from the agt claim
principalDIDstringPrincipal ID from the sub claim
scopesstring[]Scopes from the scp claim
expiresAtDateToken expiry
jtistringToken ID
grantIdstringGrant ID (from grnt claim, falls back to jti)
depthnumberDelegation depth (0 = root grant)

Example

import { createOfflineVerifier } from '@grantex/gemma';

const verifier = createOfflineVerifier({
  jwksSnapshot: bundle.jwksSnapshot,
  requireScopes: ['calendar:read'],
  maxDelegationDepth: 2,
});

const grant = await verifier.verify(bundle.grantToken);
console.log(grant.agentDID, grant.scopes);

Errors

Error ClassCodeCause
OfflineVerificationErrorMALFORMED_TOKENJWT cannot be decoded
OfflineVerificationErrorBLOCKED_ALGORITHMnone or HS256 algorithm used
OfflineVerificationErrorMISSING_KIDJWT header has no kid
OfflineVerificationErrorKID_NOT_FOUNDNo matching key in JWKS snapshot
OfflineVerificationErrorVERIFICATION_FAILEDSignature invalid
OfflineVerificationErrorFUTURE_IATiat claim is in the future
OfflineVerificationErrorDELEGATION_DEPTH_EXCEEDEDDepth exceeds max
TokenExpiredErrorTOKEN_EXPIREDToken has expired
ScopeViolationErrorSCOPE_VIOLATIONRequired scopes missing

createOfflineAuditLog

Create an append-only, Ed25519-signed, SHA-256 hash-chained audit log backed by a JSONL file.

Parameters

ParameterTypeRequiredDescription
signingKey{ publicKey, privateKey, algorithm }YesEd25519 key pair from the consent bundle
logPathstringYesPath to the JSONL log file
maxSizeMBnumberNoMaximum file size before rotation (default 50)
rotateOnSizebooleanNoWhether to auto-rotate (default true)

Returns

OfflineAuditLog with methods:
MethodSignatureDescription
append(entry: AuditEntry) => Promise<SignedAuditEntry>Append a signed entry
entries() => Promise<SignedAuditEntry[]>Read all entries
unsyncedCount() => Promise<number>Count of un-synced entries
markSynced(upToSeq: number) => Promise<void>Mark entries as synced

AuditEntry fields

FieldTypeRequiredDescription
actionstringYesAction name (e.g. 'calendar.read')
agentDIDstringYesAgent DID that performed the action
grantIdstringYesGrant ID authorizing the action
scopesstring[]YesScopes on the grant
resultstringYesOutcome ('success', 'auth_failure', etc.)
metadataRecord<string, unknown>NoArbitrary metadata

Example

import { createOfflineAuditLog } from '@grantex/gemma';

const auditLog = createOfflineAuditLog({
  signingKey: bundle.offlineAuditKey,
  logPath: './audit.jsonl',
});

const entry = await auditLog.append({
  action: 'email.send',
  agentDID: grant.agentDID,
  grantId: grant.grantId,
  scopes: grant.scopes,
  result: 'success',
});

console.log(entry.seq, entry.hash);

storeBundle / loadBundle

Encrypt a ConsentBundle to disk with AES-256-GCM, or decrypt it back.

storeBundle

ParameterTypeDescription
bundleConsentBundleThe bundle to encrypt
pathstringFile path to write
encryptionKeystringPassphrase (hashed via SHA-256 to derive AES key)
Returns Promise<void>.

loadBundle

ParameterTypeDescription
pathstringFile path to read
encryptionKeystringPassphrase used during storeBundle
Returns Promise<ConsentBundle>. Throws BundleTamperedError if decryption or integrity check fails.

File format

[12-byte IV][16-byte GCM auth tag][AES-256-GCM ciphertext]

Example

import { storeBundle, loadBundle } from '@grantex/gemma';

await storeBundle(bundle, './bundle.enc', 'my-secret-key');
const loaded = await loadBundle('./bundle.enc', 'my-secret-key');

refreshBundle / shouldRefresh

Manage consent bundle lifecycle. shouldRefresh returns true when the bundle has less than 20% of its TTL remaining. refreshBundle calls the Grantex API to get a fresh bundle.

shouldRefresh

ParameterTypeDescription
bundleConsentBundleThe bundle to check
Returns boolean.

refreshBundle

ParameterTypeDescription
bundleConsentBundleThe bundle to refresh
apiKeystringDeveloper API key
baseUrlstringAPI base URL (default https://api.grantex.dev)
Returns Promise<ConsentBundle> — a fresh bundle with extended offlineExpiresAt, new JWKS snapshot, and rotated audit keys.

Example

import { shouldRefresh, refreshBundle, storeBundle } from '@grantex/gemma';

if (shouldRefresh(bundle)) {
  const fresh = await refreshBundle(bundle, process.env.GRANTEX_API_KEY!);
  await storeBundle(fresh, './bundle.enc', process.env.BUNDLE_KEY!);
  bundle = fresh;
}

enforceScopes / hasScope

Utility functions for scope checking.

enforceScopes

Throws ScopeViolationError if any of requiredScopes is missing from grantScopes.
import { enforceScopes } from '@grantex/gemma';

enforceScopes(grant.scopes, ['calendar:read', 'email:send']);
// Throws ScopeViolationError if 'calendar:read' or 'email:send' is missing

hasScope

Returns boolean — whether a specific scope is present.
import { hasScope } from '@grantex/gemma';

if (hasScope(grant.scopes, 'email:send')) {
  // safe to send email
}

computeEntryHash / verifyChain

Audit log integrity utilities.

computeEntryHash

Compute the SHA-256 hash of an audit entry. Hash input format:
seq|timestamp|action|agentDID|grantId|scopes|result|metadata|prevHash

verifyChain

Verify the integrity of an ordered sequence of SignedAuditEntry objects. Checks:
  1. Each entry’s hash matches the recomputed value
  2. Each entry’s prevHash matches the previous entry’s hash (first entry must use GENESIS_HASH)
  3. Sequence numbers are consecutive
Returns { valid: true } or { valid: false, brokenAt: number }.

Example

import { verifyChain } from '@grantex/gemma';

const entries = await auditLog.entries();
const result = verifyChain(entries);

if (!result.valid) {
  console.error(`Chain broken at entry ${result.brokenAt}`);
}

syncAuditLog

Sync un-synced audit log entries to the Grantex cloud in batches with exponential back-off retry.

Parameters (SyncOptions)

ParameterTypeRequiredDescription
endpointstringYesGrantex API URL
apiKeystringYesDeveloper API key
bundleIdstringYesConsent bundle ID linking entries to this session
batchSizenumberNoEntries per batch (default 100)

Returns

SyncResult:
FieldTypeDescription
syncedCountnumberTotal entries synced
hasErrorsbooleanWhether any batch failed
errorsstring[]Error messages for failed batches

withGrantexAuth (Google ADK)

Wrap a Google ADK FunctionTool with offline Grantex authorization. Before the tool executes, the grant token is verified and scopes are enforced. After execution, an audit entry is logged.
import { withGrantexAuthADK } from '@grantex/gemma';

const protectedTool = withGrantexAuthADK(myAdkTool, {
  verifier,
  auditLog,
  requiredScopes: ['calendar:read'],
  grantToken: bundle.grantToken,
});

Parameters (GoogleADKAuthOptions)

ParameterTypeDescription
verifierOfflineVerifierOffline verifier instance
auditLogOfflineAuditLogAudit log for recording actions
requiredScopesstring[]Scopes required to invoke the tool
grantTokenstringGrant token JWT

withGrantexAuth (LangChain)

Wrap a LangChain StructuredTool with offline Grantex authorization. Same behavior as the ADK adapter but wraps _call or invoke methods.
import { withGrantexAuthLangChain } from '@grantex/gemma';

const protectedTool = withGrantexAuthLangChain(myLangChainTool, {
  verifier,
  auditLog,
  requiredScopes: ['email:read'],
  grantToken: bundle.grantToken,
});

Parameters (LangChainAuthOptions)

ParameterTypeDescription
verifierOfflineVerifierOffline verifier instance
auditLogOfflineAuditLogAudit log for recording actions
requiredScopesstring[]Scopes required to invoke the tool
grantTokenstringGrant token JWT

ConsentBundle Type

interface ConsentBundle {
  bundleId: string;
  grantToken: string;
  jwksSnapshot: {
    keys: JWK[];
    fetchedAt: string;
    validUntil: string;
  };
  offlineAuditKey: {
    publicKey: string;
    privateKey: string;
    algorithm: string;
  };
  checkpointAt: number;       // Unix-ms of last cloud sync
  syncEndpoint: string;       // URL for audit sync
  offlineExpiresAt: string;   // ISO-8601 expiry
}

Error Classes

ClassCodeDescription
GrantexAuthErrorvariesBase error for all auth failures
OfflineVerificationErrorMALFORMED_TOKEN, BLOCKED_ALGORITHM, MISSING_KID, KID_NOT_FOUND, VERIFICATION_FAILED, FUTURE_IAT, DELEGATION_DEPTH_EXCEEDEDJWT verification failures
TokenExpiredErrorTOKEN_EXPIREDToken has expired beyond clock-skew window
ScopeViolationErrorSCOPE_VIOLATIONRequired scopes missing from grant
BundleTamperedErrorBUNDLE_TAMPEREDBundle file decryption or integrity check failed
HashChainErrorHASH_CHAIN_BROKENAudit log hash chain integrity failure
All error classes extend GrantexAuthError, which extends Error and includes a code property.