JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 181
  • Score
    100M100P100Q87249F
  • License MIT

Specora Platform SDK for Node.js - AI governance and policy enforcement

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/sdk

Quick 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):

  1. Explicit routingHints.agentName — set in code for each SDK call
  2. SPECORA_WORKFLOW_ID environment variable — set once per execution context (CI/CD pipeline, n8n node, cron job, backend service)
  3. 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-pipeline

Sending 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:

  1. Purpose-specific assignment — admin assigned a model for this workflow + purpose
  2. Default workflow assignment — admin assigned a default model for this workflow
  3. Org model preferences — org-wide preferences by workflow type
  4. 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

  1. When the SDK sends routingHints.agentName (or SPECORA_WORKFLOW_ID is set), Specora auto-discovers the workflow and tracks all observed purposes.
  2. Org admins see discovered workflows in the Vendor Workflows dashboard page.
  3. Admins assign models — either a default for the whole workflow, or purpose-specific models for different LLM calls within the workflow.
  4. 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:11434

Supported 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 instead

Ollama 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: string

validateAssumptions(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: string

getRiskScore()

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 violation
  • platform.budget_breach - Budget limit exceeded
  • platform.provider_quarantined - AI provider quarantined
  • platform.drift_detected - Model or schema drift
  • platform.break_glass_used - Emergency override activated
  • platform.sla_breach - SLA threshold violated
  • platform.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 scriptsnpm install executes 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 Authorization header over HTTPS.
  • Keys are never logged, serialized to error messages, or persisted to disk.
  • JSON.stringify(client) and console.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 redirectsfetch() is configured with redirect: 'manual' to prevent the Authorization header 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-After values 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