npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aurite-ai/kahuna-sdk

v0.4.0

Published

Governance SDK for Kahuna — SPIFFE identity, mTLS, and fail-closed action authorization.

Downloads

139

Readme

@aurite-ai/kahuna-sdk

SDK for building Kahuna-governed agents with SPIFFE/SPIRE identity.

Overview

This package provides the infrastructure code needed to integrate with Kahuna's authorization service using mTLS authentication with SPIFFE SVIDs. It eliminates the need to copy-paste 315 lines of boilerplate code across agent implementations.

Key Benefits:

  • Reduces code duplication - 315 lines of infrastructure code maintained centrally
  • Improves correctness - Single source of truth for mTLS, SVID loading, and authorization
  • Accelerates development - New agents integrate in hours instead of days
  • Ensures consistency - All agents use the same tested patterns
  • Simplifies maintenance - Bug fixes and improvements propagate automatically

Installation

npm install @aurite-ai/kahuna-sdk

Quick Start

import { KahunaClient, SvidFileLoader } from '@aurite-ai/kahuna-sdk';

// 1. Load SVID files from spiffe-helper sidecar
const loader = new SvidFileLoader(
  process.env.SVID_CERT ?? "/tmp/agent-svid/svid.0.pem",
  process.env.SVID_KEY ?? "/tmp/agent-svid/svid.0.key",
  process.env.SVID_CA ?? "/tmp/agent-svid/bundle.0.pem",
);

const svid = loader.load();

// 2. Create Kahuna client with mTLS authentication
const client = new KahunaClient(
  process.env.KAHUNA_URL ?? "https://kahuna-server.kahuna.svc.cluster.local:8443",
  svid,
  process.env.KAHUNA_SERVER_SPIFFE_ID ?? "spiffe://kahuna.local/server",
);

// 3. Watch for SVID rotation (every 30 minutes)
loader.watchForRotation((newSvid) => {
  console.log('SVID rotated, updating client...');
  client.updateSvid(newSvid);
});

// 4. Authorize actions before executing them
const response = await client.authorize({
  action_id: crypto.randomUUID(),
  parameters: {
    action_type: 'write_data',
    target_resource_type: 'file',
    target_resource_id: '/tmp/example.txt',
    write_size_bytes: 1024,
    is_overwrite: false,
  },
  submitted_at: new Date().toISOString(),
});

if (response.outcome === 'authorized') {
  // Proceed with the action
  console.log('Action authorized!');
} else {
  // Handle denial
  console.error('Action denied:', response.deny_reason_code);
}

Core Concepts

SVID Loading

The SvidFileLoader class loads X.509 certificates from the spiffe-helper sidecar:

  • Automatic retry logic - Handles cert/key mismatch during rotation
  • Certificate-key validation - Uses OpenSSL to verify the pair matches
  • File watching - Detects rotation and invokes callbacks
  • Directory-based watching - Handles inode replacement during rotation
const loader = new SvidFileLoader(certPath, keyPath, caPath);
const svid = loader.load(); // Synchronous load with retry

loader.watchForRotation((newSvid) => {
  // Called when sidecar rotates credentials (every 30 minutes)
  client.updateSvid(newSvid);
});

mTLS Authentication

The createMtlsAgent function creates an HTTPS agent with SPIFFE ID verification:

  • URI SAN validation - Verifies SPIFFE IDs, not DNS names
  • Impersonation prevention - Ensures you're talking to the real Kahuna server
  • Handshake abortion - Rejects connections before sending request body
import { createMtlsAgent } from '@aurite-ai/kahuna-sdk';

const agent = createMtlsAgent(svid, 'spiffe://kahuna.local/server');

// Use with any HTTPS request
https.request(url, { agent }, (res) => {
  // Server's SPIFFE ID was verified during handshake
});

Why this matters: Every workload in the trust domain has a CA-signed certificate. Chain verification alone doesn't tell you that you reached Kahuna — the SPIFFE ID check is what prevents impersonation attacks.

Authorization Flow

The KahunaClient class wraps the /v1/actions/authorize endpoint:

  • mTLS-authenticated requests - Uses SPIFFE SVIDs for authentication
  • SVID rotation support - Updates agent without downtime
  • Typed interfaces - TypeScript types for requests and responses
  • Error handling - Distinguishes network errors from authorization denials
const client = new KahunaClient(baseUrl, svid, serverSpiffeId);

const response = await client.authorize({
  action_id: crypto.randomUUID(),
  parameters: {
    action_type: 'send_email',
    recipient: '[email protected]',
    subject: 'Test email',
    body_size_bytes: 1024, // Size, NOT content
  },
  submitted_at: new Date().toISOString(),
});

if (response.outcome === 'authorized') {
  // Execute the action
} else {
  // Handle denial: response.deny_reason_code
}

SVID Rotation

SVIDs are rotated every 30 minutes by the spiffe-helper sidecar. The SDK handles this automatically:

// Set up rotation handler once during initialization
loader.watchForRotation((newSvid) => {
  client.updateSvid(newSvid);
});

// The client will use new credentials for all subsequent requests
// Old connections are gracefully closed

API Reference

SvidFileLoader

Loads SVID files from the spiffe-helper sidecar.

class SvidFileLoader {
  constructor(certPath: string, keyPath: string, caPath: string);
  
  load(): SvidFiles;
  watchForRotation(callback: (svid: SvidFiles) => void): FSWatcher;
}

Methods:

  • load() - Load SVID files synchronously with retry logic. Throws SvidLoadError if files cannot be read or cert/key mismatch persists.
  • watchForRotation(callback) - Watch for SVID rotation and invoke callback with new credentials. Returns a watcher that can be closed with .close().

KahunaClient

Client for the /v1/actions/authorize endpoint.

class KahunaClient {
  constructor(
    baseUrl: string,
    svid: SvidFiles,
    serverSpiffeId: string,
    options?: KahunaClientOptions,
  );

  updateSvid(svid: SvidFiles): void;
  authorize(request: AuthorizeRequest): Promise<AuthorizeResponse>;
}

interface KahunaClientOptions {
  timeoutMs?: number;           // per attempt, default 5000
  maxRetries?: number;          // retries reuse the same action_id, default 2
  minimize?: boolean;           // default true — see below before turning it off
  minimization?: MinimizationOptions;
  definitions?: DefinitionCache; // custom actions — see "Custom actions"
}

Methods:

  • updateSvid(svid) - Update the SVID after rotation. Call this when the sidecar rotates credentials.
  • authorize(request) - Authorize an action with Kahuna. Returns the authorization response. Throws SpiffeIdMismatchError if the server presents an unexpected SPIFFE ID, and a KahunaTimeoutError / KahunaHttpError / KahunaTransportError / KahunaValidationError for the corresponding failures.

On minimize. It is on by default and should stay on. The client sends the policy-relevant projection of your parameters plus a digest of the whole set; the raw values never leave the workload. { minimize: false } puts the action's full parameters on the wire — including the ones you would not want in an authorization log. An action type this build has no tiers for is not an error: it minimizes to nothing and is sent, so the server can refuse it and record it for an operator to register. isMinimizable(parameters) answers "does this build know how to project this?" for diagnostics.

createMtlsAgent

Create an HTTPS agent with SPIFFE ID verification.

function createMtlsAgent(
  svid: SvidFiles,
  expectedServerSpiffeId: string
): https.Agent;

Parameters:

  • svid - The SVID files (cert, key, CA bundle)
  • expectedServerSpiffeId - The SPIFFE ID the server must present (e.g., 'spiffe://kahuna.local/server')

Returns: An https.Agent configured for mTLS with SPIFFE ID verification.

Types

interface SvidFiles {
  cert: string;  // PEM-encoded certificate
  key: string;   // PEM-encoded private key
  ca: string;    // PEM-encoded CA bundle
}

interface AuthorizeRequest {
  action_id: string;
  parameters: Record<string, unknown>;
  submitted_at: string;
}

interface AuthorizeResponse {
  outcome: "authorized" | "denied";
  deny_reason_code?: string;
  evaluated_policy_ids: string[];
  matched_policy_id?: string;
}

Errors

class SpiffeIdMismatchError extends Error {   // server presented an unexpected SPIFFE ID
  constructor(expected: string, actual: string);
}

class SvidLoadError extends Error {           // SVID files unreadable or cert/key mismatched
  constructor(message: string, cause?: Error);
}

class AuthorizationError extends Error {      // the action was refused
  constructor(message: string, denyReasonCode?: string);
}

class KahunaTimeoutError extends Error {}     // no answer within timeoutMs
class KahunaHttpError extends Error {}        // a non-200 from the mediator
class KahunaTransportError extends Error {}   // connection refused, DNS, TLS
class KahunaValidationError extends Error {}  // a 200 whose body is not a decision

// Narrow without instanceof, which is unreliable across duplicate installs of
// the package (two versions in one tree, or ESM and CJS of the same version).
function isKahunaError(err: unknown): boolean;
function isKahunaErrorOfKind(err: unknown, kind: string): boolean;

deny_reason_code is an open set. The SDK does not validate it against a list, so a newer mediator can add one without breaking clients — which means a switch over it needs a default. schema_violation, for instance, is what a catalog-aware mediator answers when a custom action's parameters do not match its registered schema.

Examples

Basic File Agent

import { KahunaClient, SvidFileLoader } from '@aurite-ai/kahuna-sdk';
import { writeFile } from 'node:fs/promises';

// Initialize SDK
const loader = new SvidFileLoader(
  '/tmp/agent-svid/svid.0.pem',
  '/tmp/agent-svid/svid.0.key',
  '/tmp/agent-svid/bundle.0.pem',
);

const svid = loader.load();
const client = new KahunaClient(
  'https://kahuna-server.kahuna.svc.cluster.local:8443',
  svid,
  'spiffe://kahuna.local/server',
);

loader.watchForRotation((newSvid) => {
  client.updateSvid(newSvid);
});

// Implement governed file write
const writeFileWithGovernance = governed({
  client,
  name: 'write_file',

  // Map the call to what policy decides on: the SHAPE of the action, never its
  // contents. Minimization is on by default, so only these projected values —
  // plus a digest of the full parameters — leave the workload.
  params: (i: { path: string; content: string }) => ({
    action_type: 'write_data',
    target_resource_type: 'file',
    target_resource_id: i.path,
    write_size_bytes: Buffer.byteLength(i.content, 'utf8'),
    is_overwrite: existsSync(i.path),
  }),

  // Reached only after Kahuna authorizes. Throws on a refusal, so a denied call
  // never looks to a caller — or a model — like one that ran.
  execute: async (i) => {
    await writeFile(i.path, i.content);
    return `File written: ${i.path}`;
  },
});

Any agent framework

governed() returns a plain (input) => Promise<output>, which is the tool interface every framework accepts. There is no adapter to install:

import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';

const tool = new DynamicStructuredTool({
  name: 'write_file',
  description: 'Write content to a file.',
  schema: z.object({ path: z.string(), content: z.string() }),
  func: writeFileWithGovernance,   // the governed() result from above
});

The same value drops into other frameworks with one line each:

tool({ description, parameters: schema, execute: writeFileWithGovernance });          // Vercel AI SDK
FunctionTool.from(writeFileWithGovernance, { name, description, parameters });        // LlamaIndex
createTool({ id, description, inputSchema: schema,
             execute: ({ context }) => writeFileWithGovernance(context) });          // Mastra
server.tool('write_file', schema, writeFileWithGovernance);                            // MCP
await writeFileWithGovernance({ path: '/tmp/report.txt', content: '...' });          // no framework

Full per-framework wiring: framework-recipes.md.

A LangChain base class (KahunaGovernedTool) also exists in this monorepo, but it is not published to npm — it predates governed() and wraps exactly the snippet above. Use the snippet.

Governing several actions at once

governAll declares a whole toolset in one place, and makes an action you forgot to declare fail rather than run ungoverned:

import { governAll } from '@aurite-ai/kahuna-sdk';

const governed = governAll({
  client,
  actions: {
    write_file: { params: (i) => ({ /* ... */ }), execute: async (i) => '...' },
  },
});

governed.assertCovers(tools.map((t) => t.name));   // at startup
// Error: 1 tool(s) registered with the agent but not governed: run_shell.

It does not reduce the parameter mappings — those are what a security review reads, and they cannot be inferred. It removes the silent failure where a forgotten mapping gives the agent a capability nobody authorized.

Custom actions

The SDK has tiers compiled in for five action types. Everything your organization registers in the Kahuna console — refund_order, provision_tenant, whatever the agent actually does — is described by a definition that lives on the server, and the client reads it at runtime.

That is the point: an admin registers an action in the console and your agents use it — no SDK release, and no code change per action.

Without a DefinitionCache, a custom action still authorizes, but it is sent with no attributes: no policy about its fields can match, so it is refused and recorded for an operator to see. With one, it is sent with exactly the fields the admin classified as policy-relevant.

Nothing to wire. The client fetches the catalog over its own mTLS transport and refreshes when it goes stale:

import { KahunaClient } from '@aurite-ai/kahuna-sdk';

const kahuna = new KahunaClient(baseUrl, svid, serverSpiffeId);

// An action an operator registered in the console. No setup, no refresh loop.
await kahuna.authorize({
  action_id: crypto.randomUUID(),
  parameters: { action_type: 'refund_order', amount_minor: 4999 },
  submitted_at: new Date().toISOString(),
});

A definitions call happens only for an action this build has no tiers for, so a workload using only the built-in five never contacts the endpoint. If the endpoint is unreachable the action minimizes to nothing and is refused — the same path an unregistered action takes — rather than failing the call.

Pass your own cache to set a freshness bound, cap disclosure, or see refusals:

import { DefinitionCache, KahunaClient } from '@aurite-ai/kahuna-sdk';

const definitions: DefinitionCache = new DefinitionCache({
  fetchDefinitions: (generation) => kahuna.fetchActionDefinitions(generation),
  maxAgeMs: 5 * 60_000,                 // after this, unrefreshed means minimize to nothing
  disclosureFloor: { refund_order: { customer_note: 'payload' } },
  onRefused: (actionType, reason) =>    // never silent
    console.warn(`definition refused: ${actionType} — ${reason}`),
});

const kahuna: KahunaClient = new KahunaClient(baseUrl, svid, serverSpiffeId, { definitions });

Or { definitions: false } to switch it off: custom actions then minimize to nothing, which is the pre-catalog behaviour.

const client = new KahunaClient(baseUrl, svid, serverSpiffeId, { definitions });


**Three rules worth knowing before you rely on it**, because the tier table is a
disclosure decision that now arrives from the server rather than from code you
reviewed:

1. **A fetched definition can never redefine a built-in action.** The server
   refuses such a row and the client refuses it again, independently.
2. **A stale cache minimizes to nothing** — never to a previously fetched, wider
   table. Definitions most often change because someone narrowed one.
3. **An unknown projection refuses the whole definition** rather than applying a
   closest match.

If you want a ceiling the catalog cannot raise, pass a `disclosureFloor`. It can
only narrow — there is deliberately no way to express widening a field:

```typescript
new DefinitionCache({
  fetchDefinitions,
  // Whatever the catalog says, this workload never sends customer_email.
  disclosureFloor: { refund_order: { customer_email: 'payload' } },
});

Migration Guide

Before SDK (480 lines of integration overhead)

examples/kahuna-file-agent/
├── src/
│   ├── index.ts              (113 lines)
│   ├── agent.ts              (32 lines)
│   ├── tools/
│   │   └── write-file.ts     (57 lines)
│   ├── svid-loader.ts        (117 lines) ← DUPLICATED
│   ├── mtls-client.ts        (76 lines)  ← DUPLICATED
│   └── kahuna-client.ts      (122 lines) ← DUPLICATED

After SDK (~55 lines)

examples/kahuna-file-agent/
├── src/
│   ├── index.ts              (40 lines)  ← Simplified
│   ├── agent.ts              (15 lines)  ← Simplified
│   └── tools/
│       └── write-file.ts     (45 lines)  ← Simplified
└── package.json
    └── dependencies:
        └── @aurite-ai/kahuna-sdk: ^0.1.0

Migration steps:

  1. Install the SDK: npm install @aurite-ai/kahuna-sdk
  2. Replace local imports with SDK imports:
    // Before
    import { SvidFileLoader } from './svid-loader.js';
    import { KahunaClient } from './kahuna-client.js';
       
    // After
    import { SvidFileLoader, KahunaClient } from '@aurite-ai/kahuna-sdk';
  3. Delete local infrastructure files:
    • src/svid-loader.ts
    • src/mtls-client.ts
    • src/kahuna-client.ts
  4. Update imports in your agent code
  5. Test that everything still works

Result: 87% reduction in code (429 → 55 lines)

Troubleshooting

SVID Load Errors

Problem: SvidLoadError: Failed to load SVID files after 5 attempts

Causes:

  • SVID files don't exist at the specified paths
  • Cert and key are mismatched (rotation race condition)
  • File permissions prevent reading

Solutions:

  • Verify the spiffe-helper sidecar is running
  • Check file paths in environment variables
  • Ensure the agent has read permissions on SVID files
  • The loader retries automatically, but persistent failures indicate a configuration issue

SPIFFE ID Mismatch

Problem: SpiffeIdMismatchError: expected spiffe://kahuna.local/server, got spiffe://kahuna.local/other

Causes:

  • Wrong server SPIFFE ID configured
  • Connecting to wrong service
  • Man-in-the-middle attack (rare in Kubernetes)

Solutions:

  • Verify KAHUNA_SERVER_SPIFFE_ID environment variable
  • Check that you're connecting to the correct Kahuna server URL
  • Review SPIRE server configuration

Authorization Denied

Problem: outcome: 'denied' with deny_reason_code

Common reason codes:

  • credential_revoked - Agent's credential was revoked (check kill switches)
  • policy_deny - No policy permits the action
  • rate_limit_exceeded - Too many requests
  • invalid_parameters - Action parameters don't match schema

Solutions:

  • Check Kahuna policies for the action type
  • Verify action parameters match the expected schema
  • Review agent kill switch status
  • Check rate limits

Network Errors

Problem: Request failed: connect ECONNREFUSED

Causes:

  • Kahuna server is not running
  • Wrong URL configured
  • Network connectivity issues

Solutions:

  • Verify Kahuna server is running: kubectl get pods -n kahuna
  • Check KAHUNA_URL environment variable
  • Test connectivity: curl -k https://kahuna-server.kahuna.svc.cluster.local:8443/health

Development

Building

npm run build

Type Checking

npm run typecheck

Testing

npm test

Cleaning

npm run clean

Version History

See CHANGELOG.md for version history.

License

MIT — see LICENSE, which ships in the package.

Support

For issues or questions:

Related Documentation