@vettly/shared
v0.1.16
Published
Shared TypeScript types for Vettly decision infrastructure
Maintainers
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/sharedDecision 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
- Sign up at vettly.dev
- Go to Dashboard > API Keys
- Create and copy your key
Links
- vettly.dev - Sign up
- docs.vettly.dev - Documentation
- @vettly/sdk - Server-side SDK
- @vettly/react - React components