@governancehq/sdk
v0.2.6
Published
TypeScript SDK for GaaS (Governance as a Service)
Downloads
167
Maintainers
Readme
GaaS TypeScript SDK
TypeScript client library for the GaaS governance API. Zero dependencies — uses the native fetch API with full type safety.
Installation
npm install @governancehq/sdkQuick Start
import { GaaSClient, buildIntent, ActionType, TargetType } from "@governancehq/sdk";
const client = new GaaSClient({ baseUrl: "https://api.gaas.is" });
const intent = buildIntent({
agentId: "my-agent",
actionType: ActionType.Communicate,
verb: "send",
targetType: TargetType.ApiEndpoint,
targetIdentifier: "email-service",
summary: "Send welcome email to new user",
content: { to: "[email protected]" },
});
const response = await client.submitIntent(intent);
console.log(response.data.verdict); // APPROVE, BLOCK, ESCALATE, APPROVE_MODIFIEDAuthentication
Pass an API key via headers:
const client = new GaaSClient({
baseUrl: "https://api.gaas.is",
headers: { "X-API-Key": "your-api-key" },
});Client Configuration
interface GaaSClientOptions {
baseUrl?: string; // Default: "https://api.gaas.is"
apiVersion?: string; // Default: "v1"
timeout?: number; // Default: 30000 (ms)
headers?: Record<string, string>;
fetch?: typeof globalThis.fetch; // Custom fetch implementation
}The fetch option lets you inject a custom fetch implementation for testing or environments without a global fetch.
API Methods
Intent Submission
// Submit an intent for governance evaluation
const response = await client.submitIntent(intent);
// Access the decision
response.data.verdict; // Verdict enum value
response.data.reasoning; // DecisionReasoning
response.data.modifications; // Modification[] (if APPROVE_MODIFIED)
response.data.riskAssessment; // RiskAssessmentDecision & Audit Retrieval
// Get a specific decision
const decision = await client.getDecision("intent-id");
// Get the full audit record
const audit = await client.getAudit("intent-id");Health Check
const health = await client.healthCheck();
console.log(health.data.status); // "healthy"
console.log(health.data.version); // "0.2.0"Response Object
All methods return a GaaSResponse<T> with:
interface GaaSResponse<T> {
data: T; // The typed response payload
meta: ResponseMeta; // request_id, decision_id, latency, status_code
}Building Intents
The buildIntent helper flattens nested types into a flat options object:
import { buildIntent, ActionType, TargetType, Sensitivity, Urgency } from "@governancehq/sdk";
const intent = buildIntent({
// Agent (required)
agentId: "agent-001",
agentFramework: "custom",
agentName: "My Agent",
// Action (required)
actionType: ActionType.Execute,
verb: "deploy",
// Target (required)
targetType: TargetType.Infrastructure,
targetIdentifier: "prod-cluster",
targetSensitivity: Sensitivity.Regulated,
targetJurisdiction: "US",
// Payload (required)
summary: "Deploy model v2.1 to production cluster",
content: { model: "v2.1", cluster: "prod-us-east" },
// Impact (optional)
reversible: true,
financialExposureUsd: 5000,
audienceSize: 10000,
regulatoryDomains: ["SOC2"],
// Governance (optional)
urgency: Urgency.High,
maxLatencyMs: 1000,
requireDeliberation: true,
});For full control, construct the IntentDeclaration object directly:
import type { IntentDeclaration } from "@governancehq/sdk";
const intent: IntentDeclaration = {
idempotencyKey: null,
agent: { id: "agent-001", name: null, framework: "custom" },
action: {
type: "EXECUTE",
verb: "deploy",
target: { type: "INFRASTRUCTURE", identifier: "prod-cluster", sensitivity: "PUBLIC" },
},
payload: { summary: "Deploy model v2.1", content: { model: "v2.1" } },
estimatedImpact: { reversible: true, financialExposureUsd: 5000, audienceSize: 10000 },
};Field Filtering
Request only specific fields to reduce response size:
const response = await client.submitIntent(intent, {
fields: "verdict,reasoning.summary,riskAssessment.overallScore",
});Webhook Integration
GaaS sends webhook events for decision lifecycle, escalations, learning observations, and system events. All webhooks are signed with HMAC-SHA256 for security.
Webhook Signature Validation
IMPORTANT: Always validate webhook signatures before processing events to prevent spoofing attacks.
import { createHmac, timingSafeEqual } from "crypto";
/**
* Verify HMAC-SHA256 webhook signature using constant-time comparison.
*
* @param payload - Raw request body string (not parsed JSON)
* @param signature - Value from X-GaaS-Signature header (format: "sha256=<hex>")
* @param secret - Webhook secret from registration (starts with "whsec_")
* @returns True if signature is valid, false otherwise
*/
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
const expectedSignature = `sha256=${expected}`;
// Use constant-time comparison to prevent timing attacks
try {
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch {
// Length mismatch causes timingSafeEqual to throw
return false;
}
}Webhook Event Handling
import express, { Request, Response } from "express";
const app = express();
// Store your webhook secret securely (from webhook registration response)
const WEBHOOK_SECRET = process.env.GAAS_WEBHOOK_SECRET || "whsec_abc123...";
interface WebhookEvent {
event_type: string;
timestamp: string;
webhook_id: string;
organization_id: string;
data: Record<string, any>;
// Event-specific optional fields
decision_id?: string;
intent_id?: string;
escalation_id?: string;
escalation_status?: string;
observation_id?: string;
policy_id?: string;
pattern_id?: string;
}
app.post("/webhooks/gaas", express.raw({ type: "application/json" }), (req: Request, res: Response) => {
// Get raw request body before parsing
const payloadString = req.body.toString("utf8");
const signature = req.headers["x-gaas-signature"] as string;
const eventType = req.headers["x-gaas-event"] as string;
const deliveryId = req.headers["x-gaas-delivery"] as string;
// Validate signature before processing
if (!verifyWebhookSignature(payloadString, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
// Parse event payload
const event: WebhookEvent = JSON.parse(payloadString);
// Route to appropriate handler based on event type
const handlers: Record<string, (event: WebhookEvent) => void> = {
"decision.approved": handleDecisionApproved,
"decision.blocked": handleDecisionBlocked,
"decision.escalated": handleDecisionEscalated,
"escalation.decided": handleEscalationDecided,
"escalation.timed_out": handleEscalationTimedOut,
"escalation.cancelled": handleEscalationCancelled,
"escalation.reassigned": handleEscalationReassigned,
"observation.recorded": handleObservationRecorded,
"quota.exceeded": handleQuotaExceeded,
"rate_limit.exceeded": handleRateLimitExceeded,
};
const handler = handlers[eventType];
if (handler) {
handler(event);
} else {
console.warn(`Unknown event type: ${eventType}`);
}
// Return 200 to acknowledge receipt
res.status(200).json({ status: "received" });
});
function handleDecisionApproved(event: WebhookEvent): void {
const { decision_id, intent_id, data } = event;
console.log(`Decision ${decision_id} approved for intent ${intent_id}`);
// Update your database, notify agent, etc.
}
function handleDecisionBlocked(event: WebhookEvent): void {
const { decision_id, intent_id, data } = event;
console.log(`Decision ${decision_id} blocked for intent ${intent_id}`);
// Alert agent owner, log for audit, etc.
}
function handleEscalationDecided(event: WebhookEvent): void {
const { escalation_id, escalation_status, data } = event;
console.log(`Escalation ${escalation_id} decided with status: ${escalation_status}`);
// Notify agent, update workflow, etc.
}
function handleQuotaExceeded(event: WebhookEvent): void {
const { organization_id, data } = event;
console.log(`Quota exceeded for organization ${organization_id}`);
// Send upgrade notification, throttle agent, etc.
}
app.listen(3000);Event Types
GaaS sends webhooks for these event types:
Decision Lifecycle
decision.approved- Intent was approveddecision.blocked- Intent was blockeddecision.escalated- Intent was escalated for human review
Escalation Lifecycle
escalation.decided- Escalation was reviewed and decidedescalation.timed_out- Escalation timed out without decisionescalation.cancelled- Escalation was cancelledescalation.reassigned- Escalation was reassigned to another reviewer
Learning & Patterns
observation.recorded- New learning observation recordedpolicy.calibrated- Policy weights/thresholds were calibratedpattern.detected- Behavioral pattern detected
System Events
quota.exceeded- Monthly quota limit exceededrate_limit.exceeded- Rate limit threshold exceeded
Webhook Payload Structure
All webhook payloads follow this structure:
{
event_type: "decision.approved",
timestamp: "2026-02-14T12:00:00Z",
webhook_id: "wh_abc123",
organization_id: "org_default",
data: {
// Event-specific data (full decision, escalation, etc.)
},
// Event-specific fields (optional):
decision_id?: "dec_xyz789",
intent_id?: "int_123abc",
escalation_id?: "esc_def456",
observation_id?: "obs_ghi789",
}Security Best Practices
- Always validate signatures - Reject webhooks with invalid signatures
- Use constant-time comparison - Prevents timing attacks (use
timingSafeEqual) - Store secrets securely - Use environment variables or secret managers
- Validate event structure - Check required fields before processing
- Idempotency - Use
X-GaaS-Deliveryheader to deduplicate events - Return 200 quickly - Process events asynchronously if they're slow
- Handle retries gracefully - GaaS retries failed deliveries (3 attempts max)
Next.js Example (App Router)
// app/api/webhooks/gaas/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createHmac, timingSafeEqual } from "crypto";
const WEBHOOK_SECRET = process.env.GAAS_WEBHOOK_SECRET!;
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
const expected = createHmac("sha256", secret).update(payload, "utf8").digest("hex");
const expectedSignature = `sha256=${expected}`;
try {
return timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
} catch {
return false;
}
}
export async function POST(request: NextRequest) {
const payloadString = await request.text();
const signature = request.headers.get("x-gaas-signature");
if (!signature || !verifyWebhookSignature(payloadString, signature, WEBHOOK_SECRET)) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const event = JSON.parse(payloadString);
// Process event...
return NextResponse.json({ status: "received" });
}Web Crypto API (Edge/Browser)
For edge runtimes without Node.js crypto module:
async function verifyWebhookSignatureWebCrypto(
payload: string,
signature: string,
secret: string
): Promise<boolean> {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signatureBytes = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
const expected = `sha256=${Array.from(new Uint8Array(signatureBytes))
.map(b => b.toString(16).padStart(2, "0"))
.join("")}`;
// Constant-time comparison
if (signature.length !== expected.length) return false;
let result = 0;
for (let i = 0; i < signature.length; i++) {
result |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
}
return result === 0;
}Error Handling
import {
GaaSError, // Base error class
GaaSValidationError, // 422 — invalid request structure
GaaSSemanticError, // 422 — semantic validation failures
GaaSAuthenticationError, // 401 — invalid API key
GaaSQuotaExceededError, // 402 — monthly quota exceeded
GaaSNotFoundError, // 404 — resource not found
GaaSConflictError, // 409 — idempotency conflict
GaaSRateLimitError, // 429 — rate limit exceeded
GaaSServerError, // 500 — server error
GaaSConnectionError, // Network/connection failure
} from "@governancehq/sdk";
try {
const response = await client.submitIntent(intent);
} catch (error) {
if (error instanceof GaaSAuthenticationError) {
console.error("Invalid API key");
} else if (error instanceof GaaSQuotaExceededError) {
console.error(`Quota exceeded. Upgrade plan or wait until: ${error.details.resetTime}`);
} else if (error instanceof GaaSRateLimitError) {
// Exponential backoff recommended
const retryAfter = error.details?.retryAfterSeconds ?? 60;
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
} else if (error instanceof GaaSValidationError) {
console.error(`Validation failed: ${error.message}`);
console.error(`Details:`, error.details);
} else if (error instanceof GaaSConnectionError) {
console.error("Could not reach GaaS server");
}
}Rate Limit Handling Best Practices
async function submitWithRetry(
client: GaaSClient,
intent: IntentDeclaration,
maxRetries = 3
): Promise<GaaSResponse<GovernanceDecision>> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.submitIntent(intent);
} catch (error) {
if (error instanceof GaaSRateLimitError && attempt < maxRetries - 1) {
const retryAfter = error.details?.retryAfterSeconds ?? 60;
const waitTime = Math.min(retryAfter * Math.pow(2, attempt), 300); // Cap at 5 minutes
await new Promise((resolve) => setTimeout(resolve, waitTime * 1000));
} else {
throw error;
}
}
}
throw new Error("Max retries exceeded");
}Automatic Case Conversion
The SDK automatically converts between JavaScript camelCase and the Python snake_case API:
- Outbound:
buildIntent({ agentId, actionType })sends{ agent_id, action_type }over the wire - Inbound:
{ risk_assessment, pipeline_mode }arrives as{ riskAssessment, pipelineMode }in TypeScript
No manual conversion needed.
Important Note on Enum Values
While field names are automatically converted, enum values must be used in their API format (SCREAMING_SNAKE_CASE):
// Correct - enum values use TypeScript enums or strings
actionType: ActionType.Communicate // SDK constant
actionType: "COMMUNICATE" // String literal (API format)
// Incorrect - camelCase enum values won't work
actionType: "communicate" // ❌ Will fail validationExported Types
Interfaces: IntentDeclaration, GovernanceDecision, AuditRecord, AgentInfo, Action, ActionTarget, ActionPayload, EstimatedImpact, GovernanceRequest, ContextProvided, RiskAssessment, Modification, BlockDetail, EscalationDetail, DecisionReasoning
Enums: ActionType, TargetType, Sensitivity, AgentFramework, TrustTier, Urgency, FallbackOnTimeout, DataCategory, Verdict, ModificationType, RiskClassification, PolicyVerdict, PolicySeverity, DecisionPath, ExecutionStatus, PipelineMode
Requirements
- Node.js 18+ (or any environment with global
fetch) - TypeScript 5.5+ (for development)
Development
npm install
npm run build
npm testLicense
Apache-2.0
