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
npm install ledgix-tsOptional adapters (install only what you need — each is an optional peer dependency):
# 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 llamaindexCore configuration
| Field | Type | Required | Description |
|---|---|---|---|
| vaultUrl | string | Yes | Your Vault base URL. Usually sourced from LEDGIX_VAULT_URL. |
| vaultApiKey | string | Yes | Your tenant API key. Usually sourced from LEDGIX_VAULT_API_KEY. |
| agentId | string | No | The calling service or agent identity recorded with each request. |
| sessionId | string | No | A grouping ID for the larger workflow or user session. |
| verifyJwt | boolean | No | Whether to verify approval tokens automatically. Defaults to true. |
| reviewPollInterval | number | No | Polling interval while waiting on processing or manual review. |
| reviewTimeout | number | No | Maximum 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.
// 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():
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 AsyncLocalStorage — undefined 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:
{
"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:
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():
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:
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:
- It converts your camelCase object to the snake_case wire contract.
- It calls
POST /request-clearance. - If the response status is
processingorpending_review, it pollsGET /clearance-status/{requestId}until the result becomes terminal or times out. - If the request is approved and
verifyJwtis enabled, it fetches Vault JWKS and verifies the A-JWT locally. - 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.
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.
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.
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
Recommended: configure once, wrap each tool
All adapters accept an optional client — when omitted they fall back to the global client set by configure():
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
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.
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()andverifyToken(token)for token verificationfetchLedger(limit)andfetchLedgerCheckpoints(limit)for audit historyfetchLedgerInclusionProof(requestId)for a single-entry inclusion prooffetchLedgerConsistencyProof(from, to)for a consistency proof between checkpointsfetchLedgerProofBundle(requestId)andverifyLedgerProofBundle(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 hitreviewTimeoutbefore a reviewer acted.VaultConnectionError— network or transport failure reaching Vault.TokenVerificationError— A-JWT signature, issuer, audience, or expiry check failed.PolicyRegistrationError—POST /register-policywas rejected.
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
VaultContextandwithVaultContext(ctx, fn)let you set per-call overrides (agentId, sessionId, policy hints) without rebuilding the client.LedgixCallbackHandleris a LangChain callback handler that logs clearance decisions into the tracing pipeline.ManifestRuleis the TypeScript type describing a single rule insideledgix.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
agentIdandsessionIdfor tracing. - Treating
pendingReviewlike 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.