Skip to content

SDK: Code Examples

Practical examples demonstrating common use cases with the Grizzly SDK.

Setup

All examples assume you've created and initialized a KeyClient:

import { KeyClient } from '@grizzlycbg/grizzly-sdk'
import fs from 'fs'

const client = await KeyClient.create({
   host: 'https://your-tenant.grizzlycbg.io',
   apikey: 'apikey-123456-1234567890'
})

File Encryption

Encrypt and Decrypt Files
async function encryptFile(inputPath, outputPath, keyringName) {
   const ring = await client.ring.get(keyringName)

   // Create read stream from input file
   const readStream = fs.createReadStream(inputPath)

   // Encrypt the stream
   const encryptedStream = await ring.encrypt(readStream)

   // Write encrypted stream to output file
   return new Promise((resolve, reject) => {
      const writeStream = fs.createWriteStream(outputPath)

      encryptedStream.pipe(writeStream)

      writeStream.on('finish', () => {
         console.log(`File encrypted: ${outputPath}`)
         resolve()
      })

      writeStream.on('error', reject)
   })
}

async function decryptFile(inputPath, outputPath) {
   // Client-level decrypt automatically determines the KeyRing
   const encryptedStream = fs.createReadStream(inputPath)
   const decryptedStream = await client.decrypt(encryptedStream)

   return new Promise((resolve, reject) => {
      const writeStream = fs.createWriteStream(outputPath)

      decryptedStream.pipe(writeStream)

      writeStream.on('finish', () => {
         console.log(`File decrypted: ${outputPath}`)
         resolve()
      })

      writeStream.on('error', reject)
   })
}

// Usage
await encryptFile('./document.pdf', './document.encrypted', 'Documents')
await decryptFile('./document.encrypted', './document-decrypted.pdf')

In-Memory Data Encryption

Encrypt Application Data
async function encryptUserData(userData, keyringName) {
   const ring = await client.ring.get(keyringName)

   // Serialize object to JSON
   const jsonData = JSON.stringify(userData)

   // Encrypt
   const encrypted = await ring.encrypt(jsonData)

   return encrypted
}

async function decryptUserData(encryptedData) {
   // Decrypt
   const decrypted = await client.decrypt(encryptedData)

   // Parse JSON back to object
   return JSON.parse(decrypted)
}

// Usage
const user = {
   id: '12345',
   email: '[email protected]',
   ssn: '123-45-6789'
}

const encrypted = await encryptUserData(user, 'UserData')
console.log('Encrypted:', encrypted)

const decrypted = await decryptUserData(encrypted)
console.log('Decrypted:', decrypted)

Asset Tagging

Working with Assets Assets are first-class entities in Grizzly that allow you to attach cryptographically signed metadata to encrypted data. The metadata is stored in the encrypted data's header and tracked throughout the platform, including the Activity feed.
async function encryptWithAsset(data, assetMetadata, keyringName) {
   const ring = await client.ring.get(keyringName)

   // Assets can be simple strings
   const encrypted1 = await ring.encrypt(data, {
      asset: 'user-12345'
   })

   // Or complex objects with metadata
   const encrypted2 = await ring.encrypt(data, {
      asset: {
         id: 'doc-789',
         name: 'contract.pdf',
         owner: 'user-456',
         classification: 'confidential',
         metadata: {
            department: 'legal',
            retentionYears: 7
         }
      }
   })

   return { encrypted1, encrypted2 }
}

async function encryptPatientRecord(patientId, recordData) {
   const ring = await client.ring.get('Healthcare')

   // Asset with compliance metadata
   const encrypted = await ring.encrypt(recordData, {
      asset: {
         id: `patient-${patientId}`,
         type: 'medical-record',
         policy: {
            hipaa: true,
            allowedPurposes: ['clinical_trial', 'treatment']
         },
         metadata: {
            department: 'cardiology',
            recordDate: new Date().toISOString()
         }
      }
   })

   return encrypted
}

// Usage
const patientData = JSON.stringify({
   name: 'John Doe',
   diagnosis: 'Condition XYZ'
})

const encrypted = await encryptPatientRecord('12345', patientData)

// The asset metadata is cryptographically signed and embedded in the header
// Any modification to the metadata will invalidate the encrypted data
// Assets appear in Activity feed and can be queried across the platform

Asset Workflows (client.assets)

The client.assets namespace encrypts, reads, re-tags, and relates assets over the SDK-owned HTTP transport. Populate data.name with a human-readable label — it becomes the asset's display name in activity logs and the Dashboard.

Tag-on-encrypt (returns the asset id)
// assets.encrypt is the only encrypt that returns the created asset id
const { data: ciphertext, asset } = await client.assets.encrypt('Secret record', 'Finance', {
   data: {
      name: 'Q4 Ledger',
      type: 'financial',
      classification: 'confidential'
   }
})

console.log('Ciphertext:', ciphertext)
console.log('Asset id:', asset.id)   // 'asset-4a1e...' — store this to update/query later

// Need the server-assigned createdAt too? Ask for hydration (one extra round trip)
const hydrated = await client.assets.encrypt('Another record', 'Finance', {
   data: { name: 'Q4 Adjustments' },
   hydrate: true
})
console.log(hydrated.asset.createdAt)
Read, re-tag, and re-parent `assets.update` is a metadata-only patch — the ciphertext is never touched, so the asset still decrypts after an update. Parent deltas are applied server-side per `parentMode` (`Replace` is the default; `Add` unions; `Remove` subtracts).
import { RelationshipMode } from '@grizzlycbg/grizzly-sdk'

// Read the current asset
const asset = await client.assets.get('asset-123')
if (!asset) throw new Error('asset not found')

// 1. Re-tag: replace the data bag (no re-encryption)
await client.assets.update('asset-123', {
   data: { ...asset.data, state: 'approved', approvedBy: 'user-789' }
})

// 2. Re-parent — Replace (default): the parent set becomes exactly this list ([] clears all edges)
await client.assets.update('asset-123', {
   parents: ['asset-parent-A', 'asset-parent-B'],
   parentMode: RelationshipMode.Replace
})

// 3. Re-parent — Add: union in a new parent, keeping existing edges
await client.assets.update('asset-123', {
   parents: ['asset-parent-C'],
   parentMode: RelationshipMode.Add
})

// 4. Re-parent — Remove: drop a single edge, keep the rest
await client.assets.update('asset-123', {
   parents: ['asset-parent-A'],
   parentMode: RelationshipMode.Remove
})
Iterate every matching asset with searchAll `searchAll` fetches each page only as the iterator reaches it, so a large result set is never buffered into memory.
async function countByDepartment(type) {
   const counts = {}

   for await (const asset of client.assets.searchAll({ data: { type } })) {
      const dept = asset.data.department ?? 'unknown'
      counts[dept] = (counts[dept] ?? 0) + 1
   }

   return counts
}

// Walk every PHI asset without loading them all at once
const phiByDept = await countByDepartment('phi')
console.log(phiByDept)
Retrieve a deleted asset's activity history Deleting an asset erases its content but keeps its audit trail — who did what, and when. `client.assets.get` returns `undefined` for a deleted asset, but `client.assets.activities` keeps working, which is the point: it's how you pull a deleted asset's history for a compliance review after the asset itself is gone.
await client.assets.delete('asset-123')

// get() no longer returns anything
console.log(await client.assets.get('asset-123'))   // undefined

// activities() still does
const page = await client.assets.activities('asset-123')
for (const activity of page.data) {
   console.log(activity.action, activity.createdAt)
   // data is {} for every entry — the fact of the action survives, its content does not
}
Find everything that ever passed through a parent (parents vs. everParentedBy) `search`/`searchAll` accept `parents` (current state) and `everParentedBy` (history). They read differently: `parents` matches an asset's parent set as it stands today; `everParentedBy` matches any asset that was ever linked to the given parent, even if that link has since been removed — useful when a reorganization moved assets around and you need the full lineage, not just the current snapshot.
// Everything Project Alpha currently owns
const current = await client.assets.search({ parents: [projectAlphaId] })

// Everything that ever passed through Project Alpha, including assets since re-parented elsewhere
const everOwned = await client.assets.search({ everParentedBy: projectAlphaId })

console.log(`Currently under Alpha: ${current.data.length}`)
console.log(`Ever under Alpha: ${everOwned.data.length}`)

Bulk Backfill with Idempotent Retries

Backfill assets, then retry only the failures by id `client.assets.bulk` runs an operation over many items on a bounded-concurrency pool and returns a per-item report. Pre-mint each asset id with `newAssetId()` so a retried encrypt addresses the same record instead of creating a duplicate.
import { newAssetId } from '@grizzlycbg/grizzly-sdk'

// Give every record a stable, pre-allocated id up front
const records = rows.map((row) => ({
   id: newAssetId(),
   payload: JSON.stringify(row),
   keyring: 'Finance',
   data: { name: row.label, type: 'financial', sourceRow: row.id }
}))

async function backfill(items) {
   return client.assets.bulk(items, {
      operation: (item) =>
         client.assets.encrypt(item.payload, item.keyring, {
            data: item.data,
            assetId: item.id   // idempotent: first call creates, a retry re-tags the same asset
         }),
      id: (item) => item.id,
      concurrency: 50,
      onProgress: (p) => console.log(`${p.done}/${p.total} (${p.failed} failed)`)
   })
}

let result = await backfill(records)
console.log(`Encrypted ${result.succeeded}/${result.total}`)

// Retry ONLY the failed ids — same pre-minted ids make this safe to replay
const failedIds = new Set(result.results.filter((r) => !r.ok).map((r) => r.id))
if (failedIds.size > 0) {
   const retryItems = records.filter((r) => failedIds.has(r.id))
   result = await backfill(retryItems)
   console.log(`Retry recovered ${result.succeeded}/${retryItems.length}`)
}

Typed Error Handling

Branch on the GrizzlyError hierarchy The asset, activity, and admin surfaces raise a typed `GrizzlyError` hierarchy — branch on the class (or the stable `code`) instead of string-matching messages. `RateLimitError` carries the server's `Retry-After` as `retryAfterMs`. > A missing asset from `assets.get` comes back as `undefined` (not a thrown `NotFoundError`). Among the other surfaces, **`assets.family` and `assets.update` both throw `NotFoundError`** when the addressed asset is missing — that is their 404. `assets.update` raises the same 404 whether the missing id is the target or a referenced *parent*, distinguished by the error message; `assets.delete` is idempotent and returns without throwing for an already-absent asset.
import { NotFoundError, RateLimitError, ValidationError } from '@grizzlycbg/grizzly-sdk'

async function retagAsset(id, patch) {
   try {
      // Pre-flight the id on the traversal surface: assets.family is the one asset
      // surface that throws NotFoundError (404) when the addressed asset is missing.
      await client.assets.family(id)

      // The asset exists — apply the metadata-only patch.
      const asset = await client.assets.update(id, patch)
      return { ok: true, asset }
   } catch (err) {
      if (err instanceof NotFoundError) {
         // From assets.family — the asset genuinely does not exist.
         return { ok: false, reason: 'not-found' }
      }
      if (err instanceof RateLimitError) {
         // Honor the server's backoff hint when present
         await new Promise((r) => setTimeout(r, err.retryAfterMs ?? 1000))
         return retagAsset(id, patch)   // retry once the window clears
      }
      if (err instanceof ValidationError) {
         // assets.update rejects with 400 for a missing ADDRESSED asset (a missing
         // referenced *parent* id is a 404/NotFoundError instead). Check err.code /
         // err.message to tell the missing-asset case apart from a malformed patch.
         return { ok: false, reason: 'invalid', code: err.code, message: err.message, requestId: err.requestId }
      }
      throw err   // unknown error — let it propagate
   }
}
Handle a relationship-cycle rejection (409) Asset parent relationships form a DAG — a `parents`/`parentMode` change that would make an asset its own ancestor is rejected with status 409, and nothing is applied. There's no dedicated error class for it yet, so branch on `err.status`.
import { RelationshipMode } from '@grizzlycbg/grizzly-sdk'

async function reparent(childId, parentId) {
   try {
      await client.assets.update(childId, { parents: [parentId], parentMode: RelationshipMode.Add })
      return { ok: true }
   } catch (err) {
      if (err.status === 409) {
         // parentId already descends from childId — a cycle, not a valid parent edge.
         // Model the relationship instead: a shared parent asset, or pick one canonical direction.
         return { ok: false, reason: 'would-create-cycle' }
      }
      throw err
   }
}

Key Rotation

Manual Key Rotation
async function rotateKeyRing(keyringName) {
   const ring = await client.ring.get(keyringName)

   console.log(`Current active key: ${ring.activeKey.id}`)

   // Rotate to new key
   await ring.rotate()

   // Refresh KeyRing to get updated active key
   const updatedRing = await client.ring.get(keyringName)
   console.log(`New active key: ${updatedRing.activeKey.id}`)

   return updatedRing
}

// Demonstrate that old encrypted data still decrypts after rotation
async function testRotation() {
   const ring = await client.ring.get('Finance')

   // Encrypt with current key
   const data1 = await ring.encrypt('Message 1')
   console.log('Encrypted with key:', ring.activeKey.id)

   // Rotate key
   await ring.rotate()

   // Encrypt with new key
   const ring2 = await client.ring.get('Finance')
   const data2 = await ring2.encrypt('Message 2')
   console.log('Encrypted with key:', ring2.activeKey.id)

   // Both decrypt successfully
   const decrypted1 = await ring2.decrypt(data1)
   const decrypted2 = await ring2.decrypt(data2)

   console.log('Decrypted 1:', decrypted1) // 'Message 1'
   console.log('Decrypted 2:', decrypted2) // 'Message 2'
}

await testRotation()

Customer-Managed Keys

Bring Your Own Keys
import fs from 'fs'

async function createKeyRingWithCustomKeys() {
   // Read your RSA key pair from files
   const publicKey = fs.readFileSync('./keys/public.pem', 'utf-8')
   const privateKey = fs.readFileSync('./keys/private.pem', 'utf-8')

   // Create KeyRing with your keys
   const ring = await client.ring.create('SecureFinance', {
      publicKey,
      privateKey,
      config: {
         props: {
            owner: 'finance-team',
            compliance: 'PCI-DSS'
         },
         algos: {
            aes256: {
               maxEncryptCount: 50000
            }
         }
      }
   })

   console.log(`Created KeyRing: ${ring.name}`)
   return ring
}

async function encryptWithCustomKeys() {
   const ring = await client.ring.get('SecureFinance')

   const encrypted = await ring.encrypt('Highly sensitive data')

   // Decrypt requires the private key
   const privateKey = fs.readFileSync('./keys/private.pem', 'utf-8')
   const decrypted = await client.decrypt(encrypted, privateKey)

   console.log('Decrypted:', decrypted)
}

await createKeyRingWithCustomKeys()
await encryptWithCustomKeys()

KeyRing Configuration Management

Update KeyRing Settings
async function updateKeyRingConfig(keyringName) {
   const ring = await client.ring.get(keyringName)

   // Get current config
   const config = await ring.getConfig()
   console.log('Current max encrypt count:', config.algos.aes256.maxEncryptCount)

   // Update configuration
   await ring.updateConfig((cfg) => {
      // Change encryption count before rotation
      cfg.algos.aes256.maxEncryptCount = 100000

      // Add custom metadata
      cfg.props.updatedAt = new Date().toISOString()
      cfg.props.updatedBy = 'admin'

      // Add key-level metadata (applied to new keys)
      cfg.key.props = {
         environment: 'production',
         version: '2.0'
      }
   })

   // Verify update
   const updatedConfig = await ring.getConfig()
   console.log('New max encrypt count:', updatedConfig.algos.aes256.maxEncryptCount)
}

await updateKeyRingConfig('Finance')

Batch Operations

Encrypt Multiple Items
async function encryptUserBatch(users, keyringName) {
   const ring = await client.ring.get(keyringName)

   const encryptedUsers = await Promise.all(
      users.map(async (user) => {
         const userData = JSON.stringify(user)
         const encrypted = await ring.encrypt(userData, {
            asset: {
               id: `user-${user.id}`,
               type: 'user-record',
               email: user.email,
               encryptedAt: new Date().toISOString()
            }
         })

         return {
            id: user.id,
            encrypted
         }
      })
   )

   return encryptedUsers
}

async function decryptUserBatch(encryptedUsers) {
   const decryptedUsers = await Promise.all(
      encryptedUsers.map(async (item) => {
         const decrypted = await client.decrypt(item.encrypted)
         return {
            id: item.id,
            data: JSON.parse(decrypted)
         }
      })
   )

   return decryptedUsers
}

// Usage
const users = [
   { id: '1', name: 'Alice', email: '[email protected]' },
   { id: '2', name: 'Bob', email: '[email protected]' },
   { id: '3', name: 'Carol', email: '[email protected]' }
]

const encrypted = await encryptUserBatch(users, 'UserData')
console.log('Encrypted:', encrypted.length, 'users')

const decrypted = await decryptUserBatch(encrypted)
console.log('Decrypted:', decrypted)

Error Handling

Handle Common Errors
async function encryptWithErrorHandling(data, keyringName) {
   try {
      // Try to get existing KeyRing
      let ring = await client.ring.get(keyringName)

      // Create if doesn't exist
      if (!ring) {
         console.log(`KeyRing '${keyringName}' not found, creating...`)
         ring = await client.ring.create(keyringName)
      }

      // Encrypt data
      const encrypted = await ring.encrypt(data)

      return { success: true, encrypted }

   } catch (error) {
      console.error('Encryption failed:', error.message)

      // Handle specific error types
      if (error.message.includes('authentication')) {
         return { success: false, error: 'Invalid API key' }
      }

      if (error.message.includes('network')) {
         return { success: false, error: 'Connection failed' }
      }

      return { success: false, error: error.message }
   }
}

async function decryptWithErrorHandling(encrypted) {
   try {
      const decrypted = await client.decrypt(encrypted)
      return { success: true, decrypted }

   } catch (error) {
      console.error('Decryption failed:', error.message)

      if (error.message.includes('invalid')) {
         return { success: false, error: 'Invalid encrypted data' }
      }

      if (error.message.includes('key not found')) {
         return { success: false, error: 'Decryption key unavailable' }
      }

      return { success: false, error: error.message }
   }
}

// Usage
const result = await encryptWithErrorHandling('Sensitive data', 'Finance')

if (result.success) {
   console.log('Encrypted:', result.encrypted)
} else {
   console.error('Error:', result.error)
}

Caching and Performance

Optimize with Key Caching
// Create client with key caching
const client = await KeyClient.create({
   host: 'https://your-tenant.grizzlycbg.io',
   apikey: 'apikey-123456-1234567890',
   keycache: {
      max: 100  // Cache up to 100 keys
   }
})

async function encryptHighVolume(items, keyringName) {
   const ring = await client.ring.get(keyringName)

   // First encryption fetches key from server
   // Subsequent encryptions use cached key (much faster)

   const startTime = Date.now()

   for (const item of items) {
      await ring.encrypt(item.data)
   }

   const duration = Date.now() - startTime
   console.log(`Encrypted ${items.length} items in ${duration}ms`)
   console.log(`Average: ${duration / items.length}ms per item`)
}

// Simulate high-volume encryption
const items = Array.from({ length: 1000 }, (_, i) => ({
   id: i,
   data: `Data item ${i}`
}))

await encryptHighVolume(items, 'HighVolume')

Complete Application Example

Secure Document Storage Service
import { KeyClient } from '@grizzlycbg/grizzly-sdk'
import fs from 'fs'
import path from 'path'

class SecureDocumentService {
   constructor(client) {
      this.client = client
      this.keyringName = 'Documents'
   }

   async initialize() {
      // Ensure KeyRing exists
      let ring = await this.client.ring.get(this.keyringName)

      if (!ring) {
         ring = await this.client.ring.create(this.keyringName, {
            config: {
               props: {
                  service: 'document-storage',
                  version: '1.0'
               },
               algos: {
                  aes256: {
                     maxEncryptCount: 10000
                  }
               }
            }
         })
         console.log(`Created KeyRing: ${this.keyringName}`)
      }

      return ring
   }

   async uploadDocument(userId, documentId, filePath) {
      try {
         const ring = await this.client.ring.get(this.keyringName)

         // Read file
         const content = fs.readFileSync(filePath)
         const filename = path.basename(filePath)

         // Encrypt with Asset metadata
         const encrypted = await ring.encrypt(content, {
            asset: {
               id: documentId,
               type: 'document',
               filename: filename,
               owner: userId,
               uploadedAt: new Date().toISOString(),
               metadata: {
                  size: content.length,
                  mimeType: this.getMimeType(filename)
               }
            }
         })

         // Store encrypted data
         const storagePath = `./encrypted/${documentId}.enc`
         fs.writeFileSync(storagePath, encrypted)

         console.log(`Document uploaded: ${documentId}`)

         return {
            success: true,
            documentId,
            storagePath
         }

      } catch (error) {
         console.error('Upload failed:', error.message)
         return { success: false, error: error.message }
      }
   }

   getMimeType(filename) {
      const ext = path.extname(filename).toLowerCase()
      const mimeTypes = {
         '.pdf': 'application/pdf',
         '.doc': 'application/msword',
         '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
         '.txt': 'text/plain'
      }
      return mimeTypes[ext] || 'application/octet-stream'
   }

   async downloadDocument(documentId, outputPath) {
      try {
         // Read encrypted file
         const storagePath = `./encrypted/${documentId}.enc`
         const encrypted = fs.readFileSync(storagePath)

         // Decrypt
         const decrypted = await this.client.decrypt(encrypted)

         // Write decrypted file
         fs.writeFileSync(outputPath, decrypted)

         console.log(`Document downloaded: ${outputPath}`)

         return { success: true, outputPath }

      } catch (error) {
         console.error('Download failed:', error.message)
         return { success: false, error: error.message }
      }
   }

   async rotateKeys() {
      const ring = await this.client.ring.get(this.keyringName)
      await ring.rotate()
      console.log('Keys rotated successfully')
   }
}

// Usage
const client = await KeyClient.create({
   host: 'https://your-tenant.grizzlycbg.io',
   apikey: 'apikey-123456-1234567890'
})

const service = new SecureDocumentService(client)
await service.initialize()

// Upload document
await service.uploadDocument('user-123', 'doc-456', './contract.pdf')

// Download document
await service.downloadDocument('doc-456', './contract-decrypted.pdf')

// Periodic key rotation
await service.rotateKeys()

Next Steps