Package Exports
- @specora/sdk
Readme
@specora/sdk
Specora Platform SDK for Node.js - AI governance and policy enforcement.
Installation
npm install @specora/sdk
# or
yarn add @specora/sdk
# or
pnpm add @specora/sdkQuick Start
Since v0.3.1, the SDK supports two configuration modes. Most apps want Model A (org-scoped) — the API key alone is sufficient; the platform resolves the org from the key.
Model A — org-scoped (default for SaaS + internal apps)
import { SpecoraClient } from '@specora/sdk';
const client = new SpecoraClient({
baseUrl: process.env.SPECORA_API_URL,
apiKey: process.env.SPECORA_API_KEY,
// vendorId and tenantId intentionally omitted.
});
const response = await client.proxyExecute({
provider: 'openai', // any of: openai, anthropic, gemini, deepseek, qwen, ollama, vllm
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello' }],
});Model B — vendor-reseller (B2B2C only)
Use this only if your deployment uses the nested vendor-tenant model and the platform has provisioned a platform_vendor row for you.
const client = new SpecoraClient({
baseUrl: process.env.SPECORA_API_URL,
apiKey: process.env.SPECORA_API_KEY,
vendorId: process.env.SPECORA_VENDOR_ID, // UUID; required in Model B
tenantId: process.env.SPECORA_TENANT_ID, // UUID; required in Model B
});Note: passing only one of vendorId / tenantId throws ValidationError — that's almost always a config bug.
Policy / budget / execution recording (Model B only for now)
// Policy check, budget validation, and separate execution recording
// endpoints are Model B only. Model A uses `proxyExecute` as the single
// entry point (budget enforcement at the Model A layer is tracked as a
// follow-up).
const policy = await client.checkPolicy({
subject: { type: 'pr_run', id: 'run-123' },
});
if (policy.decision === 'block') {
console.log('Blocked by policy:', policy.severity);
return;
}
// Execute your AI operation...
// Record the execution
await client.recordExecution({
provider: 'openai',
model: 'gpt-4',
costMicros: 5000, // $0.005
inputTokens: 1000,
outputTokens: 500,
success: true,
});Features
- Policy Evaluation: Check governance policies before AI operations
- Budget Management: Track and enforce spending limits
- Execution Recording: Record AI executions for tracking
- Assumptions Validation: Validate change assumptions
- Evidence Generation: Generate audit evidence bundles
- Risk Scoring: Get real-time risk metrics
- Webhook Verification: HMAC-SHA256 signature verification
- Self-Hosted Providers: Route to Ollama and other local models with full governance
Workflow Discovery & Model Assignment
Specora automatically discovers your workflows from SDK traffic so org admins can assign specific LLMs to each workflow — no manual registration needed.
How workflows are identified
The SDK resolves a workflow identity from (in priority order):
- Explicit
routingHints.agentName— set in code for each SDK call SPECORA_WORKFLOW_IDenvironment variable — set once per execution context (CI/CD pipeline, n8n node, cron job, backend service)- No identity — workflow discovery is skipped, uses default routing
This means any SDK usage context gets automatic workflow discovery:
# CI/CD pipeline step
SPECORA_WORKFLOW_ID=pr-review-pipeline node run-review.js
# n8n node environment
SPECORA_WORKFLOW_ID=nightly-report-generator
# Cron job
SPECORA_WORKFLOW_ID=monthly-compliance-scan ./run-scan.sh
# Docker / Kubernetes
env:
- name: SPECORA_WORKFLOW_ID
value: data-enrichment-pipelineSending routing hints
Pass purpose and/or agentName in routingHints during pre-authorization.
A single workflow can make multiple LLM calls with different purposes, and
org admins can assign different models to each purpose:
// Call 1: summarize (might use a fast/cheap model)
const auth1 = await client.preAuthorize({
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
estimatedTokens: 2000,
estimatedCostMicros: 500,
routingHints: {
agentName: 'pr-review-pipeline',
purpose: 'summarize',
},
});
// Call 2: security review (might use a premium model)
const auth2 = await client.preAuthorize({
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
estimatedTokens: 4000,
estimatedCostMicros: 2000,
routingHints: {
agentName: 'pr-review-pipeline',
purpose: 'security-review',
},
});
// The server may override provider/model based on admin assignments
console.log(auth1.selectedProvider); // e.g., 'deepseek' (cheaper for summaries)
console.log(auth2.selectedProvider); // e.g., 'anthropic' (premium for security)Model resolution cascade
When the SDK makes an LLM call, Specora resolves the model in this order:
- Purpose-specific assignment — admin assigned a model for this workflow + purpose
- Default workflow assignment — admin assigned a default model for this workflow
- Org model preferences — org-wide preferences by workflow type
- Router default — tier-based intelligent routing
Using with GovernanceRouter
Routing hints are also supported through GovernanceRouter.messages.create() and
GovernanceRouter.chat.completions.create() — both client and proxy modes:
import { SpecoraClient, GovernanceRouter } from '@specora/sdk';
import Anthropic from '@anthropic-ai/sdk';
const specora = new SpecoraClient({ /* ... */ });
const governed = new GovernanceRouter(specora, new Anthropic());
// Routing hints are forwarded to preAuthorize() automatically
const response = await governed.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Review this code for safety issues' }],
routingHints: {
purpose: 'safety-rewriter',
agentName: 'code-safety-agent',
},
});How it works
- When the SDK sends
routingHints.agentName(orSPECORA_WORKFLOW_IDis set), Specora auto-discovers the workflow and tracks all observed purposes. - Org admins see discovered workflows in the Vendor Workflows dashboard page.
- Admins assign models — either a default for the whole workflow, or purpose-specific models for different LLM calls within the workflow.
- On the next SDK call, the assigned model overrides default routing.
Routing hints reference
| Hint | Type | Description |
|---|---|---|
agentName |
string |
Workflow identity (e.g., 'pr-review-pipeline'). Falls back to SPECORA_WORKFLOW_ID env var. |
purpose |
string |
Purpose of this specific LLM call (e.g., 'summarize', 'security-review'). Enables purpose-scoped model assignment. |
workflowType |
string |
Workflow category for org-level preferences (e.g., 'codegen', 'review', 'qa') |
preferredRegion |
string |
Preferred data processing region |
requiredCapability |
string |
Required model capability |
fallbackAllowed |
boolean |
Whether fallback providers are acceptable |
Self-Hosted Providers (Ollama)
Specora supports routing to self-hosted models via Ollama. Ollama uses an
OpenAI-compatible API at /v1/chat/completions, requires no API key, and is
configured via the OLLAMA_BASE_URL environment variable on the Specora server.
Server configuration
Set OLLAMA_BASE_URL on the Specora API server (not in the SDK):
# In your Specora server environment
OLLAMA_BASE_URL=http://192.168.1.50:11434Supported models
| Model | Context | Use Case |
|---|---|---|
llama3:8b |
8K | General text, summarization |
llama3.2 |
8K | General text, summarization |
qwen2.5-coder:32b |
32K | Code generation, debugging |
Routing to Ollama
Use workflow model assignment or purpose-based routing to direct specific workflows to Ollama:
// Org admin assigns "safety-rewriter" workflow to Ollama via dashboard,
// or create a PURPOSE_ROUTING governance policy
const response = await governed.messages.create({
model: 'claude-sonnet-4-20250514', // default model (overridden by assignment)
max_tokens: 1024,
messages: [{ role: 'user', content: 'Rewrite for safety' }],
routingHints: { agentName: 'safety-rewriter' },
});
// If the admin assigned Ollama to this workflow, Specora routes there insteadOllama is gated to T1 (low complexity/risk) tasks with a 1200 token context limit. If Ollama is unavailable, the request either blocks (fail-closed) or escalates to T2 providers depending on the routing policy.
Model Preferences
Query and resolve per-workflow model preferences set through the dashboard.
// Get global preferences
const prefs = await client.getPreferences();
console.log(prefs.preferences); // [{slot: 1, providerName: 'anthropic', modelName: 'claude-sonnet-4', ...}]
// Get codegen-specific preferences
const codegenPrefs = await client.getPreferences('codegen');
// Resolve the best model for a specific workflow + capability
const resolved = await client.resolvePreference({
capabilities: ['code_generation'],
workflowType: 'codegen',
});
if (resolved.providerName) {
console.log(`Using: ${resolved.providerName}/${resolved.modelName}`);
console.log(`Source: ${resolved.source}, fallback: ${resolved.fallbackUsed}`);
}Available workflow types: codegen, review, qa, spec, debug, plan.
When workflowType is provided, workflow-specific preferences are tried first,
then global preferences as fallback, then governance defaults.
Configuration
const client = new SpecoraClient({
// Required
baseUrl: 'https://api.specora.ai',
apiKey: 'sk_...',
vendorId: 'vendor-uuid',
tenantId: 'tenant-uuid',
// Optional
failOpen: false, // Default: false (fail-closed)
timeout: 30000, // Request timeout in ms
retries: 2, // Retry attempts
});Fail-Closed Semantics
By default, the SDK operates in fail-closed mode. This means:
- Network errors result in blocked decisions
- Server errors result in blocked decisions
- Timeouts result in blocked decisions
This ensures that if the Specora API is unavailable, your AI operations are blocked rather than allowed without governance.
To change this behavior:
const client = new SpecoraClient({
// ...
failOpen: true, // Errors result in allowed decisions
});API Reference
checkPolicy(request)
Evaluate governance policy for an operation.
const result = await client.checkPolicy({
subject: { type: 'pr_run', id: 'run-123' },
inputs: { /* optional context */ },
});
// result.decision: 'allow' | 'block' | 'warn'
// result.severity: 'low' | 'medium' | 'high' | 'critical'
// result.invariants: Array<{ id, status, reason }>
// result.override: { eligible, requiredJustification, expiresMaxSeconds }validateBudget(request?)
Check budget status for the tenant.
const budget = await client.validateBudget();
// budget.budgetRemaining: number (USD)
// budget.budgetTotal: number (USD)
// budget.usagePercent: number (0-100)
// budget.forecastBreach: boolean
// budget.enforcementState: 'normal' | 'throttled' | 'blocked'recordExecution(request)
Record an AI execution for budget tracking.
const result = await client.recordExecution({
provider: 'openai',
model: 'gpt-4',
costMicros: 5000, // 1/1,000,000 USD
inputTokens: 1000,
outputTokens: 500,
durationMs: 1500,
success: true,
externalRunId: 'your-id', // optional
metadata: { /* custom data */ }, // optional
});
// result.executionId: string
// result.budgetRemainingMicros: number
// result.usagePercent: number
// result.enforcementState: stringvalidateAssumptions(request)
Validate assumptions for a change.
const result = await client.validateAssumptions({
runId: 'run-123',
assumptions: [
{ id: 'a1', description: 'No breaking changes', impactLevel: 'high' },
],
});
// result.severity: 'low' | 'medium' | 'high' | 'critical'
// result.verificationRequired: boolean
// result.mergeEligible: boolean
// result.violations: Array<{ assumptionId, reason }>
// result.recommendations: string[]generateEvidenceBundle(request)
Generate a signed evidence bundle.
const evidence = await client.generateEvidenceBundle({
runId: 'run-123',
artifactTypes: ['confidence_report', 'trace'],
includeProvenance: true,
});
// evidence.bundleId: string
// evidence.bundleHash: string (sha256:...)
// evidence.storagePath: stringgetRiskScore()
Get risk metrics for the tenant.
const risk = await client.getRiskScore();
// risk.riskPercentile: number (0-100)
// risk.driftRisk: number
// risk.overrideFrequencyRisk: number
// risk.anomalyStatus: 'normal' | 'elevated' | 'critical'Webhook Verification
Verify incoming webhooks from Specora:
import { verifyWebhookSignature, parseWebhookPayload } from '@specora/sdk';
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyWebhookSignature(
req.body.toString(),
req.headers['x-specora-signature'],
req.headers['x-specora-timestamp'],
process.env.WEBHOOK_SECRET,
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = parseWebhookPayload(req.body.toString());
switch (payload.eventType) {
case 'platform.budget_breach':
// Handle budget breach
break;
case 'platform.invariant_failed':
// Handle policy violation
break;
// ... other event types
}
res.status(200).json({ received: true });
});Webhook Event Types
platform.invariant_failed- Governance invariant violationplatform.budget_breach- Budget limit exceededplatform.provider_quarantined- AI provider quarantinedplatform.drift_detected- Model or schema driftplatform.break_glass_used- Emergency override activatedplatform.sla_breach- SLA threshold violatedplatform.risk_score_changed- Significant risk change
Error Handling
The SDK throws typed errors for different failure scenarios:
import {
SpecoraClient,
FailClosedError,
BudgetExceededError,
AuthenticationError,
ValidationError,
} from '@specora/sdk';
try {
await client.checkPolicy({ ... });
} catch (error) {
if (error instanceof FailClosedError) {
// Network/server error in fail-closed mode
console.log('Defaulting to blocked:', error.decision);
} else if (error instanceof BudgetExceededError) {
// Budget exceeded
console.log('Budget exceeded:', error.usagePercent);
} else if (error instanceof AuthenticationError) {
// Invalid API key
} else if (error instanceof ValidationError) {
// Invalid request parameters
}
}Security
HTTPS Required
The SDK enforces HTTPS for all non-local baseUrl values. Plaintext HTTP
would expose your API key (sent as a Bearer token on every request) to
network observers.
// OK - production
new SpecoraClient({ baseUrl: 'https://api.specora.ai', ... });
// OK - local development
new SpecoraClient({ baseUrl: 'http://localhost:8765', ... });
new SpecoraClient({ baseUrl: 'http://127.0.0.1:8765', ... });
// THROWS ValidationError - plaintext HTTP to remote host
new SpecoraClient({ baseUrl: 'http://api.specora.ai', ... });Supply Chain
- Zero runtime dependencies — no transitive attack surface.
- No install-time scripts —
npm installexecutes no lifecycle hooks. - Scoped package — published under
@specora/to prevent typosquatting. - Fail-closed default — network/server errors block operations, not allow them.
Credential Handling
- API keys are only transmitted in the
Authorizationheader over HTTPS. - Keys are never logged, serialized to error messages, or persisted to disk.
JSON.stringify(client)andconsole.log(client)never expose the API key (toJSON(),toString(), and[inspect]()are overridden).- Webhook verification uses constant-time comparison (
timingSafeEqual) to prevent timing attacks.
Network Hardening
- No redirects —
fetch()is configured withredirect: 'manual'to prevent theAuthorizationheader from leaking to redirect targets. - Response size cap — responses over 10 MB are rejected to prevent OOM from a malicious or compromised server.
- Retry-After cap — server-provided
Retry-Aftervalues are capped at 120 seconds to prevent indefinite blocking.
Webhook Replay Prevention
Use WebhookEventCache to reject replayed webhooks within the 5-minute
timestamp window:
import { WebhookEventCache, handleWebhook } from '@specora/sdk';
const cache = new WebhookEventCache(); // in-memory LRU, 10k entries, 5-min TTL
app.post('/webhook', (req, res) => {
const event = handleWebhook(body, signature, timestamp, secret, cache);
if (!event) return res.status(401).end(); // invalid or replayed
// process event...
});Strict Governance Mode
By default, callers can use proxyExecute() without calling preAuthorize()
first. Enable strict mode to enforce governance:
const client = new SpecoraClient({
// ...
strictMode: true, // requires preAuthorize() before proxyExecute()
});Dev Mode Safety
SPECORA_DEV_MODE is automatically refused when NODE_ENV=production,
preventing accidental use of dev credentials in production.
Requirements
- Node.js 18+
- TypeScript 5.0+ (for TypeScript users)
License
MIT