@vettly/shared 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +402 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# @vettly/shared
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe contracts for compliance-critical applications. Runtime-validated schemas that ensure consistency across all Vettly packages.
|
|
4
|
+
|
|
5
|
+
## Why Type-Safe Contracts Matter
|
|
6
|
+
|
|
7
|
+
Content moderation decisions have legal consequences. When a user appeals a blocking decision, you need to prove:
|
|
8
|
+
|
|
9
|
+
- The **exact policy version** that was applied
|
|
10
|
+
- The **category thresholds** that triggered the action
|
|
11
|
+
- The **content fingerprint** that proves content wasn't altered
|
|
12
|
+
|
|
13
|
+
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.
|
|
4
14
|
|
|
5
15
|
## Installation
|
|
6
16
|
|
|
@@ -8,77 +18,328 @@ Foundational types, schemas, and utilities for Vettly decision infrastructure. R
|
|
|
8
18
|
npm install @vettly/shared
|
|
9
19
|
```
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
---
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
## Decision Actions
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
### Decision Actions
|
|
25
|
+
Vettly uses **graduated actions** rather than binary allow/block:
|
|
18
26
|
|
|
19
27
|
```typescript
|
|
20
28
|
import type { Action } from '@vettly/shared'
|
|
29
|
+
import { ActionSchema } from '@vettly/shared'
|
|
30
|
+
|
|
31
|
+
// 'allow' | 'warn' | 'flag' | 'block'
|
|
32
|
+
const action: Action = 'flag'
|
|
21
33
|
|
|
22
|
-
//
|
|
34
|
+
// Validate at runtime
|
|
35
|
+
ActionSchema.parse('flag') // ✓
|
|
36
|
+
ActionSchema.parse('invalid') // throws ZodError
|
|
23
37
|
```
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
| Action | Meaning | Typical Use |
|
|
40
|
+
|--------|---------|-------------|
|
|
41
|
+
| `allow` | Content passes all policy checks | Publish immediately |
|
|
42
|
+
| `warn` | Minor concern, user should be notified | Show warning, allow post |
|
|
43
|
+
| `flag` | Needs human review before action | Queue for moderator |
|
|
44
|
+
| `block` | Violates policy, cannot be published | Reject submission |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Content Types
|
|
26
49
|
|
|
27
50
|
```typescript
|
|
28
51
|
import type { ContentType, UseCaseType } from '@vettly/shared'
|
|
29
52
|
|
|
30
53
|
// ContentType: 'text' | 'image' | 'video'
|
|
31
|
-
|
|
54
|
+
const type: ContentType = 'image'
|
|
55
|
+
|
|
56
|
+
// UseCaseType provides context for smarter moderation
|
|
57
|
+
// 'social_post' | 'comment' | 'profile' | 'message' | 'review' | 'listing' | 'bio' | 'other'
|
|
58
|
+
const context: UseCaseType = 'profile'
|
|
32
59
|
```
|
|
33
60
|
|
|
34
|
-
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Categories
|
|
64
|
+
|
|
65
|
+
Standard moderation categories with consistent naming:
|
|
35
66
|
|
|
36
67
|
```typescript
|
|
37
68
|
import type { Category } from '@vettly/shared'
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
import { CategorySchema } from '@vettly/shared'
|
|
70
|
+
|
|
71
|
+
// All categories
|
|
72
|
+
const categories: Category[] = [
|
|
73
|
+
'hate_speech', // Attacks based on protected characteristics
|
|
74
|
+
'harassment', // Targeted abuse or bullying
|
|
75
|
+
'violence', // Graphic violence or threats
|
|
76
|
+
'self_harm', // Self-harm or suicide content
|
|
77
|
+
'sexual', // Adult or explicit content
|
|
78
|
+
'spam', // Commercial spam or manipulation
|
|
79
|
+
'profanity', // Strong language
|
|
80
|
+
'scam', // Fraud or deceptive content
|
|
81
|
+
'illegal', // Illegal activities
|
|
82
|
+
]
|
|
40
83
|
```
|
|
41
84
|
|
|
42
|
-
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Policy Schema
|
|
88
|
+
|
|
89
|
+
Policies define how content is evaluated:
|
|
43
90
|
|
|
44
91
|
```typescript
|
|
45
92
|
import type { Policy, Rule, Override, FallbackConfig } from '@vettly/shared'
|
|
93
|
+
import { PolicySchema } from '@vettly/shared'
|
|
94
|
+
|
|
95
|
+
const policy: Policy = {
|
|
96
|
+
name: 'Community Guidelines',
|
|
97
|
+
version: '2024-01-15-abc123', // Immutable version identifier
|
|
98
|
+
rules: [
|
|
99
|
+
{
|
|
100
|
+
category: 'hate_speech',
|
|
101
|
+
threshold: 0.7, // Score 0-1 that triggers action
|
|
102
|
+
provider: 'openai',
|
|
103
|
+
action: 'block',
|
|
104
|
+
priority: 1, // Higher priority rules evaluated first
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
category: 'profanity',
|
|
108
|
+
threshold: 0.5,
|
|
109
|
+
provider: 'openai',
|
|
110
|
+
action: 'warn',
|
|
111
|
+
priority: 0,
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
fallback: {
|
|
115
|
+
provider: 'mock', // Fallback if primary provider fails
|
|
116
|
+
on_timeout: true,
|
|
117
|
+
timeout_ms: 5000,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Validate policy at runtime
|
|
122
|
+
PolicySchema.parse(policy)
|
|
46
123
|
```
|
|
47
124
|
|
|
48
|
-
###
|
|
125
|
+
### Custom Prompt Rules (Pro+)
|
|
126
|
+
|
|
127
|
+
For semantic image analysis with custom prompts:
|
|
49
128
|
|
|
50
129
|
```typescript
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
130
|
+
const customRule: Rule = {
|
|
131
|
+
category: 'sexual',
|
|
132
|
+
threshold: 0.5,
|
|
133
|
+
provider: 'gemini_vision',
|
|
134
|
+
action: 'flag',
|
|
135
|
+
customPrompt: 'Does this image contain nudity or sexually suggestive content?',
|
|
136
|
+
customCategory: 'nudity_detection',
|
|
137
|
+
}
|
|
58
138
|
```
|
|
59
139
|
|
|
60
|
-
|
|
140
|
+
---
|
|
61
141
|
|
|
62
|
-
|
|
142
|
+
## Decision Records
|
|
143
|
+
|
|
144
|
+
Every moderation decision is recorded with full audit trail:
|
|
63
145
|
|
|
64
146
|
```typescript
|
|
65
|
-
import {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
147
|
+
import type { Decision } from '@vettly/shared'
|
|
148
|
+
|
|
149
|
+
const decision: Decision = {
|
|
150
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
151
|
+
content: 'The original content text',
|
|
152
|
+
contentHash: 'sha256:abc123...', // Tamper-evident fingerprint
|
|
153
|
+
contentType: 'text',
|
|
154
|
+
policy: {
|
|
155
|
+
id: 'community-guidelines',
|
|
156
|
+
version: '2024-01-15-abc123', // Exact policy version applied
|
|
157
|
+
},
|
|
158
|
+
result: {
|
|
159
|
+
safe: false,
|
|
160
|
+
flagged: true,
|
|
161
|
+
action: 'block',
|
|
162
|
+
categories: [
|
|
163
|
+
{
|
|
164
|
+
category: 'hate_speech',
|
|
165
|
+
score: 0.91,
|
|
166
|
+
threshold: 0.7, // The threshold that was configured
|
|
167
|
+
triggered: true,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
category: 'harassment',
|
|
171
|
+
score: 0.08,
|
|
172
|
+
threshold: 0.8,
|
|
173
|
+
triggered: false,
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
provider: {
|
|
178
|
+
name: 'openai',
|
|
179
|
+
latency: 147,
|
|
180
|
+
cost: 0.000025,
|
|
181
|
+
},
|
|
182
|
+
metadata: {
|
|
183
|
+
userId: 'user_123',
|
|
184
|
+
sessionId: 'session_456',
|
|
185
|
+
},
|
|
186
|
+
timestamp: '2024-01-15T12:00:00.000Z',
|
|
187
|
+
requestId: 'req_unique_123', // For idempotency
|
|
188
|
+
}
|
|
189
|
+
```
|
|
74
190
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Request/Response Types
|
|
194
|
+
|
|
195
|
+
### Single Content Check
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import type { CheckRequest, CheckResponse } from '@vettly/shared'
|
|
199
|
+
import { CheckRequestSchema, CheckResponseSchema } from '@vettly/shared'
|
|
200
|
+
|
|
201
|
+
const request: CheckRequest = {
|
|
202
|
+
content: 'User-generated text',
|
|
203
|
+
policyId: 'community-safe',
|
|
204
|
+
contentType: 'text',
|
|
205
|
+
language: 'en', // ISO 639-1 code
|
|
206
|
+
metadata: { userId: 'user_123' },
|
|
207
|
+
requestId: 'req_unique_for_idempotency',
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Validate incoming requests
|
|
211
|
+
const validated = CheckRequestSchema.parse(untrustedInput)
|
|
212
|
+
|
|
213
|
+
const response: CheckResponse = {
|
|
214
|
+
decisionId: '550e8400-e29b-41d4-a716-446655440000',
|
|
215
|
+
safe: true,
|
|
216
|
+
flagged: false,
|
|
217
|
+
action: 'allow',
|
|
218
|
+
categories: [
|
|
219
|
+
{ category: 'hate_speech', score: 0.02, triggered: false }
|
|
220
|
+
],
|
|
221
|
+
provider: 'openai',
|
|
222
|
+
latency: 123,
|
|
223
|
+
cost: 0.000015,
|
|
224
|
+
requestId: 'req_unique_for_idempotency',
|
|
79
225
|
}
|
|
80
226
|
```
|
|
81
227
|
|
|
228
|
+
### Multi-Modal Check
|
|
229
|
+
|
|
230
|
+
For checking text, images, and video together:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import type { MultiModalCheckRequest, MultiModalCheckResponse, ContentItemResult } from '@vettly/shared'
|
|
234
|
+
|
|
235
|
+
const request: MultiModalCheckRequest = {
|
|
236
|
+
text: 'Post caption text',
|
|
237
|
+
images: [
|
|
238
|
+
'https://cdn.example.com/image1.jpg',
|
|
239
|
+
'data:image/png;base64,...',
|
|
240
|
+
],
|
|
241
|
+
video: 'https://cdn.example.com/video.mp4',
|
|
242
|
+
context: {
|
|
243
|
+
useCase: 'social_post',
|
|
244
|
+
userId: 'user_123',
|
|
245
|
+
userReputation: 0.95, // 0-1 trust score
|
|
246
|
+
locale: 'en-US',
|
|
247
|
+
region: 'US',
|
|
248
|
+
language: 'en',
|
|
249
|
+
},
|
|
250
|
+
policyId: 'social-media',
|
|
251
|
+
metadata: { postId: 'post_456' },
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const response: MultiModalCheckResponse = {
|
|
255
|
+
decisionId: '...',
|
|
256
|
+
safe: false, // Overall: false if ANY item unsafe
|
|
257
|
+
flagged: true, // Overall: true if ANY item flagged
|
|
258
|
+
action: 'block', // Overall: most severe action
|
|
259
|
+
results: [
|
|
260
|
+
{
|
|
261
|
+
contentType: 'text',
|
|
262
|
+
safe: true,
|
|
263
|
+
flagged: false,
|
|
264
|
+
action: 'allow',
|
|
265
|
+
categories: [...],
|
|
266
|
+
provider: 'openai',
|
|
267
|
+
latency: 50,
|
|
268
|
+
cost: 0.00001,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
contentType: 'image',
|
|
272
|
+
contentRef: 'https://cdn.example.com/image1.jpg',
|
|
273
|
+
contentItemId: 'item-uuid', // For linking to evidence
|
|
274
|
+
safe: false,
|
|
275
|
+
flagged: true,
|
|
276
|
+
action: 'block',
|
|
277
|
+
categories: [...],
|
|
278
|
+
provider: 'hive',
|
|
279
|
+
latency: 200,
|
|
280
|
+
cost: 0.0001,
|
|
281
|
+
evidence: {
|
|
282
|
+
url: 'https://evidence.vettly.dev/...', // Signed URL
|
|
283
|
+
expiresAt: '2024-01-16T12:00:00Z',
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
],
|
|
287
|
+
totalLatency: 250,
|
|
288
|
+
totalCost: 0.00011,
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Providers
|
|
295
|
+
|
|
296
|
+
Supported moderation providers:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import type { ProviderName, ProviderResult } from '@vettly/shared'
|
|
300
|
+
|
|
301
|
+
// 'openai' | 'openai_gpt' | 'hive' | 'azure' | 'gemini_vision' | 'mock' | 'fallback'
|
|
302
|
+
const provider: ProviderName = 'openai'
|
|
303
|
+
|
|
304
|
+
const result: ProviderResult = {
|
|
305
|
+
provider: 'openai',
|
|
306
|
+
flagged: true,
|
|
307
|
+
categories: {
|
|
308
|
+
hate_speech: 0.91,
|
|
309
|
+
harassment: 0.08,
|
|
310
|
+
},
|
|
311
|
+
confidence: 0.95,
|
|
312
|
+
latency: 147,
|
|
313
|
+
cost: 0.000025,
|
|
314
|
+
raw: { /* original provider response */ },
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Webhook Events
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import type { WebhookEventType, WebhookEndpoint } from '@vettly/shared'
|
|
324
|
+
|
|
325
|
+
// Event types
|
|
326
|
+
const events: WebhookEventType[] = [
|
|
327
|
+
'decision.created', // Any decision made
|
|
328
|
+
'decision.flagged', // Content flagged for review
|
|
329
|
+
'decision.blocked', // Content blocked
|
|
330
|
+
'policy.created', // New policy created
|
|
331
|
+
'policy.updated', // Policy updated
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
const webhook: WebhookEndpoint = {
|
|
335
|
+
url: 'https://your-app.com/webhooks/vettly',
|
|
336
|
+
events: ['decision.blocked', 'decision.flagged'],
|
|
337
|
+
description: 'Production webhook for content moderation',
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
82
343
|
## Error Types
|
|
83
344
|
|
|
84
345
|
```typescript
|
|
@@ -89,15 +350,27 @@ import {
|
|
|
89
350
|
} from '@vettly/shared'
|
|
90
351
|
|
|
91
352
|
try {
|
|
92
|
-
await
|
|
353
|
+
await checkContent(...)
|
|
93
354
|
} catch (error) {
|
|
94
355
|
if (error instanceof PolicyValidationError) {
|
|
95
|
-
// Invalid policy configuration
|
|
356
|
+
// Invalid policy YAML or configuration
|
|
357
|
+
console.log('Policy error:', error.message)
|
|
358
|
+
console.log('Details:', error.details)
|
|
359
|
+
} else if (error instanceof ProviderError) {
|
|
360
|
+
// Moderation provider failed
|
|
361
|
+
console.log('Provider error:', error.message)
|
|
362
|
+
console.log('Provider:', error.details?.provider)
|
|
363
|
+
} else if (error instanceof ModerationError) {
|
|
364
|
+
// General moderation error
|
|
365
|
+
console.log(`${error.code}: ${error.message}`)
|
|
366
|
+
console.log('Status:', error.statusCode)
|
|
96
367
|
}
|
|
97
368
|
}
|
|
98
369
|
```
|
|
99
370
|
|
|
100
|
-
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Utility Functions
|
|
101
374
|
|
|
102
375
|
```typescript
|
|
103
376
|
import {
|
|
@@ -109,26 +382,112 @@ import {
|
|
|
109
382
|
formatLatency
|
|
110
383
|
} from '@vettly/shared'
|
|
111
384
|
|
|
112
|
-
// SHA256 content hashing for
|
|
385
|
+
// SHA256 content hashing for tamper-evident fingerprinting
|
|
113
386
|
const hash = hashContent('user content')
|
|
387
|
+
// '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
|
|
388
|
+
|
|
389
|
+
// Generate UUIDs for decisions
|
|
390
|
+
const id = generateUUID()
|
|
391
|
+
// '550e8400-e29b-41d4-a716-446655440000'
|
|
114
392
|
|
|
115
393
|
// Generate idempotency keys
|
|
116
394
|
const requestId = generateRequestId()
|
|
395
|
+
// 'req_1705320000000_x7k9m2'
|
|
396
|
+
|
|
397
|
+
// Calculate policy version from YAML
|
|
398
|
+
const version = calculatePolicyVersion(yamlContent)
|
|
399
|
+
// 'abc123def456...' (16-char hash)
|
|
117
400
|
|
|
118
401
|
// Format for display
|
|
119
402
|
formatCost(0.000123) // '$0.000123'
|
|
120
403
|
formatLatency(1250) // '1.25s'
|
|
404
|
+
formatLatency(150) // '150ms'
|
|
121
405
|
```
|
|
122
406
|
|
|
123
|
-
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Runtime Validation
|
|
410
|
+
|
|
411
|
+
All types have corresponding Zod schemas for runtime validation:
|
|
124
412
|
|
|
125
413
|
```typescript
|
|
126
|
-
import
|
|
414
|
+
import {
|
|
415
|
+
ActionSchema,
|
|
416
|
+
CategorySchema,
|
|
417
|
+
ContentTypeSchema,
|
|
418
|
+
PolicySchema,
|
|
419
|
+
CheckRequestSchema,
|
|
420
|
+
CheckResponseSchema,
|
|
421
|
+
DecisionSchema,
|
|
422
|
+
MultiModalCheckRequestSchema,
|
|
423
|
+
MultiModalCheckResponseSchema,
|
|
424
|
+
} from '@vettly/shared'
|
|
425
|
+
|
|
426
|
+
// Validate untrusted input
|
|
427
|
+
const result = CheckRequestSchema.safeParse(untrustedInput)
|
|
127
428
|
|
|
128
|
-
|
|
429
|
+
if (result.success) {
|
|
430
|
+
// result.data is fully typed as CheckRequest
|
|
431
|
+
processRequest(result.data)
|
|
432
|
+
} else {
|
|
433
|
+
// result.error contains validation errors
|
|
434
|
+
console.log('Validation failed:', result.error.issues)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Strict parsing (throws on failure)
|
|
438
|
+
try {
|
|
439
|
+
const request = CheckRequestSchema.parse(untrustedInput)
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error instanceof z.ZodError) {
|
|
442
|
+
console.log('Validation errors:', error.issues)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
129
445
|
```
|
|
130
446
|
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## JSON Types
|
|
450
|
+
|
|
451
|
+
For database storage and serialization:
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
import type { JsonValue, JsonObject, JsonArray, JsonPrimitive } from '@vettly/shared'
|
|
455
|
+
|
|
456
|
+
// JSON-serializable metadata
|
|
457
|
+
const metadata: JsonObject = {
|
|
458
|
+
userId: 'user_123',
|
|
459
|
+
tags: ['user-reported', 'priority'],
|
|
460
|
+
score: 0.95,
|
|
461
|
+
reviewed: false,
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Moderation Context
|
|
468
|
+
|
|
469
|
+
Additional context for smarter moderation:
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
import type { ModerationContext } from '@vettly/shared'
|
|
473
|
+
|
|
474
|
+
const context: ModerationContext = {
|
|
475
|
+
userId: 'user_123',
|
|
476
|
+
sessionId: 'session_456',
|
|
477
|
+
locale: 'en-US',
|
|
478
|
+
region: 'US',
|
|
479
|
+
metadata: {
|
|
480
|
+
deviceType: 'mobile',
|
|
481
|
+
appVersion: '2.1.0',
|
|
482
|
+
},
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
131
488
|
## Links
|
|
132
489
|
|
|
133
490
|
- [vettly.dev](https://vettly.dev) - Sign up
|
|
134
491
|
- [docs.vettly.dev](https://docs.vettly.dev) - Documentation
|
|
492
|
+
- [@vettly/sdk](https://www.npmjs.com/package/@vettly/sdk) - Server-side SDK
|
|
493
|
+
- [@vettly/react](https://www.npmjs.com/package/@vettly/react) - React components
|