@vettly/shared

v0.1.16

Published

Shared TypeScript types for Vettly decision infrastructure

Readme

@vettly/shared

Type-safe contracts for UGC moderation. Runtime-validated schemas that ensure consistency across all Vettly packages.

UGC Moderation Essentials

Apps with user-generated content need four things to stay compliant and keep users safe. This package provides the canonical types and schemas that enforce each requirement across all Vettly packages:

| Requirement | Shared Types | |-------------|--------------| | Content filtering | CheckRequest, CheckResponse, Action | | User reporting | WebhookEventType, Decision | | User blocking | ModerationContext, Action | | Audit trail | Decision, ContentType, Policy |

All packages (@vettly/sdk, @vettly/react, @vettly/express, @vettly/nextjs) depend on these shared contracts. See @vettly/sdk for the full integration picture.

Why Type-Safe Contracts Matter

Content moderation decisions have legal consequences. When a user appeals a blocking decision, you need to prove:

  • The exact policy version that was applied
  • The category thresholds that triggered the action
  • The content fingerprint that proves content wasn't altered

This package provides the canonical type definitions and Zod schemas that power the entire Vettly ecosystem. All packages (@vettly/sdk, @vettly/react) depend on these shared contracts.

Installation

npm install @vettly/shared

Decision Actions

Vettly uses graduated actions rather than binary allow/block:

import type { Action } from '@vettly/shared'
import { ActionSchema } from '@vettly/shared'

// 'allow' | 'warn' | 'flag' | 'block'
const action: Action = 'flag'

// Validate at runtime
ActionSchema.parse('flag')  // ✓
ActionSchema.parse('invalid')  // throws ZodError

| Action | Meaning | Typical Use | |--------|---------|-------------| | allow | Content passes all policy checks | Publish immediately | | warn | Minor concern, user should be notified | Show warning, allow post | | flag | Needs human review before action | Queue for moderator | | block | Violates policy, cannot be published | Reject submission |


Content Types

import type { ContentType, UseCaseType } from '@vettly/shared'

// ContentType: 'text' | 'image' | 'video'
const type: ContentType = 'image'

// UseCaseType provides context for smarter moderation
// 'social_post' | 'comment' | 'profile' | 'message' | 'review' | 'listing' | 'bio' | 'other'
const context: UseCaseType = 'profile'

Categories

Standard moderation categories with consistent naming:

import type { Category } from '@vettly/shared'
import { CategorySchema } from '@vettly/shared'

// All categories
const categories: Category[] = [
  'hate_speech',   // Attacks based on protected characteristics
  'harassment',    // Targeted abuse or bullying
  'violence',      // Graphic violence or threats
  'self_harm',     // Self-harm or suicide content
  'sexual',        // Adult or explicit content
  'spam',          // Commercial spam or manipulation
  'profanity',     // Strong language
  'scam',          // Fraud or deceptive content
  'illegal',       // Illegal activities
]

Policy Schema

Policies define how content is evaluated:

import type { Policy, Rule, Override, FallbackConfig } from '@vettly/shared'
import { PolicySchema } from '@vettly/shared'

const policy: Policy = {
  name: 'Community Guidelines',
  version: '2024-01-15-abc123',  // Immutable version identifier
  rules: [
    {
      category: 'hate_speech',
      threshold: 0.7,           // Score 0-1 that triggers action
      provider: 'openai',
      action: 'block',
      priority: 1,              // Higher priority rules evaluated first
    },
    {
      category: 'profanity',
      threshold: 0.5,
      provider: 'openai',
      action: 'warn',
      priority: 0,
    }
  ],
  fallback: {
    provider: 'mock',           // Fallback if primary provider fails
    on_timeout: true,
    timeout_ms: 5000,
  }
}

// Validate policy at runtime
PolicySchema.parse(policy)

Custom Prompt Rules (Pro+)

For semantic image analysis with custom prompts:

const customRule: Rule = {
  category: 'sexual',
  threshold: 0.5,
  provider: 'gemini_vision',
  action: 'flag',
  customPrompt: 'Does this image contain nudity or sexually suggestive content?',
  customCategory: 'nudity_detection',
}

Decision Records

Every moderation decision is recorded with full audit trail:

import type { Decision } from '@vettly/shared'

const decision: Decision = {
  id: '550e8400-e29b-41d4-a716-446655440000',
  content: 'The original content text',
  contentHash: 'sha256:abc123...',        // Tamper-evident fingerprint
  contentType: 'text',
  policy: {
    id: 'community-guidelines',
    version: '2024-01-15-abc123',         // Exact policy version applied
  },
  result: {
    safe: false,
    flagged: true,
    action: 'block',
    categories: [
      {
        category: 'hate_speech',
        score: 0.91,
        threshold: 0.7,                    // The threshold that was configured
        triggered: true,
      },
      {
        category: 'harassment',
        score: 0.08,
        threshold: 0.8,
        triggered: false,
      }
    ],
  },
  provider: {
    name: 'openai',
    latency: 147,
    cost: 0.000025,
  },
  metadata: {
    userId: 'user_123',
    sessionId: 'session_456',
  },
  timestamp: '2024-01-15T12:00:00.000Z',
  requestId: 'req_unique_123',             // For idempotency
}

Request/Response Types

Single Content Check

import type { CheckRequest, CheckResponse } from '@vettly/shared'
import { CheckRequestSchema, CheckResponseSchema } from '@vettly/shared'

const request: CheckRequest = {
  content: 'User-generated text',
  policyId: 'community-safe',
  contentType: 'text',
  language: 'en',                          // ISO 639-1 code
  metadata: { userId: 'user_123' },
  requestId: 'req_unique_for_idempotency',
}

// Validate incoming requests
const validated = CheckRequestSchema.parse(untrustedInput)

const response: CheckResponse = {
  decisionId: '550e8400-e29b-41d4-a716-446655440000',
  safe: true,
  flagged: false,
  action: 'allow',
  categories: [
    { category: 'hate_speech', score: 0.02, triggered: false }
  ],
  provider: 'openai',
  latency: 123,
  cost: 0.000015,
  requestId: 'req_unique_for_idempotency',
}

Multi-Modal Check

For checking text, images, and video together:

import type { MultiModalCheckRequest, MultiModalCheckResponse, ContentItemResult } from '@vettly/shared'

const request: MultiModalCheckRequest = {
  text: 'Post caption text',
  images: [
    'https://cdn.example.com/image1.jpg',
    'data:image/png;base64,...',
  ],
  video: 'https://cdn.example.com/video.mp4',
  context: {
    useCase: 'social_post',
    userId: 'user_123',
    userReputation: 0.95,                  // 0-1 trust score
    locale: 'en-US',
    region: 'US',
    language: 'en',
  },
  policyId: 'social-media',
  metadata: { postId: 'post_456' },
}

const response: MultiModalCheckResponse = {
  decisionId: '...',
  safe: false,                              // Overall: false if ANY item unsafe
  flagged: true,                            // Overall: true if ANY item flagged
  action: 'block',                          // Overall: most severe action
  results: [
    {
      contentType: 'text',
      safe: true,
      flagged: false,
      action: 'allow',
      categories: [...],
      provider: 'openai',
      latency: 50,
      cost: 0.00001,
    },
    {
      contentType: 'image',
      contentRef: 'https://cdn.example.com/image1.jpg',
      contentItemId: 'item-uuid',           // For linking to evidence
      safe: false,
      flagged: true,
      action: 'block',
      categories: [...],
      provider: 'hive',
      latency: 200,
      cost: 0.0001,
      evidence: {
        url: 'https://evidence.vettly.dev/...',  // Signed URL
        expiresAt: '2024-01-16T12:00:00Z',
      },
    }
  ],
  totalLatency: 250,
  totalCost: 0.00011,
}

Providers

Supported moderation providers:

import type { ProviderName, ProviderResult } from '@vettly/shared'

// 'openai' | 'openai_gpt' | 'hive' | 'azure' | 'gemini_vision' | 'mock' | 'fallback'
const provider: ProviderName = 'openai'

const result: ProviderResult = {
  provider: 'openai',
  flagged: true,
  categories: {
    hate_speech: 0.91,
    harassment: 0.08,
  },
  confidence: 0.95,
  latency: 147,
  cost: 0.000025,
  raw: { /* original provider response */ },
}

Webhook Events

import type { WebhookEventType, WebhookEndpoint } from '@vettly/shared'

// Event types
const events: WebhookEventType[] = [
  'decision.created',    // Any decision made
  'decision.flagged',    // Content flagged for review
  'decision.blocked',    // Content blocked
  'policy.created',      // New policy created
  'policy.updated',      // Policy updated
]

const webhook: WebhookEndpoint = {
  url: 'https://your-app.com/webhooks/vettly',
  events: ['decision.blocked', 'decision.flagged'],
  description: 'Production webhook for content moderation',
}

Error Types

import {
  ModerationError,
  PolicyValidationError,
  ProviderError
} from '@vettly/shared'

try {
  await checkContent(...)
} catch (error) {
  if (error instanceof PolicyValidationError) {
    // Invalid policy YAML or configuration
    console.log('Policy error:', error.message)
    console.log('Details:', error.details)
  } else if (error instanceof ProviderError) {
    // Moderation provider failed
    console.log('Provider error:', error.message)
    console.log('Provider:', error.details?.provider)
  } else if (error instanceof ModerationError) {
    // General moderation error
    console.log(`${error.code}: ${error.message}`)
    console.log('Status:', error.statusCode)
  }
}

Utility Functions

import {
  hashContent,
  generateUUID,
  generateRequestId,
  calculatePolicyVersion,
  formatCost,
  formatLatency
} from '@vettly/shared'

// SHA256 content hashing for tamper-evident fingerprinting
const hash = hashContent('user content')
// '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'

// Generate UUIDs for decisions
const id = generateUUID()
// '550e8400-e29b-41d4-a716-446655440000'

// Generate idempotency keys
const requestId = generateRequestId()
// 'req_1705320000000_x7k9m2'

// Calculate policy version from YAML
const version = calculatePolicyVersion(yamlContent)
// 'abc123def456...' (16-char hash)

// Format for display
formatCost(0.000123)   // '$0.000123'
formatLatency(1250)    // '1.25s'
formatLatency(150)     // '150ms'

Runtime Validation

All types have corresponding Zod schemas for runtime validation:

import {
  ActionSchema,
  CategorySchema,
  ContentTypeSchema,
  PolicySchema,
  CheckRequestSchema,
  CheckResponseSchema,
  DecisionSchema,
  MultiModalCheckRequestSchema,
  MultiModalCheckResponseSchema,
} from '@vettly/shared'

// Validate untrusted input
const result = CheckRequestSchema.safeParse(untrustedInput)

if (result.success) {
  // result.data is fully typed as CheckRequest
  processRequest(result.data)
} else {
  // result.error contains validation errors
  console.log('Validation failed:', result.error.issues)
}

// Strict parsing (throws on failure)
try {
  const request = CheckRequestSchema.parse(untrustedInput)
} catch (error) {
  if (error instanceof z.ZodError) {
    console.log('Validation errors:', error.issues)
  }
}

JSON Types

For database storage and serialization:

import type { JsonValue, JsonObject, JsonArray, JsonPrimitive } from '@vettly/shared'

// JSON-serializable metadata
const metadata: JsonObject = {
  userId: 'user_123',
  tags: ['user-reported', 'priority'],
  score: 0.95,
  reviewed: false,
}

Moderation Context

Additional context for smarter moderation:

import type { ModerationContext } from '@vettly/shared'

const context: ModerationContext = {
  userId: 'user_123',
  sessionId: 'session_456',
  locale: 'en-US',
  region: 'US',
  metadata: {
    deviceType: 'mobile',
    appVersion: '2.1.0',
  },
}

Get Your API Key

  1. Sign up at vettly.dev
  2. Go to Dashboard > API Keys
  3. Create and copy your key

Links