npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@governancehq/sdk

v0.2.6

Published

TypeScript SDK for GaaS (Governance as a Service)

Downloads

167

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

Quick 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_MODIFIED

Authentication

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;  // RiskAssessment

Decision & 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 approved
  • decision.blocked - Intent was blocked
  • decision.escalated - Intent was escalated for human review

Escalation Lifecycle

  • escalation.decided - Escalation was reviewed and decided
  • escalation.timed_out - Escalation timed out without decision
  • escalation.cancelled - Escalation was cancelled
  • escalation.reassigned - Escalation was reassigned to another reviewer

Learning & Patterns

  • observation.recorded - New learning observation recorded
  • policy.calibrated - Policy weights/thresholds were calibrated
  • pattern.detected - Behavioral pattern detected

System Events

  • quota.exceeded - Monthly quota limit exceeded
  • rate_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

  1. Always validate signatures - Reject webhooks with invalid signatures
  2. Use constant-time comparison - Prevents timing attacks (use timingSafeEqual)
  3. Store secrets securely - Use environment variables or secret managers
  4. Validate event structure - Check required fields before processing
  5. Idempotency - Use X-GaaS-Delivery header to deduplicate events
  6. Return 200 quickly - Process events asynchronously if they're slow
  7. 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 validation

Exported 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 test

License

Apache-2.0