JavaScript/TypeScript SDK
JavaScript/TypeScript SDK for the Creed Space tool-call gateway. Promise-based, ships with type definitions. Before your agent runs a tool, the SDK asks for a decision on the exact tool name and arguments. An allow comes back with a short-lived signed token that your code redeems once with authorize.
Installation
npm install @creedspace/sdk
# or
yarn add @creedspace/sdk
# or
pnpm add @creedspace/sdkVersion
This page describes @creedspace/sdk 3.1.0, which is not published: npm has 1.0.0, which predates the authorize binding and the error classes shown below.
The SDK does not cover the content endpoints (/api/v1/safety/evaluate, /api/v1/pdp/adjudicate, constitutions). Call those over REST with a cs_ key in the X-API-Key header; see the Quickstart.
Quick Start
import { createClient } from '@creedspace/sdk';
const client = createClient({ apiKey: 'crd_live_...' });
const result = await client.decide({
toolName: 'send_email',
arguments: { to: 'user@example.com', subject: 'Hello' }
});
console.log(`Decision: ${result.decision}`); // ALLOW, DENY, REQUIRE_HUMAN
console.log(`Risk score: ${result.risk.score}`); // 0.0 - 1.0Configuration
import { createClient } from '@creedspace/sdk';
const client = createClient({
apiKey: 'crd_live_...',
baseUrl: 'https://api.creed.space', // Optional
timeoutMs: 30000, // Request timeout in ms
fetch: globalThis.fetch // Optional: pass your own fetch implementation
});The key must be a gateway key (crd_live_... or crd_test_...), which the SDK sends as Authorization: Bearer. A key in any other format throws CreedError with code INVALID_API_KEY when the client is created. See Authentication.
Environment Variables
const client = createClient({
apiKey: process.env.CREED_GATEWAY_KEY!
});Decisions
Callbacks
const result = await client.decide({
toolName: 'send_email',
arguments: { to: 'user@example.com', subject: 'Hello' },
onAllow: (decision) => console.log('Decision token:', decision.decisionToken),
onDeny: (decision) => console.log('Denied:', decision.reasons),
onRequireHuman: (decision) => console.log('Review queued:', decision.reviewId)
});constitutionId (default 'default') selects the constitution to evaluate against. context is optional; the server takes the tenant from your key and ignores any tenant, user or session ID sent there.
Result Types
type DecideResult = AllowDecision | DenyDecision | RequireHumanDecision;
// Shared fields: decision, runId, actionId, toolCallId, argsHash, risk { score, labels }
interface AllowDecision { decision: 'ALLOW'; decisionToken: string; expiresAt: string; /* ... */ }
interface DenyDecision { decision: 'DENY'; reasons: string[]; guidance: Record<string, string>; /* ... */ }
interface RequireHumanDecision { decision: 'REQUIRE_HUMAN'; reviewId?: string; featureStatus: 'active'; /* ... */ }Response keys are converted from snake_case to camelCase.
Authorize Before Running the Tool
An ALLOW is not yet permission to run the tool. Redeem the token once with authorize, passing back the identifiers decide returned, and start the side effect only if authorized is true:
if (result.decision === 'ALLOW') {
const auth = await client.authorize({
decisionToken: result.decisionToken,
runId: result.runId,
actionId: result.actionId,
toolCallId: result.toolCallId,
projectId: 'default',
toolName: 'send_email',
argsHash: result.argsHash
});
if (auth.authorized) {
// run the tool now
}
}SDK decisions are always made under project 'default'. A successful authorize authorizes starting the side effect at that instant; it cannot cancel work your code has already started.
Human Review
if (result.decision === 'REQUIRE_HUMAN' && result.reviewId) {
const status = await client.reviewStatus(result.reviewId);
console.log(status.status); // pending, approved, denied, expired
}Audit Trail
const audit = await client.audit({ runId: result.runId });
console.log(audit.eventCount);
console.log(audit.integrity.verified); // the server's own re-verification of the hash chainaudit also takes actionId and limit (default 50).
Service Status
const status = await client.status();
console.log(status.service, status.version);Error Handling
import {
createClient,
CreedError,
AuthenticationError,
RateLimitError,
ServerError
} from '@creedspace/sdk';
const client = createClient({ apiKey: process.env.CREED_GATEWAY_KEY! });
try {
const result = await client.decide({ toolName: 'send_email', arguments: { to: 'ops@example.com' } });
} catch (error) {
if (error instanceof AuthenticationError) {
console.log('Invalid gateway key');
} else if (error instanceof RateLimitError) {
console.log(`Rate limited. Retry after ${error.retryAfter}s`);
} else if (error instanceof ServerError) {
console.log(`Server error ${error.statusCode}: ${error.message}`);
} else if (error instanceof CreedError) {
console.log(`API error (${error.code}): ${error.message}`);
}
}Timeouts and network failures throw CreedError with code TIMEOUT or NETWORK_ERROR.
TypeScript Support
The SDK ships with TypeScript types for every request and response:
import type { DecideResult } from '@creedspace/sdk';
function handle(result: DecideResult): void {
if (result.decision === 'ALLOW') {
console.log(result.decisionToken);
} else if (result.risk.score > 0.5) {
console.log('High risk tool call');
}
}Examples
Gating a Tool Call
import { createClient } from '@creedspace/sdk';
const client = createClient({ apiKey: process.env.CREED_GATEWAY_KEY! });
async function runGuarded(toolName: string, args: Record<string, unknown>, run: () => Promise<unknown>) {
const decision = await client.decide({ toolName, arguments: args });
if (decision.decision === 'DENY') {
throw new Error(`Denied: ${decision.reasons.join(', ')}`);
}
if (decision.decision === 'REQUIRE_HUMAN') {
return { status: 'pending_review', reviewId: decision.reviewId };
}
const auth = await client.authorize({
decisionToken: decision.decisionToken,
runId: decision.runId,
actionId: decision.actionId,
toolCallId: decision.toolCallId,
projectId: 'default',
toolName,
argsHash: decision.argsHash
});
if (!auth.authorized) {
throw new Error(`Not authorized: ${auth.message}`);
}
return run();
}Browser Usage
In the browser, proxy requests through your server so the API key stays server-side:
// Server-side proxy
app.post('/api/tool-decision', async (req, res) => {
const result = await client.decide({ toolName: req.body.toolName, arguments: req.body.arguments });
res.json({ decision: result.decision });
});
// Client-side
const response = await fetch('/api/tool-decision', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ toolName: 'send_email', arguments: { to: 'user@example.com' } })
});
const result = await response.json();WARNING
Never expose your API key in client-side code. Always use a server-side proxy.
REST Content Endpoints
Safety Evaluation and PDP Adjudication use a cs_ key in X-API-Key, separate from the gateway SDK. Keep this helper on your server.
REST Helper
No package is needed. Keep your API key on the server and send it in the X-API-Key header:
// creed.ts (server-side only)
const CREED_BASE_URL = 'https://api.creed.space';
export async function creedRequest<T>(path: string, body?: unknown): Promise<T> {
const response = await fetch(`${CREED_BASE_URL}${path}`, {
method: body === undefined ? 'GET' : 'POST',
headers: {
'X-API-Key': process.env.CREED_API_KEY!,
'Content-Type': 'application/json'
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(30_000) // request timeout in ms
});
if (!response.ok) {
throw new CreedApiError(response.status, response.headers.get('Retry-After'), await response.text());
}
return (await response.json()) as T;
}
export class CreedApiError extends Error {
constructor(
public readonly status: number,
public readonly retryAfter: string | null,
body: string
) {
super(`Creed Space API error ${status}: ${body}`);
}
}Safety Evaluation
POST /api/v1/safety/evaluate scores text against the Psychopathia agent-dysfunction rule set. It is not a general harmful-content classifier; read the Safety Evaluation reference for what it does and does not catch.
Basic Evaluation
const { evaluation } = await creedRequest<EvaluateResponse>('/api/v1/safety/evaluate', {
input_text: 'User message here'
});
if (evaluation.decision === 'allow') {
processMessage(message);
} else {
// 'modify', 'block' or 'escalate': hold the text back from the agent.
holdForReview(message, evaluation);
}With Context
Only four boolean flags in context change the score: is_documentation, is_test_file, is_tutorial and read_only. Other keys are ignored. The flags lower the score, so set them from your own code and never from user input:
const { evaluation } = await creedRequest<EvaluateResponse>('/api/v1/safety/evaluate', {
input_text: tutorialPageText,
context: { is_tutorial: true }, // set by our crawler, not by the user
session_id: 'user-123-session-456'
});Response Type
interface EvaluateResponse {
status: 'success';
scope: string;
crisis_support: {
detected: true;
message: string;
resources: Array<{ name: string; region: string; contact: string }>;
} | null;
evaluation: {
risk_score: number; // 0.0 - 1.0
decision: 'allow' | 'modify' | 'block' | 'escalate';
is_blocked: boolean;
matched_rules: Array<{ id: string; name: string; axis: string; severity: number }>;
context_multiplier: number;
explanations: string[];
decision_time_ms: number;
};
}PDP Adjudication
For policy decisions against your constitutions, with rationale, norm citations and a verifiable decision kernel:
const kernel = await creedRequest<DecisionKernel>('/api/v1/pdp/adjudicate', {
message: 'Content to evaluate',
constitutions: ['UEF'],
context: { key: 'value' }
});
console.log(`Decision: ${kernel.decision}`); // permit | divert | forbid | depends
console.log(`Rationale: ${kernel.rationale}`);
console.log('Norms:', kernel.norms);
if (kernel.decision !== 'permit') {
// divert, forbid and depends are all "not permitted"
}interface DecisionKernel {
decision: 'permit' | 'divert' | 'forbid' | 'depends';
rationale: string;
norms: Array<{ t: 'O' | 'F' | 'P'; act: string; ref: string }>;
precedence: string[];
caveats: string[];
nonce: string | null;
hash: string;
timestamp: number;
decision_id?: string | null;
}See the PDP reference for every field.
Constitutions
List Available Constitutions
const hierarchy = await creedRequest<{
rootFolders: Array<{
folderTitle: string;
constitutions: Array<{ title: string; relativePath: string }>;
}>;
}>('/api/v1/constitutions/');
for (const folder of hierarchy.rootFolders) {
for (const c of folder.constitutions) {
console.log(`${folder.folderTitle}: ${c.title} (${c.relativePath})`);
}
}