Skip to content

SDK: API Reference

Complete API reference for the Grizzly SDK.

KeyClient

The main entry point for interacting with the Grizzly platform.

Static Methods

KeyClient.create(options: KeyClientOptions): Promise<IKeyClient>

Creates and initializes a new KeyClient instance.

Parameters:

Parameter Type Required Description
options.host string Yes Base URL of your Grizzly instance (e.g., https://tenant.grizzlycbg.io)
options.asymHost string No URL for Asymmetric Key Service (defaults to {host}/asym)
options.apikey string Yes API Key for authentication
options.keycache.max number No Maximum number of keys to cache in memory
options.logger Logger No Structured logger sink (see Logger & Observability Hooks). Injected into the SDK-owned HTTP transport instead of console
options.hooks ObservabilityHooks No Request-lifecycle hooks (onRequest / onResponse / onRetry) fired across the asset/activity/admin transport
options.tagSchema TagSchema No Default tag schema validated client-side against every asset data bag before it reaches the wire

Returns: Promise<IKeyClient>

Example:

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

Properties

ring: IKeyRingWarden (readonly)

Access to the KeyRingWarden for managing KeyRings.

store: IKeyStore (readonly)

The underlying key storage implementation.

utils: KeyClientUtils (readonly)

Utility functions for working with the SDK.

assets: IAssetClient (readonly)

The asset namespace — tagged encrypt plus read/search/update/family/delete/anchors/bulk over the SDK-owned HTTP transport. See Assets (client.assets). HTTP operations require a host; assets.encrypt (which rides the crypto path) works without one.

activities: IActivityClient (readonly)

The activity namespace — read-only search over the platform's audit/activity records. See Activities (client.activities). Requires a host.

admin: IAdminClient (readonly)

The admin/RBAC namespace — admin.accounts and admin.apiKeys, the primitives an IdP-driven (Okta/SCIM-style) integration drives over the auth-server routes. See Admin / RBAC (client.admin). Requires a host.

bulk: Bulk (readonly)

High-throughput batch encrypt / decrypt / rotate over a bounded-concurrency pool with per-item partial-failure isolation. See Bulk Operations (client.bulk).


Methods

encrypt(data: string | Buffer | Readable, keyring: string, options?: EncryptOptions): Promise<string | Buffer | Readable>

Encrypts data using a specified KeyRing.

Parameters:

Parameter Type Required Description
data string | Buffer | Readable Yes Data to encrypt
keyring string Yes Name of the KeyRing to use
options EncryptOptions No Encryption options
options.asset string | object No Asset metadata (cryptographically signed and embedded in header)
options.outFormat EncodingFormat No Output format: 'string', 'buffer', or 'readable-stream'
options.inEncoding Encoding No Input data encoding
options.outEncoding Encoding No Output data encoding

Returns: Promise<string | Buffer | Readable> - Encrypted data in the same format as input (unless outFormat is specified)

Example:

// Encrypt string
const encrypted = await client.encrypt('Secret data', 'Finance')

// Encrypt with Asset (simple string)
const encrypted = await client.encrypt('User data', 'UserData', {
   asset: 'user-123'
})

// Encrypt with Asset (complex metadata)
const encrypted = await client.encrypt('Sensitive document', 'Documents', {
   asset: {
      id: 'doc-789',
      type: 'contract',
      owner: 'user-456',
      classification: 'confidential'
   }
})

// Encrypt stream
const fileStream = fs.createReadStream('document.pdf')
const encryptedStream = await client.encrypt(fileStream, 'Documents')

decrypt(data: string | Buffer | Readable, privateKey?: string, options?: EncodingOptions): Promise<string | Buffer | Readable>

Decrypts data. The KeyRing used for encryption is automatically determined from the data's header.

Parameters:

Parameter Type Required Description
data string | Buffer | Readable Yes Encrypted data to decrypt
privateKey string No Private key if using customer-managed keys
options EncodingOptions No Encoding options
options.asset string | object No Asset metadata for tracking (optional during decryption)
options.outFormat EncodingFormat No Output format: 'string', 'buffer', or 'readable-stream'
options.inEncoding Encoding No Input data encoding
options.outEncoding Encoding No Output data encoding

Returns: Promise<string | Buffer | Readable> - Decrypted data

Example:

// Decrypt string
const decrypted = await client.decrypt(encryptedData)

// Decrypt with private key (customer-managed keys)
const privateKey = fs.readFileSync('./private.pem', 'utf-8')
const decrypted = await client.decrypt(encryptedData, privateKey)

// Decrypt stream
const encryptedStream = fs.createReadStream('encrypted.bin')
const decryptedStream = await client.decrypt(encryptedStream)

KeyRingWarden

Manages KeyRing lifecycle operations. Access via client.ring.

Methods

create(name: string, options?: KeyRingCreateOptions): Promise<IKeyRing>

Creates a new KeyRing with a unique name.

Parameters:

Parameter Type Required Description
name string Yes Unique name for the KeyRing
options KeyRingCreateOptions No Configuration options
options.algo Algorithm No Encryption algorithm (default: 'aes256')
options.publicKey string No PEM-formatted RSA public key
options.privateKey string No PEM-formatted RSA private key
options.config KeyRingConfig No KeyRing configuration
options.config.props object No Additional KeyRing metadata
options.config.key.props object No Metadata for keys created in this KeyRing
options.config.algos.aes256.maxEncryptCount number No Number of encryptions before key rotation

Returns: Promise<IKeyRing> - The created KeyRing

Example:

// Create with defaults
const ring = await client.ring.create('TeamA')

// Create with configuration
const ring = await client.ring.create('Finance', {
   config: {
      props: { team: 'finance', region: 'us-west' },
      algos: {
         aes256: {
            maxEncryptCount: 50000
         }
      }
   }
})

// Create with customer-managed keys
const publicKey = fs.readFileSync('./public.pem', 'utf-8')
const privateKey = fs.readFileSync('./private.pem', 'utf-8')

const ring = await client.ring.create('Secure', {
   publicKey,
   privateKey
})

get(name: string): Promise<IKeyRing | undefined>

Retrieves a KeyRing by name.

Parameters:

Parameter Type Required Description
name string Yes KeyRing name

Returns: Promise<IKeyRing | undefined> - The KeyRing, or undefined if not found

Example:

const ring = await client.ring.get('Finance')

if (ring) {
   const encrypted = await ring.encrypt('data')
}

getById(id: string): Promise<IKeyRing | undefined>

Retrieves a KeyRing by ID.

Parameters:

Parameter Type Required Description
id string Yes KeyRing ID

Returns: Promise<IKeyRing | undefined> - The KeyRing, or undefined if not found

Example:

const ring = await client.ring.getById('ring-123456')

getAll(): Promise<string[]>

Lists all KeyRing names. This eagerly walks every page of the tenant's KeyRings and buffers all names into memory — fine for modest tenants, but for large or unbounded sets prefer iterate() (below), which streams one page at a time.

Returns: Promise<string[]> - Array of KeyRing names

Example:

const rings = await client.ring.getAll()
console.log(rings)  // ['Finance', 'HR', 'Engineering']

iterate(options?: { pageSize?: number }): AsyncIterable<string>

Lazily iterates every KeyRing name, fetching one page at a time. Unlike getAll(), the full set is never buffered: each page is pulled only as the iterator advances, so a large tenant can be walked in bounded memory.

Parameters:

Parameter Type Required Description
options.pageSize number No Server page-size hint (maps to the route's limit). Default 100

Returns: AsyncIterable<string> - Yields each KeyRing name

Example:

for await (const name of client.ring.iterate({ pageSize: 500 })) {
   console.log(name)
}

delete(name: string): Promise<void>

Deletes a KeyRing. This operation cannot be undone.

Parameters:

Parameter Type Required Description
name string Yes KeyRing name to delete

Returns: Promise<void>

Example:

await client.ring.delete('OldKeyRing')

KeyRing

Represents a single KeyRing. Obtained through KeyRingWarden methods.

Properties

id: string (readonly)

The unique identifier for this KeyRing.

name: string (readonly)

The KeyRing name (lowercase).

displayName: string (readonly)

The KeyRing name with original casing preserved.

activeKey: Key (readonly)

The currently active encryption key.


Methods

encrypt(data: string | Buffer | Readable, options?: EncodingOptions): Promise<string | Buffer | Readable>

Encrypts data using this KeyRing's active key.

Parameters:

Parameter Type Required Description
data string | Buffer | Readable Yes Data to encrypt
options EncodingOptions No Encoding options
options.asset string | object No Asset metadata (cryptographically signed and embedded in header)
options.outFormat EncodingFormat No Output format
options.inEncoding Encoding No Input encoding
options.outEncoding Encoding No Output encoding

Returns: Promise<string | Buffer | Readable> - Encrypted data

Example:

const ring = await client.ring.get('Finance')

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

// Encrypt with Asset metadata
const encrypted = await ring.encrypt('User record', {
   asset: {
      id: 'user-456',
      type: 'user-record',
      department: 'finance'
   }
})

// Encrypt buffer
const buffer = Buffer.from('Binary data')
const encrypted = await ring.encrypt(buffer)

decrypt(data: string | Buffer | Readable, options?: EncodingOptions): Promise<string | Buffer | Readable>

Decrypts data that was encrypted with this KeyRing.

Parameters:

Parameter Type Required Description
data string | Buffer | Readable Yes Encrypted data
options EncodingOptions No Encoding options

Returns: Promise<string | Buffer | Readable> - Decrypted data

Example:

const ring = await client.ring.get('Finance')

const decrypted = await ring.decrypt(encryptedData)

rotate(algo?: Algorithm): Promise<Key>

Manually rotates the KeyRing's active key. Normally, keys rotate automatically based on configuration.

Parameters:

Parameter Type Required Description
algo Algorithm No Algorithm for new key (default: current algorithm)

Returns: Promise<Key> - The new active key

Example:

const ring = await client.ring.get('Finance')

// Rotate key
await ring.rotate()

// New encryptions use the new key
const encrypted = await ring.encrypt('New data')

// Old data still decrypts with rotated keys
const old = await ring.decrypt(oldEncryptedData)

getConfig(): Promise<KeyRingConfig>

Retrieves the current KeyRing configuration.

Returns: Promise<KeyRingConfig> - Current configuration

Example:

const ring = await client.ring.get('Finance')
const config = await ring.getConfig()

console.log(config.algos.aes256.maxEncryptCount)

updateConfig(handler: KeyRingConfigUpdateHandler): Promise<void>

Updates the KeyRing configuration.

Parameters:

Parameter Type Required Description
handler function Yes Callback to modify config

Handler signature: (config: KeyRingConfig) => void

Returns: Promise<void>

Example:

const ring = await client.ring.get('Finance')

await ring.updateConfig((config) => {
   config.algos.aes256.maxEncryptCount = 100000
   config.props.team = 'finance-v2'
})

Assets (client.assets)

The asset namespace attaches business metadata (the data bag) to encrypted data and reads it back over the SDK-owned HTTP transport. Access via client.assets.

Methods

encrypt(data: string | Buffer | Readable, keyring: string, options?: EncryptAssetOptions): Promise<EncryptAssetResult>

Encrypts data on keyring and creates the associated Asset in one call, returning both the ciphertext and a reference to the created Asset. This is the only encrypt that returns the Asset id. It reuses the exact same crypto path as ring.encrypt — there is no parallel crypto.

Parameters:

Parameter Type Required Description
data string | Buffer | Readable Yes Data to encrypt
keyring string Yes Name of the KeyRing to use
options.data object No Business metadata for the Asset. Populate data.name with a human-readable label — it becomes the display name in activity logs and the Dashboard
options.parents string[] No Parent Asset ids to relate this new Asset to
options.assetId string No Pre-allocated asset-<uuid> (see newAssetId). Supplying the same id for a retried request makes the operation idempotent — the first call creates the Asset with that id, a retry re-tags the same record. Mint it with newAssetId()
options.hydrate boolean No When true, follows the encrypt with a GET /assets/:id so the returned asset carries createdAt. Costs one extra round trip. Default false
options.tagSchema TagSchema No Per-call tag schema; overrides the client-level default. A violation throws a ValidationError before anything is encrypted
options.outFormat / options.inEncoding / options.outEncoding — No Same encoding options as ring.encrypt

Returns: Promise<EncryptAssetResult> — { data, asset }, where data is the ciphertext and asset is an AssetRef (always carries id; fully populated when hydrate: true).

Example:

import { newAssetId } from '@grizzlycbg/grizzly-sdk'

const { data: ciphertext, asset } = await client.assets.encrypt('Secret record', 'Finance', {
   data: { name: 'Q4 Ledger', type: 'financial' },
   parents: ['asset-9f2c...'],
   assetId: newAssetId()   // pre-mint for idempotent retries
})

console.log(asset.id)   // 'asset-4a1e...'

get(id: string): Promise<Asset | undefined>

Retrieves a single Asset by id, including its data, parent relationships, and full activity history. Returns undefined if no Asset matches.


search(query: AssetQuery): Promise<Page<AssetSummary>>

Returns one page of matching Assets (no cursor). Each hit is an AssetSummary (id, createdAt, data — without the potentially large parents/activities).

query.parents and query.everParentedBy look similar but read different things: parents matches an asset's current parent set, while everParentedBy matches any asset that was ever parented by the given id, including relationships that have since been removed. Use parents for "what does this asset own right now" and everParentedBy for "what has ever passed through this asset" — the latter is what a lineage or compliance query usually wants.


searchAll(query: AssetQuery): AsyncIterable<AssetSummary>

Lazily iterates every matching Asset, fetching each page only as the iterator crosses into it — the full result set is never buffered.

Example:

for await (const asset of client.assets.searchAll({ data: { type: 'phi' } })) {
   console.log(asset.id, asset.data.name)
}

family(id: string, options?: FamilyOptions): Promise<AssetFamily>

Returns the Asset together with its ancestors, descendants, and a hierarchy summary.

Parameters:

Parameter Type Required Description
id string Yes The Asset id
options.maxDepth number No Limit how many hierarchy levels are walked
options.accountId / options.keyringId string No Scope the traversal

Returns: Promise<AssetFamily> — { asset, ancestors, descendants, summary }.


update(id: string, patch: AssetUpdate): Promise<Asset>

Applies a metadata-only patch (data and/or parents) to an existing Asset and returns the updated Asset. The ciphertext is never touched — decrypting the Asset's data still works after an update. Parent deltas are resolved server-side per patch.parentMode.

Parameters:

Parameter Type Required Description
id string Yes The Asset id
patch.data object No New business metadata (replaces the stored data when present)
patch.parents string[] No Parent ids to apply, interpreted by parentMode
patch.parentMode RelationshipMode No How to apply parents: Replace (default) / Add / Remove
patch.context object No Extra context recorded on the update activity
patch.tagSchema TagSchema No Per-call tag schema, validated against patch.data only when data is present

Returns: Promise<Asset> — the updated Asset.

Parent relationships form a directed acyclic graph — an asset can never become its own ancestor, at any depth. A parents/parentMode change that would create a cycle is rejected with status 409 and nothing is applied. This only applies to changes on an existing asset (update); a newly created asset cannot close a cycle no matter what parents it is given. See Asset Tagging: Relationships Form a DAG for how to model genuine mutual derivation instead of cycling it.

Example:

import { RelationshipMode } from '@grizzlycbg/grizzly-sdk'

// Re-tag (metadata only — ciphertext untouched)
await client.assets.update('asset-123', { data: { name: 'Q4 Ledger (Approved)', state: 'approved' } })

// Add a parent edge without disturbing existing ones
await client.assets.update('asset-123', {
   parents: ['asset-parent-9'],
   parentMode: RelationshipMode.Add
})

// A relationship that would create a cycle is rejected — nothing is applied
try {
   await client.assets.update('asset-a', { parents: ['asset-c'], parentMode: RelationshipMode.Add })
} catch (err) {
   if (err.status === 409) {
      // asset-c already descends from asset-a — model the relationship instead of cycling it
   }
}

delete(id: string): Promise<void>

Deletes an Asset by id. Deletion is idempotent — deleting an already-deleted (or never-existing) id succeeds without error.

The asset's activity trail is not deleted with it: it remains reachable via activities below, with every entry's data erased down to {} (the fact that an action occurred, and who performed it, survives; the content it carried does not). The deletion itself is recorded as its own activity, and any surviving child that named the deleted asset as a parent gets a parent.removed activity explaining why the link disappeared. Decrypting ciphertext that belonged to a deleted asset continues to work — its decrypt activity is recorded the same redacted way.


activities(id: string, page?: PageRequest): Promise<Page<AssetActivity>>

Returns one page of id's activity trail. Unlike get, this keeps working after the asset has been deleted — it's the way to retrieve a deleted asset's audit history for a compliance review or investigation.

Parameters:

Parameter Type Required Description
id string Yes The Asset id
page.cursor string No Pagination cursor for the next page
page.limit number No Items per page

Returns: Promise<Page<AssetActivity>>

Example:

await client.assets.delete('asset-123')

// Still works — the trail survives the asset
const page = await client.assets.activities('asset-123')
for (const activity of page.data) {
   console.log(activity.action, activity.createdAt, activity.data)   // data is {} for a deleted asset
}

anchors(id: string, page?: PageRequest): Promise<Page<SigningAnchor>>

Returns one page of the provenance/signing anchors recorded for an Asset.


bulk<T, R>(items: T[], options: BulkExecuteOptions<T, R>): Promise<BulkResult<R>>

Runs a caller-supplied operation over many items on a bounded-concurrency pool, returning a per-item partial-failure report. This is the generic executor behind backfill / event-stream workloads — pair it with client-minted assetIds for idempotent retries. Shares the same pool and result vocabulary as client.bulk.

Parameters:

Parameter Type Required Description
items T[] Yes The work items
options.operation (item, index) => Promise<R> Yes Unit of work per item; its resolved value becomes the item's result
options.id (item, index) => string No Derives the result id echoed back per item (for retry-by-id). Defaults to the item index
options.concurrency number No Max simultaneous in-flight operations. Default 25
options.stopOnError boolean No Stop scheduling new work after the first failure. Default false (partial-failure)
options.signal AbortSignal No Abort the batch; unscheduled items come back marked unprocessed
options.onProgress (progress: BulkProgress) => void No Fired after each item settles

Returns: Promise<BulkResult<R>> — ordered per-item results plus total / succeeded / failed counts.


Bulk Operations (client.bulk)

High-throughput batch encrypt / decrypt / rotate over many items on a single bounded-concurrency pool. Per-item errors are isolated: one failure never rejects the batch — failed and unprocessed items come back with ok: false so callers retry by id. Access via client.bulk.

encrypt(items: BulkItem[], options?: BulkEncryptOptions): Promise<BulkResult>

Encrypts each item. Each distinct keyring is resolved exactly once. An item may carry its own keyring; otherwise options.keyring is used.

decrypt(items: BulkItem[], options?: BulkDecryptOptions): Promise<BulkResult>

Decrypts each item. An item may carry its own privateKey (BulkItem.privateKey); otherwise options.privateKey is used, so a batch can span keyrings with different private keys.

rotate(keyrings: string[], options?: BulkRotateOptions): Promise<BulkResult<void>>

Rotates each named keyring. One options set (algo) applies to every ring in the batch — to rotate rings with distinct algorithms, issue separate calls. Defaults to a concurrency of 8.

Shared control options (all of encrypt/decrypt/rotate): concurrency (default 25, or 8 for rotate), stopOnError (default false), signal, onProgress.

BulkItem shape: { id, data, keyring?, privateKey? } — id is echoed back on the result so failures can be retried by id.

Example:

const result = await client.bulk.encrypt(
   [
      { id: 'rec-1', data: 'first' },
      { id: 'rec-2', data: 'second' }
   ],
   { keyring: 'Finance', concurrency: 50 }
)

console.log(result.succeeded, result.failed)
const retryable = result.results.filter((r) => !r.ok).map((r) => r.id)

Activities (client.activities)

Read-only search over the platform's audit/activity records — the primitive a compliance / SIEM export integration drives. Mirrors the assets namespace's pagination + laziness contract. Access via client.activities.

search(query: ActivityQuery): Promise<Page<Activity>>

One page of matching activities (no cursor).

searchAll(query: ActivityQuery): AsyncIterable<Activity>

Lazily iterates every matching activity, fetching each page only as it is reached.

ActivityQuery fields (all optional, AND-combined; an empty query matches everything): accountId, accountUid, keyid (an API Key id), action, ring (a KeyRing name, not an id), valid (was the action granted?), time ({ start?, end? } ISO-8601), props (exact-match subset of stored props).

Activity.createdAt is epoch milliseconds (a number), unlike the ISO-8601 strings elsewhere in the SDK.

Example:

for await (const activity of client.activities.searchAll({ action: 'encrypt', valid: true })) {
   console.log(activity.keyid, activity.createdAt)
}

Admin / RBAC (client.admin)

The identity primitives an IdP-driven (Okta / SCIM-style) integration drives over the existing auth-server routes. Two sub-namespaces: admin.accounts manages identities, admin.apiKeys manages the access credentials (entitlements) that bridge an Account to the KeyRings it may use. To grant access you create an API Key with entitlements; to revoke you delete or disable it.

client.admin.accounts

Method Returns Description
create(input: CreateAccountInput) Promise<Account> Creates an Account (input.uid required)
get(id, options?) Promise<Account \| undefined> Reads one Account by id (or uid via options.isUid)
list(page?, options?) Promise<Page<Account>> One page of Accounts
listAll(options?) AsyncIterable<Account> Lazily iterates every Account
deactivate(id, ref?) Promise<Account> Soft-deletes an Account. Deactivation also disables every API Key on the account, immediately revoking its access. Returns the deactivated Account

client.admin.apiKeys

Method Returns Description
create(input: CreateApiKeyInput) Promise<ApiKey> Creates an API Key for an Account. The returned key carries the secret key (returned once, on create)
get(apikeyId) Promise<SafeApiKey \| undefined> Reads one API Key (never exposes the secret)
list(page?) Promise<Page<SafeApiKey>> One page of API Keys across all accounts
listAll() AsyncIterable<SafeApiKey> Lazily iterates every API Key
listByAccount(accountId, page?, ref?) Promise<Page<SafeApiKey>> One page of an Account's API Keys
listAllByAccount(accountId, ref?) AsyncIterable<SafeApiKey> Lazily iterates an Account's API Keys
delete(apikeyId) Promise<void> Permanently revokes an API Key
disable(apikeyId) Promise<SafeApiKey> Revokes access without deleting the record
updateEntitlements(apikeyId, flags, mode?) Promise<SafeApiKey> Updates entitlements; mode is EntitlementMode.Merge (default) / Replace / Delete

Entitlements are a list of names. flags is a string[] in requests and in responses alike; an entitlement is granted by being present in it. The { 'keyring.finance.decrypt': true } object earlier versions accepted is rejected with a 400, and there is no way to express a denial — leave the name out.

Casing is no longer your problem. Every name is trimmed, lower-cased and de-duplicated by the server, on all four paths: create, Merge, Replace and Delete. This note used to warn that Delete matched verbatim, so revoking Keyring.Finance.read silently removed nothing; that asymmetry is gone. Pass whatever spelling you like.

Example:

import { EntitlementMode } from '@grizzlycbg/grizzly-sdk'

const account = await client.admin.accounts.create({ uid: 'Account-Export-Service' })

const apiKey = await client.admin.apiKeys.create({
   accountId: account.id,
   flags: ['keyring.finance.decrypt']
})
console.log(apiKey.key)   // secret — shown only here

// Later: revoke by disabling the key
await client.admin.apiKeys.disable(apiKey.keyid)

Client-Minted Asset IDs

Mint an asset-<uuid> up front so a retried encrypt addresses the same record (idempotency). Mirrors the server's format and validation.

newAssetId(): string — Mints a fresh, well-formed asset id (asset-<uuid>).

isValidAssetId(id: string): boolean — True when id matches the platform's asset-<uuid> format.

Pass the minted id as EncryptAssetOptions.assetId. The first call creates the Asset with that id; a retry with the same id re-tags the same record (no duplicate). A concurrent create that loses the race is returned as a clean 409, and a malformed id is rejected with 400.


Tag Schema Validation

TagSchema validates an Asset's data bag before it hits the wire — a hand-rolled check (no JSON-schema dependency) of required keys, per-key runtime types, and an optional whole-bag custom function. A violation throws a ValidationError (code tag_schema.violation) client-side; the bad request never leaves the process.

Attach a schema per client (KeyClientOptions.tagSchema) or per call (encrypt/update options); a per-call schema overrides the client default. It is wired into assets.encrypt, assets.update, and — transitively — assets.bulk.

new TagSchema(init: TagSchemaInit)

type TagType = 'string' | 'number' | 'boolean' | 'object'
type TagRule = { type?: TagType; required?: boolean }
type TagSchemaInit = {
   fields?: Record<string, TagRule>              // per-key rules
   required?: string[]                           // required keys (merged with per-field required:true)
   validate?: (data: object) => true | string    // whole-bag check; return true or a violation message
}

Example:

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

const schema = new TagSchema({
   fields: { name: { type: 'string', required: true }, retentionYears: { type: 'number' } }
})

const client = await KeyClient.create({ host, apikey, tagSchema: schema })

// Throws ValidationError('Tag schema violation: required key "name" is missing.') — nothing is encrypted
await client.assets.encrypt('data', 'Finance', { data: { retentionYears: 7 } })

Logger & Observability Hooks

Logger — a structured logger sink injected via KeyClientOptions.logger; the SDK-owned transport uses it instead of console. All methods are optional:

interface Logger {
   debug?(message: string, meta?: Record<string, unknown>): void
   info?(message: string, meta?: Record<string, unknown>): void
   warn?(message: string, meta?: Record<string, unknown>): void
   error?(message: string, meta?: Record<string, unknown>): void
}

Supplying a logger also opts the legacy transport into its error channel, surfacing real decrypt failures as typed GrizzlyErrors (code decrypt.failed) instead of a misleading "No KeyRing/Key pair found".

ObservabilityHooks — request-lifecycle hooks injected via KeyClientOptions.hooks, additive alongside the logger:

interface ObservabilityHooks {
   onRequest?(info: RequestHookInfo): void     // before each attempt's fetch
   onResponse?(info: ResponseHookInfo): void   // when a response arrives (every attempt)
   onRetry?(info: RetryHookInfo): void         // when a retry is scheduled
}

RequestHookInfo is { method, path, requestId, attempt } (attempt is 1-based); onResponse adds { status, durationMs }; onRetry adds { reason, delayMs }.

Hooks are observability only. An exception thrown from a hook is caught and logged, never propagated — a throwing hook can never skip a fetch, drop a response, or cancel a retry.


Offline Verification (verify)

Signed assets can be verified offline — with no live connection to the platform — via the re-exported verify namespace, so one dependency covers both integration and verification. Verification-only: it cannot sign.

import { verify } from '@grizzlycbg/grizzly-sdk'

const keyset = await verify.getSigningKeyset(host, ringId, { apiKey })  // mirror a ring's keyset once
const result = await verify.verify(signedAsset, keyset)                 // then verify offline

The SDK also re-exports the typed signing errors and their guards so an encrypt failure can be branched precisely: SigningPausedError / isSigningPausedError (a deterministic pause — do not retry), AsymUnavailableError / isAsymUnavailableError (a retryable asym-key-server outage), and SigningRequiredError / isSigningRequiredError (a signing-required policy), keyed off the SigningErrorCode discriminant. These are separate from the GrizzlyError hierarchy.


Type Definitions

Algorithm

Supported encryption algorithms:

type Algorithm = 'aes256'

EncodingFormat

Output format options:

type EncodingFormat = 'string' | 'buffer' | 'readable-stream'

Encoding

Character encoding options:

type Encoding = 'utf8' | 'ascii' | 'base64' | 'hex' | 'binary' | 'utf16le'

KeyRingConfig

interface KeyRingConfig {
   props?: Record<string, any>
   key?: {
      props?: Record<string, any>
   }
   algos: {
      aes256?: {
         maxEncryptCount: number
      }
   }
}

Asset types

Timestamps cross the wire as ISO-8601 strings (except Activity.createdAt, which is epoch ms).

type Asset = {
   id: string
   createdAt: string
   data: object                              // the business-metadata bag
   parents: { assetId: string; createdAt: string }[]
   activities: AssetActivity[]
}

type AssetSummary = Pick<Asset, 'id' | 'createdAt' | 'data'>   // a search hit

type AssetRef = {
   id: string
   keyringId: string
   parents: string[]
   data: object
   createdAt?: string                        // present when hydrated
}

type EncryptAssetResult = {
   data: string | Buffer | Readable          // ciphertext
   asset: AssetRef
}

enum RelationshipMode { Replace = 'replace', Add = 'add', Remove = 'remove' }

type AssetUpdate = {
   data?: object
   parents?: string[]
   parentMode?: RelationshipMode             // default Replace
   context?: object
   tagSchema?: TagSchema
}

type AssetQuery = {
   accountId?: string
   keyringId?: string
   apikeyId?: string
   parents?: string[]         // CURRENT state: assets whose parent set right now contains any of these ids
   everParentedBy?: string    // HISTORY: assets ever parented by this id, including removed links
   time?: { start?: string; end?: string }   // ISO-8601
   data?: object                             // exact-match
}

type Page<T> = {
   data: T[]
   pagination: { limit: number; count: number; hasMore: boolean; nextCursor?: string }
}

Bulk types

type BulkItem = { id: string; data: string | Buffer | Readable; keyring?: string; privateKey?: string }

type BulkItemResult<T> =
   | { id: string; ok: true; result: T }
   | { id: string; ok: false; error: { message: string; code?: string } }

type BulkResult<T> = {
   results: BulkItemResult<T>[]
   total: number
   succeeded: number
   failed: number
}

Error Handling

SDK Error Types (GrizzlyError)

The asset, activity, and admin surfaces (client.assets, client.activities, client.admin, client.bulk) raise a typed GrizzlyError hierarchy over the SDK-owned transport — never a bare Error(message) or a swallowed undefined. Every error carries:

Field Type Description
status number? HTTP status, when the error came from a response
code string Stable machine code — validation / unauthorized / forbidden / not_found / rate_limited / server_error / network, or SDK-local decrypt.failed / tag_schema.violation / aborted. Safe to branch on
retryable boolean True when retrying the same request is safe
requestId string? The x-request-id sent with the call, for correlating to a server Activity

Branch on the class or the code:

Class Status Notes
AuthError 401 / 403 API key missing, invalid, or lacking the entitlement
NotFoundError 404 The addressed resource does not exist
ValidationError 400 Rejected as invalid (bad body/cursor; also thrown client-side by tag schemas)
RateLimitError 429 Rate limited; retryAfterMs? carries the server's Retry-After when present
ServerError 5xx / network Server-side or connect failure. Retryable
GrizzlyError — Base class / any other status
import { NotFoundError, RateLimitError, ValidationError } from '@grizzlycbg/grizzly-sdk'

try {
   // assets.family is the one asset surface that 404s on a missing asset
   await client.assets.family('asset-123')
   await client.assets.update('asset-123', { data: { state: 'approved' } })
} catch (err) {
   if (err instanceof NotFoundError) {
      // 404 from assets.family — the asset does not exist
   } else if (err instanceof RateLimitError) {
      await new Promise((r) => setTimeout(r, err.retryAfterMs ?? 1000))
   } else if (err instanceof ValidationError) {
      // 400 from assets.update — the ADDRESSED asset is missing (a missing referenced
      // *parent* id is a 404/NotFoundError instead); check err.code / err.message for the cause
   } else {
      throw err
   }
}

assets.get is the exception: a missing asset returns undefined rather than throwing NotFoundError. Among the surfaces that do throw, assets.family and assets.update both raise NotFoundError (404) for a missing asset — assets.update raises the same 404 whether the missing id is the target or a referenced parent, distinguished by the error message — and assets.delete is idempotent, returning without throwing when the asset is already gone.

Retry semantics. The transport retries with exponential backoff and full jitter (default 2 retries — up to 3 attempts). 429 and 503 are always retried, honoring Retry-After. Other 5xx and connect errors are retried only for idempotent requests: GET/DELETE are idempotent by default and POST/PATCH are not, so a write that already received a definitive response is never silently replayed. See API Error Responses for the full taxonomy.

Legacy errors

The KeyRing crypto path (ring.encrypt / ring.decrypt, client.encrypt / client.decrypt) still throws standard JavaScript Error objects. Wrap those calls in try-catch:

try {
   const ring = await client.ring.get('Finance')
   const encrypted = await ring.encrypt('data')
} catch (error) {
   console.error('Encryption failed:', error.message)
}

Common error scenarios:

  • Authentication failures: Invalid API key or insufficient permissions
  • KeyRing not found: Attempting to access non-existent KeyRing
  • Network errors: Connection issues with Grizzly instance
  • Decryption failures: Invalid encrypted data or missing keys

See Error Handling & Troubleshooting for detailed error resolution.