New:Socket for Asana Is Now Available.Learn more
Get Started

@grantex/gemma

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@grantex/gemma

Offline authorization adapter for Google Gemma on-device AI agents

latest
Source
npmnpm
Version
0.1.1
Version published
Maintainers
1
Created
Source

@grantex/gemma

Offline authorization adapter for Google Gemma on-device AI agents -- issue consent bundles online, verify grant tokens and enforce scopes entirely offline, and sync tamper-evident audit logs back to Grantex cloud when connectivity returns.

npm version License npm downloads Tests Node.js

Homepage | Docs | API Reference | GitHub | Sign Up Free

What is @grantex/gemma?

Google Gemma 4 is a family of lightweight, open-weight models designed to run directly on edge devices -- phones, Raspberry Pis, Jetson boards, and embedded hardware. When an AI agent built on Gemma operates in the field, it often loses internet connectivity for hours or days at a time. Traditional cloud-based authorization (OAuth 2.0, API-key checks) simply stops working the moment the network drops.

@grantex/gemma solves this by bringing the Grantex delegated authorization protocol to offline environments. While the device is still online, it fetches a consent bundle -- a self-contained package containing a signed grant token, a JWKS snapshot for signature verification, and an Ed25519 key pair for audit signing. Once offline, the agent verifies tokens, enforces scopes, and logs every action to a tamper-evident, hash-chained audit log -- all without a single network call.

The lifecycle follows three phases: (1) Online bundle issue -- fetch the consent bundle and persist it with AES-256-GCM encryption, (2) Offline verify + audit -- verify JWTs, enforce scopes, and sign audit entries locally, and (3) Sync -- when connectivity returns, upload the audit log to the Grantex cloud with batched retry. This package also ships framework adapters for Google ADK and LangChain, so you can wrap any tool with offline authorization in a single function call.

Installation

npm install @grantex/gemma
yarn add @grantex/gemma
pnpm add @grantex/gemma

Quick Start

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

// While the device is online, request a consent bundle from Grantex
const bundle = await createConsentBundle({
  apiKey: process.env.GRANTEX_API_KEY!,
  agentId: 'agent_gemma_01',
  userId: 'user_alice',
  scopes: ['calendar:read', 'email:send'],
  offlineTTL: '72h', // bundle is valid for 72 hours offline
});

// Persist the bundle to encrypted local storage (AES-256-GCM)
await storeBundle(bundle, '/data/grantex/bundle.enc', process.env.ENCRYPTION_KEY!);

Phase 2 -- Offline: Verify Tokens and Log Actions

import {
  loadBundle,
  createOfflineVerifier,
  createOfflineAuditLog,
  enforceScopes,
} from '@grantex/gemma';

// Load the bundle from encrypted storage
const bundle = await loadBundle('/data/grantex/bundle.enc', process.env.ENCRYPTION_KEY!);

// Create the offline verifier using the embedded JWKS snapshot
const verifier = createOfflineVerifier({
  jwksSnapshot: bundle.jwksSnapshot,
  requireScopes: ['calendar:read'],
  maxDelegationDepth: 2,
});

// Verify the grant token entirely on-device -- no network call
const grant = await verifier.verify(bundle.grantToken);
console.log(grant.agentDID, grant.scopes, grant.expiresAt);

// Create a hash-chained, Ed25519-signed offline audit log
const auditLog = createOfflineAuditLog({
  signingKey: bundle.offlineAuditKey,
  logPath: '/data/grantex/audit.jsonl',
});

// Log every action the agent performs
await auditLog.append({
  action: 'tool:read_calendar',
  agentDID: grant.agentDID,
  grantId: grant.grantId,
  scopes: grant.scopes,
  result: 'success',
  metadata: { eventCount: 5 },
});

Phase 3 -- Online: Sync Audit Log

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

// When connectivity returns, sync all un-uploaded entries
const result = await syncAuditLog(auditLog, {
  endpoint: bundle.syncEndpoint,
  apiKey: process.env.GRANTEX_API_KEY!,
  bundleId: bundle.bundleId,
  batchSize: 100, // entries per HTTP request
});

console.log(`Synced ${result.syncedCount} entries`);
if (result.hasErrors) console.error(result.errors);

Architecture

  ONLINE (Phase 1)                OFFLINE (Phase 2)              ONLINE (Phase 3)
  ─────────────────               ─────────────────              ─────────────────
  Grantex Cloud                   On-Device (Gemma 4)            Grantex Cloud
  ┌──────────────┐                ┌──────────────────┐           ┌──────────────┐
  │ POST /v1/    │  consent       │ loadBundle()      │  sync    │ POST /v1/    │
  │ consent-     │  bundle        │   ▼ JWKS Snapshot  │  audit   │ audit/       │
  │ bundles      │ ──────────►    │   ▼ verify(token)  │ ──────►  │ offline-sync │
  └──────────────┘                │   ▼ enforceScopes()│          └──────────────┘
       AES-256-GCM ◄──────────   │   ▼ auditLog       │
       encrypted on disk          │   ▼ Hash-Chained   │
                                  │     (.jsonl)       │
                                  └──────────────────┘

API Reference

Verifier

createOfflineVerifier(options): OfflineVerifier

Create an offline JWT verifier that validates Grantex grant tokens using a pre-fetched JWKS snapshot. No network call is made during verification.

Options:

OptionTypeDefaultDescription
jwksSnapshotJWKSSnapshotrequiredPre-fetched JWKS keys from the consent bundle
clockSkewSecondsnumber30Clock skew tolerance in seconds
requireScopesstring[]--Scopes that must be present in every verified token
maxDelegationDepthnumber--Maximum delegation chain depth (inclusive)
onScopeViolation'throw' | 'log''throw'Behaviour when a scope check fails

Returns an OfflineVerifier with a single method:

  • verify(token: string): Promise<VerifiedGrant> -- verify a JWT and return the decoded grant.

VerifiedGrant fields:

FieldTypeDescription
agentDIDstringAgent's DID (agt JWT claim)
principalDIDstringUser who authorized the grant (sub claim)
scopesstring[]Granted scopes (scp claim)
expiresAtDateToken expiry
jtistringUnique token ID (jti claim)
grantIdstringGrant record ID (grnt claim, falls back to jti)
depthnumberDelegation depth (0 = root grant)

Example:

const verifier = createOfflineVerifier({
  jwksSnapshot: bundle.jwksSnapshot,
  requireScopes: ['files:read'],
  clockSkewSeconds: 60,
  maxDelegationDepth: 3,
});

const grant = await verifier.verify(bundle.grantToken);
console.log(grant.agentDID);   // 'did:web:agent.example'
console.log(grant.scopes);     // ['files:read', 'files:write']
console.log(grant.grantId);    // 'grnt_01J...'

enforceScopes(grantScopes, requiredScopes): void

Throws ScopeViolationError if any required scope is missing from the grant.

hasScope(grantScopes, scope): boolean

Check whether a single scope is present. Returns boolean.

createConsentBundle(options): Promise<ConsentBundle>

Request a consent bundle from the Grantex API. This call requires network connectivity -- the returned bundle is then used for all offline operations.

Options:

OptionTypeDefaultDescription
apiKeystringrequiredGrantex developer API key
baseUrlstringhttps://api.grantex.devAPI base URL
agentIdstringrequiredAgent requesting the bundle
userIdstringrequiredEnd-user / principal granting consent
scopesstring[]requiredRequested scopes
offlineTTLstring'72h'Offline validity period (e.g. '24h', '7d')
offlineAuditKeyAlgorithmstring'Ed25519'Signing algorithm for audit entries
storagestring--Storage backend: 'encrypted-file', 'keychain', 'secure-enclave'
storagePathstring--File path when storage is 'encrypted-file'

ConsentBundle fields:

FieldTypeDescription
bundleIdstringUnique bundle identifier
grantTokenstringGrantex grant token (RS256 JWT)
jwksSnapshotJWKSSnapshotJWKS keys for offline verification
offlineAuditKey{ publicKey, privateKey, algorithm }Ed25519 key pair for signing audit entries
checkpointAtnumberUnix-ms timestamp of last successful cloud sync
syncEndpointstringURL for syncing audit entries back to cloud
offlineExpiresAtstringISO-8601 timestamp after which offline operation is disallowed

Example:

const bundle = await createConsentBundle({
  apiKey: 'gx_live_...',
  agentId: 'agent_gemma_field',
  userId: 'usr_01J...',
  scopes: ['sensor:read', 'actuator:write'],
  offlineTTL: '7d',
});

storeBundle(bundle, path, encryptionKey): Promise<void>

Encrypt and write a bundle to disk using AES-256-GCM. File format: [12-byte IV][16-byte auth-tag][ciphertext]. The encryption key is SHA-256-hashed to derive a 32-byte AES key, so any passphrase length works.

loadBundle(path, encryptionKey): Promise<ConsentBundle>

Read and decrypt a bundle from disk. Throws BundleTamperedError if decryption or integrity check fails.

shouldRefresh(bundle): boolean

Returns true when less than 20% of the bundle's total offline TTL remains. Call this proactively when connectivity is available.

refreshBundle(bundle, apiKey, baseUrl?): Promise<ConsentBundle>

Refresh an expiring bundle via the Grantex API. Returns a new bundle with extended offlineExpiresAt, fresh JWKS snapshot, and rotated audit keys.

if (shouldRefresh(bundle)) {
  const refreshed = await refreshBundle(bundle, process.env.GRANTEX_API_KEY!);
  await storeBundle(refreshed, '/data/grantex/bundle.enc', process.env.ENCRYPTION_KEY!);
}

Audit Log

createOfflineAuditLog(options): OfflineAuditLog

Create an append-only, Ed25519-signed, hash-chained audit log backed by a JSONL file. Every entry is cryptographically linked to the previous one, forming a tamper-evident chain.

Options:

OptionTypeDefaultDescription
signingKey{ publicKey, privateKey, algorithm }requiredEd25519 key pair (from consent bundle)
logPathstringrequiredPath to the JSONL log file
maxSizeMBnumber50Max file size in MB before rotation
rotateOnSizebooleantrueEnable automatic size-based rotation

Methods:

MethodSignatureDescription
append(entry: AuditEntry) => Promise<SignedAuditEntry>Append a new signed, hash-chained entry
entries() => Promise<SignedAuditEntry[]>Read all entries from the log file
unsyncedCount() => Promise<number>Count entries not yet synced to cloud
markSynced(upToSeq: number) => Promise<void>Mark entries as synced up to a sequence number

SignedAuditEntry fields:

FieldTypeDescription
seqnumberMonotonically increasing sequence number
timestampstringISO-8601 timestamp of the entry
actionstringAction performed (e.g. 'tool:read_calendar')
agentDIDstringAgent that performed the action
grantIdstringGrant under which the action was authorized
scopesstring[]Scopes the grant carried at the time
resultstringOutcome: 'success', 'auth_failure', 'scope_violation', 'execution_error'
metadataRecord<string, unknown>?Optional structured metadata
prevHashstringSHA-256 hash of the previous entry (genesis: '0000000000000000')
hashstringSHA-256 hash of this entry
signaturestringEd25519 signature of the hash (hex-encoded)

syncAuditLog(auditLog, options): Promise<SyncResult>

POST un-synced audit entries to the Grantex cloud in batches with automatic retry (3 attempts, exponential back-off).

OptionTypeDefaultDescription
endpointstringrequiredSync endpoint URL (from bundle.syncEndpoint)
apiKeystringrequiredDeveloper API key
bundleIdstringrequiredConsent bundle ID linking entries to an offline session
batchSizenumber100Entries per HTTP request

Returns SyncResult with syncedCount, hasErrors, and errors[].

computeEntryHash(entry): string

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

verifyChain(entries): { valid: boolean; brokenAt?: number }

Verify the integrity of an ordered sequence of entries. Checks hash recomputation, prevHash linkage, and sequence continuity.

verifyEntrySignature(entry, publicKey): boolean

Verify the Ed25519 signature of a single audit entry.

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

const { valid, brokenAt } = verifyChain(await auditLog.entries());
const sigValid = verifyEntrySignature(entry, bundle.offlineAuditKey.publicKey);

Adapters

withGrantexAuthADK(tool, options) / withGrantexAuthLangChain(tool, options)

Wrap a Google ADK FunctionTool or LangChain StructuredTool with Grantex offline authorization. Before the tool executes, the grant token is verified and scopes are enforced. After execution, an audit entry is appended automatically.

import { withGrantexAuthADK, withGrantexAuthLangChain } from '@grantex/gemma';

// Google ADK -- wraps tools with `func` or `run` methods
const protectedAdkTool = withGrantexAuthADK(myAdkTool, {
  verifier, auditLog,
  requiredScopes: ['calendar:read'],
  grantToken: bundle.grantToken,
});

// LangChain -- wraps tools with `_call` or `invoke` methods
const protectedLcTool = withGrantexAuthLangChain(myLangChainTool, {
  verifier, auditLog,
  requiredScopes: ['email:read'],
  grantToken: bundle.grantToken,
});

Both adapters accept the same options:

OptionTypeDescription
verifierOfflineVerifierOffline verifier instance
auditLogOfflineAuditLogOffline audit log instance
requiredScopesstring[]Scopes required to invoke this tool
grantTokenstringGrantex grant token (JWT)

Type Definitions

interface JWKSSnapshot {
  keys: JWK[];        // JWK key objects (must include kid and alg)
  fetchedAt: string;   // ISO-8601 when snapshot was fetched
  validUntil: string;  // ISO-8601 after which snapshot should be refreshed
}

interface ConsentBundle {
  bundleId: string;
  grantToken: string;                   // RS256 JWT
  jwksSnapshot: JWKSSnapshot;
  offlineAuditKey: { publicKey: string; privateKey: string; algorithm: string };
  checkpointAt: number;                 // Unix-ms timestamp
  syncEndpoint: string;
  offlineExpiresAt: string;             // ISO-8601
}

interface VerifiedGrant {
  agentDID: string;      principalDID: string;
  scopes: string[];      expiresAt: Date;
  jti: string;           grantId: string;       depth: number;
}

interface SignedAuditEntry {
  seq: number;           timestamp: string;      action: string;
  agentDID: string;      grantId: string;        scopes: string[];
  result: string;        metadata?: Record<string, unknown>;
  prevHash: string;      hash: string;           signature: string;
}

interface SyncResult {
  syncedCount: number;   hasErrors: boolean;     errors: string[];
}

// Errors -- all extend GrantexAuthError (which extends Error with a `code` property)
class OfflineVerificationError  { code: string }  // VERIFICATION_FAILED, MALFORMED_TOKEN,
                                                   // BLOCKED_ALGORITHM, MISSING_KID, KID_NOT_FOUND,
                                                   // FUTURE_IAT, DELEGATION_DEPTH_EXCEEDED
class ScopeViolationError       { requiredScopes: string[]; grantScopes: string[] }
class TokenExpiredError          { expiredAt: Date }
class BundleTamperedError       { /* code: BUNDLE_TAMPERED */ }
class HashChainError            { brokenAt: number }

Security

What offline verification guarantees:

  • Signature integrity -- every token is verified against a pre-fetched JWKS snapshot using RS256.
  • Scope enforcement -- required scopes are checked on every verification; agents cannot escalate permissions.
  • Tamper-evident audit trail -- every entry is SHA-256 hash-chained and Ed25519-signed. verifyChain() detects any modification.
  • Encrypted storage -- bundles are stored with AES-256-GCM (12-byte IV, 16-byte auth-tag). The encryption key is SHA-256-hashed to derive the 32-byte AES key.

What it does NOT guarantee:

  • Real-time revocation -- if a grant is revoked in the cloud while the device is offline, the local verifier continues to accept the token until expiry or offlineExpiresAt. This is an inherent trade-off of offline operation.
  • Clock accuracy -- verification depends on the device clock. Use clockSkewSeconds to account for drift.

Additional security details:

  • Blocked algorithms -- none and HS256 are explicitly blocked to prevent signature bypass and symmetric-key confusion attacks. Only RS256 tokens are accepted.
  • Hash chain -- each entry hash is computed from seq|timestamp|action|agentDID|grantId|scopes|result|metadata|prevHash. Genesis hash: '0000000000000000'.
  • Ed25519 audit signing -- every entry's hash is signed with the private key from the consent bundle. Verify with verifyEntrySignature().

Platform Compatibility

@grantex/gemma runs anywhere Node.js 18+ is available:

PlatformRequirementNotes
Raspberry Pi 5Node.js 18+ (ARM64)3.2ms avg verification
AndroidReact Native / Termux1.8ms avg verification
iOSJavaScriptCore / embedded V81.8ms avg verification
NVIDIA JetsonNode.js 18+ (ARM64)1.1ms avg verification
DesktopNode.js 18+ (x64/ARM64)< 1ms avg verification
Edge serversCloudflare Workers, Deno Deploy< 1ms avg verification

The package is pure ESM ("type": "module" in your package.json, or use dynamic import()).

Error Handling

All errors extend GrantexAuthError and include a machine-readable code property:

import {
  TokenExpiredError, ScopeViolationError,
  OfflineVerificationError, BundleTamperedError, HashChainError,
} from '@grantex/gemma';

try {
  const grant = await verifier.verify(token);
  enforceScopes(grant.scopes, ['files:write']);
} catch (err) {
  if (err instanceof TokenExpiredError) {
    console.error(`Token expired at ${err.expiredAt.toISOString()}`);
  } else if (err instanceof ScopeViolationError) {
    console.error(`Missing: required ${err.requiredScopes}, got ${err.grantScopes}`);
  } else if (err instanceof OfflineVerificationError) {
    console.error(`[${err.code}]: ${err.message}`);
  } else if (err instanceof BundleTamperedError) {
    console.error('Bundle integrity check failed');
  } else if (err instanceof HashChainError) {
    console.error(`Chain broken at entry ${err.brokenAt}`);
  }
}

Testing

49 tests across 6 test files (offline verifier, scope enforcer, consent bundles, hash chain, audit log, security):

npm test            # run all tests
npm run test:watch  # watch mode
npm run typecheck   # type checking only

Framework Integrations

FrameworkAdapterImport
Google ADKwithGrantexAuthADKimport { withGrantexAuthADK } from '@grantex/gemma'
LangChainwithGrantexAuthLangChainimport { withGrantexAuthLangChain } from '@grantex/gemma'

Both adapters automatically handle token verification, scope enforcement, and audit logging. See the Adapters section above for full usage examples.

Examples

ExamplePlatformDescription
gemma-raspberry-piRaspberry Pi 5Python agent with bundle setup, offline operation, and audit sync
gemma-android-kotlinAndroidKotlin app with offline auth manager and audit logging
gemma-ios-swiftiOSSwift package with offline verifier and consent bundle manager

Troubleshooting

Token verification fails offline

The JWKS snapshot embedded in your consent bundle may have expired. Check the validUntil field:

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

if (isSnapshotExpired(bundle.jwksSnapshot)) {
  // Snapshot expired -- refresh the bundle when connectivity is available
}

Also verify the offlineExpiresAt on the bundle itself has not passed.

BundleTamperedError when loading

This means decryption failed. Common causes:

  • Wrong encryption key -- ensure you pass the same key used during storeBundle().
  • Corrupted file -- the bundle file was partially written or modified on disk.
  • File too short -- the file must contain at least 28 bytes (12-byte IV + 16-byte auth-tag).

Hash chain broken

verifyChain() returns { valid: false, brokenAt: N } when entries have been modified outside the audit log. This is expected tamper-detection behaviour. Do not manually edit the JSONL audit file.

Clock skew errors

If verification fails with FUTURE_IAT or tokens appear expired prematurely, increase the clock skew tolerance:

const verifier = createOfflineVerifier({
  jwksSnapshot: bundle.jwksSnapshot,
  clockSkewSeconds: 120, // allow up to 2 minutes of drift
});
PackageDescription
@grantex/sdkTypeScript SDK for the Grantex protocol
grantexPython SDK
grantex-adkGoogle ADK integration (Python)
@grantex/langchainLangChain integration
@grantex/mcpMCP server for Claude Desktop / Cursor / Windsurf
@grantex/cliCommand-line tool
@grantex/expressExpress.js middleware
@grantex/gatewayReverse-proxy gateway

Contributing

See CONTRIBUTING.md for guidelines on setting up the development environment, running tests, and submitting pull requests.

License

Apache 2.0

Keywords

grantex

FAQs

Package last updated on 25 Jun 2026

Related posts