Customer developer docs

TypeScript SDK guide

Add Ledgix to Node, Next.js, LangChain, and Vercel AI flows with one customer-facing example path.

TypeScript SDK guide

Use the TypeScript SDK when your customer integration runs in Node.js, Next.js server routes, workers, or TypeScript-based agent frameworks. The examples on this page all guard the same payment or refund workflow so you can compare patterns without changing the mental model.

Install

text
npm install ledgix-ts

Optional adapters (install only what you need — each is an optional peer dependency):

text
# Next.js or server-side usage uses the base package only
 
# Vercel AI SDK
npm install ledgix-ts ai zod
 
# LangChain (only @langchain/core is required)
npm install ledgix-ts @langchain/core zod
 
# LlamaIndex
npm install ledgix-ts llamaindex

Core configuration

FieldTypeRequiredDescription
vaultUrlstringYesYour Vault base URL. Usually sourced from LEDGIX_VAULT_URL.
vaultApiKeystringYesYour tenant API key. Usually sourced from LEDGIX_VAULT_API_KEY.
agentIdstringNoThe calling service or agent identity recorded with each request.
sessionIdstringNoA grouping ID for the larger workflow or user session.
verifyJwtbooleanNoWhether to verify approval tokens automatically. Defaults to true.
reviewPollIntervalnumberNoPolling interval while waiting on processing or manual review.
reviewTimeoutnumberNoMaximum wait time before the SDK raises a timeout error.

Quick start

The recommended pattern is manifest-driven: declare enforcement once, instrument at startup, and keep tool signatures unchanged.

text
// ledgix.json
// {
//   "enforce": [
//     { "tool": "stripe*", "policyId": "financial-high-risk" },
//     { "tool": "*", "policyId": "default" }
//   ]
// }
 
import * as rawTools from "./tools.js";
import { configure, autoInstrument, currentToken } from "ledgix-ts";
 
// 1. Configure once at startup — reads LEDGIX_* env vars for most settings
configure({ agentId: "payments-agent" });
 
// 2. Auto-instrument matching tool exports according to ledgix.json
const tools = autoInstrument(rawTools);

Inside any instrumented function you can still access the A-JWT with currentToken():

text
export async function stripeCharge(amount: number, customerId: string) {
  const token = currentToken();
  return stripe.paymentIntents.create({
    amount: Math.round(amount * 100),
    customer: customerId,
    metadata: { vault_token: token },
  });
}

configure() accepts a partial config object (merged with env vars) or a pre-built LedgixClient instance.

currentToken() returns the A-JWT token for the current async context via AsyncLocalStorageundefined if called outside an instrumented or enforce-wrapped function. Use currentClearance() for the full ClearanceResponse object.

Manifest format

The TypeScript SDK uses ledgix.json in the current working directory by default:

text
{
  "enforce": [
    { "tool": "stripe*", "policyId": "financial-high-risk" },
    { "tool": "dbWrite*", "policyId": "data-mutation" },
    { "tool": "*", "policyId": "default" }
  ]
}

Rules are evaluated in order. The first matching glob wins.

You can also pass a path, inline object, or pre-built Manifest:

text
const tools = autoInstrument(rawTools, "./config/ledgix.json");
const moreTools = autoInstrument(rawTools, {
  enforce: [{ tool: "stripe*", policyId: "financial-high-risk" }],
});

Escape hatch: tool()

For functions that live outside the scanned object, use tool():

text
import { tool, currentToken } from "ledgix-ts";
 
export const specialFn = tool(async function specialRefund(amount: number) {
  return currentToken();
});
 
export const overrideFn = tool(
  async function stripeCharge(amount: number) {
    return currentToken();
  },
  { policyId: "override-policy" },
);

If a manifest has already been loaded, tool() uses the matching rule first and then applies any explicit overrides.

Advanced: explicit client API

For cases that need per-call client control or direct clearance requests:

text
import { LedgixClient } from "ledgix-ts";
 
export const client = new LedgixClient({
  vaultUrl: process.env.LEDGIX_VAULT_URL!,
  vaultApiKey: process.env.LEDGIX_VAULT_API_KEY!,
  agentId: "payments-agent",
  sessionId: "checkout-42",
});

The enforce(options)(fn) and vaultEnforce(client, options)(fn) APIs remain available for advanced or compatibility cases. vaultEnforce injects the clearance as the last parameter instead of using AsyncLocalStorage.

What requestClearance actually does

requestClearance is more than a single POST:

  1. It converts your camelCase object to the snake_case wire contract.
  2. It calls POST /request-clearance.
  3. If the response status is processing or pending_review, it polls GET /clearance-status/{requestId} until the result becomes terminal or times out.
  4. If the request is approved and verifyJwt is enabled, it fetches Vault JWKS and verifies the A-JWT locally.
  5. If the result is denied, it throws ClearanceDeniedError.

Use vaultEnforce when you want the approval token injected into the function that runs the real side effect.

text
import { vaultEnforce, type ClearanceResponse } from "ledgix-ts";
 
const guardedRefund = vaultEnforce(client, {
  toolName: "stripe_refund",
  policyId: "payments-prod",
})(async (
  amount: number,
  reason: string,
  orderEventId: string,
  clearance?: ClearanceResponse,
) => {
  return stripe.refunds.create({
    amount,
    reason,
    metadata: {
      order_event_id: orderEventId,
      ledgix_request_id: clearance!.requestId,
      ledgix_token: clearance!.token!,
    },
  });
});
 
await guardedRefund(4500, "duplicate charge", "ord_evt_2048");

Next.js server-side example

Use requestClearance directly when you want full control inside a route handler or server action.

text
import { NextResponse } from "next/server";
import { LedgixClient } from "ledgix-ts";
 
const client = new LedgixClient({
  vaultUrl: process.env.LEDGIX_VAULT_URL!,
  vaultApiKey: process.env.LEDGIX_VAULT_API_KEY!,
  agentId: "payments-agent",
});
 
export async function POST(request: Request) {
  const body = await request.json();
 
  const clearance = await client.requestClearance({
    toolName: "create_stripe_payment",
    toolArgs: {
      amount: body.amount,
      currency: body.currency,
      customer_id: body.customerId,
      payment_method_id: body.paymentMethodId,
      order_event_id: body.orderEventId,
      reasoning: body.reasoning,
    },
    agentId: "payments-agent",
    sessionId: body.checkoutSessionId,
    context: { policy_id: "payments-prod" },
  });
 
  if (!clearance.approved || !clearance.token) {
    return NextResponse.json(
      {
        status: clearance.status,
        reason: clearance.reason,
        requestId: clearance.requestId,
      },
      { status: 202 },
    );
  }
 
  const payment = await stripe.paymentIntents.create({
    amount: body.amount,
    currency: body.currency,
    customer: body.customerId,
    metadata: {
      ledgix_request_id: clearance.requestId,
      ledgix_token: clearance.token,
    },
  });
 
  return NextResponse.json({ payment, requestId: clearance.requestId });
}

Vercel AI SDK example

Use the adapter when your tool should fail closed before the model can execute it.

text
import { tool } from "ai";
import { z } from "zod";
import { wrapVercelTool } from "ledgix-ts/adapters/vercel-ai";
 
const refundTool = tool({
  description: "Refund a customer payment",
  parameters: z.object({
    amount: z.number(),
    reason: z.string(),
    orderEventId: z.string(),
  }),
  execute: wrapVercelTool(
    client,
    "stripe_refund",
    async ({ amount, reason, orderEventId }) => {
      return stripe.refunds.create({
        amount,
        reason,
        metadata: { order_event_id: orderEventId },
      });
    },
    { policyId: "payments-prod" },
  ),
});

LangChain example

All adapters accept an optional client — when omitted they fall back to the global client set by configure():

text
import { configure } from "ledgix-ts";
import { wrapLangChainTool } from "ledgix-ts/adapters/langchain";
import { wrapTool } from "ledgix-ts/adapters/llamaindex";
import { wrapVercelTool } from "ledgix-ts/adapters/vercel-ai";
 
configure({ agentId: "payments-agent" });
 
// LangChain — no client arg needed
const guardedFn = wrapLangChainTool("stripe_refund", originalFn, { policyId: "stripe-agent-prod" });
 
// LlamaIndex — no client arg needed
const guardedFn = wrapTool("stripe_refund", originalFn, { policyId: "stripe-agent-prod" });
 
// Vercel AI — no client arg needed
execute: wrapVercelTool("stripe_refund", originalFn, { policyId: "stripe-agent-prod" })

Advanced: explicit wrapper at the tool boundary

text
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
import { wrapLangChainTool } from "ledgix-ts/adapters/langchain";
 
const refundSchema = z.object({
  amount: z.number(),
  reason: z.string(),
  orderEventId: z.string(),
});
 
const guardedRefund = wrapLangChainTool(
  client,
  "stripe_refund",
  async ({ amount, reason, orderEventId }) => {
    return stripe.refunds.create({
      amount: Number(amount),
      reason: String(reason),
      metadata: { order_event_id: String(orderEventId) },
    });
  },
  { policyId: "payments-prod" },
);
 
const refundTool = new DynamicStructuredTool({
  name: "stripe_refund",
  description: "Refund a customer payment",
  schema: refundSchema,
  func: guardedRefund,
});

Direct requestClearance example

Use this pattern when you need the raw response and the token explicitly.

text
const clearance = await client.requestClearance({
  toolName: "create_stripe_payment",
  toolArgs: {
    amount: 249.99,
    currency: "USD",
    customer_id: "cus_123",
    payment_method_id: "pm_123",
    order_event_id: "ord_evt_2048",
    reasoning: "Charge matches a completed order event.",
  },
  agentId: "payments-agent",
  sessionId: "checkout-42",
  context: { policy_id: "payments-prod" },
});
 
if (clearance.status === "pendingReview") {
  console.log("Waiting for reviewer", clearance.requestId);
} else if (clearance.approved && clearance.token) {
  console.log("Approved token", clearance.token);
} else {
  console.log("Blocked", clearance.reason);
}

Verification helpers

The client also supports:

  • fetchJwks() and verifyToken(token) for token verification
  • fetchLedger(limit) and fetchLedgerCheckpoints(limit) for audit history
  • fetchLedgerInclusionProof(requestId) for a single-entry inclusion proof
  • fetchLedgerConsistencyProof(from, to) for a consistency proof between checkpoints
  • fetchLedgerProofBundle(requestId) and verifyLedgerProofBundle(bundle) for combined proof verification

Exceptions

All SDK errors inherit from LedgixError. Catch the specific subclass you care about:

  • ClearanceDeniedError — Ledgix denied the request. Do not execute the action.
  • ManualReviewTimeoutError — polling hit reviewTimeout before a reviewer acted.
  • VaultConnectionError — network or transport failure reaching Vault.
  • TokenVerificationError — A-JWT signature, issuer, audience, or expiry check failed.
  • PolicyRegistrationErrorPOST /register-policy was rejected.
text
import { ClearanceDeniedError, ManualReviewTimeoutError } from "ledgix-ts";
 
try {
  await guardedRefund(4500, "duplicate charge", "ord_evt_2048");
} catch (err) {
  if (err instanceof ClearanceDeniedError) {
    // reviewer rejected or policy denied
  } else if (err instanceof ManualReviewTimeoutError) {
    // put the request on a retry queue or escalate
  } else {
    throw err;
  }
}

Context and manifest types

  • VaultContext and withVaultContext(ctx, fn) let you set per-call overrides (agentId, sessionId, policy hints) without rebuilding the client.
  • LedgixCallbackHandler is a LangChain callback handler that logs clearance decisions into the tracing pipeline.
  • ManifestRule is the TypeScript type describing a single rule inside ledgix.json — useful when you build the manifest from your own config source.

Common TypeScript mistakes

  • Wrapping a planner or orchestration step instead of the real payment or refund call.
  • Forgetting to pass a stable agentId and sessionId for tracing.
  • Treating pendingReview like a transport failure instead of an intentional workflow state.
  • Using a bundler-minified wrapper without validating how your tool arguments are serialized into the clearance request.